AAI Agentic Handbook
Embeddings & Encoders

Cosine similarity

Budding levelReviewed Oct 2026In 4 interview questionsIn 1 post

A score from −1 to 1 measuring the angle between two vectors, ignoring their length: the standard way to compare embeddings.

In plain English

Cosine similarity measures how closely two embeddings point in the same direction. A score near 1 means very similar meaning, near 0 means unrelated, and negative means pointing in opposite directions (rare for text embeddings).

It ignores how long the vectors are and only cares about direction, which suits comparing meaning.

Analogy

Two arrows on a compass: what matters is whether they point the same way, not how long each arrow is.

Common pitfall

Treating a high cosine score as proof the chunk answers the question. It measures topical closeness; an on-topic chunk can say the opposite.

How it actually works

cos(a, b) = (a · b) / (‖a‖ ‖b‖). It is the Dot product of the two vectors after scaling each to unit length. For unit vectors, ‖a − b‖² = 2 − 2 cos(a, b), so cosine, dot product and Euclidean distance all produce the same ranking. Without normalisation they disagree: dot product rewards long vectors, Euclidean distance punishes vectors that point the right way but sit at a different length, and only cosine ignores length entirely. That is why vector stores normalise once at write time and then use the cheaper inner product.

Absolute values are model-specific. Anisotropy means unrelated texts score ~0.1 on some models and 0.6–0.8 on others, so any threshold (for a Semantic cache, deduplication or abstention) must be calibrated per model on labelled pairs. Some models are trained for dot product on unnormalised vectors, where magnitude carries signal; follow the model card. Steck et al. (2024) also showed cosine on learned embeddings can be arbitrary when training regularisation leaves dimensions free to rescale. Cosine measures topical closeness, not truth or answer quality.

What I'd ship on day one

L2-normalise every vector at write time and at query time; index with inner product (pgvector vector_ip_ops, FAISS IndexFlatIP/HNSW-IP); follow the model card if it specifies dot product on raw vectors; calibrate thresholds per model on 500–1,000 labelled pairs.

Worked example

Query q = (3, 4). Doc A = (6, 8) points the same way but is twice as long: cosine 1.00, dot 50, Euclidean 5.0. Doc B = (4, 3): cosine 0.96, dot 24, Euclidean 1.41. Doc C = (0.6, 0.8): cosine 1.00, dot 5, Euclidean 4.0. Doc D = (10, 0): cosine 0.60, dot 30, Euclidean 8.06. Cosine ranks A = C > B > D; dot product ranks A > D > B > C; Euclidean ranks B > C > A > D. Normalise all four to unit length and every metric agrees: A = C > B > D.

In the interview

Commit: normalise at write, use inner product, and calibrate every threshold per model on labelled pairs. Show you know when the three metrics disagree (unnormalised vectors) and when they don't (unit vectors). Follow-up: 'set a dedup threshold for catalog listings': label 1,000 pairs, pick the cosine giving 99% precision, and re-derive it on any model change.

Doc vector (q = 3,4)AngleCosineDotEuclideanRank cos / dot / L2
A (6, 8)0°1.00505.001= / 1 / 3
B (4, 3)16.3°0.96241.413 / 3 / 1
C (0.6, 0.8)0°1.0054.001= / 4 / 2
D (10, 0)53.1°0.60308.064 / 2 / 4
Unnormalised vectors: cosine follows the angle, dot product rewards length, Euclidean rewards nearby endpoints. Three different rankings.
Doc after L2-normalisingCosineDotEuclideanRank (all three)
A (0.6, 0.8)1.001.000.001=
B (0.8, 0.6)0.960.960.283
C (0.6, 0.8)1.001.000.001=
D (1.0, 0.0)0.600.600.894
On unit vectors ‖a − b‖² = 2 − 2cos, so all three metrics give the same order.

Numbers worth knowing

Unit-vector identity
‖a − b‖² = 2 − 2cos(a, b)holds only when both vectors are L2-normalised
Random-pair cosine
≈0 ± 1/√drandom directions in d dims; ±0.03 at d = 1,024
Unrelated-text cosine
≈0.1 to 0.8model-dependent because of anisotropy; measure before thresholding

How to decide

WhenDo this
Model card says 'normalise' or doesn't sayNormalise and use inner product; cosine, dot and L2 then agree.
Model is trained for dot product on raw vectorsDon't normalise; magnitude carries signal for that model.
You need a yes/no threshold (cache hit, duplicate, abstain)Calibrate on labelled pairs per model and per language; never reuse another model's number.

Cost maths

Per comparison: d multiply-adds for a dot product, plus two norms and a division for raw cosine. Normalising once at write removes the norms: at 10M vectors × 1,024 dims, brute-force cosine without pre-normalisation does ~3× the arithmetic of inner product on normalised vectors.

Operating it

Time to scale up when…
  • Any threshold-based feature (cache, dedup, abstention) after an embedding-model change
  • Mixed normalised and unnormalised vectors in one index
What to monitor
  • Vector norm distribution at ingest: alert if any vector's norm deviates from 1 by more than 0.01 (normalised indexes)
  • Precision of threshold decisions on a weekly 100-pair labelled sample: alert below 0.97

When it goes wrong

What users saw

A catalog dedup job merged 12% of listings that were different products; merchants complained their items had vanished.

How it was caught

Merchant support tickets, then a 500-pair manual audit of merged pairs.

Root cause

The job compared raw dot products on unnormalised vectors from a model that does not normalise its output; long, keyword-stuffed listings had large norms and scored high against everything.

Fix

L2-normalisation at write, threshold recalibrated on 1,000 labelled pairs (precision 0.99 at cosine 0.94), and merges above 0.90 but below 0.94 sent to human review.

Sources

  1. Is Cosine-Similarity of Embeddings Really About Similarity? — Steck et al., 2024paper
  2. Sentence-BERT: Sentence Embeddings using Siamese BERT-Networks — Reimers & Gurevych, 2019paper

Check yourself

Three questions. Every option is explained once you answer.

1ConceptWhy do vector stores L2-normalise vectors at write time and then index with inner product instead of computing cosine directly?
2ScenarioA model card states the model is trained for dot product on raw, unnormalised vectors. You are building a duplicate-detection feature on it. What is right?
3CalculationQuery q = (3, 4) and document v = (8, 6), both unnormalised. What are the cosine similarity and the dot product?