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.
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.
const res = await gemini.embedContent({ contents: "How do I reset my password?" });
res.embedding.values; // [0.021, -0.118, 0.093, … ] — a few hundred numbersMeasuring 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.
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
- Chunk: split your documents into passages of a few hundred words. Let neighbors overlap a little so an idea isn't cut in half.
- Embed every chunk once, and store the vectors.
- Embed the question when it arrives.
- Retrieve the top few chunks by similarity.
- Generate: put those chunks in the prompt and tell the model to answer only from them.
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.