fix(index): report distance comparisons from HNSW search - #8142
fix(index): report distance comparisons from HNSW search#8142wombatu-kun wants to merge 2 commits into
Conversation
There was a problem hiding this comment.
Gate recommendation: approve with a non-blocking risk. The implementation accounts for the entry point, every scored traversal candidate, exact flat-scan work, and both ACORN and fallback traversals. Exact graph-path totals remain source-audited rather than independently measured; the proposed deterministic calculator-backed regression would make that contract durable.
The public graph helper return types are source-breaking. Please mark this PR with the breaking-change label.
| for use_acorn in [false, true] { | ||
| let dense_comparisons = comparisons(masked(dense.clone()), use_acorn); | ||
| assert!( | ||
| dense_comparisons >= k, |
There was a problem hiding this comment.
The graph-path checks only require >= k, so they would still pass if the implementation reported a constant k; this case also does not force the ACORN under-delivery branch where two traversals must be summed. Please compare the metric with an independently counting DistCalculator on deterministic inputs, including a forced fallback, so missed or double-counted distance calls are caught.
There was a problem hiding this comment.
Gate recommendation: approve. The deterministic counting-storage oracle now matches reported distances across unfiltered, all-pass, sparse flat-scan, filtered basic, filtered ACORN, and forced ACORN-fallback searches, including the abandoned traversal’s work. This resolves the earlier evidence gap.
The public graph helper return types remain source-breaking. Please mark this PR with the breaking-change label.
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
98e1178 to
23ba384
Compare
|
Rebased onto current main. @BubbleCal mind taking a look when you get a chance? It is a small change, but the metric it adds is load-bearing: |
There was a problem hiding this comment.
✅ Gate recommendation: approve.
The rebase preserves the new base’s level-descent and entry-point semantics while retaining exact accounting across the main search paths, including forced ACORN fallback. No new acceptance risk was introduced.
The public graph helper return types remain source-breaking. Please mark this PR with the breaking-change label.
HNSW::searchtakes_metrics: &dyn MetricsCollectorand never touches it, so every HNSW query reports zero distance comparisons in per-query metrics whileFlatIndexreports real numbers for the same workload. That makes the comparison count useless for exactly the case it is most interesting in: judging whether a graph traversal is doing more work than a scan would.Change
The traversal primitives in
graph.rsnow return how many distances they computed alongside their results, andHNSW::searchsums the paths it actually took and reports the total in a singlerecord_comparisonscall.Counting happens where the distance is computed, so all four dispatch paths are covered: the unfiltered graph traversal, the filtered one, the ACORN traversal, and the exact flat scan. Two details worth calling out:
count_ones().The entry point distance is computed outside the traversal, so it is added explicitly rather than being silently dropped.
Cost
The hot loops gain one
usizeincrement per distance computation, which is the accumulate-locally-report-once shape the other index types already use (wand.rsdoes the same with itscomparisonsfield). Index construction is unaffected: the build path goes throughbeam_searchandgreedy_search, the query path through the_borrowedvariants andbeam_search_acorn, and the two sets are disjoint.search_basic,search_acorn, andsearch_innerkeep their existing signatures, since they are public and have callers in the benches and examples. The counted variants sit underneath them.Tests
test_search_reports_distance_comparisonscovers all four dispatch paths. The sparse-mask case asserts an exact count rather than merely a non-zero one, since the flat scan's work is known up front. The test fails onmain(every path reports 0) and passes here.MaskPreFiltermoved from insidetest_subindex_prefilter_dispatchto the enclosing test module so both tests can use it.