NuclaDB
Design Decisions

Tuning HNSW: what the recall/latency curve looks like

The full recall@10/QPS sweep on SIFT-small, and why efSearch is a per-query parameter.

efSearch controls how large a candidate list HNSW keeps while searching. A higher ef means better recall at the cost of latency. NuclaDB exposes it as a per-query parameter (ef_search in the API, -ef on the CLI) instead of a fixed server-wide setting. Here's the curve that decision is based on.

The sweep, against Qdrant on the same dataset

10,000 base vectors, 100 queries, dim=128, SIFT-small:

efNuclaDB recall@10Qdrant recall@10NuclaDB QPSQdrant QPSNuclaDB RSSQdrant RSS
100.89601.00007432.43661.145.2 MB102.7 MB
200.96201.00006298.94522.645.2 MB102.7 MB
500.99701.00004932.95057.745.3 MB102.7 MB
1001.00001.00003675.84951.545.4 MB102.7 MB
2001.00001.00002367.55063.345.6 MB102.7 MB

Recall saturates by ef=50

Going from ef=50 (0.997 recall) to ef=200 (1.000 recall) buys +0.3% recall for more than 2x the latency cost. Past ef=50, it's mostly diminishing returns.

Where NuclaDB wins

At low ef (10 to 20), NuclaDB beats Qdrant's QPS outright: 7432 vs. 3661 at ef=10. RSS stays under half of Qdrant's across every ef tested. The recall gap at low ef is real (0.896 vs. Qdrant's 1.000), but so is the throughput and memory advantage that comes with it.

A methodology bug the benchmark caught

The first run of this benchmark showed Qdrant at a suspiciously perfect 1.0 recall at every ef, including ef=10, which was a red flag since exact recall at low ef isn't how HNSW behaves. The cause: Qdrant's default full_scan_threshold (10,000 KB) sits above this dataset's raw size (about 5,120 KB), so out of the box, Qdrant was silently doing exact brute-force search, not HNSW. That made the comparison exact-search-vs-HNSW instead of HNSW-vs-HNSW. It was fixed by forcing Qdrant's threshold down before re-running.

Why efSearch is per-query, not fixed

There's no single right ef. It depends on the dataset and the recall/latency tradeoff a given query needs. A server-wide default would force every caller onto the same tradeoff, so it's exposed per-query instead (ef_search: 0 falls back to a default), letting a caller trade recall for latency where it matters.

For the compression side of this tradeoff, see Product quantization cost.

On this page