Purrx

Embeddings and RAG from scratch

Turn text into vectors, find the closest one with cosine similarity, and build retrieval-augmented generation with no vector database at all.

13 min+40 XPHands-on

Models only know what was in their training data or what is in the prompt. You can't retrain the model, so the job becomes: find the right text and put it in the prompt. Doing that well is called (retrieval-augmented generation), and the "find" half runs on embeddings.

What an embedding is

An turns text into a list of numbers (a ), usually hundreds of numbers long. Text with similar meaning gets similar numbers.

"How do I reset my password?" and "I'm locked out of my account" share almost no words, but their vectors are close together. That's the whole trick, and it's why beats searching by keyword.

text in, vector out
const res = await gemini.embedContent({ contents: "How do I reset my password?" });
res.embedding.values;   // [0.021, -0.118, 0.093, … ] — a few hundred numbers
Cosine similarity between sentence vectorsTwo sentences about resetting a password point in nearly the same direction, giving a similarity near 0.9. An unrelated sentence about cats points far away, giving a similarity near 0.1."reset my password""locked out"small angle → 0.91"cats sleep 16 hours"wide angle → 0.08
Similar meaning points in a similar direction. Cosine similarity measures that angle, ignoring length.

Measuring closeness

scores how closely two vectors point in the same direction: 1.0 means the same direction (similar meaning), 0 means unrelated. It's a short function, and it's all the "vector search" you need until you have a lot of documents.

the whole algorithm
function cosineSimilarity(a, b) {
  let dot = 0, magA = 0, magB = 0;
  for (let i = 0; i < a.length; i++) {
    dot  += a[i] * b[i];
    magA += a[i] * a[i];
    magB += b[i] * b[i];
  }
  return dot / (Math.sqrt(magA) * Math.sqrt(magB));
}
Under the hood — What does "indexing" a pile of vectors actually do?

Comparing a query against every stored vector is an exact search: correct, and linear in the number of documents. At a few hundred chunks that is microseconds. At ten million it is far too slow to put in a request.

A vector database trades a little accuracy for speed using search. Rather than scanning everything, it builds a structure that lets it skip most of the collection. Two common ones: IVF clusters vectors into groups and searches only the nearest few groups; HNSW builds a layered graph you can descend through, taking large hops at the top and small ones near the bottom.

"Approximate" is the trade: these can miss a true nearest neighbor occasionally, in exchange for being orders of magnitude faster. That is almost always the right deal at scale — and exactly the wrong complexity to take on at four documents.

The RAG pipeline

Once, ahead of time

Your documents

Chunks

a few hundred words each

Embed each chunk

Stored vectors

an array is fine to start

For every question

Question

Embed it

Find closest chunks

cosine similarity

Prompt

chunks + question + “answer only from this”

Answer

traceable to a source

Prepare your documents once. Then for every question: embed it, find the closest chunks, and answer from them.
  1. Chunk: split your documents into passages of a few hundred words. Let neighbors overlap a little so an idea isn't cut in half.
  2. Embed every chunk once, and store the vectors.
  3. Embed the question when it arrives.
  4. Retrieve the top few chunks by similarity.
  5. Generate: put those chunks in the prompt and tell the model to answer only from them.
step 5 — where grounding happens
const prompt = `Answer using ONLY the context below.
If the answer isn't there, say "I don't know".

Context:
${topChunks.join("\n---\n")}

Question: ${question}`;

That instruction matters. Without it, the model mixes the retrieved facts with half-remembered training data, and you lose the main benefit of RAG: answers you can trace back to a source ().

Where RAG goes wrong

  • Usually, the search. When a RAG system answers badly, the passage it needed was almost always missing from the prompt. Log what you retrieved before you blame the model.
  • Chunks too big or too small. Too big, and the answer is buried in unrelated text. Too small, and the surrounding context that gave it meaning is cut off.
  • Mixed embedding models. The question and the documents must be embedded with the same model. Vectors from different models can't be compared.

Your exercise is the search half: the part that decides whether the whole system works.

Key takeaways

  • Embeddings turn text into lists of numbers where similar meanings end up close together.
  • RAG finds the most relevant passages and puts them in the prompt with “answer only from this”.
  • When RAG answers badly, check what was retrieved before blaming the model.

Sign in to run the exercise

Reading is free. Writing code here needs an account so we have somewhere to keep your Gemini key and the +40 XP you are about to earn.