Text embeddings · Build a tiny semantic search5 / 5

Build a tiny semantic search

Example on GitHub(packages/sdk/examples/embed-p2p.ts)

Now that we can compare two vectors, we're ready to build the smallest useful search engine.

A semantic search is just three steps: embed a small corpus once, embed the query separately, then loop through the corpus picking the highest score. The corpus doesn't have to be perfect. Three documents are enough to see the pattern.

Embedding the whole corpus in a single batch call would look like this:

const { embedding: corpusVectors } = await embed({ modelId, text: corpus });

The query goes through the same embed() API with a single string, destructured into a one-element array. Note that the [queryEmbedding] array-destructure follows the same pattern as the corpus destructure, just with a single element:

const { embedding: [queryEmbedding] } = await embed({ modelId, text: query });

With the corpus and query both embedded, the loop does the work: it scores every corpus vector against the query, tracks the highest score, and remembers its index. The whole thing runs in O(N·d), N cosine calls each touching d dimensions, and that's the brute-force approach ragSearch generalizes to a vector index for O(log N):

let bestIdx = 0;
let bestScore = -Infinity;
for (let i = 0; i < corpusVectors.length; i++) {
  const score = cosineSimilarity(queryEmbedding, corpusVectors[i]!);
  if (score > bestScore) {
    bestScore = score;
    bestIdx = i;
  }
}
console.log(`Query: ${query}`);
console.log(`Best match: ${titles[bestIdx]} (score ${bestScore.toFixed(4)})`);

After the loop, bestIdx holds the position of the highest-scoring corpus vector, and titles[bestIdx] is the document title closest in meaning to the query.

Note: the same pattern scales to thousands of documents. The only thing that changes is where the vectors live. In memory for a few hundred, on disk for the rest. Chapter 4 shows the on-disk version.

Questions

Question 1 of 3

What's the difference between how the corpus and the query are embedded?

Question 2 of 3

Does starting bestScore at 0 instead of -Infinity change the search result?

Question 3 of 3

As the corpus grows from three documents to a few hundred, what does this loop keep doing?

index.ts
Loading editor...

Run your code, check your answer, or ask a question. It all shows up here.