Claude API Semantic Search App Tutorial
Claude API Semantic Search App Tutorial
A semantic search app lets users find relevant results by meaning instead of exact keyword matches. If you're searching "claude api semantic search app tutorial," you're likely trying to figure out how to combine vector embeddings with Claude to build a search experience over your own documents, support tickets, or product catalog — one that understands "how do I cancel my plan" matches a document titled "subscription management."
This tutorial walks through the full pipeline: generating embeddings, storing and querying vectors, then using Claude to rerank results and synthesize a direct answer from the retrieved content. It's a leaner version of a full RAG pipeline — focused specifically on search UX rather than conversational memory.
How semantic search with Claude actually works
Claude itself doesn't generate embeddings. The architecture has two separate jobs:
- Embedding model — converts text into vectors so you can do similarity search (cosine distance, dot product, etc.). You'll use a dedicated embeddings API or open-source model for this part.
- Claude — takes the top-ranked chunks returned by your vector search and uses them to rerank, filter, or write a natural-language answer with citations.
Trying to use Claude alone as a vector database doesn't work — it has no persistent index of your documents between requests. You need:
- A document store (your raw text/files)
- An embeddings model (OpenAI, Voyage, Cohere, or a local model) to vectorize chunks
- A vector database (pgvector, Pinecone, Qdrant, or even an in-memory array for small datasets)
- Claude to handle the "understanding" layer: reranking ambiguous matches and generating a readable answer
Step 1: Chunk and embed your content
Split documents into chunks of 200–500 tokens with some overlap, then embed each chunk.
import { OpenAI } from "openai";
const openai = new OpenAI();
async function embedChunks(chunks) {
const response = await openai.embeddings.create({
model: "text-embedding-3-small",
input: chunks,
});
return response.data.map((d) => d.embedding);
}
Store each chunk alongside its vector, source document id, and raw text in your vector database.
Step 2: Run the similarity search
At query time, embed the user's search query the same way, then query your vector store for the top N nearest chunks.
async function searchVectors(query, topK = 8) {
const [queryVector] = await embedChunks([query]);
return vectorDb.query({
vector: queryVector,
topK,
includeMetadata: true,
});
}
This gives you candidates ranked by cosine similarity — but similarity scores alone are often noisy. A chunk can be lexically close without being the actually useful answer. That's where Claude comes in.
Step 3: Use Claude to rerank and synthesize
Pass the top candidate chunks to Claude along with the original query and ask it to pick the most relevant ones and write a concise answer with source references. This is the step where SubToAPI is useful: you call Claude through a standard HTTPS endpoint using your existing Claude access, with an application key you control instead of juggling raw provider credentials across services.
async function rerankAndAnswer(query, candidates) {
const context = candidates
.map((c, i) => `[${i}] ${c.metadata.text}`)
.join("\n\n");
const response = await fetch("https://api.subtoapi.app/v1/messages", {
method: "POST",
headers: {
"Authorization": `Bearer ${process.env.SUBTOAPI_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
model: "claude-sonnet-4-5",
max_tokens: 600,
messages: [
{
role: "user",
content: `Query: "${query}"\n\nCandidate passages:\n${context}\n\nRank the passages by relevance to the query, discard irrelevant ones, and write a short answer citing passage numbers like [0], [2].`,
},
],
}),
});
const data = await response.json();
return data.content[0].text;
}
This combination — vector search for recall, Claude for precision — consistently outperforms pure similarity search on ambiguous or multi-concept queries, because Claude can reason about why a passage matches rather than just how close its vector is.
Step 4: Stream the answer for better UX
Search UIs feel much faster when the synthesized answer streams token by token instead of waiting for the full response. If you're using SubToAPI, set "stream": true and read the response as server-sent events — see /docs/streaming for the event format and a full client example. Streaming matters more here than in typical chat apps because users are scanning for relevance signals in real time; showing the first sentence quickly reduces perceived latency even if the full citation list takes another second.
Step 5: Add structured output for citations
If your frontend needs clean, parseable citations instead of free text, use Claude's tool-calling support to force a structured response:
{
"name": "search_answer",
"input_schema": {
"type": "object",
"properties": {
"answer": { "type": "string" },
"sources": {
"type": "array",
"items": { "type": "integer" }
}
},
"required": ["answer", "sources"]
}
}
This lets you render highlighted source cards next to the answer instead of parsing markdown links out of free-form text. Tool use is documented at /docs/tools if you want to see the full request/response shape.
Handling scale and cost
A few practical notes once you move past a prototype:
- Cache embeddings. Re-embedding unchanged documents on every deploy wastes money and time — only re-embed chunks whose source text changed.
- Limit candidates sent to Claude. Sending 50 chunks to Claude for reranking is slow and expensive; 5–10 well-chosen candidates from your vector search are usually enough.
- Track usage per feature. If search is one of several Claude-powered features in your product, keeping it on a separate API key makes cost attribution straightforward. SubToAPI's dashboard shows usage per key, which is handy when search is sharing a Claude account with a chatbot or internal tool.
- Set a sensible max_tokens. Search answers should be short — capping output length keeps latency low and avoids rambling responses.
Getting started quickly
If you already have Claude access through a Pro or Team plan and want to wire it into a search app without managing provider-specific SDKs or key rotation, SubToAPI turns that access into a standard HTTPS API with an application key (sub_live_...). Start with /docs/quickstart for setup, and /docs/messages for the full request reference. Plans start at €9/month on the Solo tier, with Team and Scale tiers for multi-seat setups — see /pricing for details, and /signup includes a free trial.
questions
Do I need a separate embeddings provider, or can Claude generate embeddings? Claude is a chat/completion model, not an embeddings model. You need a dedicated embeddings API (OpenAI, Voyage, Cohere) or a local model to generate the vectors used for similarity search; Claude's role is reranking and answer synthesis afterward.
How many candidate chunks should I send to Claude for reranking? Start with 5–10 chunks from your vector search. Sending more increases cost and latency without meaningfully improving accuracy if your initial retrieval is reasonably tuned.
Can I stream the final answer to the frontend? Yes. Set stream: true in your Claude request and consume the server-sent events as they arrive — this gives a much snappier search UX. Full details are in /docs/streaming.