HNSW layers
Trace IVF and HNSW search and explain how candidate exploration trades latency for recall.
THE IDEA
HNSW layers
HNSW stacks graphs in layers. Each node gets a top layer at random so that about 1/M of the nodes reach layer 1, 1/M² reach layer 2, and so on. Search starts at the sparse top, makes long hops, drops a layer at a time, and finishes with a detailed beam search of width efSearch on the dense bottom layer. Build parameters (such as M) affect connectivity and memory; efSearch affects search breadth and latency.
| layer ≥ | chance for a node | expected nodes |
|---|---|---|
| 0 | 1 | 1000000 |
| 1 | 0.0625 | 62500 |
| 2 | 0.003906 | 3906.25 |
| 3 | 0.000244 | 244.14 |
Predict first. Is efSearch the same setting as the number of returned results?
Chapter context
An approximate index organizes vectors so a query can avoid most comparisons. Clustering and graph navigation use different mechanisms to choose candidates, and their build settings influence later search quality.
TRY IT YOURSELF
Part 1. Do it hands-on
Compute layer populations by hand, then write level assignment and a multi-layer search.
| layer ≥ | chance for a node | expected nodes |
|---|---|---|
| 0 | 1 | 1000000 |
| 1 | 0.0625 | 62500 |
| 2 | 0.003906 | 3906.25 |
| 3 | 0.000244 | 244.14 |
- To do: HNSW draws each node's top layer from a geometric-like distribution: the chance of reaching layer 1 or higher is 1/M. With M = 16, that probability (4 decimals)?
- To do: Out of 1,000,000 nodes, about how many appear on layer 1 or higher?
- To do: On layer 2 or higher (10⁶ / 16², two decimals)?
- To do: efSearch is the same setting as the number of results returned. Type yes or no
Part 2. Do it in code
- level_for(u, M): the top layer of a node given a uniform random number u in (0, 1): floor(−ln(u) / ln(M)). u outside (0, 1) raises ValueError.
- expected_at_least(n, M, layer): the expected number of nodes on that layer or above: n / M**layer.
- greedy_layer(graph, pos, q, entry): move to a strictly closer neighbour until none is closer; return the final node. (Provided: beam_search below does the base-layer search.)
- search_layers(layers, pos, q, entry, ef, k): layers[0] is the base graph with every node, higher layers are sparser. Descend from the top layer to layer 1 with greedy_layer, passing the result as the entry of the next layer down, then run beam_search on layer 0 with ef = max(ef, k) and return the first k ids.
Stuck, or something not working? You can come back to it any time.
CHECK YOUR UNDERSTANDING
Is efSearch the same setting as the number of returned results?
Chapter reference: math, worked example & code
This section belongs to the whole chapter, so it is the same on every lesson in it and may cover a different lesson's topic. The code for the experiment you just ran is inside the experiment itself.
Math and notation
Under the simplifying assumption of equally sized IVF lists, N vectors, C cells and p probes produce approximately
Add coarse scoring, uneven list sizes and any reranking costs separately. HNSW has no equivalent universal fixed candidate-count formula.
Worked example
For 102,400 vectors evenly divided among 1024 cells, a cell has 100 vectors. Probing eight cells scans about 800 candidates, compared with 102,400 in a full scan. This excludes coarse-centroid comparisons and says nothing by itself about recall.
Reference code
The following example uses the Python standard library and does not call a remote service.
total,cells,probes=102400,1024,8
assert total/cells == 100
assert total*probes/cells == 800
assert total/(total*probes/cells) == 128
Python examples are provided for study and local execution; this page does not execute Python.
Common mistakes
An average candidate count is not an actual worst-case bound. More graph edges consume memory. Approximate search settings must be evaluated against the current corpus and metric.