Skip to content

16 min read

Cosine is a dot product divided by two norms

On this page

A similarity metric is the arithmetic rule a system uses to turn two embeddings into one number saying how alike they are. The rule exists because an embedding on its own does nothing. The reason for representing a passage of text as a vector of numbers is that comparison then becomes arithmetic, and search over a corpus of documents is that comparison run at scale — the question is embedded, every stored document's embedding is scored against it, and the highest-scoring documents come back as the answer. Three rules are in general use for that scoring step: cosine similarity, dot product, and Euclidean distance, the last of which is usually written L2 in software after the mathematical family it belongs to.

Where an engineer meets the choice is a single field in a configuration file. Creating an index in a vector database — the store that holds a corpus of embeddings and answers the question of which stored vectors are nearest to a given one — requires naming the metric it will score with, and every such system supplies a default when the field is left out. That default was chosen by the vendor, who has never seen the model whose vectors are about to be loaded into it. Most teams inherit one, and then find themselves arguing about whether a score of 0.76 is good without anybody in the room being able to say what the arithmetic that produced it did. The quantity is not complicated. It is one multiplication-and-add over the vector's dimensions, divided by two square roots, and it can be worked out on paper for a small case in about ninety seconds. Doing that once changes how every later number reads, because you stop seeing "the similarity" as an opaque output of the vector database and start seeing which of three separate quantities you happen to be looking at.

Three numbers from the same two vectors

Take two four-dimensional vectors. Real embeddings have 384 or 768 or 3,072 dimensions, but nothing about the arithmetic changes with the count, so four is enough to see all of it:

text
a = [ 0.2,  0.5, -0.1,  0.8]
b = [ 0.4,  0.1,  0.3,  0.6]

The dot product multiplies the vectors component by component and adds the results:

text
a · b = (0.2)(0.4) + (0.5)(0.1) + (-0.1)(0.3) + (0.8)(0.6)
      =   0.08     +   0.05     +   -0.03     +   0.48
      =   0.58

The pairing is what makes this the right operation rather than an arbitrary one. Both vectors came out of the same model, so their third component means the same thing as each other's third component and nothing like either one's first. Multiplying two components that describe the same axis asks whether the two vectors agree about that axis: two positives multiply to a positive, two negatives also multiply to a positive, and a positive against a negative multiplies to a negative. Adding the four products accumulates four separate verdicts into one. That single number is the raw material for everything else. It is large when the two vectors have big components with matching signs in the same positions, and it goes negative when they systematically disagree — the third term here contributes −0.03 because a is slightly negative in that dimension while b is positive.

The norm of a vector, written ‖a‖, is its length in the ordinary Pythagorean sense, the square root of the sum of its squared components:

text
‖a‖² = 0.04 + 0.25 + 0.01 + 0.64 = 0.94   →   ‖a‖ = 0.9695
‖b‖² = 0.16 + 0.01 + 0.09 + 0.36 = 0.62   →   ‖b‖ = 0.7874

Squaring is the step that removes the signs, which is why the −0.1 in a contributes 0.01 rather than subtracting anything: a length is a distance from the origin and cannot depend on which way the vector points.

Cosine similarity is the dot product with both lengths divided out:

text
cos(a, b) = 0.58 / (0.9695 × 0.7874) = 0.58 / 0.7634 = 0.7597

The division is the whole idea. Dividing by ‖a‖ and by ‖b‖ is arithmetically identical to replacing each vector with a version of itself scaled to length 1 and then taking the dot product of those, which is why cosine is often described as the dot product of the directions. That equivalence is worth seeing rather than accepting: dividing every component of a by 0.9695 and every component of b by 0.7874 before multiplying pulls both divisors out of every one of the four products, and pulling a common factor out of a sum is the same as dividing the sum. Whatever the vectors' magnitudes were, they are gone by the time the score is produced.

Euclidean distance is the straight-line distance between the two points, computed the way you would compute it on a map:

text
a − b = [-0.2, 0.4, -0.4, 0.2]
‖a − b‖² = 0.04 + 0.16 + 0.16 + 0.04 = 0.40   →   ‖a − b‖ = 0.6325

There is an identity connecting it back to the two quantities already computed, and it is worth expanding once because it explains almost every equivalence argument that follows in this topic:

text
‖a − b‖² = ‖a‖² + ‖b‖² − 2(a · b)
         = 0.94 + 0.62 − 2(0.58)
         = 0.94 + 0.62 − 1.16
         = 0.40

The first line comes out of the same pairing rule applied to the difference. The squared length of any vector is that vector's dot product with itself, so ‖a − b‖² is (a − b)·(a − b). Multiplying that out gives four groups of terms: a·a, then b·b, then a·b and b·a. The last two are the same number, since multiplying component i of one by component i of the other does not care which is written first, so they combine into −2(a · b). That leaves the identity as written, with no approximation anywhere in it.

The same 0.40, from the same three ingredients, computed by a route that never looked at the difference vector. Euclidean distance is not an independent measurement of the pair; it is a particular combination of the dot product and the two norms. Every metric in this topic is built from those three numbers, and the metrics differ only in how they weigh them. Substituting the geometric form of the dot product, a · b = ‖a‖ ‖b‖ cos θ, turns the identity into the law of cosines from school trigonometry, which is a reassuring place for it to land: the distance between two points is decided by the two lengths and the angle between them, and that has been true since long before anyone stored a document as a vector.

What each one can see

The reason this matters practically is that the three quantities respond differently when a vector's magnitude changes while its direction does not. Take c = 2a = [0.4, 1.0, −0.2, 1.6], a vector pointing in exactly the same direction as a but twice as long, and recompute everything against the same b:

Quantitya vs b2a vs bEffect of doubling
Dot product0.5801.160doubles
Norm of the first vector0.96951.9391doubles
Cosine similarity0.75970.7597unchanged
Euclidean distance0.63251.4353grows by 2.27×

The first three rows are predictable from the definitions. Doubling every component doubles every one of the four products, so the dot product doubles. It multiplies every squared component by four, so the sum of squares is quadrupled and its square root — the norm — doubles. Cosine divides a doubled numerator by a doubled denominator, so the factors cancel exactly and the score does not move by so much as a rounding error.

The fourth row is the one worth deriving, because 2.27 is not a number anybody would guess and it is the tell for what Euclidean distance actually measures. Put 2a into the identity: ‖2a − b‖² = 4‖a‖² + ‖b‖² − 4(a · b). The first term picks up a factor of four because the norm is squared, the cross term picks up a factor of four because one of its two operands doubled and it is multiplied by two already, and the middle term picks up nothing at all, because b did not change. Multiplying 0.94 by four gives a shade over three and three-quarters; multiplying 0.58 by four gives a shade over two and a third; three and three-quarters plus 0.62 less two and a third leaves a little over two, and the square root of that is the 1.4353 in the table. Divide that by the original 0.6325 and the ratio is the 2.27.

So the factor is not two, and it is not four, and it is not any constant at all: it depends on the particular values of both vectors, because the operation mixed a quantity that scaled with a quantity that did not. Cosine is blind to magnitude by construction, because the magnitude is exactly what the denominator removes. Dot product scales linearly with it, so a vector twice as long gets twice the score against every candidate in the corpus. Euclidean distance moves in the opposite direction from similarity — the pair got further apart, not closer — and it responds to magnitude in a way that is neither proportional nor intuitive, because the doubled vector is now sitting far away from b in absolute position even though it is aimed at exactly the same place.

The geometric reading is worth holding onto, offered here as a picture rather than as a mechanism. Cosine asks about bearing: two ships on the same heading have cosine 1 regardless of how far each has sailed. Dot product asks about heading and distance travelled at once, since it is the length of a multiplied by the length of b multiplied by the cosine between them. Euclidean distance asks where the two ships actually are, which conflates both. Where the picture stops working is that ships sail on a surface with a fixed scale and embeddings do not: nothing outside the model gives a norm of 1.9391 any physical meaning, so "how far it has sailed" is only interpretable against the other vectors the same model produced. If you know a model puts meaning in direction and nothing in length, cosine is measuring the part you care about and discarding the part you do not. If a model deliberately encodes something in length, cosine is discarding it.

