ANN indexing: HNSW and IVF
Two ideas. IVF clusters the vectors and searches only the nearest few clusters. HNSW builds a layered graph — sparse at the top for long hops, dense at the bottom for precision — and walks greedily downwards. Both skip most of the corpus; they differ in how they choose what to skip.
Overview
HNSW: skip lists, in vector space
Take a proximity graph — each vector linked to its nearest neighbours — and stack several, each a random sample of the one below. Search enters at the sparse top layer and greedily moves to whichever neighbour is closer to the query, until no neighbour improves. Then it drops a layer and repeats.
The top layers have long edges, so a few hops cross most of the space; the bottom layer has short edges, so the final steps are precise. It is the skip-list idea applied to geometry, and it is the reason HNSW dominates: excellent recall at low latency.
Parameters
Visualisation
—Readout
What to watch
- The top layer covers distance; the bottom layer covers precision.
- Search is greedy — it can settle in a local minimum and miss.
efSearchwidens the beam: better recall, more time.
ANN indexing: HNSW and IVF: A Practical Guide
How does approximate nearest neighbour search actually work? Explain HNSW.
The parameters worth naming
M — edges per node, fixed at build time. Higher means a better-connected graph, better recall, more memory. The graph itself is a real memory cost on top of the vectors, which is HNSW's main drawback.
efConstruction — how hard the builder works to find good neighbours. Build-time only: slower indexing, better graph forever.
efSearch — the size of the candidate list during a query. The runtime knob: raise it for recall, lower it for latency, per query if you like. This is the one to mention, because it is the one you actually tune in production.
How each one fails
HNSW's search is greedy, so it can settle in a local minimum and return a neighbourhood that is good but not the best. A wider efSearch makes that less likely without eliminating it. Deletion is also awkward — removing a node can disconnect the graph — so most implementations tombstone and rebuild periodically.
IVF's failure is cleaner: a true neighbour sitting just across a cluster boundary is missed unless that cluster is probed. More probes fixes it and costs latency. IVF is cheaper to build and to update, which is why it survives alongside HNSW — and combined with product quantization it is what runs when memory, not latency, is the binding constraint.
Product quantization, and when memory is the constraint
HNSW keeps every vector in memory plus the graph, so a million 768-dimensional float32 vectors is about 3 GB before the graph. Past some scale that, not latency, is what stops you.
Product quantization splits each vector into subvectors, clusters each subspace, and stores the cluster ids instead of the values — typically a 10–30× reduction. Distances are then computed against the codes using a precomputed lookup table, which is also faster.
The cost is precision: distances become approximate on top of the search already being approximate, so recall drops. The standard mitigation is rerank with the real vectors — retrieve a generous candidate set from the compressed index, then rescore the top few hundred against the originals held on disk. IVF-PQ with reranking is what most billion-scale deployments actually run.
Choosing between them, and what to measure
HNSW when the index fits in memory and query latency matters most. Best recall-for-latency, worst memory, awkward deletes.
IVF when builds and updates need to be cheap, or as the base for quantization. Simpler to reason about; recall depends on probing enough clusters.
IVF-PQ when memory is binding. Accept lower raw recall and recover it with a rerank pass.
Flat when the corpus is small or a miss is a correctness bug. Under a hundred thousand vectors, a brute-force scan is often a few milliseconds and needs no tuning at all — which is worth checking before adopting anything else.
Measure on your own data. Recall/latency curves depend on the intrinsic dimensionality and clustering of your embeddings, and published benchmarks on academic datasets transfer poorly.
Why exact search stops working
Finding the nearest vectors exactly means comparing the query against every stored vector. With a million 768-dimensional vectors that is 768 million multiply-adds per query — fine in a batch job, too slow for an interactive request, and hopeless at ten million.
Approximate nearest neighbour indexes trade a small amount of recall for orders of magnitude of speed. "Approximate" means they may miss a true nearest neighbour occasionally; in practice, well-tuned indexes return 95–99% of them, and the misses rarely change what a user sees.
| Index | Structure | Build cost | Query speed | Memory |
|---|---|---|---|---|
| Flat | None — brute force | Zero | Slow | Vectors only |
| IVF | Clusters | Moderate, needs training | Fast | Vectors + centroids |
| HNSW | Layered graph | High | Very fast | Vectors + graph links |
| IVF+PQ | Clusters + compression | High | Fast | Much smaller |
IVF: search only nearby clusters
The idea is a library organised into sections.
Build: run k-means over the vectors to find, say, 4,096 centroids. Assign every vector to its nearest centroid, producing an inverted list per cluster.
Query: compare the query against the 4,096 centroids, pick the closest nprobe of them, and search only the vectors in those lists.
With nprobe = 8 out of 4,096 clusters, roughly 0.2% of the vectors are examined. The speed-up is proportional.
The tunable is nprobe, and it is a direct recall-versus-latency dial:
nprobe | Recall | Latency |
|---|---|---|
| 1 | Low — misses vectors near cluster edges | Fastest |
| 8–16 | Good | Fast |
| 64+ | Approaching exact | Slower |
The failure mode is inherent: a vector just across a cluster boundary from the query is missed unless that cluster is probed. Higher nprobe reduces it.
IVF requires a training step on a representative sample before vectors can be added, which is the main operational difference from HNSW.
HNSW: a navigable graph
HNSW builds a multi-layer graph where each vector links to its nearest neighbours. Upper layers are sparse and connect distant regions; lower layers are dense and local.
Query: start at an entry point in the top layer, greedily move to the neighbour closest to the query, and when no neighbour is closer, drop to the next layer down and repeat. The top layers cover distance quickly; the bottom layer refines.
The result is search time growing logarithmically rather than linearly with the number of vectors, and recall that is typically higher than IVF at the same latency.
Three parameters:
| Parameter | Controls | Typical |
|---|---|---|
M | Links per node | 16–48 |
ef_construction | Search breadth while building | 100–400 |
ef_search | Search breadth at query time | 50–200 |
M and ef_construction are build-time and fix the index's quality ceiling; raising them costs build time and memory. ef_search is the query-time dial, adjustable per request — higher for better recall, lower for lower latency.
The cost is memory. The graph links themselves consume roughly M × 2 × 4 bytes per vector on top of the vectors, so an HNSW index is often 1.5–2× the size of the raw vectors.
Building an IVF index and measuring what it misses
The sections above describe how IVF partitions the space and why exact search stops scaling. Here it is built and measured -- a real index over real vectors, with the recall it loses and the work it saves at every setting of nprobe.
Things to try
- Sweep efSearch from 8 to 512. Recall and latency both climb — this is the knob you tune in production, and it can differ per query.
- Drop M to 4. Recall is capped no matter how large efSearch gets: build-time damage cannot be repaired at query time.
- Raise M to 48 and watch the index memory. The graph is stored alongside the vectors, which is HNSW's main cost.
What to remember
HNSW stacks proximity graphs: sparse upper layers with long edges to cross the space, a dense bottom layer to refine. Search is greedy with a candidate list of width efSearch, which is the runtime recall/latency knob. M and efConstruction are fixed at build time. IVF is the simpler alternative — cluster then probe — cheaper to build and update, and it misses neighbours across cluster boundaries.
Compression: product quantisation
Both index types can be combined with compression, which is what makes very large collections affordable.
Product quantisation splits each vector into sub-vectors, clusters each sub-space separately, and stores only the cluster ids. A 768-dimensional float32 vector (3,072 bytes) split into 96 sub-vectors with 256 centroids each becomes 96 bytes — a 32-fold reduction.
Distances are then computed approximately from precomputed lookup tables, without decompressing.
The cost is accuracy: the stored representation is lossy. The standard mitigation is re-ranking — use the compressed index to find 100 candidates cheaply, then rescore those against their full-precision vectors, which are fetched from disk.
| Memory per vector | Recall | |
|---|---|---|
| Flat float32 | 3,072 bytes | 100% |
| Flat float16 | 1,536 bytes | ~100% |
| PQ, 96 bytes | 96 bytes | 85–95% alone |
| PQ + full-precision rerank | 96 bytes in RAM | 95–99% |
Choosing an index by scale
| Vectors | Reasonable choice |
|---|---|
| Under 100k | Flat — exact, simple, fast enough |
| 100k–10M | HNSW — best quality per millisecond |
| 10M–100M | IVF, or HNSW with compression |
| Over 100M | IVF+PQ, sharded across machines |
The first row is worth stating plainly because it saves a great deal of unnecessary infrastructure: below about 100,000 vectors, a NumPy array and a dot product is genuinely adequate. A million-vector brute-force search takes tens of milliseconds on a modern CPU with a good BLAS.
Operational differences that matter as much as the benchmarks:
Insertions. HNSW supports incremental additions well. IVF needs its centroids trained first, and quality degrades if the distribution shifts away from the training sample.
Deletions. Both handle them poorly — usually via tombstones, with periodic rebuilds to reclaim space and quality.
Filtered search. Applying a metadata filter can defeat the index structure, since the graph or clusters were built over everything. Modern vector databases implement filter-aware traversal; where unavailable, over-retrieve and filter afterwards.
Measuring recall
Approximate means you should know how approximate. The measurement is straightforward:
# ground truth from exact search on a sample of queries
exact = brute_force_top_k(queries, vectors, k=10)
approx = index.search(queries, k=10)
recall = sum(len(set(a) & set(e)) for a, e in zip(approx, exact)) / (10 * len(queries))Run that for several settings of ef_search or nprobe and plot recall against latency. The resulting curve is what the parameter should be chosen from — and the shape is usually a sharp rise followed by a long flat region, so there is a clear point past which extra latency buys nothing.
Questions people ask
HNSW or IVF? HNSW for quality per millisecond and easy insertions; IVF when memory is tight or the collection is very large.
What recall should I target? 95–99% for most applications. The difference is rarely visible to users, and the latency saving is large.
Does approximate search hurt RAG quality? Marginally, and far less than poor chunking or a missing reranker. It is not where the quality problems are.
How much memory will I need? Roughly vectors × dimensions × bytes_per_value, times 1.5–2 for HNSW's graph. Compression reduces the first term substantially.
Can I change parameters after building? ef_search and nprobe yes, per query. M and the cluster count require a rebuild.
Do I need a vector database? For under 100k vectors, no. Above that, one earns its place — as much for filtering, persistence and updates as for the index itself.
Recap in one screen
- Exact search is linear and stops being interactive past a few hundred thousand vectors.
- IVF clusters the space and searches only the nearest clusters;
nprobetrades recall for speed. - HNSW walks a layered neighbour graph, giving logarithmic search and high recall at 1.5–2× the memory.
- Product quantisation compresses vectors 30-fold, with a full-precision rerank to recover accuracy.
- Measure recall against exact search and pick parameters from the recall-latency curve.