← All work
Systems/ANN · from scratch/2024 — 2025/Built

HNSW Vector Search

Hierarchical Navigable Small World indexing built by hand: multilayer graphs, greedy descent, heuristic pruning, custom distance kernels, persistence and recall-vs-latency benchmarks.

PythonC++Graph indexBenchmarks
Personal systems work
01

The problem

I had used vector databases without understanding what the index actually did or why recall degraded when parameters changed. Building HNSW from the paper — no library — was the way to find out where the accuracy and the latency really come from.

02

Who uses it

2 groups

Me, as the reader of my own retrieval stack

Know what ef, M and layer assignment actually trade against each other before tuning a production index.

Any RAG system I build

A defensible answer to 'why this recall at this latency' instead of vendor defaults.

03

Workflow

6 stages
01Assign layer
02Greedy descent from entry point
03Beam search at base layer
04Heuristic neighbour prune
05Bidirectional link
06Persist & benchmark
01 · LayerTriggerNew vector insertedSystemExponentially decaying random level assignmentOutputA top layer for the node; most nodes live only at the base
02 · DescendTriggerInsert or query beginsSystemGreedy search from the entry point, one layer at a timeOutputA good entry node handed down to the next layer
03 · SearchTriggerBase layer reachedSystemBeam search with a candidate heap of width efOutputef nearest candidates; ef is the recall/latency dial
04 · PruneTriggerCandidates selected for a new nodeSystemNeighbour-selection heuristic rather than plain top-MOutputUp to M diverse neighbours, keeping the graph navigable
05 · LinkTriggerNeighbours chosenSystemBidirectional edges with degree repair on the other sideOutputConnectivity maintained without unbounded node degree
06 · MeasureTriggerIndex built or parameters changedSystemRecall@k against brute force, latency percentilesOutputA recall-vs-latency curve per parameter set
04

How it was built

6 parts

Multilayer graph

Each node gets a maximum layer from an exponentially decaying distribution: a few nodes sit high and act as long-range hubs, most exist only at the base. Search starts at the sparse top and descends, so the upper layers do the coarse travel and the base does the fine work.

Greedy descent, then beam search

At every layer above the base, search is plain greedy — move to a closer neighbour until none is closer, then drop down. At the base it becomes a beam search with a candidate heap of width ef. That single parameter is the recall/latency dial, and seeing it move the curve made it concrete.

Neighbour selection heuristic

Taking the M nearest neighbours produces clustered edges and a graph that traps greedy search in local minima. The heuristic keeps a candidate only if it is closer to the new node than to any already-selected neighbour, which preserves diverse directions and keeps long-range navigability.

Custom distance kernels

Cosine and Euclidean written directly, with vectors pre-normalised where the metric allows it so the inner loop is a dot product. Distance calls are the hot path — counting them per query turned out to be more informative than wall-clock timing alone.

Persistence

The graph is serialised — vectors, adjacency per layer, entry point, parameters — and loaded back without a rebuild, so a benchmark run does not start with an index build.

Benchmarks

Recall@k measured against brute-force ground truth, with latency percentiles across M, ef_construction and ef_search. The output is a curve, not a single number: for this dataset, this is what each parameter buys and what it costs.

05

What it is built with

Implementation

  • Python
  • C++
  • NumPy

Index

  • HNSW
  • Multilayer graph
  • Beam search
  • Heuristic pruning

Metrics

  • Cosine
  • Euclidean
  • Recall@k
  • Latency percentiles
06

Outcome