Skip to content

Apply the neighbour heuristic incrementally on full back-links (1.4–2.9× faster build) - #28

Merged
matengo merged 1 commit into
masterfrom
insert-prune-incremental
Sep 16, 2026
Merged

matengo merged 1 commit into
masterfrom
insert-prune-incremental

Conversation

@matengo

@matengo matengo commented Sep 16, 2026

Copy link
Copy Markdown
Owner

Summary

Replaces the O(M0²) re-run of the neighbour-selection heuristic on every full back-link with Lucene's incremental findWorstNonDiverse (O(M0)), falling back to the full heuristic only when the new node makes nothing redundant (0.1–0.7 % of cases — needed because our lists are topped up with pruned candidates, unlike Lucene's; without it 7/200 nodes went unreachable in an existing test). Also removes LINQ from SelectNeighborsHeuristic. Design doc: docs/design-insert-prune.md.

Measurements (same machine, same seeds)

dataset build before → after inserts/s recall change
siftsmall ~7–10 s → 4.0 s ~1,000–1,500 → 2,485 none
Cohere 100K (768d) 1,696 s → 589 s 59 → 170 (2.9×) recall@100 −0.3 pp
SIFT-1M, old graph rebuilt same day 2,279 s → 1,631 s 439 → 613 (1.4×) recall@10 −1.5 pp @ ef10, −0.3 @ ef80, 0 @ ef320

QPS at a given efSearch is unchanged within ±5 % run-to-run noise on both datasets (measured back to back with --reuse-index). The README SIFT-1M table is updated with the new graph's numbers and states the trade-off.

Tests

  • New NeighborDiversityTests: three hand-checkable geometric cases (reject redundant new node / evict the neighbour it makes redundant / evict farthest when all diverse) + well-formedness sweep under all three metrics. They pass against both the old and new implementation — they pin the heuristic, not the shortcut.
  • RawGraph shared out of GraphFanoutTests.
  • 332 tests green incl. Slow; build 0 warnings.

Every insert creates up to M0 back-links, and once the graph has warmed
up the target list is almost always full. The old code handled that by
re-running the complete selection heuristic over all M0 + 1 candidates:
~2,000 distance computations per back-link, ~130,000 per insert, each a
3 KiB memory-mapped read at 768 dimensions. Profiling the Cohere 100K
build put 66 % of insert time there.

AddNeighborConnection now does what Lucene's findWorstNonDiverse does:
only the new node is unchecked, so an existing neighbour is compared
against the new node alone and the new node against the neighbours
nearer than itself. O(M0) distances instead of O(M0^2).

Lucene's fallback of evicting the farthest candidate assumes the list
only holds mutually diverse nodes. Ours does not, because
SelectNeighborsHeuristic tops up with pruned candidates, and applying
the fallback blindly left 7 of 200 nodes unreachable in an existing
Euclidean test. When the incremental walk finds nothing redundant the
code falls back to the full heuristic instead; instrumented on clustered
data that happens on 0.1-0.7 % of full back-links.

SelectNeighborsHeuristic drops LINQ for a stable index sort with
identical ordering.

The graph is not byte-identical to the old one, so recall is re-measured
rather than assumed. Same machine, same seeds, old graph rebuilt the same
day for the SIFT-1M comparison:

  siftsmall     ~1,000-1,500 -> 2,485 inserts/s, recall unchanged
  Cohere 100K   1,696 s -> 589 s (59 -> 170 inserts/s), recall@100 -0.3 pp
  SIFT-1M       2,279 s -> 1,631 s (439 -> 613 inserts/s),
                recall@10 -1.5 pp at efSearch 10, -0.3 at 80, 0 at 320

Query throughput at a given efSearch is unchanged within run-to-run
noise on both datasets.

New tests: NeighborDiversityTests pins the heuristic's required outcome
on hand-checkable 2-D/3-D constructions plus a well-formedness sweep,
and passes against both implementations. RawGraph is shared out of
GraphFanoutTests.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@matengo
matengo merged commit e2a78b6 into master Sep 16, 2026
2 checks passed
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.

1 participant