Course Content
Live Coding Interview Prep
7 sections · 50 lessons
Write cosine similarity, dot product, and Euclidean distance functions manually.
What you need to know
| measure | formula | range | meaning |
|---|---|---|---|
| dot product | sum of a[i] × b[i] | any number | alignment and length |
| cosine similarity | dot / (|a| × |b|) | -1 to 1 | alignment only |
| Euclidean distance | sqrt(sum of (a[i] − b[i])²) | 0 to ∞ | straight-line gap; smaller is closer |
- Dot product grows with vector length, so on raw vectors it favours long vectors. It is only a similarity measure when vectors are normalised.
- Cosine divides out the lengths, leaving only the angle.
- Euclidean is a distance: 0 means identical, and you sort ascending.
For unit vectors (length 1) they are tied together:
|a − b|² = |a|² + |b|² − 2(a · b) = 1 + 1 − 2 cos = 2 − 2 cosSo ranking by highest cosine and by lowest Euclidean distance gives the same order. That is why vector databases normalise once and then use the cheapest one, the dot product.
1import math2from collections.abc import Sequence34def _check(a: Sequence[float], b: Sequence[float]) -> None:5 if len(a) != len(b):6 raise ValueError(f"length mismatch: {len(a)} vs {len(b)}")78def dot(a: Sequence[float], b: Sequence[float]) -> float:9 """Sum of element-wise products."""10 _check(a, b)11 return sum(x * y for x, y in zip(a, b))1213def magnitude(a: Sequence[float]) -> float:14 """L2 length. math.hypot scales internally, so huge values do not overflow."""15 return math.hypot(*a)1617def cosine_similarity(a: Sequence[float], b: Sequence[float]) -> float:18 """Cosine of the angle between a and b; 0.0 if either is a zero vector."""19 _check(a, b)20 ma, mb = magnitude(a), magnitude(b)21 if ma == 0 or mb == 0:22 return 0.023 c = sum((x / ma) * (y / mb) for x, y in zip(a, b)) # normalise first, then dot24 return max(-1.0, min(1.0, c)) # clip rounding error2526def euclidean_distance(a: Sequence[float], b: Sequence[float]) -> float:27 _check(a, b)28 return math.hypot(*(x - y for x, y in zip(a, b))) # sqrt of summed squaresThe tricky parts:
- Check lengths first, in every function.
zip([1, 2, 3], [1, 2])quietly stops after two pairs, so without the check you get a wrong answer and no error. The check also has to come before the zero-vector shortcut incosine_similarity, or a mismatched pair with a zero vector returns 0.0 silently. - Zero vector. Cosine is undefined for a zero vector. Returning 0.0 ("no similarity") is the common convention; raising is also defensible — say which you chose.
- Large values. The textbook
dot(a, b) / (|a| × |b|)overflows: fora = [1e200, 1e200]both the dot product and the product of lengths becomeinf, andinf / infisnan. Dividing each element by its vector's length before multiplying keeps every number between -1 and 1, andmath.hypotcomputes the length without squaring huge numbers directly. - Floating point. Rounding can push a cosine to 1.0000000000000002, so the result is clipped to [-1, 1]. In tests, compare with
math.isclose, never==.
Complexity: each function makes one or two passes over d elements, so O(d) time. math.hypot(*...) unpacks the values into arguments, so it uses O(d) temporary space; a plain sum of squares would be O(1) extra space but can overflow — (1e200) ** 2 raises OverflowError. The NumPy versions are a @ b, a @ b / (np.linalg.norm(a) * np.linalg.norm(b)) and np.linalg.norm(a - b).
A real-life example
Take a = [1, 2, 2] and b = [2, 0, 1]:
| step | computation | value |
|---|---|---|
| dot | 1×2 + 2×0 + 2×1 | 4 |
| |a| | sqrt(1 + 4 + 4) | 3.0 |
| |b| | sqrt(4 + 0 + 1) | 2.236 |
| cosine | 4 / (3 × 2.236) | 0.5963 |
| Euclidean | sqrt(1 + 4 + 1) | 2.4495 |
1a, b = [1, 2, 2], [2, 0, 1]2print(dot(a, b), round(cosine_similarity(a, b), 4), round(euclidean_distance(a, b), 4))3# 4 0.5963 2.449545ua = [x / magnitude(a) for x in a]6ub = [x / magnitude(b) for x in b]7print(round(euclidean_distance(ua, ub) ** 2, 4), round(2 - 2 * cosine_similarity(a, b), 4))8# 0.8074 0.80749print(cosine_similarity([0, 0, 0], a)) # 0.010print(cosine_similarity([1e200, 1e200], [1e200, 1e200])) # 0.9999999999999998, not nanThe last two lines confirm the unit-vector identity on real numbers, which is a nice thing to show the interviewer after writing the code.
Deduplicating near-identical product descriptions or support tickets is often just "cosine above 0.95 means duplicate", computed with exactly these functions.
Follow-up questions to expect
- "Which should a vector database use?" — Normalise at write time and use dot product: it gives the cosine ranking with the fewest operations.
- "Is cosine distance a true metric?" —
1 − cosinedoes not satisfy the triangle inequality in general; angular distance (arccos(cosine) / π) does. It matters for some tree-based indexes, rarely in practice. - "How would you test these?" — Known pairs:
[1, 0]and[0, 1]give cosine 0 and distance √2; identical vectors give cosine 1 and distance 0; and a property test that the unit-vector identity holds on random vectors.