← Blog

Claude API Semantic Search App Tutorial

2026-10-21 · 5 min read · SubToAPI Team

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:

  1. 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.
  2. 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:

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:

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.

Turn your Claude access into an HTTPS API

SubToAPI gives you application API keys, streaming, tool use and usage insights on top of your existing Claude access — set up in minutes.

Start free  Read the quickstart →