Similarity Metrics
Cosine similarity compares which way two embeddings point and ignores how long they are. That single choice decides whether your search ranks by meaning or by an accident of vector length.
Why Does This Exist?
Every vector search has to answer one question: which stored vector sits closest to this query? Embeddings turn your text into lists of numbers, but they don't say how to compare two lists. You choose the ruler once, usually in a vector database dropdown at index creation, and then live with it across millions of comparisons. It feels like a minor setting. It decides the ranking.
Here is the uncomfortable part. The rulers genuinely disagree. Take query with candidates and . Cosine picks , scoring against . Euclidean distance picks , scoring about against . Same vectors, opposite winners. Pick the wrong ruler and nothing errors. You get a plausible list, slightly worse, ranked partly by an accident of vector length. That failure is quiet, which is why this page exists.
This page covers the ruler choice and nothing else. What vectors are lives in embeddings. How you avoid scoring every stored vector lives in approximate nearest neighbor search. How you mix vector scores with exact word matching lives in hybrid search.
The one idea you need first: an embedding is a list of numbers, hundreds of them, where direction carries meaning and length often does not. Two passages about the same idea point a similar way even when one is a sentence and the other is a chapter. That is why a metric that ignores length is the default for search, and why the rest of this page keeps asking what each metric does with length.
Think of It Like This
Compass bearings versus footsteps
You stand at a trailhead at dusk with a compass. Your map says head due east. Two signs stand in the field. Sign A sits one block east and a little north, a short walk with a slight bearing error. Sign B sits five blocks due east, a long walk on a perfect bearing.
If you ask which sign matches your heading, B wins. The needle agrees exactly. If you ask which sign is the shorter walk, A wins by a lot. Same signs, opposite answers, and both answers are correct for the question asked. Cosine asks the compass question. Euclidean asks the footsteps question. Dot product asks a stranger rule: it multiplies bearing match by distance walked, so a far sign with a decent bearing can outscore a near sign with a perfect one.
The trail stops here. Embeddings point in hundreds of directions at once, not four, and no compass shows that. Training decides what length means, not terrain. And the production trick of shrinking every walk to one pace has no trail counterpart. Use the bearing picture to feel the difference, then trust the formulas for the rest.
How It Actually Works
The vectors below are embedding vectors: ordered lists of numbers, where is the dimension count. Write and for two stored vectors, for your query vector, for the -th entry of , for its length (the root sum of squares, the quantity norms measures), and for the angle between two vectors. All three rulers read the same vectors and ask different questions.
The three rulers
Cosine similarity asks how well two vectors agree in direction, ignoring length entirely:
In plain words: take the dot product , then divide out both lengths. The score runs from (opposite) through (right angle) to (identical direction). The full glossary form is cosine similarity. Long or short, a vector pointing your way scores the same.
Euclidean distance asks how far apart two endpoints sit, counting both direction and length:
In plain words: subtract entry by entry, square, add, take the root. Scores run from upward, and smaller means closer. The glossary form is Euclidean distance. A long vector pointing your way can still sit far from you.
Dot product skips the division and mixes direction with both lengths at once:
In plain words: multiply matching entries and add. Scores are unbounded, larger means closer, and length inflates everything. It is the cheapest to compute, which is why indexes love it and why you should check what it is rewarding.
One identity ties them together. When both vectors already have unit length (), the dot product equals the cosine, and squared Euclidean distance equals . So on unit vectors, cosine and Euclidean rank identically: same order, two dialects. That fact is the whole basis for the normalization shortcut below.
The trade-off is direct. Cosine assumes length carries no reliable meaning, which holds for most text embeddings and fails when it does not. Don't use cosine when length is the signal: term count vectors, some image and audio embeddings, or any model trained with a Euclidean or dot product objective. Match the ruler to what the embedding model was trained to make meaningful, not to which dropdown entry looked fastest.
Worked example: one query, opposite winners
Keep query , candidate , candidate , the same trio drawn above.
Lengths first. . . . So already has unit length and is twice as long.
Raw dots next. . .
Cosines divide out the lengths. , an angle of about degrees. , an angle of zero. Cosine picks .
Euclidean measures the gaps. . . Euclidean picks .
Dot product keeps the inflation. against , so dot picks by a landslide that comes almost entirely from being long. Shrink to unit length, , and , exactly its cosine. That collapse is the production trick.
Why production stores unit vectors
Divide every vector by its own length before you store it, , and the cheap dot product becomes the cosine with no division left at query time. You get cosine correctness at dot product speed, which matters when a query fans out across millions of candidates. Most vector stores offer this as a normalize on write option, and most embedding pipelines take it.
It costs one thing: you throw length away on purpose. When length is noise, that is the point. When length carries confidence, popularity, or energy your model put there deliberately, normalizing deletes signal you wanted. If your model docs name a distance, use that distance. If they ship normalized embeddings, keep them normalized through quantization and reindexing instead of letting a later step silently stretch them again.
Code
The point of this snippet is the flip, not the formula. It scores the same trio three ways and then shows normalization collapsing dot into cosine.
import numpy as np
def cosine(u: np.ndarray, v: np.ndarray) -> float: return float(u @ v / (np.linalg.norm(u) * np.linalg.norm(v)))
q = np.array([1.0, 0.0])a = np.array([0.8, 0.6])b = np.array([2.0, 0.0])
print(round(cosine(q, a), 4)) # -> 0.8print(round(cosine(q, b), 4)) # -> 1.0print(round(float(np.linalg.norm(q - a)), 4)) # -> 0.6325print(round(float(np.linalg.norm(q - b)), 4)) # -> 1.0print(round(float(q @ a), 4)) # -> 0.8print(round(float(q @ b), 4)) # -> 2.0
b_unit = b / np.linalg.norm(b)print(round(float(q @ b_unit), 4)) # -> 1.0Cosine ranks first ( over ) while Euclidean ranks first ( over ). The raw dot of looks decisive until you see it is mostly length. After normalization the dot reads , matching cosine exactly.
Watch Out For
Taking the inner product default on unnormalized vectors
Some stores default to inner product because it is the cheapest operation per comparison. On unnormalized embeddings that choice ranks partly by length: longer documents and certain content types float up regardless of relevance, and the list still looks plausible.
You will not get an error. You will get a search demo that feels fine and a production tail that never quite satisfies. Check lengths before you blame the model: if np.linalg.norm varies widely across your store, either switch the metric to cosine or turn on normalize on write and rebuild. Don't mix: an index built unnormalized and queried as if it were normalized misranks everything.
Building the index with one metric and querying with another
The metric is fixed when the index is built, especially for graph and cluster indexes that placed every vector using that distance. Flip the dropdown at query time from Euclidean to cosine without rebuilding, and the graph neighborhoods no longer mean what the query assumes.
Symptom: recall falls off a cliff on exactly the queries that used to work, while brute force on the same vectors looks fine. Fix: lock the metric once to match the embedding model docs, rebuild the index to change it, and keep one integration check that scores a known query trio both brute force and through the index.
The Quick Version
- Cosine similarity scores direction only, so beats for query with against .
- Euclidean distance scores the gap including length, so beats for the same query with about against .
- Dot product mixes direction with length, so a long off-target vector can outscore a short exact one.
- On unit length vectors the cheap dot product equals cosine, and Euclidean gives the same ranking through .
- Normalize to unit length at write time when embedding length is noise, and match the metric to the embedding model docs.
- A metric mismatch between index build and query time breaks retrieval silently, so lock it once and rebuild to change it.