Euclidean distance, cosine similarity and the dot product produce exactly the same ranking when the vectors are unit length, and the proof is two lines of algebra. When they are not unit length, the three disagree, and which one your index uses becomes a decision with consequences.
Four metrics on the same two vectors
Take a = [1, 2, 3] and b = [4, 6, 3]. Difference a - b = [-3, -4, 0].
Euclidean (L2) sqrt(3^2 + 4^2 + 0^2) = sqrt(25) = 5.0000
Manhattan (L1) |3| + |4| + |0| = 7.0000
Chebyshev (Linf) max(3, 4, 0) = 4.0000
Cosine:
a . b = 1*4 + 2*6 + 3*3 = 4 + 12 + 9 = 25
||a|| = sqrt(1 + 4 + 9) = sqrt(14) = 3.741657
||b|| = sqrt(16 + 36 + 9)= sqrt(61) = 7.810250
cos = 25 / (3.741657 * 7.810250) = 25 / 29.223278
= 0.855477
cosine distance = 1 - 0.855477 = 0.144523
Four different numbers for the same pair, and they are not on comparable scales. Manhattan is always at least as large as Euclidean; Chebyshev is always at most as large. Cosine distance is bounded in [0, 2] and the others are unbounded. A threshold tuned for one is meaningless for another.
| Metric | Description |
|---|---|
| Euclidean (L2) | Straight-line distance. Sensitive to magnitude. The default in most vector indexes, and correct when magnitude is meaningful — coordinates, measurements, image features. |
| Cosine | Angle only, magnitude discarded. The default for text embeddings, because a longer document should not be less similar to a query just for being longer. |
| Dot product | Angle and magnitude together. Correct where magnitude encodes something you want — a learned popularity or confidence term in a recommender. |
| Manhattan (L1) | Sum of absolute differences. Less dominated by any single large coordinate, so more robust to one outlier dimension. Used in some sparse and tree-based settings. |
| Hamming | Count of differing positions in two equal-length sequences. Only defined for discrete vectors — bits, categories, characters. |
The identity that collapses three into one
Expand the squared Euclidean distance. This is the whole derivation:
||a - b||^2 = (a - b) . (a - b)
= a.a - 2(a.b) + b.b
= ||a||^2 - 2(a.b) + ||b||^2
If ||a|| = ||b|| = 1:
||a - b||^2 = 1 - 2(a.b) + 1
= 2 - 2(a.b)
= 2 - 2*cos(a, b)
Squared Euclidean distance is a strictly decreasing function of cosine similarity. Not approximately, not usually — exactly, for all unit vectors. So sorting by ascending Euclidean distance and sorting by descending cosine similarity produce identical orderings, and since the dot product of unit vectors is the cosine, that makes three.
cos = 1.00 -> ||a-b||^2 = 0.00 -> ||a-b|| = 0.0000
cos = 0.90 -> ||a-b||^2 = 0.20 -> ||a-b|| = 0.4472
cos = 0.50 -> ||a-b||^2 = 1.00 -> ||a-b|| = 1.0000
cos = 0.00 -> ||a-b||^2 = 2.00 -> ||a-b|| = 1.4142
cos =-1.00 -> ||a-b||^2 = 4.00 -> ||a-b|| = 2.0000
This has a practical payoff. If your vector database only offers Euclidean or only offers inner product, you can still get cosine behaviour by normalising the vectors before insertion and before query. The index does not need to know.
What happens when you skip the normalisation
Now the failure case, which is common and quiet. Query q = [1, 0], and three stored vectors:
d1 = [0.60, 0.80] norm 1.00 direction 53.1 degrees off q
d2 = [0.99, 0.14] norm 1.00 direction 8.0 degrees off q
d3 = [2.00, 1.50] norm 2.50 direction 36.9 degrees off q
Inner product against q:
d1: 1*0.60 + 0*0.80 = 0.60
d2: 1*0.99 + 0*0.14 = 0.99
d3: 1*2.00 + 0*1.50 = 2.00 <- ranks first
Cosine against q:
d1: 0.60 / 1.00 = 0.60
d2: 0.99 / 1.00 = 0.99 <- ranks first
d3: 2.00 / 2.50 = 0.80
Two different top results from the same data. d3 wins on inner product purely for being two and a half times as long, despite pointing 37 degrees away from the query while d2 points 8 degrees away. Normalise d3 to [0.8, 0.6] and its inner product becomes 0.80, the ranking matches cosine, and the bug disappears in one line.
The reason this is hard to catch: the results are not obviously broken. They are plausible, related documents — just systematically the longer ones. Recall degrades by a few points and nothing throws an error. The check is to measure the norms of a hundred stored vectors; if they are not all 1.0, and your metric is inner product, you have this.
What makes something a metric, and why an index cares
“Distance” has a technical definition, and several things people call distances do not meet it. A function d is a metric if it satisfies four conditions:
- Non-negativity.
d(a, b) >= 0. - Identity.
d(a, b) = 0only whena = b. - Symmetry.
d(a, b) = d(b, a). - Triangle inequality.
d(a, c) <= d(a, b) + d(b, c). Going via a third point is never a short cut.
Euclidean, Manhattan and Hamming satisfy all four. Cosine distance, defined as 1 - cos, fails the last one, and it is easy to show:
a = [1, 0]
b = [0.7071, 0.7071]
c = [0, 1]
cos(a,b) = 0.7071 -> d(a,b) = 0.2929
cos(b,c) = 0.7071 -> d(b,c) = 0.2929
cos(a,c) = 0.0000 -> d(a,c) = 1.0000
Triangle inequality would require:
1.0000 <= 0.2929 + 0.2929 = 0.5858
It does not hold. Cosine distance is not a metric.
Angular distance fixes it. Use the angle itself, normalised, rather than one minus its cosine:
d_ang(a, b) = arccos(cos_sim(a, b)) / pi
a to b: 45 degrees -> 0.25
b to c: 45 degrees -> 0.25
a to c: 90 degrees -> 0.50
0.50 <= 0.25 + 0.25 holds, with equality.
Squared Euclidean also fails, for the same reason:
a=[0,0] b=[1,0] c=[2,0]
d(a,c)^2 = 4, d(a,b)^2 + d(b,c)^2 = 1 + 1 = 2
4 <= 2 is false. Always take the square root
if you need a metric; ranking is unaffected either way.
Whether this matters depends on the index. Graph-based approximate indexes such as HNSW, and clustering indexes such as IVF, use the measure only for ordering and behave fine with cosine — they are approximate by design, and the ordering is all they need. Exact metric-tree structures, including ball trees, VP-trees and cover trees, prune whole branches using the triangle inequality; give them a non-metric and they return wrong results rather than slow ones.
The safe move is the one from the previous section: normalise the vectors and use Euclidean distance. It is a genuine metric, it ranks identically to cosine, and nothing downstream has to know that cosine was what you meant.
Hamming, and binary vectors
Hamming distance counts positions where two equal-length sequences differ. It applies to bits, characters or categories, and not to continuous values.
a = 1 0 1 1
b = 1 1 0 1
- x x - distance 2
XOR then popcount, which is why it is fast:
1011 XOR 1101 = 0110
popcount(0110) = 2
The reason to care: binary quantisation of embeddings. Take a 1,024-dimensional float vector and keep only the sign of each component. The vector becomes 1,024 bits — 128 bytes instead of 4,096 — and the comparison becomes XOR plus popcount, which modern CPUs do at enormous rates.
Storage: fp32 1024 * 4 = 4096 bytes
int8 1024 * 1 = 1024 bytes
binary 1024 / 8 = 128 bytes 32x smaller
Relationship to cosine, for random-ish vectors:
hamming_fraction ~= theta / pi
cos = 1.00 -> theta = 0 -> 0% of bits differ
cos = 0.70 -> theta = 0.795 -> 25% of bits differ
cos = 0.00 -> theta = 1.571 -> 50% of bits differ
Binary vectors lose real accuracy, so the standard pattern is a two-stage search: retrieve a few hundred candidates with Hamming on the binary vectors, then rescore those few with full precision. The expensive metric only runs on 0.01% of the corpus.
Choosing, in four questions
- Does magnitude mean anything in your vectors? For text embeddings, almost never — so cosine, or normalise and use anything. For learned recommender vectors where magnitude encodes popularity, the dot product is the metric the model was trained with and switching to cosine silently discards a signal.
- What was the model trained with? This overrides everything else. An embedding model trained with a cosine objective has no reason to place meaning in its norms. Use the metric the model card specifies; if it does not specify one, cosine is the safe default for text.
- Are the vectors discrete? Then Hamming, and the question of Euclidean versus cosine does not arise.
- Have you checked the norms? One line:
np.linalg.norm(V, axis=1). If they are all 1.0, your choice between L2, cosine and inner product is cosmetic and you can pick whichever your index runs fastest.
Manhattan, Chebyshev and the rest of the Minkowski family show up in classical machine learning and rarely in embedding retrieval. If you are choosing between them for a nearest-neighbour index over neural embeddings, the choice is almost certainly not where your recall problem is — the retrieval strategy matters far more than the metric.
Top comments (0)