Skip to content

A search may name a source and an external_id prefix, and both channels recall only those documents before fusion - #1427

Merged
WaylandYang merged 2 commits into
deeplethe:devfrom
re-cinq:feat/search-filters
Oct 26, 2026
Merged

WaylandYang merged 2 commits into
deeplethe:devfrom
re-cinq:feat/search-filters

Conversation

@gedaiu

@gedaiu gedaiu commented Oct 25, 2026

Copy link
Copy Markdown
Contributor

Part of #1271, the filters PR we agreed on. Thanks again for the careful review on #1273. The search request now takes source_id and external_id_prefix. A source from another base is a 404, like the other source routes. The prefix needs the source_id of an API or Statements source; otherwise it is a 422 (prefix_needs_push_source). I went with a prefix as the issue proposed, since an exact external_id already reads back through #1280. Both filters go into both channels before fusion: Tantivy gets a document_id term set, and the vector CTE gets document_id = ANY(...) before its LIMIT. So the base's other hits can't use up the 100 candidates, and paging works the same as without a filter. As you suggested, the external_id derivation moved out of the SQL into pushed_identity, next to pushed_key. The prefix is matched against the key pushed_key builds, with starts_with, so _ and % are plain characters. Unknown fields are still a 422. ingest.md and chat-and-mcp.md describe the filters.

New tests: a_source_filter_pages_through_its_six_hits_under_a_hundred_better_ones puts six weak hits of one source under a hundred better ones, and checks that pages of three join into the whole list. It fails if the filter runs after fusion. a_search_by_source_and_external_id_prefix_docs_finds_only_docs_a_md and a_search_by_another_bases_source_gets_404_and_a_prefix_without_a_push_source_gets_422 fail on dev. I ran cargo test for utopia-server (762 passed), utopia-store (474), utopia-search (7) and utopia-core (25) against a migrated Postgres with UTOPIA_TEST_REQUIRE_DB=1, plus cargo fmt --check and cargo clippy --workspace --all-targets -D warnings. MCP search_chunks comes next.

@WaylandYang WaylandYang 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.

Thanks, this is the shape we agreed on #1271. Both channels filter before fusion: the Tantivy term set and the ANY($5) inside the nearest-neighbour CTE. The prefix goes through starts_with on the pushed_key, a source from another base answers like a missing one, and the external_id derivation now sits next to pushed_key. Three small things before merge:

  1. In a_source_filter_pages_through_its_six_hits_under_a_hundred_better_ones, please also assert that every hit has both a bm25_rank and a vector_rank. As it stands, BM25 alone satisfies the test, so moving the vector filter after the LIMIT would still pass.
  2. Please add a test that _ and % in external_id_prefix are plain characters: pushing docs_a.md and docsXa.md with the prefix docs_ should find only the first.
  3. In ADR 0014, add the filters to the chunk-search paragraph of decision 7 (source_id, 404 outside the base; external_id_prefix for push sources, 422 prefix_needs_push_source; applied inside both channels), plus a dated revision line.

Optional: retrieval.rs now imports from api::sources_routes. Moving pushed_key/pushed_identity into a small shared module would keep sources_routes private. MCP search_chunks can follow in its own PR.

…ls recall only those documents before fusion

Signed-off-by: Bogdan Szabo <bogdan@re-cinq.com>
@gedaiu
gedaiu force-pushed the feat/search-filters branch from bb72c58 to 3f461fc Compare October 25, 2026 14:14
@gedaiu

gedaiu commented Oct 25, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for the careful read, @WaylandYang. You were right that BM25 alone carried that test. All three are in, plus the optional one:

  1. The six-hit test now also checks that every hit has both a bm25_rank and a vector_rank. To make sure it catches the bug, I moved the vector filter out of the nearest-neighbour CTE to after the LIMIT, and the test failed with [(true, false); 6]. Then I put it back.
  2. New test an_external_id_prefix_docs_underscore_finds_docs_a_md_but_not_docsxa_md. It pushes docs_a.md, docsXa.md, 50%off.md and 50 percent off.md. The prefix docs_ finds only docs_a.md and 50% finds only 50%off.md. When I swapped starts_with for LIKE $3 || '%', both cases failed.
  3. In ADR 0014, decision 7's chunk-search paragraph now names source_id (404 outside the base), external_id_prefix (push sources only, 422 prefix_needs_push_source) and says both apply inside each channel. There is also a 2026-10-25 revision line.

Optional: is_push_kind, pushed_key and pushed_identity now live in a small pushed_keys module. The push handlers, the search route and retrieval.rs all use it, and sources_routes is private again. Agreed that MCP search_chunks should get its own PR.

@WaylandYang WaylandYang 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.

Thanks @gedaiu, all three are in. The six-hit test now needs both ranks and fails with the vector filter after the LIMIT, _/% are taken literally, and 0014 records the filters. Approving; the stale sources_routes::pushed_identity reference in models.rs:483 can go in the MCP search_chunks PR.

@WaylandYang
WaylandYang merged commit 6466a5e into deeplethe:dev Oct 26, 2026
6 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.

2 participants