Semantic search is only as good as its distance metric. The way you measure “how close” two vectors are determines what your retrieval system considers similar — and the wrong choice produces subtly wrong results that are hard to debug.
All modern vector databases support three distance metrics. Understanding when each applies will save you hours of debugging mysterious retrieval failures.
Range: −1 to 1 (higher = more similar)
Measures the angle between two vectors, ignoring their magnitude. A document with 1,000 words and one with 5 words about the same topic will score ~1.0.
Best for: General-purpose text retrieval, RAG pipelines, normalized embeddings
Consider three 2D vectors (simplified for illustration). In the real world, these would be 1,536-dimensional, but the math is identical:
A = [0.6, 0.8] — a short document about AI
B = [0.8, 0.6] — a different short document about AI
C = [6.0, 8.0] — a very long document about AI (A × 10)
| Pair | Cosine | Dot Product | Euclidean |
|---|---|---|---|
| A vs B (same length, similar topic) | 0.9600 | 0.9600 | 0.2828 |
| A vs C (same direction, C is 10× longer) | 1.0000 | 10.0000 | 9.0000 |
Cosine similarity correctly identifies that A and C point in the same direction(score 1.0000) even though C is 10× larger. Dot product ranks C much higher than B, even though they're about equally relevant — the length difference is inflating the score.
Finding the exact nearest neighbor requires computing the distance from the query to every vector in the index — O(n × d) where n is the number of vectors and d is the dimension. At 10M vectors × 1536 dims, that is 15.36 billion multiply-add operations per query. Even on a GPU, this takes seconds.
Approximate Nearest Neighbor (ANN)algorithms build an index that lets you skip most comparisons. The most popular algorithm, HNSW, typically achieves >95% recall at 1-10ms query time — covered in depth in the next lesson.
import chromadb
client = chromadb.PersistentClient(path="./db")
# IMPORTANT: set the metric at collection creation time — it cannot be changed later
cosine_collection = client.get_or_create_collection(
name="rag_docs",
metadata={"hnsw:space": "cosine"}, # Most common for text
)
dot_collection = client.get_or_create_collection(
name="recommendations",
metadata={"hnsw:space": "ip"}, # Inner product (dot product)
)
l2_collection = client.get_or_create_collection(
name="image_search",
metadata={"hnsw:space": "l2"}, # Euclidean distance
)If you L2-normalize all your vectors before storing them (so each has unit length, i.e. ‖v‖ = 1), then dot product and cosine similarity become equivalent. This is why many providers return pre-normalized vectors — you can use the faster dot product operation and still get cosine semantics.
import numpy as np
def normalize(vec: list[float]) -> list[float]:
v = np.array(vec)
return (v / np.linalg.norm(v)).tolist()
# After normalization, dot product == cosine similarity
vec_a = normalize([0.6, 0.8])
vec_b = normalize([0.8, 0.6])
dot = sum(a * b for a, b in zip(vec_a, vec_b))
cos = cosine_similarity(vec_a, vec_b)
print(f"dot: {dot:.6f}") # → 0.960000
print(f"cosine: {cos:.6f}") # → 0.960000 (identical!)cosine_similarity, dot_product, and euclidean_distance from scratch using NumPy.hnsw:space: cosine, add 5 documents, and run a query that returns the top 3 results with distances printed.Next: how HNSW actually builds the graph index that makes ANN search fast — and what parameters control the speed vs. recall trade-off.