Skip to content

Commit 76b101c

Browse files
committed
Add diagrams and search-focused rewrite for ScaNN post
Three new SVG widgets: - scann-pipeline: funnel flow showing 10M→300K→200→10 reduction - scann-partition: interactive 2D cluster diagram with query toggle - scann-error: step-through error decomposition (parallel vs perpendicular) Post rewritten around e-commerce search example throughout. Diagrams embedded at the point where each concept is introduced. https://claude.ai/code/session_012c6pzQCjnRVPikAphiPgw4
1 parent 4ee3dd2 commit 76b101c

5 files changed

Lines changed: 658 additions & 61 deletions

File tree

src/posts/vector-search-pre-filter-post-filter.md

Lines changed: 67 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -2,121 +2,127 @@
22
date: 2026-06-03
33
tag: search
44
title: "How ScaNN works"
5-
read: 8 min
6-
deck: "Google's vector search algorithm explained with analogies — partitioning, approximate scoring, and why compression isn't just compression."
5+
read: 9 min
6+
deck: "Google's vector search algorithm from the ground up — with diagrams. Partitioning, approximate scoring, and why compression isn't just compression."
77
---
88

9-
You're building a music recommendation system. You have 100 million songs. A user plays one and you want to find the 10 most similar songsinstantly, every time.
9+
You're running an e-commerce search engine. A user types "red running shoes" and you want to find the 10 most relevant productsnot just keyword matches, but semantically similar ones. You have 10 million product embeddings. You need a result in under 50 milliseconds.
1010

11-
The obvious approach: compare the query song to every other song, rank by similarity, return top 10. That's 100 million comparisons per request. At any real scale, that's too slow.
11+
The obvious approachcompare the query vector to every product, rank by similarity, return top 10 — is too slow at that scale. 10 million dot products, 768 dimensions each, at every query.
1212

13-
ScaNN is Google's solution to this problem. It's the algorithm behind several of Google's production search and recommendation systems, open-sourced in 2020. It solves the speed problem in three phases, and each phase has a neat idea behind it.
13+
ScaNN (Scalable Nearest Neighbors) is Google's solution. It's been open-sourced since 2020 and underlies several of Google's production retrieval systems. It solves the speed problem in three phases, each built on a specific insight about the structure of similarity search.
1414

15-
## first: what is "similarity" here?
15+
## the three phases
1616

17-
Before the algorithm, a quick grounding. In vector search, each item (a song, a product, a document) is represented as a list of numbers — a *vector*. These numbers capture some learned sense of the item's meaning or character: similar items end up with similar vectors.
17+
<div data-widget="scann-pipeline"></div>
1818

19-
Similarity between two vectors is usually measured as a dot product: multiply each pair of corresponding numbers and add them up. A high result means the vectors are pointing in the same direction — the items are similar. This is called MIPS: Maximum Inner Product Search. ScaNN is built to do MIPS fast.
19+
ScaNN breaks the search into three stages, each dramatically narrowing the candidate set:
2020

21-
## phase 1: partitioning — stop searching everywhere
21+
1. **Partitioning** — group the corpus into neighborhoods, search only the most relevant ones.
22+
2. **Approximate scoring** — use compressed vectors for fast similarity estimates.
23+
3. **Exact rescoring** — rerank a small shortlist with precise dot products.
2224

23-
The first idea is simple: **don't look at everything**.
25+
## phase 1: partitioning
2426

25-
Think of a library with 100,000 books. If you want books similar to a mystery novel you enjoyed, you don't start at shelf A1 and check every book. You walk to the mystery section. You've already cut the search down to a few thousand books.
27+
The core idea: most of the corpus is irrelevant to any given query. Before scoring anything, figure out *where in the space* the answer probably lives — then only look there.
2628

27-
ScaNN does the same thing. At index time, it runs k-means clustering on all the vectors and groups them into *k* clusters — neighborhoods of similar vectors. Each cluster has a centroid, which is the average point at the center of that group.
29+
ScaNN runs k-means clustering on the corpus at index time. This groups all 10 million products into `k` clusters — neighborhoods of similar items. Each cluster has a centroid (its average position).
2830

2931
At query time:
3032

31-
1. Score the query against all `k` centroids. This is cheapjust `k` comparisons.
32-
2. Pick the top `t` most similar centroids (the most promising neighborhoods).
33-
3. Only look at vectors inside those `t` clusters.
33+
1. Score the query against all `k` centroids. This is cheap: just `k` dot products.
34+
2. Pick the top `t` closest centroids the most promising neighborhoods.
35+
3. Score only the vectors inside those `t` clusters.
3436

