The vector database showed a query hopping across a web of neighbor links instead of measuring every distance. This essay is about that web: how it is built one insertion at a time, why it has layers, and the single dial that trades speed for certainty. Every figure below runs a real search over real points; nothing is canned.
A graph of near neighbors has a flaw: if the query lands far from the entry point, the greedy walk crawls there in tiny local steps. The fix, and the H in HNSW (Hierarchical Navigable Small World), is to stack sparser copies of the graph on top. The top layer holds a handful of points with long links: highways. Each layer down is denser: avenues, then streets. Scrub the descent:
The graph is never computed from the whole dataset at once; it accretes. Each arriving vector searches the existing graph for its nearest neighbors, exactly as a query would, then links to them. Two settings shape the result: m, how many links each node keeps, and ef_construction, how wide the search is when a vector arrives. Drag through the construction:
One thing the figure hides: Elasticsearch does not keep one graph for the whole index. Like everything else in Lucene, vectors live in immutable segments, and each segment carries its own small graph. A query walks every segment's graph and merges the results. When segments merge, their graphs have to be rebuilt into one, reusing the largest as a head start. That rebuild is where vector indexing spends much of its CPU, and why bulk loads are faster with fewer, larger segments.
The walk keeps a short list of the best candidates it has seen so far, and keeps exploring until nothing it could still visit would make that list better. The length of the list is the knob every vector deployment ends up tuning. The HNSW paper calls it ef; in Elasticsearch it is num_candidates on the kNN query, set per shard, at least k, 1.5×k by default and at most 10,000. Too small, and the walk can settle in the wrong neighborhood and miss true neighbors. Larger, and it explores more paths at more cost. This figure runs a real beam search over 240 clustered points, on a graph built the HNSW way, and checks it against the exact answer, live:
One honest footnote on defaults: HNSW is no longer the only index Elasticsearch builds for vectors. Since 9.4, where the license allows it (an Enterprise subscription), new dense_vector fields default to bbq_disk, which groups vectors into clusters with a variant of k-means and searches only the closest clusters, instead of walking a graph. Without that license, the default is still an HNSW graph (bbq_hnsw from 384 dimensions, int8_hnsw below). Everything above is how that graph works.
Layers to cross space quickly, accretion instead of construction, one dial for the speed-certainty trade. What the walk actually compares, compressed vectors that keep comparisons honest at one bit per dimension, is back in The vector database; what the vectors mean is in Semantic search in practice.