Modules/Gen AI/ Similarity Metric Lab

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

1.0×

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 productCosine
a · b18 + 32 = 5050 / (5 × 10) = 1.00
a · c12 + 12 = 2424 / (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.

SituationMeasure
Text and document embeddingsCosine
Semantic search, RAGCosine
Recommendations with popularity in the magnitudeDot product
Trained with a dot-product objectiveDot product — match the training
Comparing images by feature vectorsCosine, 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.

example_01.pyNumPy
Output

Guided tour

  1. Start at 1x with cosine ranking. Doc A, the concise on-topic passage, ranks first.
  2. 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.
  3. 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.
  4. 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:

ValueMeaning
1.0Identical direction
0.8–0.95Strongly related
0.5–0.8Loosely related
0.0Orthogonal — nothing in common
NegativeOpposed 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 it

The 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

MeasureSensitive to length?Use
CosineNoText, embeddings, semantic similarity
Dot productYesWhen magnitude encodes something real
EuclideanYesCoordinates in real space
ManhattanYesSparse or high-dimensional data
MahalanobisYes, and accounts for correlationOutlier 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.

Recall check

0 of 3

Say the answer out loud before you reveal it — recalling it is what makes it stick, and rereading it is not.

  1. Without scrolling back — what is the one-line takeaway from this module?

  2. What does this module say about “What this is”?

  3. What does this module say about “Stretch Doc B”?

Cheat sheet

Dot Product vs Cosine Similarity for Retrieval

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.

GEN AI · vizlearn.in/gen_ai/dot_product_vs_cosine_similarity.html

About the author

Ashish Jangra builds and maintains VizLearn. Every module here is written and the visualisation behind it hand-built, so the numbers in a readout come from the same code that draws the picture. Corrections are genuinely welcome and get priority over everything else — if a page states something wrong, or an animation misrepresents what the algorithm does, get in touch.