35-
With 10 million vectors and `k = 3,000` clusters (~10M), each cluster holds about 3,000 vectors. If you search the top 100 clusters, you're scoring 300,000 vectors instead of 10 million — a 33x reduction before doing anything else.
37+
A practical rule of thumb is `k ≈ √N`. With 10M products, that's roughly 3,000 clusters of ~3,000 vectors each. If you search the top 100 clusters, you're scoring 300,000 vectors instead of 10 million — a 33× speedup before any other optimization.
3638

37-
The dial here is `t`: how many clusters to search. More clusters → better results → higher latency. In practice, searching 3–10% of clusters gives good recall for most datasets.
39+
<div data-widget="scann-partition"></div>
3840

39-
## phase 2: approximate scoring — compress the vectors
41+
Toggle the query above to see how different searches land in different parts of the product space. The key point: vectors outside the searched clusters are never touched — no memory reads, no dot products.
4042

41-
After partitioning you still have hundreds of thousands of vectors to score. Storing 10 million vectors at 768 dimensions in full precision is ~30GB. Doing exact dot products across that is expensive even after the partitioning step.
43+
The tradeoff dial is `t` (how many clusters to search). More clusters → better recall, higher latency. Most production systems search 3–10% of clusters.
4244

43-
So ScaNN compresses the vectors. The technique is called **quantization** — instead of storing each vector precisely, you store an approximation that's cheap to compute with.
45+
## phase 2: approximate scoring
4446

45-
Here's the analogy: think of your music taste as a point on a map. Storing your exact location takes many decimal places. Instead, you just say "I'm in neighborhood #42." You lose some precision, but you can compare neighborhoods cheaply.
47+
After partitioning, you still have ~300,000 vectors to score. Doing exact dot products for all of them is still expensive — 300,000 × 768-dimensional float multiplications, repeated for every query.
4648

47-
In quantization, ScaNN pre-computes a "codebook" of reference vectors during index building. Each database vector is then encoded as a sequence of references into that codebook — a short code instead of a full vector. At query time, ScaNN pre-computes how similar the query is to each codebook entry, then scores every compressed vector via fast table lookups instead of full dot products.
49+
ScaNN compresses the vectors. The technique is **quantization**: instead of storing each vector as 768 float32 numbers, store it as a compact code that's fast to compare.
4850

49-
This is dramatically faster. You're looking up pre-computed numbers in a small table, not doing hundreds of floating-point multiplications per vector.
51+
Here's how it works. At index time, ScaNN groups each vector's dimensions into sub-blocks and builds a small "codebook" of reference patterns for each block. A 768-dimensional vector might be split into 8 blocks of 96 dimensions, with a 256-entry codebook per block. Each vector is then encoded as 8 numbers (codebook indices), one per block — instead of 768 floats.
5052

51-
## the clever part: compression that cares about what matters
53+
At query time, ScaNN precomputes the dot product between the query and every codebook entry (8 blocks × 256 entries = 2,048 lookups). Then scoring any compressed vector is just 8 table lookups and an addition — dramatically faster than a full dot product.
5254

53-
Standard compression minimizes error uniformly — it tries to reconstruct each vector as accurately as possible in all directions, equally. That sounds right. But for similarity search, it's the wrong goal.
55+
This is product quantization (PQ), and it's standard. What makes ScaNN different is *how* it trains the compressor.
5456

55-
Here's why. You only care about getting the *ranking* right for the vectors with the *highest* similarity to your query. The vector at rank 47,000 can have a completely wrong approximate score and it doesn't matter — you were never going to return it.
57+
## the key insight: compress where it matters
5658

57-
And here's the key geometry: vectors with high similarity to your query tend to be *pointing in the same direction* as your query. If you think of the query as an arrow, the relevant results are other arrows roughly aligned with it.
59+
Standard PQ trains the codebooks to minimize reconstruction error — the distance between the original vector and its compressed version, averaged uniformly across all directions. That sounds right, but it's the wrong goal for search.
5860

59-
When you compress a vector and introduce a small error, that error can be decomposed into two parts:
60-
- **parallel error** — error in the direction the vector is pointing
61-
- **perpendicular error** — error in the sideways directions
61+
Here's why. When you compress a vector `x` to ``, you introduce an error `e = x̃ − x`. That error shows up in your similarity estimates: instead of computing the true dot product `⟨q, x⟩`, you compute the approximate `⟨q, x̃⟩`. The difference is `⟨q, e⟩`.
6262

