Skip to content

fix: replenish() hardcodes CosineDistance instead of graph distance#21

Merged
ammario merged 2 commits into
coder:mainfrom
suykerbuyk:fix/replenish-distance
Jun 22, 2026
Merged

fix: replenish() hardcodes CosineDistance instead of graph distance#21
ammario merged 2 commits into
coder:mainfrom
suykerbuyk:fix/replenish-distance

Conversation

@suykerbuyk

Copy link
Copy Markdown
Contributor

Summary

  • replenish() unconditionally called addNeighbor(..., CosineDistance) regardless of the graph's configured distance function
  • This corrupted graph topology for Euclidean or custom metrics
  • Fix: thread DistanceFunc through replenish() and isolate() so the correct function is always used

Depends on #20.

Test plan

  • Added TestGraph_DeleteReplenishUsesGraphDistance — builds a Euclidean graph, deletes a node to trigger replenish, verifies search still returns the correct nearest neighbor
  • go test ./... and go vet ./... pass

John Suykerbuyk added 2 commits April 11, 2026 12:26
In a binary min-heap, the last array element is not necessarily the
maximum. Max() must scan the leaf nodes (indices n/2..n-1) to find
the true maximum. PopLast() used Remove(Len()-1) which removed an
arbitrary element instead of the worst.

This caused incorrect evictions during neighbor selection and search
result trimming, degrading graph quality.
…ance

replenish() is called after neighbor eviction and node deletion to
restore connectivity. It unconditionally used CosineDistance, ignoring
the graph's configured distance function. This corrupted the graph
topology for any non-Cosine metric.

Thread DistanceFunc through replenish() and isolate() so the correct
distance function is always used.
@suykerbuyk

Copy link
Copy Markdown
Contributor Author

Friendly ping @ammario — part of the recall-fix trio with #20 (root cause) and #22. Small and independently mergeable; happy to rebase or adjust if anything's needed.

@ammario ammario left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct fix: threads the graph's DistanceFunc through replenish()/isolate() instead of hardcoding CosineDistance. Tests pass.

@ammario ammario merged commit bc94177 into coder:main Jun 22, 2026
1 check passed
ammario added a commit to suykerbuyk/hnsw that referenced this pull request Jun 22, 2026
Heap fix (coder#20) and replenish distance fix (coder#21) are already present in
this branch; they landed on main via rebase, so absorb main with the
'ours' strategy to clear the duplicate-commit conflict. Tree is unchanged.
ammario pushed a commit that referenced this pull request Jun 22, 2026
Rewrites layerNode.search() to follow HNSW Algorithm 5 (SEARCH-LAYER):
the result set is bounded by efSearch during exploration, termination
uses the worst-of-results condition, and only the k closest are returned
(with deterministic key tiebreaking).

This fixes severely degraded recall: on a 2000-point, 16-dim Euclidean
benchmark, recall@1 improves from 0.20 to 1.00 and recall@10 from 0.175
to 0.998.

Also clamps efSearch to max(k, efSearch) so Search returns up to k
results even when k exceeds the configured EfSearch.

Builds on #20 (heap Max/PopLast fix) and #21 (replenish distance fix).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants