Dot Product vs Cosine Similarity
Two ways to score "how similar", and they disagree the moment length stops being constant. Which one your embedding model was trained for is not optional trivia.
Overview
What this is
Cosine similarity asks "what angle apart are these two vectors" and ignores length entirely. Dot product asks "how much do these two vectors agree, weighted by how long they both are" — length is part of the answer, not discarded. Most embedding search defaults to cosine, but a growing number of models (some recommendation embeddings, some matryoshka and MIPS-optimised models) are trained so that dot product is the correct similarity, and using cosine on them silently under-uses the model.
Stretch Doc B
same direction as before, just longer — like a verbose, repetitive document embedding
Rank By
Query: "lightweight training method"
—The Space, 2D For Clarity
Both Scores
Dot Product vs Cosine: A Practical Guide
The same two vectors, two different questions asked of them.
The formulas
dot(a,b) = Σ aᵢbᵢ cos(a,b) = dot(a,b) / (‖a‖ ‖b‖)
Cosine is the dot product after dividing out both vectors' lengths — which is exactly why stretching one vector along its own direction changes its dot product with anything but never changes its cosine with anything.
The same formula, one difference
dot product = a · b = Σ aᵢbᵢ
cosine similarity = (a · b) / (‖a‖ ‖b‖)
Cosine similarity is the dot product divided by both lengths. That division is the whole difference, and it removes magnitude from the comparison, leaving only direction.
Worked through. Take a = [3, 4] and two candidates:
- b = [6, 8] — the same direction, twice as long.
- c = [4, 3] — a different direction, the same length.
| Dot product | Cosine | |
|---|---|---|
| a · b | 18 + 32 = 50 | 50 / (5 × 10) = 1.00 |
| a · c | 12 + 12 = 24 | 24 / (5 × 5) = 0.96 |
By dot product, b scores twice as high as c. By cosine, b is a perfect match (identical direction) and c is very close. The two measures rank them the same way here, and for different reasons — and they will disagree whenever lengths vary substantially.
When magnitude is noise, and when it is signal
Use cosine when length carries no meaning. Text embeddings are the standard case: a 4,000-word article and a 200-word summary of it should be close, and their vector lengths differ mainly because of length and frequency, not content.
Use the dot product when length carries meaning. Some recommendation systems deliberately encode popularity or confidence as vector magnitude, so that a popular item scores higher for an equally-good direction match. Discarding the length would discard that information.
| Situation | Measure |
|---|---|
| Text and document embeddings | Cosine |
| Semantic search, RAG | Cosine |
| Recommendations with popularity in the magnitude | Dot product |
| Trained with a dot-product objective | Dot product — match the training |
| Comparing images by feature vectors | Cosine, usually |
That fourth row is the one people miss. If the embedding model was trained with an inner-product objective, its geometry is arranged for the dot product, and using cosine may not be what it was optimised for. Model documentation usually states which to use, and following it matters more than the general argument.
The identity that makes this a non-issue in practice
On vectors normalised to length 1, the two measures are identical — the denominator of cosine similarity becomes 1 × 1.
That is why almost every embedding pipeline normalises on the way in and then uses plain dot products. You get cosine semantics with the cost of a dot product, and a dot product over a matrix is a single fast matrix multiplication.
There is a second identity worth knowing. On normalised vectors:
‖a − b‖² = 2 − 2(a · b)
So squared Euclidean distance is a decreasing function of the dot product, which means ranking by cosine similarity and ranking by Euclidean distance give exactly the same order on normalised vectors. The choice of metric in a vector database is then a question of implementation rather than of results.
import numpy as np
vecs = model.encode(chunks, normalize_embeddings=True) # normalise once
q = model.encode([question], normalize_embeddings=True)[0]
scores = vecs @ q # dot product == cosine, because both are unit length
top = np.argsort(-scores)[:5]
The one difference, and when it bites
Cosine similarity is the dot product with the lengths divided out, so the two agree exactly when vectors are normalised and disagree in a specific, predictable way when they are not. This shows which retrieval bugs come from that gap.
Guided tour
- Start at 1x with cosine ranking. Doc A, the concise on-topic passage, ranks first.
- Stretch Doc B to 5x. Under cosine, nothing about the ranking moves — Doc B's direction, and therefore its angle to the query, never changed.
- Switch to dot product and stretch again. Somewhere around 3-4x, Doc B overtakes Doc A — purely because it got longer, with its actual topical relevance unchanged.
- Read both scores side by side at 5x. Cosine still ranks Doc A first; dot product now ranks Doc B first. Same embeddings, same query, opposite answer.
Which one is "right"?
Neither, in the abstract — it depends on what the embedding model was trained to make meaningful. If length in your model's embeddings correlates with noise (document verbosity, padding, repetition), cosine is safer. If length was trained to carry real signal (popularity, confidence, specificity), dot product is the metric the model actually optimised for, and cosine throws that signal away. Check your model's documentation; do not assume.
What to remember
Cosine similarity divides out vector length and only ever measures direction; dot product does not, so it rewards longer vectors regardless of whether that length means anything. The two metrics agree only when every vector has the same length — true if you normalise your embeddings to unit length, false otherwise. Know which one your embedding model was trained against, because using the wrong one is a silent, hard-to-debug retrieval quality bug.
Interpreting the numbers
Cosine similarity has a fixed range, which makes it thresholdable:
| Value | Meaning |
|---|---|
| 1.0 | Identical direction |
| 0.8–0.95 | Strongly related |
| 0.5–0.8 | Loosely related |
| 0.0 | Orthogonal — nothing in common |
| Negative | Opposed directions |
The dot product has no fixed range at all, which is a practical disadvantage: a score of 42 means nothing without knowing the typical magnitudes in that space.
Two cautions about cosine thresholds:
Calibrate against your own data. Many sentence-embedding models score unrelated text at 0.2–0.4, so a threshold of 0.5 admits considerable noise. Score a few hundred known-related and known-unrelated pairs and put the cut where the distributions separate.
Negative values are rare in practice. Most modern embedding models place nearly everything in a narrow cone of the space, so scores cluster in a band and true opposition is uncommon. Relative ranking remains reliable; absolute values are model-specific.
The zero-vector trap
Cosine similarity divides by the vector lengths, so a zero-length vector produces a division by zero.
That is not hypothetical. An empty document, a chunk that contained only whitespace or markup, a user with no interactions — all produce zero vectors, and the result is a nan that propagates silently through averages and rankings.
def cosine(a, b):
denom = np.linalg.norm(a) * np.linalg.norm(b)
return float(a @ b / denom) if denom else 0.0 # guard itThe dot product has no such problem, which is a minor point in its favour. The real fix is to filter empty content before indexing it.
Other measures, briefly
| Measure | Sensitive to length? | Use |
|---|---|---|
| Cosine | No | Text, embeddings, semantic similarity |
| Dot product | Yes | When magnitude encodes something real |
| Euclidean | Yes | Coordinates in real space |
| Manhattan | Yes | Sparse or high-dimensional data |
| Mahalanobis | Yes, and accounts for correlation | Outlier detection |
Euclidean distance is the one people reach for by habit from geometry, and it is usually the wrong choice for unnormalised embeddings: a long vector is far from everything, so document length dominates the ranking. Normalise, and it becomes equivalent to cosine.
Questions people ask
Which should I use for RAG? Cosine — or equivalently, normalise your vectors and use dot products.
Are they ever genuinely different in results? Only on unnormalised vectors. Normalised, they rank identically.
Why do some databases default to inner product? Because it is the cheapest operation, and it is correct if you normalise on the way in. Check whether yours normalises for you.
Can cosine similarity be negative? Mathematically yes, down to −1. With most text embedding models it is uncommon.
Does normalising lose information? It discards magnitude. For text embeddings that is usually noise; for models that encode confidence in the magnitude, it is a loss.
What threshold should I use? Do not guess — measure it on labelled pairs from your own corpus. Model-to-model variation is large.
Recap in one screen
- Cosine similarity is the dot product divided by both lengths, which removes magnitude and keeps direction.
- Use cosine when length is noise (almost always, for text); use the dot product when magnitude encodes something real.
- On unit-length vectors the two are identical, and Euclidean distance ranks the same way.
- Normalise once at indexing time, then a fast matrix multiply gives you cosine scores.
- Guard against zero vectors, and calibrate thresholds against your own labelled pairs.