Skip to content

feat(recall): add a per-bank enable_text_search toggle for pure vector recall - #3819

Merged
nicoloboschi merged 3 commits into
mainfrom
feat/enable-text-search-flag
Aug 27, 2026
Merged

feat(recall): add a per-bank enable_text_search toggle for pure vector recall#3819
nicoloboschi merged 3 commits into
mainfrom
feat/enable-text-search-flag

Conversation

@nicoloboschi

Copy link
Copy Markdown
Collaborator

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_bm25 path, which already omitted the arm for a query with no word characters:

tokens = tokenize_query(query_text) if enable_text_search else []

What a disabled arm no longer pays for:

Cost Disabled
BM25 arms in the UNION (@@ / ts_rank_cd scan, one per fact type) not built
select_selective_bm25_tokens — a real pg_stats round trip not called
tokenize_query over the query text not called
$3/$4 bind params (BM25 limit + prepared text) not appended

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_retrieval

Hierarchical (global env → tenant → bank), threaded as a parameter rather than read off the global config:

  • config.pyENV_/DEFAULT_, dataclass field, from_env(), _CONFIGURABLE_FIELDS
  • http.pyCreateBankRequest + BankTemplateConfig + the get_config_updates field list
  • memory_engineretrieve_all_fact_types_parallelrecall_unifiedsearch → the SQL builder
  • Regenerated: OpenAPI spec, bank-template schema, Python/TS/Go clients, docs skill
  • Hand-written wrappers: HindsightClient (TS) and Hindsight (Python), both createBank and updateBankConfig
  • Control plane: the Recall section of the bank config view, plus recallTextSearch* strings in all ten locales

HINDSIGHT_API_ENABLE_TEXT_SEARCH sets 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 bm25 entry 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_vector is a GENERATED column on the native backend 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, no pg_stats lookup, 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) and test_bank_template_full_roundtrip.py updated.

Notes for the reviewer

  • plain-retrieval.json and 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.
  • Unrelated: consolidation/consolidator.py calls recall_unified and 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.py and test_bank_template_full_roundtrip.py fail 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

…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

strix-security Bot commented Aug 26, 2026

Copy link
Copy Markdown

Strix Security Review

Warning

This pull request has 2 commits after the last Strix review (7c21d70). Strix has not reviewed these changes.
Automatic review on push is off for this repository. To review the latest changes, tag @strix-security in a comment, or turn on re-review on push.

No security issues found.

Updated for 7c21d70.


Reviewed by Strix
Re-run review · Configure security review settings

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
@nicoloboschi
nicoloboschi merged commit cd8d4d3 into main Aug 27, 2026
321 of 324 checks passed
@nicoloboschi
nicoloboschi deleted the feat/enable-text-search-flag branch August 27, 2026 08:07
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