Vibe Engines is free, interactive AI engineering & system design — runs in your browser. Pick a learning path →
Cosine Similarity
The measure behind every embedding search and RAG system: how aligned are two vectors, regardless of their length? It is the dot product divided by both magnitudes — 1 means identical direction, 0 orthogonal, -1 opposite.
The problem
Given two equal-length vectors a and b, return their cosine similarity: dot(a, b) / (‖a‖ · ‖b‖), where ‖v‖ is the Euclidean norm (square root of the sum of squares). The result lies in [-1, 1] and ignores vector length — only direction matters.
Your turn — write it
Edit the stub, hit Run (or ⌘/Ctrl + Enter), and watch the hidden tests. Stuck? open the hints under the problem, or Reveal solution is one click away.
Implement cosine_similarity(a, b) = dot product of a and b, divided by the product of their Euclidean norms.
Examples and constraints 4 examples
a = [1, 2, 3], b = [1, 2, 3]1.0a = [1, 0], b = [0, 1]0.0a = [1, 0], b = [-1, 0]-1.0a = [1, 1], b = [2, 2]0.9999999999999998- len(a) == len(b) ≥ 1, and neither vector is all zeros.
- Result in [-1, 1], within floating-point tolerance.
- Cosine ignores magnitude — scaling a vector does not change the answer.
Hints 4 ideas
- The dot product is the sum of element-wise products:
Σ aᵢ·bᵢ. - The Euclidean norm of
vissqrt(Σ vᵢ²). - Divide the dot product by
‖a‖ · ‖b‖to cancel out length and leave only direction. - Identical vectors give 1; orthogonal give 0; opposite give -1.
Approach, complexity & discussion — open after you solve
Start from brute force
The tempting shortcut is to score similarity with the raw dot product, sum(x * y for x, y in zip(a, b)). It is one pass, O(d) time and O(1) space, and it does reward alignment, but it also grows with length: [2, 2]·[2, 2] = 8 outscores [1, 0]·[1, 0] = 1, although both pairs point in exactly the same direction. Euclidean distance fails the other way: [1, 1] and [2, 2] are about 1.41 apart even though their directions are identical. The question is about direction only, so the score has to divide out both lengths.
The approach
Cosine is the dot product of the two unit vectors, so divide out both lengths. The reference cosine_similarity computes three sums: dot = sum(x * y for x, y in zip(a, b)), then norm_a and norm_b as math.sqrt of each vector’s sum of squares, and returns dot / (norm_a * norm_b). A scale factor on either vector multiplies the numerator and the denominator alike, so it cancels, which is the length-independence the problem asks for. The Cauchy–Schwarz inequality, |a·b| ≤ ‖a‖‖b‖, is why the answer lands in [-1, 1].
The TypeScript cosineSimilarity fuses the three sums into one loop with running totals dot, na and nb, and takes both square roots only at the end. Either way the work is three multiply-adds per dimension. The constraints rule out an all-zero vector; without that promise you need an explicit guard, because the norm product is 0 and the division fails.
b to 2b (dashed) doubles the dot product and one norm together, so the reference returns 0.7071067811865475 for both pairs, and acos of it is 45°. A raw dot product would score 2b twice as similar. Turning b toward a raises the score toward 1; turning it to [-1, 3], perpendicular to a, makes the dot product −3 + 3 = 0 and the score exactly 0, whatever the two lengths are.Walk through an example
For the first example, a = b = [1, 2, 3], the running sums grow together and the division returns exactly 1.0.
| step | work | dot | sum of a² | sum of b² | result |
|---|---|---|---|---|---|
| 1 | index 0: x = 1, y = 1 | 1 | 1 | 1 | — |
| 2 | index 1: x = 2, y = 2 | 5 | 5 | 5 | — |
| 3 | index 2: x = 3, y = 3 | 14 | 14 | 14 | — |
| 4 | norm_a = norm_b = math.sqrt(14) = 3.7416573867739413 | 14 | 14 | 14 | — |
| 5 | dot / (norm_a * norm_b), where the product rounds to exactly 14.0 | 14 | 14 | 14 | 1.0 |
Complexity
Time O(d) for d dimensions: three multiply-adds per index, whether done as three passes (Python) or one loop (TypeScript). Space O(1): three running sums. The raw dot product is also O(d) time and O(1) space, so normalizing costs only two extra sums and two square roots. Comparing one query against N stored vectors is O(N·d) either way.
Edge cases to test
a = [1, 1], b = [2, 2]→0.9999999999999998, not exactly 1.0, becausesqrt(2) * sqrt(8)rounds to 4.000000000000001. Compare with a tolerance, as the tests do, never with==.a = b = [1, 5]→1.0000000000000002: rounding can also land just above 1, so clamp to [-1, 1] before passing a score toacos.a = [0, 0], b = [1, 1]: the norm product is 0, so Python raisesZeroDivisionErrorand TypeScript returnsNaN. The constraints exclude it; production code should return 0 or raise a clear error.a = [1, 2, 3], b = [1, 2]→ Python returns0.5976…, becausezipstops at the shorter list whilenorm_astill uses all ofa; TypeScript returnsNaN. Check the lengths first, or usezip(a, b, strict=True)in Python 3.10+.a = b = [1e200, 1e200]→nan: every square overflows to infinity, and infinity over infinity is NaN. Dividing each vector by its largest absolute entry first gives the same cosine without overflow.a = b = [1e-200, 1e-200]→ZeroDivisionError: the squares underflow to 0.0, so a nonzero vector looks like a zero vector. The same rescaling fixes it.
Common mistakes
- Returning the raw dot product without dividing by the magnitudes — then length dominates and it is no longer cosine.
- Dividing by zero on a zero vector — guard it (define the similarity as 0 or handle explicitly).
- Assuming the inputs are the same length without checking.
Follow-ups an interviewer asks next
- You compare one query against a million stored embeddings.
- Normalize every stored vector to unit length once, at write time, and the query once per search. Cosine then reduces to a plain dot product, so no comparison pays for norms. A full scan is still O(N·d), so at scale an approximate nearest-neighbor index such as HNSW or IVF answers top-k queries without touching every vector, trading a little recall for speed.
- Make it numerically robust.
- Compare with a tolerance, and clamp the result to [-1, 1] before any
acos, since rounding can land just outside. To avoid overflow and underflow, scale each vector by its largest absolute entry before squaring; cosine is unchanged by scaling. In Python,math.fsumsums many terms more accurately, andmath.hypot(*v)computes a norm without intermediate overflow. - When should you use the dot product or Euclidean distance instead?
- On unit-length vectors all three give the same ranking: the dot product equals cosine, and squared Euclidean distance equals
2 − 2·cos. They diverge when length carries meaning, for example in a model trained so that magnitude encodes something. Use the metric the embedding model was trained with; its documentation usually names it. - The vectors are sparse, like TF-IDF over a large vocabulary.
- Store each one as a map from index to nonzero value, iterate over the smaller map, and look each key up in the other. The dot product then costs O(min(nnz(a), nnz(b))) instead of O(d), and each document’s norm can be computed once and stored. Most entries are zero, so skipping them is where nearly all of the saving comes from.
- Score one query against a whole matrix at once.
- Stack the stored vectors as the rows of a matrix and divide each row by its norm once. With NumPy,
scores = M @ (q / np.linalg.norm(q))then gives every cosine in one matrix-vector product, which runs in optimized native code instead of a Python loop. To pick the top k without sorting all N scores,np.argpartition(-scores, k)[:k]finds them in O(N) and only those k need sorting. A zero row still divides by zero, so drop or flag zero vectors when you normalize.
Where this shows up
Cosine similarity is the standard score for comparing embeddings — semantic search, RAG retrieval, clustering, deduplication, recommendations. On vectors that are already unit-normalized (many embedding models return these) it reduces to a plain dot product, which is exactly why vector databases can rank by dot product and get cosine ordering for free.
Before embeddings. The classic vector space model of information retrieval represented each document and query as a TF-IDF vector over the vocabulary and ranked documents by cosine. Dividing out length was the point there too: without it, a long document would beat a short, focused one simply by repeating more words. Item-based recommenders use the same score between columns of a user-item rating matrix, often in an “adjusted” form that first subtracts each user’s average rating, so that a generous rater and a harsh one who rank items the same way still look alike.
Scores are not probabilities. A cosine of 0.8 is not “80% similar”, and the same number means different things for different embedding models, because each model spreads its vectors over the sphere differently. A retrieval threshold, such as “only pass chunks above 0.75 to the model”, has to be chosen per model from labeled pairs of relevant and irrelevant results, and chosen again when the model changes.
Similarity versus distance. Libraries often report cosine distance, 1 − cos, so a smaller number is better; reading it as a similarity flips every ranking. It is also not a true metric, because it can violate the triangle inequality. When an algorithm needs a real metric, use the angle itself, acos(cos), or, on unit vectors, Euclidean distance, which equals sqrt(2 − 2·cos) and so ranks identically.
Check yourself
Predict cosine_similarity([1, 2, 3], [3, 2, 1]) as a fraction.
5/7 ≈ 0.7142857142857143. The dot product is 3 + 4 + 3 = 10, and both vectors have squared norm 14, so the denominator is exactly 14. Reversing a vector keeps its length but changes its direction.
Query q = [3, 1]; candidates x = [1, 0] and y = [2, 4]. Which ranks first by dot product, and which by cosine?
Dot product prefers y (10 against 3); cosine prefers x (0.949 against 0.707). y wins the dot product mainly by being long, about 4.47 against 1. On unit-normalized vectors the two rankings would agree.
Add 10 to every entry of [1, 0] and [0, 1]. Does the cosine change?
Yes, from 0.0 to about 0.9955 for [11, 10] and [10, 11]. Cosine ignores scaling but not shifting: a shared offset pulls both vectors toward the same direction. Subtracting each vector’s mean first is what makes it Pearson correlation.
Every entry of every vector is non-negative, as with word counts or TF-IDF. What range can the cosine take?
[0, 1]. Each product x * y is at least 0, so the dot product cannot be negative; it is 0 only when the two vectors have no nonzero index in common, such as documents that share no word. There, 0 means “nothing in common”, never “opposite”.
Normalize [3, 4] and [4, 3] to unit length. What are the unit vectors, and what is their dot product?
[0.6, 0.8] and [0.8, 0.6], since both vectors have length 5. Their dot product is 0.6 × 0.8 + 0.8 × 0.6 = 0.96, which is exactly cosine_similarity([3, 4], [4, 3]) = 24 / 25. Normalizing first turns cosine into a plain dot product.
Reference solution
Reference solution — Python
import math
def cosine_similarity(a, b):
dot = sum(x * y for x, y in zip(a, b)) # alignment
norm_a = math.sqrt(sum(x * x for x in a)) # length of a
norm_b = math.sqrt(sum(y * y for y in b)) # length of b
return dot / (norm_a * norm_b)
print(cosine_similarity([1, 2, 3], [1, 2, 3])) # -> 1.0
print(cosine_similarity([1, 0], [0, 1])) # -> 0.0Reference solution — TypeScript
function cosineSimilarity(a: number[], b: number[]): number {
let dot = 0, na = 0, nb = 0;
for (let i = 0; i < a.length; i++) {
dot += a[i] * b[i]; // alignment
na += a[i] * a[i]; // |a|^2
nb += b[i] * b[i]; // |b|^2
}
return dot / (Math.sqrt(na) * Math.sqrt(nb));
}
console.log(cosineSimilarity([1, 2, 3], [1, 2, 3])); // -> 1
console.log(cosineSimilarity([1, 0], [0, 1])); // -> 0Questions about Cosine Similarity
What does cosine similarity measure?
It measures the angle between two vectors and ignores their lengths. The value is the cosine of that angle: 1 when the vectors point the same way, 0 when they are perpendicular, and -1 when they point in opposite directions. It is computed as the dot product divided by the product of the two Euclidean norms. Because both lengths are divided out, doubling a vector changes nothing, which is why it suits embeddings, where the direction of a vector is what carries the meaning.
What is the difference between cosine similarity and the dot product?
The dot product multiplies matching entries and sums them, so it grows with the lengths of the vectors as well as with their alignment. Cosine similarity divides the dot product by both lengths, so only alignment remains. For [1, 0] and [1, 0] the dot product is 1, while for [2, 2] and [2, 2] it is 8, yet the cosine is 1 in both cases. When every vector is normalized to length 1 the two are identical, which is why many vector databases normalize once and then rank by dot product.
Can cosine similarity be negative?
Yes. It ranges from -1 to 1, and it is negative whenever the angle between the vectors is more than 90 degrees, which is exactly when the dot product is negative. [1, 0] against [-1, 0] scores -1. Whether you see negative values depends on the data: vectors with only non-negative entries, such as word counts or TF-IDF weights, can never score below 0, while learned embeddings can. A ranking or threshold that assumes a 0 to 1 range should be checked against the model you actually use.
Why does cosine similarity return 0.9999999999999998 instead of 1?
Because floating-point arithmetic rounds. For [1, 1] and [2, 2] the exact answer is 1, but the norms are the square roots of 2 and 8, neither of which is exactly representable, and their product rounds to 4.000000000000001, so 4 divided by it gives 0.9999999999999998. Rounding can also go the other way: [1, 5] against itself gives 1.0000000000000002. Compare scores with a tolerance, such as an absolute difference below 1e-9, and clamp to the range -1 to 1 before passing a score to acos.
This idea in other formats
Explore the topic
See this alongside everything else on the same subject — handbooks, system designs, challenges and tools, in one place.
More Challenges
- Two SumThe classic warm-up: find the two numbers that add up to a target. Brute force is O(n²) — a hash map gets you to one pass, O(n). Solve it in Python or TypeScript, right in your browser, with hidden tests and a reveal-solution button.Read →
- Valid ParenthesesThe canonical stack problem: decide whether every bracket is closed by the right type, in the right order. A stack turns nested matching into a single pass. Solve it in Python or TypeScript with hidden tests.Read →
- Fizz BuzzThe famous screening question. Print 1…n, but multiples of 3 become "Fizz", of 5 become "Buzz", and of both become "FizzBuzz". Easy — the catch is testing divisibility in the right order. Solve it in Python or TypeScript.Read →
- SoftmaxThe function at the end of every classifier and language model: turn raw scores (logits) into a probability distribution. Implement the numerically stable version so big logits do not overflow. Solve it in Python or TypeScript.Read →
- Top-K RetrievalThe core of the "R" in RAG: given a query embedding and a set of document embeddings, return the indices of the k most similar docs by cosine similarity, with a stable tie-break. Solve it in Python or TypeScript.Read →
- Token-Level F1The metric behind QA evaluation (SQuAD and friends): how well does a predicted answer overlap a reference as a bag of words? Compute token precision and recall, then their harmonic-mean F1. Solve it in Python or TypeScript.Read →