feat(recall): add a per-bank enable_text_search toggle for pure vector recall - #3819
Merged
Conversation
…r recall Recall runs four arms behind three per-bank switches — temporal, graph and reranking. The keyword (BM25) arm had none, so a bank whose queries are conceptual rather than literal could not opt out of it. Semantic and BM25 share a single UNION query, so "disabled" has to mean the BM25 half is never built rather than filtered away afterwards. The gate lands on the existing `_include_bm25` path, which already omitted the arm for a query with no word characters: tokens = tokenize_query(query_text) if enable_text_search else [] so a disabled arm skips its SQL, its query tokenization, its `pg_stats` term-selection round trip and its bind parameters. The dense arm, its thresholds and the graph seeding off it are unchanged. Wired like `enable_graph_retrieval` throughout: `_CONFIGURABLE_FIELDS`, both bank config models, the bank template schema, the OpenAPI spec, the generated and wrapper SDKs (TS + Python), and the control plane's recall section in all ten locales. `HINDSIGHT_API_ENABLE_TEXT_SEARCH` sets the deployment default a bank inherits. Knowledge-page search resolves the same per-bank flag and collapses to its vector half, so a bank that opted out does not keep hitting a text index. Its BM25-only fallback for a missing embedding returns nothing rather than degrading into the arm the bank disabled. Read path only: `search_vector` and its index are still maintained on write, so the flag is reversible without a reindex. Claude-Session: https://claude.ai/code/session_017ufCz6qrNxn36Stug7ek8A
Strix Security ReviewWarning This pull request has 2 commits after the last Strix review ( No security issues found. Updated for Reviewed by Strix |
check-cli-coverage requires every request-body field to be either a CLI arg or listed with a reason. The three sibling recall toggles are all listed as set through `bank set-config`; the new one needs the same entry, on both create_or_update_bank and update_bank. Claude-Session: https://claude.ai/code/session_017ufCz6qrNxn36Stug7ek8A
MemoriesExtension.recall_unified gained the parameter, and test_stub_signature_accepts_interface_params guards that every store implementing the interface accepts each of its params. InMemoryMemories is the only other implementation, so it needs the same keyword. Claude-Session: https://claude.ai/code/session_017ufCz6qrNxn36Stug7ek8A
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.
Why
Recall runs four arms behind three per-bank switches —
enable_temporal_retrieval,enable_graph_retrieval,enable_reranking. The keyword (BM25) arm had none, so a bank whose queries are conceptual rather than literal could not opt out of it, and there was no way to run pure vector recall.The gate
Semantic and BM25 share a single UNION query, so "disabled" has to mean the BM25 half is never built, not that its rows are filtered away afterwards. The flag lands on the existing
_include_bm25path, which already omitted the arm for a query with no word characters:What a disabled arm no longer pays for:
@@/ts_rank_cdscan, one per fact type)select_selective_bm25_tokens— a realpg_statsround triptokenize_queryover the query text$3/$4bind params (BM25 limit + prepared text)The dense arm, its thresholds, and the graph seeding off it are unchanged. The emitted SQL is the semantic UNION alone.
Wired like
enable_graph_retrievalHierarchical (global env → tenant → bank), threaded as a parameter rather than read off the global config:
config.py—ENV_/DEFAULT_, dataclass field,from_env(),_CONFIGURABLE_FIELDShttp.py—CreateBankRequest+BankTemplateConfig+ theget_config_updatesfield listmemory_engine→retrieve_all_fact_types_parallel→recall_unified→search→ the SQL builderHindsightClient(TS) andHindsight(Python), bothcreateBankandupdateBankConfigrecallTextSearch*strings in all ten localesHINDSIGHT_API_ENABLE_TEXT_SEARCHsets the deployment-wide default a bank inherits.Two follow-through details
Knowledge-page search resolves the same per-bank flag and collapses to its vector half, so a bank that switched the arm off in recall does not keep hitting a text index there. Its BM25-only fallback (for a missing embedding) returns nothing rather than degrading into the arm the bank disabled.
The recall trace omits its
bm25entry when the arm is off, mirroring the existing graph guard — an empty entry reads as "ran, matched nothing" rather than "absent", which is backwards for anyone comparing traces to tune latency.Scope
Read path only.
memory_units.search_vectoris aGENERATEDcolumn on thenativebackend and the text index is created by migrations, so skipping write-side maintenance would be a schema change and would make the flag one-way. As built it is reversible without a reindex.Tests
test_enable_text_search_flag.py— asserts the absence of work (no tokenization, nopg_statslookup, no BM25 arm, 2 bind params instead of 4), each paired with an enabled-case guard so nothing passes vacuously; plus env parsing and membership in the configurable set.test_knowledge_search_text_search_disabled.py— the vector-only knowledge SQL, the empty-result case, the untouched fallback, and that bank isolation (kp.bank_id = $2) survives the rewritten query.test_recall_pipeline_toggles.py— extended so the new toggle is covered by both the plumbing test and the bank-override → retrieval-call wiring test.recall_pipeline_toggles_mapping.test.ts— new; the TS wrapper had no mapping test for any of the four toggles, mirroring the Python one.test_hierarchical_config.py(count 46 → 47) andtest_bank_template_full_roundtrip.pyupdated.Notes for the reviewer
plain-retrieval.jsonand the "plain-retrieval (RAG) banks" doc section deliberately keep hybrid search — BM25 is cheap next to the LLM/graph/rerank work that section is about, and flipping them is a product opinion, not part of adding the flag.consolidation/consolidator.pycallsrecall_unifiedand reads only.semantic, so it pays for the keyword arm it discards (its comment claims it needs "dense/keyword"). Left alone — changing dedup's candidate set is not this PR's business.test_hnsw_indexes.pyandtest_bank_template_full_roundtrip.pyfail on my machine against a shared pg0 stamped ahead by another branch; both were verified to fail identically on a clean tree.https://claude.ai/code/session_017ufCz6qrNxn36Stug7ek8A