Skip to content

refactor(core): move FTS execution behind FtsBackend and bind the read path to scope - #1563

Merged
phernandez merged 1 commit into
feat/1558-scoped-search-1-compilefrom
feat/1558-scoped-search-2-reader
Sep 15, 2026
Merged

phernandez merged 1 commit into
feat/1558-scoped-search-1-compilefrom
feat/1558-scoped-search-2-reader

Conversation

@phernandez

Copy link
Copy Markdown
Member

Why

Part 2a of the #1558 stack, on top of #1562. The goal is one read pipeline parameterized by project scope, shared by the project route, the database-scoped route, and MCP search_all_projects. This PR takes full-text execution out of the project-bound repositories and makes every statement the shared read path issues bind the repository's ProjectScope. No query behavior changes.

Part 2b extracts the vector, fusion, relaxation, and chunk-hydration path into a SearchReader the routes can construct without a repository. Splitting keeps this diff reviewable: 2a is scope plumbing and a moved FTS body; 2b is the class extraction and the test-double migration that comes with it.

What changed

  • FtsBackend in search_filters: the narrow contract for running a compiled full-text statement on one engine. SQLiteFts and PostgresFts implement it in the backend query modules with the bodies that used to live in each repository's search() and count(). FTS5 syntax errors answer empty. Postgres retries a malformed strict tsquery relaxed inside a savepoint so a caller-owned transaction survives.
  • SearchRepositoryBase.search and count are concrete. Shared vector and hybrid dispatch first, then self._fts. Both repositories lose their search(), count(), the pass-through _run_vector_query overrides, and the SQLite entity-column cache, which now lives in SQLiteFts.
  • Scope in the read path. Manifest hydration, candidate row fetch, read_manifest_readiness, and classify_hydration_drops take the scope. current_vector_manifest_predicate(scope, params) replaces the :project_id constant; project_readiness uses it for its one project.
  • PreparedSearchQuery moves to repository/search_query.py with defaults, so the repository layer can build and pass it. compile_fts_filter takes it instead of thirteen keyword arguments. The service imports it from there.
  • The filter helpers both backends share, and the SearchIndexKey alias, move out of the base class into search_filters and search_index_row.
  • Test fakes that bypass __init__ set scope beside project_id, and their search() overrides carry the session parameter the base method has. Postgres tests patch the tsquery classifier on postgres_search_query, where PostgresFts reads it.

Kept for 2b on purpose: the best_distance and hasattr(_semantic_vector_index) compatibility branches in the base vector path. About fourteen test files patch _run_vector_query and _ensure_vector_tables on repository instances; those move together with the reader.

Testing

Check Result
just fast-check passed
uv run pytest tests/repository tests/services tests/api/v2/test_search*.py tests/api/v2/test_cjk_search_guidance.py tests/mcp/test_tool_search.py --no-cov (SQLite) 1798 passed, 40 skipped
BASIC_MEMORY_TEST_POSTGRES=1 uv run pytest over the search repository, quoted-query, relaxation, temporal, candidate-restriction, rerank pipeline, trace, readiness, and search router suites (testcontainers) 421 passed, 15 skipped
just doctor passed

🤖 Generated with Claude Code

https://claude.ai/code/session_019YW9ysxugGGBCNEGzsxtFV

…d path to scope

Second step of #1558. The project-bound repositories no longer own full-text
execution, and every statement the shared read path issues binds the
repository's ProjectScope instead of assuming a single :project_id. No query
behavior changes.

- search_filters.FtsBackend: the narrow contract for running a compiled
  full-text statement on one engine. SQLiteFts and PostgresFts implement it in
  the backend query modules, carrying the bodies that used to live in each
  repository's search()/count(): FTS5 syntax errors answer empty; Postgres
  retries a malformed strict tsquery relaxed inside a savepoint.
- SearchRepositoryBase.search and count are concrete: shared vector/hybrid
  dispatch, then self._fts. The two repositories lose their search(), count(),
  pass-through _run_vector_query overrides, and the SQLite entity-column
  cache (now inside SQLiteFts).
- Manifest hydration, candidate row fetch, readiness, and hydration-drop
  classification take the scope. current_vector_manifest_predicate replaces
  the :project_id constant; project_readiness uses it for its one project.
- PreparedSearchQuery moves to repository.search_query with defaults so the
  repository layer can build and pass it. compile_fts_filter takes it.
- The filter helpers both backends share, and the SearchIndexKey alias, move
  out of the base class into search_filters and search_index_row.
- Test fakes that bypass __init__ set scope beside project_id, and their
  search() overrides carry the session parameter the base method has.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_019YW9ysxugGGBCNEGzsxtFV
Signed-off-by: phernandez <[email protected]>
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@phernandez
phernandez added this pull request to stack #1564 September 15, 2026 17:08
@phernandez
phernandez merged commit 51f95bf into main Sep 15, 2026
34 checks passed
@phernandez
phernandez deleted the feat/1558-scoped-search-2-reader branch September 15, 2026 23:55
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