Which of those is true is not a matter of taste, and it is not something the arithmetic can tell you. It is decided by how the model was trained. The contrastive objectives behind most current text embedding models compute a similarity between a query and a passage, divide it by a temperature — a constant that controls how sharply the model is pushed to separate the right pair from the wrong ones — and push the correct pair's score up relative to a batch of incorrect ones. When that similarity term is a cosine, every gradient the model ever received was blind to magnitude, so the model had no reason to put anything there and the norms that come out are whatever the pooling arithmetic happened to produce. When the similarity term is a raw dot product, magnitude is inside the loss, gradients flow through it, and the model can and does learn to use it. Two models with identical architectures and identical training corpora can therefore differ on this point entirely because of one line in the loss function.

Here is the whole calculation in Python with no libraries, which is worth running once so that no vector database is ever the sole source of truth for a number you are reasoning about:

python
import math

def dot(u, v):
    return sum(x * y for x, y in zip(u, v))

def norm(u):
    return math.sqrt(dot(u, u))

def cosine(u, v):
    return dot(u, v) / (norm(u) * norm(v))

def euclidean(u, v):
    return math.sqrt(sum((x - y) ** 2 for x, y in zip(u, v)))

a = [0.2, 0.5, -0.1, 0.8]
b = [0.4, 0.1, 0.3, 0.6]
c = [2 * x for x in a]

for name, v in (("a", a), ("c = 2a", c)):
    print(f"{name:7} dot={dot(v, b):.4f}  norm={norm(v):.4f}  "
          f"cos={cosine(v, b):.4f}  L2={euclidean(v, b):.4f}")

# a       dot=0.5800  norm=0.9695  cos=0.7597  L2=0.6325
# c = 2a  dot=1.1600  norm=1.9391  cos=0.7597  L2=1.4353

# the identity, checked numerically
lhs = euclidean(a, b) ** 2
rhs = norm(a) ** 2 + norm(b) ** 2 - 2 * dot(a, b)
print(round(lhs, 12) == round(rhs, 12))   # True

Note that norm is defined as the square root of dot(u, u) rather than as a separate sum of squares, which is the identity from the previous section written as code: a length is a dot product of a vector with itself, so one function suffices for both jobs.

The same three functions run unchanged on 768-dimensional vectors from an actual model. The only thing that changes is the loop length, which is why a similarity computation over a million 768-dimensional documents is a memory-bandwidth problem rather than an arithmetic one — 768 multiply-adds per document is nothing, and reading 3 GB of float32 vectors off memory to feed them is the entire cost. That figure is a million documents times 768 components times four bytes for each float32. It is also why the cosine version is not meaningfully more expensive than the dot product version at query time, despite the extra division and square roots, since each document's norm can be computed once when it is stored and the query's norm once per query. Two divisions against 768 multiply-adds is not where the time goes, and a team that switches from cosine to inner product expecting a large speedup is usually measuring something else.

Where in the range a score actually sits

A score of 0.76 is hard to argue about because its theoretical range is a bad guide to its practical one. Cosine runs from −1 to 1 by definition, so the midpoint looks like 0 and 0.76 looks like three quarters of the way to perfect. What actually determines whether 0.76 is high is the distribution the model produces on realistic pairs, and that distribution is nowhere near uniform over the interval.

Start with what chance looks like. Two independent random vectors in 768 dimensions have an expected cosine of 0, with a standard deviation of roughly 1/sqrt(768) = 0.036. Random directions in high-dimensional space are almost always close to orthogonal — at right angles to one another, which is the configuration that reads as cosine 0. That near-orthogonality is the property that makes the whole approach work, since it means there is room for an enormous number of nearly independent directions and unrelated concepts do not collide. If embeddings were distributed like random directions, a cosine of 0.20 would already be five and a half standard deviations above chance, 0.20 divided by 0.036 being a little over five and a half, and 0.76 would be an astronomical outlier.

