fix: stream-scoping parity across BM25 retrieval lanes - #61
Merged
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Enforce a single retrieval invariant across the search surface: every BM25 lane applies stream scoping at index-query time, not post-hoc. The vector, graph and plain-BM25 lanes already did this; the date-range and entity lanes queried the global Tantivy index without a stream predicate and leaned on downstream stages. This brings both lanes to parity with their siblings and adds a defense-in-depth guard. P0 — stream predicate at the source: - search_with_date_range / search_with_entity take Option<&str> stream and add a MUST stream clause, mirroring search_with_stream / search_with_agent. None = unfiltered (backward-compatible for callers that already pre-scope). - Bm25Leaf::date / Bm25Leaf::entity now carry stream instead of hardcoding None; this also closes the agent-scoped date/entity branch, which routes through search_with_agent with leaf.stream. - bm25_retrieve routes date / entity through the per-stream merge helper (bm25_over_streams), so multi-stream callers get per-stream union semantics (single-stream fast path, multi-stream max-score merge, None unscoped). P1 — defense-in-depth: - filter_and_truncate runs a fail-closed retain on stream membership before top-K truncation. A result whose chunk is missing, unreadable, or carries no in-scope stream attribution is dropped, not passed through — so a future lane that forgets the predicate cannot leak cross-stream chunks. NLOC: adding the stream clause would have pushed both core functions past NLOC<=100, so the duplicated inline doc->SearchResult loops were replaced with the existing collect_results helper (behavior-identical). This also removed the pre-existing `as i32` level casts on both paths. Tests (deterministic, tempdir/in-memory; no network, no LLM): - core: date-lane and entity-lane isolation, None-parity union, date-only (empty query) scoping, and a high-BM25 L1 twin in another stream that must not surface in a scoped date query (guards the invariant vs ranking reorder). - handler e2e through search_handler: time_filter and entity isolation, plus a non-vacuous check that the dropped twin is retrievable when scoped to its own stream. Implementation note (Loomem adaptation): the sibling helper builds a leaf per stream, but Rust's HRTB would force the leaf's borrowed query/entity fields to 'static. bm25_over_streams instead takes a run-leaf closure returning owned results, keeping the same union semantics without the lifetime coupling. Critical file rationale (handlers/search.rs, god file): minimal-risk. New code is additive — a pure merge helper (bm25_over_streams) and a guarded retain; the existing date/entity call sites were re-pointed at the helper with identical per-stream union semantics to the plain branch. No control-flow change to fusion, ranking, boosts or truncation order. Verified: full handlers::search suite (40), agent_filter (5), new e2e (3), core tantivy_index (20) and integration (6) all green; canonical clippy gate clean. Two pre-existing mcp/dispatcher ac6_3 test failures are unrelated (MCP tool-definition count drift, untouched here). Refs sibling-engine port: Loomem-Port planned, commit d0c84c2 on branch claude/implementation-request-coxdgz. Signed-off-by: Claude <noreply@anthropic.com>
vvooki-sys
force-pushed
the
claude/bm25-stream-scoping-parity-tm3c29
branch
from
July 10, 2026 20:11
f3834b0 to
504539b
Compare
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.
What & why
Enforces a single retrieval invariant across the whole search surface: every BM25 lane applies stream scoping at index-query time, not post-hoc. The vector, graph and plain-BM25 lanes already did this; the BM25 date-range and entity lanes queried the global Tantivy index without a stream predicate and relied on downstream stages. This brings both lanes to parity with their siblings and adds a fail-closed guard as defense-in-depth.
Loomem-side counterpart of the sibling-engine change (
Loomem-Port: planned, commitd0c84c2onclaude/implementation-request-coxdgz) — design ported, adapted to Loomem's module layout.Changes
P0 — stream predicate at the source
search_with_date_range/search_with_entity(loomem-core/src/tantivy_index.rs) takeOption<&str> streamand add aMUST streamclause, mirroringsearch_with_stream/search_with_agent.None= unfiltered (backward-compatible for pre-scoping callers). A smallpush_stream_clausehelper factors the shared term-query construction.Bm25Leaf::date/Bm25Leaf::entity(loomem-server/src/handlers/search.rs) now carrystreaminstead of hardcodingNone— this also closes the agent-scoped date/entity branch (which routes throughsearch_with_agentwithleaf.stream).bm25_retrieveroutesdate/entitythrough a per-stream merge helper (bm25_over_streams): single-stream fast path, multi-stream max-score union,Noneunscoped. The plain branch's dual original/expanded query now shares the same helper.P1 — defense-in-depth
filter_and_truncateruns a fail-closedretainon stream membership before top-K truncation. A result whose chunk is missing, unreadable, or carries no in-scope stream attribution is dropped, not passed through — so a future lane that forgets the predicate cannot leak cross-stream chunks. Mirrors the graph lane's gate, but fail-closed.NLOC: the stream clause would have pushed both core functions past NLOC≤100, so the duplicated inline
doc→SearchResultloops were replaced with the existingcollect_resultshelper (behavior-identical). This also removed the pre-existingas i32level casts on both paths.Tests (deterministic, tempdir/in-memory — no network, no LLM)
tantivy_index.rs): date-lane and entity-lane isolation;None-parity union (backward compat); date-only (empty query) scoping; and a high-BM25 L1 twin in another stream that must not surface in a stream-scoped date query (guards the invariant against ranking-stage reorder).search_handler):time_filterand entity isolation, plus a non-vacuous check that the dropped twin is retrievable when scoped to its own stream.Gates
cargo fmt --check✓cargo clippy --workspace -- -D warnings(canonical CI gate) ✓tantivy_index(20), handlerhandlers::search(40),agent_filter(5), new e2e (3), coreintegration_test(6).Out of scope
MCP tool schema (
time_filtersemantics), ranking/boost changes (L1 ×1.5 untouched), and the already-conformant vector/graph/plain-BM25 lanes.No merge — merge is Łukasz's call.
🤖 Generated with Claude Code
Generated by Claude Code
Greptile Summary
This PR makes BM25 stream scoping consistent across date-range and entity retrieval. The main changes are:
search_with_date_rangeandsearch_with_entity.Confidence Score: 5/5
Safe to merge with low risk.
The changed retrieval paths preserve unscoped
Nonebehavior while adding source-level stream constraints for scoped searches. Tests cover date, entity, empty-query date, high-score cross-stream twins, and handler-level behavior. No functional or security issues were identified in the changed files.No files require special attention.
What T-Rex did
Important Files Changed
Nonestream argument.Sequence Diagram
%%{init: {'theme': 'neutral'}}%% sequenceDiagram participant Client participant Handler as search_handler/bm25_retrieve participant Merge as bm25_over_streams participant Leaf as bm25_leaf participant Tantivy as TantivyIndex participant Guard as filter_and_truncate Client->>Handler: SearchRequest with streams/date/entity Handler->>Merge: streams + branch-specific leaf builder alt no stream scope Merge->>Leaf: run_leaf(None) else one stream Merge->>Leaf: run_leaf(Some(stream)) else multiple streams loop each stream Merge->>Leaf: run_leaf(Some(stream)) Leaf-->>Merge: scoped BM25 hits end Merge->>Merge: merge by id, max score wins end Leaf->>Tantivy: search_with_date_range/search_with_entity(..., stream) Tantivy->>Tantivy: add MUST stream TermQuery when Some(stream) Tantivy-->>Handler: SearchResult candidates Handler->>Guard: hybrid results + ctx.stream_list Guard->>Guard: retain only chunks whose stored stream is in scope Guard-->>Client: top-K scoped results%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%% sequenceDiagram participant Client participant Handler as search_handler/bm25_retrieve participant Merge as bm25_over_streams participant Leaf as bm25_leaf participant Tantivy as TantivyIndex participant Guard as filter_and_truncate Client->>Handler: SearchRequest with streams/date/entity Handler->>Merge: streams + branch-specific leaf builder alt no stream scope Merge->>Leaf: run_leaf(None) else one stream Merge->>Leaf: run_leaf(Some(stream)) else multiple streams loop each stream Merge->>Leaf: run_leaf(Some(stream)) Leaf-->>Merge: scoped BM25 hits end Merge->>Merge: merge by id, max score wins end Leaf->>Tantivy: search_with_date_range/search_with_entity(..., stream) Tantivy->>Tantivy: add MUST stream TermQuery when Some(stream) Tantivy-->>Handler: SearchResult candidates Handler->>Guard: hybrid results + ctx.stream_list Guard->>Guard: retain only chunks whose stored stream is in scope Guard-->>Client: top-K scoped resultsReviews (3): Last reviewed commit: "fix: scope BM25 date/entity lanes at the..." | Re-trigger Greptile