63-
For two arrows pointing roughly the same way, the parallel error is what throws off the dot product between them. Perpendicular error mostly cancels out when you compute the dot product.
63+
Now split the error into two parts: one component pointing in the same direction as `x` (parallel), and one pointing sideways (perpendicular).
6464

65-
Standard quantization treats both error types equally. **Anisotropic vector quantization (AVQ)** — ScaNN's approach — penalizes parallel error more heavily during training. The result: the compressed vectors are more accurate in the direction that matters for ranking, at the cost of being slightly less accurate sideways.
65+
<div data-widget="scann-error"></div>
6666

67-
A concrete analogy: imagine rating restaurants by how much you'd enjoy them. You care a lot about cuisine type (Italian vs. Thai) and not much about how many plants are in the decor. Good compression for *you specifically* would preserve cuisine type precisely and be approximate on the plants. ScaNN does the same — it figures out which "directions" matter for inner product search and compresses to be accurate there.
67+
Step through the diagram above. The key observation at step 3: when `q` and `x` are similar (roughly aligned — which they must be for `x` to be a relevant result), the query `q` is approximately parallel to `x`. That means:
6868

69-
The practical effect: at the same compression ratio, AVQ produces better ranking of the top results than standard quantization does. You're not paying more — you're spending the same compression budget more wisely.
69+
- **Parallel error** `e∥` is in a direction `q` is sensitive to — it directly changes the dot product estimate.
70+
- **Perpendicular error** `e⊥` is in a direction `q` is mostly blind to — it largely cancels out.
7071

71-
## phase 3: rescoring — clean up the ranking
72+
Standard PQ treats both error types equally. **Anisotropic vector quantization (AVQ)** — ScaNN's approach — penalizes parallel error more during training. The quantizer learns codebooks that are more accurate in the direction that matters for ranking, accepting more perpendicular error in exchange.
7273

73-
Approximate scoring gives you a rough ranked list. It's good but not perfect — the item that should be rank 2 might have slipped to rank 8 due to compression error.
74+
Same compression ratio. Better ranking of the results that actually matter.
7475

75-
The fix is cheap: take the top-c candidates from phase 2 (say, the top 200), and compute their exact similarity scores. No compression, no shortcuts — just the real dot product. Rerank those 200 and return the final top 10.
76+
## phase 3: exact rescoring
7677

77-
This works because `c` is tiny compared to `N`. You did the hard work narrowing from 10 million to 200. Re-scoring 200 vectors exactly is fast. The cost is proportional to `c`, not `N`.
78+
After approximate scoring, you have a ranked shortlist of ~200 candidates. Quantization errors can scramble this list slightly — an item that should be rank 3 might appear at rank 11.
7879

79-
The full pipeline then looks like:
80+
The fix is straightforward: take those 200 candidates and compute their exact dot products. Rerank. Return top 10.
8081

81-
```
82-
10,000,000 vectors
83-
→ partition: score 3,000 centroids → pick top 100 clusters
84-
→ approximate score: ~300,000 vectors via fast table lookups → top 200
85-
→ exact rescore: 200 vectors → top 10 returned
86-
```
82+
This is fast because you're doing exact arithmetic on 200 vectors, not 10 million. The expensive part was finding those 200. Rescoring them is cheap — and it brings accuracy back close to brute-force quality.
8783

88-
Each step is much faster than the one before, and the final answer is nearly as accurate as searching everything exactly.
84+
The three phases form a cost pyramid:
8985

90-
## SOAR: the problem at the borders
86+
| phase | candidates | cost |
87+
|---|---|---|
88+
| centroid scoring | 3,000 centroids | cheap |
89+
| AQ scoring | ~300,000 vectors | fast (table lookups) |
90+
| exact rescoring | ~200 candidates | fast (small count) |
91+
| brute force | 10,000,000 vectors | too slow |
9192

92-
There's a subtle issue with single-cluster assignment. A vector sitting near the boundary between two clusters might belong to cluster A, but some queries that would love that vector happen to search only cluster B.
93+
## SOAR: the boundary problem
9394

94-
It's like a book about "spy thrillers" that could reasonably sit in either the Mystery section or the Thriller section. If the librarian files it in Mystery but a reader browsing Thrillers would have loved it, they never find it.
95+
There's a subtle failure mode in the partitioning phase. Each vector is assigned to exactly one cluster — its nearest centroid. But vectors near the boundary between two clusters can fall through the cracks.
9596

