Skip to content
AI360XpertBeta

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.

Cosine picks the long on-target vector b over the nearer off-target vector a, while Euclidean distance picks a over b, so the two metrics rank the same query in opposite orders
Cosine picks the long on-target vector b over the nearer off-target vector a, while Euclidean distance picks a over b, so the two metrics rank the same query in opposite orders

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 q=[1,0]q = [1, 0] with candidates a=[0.8,0.6]a = [0.8, 0.6] and b=[2,0]b = [2, 0]. Cosine picks bb, scoring 1.01.0 against 0.80.8. Euclidean distance picks aa, scoring about 0.630.63 against 1.01.0. 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 dd numbers, where dd is the dimension count. Write aa and bb for two stored vectors, qq for your query vector, aia_i for the ii-th entry of aa, ∥a∥\lVert a \rVert for its length (the root sum of squares, the quantity norms measures), and θ\theta 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:

cos⁡θ=a⋅b∥a∥ ∥b∥\cos\theta = \frac{a \cdot b}{\lVert a \rVert \, \lVert b \rVert}

In plain words: take the dot product a⋅ba \cdot b, then divide out both lengths. The score runs from −1-1 (opposite) through 00 (right angle) to 11 (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:

d(a,b)=∥a−b∥=∑i(ai−bi)2d(a, b) = \lVert a - b \rVert = \sqrt{\textstyle\sum_i (a_i - b_i)^2}

In plain words: subtract entry by entry, square, add, take the root. Scores run from 00 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:

a⋅b=∑iaibi=∥a∥ ∥b∥cos⁡θa \cdot b = \textstyle\sum_i a_i b_i = \lVert a \rVert \, \lVert b \rVert \cos\theta

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 (∥a∥=∥b∥=1\lVert a \rVert = \lVert b \rVert = 1), the dot product equals the cosine, and squared Euclidean distance equals 2−2cos⁡θ2 - 2\cos\theta. 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 q=[1,0]q = [1, 0], candidate a=[0.8,0.6]a = [0.8, 0.6], candidate b=[2,0]b = [2, 0], the same trio drawn above.

Lengths first. ∥q∥=1\lVert q \rVert = 1. ∥a∥=0.82+0.62=0.64+0.36=1=1\lVert a \rVert = \sqrt{0.8^2 + 0.6^2} = \sqrt{0.64 + 0.36} = \sqrt{1} = 1. ∥b∥=2\lVert b \rVert = 2. So aa already has unit length and bb is twice as long.

Raw dots next. q⋅a=1×0.8+0×0.6=0.8q \cdot a = 1 \times 0.8 + 0 \times 0.6 = 0.8. q⋅b=1×2+0×0=2.0q \cdot b = 1 \times 2 + 0 \times 0 = 2.0.

Cosines divide out the lengths. cos⁡(q,a)=0.8/(1×1)=0.8\cos(q, a) = 0.8 / (1 \times 1) = 0.8, an angle of about 36.936.9 degrees. cos⁡(q,b)=2.0/(1×2)=1.0\cos(q, b) = 2.0 / (1 \times 2) = 1.0, an angle of zero. Cosine picks bb.

Euclidean measures the gaps. d(q,a)=(1−0.8)2+(0−0.6)2=0.04+0.36=0.4≈0.6325d(q, a) = \sqrt{(1 - 0.8)^2 + (0 - 0.6)^2} = \sqrt{0.04 + 0.36} = \sqrt{0.4} \approx 0.6325. d(q,b)=(1−2)2+02=1.0d(q, b) = \sqrt{(1 - 2)^2 + 0^2} = 1.0. Euclidean picks aa.

Dot product keeps the inflation. 0.80.8 against 2.02.0, so dot picks bb by a landslide that comes almost entirely from bb being long. Shrink bb to unit length, b^=b/∥b∥=[1,0]\hat{b} = b / \lVert b \rVert = [1, 0], and q⋅b^=1.0q \cdot \hat{b} = 1.0, 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, a^=a/∥a∥\hat{a} = a / \lVert a \rVert, 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.0

Cosine ranks bb first (1.01.0 over 0.80.8) while Euclidean ranks aa first (0.63250.6325 over 1.01.0). The raw dot of 2.02.0 looks decisive until you see it is mostly length. After normalization the dot reads 1.01.0, 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 [2,0][2, 0] beats [0.8,0.6][0.8, 0.6] for query [1,0][1, 0] with 1.01.0 against 0.80.8.
  • Euclidean distance scores the gap including length, so [0.8,0.6][0.8, 0.6] beats [2,0][2, 0] for the same query with about 0.630.63 against 1.01.0.
  • 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 2−2cos⁡θ2 - 2\cos\theta.
  • 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.