Express lanes — layers and ef_search
HNSW stacks skip-list-style layers — a few nodes promoted to sparse upper layers act as express lanes that cross huge distances in one hop before the search descends to the dense bottom layer for fine refinement — making search roughly logarithmic, while ef_search trades a wider, slower candidate beam for more recall until it plateaus.
A flat graph hops one short link at a time; HNSW's promoted upper layers are express lanes that cross the space in a few big hops, then descend for precise local steps — roughly logarithmic search, with ef_search as the recall-vs-latency dial. But all that speed and recall has a hidden bill: HNSW keeps every full vector AND every edge in memory.
Scene 07
Express lanes — layers and ef_search
- Watch
- Try it
- Predict
- Capture
Last scene the graph was flat — one short hop at a time. Picture searching a billion points that way: from a random start you'd take thousands of tiny steps just to cross the map. The fix is borrowed straight from a skip list. Most songs live only on the BOTTOM layer with their close local links. But a few are PROMOTED to a sparse TOP layer and joined by long edges — think of an express train that skips every local stop, gets you across town in one ride, and then you switch to the local for the last block. Watch the search enter at the top, take one big express hop into the query's region, drop down a layer, and finish with small careful hops — landing on Midnight Drive, Synth Dawn, and Lonely Synth, the query's true neighbors.
Highlighted lines are the ones running in the diagram right now.
def search(query, k):ep = entry_point # a promoted top-layer node# express lanes: greedy-hop down the sparse upper layers,# keeping just the single best node at each levelfor level in range(top_layer, 0, -1):ep = search_layer(query, ep, ef=1, level)# bottom layer: dense local refinement with the full beamcandidates = search_layer(query, ep, ef=ef_search, level=0)return candidates.nearest(k)
def search_layer(query, ep, ef, level):visited = {ep}candidates = pq([ep]) # frontier to explorefound = pq([ep]) # best ef so farwhile candidates:c = candidates.pop_nearest(query)if dist(c, query) > found.farthest_dist():break # nothing closer left to findfor n in neighbors(c, level):if n not in visited:visited.add(n)candidates.push(n); found.push(n)found.trim_to(ef) # keep only ef closestreturn found
def insert(node):# most nodes stay at level 0; a rare few are promoted high,# which is what builds the sparse upper express laneslevel = floor(-ln(rand()) * mL)ep = entry_pointfor lvl in range(top_layer, level, -1):ep = search_layer(node, ep, ef=1, lvl)for lvl in range(min(level, top_layer), -1, -1):nbrs = search_layer(node, ep, ef=ef_construction, lvl)node.link(select_M(nbrs), lvl) # M edges per levelif level > top_layer: entry_point = node
Where this sits in Build a vector database (Pinecone / Weaviate / pgvector style)
Scene 07 of 15, in the The graph act — HNSW: hop a proximity graph; express-lane layers and the ef_search knee.. Skip-list layers add express-lane nodes that cross huge distances in one hop, then descend for local refinement — roughly log search, with ef_search trading beam width for recall until it plateaus.
Up next. HNSW won the speed-and-recall corner, but it pays in RAM — full float32 vectors plus a graph of edges. At a billion 1536-dim vectors that's the 6 TB wall from scene 1, back again. So the next question is: can we shrink each vector from 6 KB to a hundred bytes without wrecking recall?
All 15 scenes in Build a vector database (Pinecone / Weaviate / pgvector style) · Every curriculum