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.
| recall | latency 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.
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.
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.
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.