Area 02 // RAGLesson 33 / 14015 min read
Embeddings and cosine similarity
An embedding model turns a piece of text into a vector of numbers that captures its meaning, so texts that mean similar things end up close together, and cosine similarity measures how close two of them are; this is how RAG finds relevant chunks even when the words differ.
Think of it like this
Analogy
Imagine a giant library where books are shelved not by title or author but by what they're about. Cookbooks about Italian food sit next to each other; a book on pasta is right beside one on pizza, even if their titles share no words. To find books like yours, you walk to where your book would be shelved and look at the neighbours.
An embedding is a book's shelf position (as coordinates). Cosine similarity measures how close two positions are.
The problem it solves
Keyword search fails when people use different words from your documents. A customer asks "how do I get my money back?" and your policy says "refunds are processed within 5 days". No overlapping words, so keyword search finds nothing, and the bot says "I don't know" even though the answer exists.
You need search that understands meaning. Embeddings provide it, and they're the heart of most RAG systems.
How it works: embeddings
- An embedding model is a neural network (usually an encoder transformer) trained so that texts with similar meanings produce similar vectors. It reads the whole text with attention and outputs one vector.
- Every input becomes the same length: a vector with a fixed dimension, for example 384, 1,024, 1,536 or 3,072 numbers, whether the input is three words or three paragraphs (up to the model's input limit).
- Meaning becomes geometry. "Refund", "money back" and "reimbursement" land close together; "breakfast" lands far away.
- Embed your chunks once during ingestion and store the vectors.
- Embed each query with the same model at search time.
How it works: cosine similarity
- Cosine similarity measures the angle between two vectors:
cos = (a · b) / (|a| × |b|)(vectors refresher). - Range −1 to 1. For text embeddings, scores are usually between 0 and 1; higher means more similar.
- Normalise once, then use the dot product. If all vectors have length 1, cosine similarity equals the dot product, which databases compute very fast. Many embedding APIs return normalised vectors already.
- Rank by similarity and take the top k chunks. That's semantic search.
- Scores are relative, not absolute. A "0.78" means nothing on its own; compare candidates, and tune any threshold on your own data.
How it works: choosing an embedding model
- Shortlist a few models (as of November 2026, for example OpenAI
text-embedding-3-smalland-large, Googlegemini-embedding-001, and open models such as the BGE, E5, Nomic, mxbai and Qwen3-Embedding families). Public leaderboards like MTEB help narrow the list. - Check fit: languages you need, maximum input length, dimensions (storage cost), price per million tokens, speed, and whether data may go to an API or must stay local.
- Test on your data: 50–200 real questions with their correct chunks; measure Recall@k for each model.
- Consider dimension reduction: several models let you keep fewer dimensions (e.g. 256 instead of 1,536) with a small quality loss, saving storage and speed.
- Commit and version it: store the model name with every vector; switching later means re-embedding everything.
| Option (as of Nov 2026) | Hosting | Notes |
|---|---|---|
OpenAI text-embedding-3-small | API | $0.02 per 1M tokens; 1,536 dims (reducible) |
OpenAI text-embedding-3-large | API | $0.13 per 1M tokens; 3,072 dims (reducible) |
Google gemini-embedding-001 | API | $0.15 per 1M tokens; free tier |
| Open models (BGE, E5, Nomic, Qwen3-Embedding…) | Self-host or Ollama | No per-token cost; you run them |
See it in code
- Install
pip install numpy- Needs
- Python 3.10+. The offline part uses hand-made vectors so you can see why meaning beats keywords; the API version needs OPENAI_API_KEY.
Four "embeddings" with readable dimensions, to show that the best match shares no words with the question.
import numpy as np
# Pretend embedding model output (real ones have 384-3072 numbers, learned from data).
# Dimensions here mean roughly: [money, time, travel, food]
EMB = {
"How do I get my money back?": np.array([0.9, 0.1, 0.0, 0.0]),
"Refunds are paid within 5 days.": np.array([0.8, 0.5, 0.0, 0.0]),
"Delivery takes 3 working days.": np.array([0.1, 0.8, 0.6, 0.0]),
"Our cafe serves vegan breakfast.": np.array([0.1, 0.0, 0.0, 0.9]),
}
def cosine(a, b):
return float(a @ b / (np.linalg.norm(a) * np.linalg.norm(b)))
query = "How do I get my money back?"
ranked = sorted(((cosine(EMB[query], v), text) for text, v in EMB.items() if text != query), reverse=True)
for score, text in ranked:
print(f"{score:.2f} {text}")
print("Note: no words overlap between the question and the best match ('money back' vs 'Refunds').")0.90 Refunds are paid within 5 days.
0.19 Delivery takes 3 working days.
0.11 Our cafe serves vegan breakfast.
Note: no words overlap between the question and the best match ('money back' vs 'Refunds').With a real embedding model the vectors come from the API (or a local model via Ollama):
from openai import OpenAI
import ollama
texts = ["How do I get my money back?", "Refunds are paid within 5 days."]
r = OpenAI().embeddings.create(model="text-embedding-3-small", input=texts)
vectors = [d.embedding for d in r.data] # two lists of 1,536 floats
local = ollama.embed(model="nomic-embed-text", input=texts) # free, runs on your machine
print(len(vectors[0]), len(local.embeddings[0]))A real example
Example
A help centre that finally understands customers' words
A bank's help-centre search used keywords. Customers typed "card got eaten by the machine" and got nothing; the article was titled "Retained card at an ATM". After switching to embeddings, that query found the right article first. They then tested three embedding models on 400 real search queries: the mid-priced API model and a self-hosted open model both reached about 90% Recall@5; they chose the open model because the bank preferred to keep queries in-house.
They also noticed embeddings failed on exact error codes ("error 2306"), which led them to add keyword search alongside (hybrid search).
The easy way
Start here
Pick one well-known embedding model (an API model, or nomic-embed-text via Ollama for free), embed your chunks, and test with 30 real questions: does the right chunk appear in the top 5? That tells you more than any leaderboard.
When you grow: the production setup
Trade-offs
What you gain
- Finds relevant text by meaning, even with completely different words.
- Works across languages with multilingual models.
- Fast at scale: a vector comparison is cheap.
What it costs you
- Weak on exact identifiers, codes, names and numbers.
- Similar isn't always relevant ("cancel my order" vs "how to place an order").
- Changing model means re-embedding everything.
When not to use it
- Users mostly search exact IDs, SKUs or error codes: keyword search (or hybrid) works better.
- Data is structured: query it with SQL instead of embedding rows as text.
Common mistakes
- Embedding documents and queries with different models.
- Choosing a model only from a leaderboard, without testing on your own questions.
- Treating a similarity score as an absolute "confidence" threshold without tuning it.
- Embedding huge chunks that mix many topics, which blurs their meaning.
- Forgetting to store which model produced each vector.
Interview questions
Try answering out loud first, then open the model answer.
What are embeddings and why do they work for search?
Model answer
An embedding model maps text to a dense vector such that semantically similar texts are close, typically trained contrastively on pairs of related texts (questions and answers, duplicates) so related pairs are pulled together and unrelated pushed apart. Search embeds the query and finds nearest neighbours by cosine similarity, retrieving relevant passages even without word overlap.
How would you choose an embedding model for a RAG system?
Model answer
Define requirements: languages, domain, input length, latency, cost, privacy (API vs self-hosted) and storage (dimension). Shortlist from public benchmarks, then evaluate on a labelled set of real queries and relevant chunks using Recall@k and MRR. Consider dimension reduction and quantised vectors for cost. Pick the best quality-per-cost option, version it, and plan re-embedding for future upgrades.
Why use cosine similarity rather than Euclidean distance?
Model answer
Cosine compares direction, ignoring vector magnitude, which for text embeddings mostly carries information unrelated to topic. For normalised vectors cosine similarity and Euclidean distance give the same ranking, and the dot product is cheapest to compute, so most systems normalise and use the dot product.
Quiz
Pick an answer to see whether it’s right and why.
0 of 6 answered
Key terms
- Embedding
- A vector of numbers that represents the meaning of a piece of text (or an image), so that similar meanings get similar vectors.
- Embedding model
- A model that turns a text (or image) into a fixed-length vector so that similar meanings get similar vectors.
- Cosine similarity
- A score from -1 to 1 for how closely two vectors point in the same direction, ignoring their length. The standard way to compare embeddings.
- Dimension
- The number of values in a vector. An embedding with dimension 1,536 is a list of 1,536 numbers.
- Semantic search
- Search by meaning rather than exact words, usually by comparing embeddings.
Before and after
Progress is saved in this browser only.