Distance and Similarity Metrics
Several defensible ways to measure how alike two things are, which disagree with each other — so picking one is a modelling decision, not a formatting choice.
Why Does This Exist?
Every vector database setup asks you to pick a metric from a dropdown, offers no explanation, and then never mentions it again. That dropdown decides your search quality, and the three options do not merely differ in scale — they can rank the same candidates in opposite orders.
The demonstration is in the diagram. Take and : cosine similarity says they are identical, and Euclidean distance says they are apart. Now take and : Euclidean says , closer than the first pair, while cosine says — completely unrelated. The two metrics have inverted the ranking.
This is the page behind the most common retrieval bug there is, and behind a subtler one: k-nearest neighbours and k-means both silently assume Euclidean, so feeding them unscaled features means the largest-range feature decides every neighbour.
Think of It Like This
Two ways to say two people are similar
Two people describe how much they like ten films, scoring each out of ten.
One is generous and rates everything 8 to 10. The other is harsh and rates the same films 2 to 4. Ask "are their tastes similar?" and you get two defensible answers.
By distance, they could not be further apart — every single score differs by six points. By direction, they are nearly identical: both rank the same films highest and lowest, and one is simply the other shifted down. Their pattern of preferences agrees completely.
Which answer is right depends on what you are building. A recommender wants direction, because it cares about relative preference and not about whether someone is a generous rater. A quality-control system wants distance, because the absolute values are the thing being checked.
That is the whole content of metric choice, and it is why the dropdown matters. You are declaring what "similar" means for your problem. The analogy stops where magnitude carries meaning: sometimes the generous rater really does like films more, and direction-only comparison throws that away.
How It Actually Works
Four measures cover nearly all practical use.
Euclidean (L2) distance is straight-line separation:
In plain words: subtract, square, sum, root. It is sensitive to magnitude, and it is the default assumption of k-NN, k-means, and most clustering.
Cosine similarity is the angle only:
In plain words: the dot product divided by both lengths, so length cancels. It is bounded in and completely blind to length. It is the default for embeddings. Note "cosine distance" usually means , which is not a true metric — it violates the triangle inequality — so it cannot be used in algorithms that require one.
Dot product is , unnormalised. It mixes direction and both magnitudes, which is the trap: a long irrelevant vector beats a short relevant one. It is fast, and correct only when your vectors are already unit length. Lengths themselves are measured with norms.
Manhattan (L1) distance is . It does not square, so one large discrepancy punishes less than Euclidean. It is sometimes preferred in high dimensions for that reason.
The identity that resolves most of it
For unit-length vectors, Euclidean distance and cosine similarity give the same ranking:
In plain words: once both vectors have length , squared distance is a decreasing function of cosine similarity, so sorting by one sorts by the other. Expand to see it. This is why normalising at ingestion is such a clean move: normalise once, then the cheap dot product is cosine similarity, and Euclidean agrees with both.
Choosing
- Embeddings from a neural model — cosine, or dot product on pre-normalised vectors. Magnitude is a training artifact, not meaning.
- Physical measurements in real units — Euclidean, after standardising features so no unit dominates.
- Sparse count vectors — cosine. Document length would otherwise dominate.
- Recommender inner products — dot product deliberately, because in matrix factorisation the magnitude encodes popularity and you may want that.
- Sets rather than vectors — Jaccard, the size of the intersection over the union. Different family, and the right answer for tags or shingles.
High dimensions change the picture
In very high dimensions distances concentrate: the ratio between the nearest and farthest neighbour of a random point approaches , so "nearest" becomes weakly meaningful. This is one face of the curse of dimensionality, and it hits Euclidean harder than cosine.
Real embeddings are less affected than random vectors, because they occupy a low-dimensional manifold rather than filling the space. But the effect is why cosine dominates for high-dimensional embeddings, and why approximate nearest-neighbour indexes can afford to be approximate.
Worked example
Take and , so — exactly the same direction.
Hand-computed: and . Dot product: .
Perfect similarity, since they are parallel. But Euclidean distance:
Very far apart. Same pair, opposite verdicts.
Now invert it with and . Euclidean distance is — much closer than the first pair. Cosine similarity is — orthogonal, no similarity at all.
So by Euclidean, at is far closer than at . By cosine, at beats at . The ranking has completely reversed, from four vectors and no ambiguity.
Finally, normalise and watch the disagreement vanish. , so their distance becomes and their cosine stays . Both metrics now agree they are identical.
Code
The snippet reproduces every number above, then shows the k-NN failure that unscaled features cause.
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)))
a, b = np.array([1.0, 1.0]), np.array([10.0, 10.0]) # same directionc, d = np.array([1.0, 0.0]), np.array([0.0, 1.0]) # orthogonal
print(round(cosine(a, b), 4), round(float(np.linalg.norm(a - b)), 4)) # -> 1.0 12.7279print(round(cosine(c, d), 4), round(float(np.linalg.norm(c - d)), 4)) # -> 0.0 1.4142
# Normalising makes the two metrics agree: ||a-b||^2 == 2 - 2*cos.u, v = a / np.linalg.norm(a), b / np.linalg.norm(b)print(round(float(np.linalg.norm(u - v) ** 2), 10), round(2 - 2 * cosine(u, v), 10)) # -> 0.0 0.0
# Unscaled features let the widest-range column decide every neighbour.X = np.array([[1.0, 1000.0], [2.0, 1000.0], [1.0, 2000.0]]) # (3, 2)print(round(float(np.linalg.norm(X[0] - X[1])), 3), round(float(np.linalg.norm(X[0] - X[2])), 3)) # -> 1.0 1000.0Every number matches the hand calculation. The last block is the k-NN failure: rows 0 and 1 differ by in feature one, rows 0 and 2 by in feature two, and Euclidean treats the second as a thousand times more important purely because of units.
Watch Out For
Using Euclidean distance on unstandardised features
Euclidean distance sums squared differences across features, so a feature with a wide numeric range contributes far more than a narrow one — regardless of how informative either is.
Put income in dollars alongside age in years and the squared income difference dwarfs everything. Your k-NN classifier becomes a one-feature model using income, and your k-means clusters become income bands. Nothing errors, accuracy is mediocre-but-plausible, and people go looking for better values of .
The fix is standardisation before any distance-based method: subtract the mean and divide by the standard deviation per feature, fitted on training data only. Fitting on the full dataset leaks test statistics into training.
Assuming your index metric matches your training objective
Models trained with a contrastive objective on cosine similarity produce vectors whose angles are meaningful and whose magnitudes are not. Search that index with inner product on unnormalised vectors and you have changed the objective: results rank partly by magnitude, which correlates with document length rather than relevance. Quality drops a few points and no test fails.
It cuts the other way too. Matrix-factorisation recommenders trained with an inner-product objective deliberately encode popularity in magnitude. Normalise those vectors and you throw away real signal.
Write down the training objective metric next to the index configuration and make them match. If the index expects unit vectors, assert np.linalg.norm(v) is within tolerance at write time.
The Quick Version
- Euclidean is straight-line distance and is magnitude-sensitive. Cosine is angle only, bounded in .
- Raw dot product mixes direction with both magnitudes, so long vectors win. It is correct only on pre-normalised vectors.
- The metrics can rank pairs in opposite orders: vs is cosine and distance .
- For unit vectors, , so both give the same ranking. Normalise once at ingestion and the choice stops mattering.
- "Cosine distance" is not a true metric — it fails the triangle inequality.
- Embeddings take cosine. Physical units take standardised Euclidean. Sparse counts take cosine. Sets take Jaccard.
- In high dimensions distances concentrate, so "nearest" weakens. Cosine holds up better than Euclidean.
- Standardise features before any distance-based method, or the widest-range feature decides every neighbour.