fix(hnsw): search silently omitted stored rows (#773), plus post-merge follow-ups - #791
Open
ruvnet wants to merge 1 commit into
Open
fix(hnsw): search silently omitted stored rows (#773), plus post-merge follow-ups#791ruvnet wants to merge 1 commit into
ruvnet wants to merge 1 commit into
Conversation
Two distinct defects, both in the insert path, both leaving points that no efSearch could ever reach again. FIRST, an early return treated points_by_layer[layer] as the membership of that layer. It is not: that list records points whose MAXIMUM level is the layer, while a point of level L participates in every layer 0..=L. So an empty list did not mean an empty layer, and bailing out made insert link the new point to nothing there. Two points both entering at level 1 ended up with no layer-0 edges at all. SECOND, the reciprocal edge was filed under the new point own level instead of the layer being linked. Malkov Algorithm 1 says bidirectional connections go at layer lc. Using the point level meant a point entering at level >= 1 had its layer-0 back-edges recorded on an upper layer, so it held no in-edge on layer 0. The moment it stopped being the graph entry point, nothing on layer 0 pointed at it and search dropped it permanently. VERIFIED AS A NEGATIVE CONTROL, not merely observed green: reverting the patch makes 3 of the 4 guards fail with "VectorDB::search omitted stored rows in 3/30 trials". Restoring it returns them to green. A regression test that has never been shown to fail is not a regression test. The long-running scan, ignored by default and run once here, measures the invariant rather than asserting it: 0 of 25,000 trials returned an incomplete result set across five graph sizes, and a 12,000-point parallel build leaves no orphans. Also carried in this working tree: rvf re-ingest of a deleted identifier now makes it live again (issue #748). The tombstone described the vector the batch replaces, not the identifier, so the bitmap is cleared before the record is built and rolled back on every error path — otherwise the new vector was queryable but reclaimed as dead by the next compact. 562 Rust tests green across the blast radius: ruvector-core 188, rvf-runtime 334, rvf-integration 40. Co-Authored-By: claude-flow <ruv@ruv.net>
This was referenced Aug 4, 2026
Flaky: recall_trigger_holds_under_no_drift asserts a hard threshold on an unseeded random graph
#792
Open
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Rebuilt cleanly on top of
mainrather than mergingfeat/implement-adr-273-275, which conflicted in 24 hunks across 5 files — almost all of it re-litigating contentmainalready had from #774's squash merge. Verified equivalent: the only difference between this branch and the old one is 16 compiled NAPI binaries, 0 insertions and 0 deletions of source, andmain's binaries are the newer ones.The defect (#773)
search()silently omitted stored rows. Two distinct bugs in the insert path, both leaving points noefSearchcould ever reach:points_by_layer[layer]as that layer's membership. It isn't — that list records points whose maximum level is the layer, while a point of level L participates in every layer 0..=L. Bailing out linked the new point to nothing there, so two points both entering at level 1 got no layer-0 edges at all.lc. Using the point's level meant layer-0 back-edges landed on an upper layer, so the point held no in-edge on layer 0 — and the moment it stopped being the entry point, search dropped it permanently.Verified as a negative control
Reverting the patch makes 3 of the 4 guards fail with
VectorDB::search omitted stored rows in 3/30 trials; restoring it returns them to green. A regression test that has never been shown to fail is not a regression test.The long-running scan (ignored by default, run once here) measures the invariant rather than asserting it: 0 of 25,000 trials returned an incomplete result set across five graph sizes, and a 12,000-point parallel build leaves no orphans.
Also included
compact().Tests
562 Rust tests green across the blast radius: ruvector-core 188, rvf-runtime 334, rvf-integration 40.
🤖 Generated with claude-flow
https://claude.ai/code/session_01WfyBMvexdN4bhWVaqSXKvR