Real embeddings are not distributed like random directions. Transformer sentence encoders are anisotropic: their outputs occupy a comparatively narrow cone rather than spreading over the whole sphere, so two completely unrelated sentences typically score 0.10 to 0.35 rather than 0.00, and some models push that floor as high as 0.75. The gap between what chance predicts and what is observed is the finding, and neither figure states it on its own: chance says 0.20 is extraordinary, measurement says 0.20 is below the floor for unrelated text in many models, and the distance between those two verdicts is how much of the nominal range the model has thrown away. The cone is a consequence of shared components in the representation — frequency effects and a few dominant directions that almost every input has a large projection onto — and it varies substantially between model families and even between checkpoints of the same family. This is why a single number carries no information until you know which model produced it, and it is why the answer to "is 0.76 good" always begins by measuring what the model does on pairs whose relevance you already know.

Negative cosines are worth a sentence because their absence is informative. A score of −0.4 would mean two texts pointing in systematically opposing directions, which is a strong claim about a representation space that has no notion of negation; embedding models place "the refund window is 30 days" and "the refund window is not 30 days" very close together, because they share nearly all their content. Retrieval scores that cluster in a positive band with nothing below zero are the normal case, and a corpus producing many negative similarities usually means the vectors are not what you think they are.

The number is a measurement, not a verdict

Treating a similarity score as a grade is the source of several neighbouring intuitions that are wrong in ways that cause real damage later.

Cosine similarity is not a probability and it is not calibrated to anything. It does not estimate the chance that a passage answers a query, it does not compose the way probabilities do, and a score twice as large does not indicate twice the relevance. Nor is it comparable across models: the anisotropy above means each model occupies its own band, so a threshold tuned against one encoder carries no meaning when applied to another, even when both report cosine and both look like sensible numbers in the same interval.

It is also not a distance in the sense that a database index or a clustering algorithm might assume. Cosine distance, usually defined as 1 − cos, does not satisfy the triangle inequality, so it is not a metric in the mathematical sense; the angular distance arccos(cos) / π is, which is why a few libraries offer that variant instead. This is invisible for straightforward top-k retrieval, where a query asks only for the k highest-scoring documents in order, and becomes visible when you feed the scores into something that assumes metric-space geometry, such as certain tree-based indexes or hierarchical clustering with average linkage.

And the direction of "better" is not the same across the three. Dot product and cosine are similarities, where higher is better; Euclidean is a distance, where lower is better. Every vector database has a configuration flag for the metric, and every one of them has a support forum full of people who switched from cosine to L2, kept a ORDER BY score DESC somewhere in their code, and got the least relevant documents in the corpus returned with complete confidence. That failure at least announces itself. The failures worth most of this topic are the ones that leave the ranking plausible and merely wrong.

So three metrics, three combinations, one set of ingredients: a dot product that pairs components position by position, and two lengths that say how far each vector reaches. Cosine divides both lengths out and reports angle alone. Dot product leaves both in. Euclidean distance combines all three through the identity expanded above, which is why doubling one input moved it by a factor nobody could have predicted without doing the substitution. None of the three is more correct than the others in the abstract; each is sensitive to something different, and knowing which is the difference between reading a score and guessing at one.

Nothing so far has said which metric to use, because the answer depends on a condition that has not yet been introduced: whether the vectors coming out of the model have been scaled to unit length. The three numbers computed above came from vectors with norms 0.97 and 0.79, chosen arbitrarily, and the three metrics disagreed about magnitude accordingly. Force every norm to exactly 1 and something specific happens to the arithmetic — the denominator of the cosine becomes 1, and the identity ‖a − b‖² = ‖a‖² + ‖b‖² − 2(a · b) collapses into a form with only one moving part. That collapse is what makes three index settings interchangeable in some systems and not in others.

Similarity metrics and matching the metric to the model — read the current version at https://daviddominguez.dev/learn/en/similarity-metrics-and-matching-the-metric-to-the-model/cosine-is-a-dot-product-divided-by-two-norms

Back to top