Skip to content

test(integration): raise per-query HNSW ef so small-graph recall is reliable#1512

Closed
dawsontoth wants to merge 4 commits into
claude/fix-ci-5of6from
claude/fix-hnsw-reindex-tolerance
Closed

test(integration): raise per-query HNSW ef so small-graph recall is reliable#1512
dawsontoth wants to merge 4 commits into
claude/fix-ci-5of6from
claude/fix-hnsw-reindex-tolerance

Conversation

@dawsontoth

Copy link
Copy Markdown
Contributor

Status: Draft — focused fix for the HNSW recall flake in shard 5/6. Stacked on #1505 (same file).

Flake

The reindex backfill (and occasionally delete-entry-point / update-churn) recall assertions intermittently failed — e.g. on PR #1504's run, reindex backfill failed on v24+v26 with too many misses, even though the test is unmodified there. An exact-match self-vector search would miss records that are correctly indexed.

Root cause

Harper's HNSW search ef auto-scales with √N, deliberately tuned for 5K–30K-node graphs (resources/indexes/HierarchicalNavigableSmallWorld.ts). On these tiny 40–50-node test graphs the auto-scaled ef under-explores, so approximate recall drops below the tolerance — nondeterministically, depending on the graph the backfill happened to build.

Fix

A per-query ef wins over the auto-scale (search() resolves per-query ef first). The vector-search helpers now pass ef = 64 (above the working set) so the search explores essentially the whole small graph → reliable recall.

Tolerances are left unchanged. A genuinely unindexed/disconnected record (a real backfill bug — what these tests guard) is still missed regardless of ef, so the data-integrity assertions stand. This only removes the small-graph approximate-recall noise, not the guard.

Validation

CI-only (local restart_service is flaky). If CI shows the ef field doesn't thread through the sort/condition query path, the fallback is an explicit efConstructionSearch on the index schema — but note that path currently stores the value as a string (the @indexed arg parser uses a string cast, not the numeric coercer), which is a separate latent bug worth its own fix.

🤖 Generated with Claude Code

dawsontoth and others added 3 commits June 26, 2026 16:27
The vector-index-integrity suite is a major Windows CI hotspot (~29 min for the
suite). Most of the cost is ~100–200 sequential, individually-committed HTTP
round-trips per test — tiny data (DIMS=8, N≤50), so it's round-trip + per-write
fsync overhead, not vector math. Apply correctness-neutral reductions:

- reindex backfill: bulk-insert the N pre-index seed records in one request
  instead of N sequential POSTs. There is no HNSW index during insert, so
  per-record ordering is irrelevant; the backfill builds the index afterward.
- update-churn & reindex: run the independent read-back verification searches
  concurrently (Promise.all) instead of serializing N round-trips.

Left untouched (semantically order-sensitive): the entry-point insert/delete
sequence and the churn update sequence.

Note: validated on CI, not locally — restart_service is flaky in a local
multi-loopback setup (readiness probes time out at 60s), which both fails and
dominates local timings; the churn test (the one that runs cleanly locally)
still passes with the parallelized read-back.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Each test in this suite deployed its own component and restarted the HTTP
workers; the restart is the dominant per-test cost on slow runners (a test doing
3 inserts still took ~265s on Windows — almost entirely restart/setup). Deploy
all four tables in one component with a single restart in `before`, and drop the
per-test add_component/set_component_file/restart. Restarts: ~5 -> 2 (the shared
one, plus the single restart `reindex` needs for its mid-test index-add);
add_component: 4 -> 1.

Tables keep distinct names/databases so the tests stay isolated despite sharing
one component. ReindexTable is deployed without an index initially; the reindex
test redeploys the full schema with it indexed (a no-op re-assert for the other
three, which have already run — reindex is last) to trigger backfill.

CI-validated (local restart_service is flaky in a multi-loopback setup).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…eliable

The reindex/delete-entry/churn recall assertions intermittently failed: an
exact-match self-vector search occasionally missed records that ARE correctly
indexed. Root cause — Harper's search ef auto-scales with √N (tuned for
5K–30K-node graphs); on these 40–50-node test graphs it under-explores, so HNSW
approximate recall drops below the tolerance. A per-query ef overrides the
auto-scale (search() resolves per-query ef first), so the vector-search helpers
now pass ef=64 (above the working set) for reliable recall on the small graphs.

Tolerances are left unchanged: a genuinely unindexed/disconnected record (a real
backfill bug) is still missed regardless of ef, so the data-integrity guards
stand — this only removes the small-graph approximate-recall noise.

Stacked on #1505. CI-validated (local restart_service is flaky).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a default SEARCH_EF constant of 64 in the vector index integrity integration tests to prevent flaky recall assertions on small test graphs. It updates vectorSearch and vectorThresholdSearch to apply this default ef value. The reviewer suggests allowing opts.ef to be explicitly set to null so that tests can still omit the ef parameter and verify the system's default auto-scaling behavior.

Comment thread integrationTests/server/vector-index-integrity.test.ts Outdated
@claude

claude Bot commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

Reviewed; no blockers found.

Address review feedback (#1512): vectorSearch now accepts `ef: null` to omit the
ef param entirely, so a future test can exercise Harper's default auto-scaling
behavior. `ef` undefined still defaults to SEARCH_EF (the small-graph recall fix);
an explicit number still overrides.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Base automatically changed from claude/perf-vector-tests to claude/fix-ci-5of6 June 28, 2026 22:02
@dawsontoth dawsontoth closed this Jun 29, 2026
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