Skip to content

Comparing vectors with cosine similarity

You’ve got two embeddings and you want one number telling you how alike they are. That’s this page. There’s a little arithmetic, all of it GCSE-level, and I’ll walk every step.

The obvious idea is straight-line distance. Two points, Pythagoras, done. That’s Euclidean distance, and it does work — but it has a quirk that matters for text.

Distance cares about magnitude: how far from the origin each point sits. And for embeddings, magnitude tends to track things you don’t care about, like how long the text was or how emphatic it is. Consider:

  • “The cat sat on the mat.”
  • “The cat sat on the mat, and stayed there for the rest of the afternoon, occasionally twitching an ear.”

These mean nearly the same thing. But the longer one has more going on, and it can land further from the origin. Euclidean distance would call them noticeably different, purely because one is wordier.

Cosine similarity ignores magnitude entirely and looks only at direction. Point the same way, and you’re similar — however far along the arrow you happen to sit. For text, direction is where meaning lives and magnitude is mostly noise. That’s why cosine won.

│ ╱ B "the cat sat on the mat, and stayed…"
│ ╱
│ ╱ ← small angle = high similarity
│╱ ╱ A "the cat sat on the mat"
└──────────────→
Cosine cares about the angle between the arrows.
Euclidean cares about the gap between the arrowheads.
A · B
similarity = ─────────────────
‖A‖ × ‖B‖
A · B the dot product of the two vectors
‖A‖ the length (magnitude) of vector A

Three pieces. Let’s do each with real numbers, using two small 2D vectors so you can check the arithmetic by hand.

Take A = [3, 4] and B = [4, 3].

  1. The dot product A · B — multiply the vectors element by element, then add it all up.

    A · B = (3 × 4) + (4 × 3)
    = 12 + 12
    = 24

    That’s the entire operation. Pair up the positions, multiply, sum. It’s a single for loop, and it’s the line doing the real work in every code guide on this site.

  2. The magnitudes ‖A‖ and ‖B‖ — the length of each arrow, via Pythagoras.

    ‖A‖ = √(3² + 4²) = √(9 + 16) = √25 = 5
    ‖B‖ = √(4² + 3²) = √(16 + 9) = √25 = 5
  3. Divide

    similarity = 24 / (5 × 5) = 24 / 25 = 0.96

0.96 out of a maximum of 1. Those two vectors point in very similar directions.

Cosine similarity always lands between -1 and 1:

Score Meaning
1.0 Identical direction. Same text, or a near-perfect paraphrase.
0.7 – 0.9 Strongly related. What a good search hit usually looks like.
0.3 – 0.6 Loosely related. Same general topic, different specifics.
0.0 Unrelated. The vectors are at right angles.
Below 0 Pointing away from each other.

Look at that formula again. If both vectors already have length 1, the denominator is 1 × 1 = 1, and dividing by 1 does nothing.

when ‖A‖ = 1 and ‖B‖ = 1
A · B A · B
similarity = ───────── = ─────── = A · B
1 × 1 1

So: normalise once, at embedding time, and cosine similarity collapses into a plain dot product forever after. No square roots, no division, just multiply-and-add.

Normalising means dividing every element by the vector’s length:

import numpy as np
vector = np.array([3.0, 4.0])
unit = vector / np.linalg.norm(vector) # [0.6, 0.8]
np.linalg.norm(unit) # 1.0

This is why every guide on this site normalises up front, and why the search line ends up being one short expression. You’ve moved the expensive part to the moment you store the vector, where it happens once, instead of the moment you query, where it happens for every document on every request.

You’ll see all three in the wild. Here’s how to choose without agonising:

Cosine similarity — the default, and correct for text search almost always. Immune to length effects.

Dot product — identical to cosine when your vectors are normalised. When they’re not, it rewards longer vectors, which is occasionally what you want (a recommender that should favour popular items, for instance). Faster, since it skips the division.

Euclidean distance (L2) — measures actual separation. Fine for image embeddings and clustering. For normalised vectors it ranks identically to cosine, so on text the distinction is often academic. Note it runs the other way round: smaller means closer.

Everything above used two dimensions so you could follow the arithmetic. The formula doesn’t change at all for 1,536 — the dot product loop just runs 1,536 times instead of twice, and the magnitude sums 1,536 squares instead of two.

There’s no extra concept hiding in the higher dimensions. It’s the same three operations, more times. You can’t picture it, and you don’t need to.