Skip to main content

Semantic Similarity: Cosine, Dot & Euclidean

Introduction

Last lesson, every piece of text became a vector. This lesson answers the question that turns those vectors into retrieval: given two vectors, how do you score "how similar" they are?

That score is the whole engine of search. Every RAG system, every semantic search, every recommendation does the same thing: embed everything, then rank by a similarity score and return the top matches. Get the metric right and search just works; get it subtly wrong and it quietly returns plausible-but-irrelevant junk.

There are three you'll meet everywhere — cosine similarity, dot product, and Euclidean distance. The good news: once you see how they relate, choosing is easy.

In this lesson you'll learn:

  • What each metric actually measures (and you'll drag vectors to feel it)
  • The unifying insight: for normalized vectors, they all agree
  • When they diverge — the magnitude-bias gotcha
  • Which to use in practice (and why matching your model matters)

The Three Metrics

Picture two vectors as arrows from the origin. The three metrics each look at a different aspect:

1. Cosine similarity — the angle. It measures only the direction between the two vectors, ignoring their length. It ranges −1 to 1: 1 = pointing the same way (very similar), 0 = perpendicular (unrelated), −1 = opposite. Because it ignores magnitude, it's the natural fit for text — meaning lives in the direction. This is the default for text embeddings.

2. Dot product — angle and magnitude. Mathematically dot = cosine × |a| × |b|, so it rewards both alignment and length. A longer vector scores higher. On normalized (unit-length) vectors this is identical to cosine; on raw vectors it's biased toward longer ones.

3. Euclidean (L2) distance — the straight-line gap. The plain distance between the two points: ‖a − b‖. 0 = identical, larger = more different. Note the flip: it's a distance, so smaller means more similar (the opposite of the two similarities). For text it's rarely the best choice.

An infographic titled 'Three Ways to Measure Similarity' with three cards. The green COSINE card (marked 'text default') shows two vectors from an origin with the angle theta between them: it measures only direction (angle), not length, ranges from -1 to 1 (1 = same, 0 = perpendicular, -1 = opposite), and is the right call for text. The indigo DOT card shows one long and one short vector: it equals cosine times the magnitudes, so longer vectors score higher — fine for normalized vectors, biased on raw ones. The amber EUCLIDEAN card shows two points joined by a dashed line labelled the norm of a minus b: the straight-line distance between points, 0 = identical, larger = more different, a distance (smaller = closer), rarely best for text. An indigo callout: normalize to unit length and cosine equals dot, and all three give the same ranking, so for text embeddings use cosine (or dot for speed). A bottom banner: every RAG search is just ranking documents by one of these scores — pick the metric your embedding model was trained for.

The Key Relationship: For Unit Vectors, They All Agree

Here's the insight that dissolves most of the confusion — and it's why last lesson told you to normalize:

If your vectors are unit-length (normalized), cosine similarity equals the dot product, and ranking by cosine, dot, or Euclidean all return the same nearest neighbors.

Why? Normalizing throws away magnitude, so the only thing left for any metric to see is the angle. Cosine and dot become literally the same number; and for unit vectors, Euclidean distance is just a monotonic function of the angle (‖a − b‖² = 2 − 2·cosine), so it sorts results in the identical order.

The practical payoff: with normalized text embeddings (which is almost all of them), the metric choice barely affects which documents you retrieve. So pick the fastest — usually the dot product — which is exactly what vector databases do under the hood. (Try it in the widget: tick unit-normalize and watch cosine and dot snap to the same value.)

When They Differ: The Magnitude-Bias Gotcha

If they agree when normalized, when do they diverge? Only when vectors are not normalized — and then magnitude starts to matter, which is usually a bug, not a feature:

  • Dot product rewards longer vectors. If your embeddings aren't normalized, a long, loosely-related document can out-score a short, perfectly-relevant one — purely because its vector happens to be longer. This is the classic "my retrieval keeps surfacing rambling docs" failure.
  • Cosine is immune: it strips out length and compares pure direction, so a one-sentence perfect answer and a ten-page perfect answer score the same on relevance.
  • Euclidean mixes direction and magnitude, so it inherits some of the same length sensitivity.

The takeaway: if you're ever unsure whether your vectors are normalized, cosine is the safe default because it makes the question moot. (Some models do let magnitude encode confidence or length on purpose — but for general semantic search, you want direction only.)

Which to Use in Practice

A short, opinionated guide:

  • Text / semantic search → cosine (the safe, universal default; it's what papers and leaderboards report). If your vectors are normalized, dot product is equivalent and faster — use it for speed at scale.
  • Euclidean → mostly avoid for text. Reach for it only when magnitude genuinely encodes meaning (some image or geospatial embeddings).
  • Match the metric your embedding model was trained with. Models are trained to align similarity with a specific metric (usually cosine/dot). Using the wrong one — e.g., Euclidean on a cosine-trained model — measurably hurts recall. Check the model card.
  • Your vector DB lets you choose the metric per index (cosine, dot/ip, l2). Set it to the one your model expects — a one-line config that silently ruins relevance if it's wrong. (Vector databases are the next section.)

See It in Code

All three in a few lines of numpy — and the normalized equivalence made concrete:

import numpy as np

def cosine(a, b):    return a @ b / (np.linalg.norm(a) * np.linalg.norm(b))
def dot(a, b):       return a @ b
def euclidean(a, b): return np.linalg.norm(a - b)

q  = np.array([0.9, 0.4, 0.2])     # a query embedding
d1 = np.array([0.8, 0.5, 0.1])     # a RELEVANT doc  (similar direction)
d2 = np.array([-0.3, 0.2, 0.9])    # an UNRELATED doc

print(cosine(q, d1), cosine(q, d2))        # ~0.99   ~-0.01   higher = more similar -> d1
print(euclidean(q, d1), euclidean(q, d2))  # ~0.17    ~1.40   lower  = more similar -> d1
# both agree d1 is the match — they just point in opposite directions (sim up vs distance down)
# The unit-vector equivalence: normalize, and cosine == dot
def unit(v): return v / np.linalg.norm(v)

qn, d1n = unit(q), unit(d1)
print(cosine(qn, d1n), dot(qn, d1n))   # ~0.985  ~0.985   identical for unit vectors
# ...which is why vector DBs store normalized vectors and rank by the (faster) dot product.

🧪 Try It Yourself

Predict, then drag. Use the playground above:

  1. Point A and B the same way but make B much longer. What happens to cosine vs dot? → Cosine stays high (same direction); dot grows (it rewards length). That gap is the magnitude bias.
  2. Now tick unit-normalize. → Cosine and dot become the same number — magnitude is gone.
  3. Rotate B to ~90° from A. → Cosine → ~0 (perpendicular = unrelated), even though the points may be close in Euclidean terms.

The lesson in your hands: direction = meaning (cosine), and normalizing makes the metric choice a non-issue. When your retrieval surfaces long, rambling, loosely-related docs, suspect un-normalized dot product first.

Similarity playground — drag two vectors and watch cosine, dot & Euclidean change live.

Mental-Model Corrections

  • "Higher is always more similar." True for cosine/dot, false for Euclidean — it's a distance, so lower is closer. Don't mix up the direction.
  • "I have to agonize over the metric." For normalized text embeddings, cosine, dot, and Euclidean give the same ranking — pick the fastest (dot). It only matters when vectors aren't normalized.
  • "Dot product is just a faster cosine." Only when normalized. On raw vectors it's magnitude-biased — it favors longer vectors (often longer/rambling docs).
  • "Euclidean is the 'real' distance, so it's best." For text, no — meaning is angular; cosine almost always wins. Euclidean shines only when magnitude is meaningful.
  • "Any metric works with any model." Match the metric the model was trained for (usually cosine/dot); the wrong one quietly drops recall.

Key Takeaways

  • Retrieval = ranking by a similarity score. The three metrics: cosine (angle/direction), dot (angle × magnitude), Euclidean (straight-line distance; smaller = closer).
  • For unit-normalized vectors, cosine = dot, and all three rank identically — so the metric choice barely matters; use the fastest (dot).
  • They only diverge on un-normalized vectors, where dot/Euclidean become magnitude-biased (favoring longer vectors/docs) and cosine stays pure direction — the safe default.
  • Use cosine for text (dot if normalized, for speed); avoid Euclidean for semantic text. Match the metric your model was trained for, and set your vector DB's index metric to match.
  • Direction = meaning — that single idea is why cosine dominates semantic search.