96-
SOAR (Google, NeurIPS 2023) fixes this by assigning borderline vectors to **two** clusters instead of one. The primary cluster is the nearest centroid as normal. The secondary cluster is chosen with a specific rule: pick the backup cluster such that its centroid is a good proxy for this vector *from the perspective of likely queries*.
97+
Imagine a product that's borderline between "running shoes" and "athletic footwear." Its true nearest centroid says "athletic footwear," but queries for running shoes mostly search the "footwear" cluster. If that cluster isn't in the top-t for the query, the product is never scored.
9798

98-
The math behind choosing the secondary cluster is what gives SOAR its name (Spilling with Orthogonality-Amplified Residuals), but the intuition is: find a backup neighborhood where queries that would want this item are likely to look. The vector gets a second chance to be found without you having to search more clusters.
99+
This is the boundary recall problem: a correct result gets missed because its assigned cluster wasn't selected, even though it's very similar to the query.
99100

100-
The cost: each borderline vector appears in two posting lists, so storage increases by up to 2x. The benefit: meaningfully better recall at the same search latency.
101+
**SOAR** (NeurIPS 2023) fixes this by assigning each boundary vector to *two* clusters instead of one. The secondary cluster is chosen so that its centroid is a good proxy for the vector from the perspective of likely queries. Specifically: the residual from the backup centroid should be nearly perpendicular to typical query directions — ensuring queries that would want this vector will find it via the backup cluster.
102+
103+
The cost is roughly 2× storage for boundary vectors. The benefit is meaningfully better recall without searching more clusters.
101104

102105
## pre-filtering: searching a subset
103106

104-
One last thing ScaNN handles cleanly: filters. "Find similar songs — but only from artists I follow."
107+
One more thing ScaNN handles cleanly. Suppose the user adds a filter: "red running shoes, under $100." You need similar products *and* matching the price constraint.
108+
109+
The naive approach (post-filter): find the 10 most similar products, then apply the filter. If only 1% of products are under $100, most results get discarded. You return 1 product instead of 10.
105110

106-
The naive approach (post-filtering) finds the 10 most similar songs from 100M, then checks if you follow the artist. If you follow 1% of artists, 9 of those 10 results get discarded. You return 1 song instead of 10.
111+
ScaNN supports filtering *during* scoring rather than after:
107112

108-
ScaNN can apply the filter *during* scoring instead of after. At the cluster level, it skips entire clusters that contain no songs from artists you follow. At the vector level, it skips individual vectors that don't match the filter before computing any approximate score.
113+
- **At the cluster level**: skip any cluster that contains zero products matching the filter. No quantized scoring at all for those clusters.
114+
- **At the vector level**: inside a selected cluster, skip any vector that fails the filter before computing its approximate dot product.
109115

110-
This works because ScaNN's phases are independent scoring steps — there's no graph to navigate, no requirement that all vectors be reachable. You can freely skip any vector and the remaining ones are still found correctly. Filtering just reduces work; it doesn't break anything structural in the index.
116+
This works cleanly because ScaNN's scoring is independent per vector — there's no graph to navigate, no connectivity to preserve. Skipping a vector just means not scoring it. The remaining vectors are still found correctly.
111117

112-
## the shape of the algorithm
118+
## putting it together
113119

114120
Each phase of ScaNN answers one question:
115121

116-
- **Partitioning***where* in the space should we look?
117-
- **Approximate scoring (AVQ)***how cheaply* can we score vectors there?
118-
- **Rescoring***how accurately* do we finalize the ranking?
122+
- **Partitioning** — where in the space should we look?
123+
- **AQ scoring** — how cheaply can we score candidates there?
124+
- **Rescoring** — how accurately do we finalize the ranking?
119125

120-
The interesting part is the compression. Anisotropic quantization isn't just an implementation detail — it reflects a real insight about what makes MIPS different from generic vector compression. The error that matters is the error in the direction the query is pointing. Spend your compression budget there.
126+
The interesting design is the compression. Anisotropic quantization isn't just an implementation tweak — it reflects a real asymmetry in similarity search: the error that corrupts ranking is directional. The error in the query's direction matters; error sideways mostly doesn't. Spending the compression budget to reduce directional error is the right trade, and it's why ScaNN consistently outperforms standard PQ on recall benchmarks at the same compression ratio.
121127

122128
— v

0 commit comments

Comments
 (0)