Skip to content

perf(memory): Use the HNSW index for public memory recall - #2596

Merged
thomasjiangcy merged 1 commit into
mainfrom
perf/memory-recall-hnsw
Nov 3, 2026
Merged

thomasjiangcy merged 1 commit into
mainfrom
perf/memory-recall-hnsw

Conversation

@sentry-junior

@sentry-junior sentry-junior Bot commented Nov 3, 2026

Copy link
Copy Markdown
Contributor

Recall's vector query brute-forces every stored vector when the scopes include the public scope. On prod (pgvector 0.8.0, ~48.7k active memories, 26k public), EXPLAIN ANALYZE shows the planner computing the distance for all voyage/voyage-4-lite embeddings (bitmap scan on junior_memory_embeddings_model_idx or a parallel seq scan, then a sort), reading ~215k buffers. That takes ~0.36–0.39s on the server and ~0.43s p50 in the recall span. The embeddings are TOASTed, and the planner does not cost TOAST reads, so brute force always looks cheaper than the HNSW index. Fixing row estimates did not change the plan.

When the scopes include the public scope and the query vector is 1024-d (the only size with an HNSW index, migration 0013), the vector search now runs an ANN-first query in a transaction:

  • set_config('enable_seqscan', 'off', true), set_config('enable_bitmapscan', 'off', true), and set_config('hnsw.ef_search', K, true) in one statement. set_config takes bind params, so this is one round trip, not three SET LOCALs.
  • Inner query: WHERE dimensions = 1024 ORDER BY embedding::vector(1024) <=> q LIMIT K, with K = max(200, limit × 5). Its only filter is the index predicate. Adding the model filter here broke the plan on prod.
  • Outer query: joins memories and applies the same model match, visibility predicate, distance cutoff, order, and limit as before. Content-hash checks and dense ranks do not change.

On prod, Thomas forced this query by hand. It used junior_memory_embeddings_embedding_1024_hnsw_idx, took ~4ms of search work, and returned all 40 rows. For 10 probe vectors, its top 40 matched the exact top 40 (40/40). Those probes were stored vectors, which are easier than real prompts. Without forcing, the planner still picks a parallel seq scan (393ms). Expected effect (an estimate): recall p50 drops from ~1.15s to ~0.75s.

Local check against the compose.yaml pgvector image, with 48.7k clustered 1024-d vectors (26k public): the ANN query took ~5ms against ~170ms for the exact query. Its top 40 overlapped the exact top 40 in 677 of 680 slots, on fresh and stored query vectors.

What stays the same:

  • Private-only scope sets (the separate private probe) keep the exact query. Their visible set is small and already fast (7–43ms). A shared top-K could miss all of one User's memories.
  • 1536-d vectors have no ANN index, so they keep the exact query.
  • No hnsw.iterative_scan: when fewer rows than the limit pass the cutoff, it scans up to max_scan_tuples (4ms → 250ms in a local test).

Cost: the public vector query now uses its own pooled connection and needs BEGIN, set_config, SELECT, and COMMIT. That is 3 more round trips to Neon. Transactions already work through the Neon Pool driver: drizzle checks out a client for db.transaction, as the memory write path does.

Risk: ANN results are approximate. Other Users' private memories and other 1024-d models use part of the K budget before filtering. So a search can return fewer than limit rows when most of the nearest vectors are hidden (for example, while two 1024-d models coexist during a reembed).

Follow-up: listVectorAdjudicationCandidates (write path) probably has the same brute-force plan for the public scope. This PR does not change it.

via @thomasjiangcy.

--

View Session [Sentry]

The planner does not cost TOAST reads of stored vectors, so recall over the
public scope computed the distance to every stored vector. Take the nearest
candidates from the HNSW index in a transaction that turns off the other scans,
then apply the model, visibility, and cutoff filters. Private-only searches
keep the exact query.

Co-Authored-By: Thomas Jiang <20501235+thomasjiangcy@users.noreply.github.com>
@vercel

vercel Bot commented Nov 3, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
junior-docs Ready Ready Preview Nov 3, 2026 9:53am UTC

Request Review

@sentry-junior
sentry-junior Bot marked this pull request as ready for review November 3, 2026 10:00
@github-actions github-actions Bot added the risk: medium PR risk score: medium label Nov 3, 2026
@thomasjiangcy
thomasjiangcy merged commit b6ac0a7 into main Nov 3, 2026
47 checks passed
@thomasjiangcy
thomasjiangcy deleted the perf/memory-recall-hnsw branch November 3, 2026 10:07

This branch was successfully deployed

1 active deployment
Preview – junior-docs — df623b1d Deployed Nov 3, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

risk: medium PR risk score: medium

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant