How do you build a reproducible method for tuning HNSW instead of guessing?

Created: Updated: 4 min read

A reproducible method for tuning HNSW replaces guesswork with a structured procedure: fix what “correct” means before touching any parameter, sweep the parameters in the order they actually depend on each other, and judge the results as a trade-off curve rather than a single number, so the final choice reflects a genuine measurement rather than a value that happened to work well for somebody else’s dataset.

What has to be fixed before any parameter sweep starts?

Before adjusting a single parameter, the target k and the distance metric being used both need to be nailed down, since every later recall measurement is only meaningful relative to a specific choice of both. From there, an exact ground truth needs to be computed — the true top-k nearest neighbors for a representative set of test queries, found through brute-force search over the same dataset and the same metric the approximate index will actually use. This ground truth is what every later recall measurement gets compared against, and it needs to be trustworthy before anything built on top of it can be. The query sample used for this should genuinely represent the queries the system will face in practice; tuning against an unrepresentative sample — one drawn from a different distribution than real queries, or too small to capture how difficult the workload’s harder queries actually are — produces parameter choices that look great in testing and disappoint once deployed.

In what order should the three parameters actually be swept?

Because M shapes the graph and efConstruction determines how well that graph gets built, these two need to be explored together rather than in isolation — for each candidate value of M under consideration, a range of efConstruction values should be tried, since the right efConstruction for a small M is not necessarily the right one for a much larger M. Only once a specific graph has actually been built, for one particular combination of M and efConstruction, does it make sense to sweep efSearch, since efSearch is evaluated at query time against a completed, fixed graph and doesn’t require rebuilding anything to test a new value. This ordering reflects which parameters are baked into the graph permanently and which ones are freely adjustable afterward, and following it avoids wasted effort re-testing efSearch values against graphs that are about to be discarded anyway because their M and efConstruction combination turned out to be a poor one.

How do you turn a pile of individual results into a real decision?

Every parameter combination tested produces a recall number and a latency number, and plotting these together as a recall-versus-latency curve — commonly called a Pareto front — reveals which combinations are actually worth considering: for any given latency budget, only the combination achieving the highest recall at or below that budget is relevant, and every other combination tested is dominated by it. This is a meaningfully better way to make a decision than picking whichever single configuration happened to score highest on one metric in isolation, since it directly shows the trade-off being accepted rather than hiding it behind a single chosen number. Memory is worth adding as a third dimension to this same picture wherever it’s a real constraint, since a configuration sitting right on the recall-latency frontier can still be the wrong practical choice if its memory footprint doesn’t fit the hardware actually available to run it.

Why does a single build and a single set of test queries not settle the question?

Random level assignment and, in a multithreaded build, nondeterministic insertion ordering both mean that two builds of the same dataset with identical parameters can produce graphs that behave slightly differently, so a tuning result based on a single build risks mistaking ordinary build-to-build variance for a genuine difference between parameter settings. Repeating key measurements across a handful of different random seeds or insertion orders, and reporting the resulting spread rather than a single number, gives a much more honest picture of how reliable a given result actually is. It’s also worth validating a chosen configuration against queries drawn from a different distribution than the tuning set, wherever that’s a realistic risk — a system tuned entirely against one snapshot of query traffic can behave worse than expected once real usage patterns shift in ways the tuning process never saw.

With a genuine measurement procedure in place, the final step is simply picking a point on the resulting trade-off curve based on an actual service-level objective — a maximum acceptable latency, a minimum required recall, or a memory ceiling — rather than picking whichever configuration looks most impressive in isolation. The next page moves past parameter tuning to structural optimizations that improve HNSW’s performance without changing M, efConstruction, or efSearch at all.