← cosmin bararu

Sweeping the curve instead of picking a point_

After rebuilding Astral's graph, recall reached 99.22%. The cost was 8.80 milliseconds per query at the largest search budget I tested.

One passing result was not enough to choose a setting. I measured the full recall-latency curve to find out whether a cheaper budget could still hold 99%.

Sweeping the curve

I swept efSearch, the size of the candidate pool carried through an HNSW query. A larger pool usually visits more candidates and takes longer. The table keeps four exact rows from the ten measurements, while the figure shows the full curve.

recalllatency per query
69.14%1.78 ms
96.1%5.68 ms
98.8%8.0 ms
99.22%8.80 ms

The sweep runs from a budget of 512 to 4096. Near the top, a budget of 3840 repeated the previous 98.8% recall at about 8.4 milliseconds instead of 8.0. Early in the sweep, roughly tripling latency added nearly 27 recall points. The final 0.8 milliseconds added less than half a point.

RECALL vs LATENCY, 100K SWEEP ten-point budget sweep at 100k vectors recall 99% floor 100% 75% 50% 25% 0% 0 2 4 6 8 10 per-query latency (ms) 1.78 ms → 69.14% 96.1% at 5.68 ms 99.22% recall at 8.80 ms (budget 4096) the only point that clears the 99% floor
the 100k sweep: a steep climb, a knee, then flattening

The curve flattened near the 99% target. This graph cleared it only at a budget of 4096, where recall reached 99.22% and each query took 8.80 milliseconds. That was too slow for the retrieval path.

This result is specific to this index, data, machine, and harness.

Diminishing returns at both scales

Two earlier changes had passed at 10,000 vectors and failed at 100,000, so I swept the rebuilt graph at 10,000 too. It ran from 50% recall at about 34 microseconds to 100% between 539 and 684 microseconds. Both curves showed large early gains followed by diminishing returns, but their useful settings were different.

DIMINISHING RETURNS, TWO SCALES 10K SCALE 100 50 0 0 200 400 600 50% at 34 us 100% at 610 us latency (us), 10k 100K SCALE 100 50 0 0 2 4 6 8 10 69% at 1.78 ms 99.2% at 8.8 ms latency (ms), 100k both curves flatten. Their independent axes prevent a stronger comparison.
both scales show diminishing returns, with no claim that their knees coincide

Both scales flattened, but their useful settings were different, so I chose the operating point from the 100,000-vector fixture.

Connectivity, measured twice

Clearing 99% still cost 8.80 milliseconds per query. On the rebuilt graph, I doubled connectivity across every layer. The query budget needed to hold the recall floor fell from 4096 to 1536, a 2.67x reduction.

An earlier test on the old graph had doubled only level-zero connectivity. Recall fell to 50% at 100,000 vectors and latency increased. The later test preserved the ratio between layers, and the builder itself had changed. The two results are not a controlled comparison. They show that the old graph did not predict the rebuilt one.

The documented starting point for 100k at 384 dimensions moved from 32 neighbors, 64 build-time search, and 4096 query-time search to 64 neighbors, 128 build-time search, and 1536 query-time search. The required efSearch budget fell by 2.67x. I did not record visited-node counts, so that ratio applies to the configured budget, not execution work or latency.

CONNECTIVITY ON TWO GRAPH BUILDS ON THE BROKEN GRAPH double base connectivity 100 50 0 100% 50% before after doubling halved recall, 100% to 50% ON THE REBUILT GRAPH double connectivity → 2.67× lower efSearch 4096 2048 0 4096 1536 before after budget 4096 to 1536, still clears 99% different graph builds and different connectivity changes produce different results
the old graph rejected a base-layer-only change. The rebuilt graph benefited from uniform doubling.

Budgets split by storage format

The rebuilt index can route with int8 scores and rerank the final candidates in float32. Routing only needs the true neighbors to appear somewhere in the pool. Reranking needs a wider pool so float32 has enough candidates to reorder. Once routing has found the right neighbors, making its pool larger adds little.

The current defaults give reranked formats three times the query budget of plain quantized storage. I did not run a 1x-to-4x sweep, so 3x is a starting configuration rather than a proven optimum. A new format still needs its own sweep.

The current starting point

This is a starting point for this fixture: 64 neighbors, 128 build search, and 1536 query search at 100,000 vectors and 384 dimensions. Other data and scales need fresh sweeps.

Some queries still miss true neighbors at every measured budget, and I have not yet inspected what they share. The 10k curve did not predict the 100k curve, so the million-vector index starts with a fresh sweep too.