Repository navigation
Description
Summary
When a vector or hybrid query carries a structured filter, the filter-only FTS call is capped at VECTOR_FILTER_SCAN_LIMIT (50,000) rows, and every vector candidate outside that page is treated as disallowed. A highly similar, genuinely matching result can therefore vanish purely because of where it fell in that scan.
Raised during review of #1426 (SPEC-82 temporal search) and declined there on scope — this is pre-existing and not temporal-specific. Filing so it isn't lost.
Why it is not merely "arbitrary"
With search_text=None there is no relevance signal in the ordering, so the retained 50,000 are not a random sample:
- Postgres tiebreaks
search_index.id ASC— the survivors are deterministically the earliest-indexed rows, i.e. systematically biased against newer content - SQLite has no id tiebreaker at all
Why it is not covered by the eventual-consistency carve-out
CLAUDE.md asks reviewers not to raise derived-state races that converge on a later write or index pass. This is not one of those. It is a result-completeness bug: the query returns a wrong answer now and returns the same wrong answer on every rerun. Nothing repairs it.
Scope — this is not about temporal
The filter_requested block, the capped self.search(...) call, and the allowed_keys intersection are byte-identical on posix-meta-predicates, the base branch. VECTOR_FILTER_SCAN_LIMIT dates to #550, with #1141 on top. SPEC-82 adds temporal to a list that already held nine filters with the identical exposure.
temporal is in fact the least reachable of the ten — it only matches rows carrying authored valid-time markup, whereas search_item_types=[observation] or a shallow file_path_prefix crosses 50k on any large vault. Fixing it for the newest and narrowest filter alone would leave the eight more reachable ones broken while making the path look safe, which is worse than leaving it visibly unfixed.
Why the obvious fixes are not small
- Intersect with the candidate keys (the right design):
search()has no id-restriction parameter, so this means a new argument on the abstract signature plus both backend implementations plus batchedINlists — the file already batches hydration at 250 because both engines cap bind parameters. - Exhaust the pages: removes the safety valve that stops a filter-only scan materializing an entire project's search index into memory.
- Raise the constant: moves the cliff, does not remove it.
Suggested direction
Scope a fix to the whole filter-intersection path rather than one filter, with candidate-key restriction as the design: push the candidate ids into the filter query instead of paging the filter and intersecting client-side.
Reachability
Requires a project whose filter-matching rows exceed 50,000 — large vaults and cloud tenants, not typical local use. That is what makes it a tracked issue rather than a release blocker.
Activity
claude commentedon Sep 1, 2026
Claude finished @phernandez's task in 50s —— View job
Triage complete
Type: Bug — confirmed against code, not just as reported.
src/basic_memory/repository/search_repository_base.py:2317-2355buildsfilter_requestedfor a vector/hybrid query, runs a filter-only FTSsearch()capped atVECTOR_FILTER_SCAN_LIMIT(50,000, defined at line 85), then intersects vector candidates againstallowed_keysderived from that capped page. Any vector match whose(type, id)key isn't in the first 50k FTS rows is dropped fromsearch_index_rowsregardless of true relevance.Component: Database/search repository (
search_repository_base.py) — the intersection logic is retrieval-mode agnostic (vector and hybrid paths both hit it) and is not specific to MCP, CLI, or Cloud; Cloud/large vaults are just where the row count is most likely to exceed 50k.Priority: Medium. It's a genuine result-completeness bug (deterministic wrong answers, not a self-healing eventual-consistency case per
CLAUDE.md's carve-out), but only reachable once filter-matching rows exceed 50,000 — large vaults / cloud tenants, not typical local use. That reachability bar is why this is being tracked rather than treated as a blocker.Complexity: Complex. As the issue notes, the correct fix (push candidate ids into the filter query instead of paging + client-side intersecting) requires a new id-restriction parameter on the abstract
search()signature plus both backend implementations, plus batching to respect each engine's bind-parameter cap (existing hydration already batches at 250). This is a design change to the whole filter-intersection path, not a one-line constant bump — and per the issue, the fix should cover all ten filters uniformly (not justtemporal) to avoid leaving the more-reachable filters (e.g.,search_item_types,file_path_prefix) silently broken.No duplicate issues found (
gh search issuesforVECTOR_FILTER_SCAN_LIMITand "vector filter intersection" returned only this issue). Root cause, scope, and suggested direction in the issue body are accurate based on reading the current code — recommend keeping this open for a dedicated design/implementation pass on the filter-intersection path rather than a narrow patch.fix(core): restrict the vector filter pass to its own candidates
fix(core): restrict the vector filter pass to its own candidates