perf(core): read only the projects in scope from the vector indexes - #1569
Closed
phernandez wants to merge 8 commits into
Closed
phernandez wants to merge 8 commits into
phernandez wants to merge 8 commits into
Conversation
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
phernandez
force-pushed
the
feat/1558-scoped-search-4a-ann-prefilter
branch
from
September 15, 2026 23:04
6c711bf to
e423cfb
Compare
7 tasks
…1562) Signed-off-by: phernandez <[email protected]> Co-authored-by: Claude Fable 5.1 <[email protected]>
…d path to scope (#1563) Signed-off-by: phernandez <[email protected]> Co-authored-by: Claude Fable 5.1 <[email protected]>
…ch repository (#1565) Signed-off-by: phernandez <[email protected]> Co-authored-by: Claude Fable 5.1 <[email protected]>
…over a ProjectScope (#1566) Signed-off-by: phernandez <[email protected]> Co-authored-by: Claude Fable 5.1 <[email protected]>
Signed-off-by: phernandez <[email protected]> Co-authored-by: Claude Fable 5.1 <[email protected]>
Base automatically changed from
feat/1558-scoped-search-5-mcp-scoped
to
feat/1558-scoped-search-3b-scoped-route
September 15, 2026 23:56
phernandez
added this pull request to stack #1572
September 15, 2026 23:56
`search_notes(search_all_projects=True)` used to run one project search per project and merge the pages client-side. It now groups the accessible projects by the database they live in (the local database, or one cloud workspace each) and runs one `QUERY /v2/search/` per group through the new `ScopedSearchClient`, passing the group's internal project ids. The server ranks the union in one query and attributes each hit with `project_external_id`; the tool qualifies permalinks and paths from that attribution rather than from which request a result came back on, so a name collision across workspaces can no longer route a hit to the wrong project. A new `projects` parameter searches a chosen subset by name or external id. It shares the grouping and merge with the all-projects path; an unknown name is an error before any query runs. Cross-database results are still merged by score and sliced to the requested page, a failing database is skipped with a warning and an inexact total, a retryable outage fails the merged page, and a server too old to attribute its results raises a version-skew error like the existing valid-time skew check. `list_memory_projects` now carries the internal `id` in its merged project rows, which is what the scoped route addresses projects by. The temporal confirmation check moves to a module function shared by both search clients. Refs #1558 Co-Authored-By: Claude Fable 5.1 <[email protected]> Claude-Session: https://claude.ai/code/session_019YW9ysxugGGBCNEGzsxtFV Signed-off-by: phernandez <[email protected]>
…meter `tests/test_man_pages.py` compares the rendered SYNOPSIS and PARAMETERS of search-notes(3) with the tool registry; the new `projects` parameter left the committed page stale. Co-Authored-By: Claude Fable 5.1 <[email protected]> Claude-Session: https://claude.ai/code/session_019YW9ysxugGGBCNEGzsxtFV Signed-off-by: phernandez <[email protected]>
Both built-in vector adapters took the k nearest vectors across the whole database and applied the scope afterwards. In a shared database a small project could get an under-filled or empty page for a query its own notes answered, because a larger neighbour's vectors filled the window first. On a 20k-chunk corpus where one project holds 2% of the vectors, a limit-100 query for that project returned 1 row from sqlite-vec. sqlite-vec: `project_id` becomes a vec0 partition key, so the KNN ranks each partition in scope and the outer ORDER BY merges them (100 of 100 rows, 0.2 ms against 3.1 ms). Existing local storage is carried into the partitioned table through a temporary table inside one transaction; vectors and ready manifests survive, nothing is re-embedded. Dimension and source_hash mismatches still rebuild as before. pgvector: the statement's tie-break sort keys had kept the planner off the HNSW index entirely, so every vector query was an exact scan of the table. The window is now taken by distance alone inside a materialized CTE and re-sorted with the tie-breaks outside it. `hnsw.ef_search` is set per transaction to the window the query must fill (the default 40 capped every larger candidate pool), and `hnsw.iterative_scan = relaxed_order` lets the scan continue until the scope and manifest filters have admitted enough rows. That scan needs pgvector 0.8; an older extension now raises SemanticDependenciesMissingError at first use rather than returning short windows. Neon tenants run 0.8.0. Refs #1558 Co-Authored-By: Claude Fable 5.1 <[email protected]> Claude-Session: https://claude.ai/code/session_019YW9ysxugGGBCNEGzsxtFV Signed-off-by: phernandez <[email protected]>
phernandez
force-pushed
the
feat/1558-scoped-search-4a-ann-prefilter
branch
from
September 15, 2026 23:59
e423cfb to
e9c4bff
Compare
Member
Author
|
Superseded by #1574: same branch and commit, opened against main because GitHub refuses to retarget a PR that is part of a stack once its base has been merged. |
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.
Summary
Sixth PR in the #1558 stack, on top of #1568. First half of the "vector order and ANN pre-filter" item: the scope becomes a real pre-filter in both built-in vector adapters, and the pgvector statement actually uses its HNSW index. The second half (filling the window when structured filters reject most of the nearest candidates) follows as its own PR on top of this one.
What was wrong. Both adapters ranked the k nearest vectors across the whole database and applied the scope afterwards. In a shared database (a local vault with many projects, or a tenant with several) a small project could get an under-filled or empty page for a query its own notes answered, because a larger neighbour's vectors filled the window first. This predates the stack; the scoped route just makes it easier to hit.
sqlite-vec.
project_idbecomes a vec0 partition key. The KNN ranks each partition in scope and the outerORDER BYmerges them. Existing local storage is carried into the partitioned table through a temporary table inside one transaction: vectors and ready manifests survive, nothing is re-embedded. Dimension andsource_hashmismatches still rebuild as before.pgvector. Three things, found while measuring:
hnsw.ef_searchwas never set, so an HNSW scan would have returned at most 40 rows whatever the candidate window asked for. It is now set per transaction to the window the query must fill (capped at the server maximum of 1000).hnsw.iterative_scan = relaxed_orderlets the scan continue until the scope and manifest filters have admitted enough rows. That needs pgvector 0.8. An older extension raisesSemanticDependenciesMissingErrorat first use, which the API reports as a bad request, instead of quietly returning short windows. Neon tenants run 0.8.0; the test image runs 0.8.5.Measurements
20k chunks x 384 dims, three projects, project 1 holds 2% of the vectors. Window of 100 rows. Medians of 5 runs.
The "iterative off" row is why pgvector 0.8 is required: with the index in use and a scope covering half the table, an ef_search-sized scan alone under-fills the window. For the 2% scope the planner chose the project btree index and an exact sort on its own, so that case is exact under both statements.
Test plan
just fast-checkef_searchto the window, always requests the iterative scan, and refuses pgvector < 0.8 before creating storagetests/repository tests/services tests/apion SQLite (2175 passed)test-intvec0 and semantic coverage (7 passed)just doctorRefs #1558
🤖 Generated with Claude Code
https://claude.ai/code/session_019YW9ysxugGGBCNEGzsxtFV