5 · Cosine similarity
How do you measure 'nearby' for embeddings? Cosine similarity compares direction, not magnitude — the default metric for text vectors, and equal to the dot product once vectors are normalized.
Cosine similarity measures the angle between two vectors: it's the dot product divided by both lengths, ranging from -1 (opposite) to 1 (identical direction). For text it beats Euclidean distance because it ignores magnitude — only the direction (meaning) matters. On L2-normalized vectors, cosine similarity equals the plain dot product.
Without this:
Pick the wrong metric and a long document looks 'far' from a short query about the same topic purely because it has more words — your search ranks by length instead of meaning.
Once text is a vector, "similar" needs a precise definition. Two candidates:
-
Euclidean distance
||a - b||— straight-line distance between the two points. Small distance = similar. But it's sensitive to magnitude: a vector twice as long is "far" even if it points the exact same way. -
Cosine similarity
cos(a, b) = (a · b) / (||a|| · ||b||)— the cosine of the angle between the vectors. It ranges from +1 (same direction), through 0 (perpendicular / unrelated), to -1 (opposite). It is invariant to magnitude: scaling a vector doesn't change its direction, so it doesn't change the score.
For text, direction encodes meaning and magnitude mostly encodes length/emphasis — so cosine is the standard choice. A 3-word query and a 300-word document about the same topic point the same way; cosine sees them as similar, while Euclidean distance would penalize the document just for being long.
The neat trick: if you L2-normalize every vector first (set ||a|| = 1), then cos(a, b) = a · b — the denominator is just 1 × 1. That's why production vector databases normalize once on insert and then use the cheap dot product for every query.
Python (in browser)
cosine_similarity ranks the pet documents above sports/finance/weather, is unchanged by scaling a vector 5×, and — on normalized vectors — equals the plain dot product.
Python runs entirely in your browser via Pyodide (~6 MB on first Run, cached after).
One subtlety: cosine similarity and Euclidean distance agree on the ranking once vectors are normalized — for unit vectors, ||a - b||² = 2 - 2·cos(a, b), so smaller distance ⇔ larger cosine. The difference matters most on un-normalized vectors, where Euclidean is swayed by length. This is why almost every vector store either normalizes for you or exposes cosine as the default metric for text.
Real embeddings, same metric: normalize, then cosine (= dot product). The from-scratch NumPy version you ran is exactly what `util.cos_sim` computes.
Why is cosine similarity usually preferred over Euclidean distance for comparing text embeddings?
- Cosine similarity = (a·b)/(‖a‖‖b‖), the cosine of the angle between vectors, ranging from -1 to 1.
- Cosine ignores magnitude (compares direction = meaning), which is why it beats Euclidean distance for text.
- On L2-normalized vectors, cosine equals the dot product — so production stores normalize once and rank with one `vectors @ query`.
Every vector database (Pinecone, Weaviate, pgvector, FAISS) exposes cosine as the default text metric; semantic search and RAG retrieval are cosine ranking under the hood.
If you remove it: Use raw Euclidean distance on un-normalized embeddings and your search ranks partly by document length, surfacing long irrelevant chunks over short relevant ones.