Extends hotdata_langchain/search.py, which currently exposes BM25 only, to semantic search and then to fusion over both. Design context: docs/ai-native-layer-roadmap.md, verified engine behaviour: docs/engine-contract.md.
1. Semantic search tool
vector_search('catalog.schema.table', 'column', 'text', k) is registered in the engine (src/engine/builder.rs, rewritten to vector_search_vector during planning). Wrap it the same way hotdata_search_text wraps bm25_search.
Name it for the capability, not the mechanism — hotdata_search_semantic, not hotdata_search_vector or anything naming HNSW. tests/test_search.py::test_search_description_names_the_capability_not_the_index already enforces this for the text tool: the description must never leak "bm25"/"vector"/"hnsw"/"embedding". Reason: the contract the model is given has to survive the retrieval strategy changing underneath it.
Note the asymmetry with BM25 — vector has scalar distance UDFs (cosine_distance/l2_distance/negative_dot_program) that work without any index, brute-force but correct, so a semantic tool can function before an index exists. BM25 has no such fallback. See docs/vectorstore-plan.md, which already covers the same SQL contract for HotdataVectorStore.
2. Hybrid fusion
Then fuse, and expose it as one tool, not two the model chooses between. hotdata_search_text's contract stays "find rows whose text is relevant" and quietly becomes hybrid.
Rationale: BM25 and vector fail differently — BM25 misses paraphrase ("garden" vs "yard"), vector misses rare exact tokens (ids, proper nouns, model numbers, which embeddings smear). They cover each other's blind spots, so fusion beats either. Making the model pick is a worse product than doing both.
Fuse ranks, not scores. Observed BM25 scores run ~8–11 and are unbounded; cosine distance is 0–2. They cannot be averaged, thresholded or compared. Reciprocal rank fusion sidesteps this entirely by operating on rank position. This is the main implementation trap.
Client-side first, and issue the two searches concurrently. Measured on a real agent run: 7,057 ms total, tool calls ~1,200 ms wall each against 49–79 ms of engine execution — so ~1,100 ms per call is round trip. Sequential fan-out would add a full extra round trip; concurrent fan-out costs roughly one. Engine-side fusion removes it (see #37 item 2) but should come after the parameters are settled here, because the port is then a spec rather than a design exercise.
Open parameters to settle empirically: the RRF constant, the dedup key (row id presumably), tie-breaking, and whether to expose per-pathway scores in the payload for debuggability.
Depends on
Extends
hotdata_langchain/search.py, which currently exposes BM25 only, to semantic search and then to fusion over both. Design context:docs/ai-native-layer-roadmap.md, verified engine behaviour:docs/engine-contract.md.1. Semantic search tool
vector_search('catalog.schema.table', 'column', 'text', k)is registered in the engine (src/engine/builder.rs, rewritten tovector_search_vectorduring planning). Wrap it the same wayhotdata_search_textwrapsbm25_search.Name it for the capability, not the mechanism —
hotdata_search_semantic, nothotdata_search_vectoror anything naming HNSW.tests/test_search.py::test_search_description_names_the_capability_not_the_indexalready enforces this for the text tool: the description must never leak "bm25"/"vector"/"hnsw"/"embedding". Reason: the contract the model is given has to survive the retrieval strategy changing underneath it.Note the asymmetry with BM25 — vector has scalar distance UDFs (
cosine_distance/l2_distance/negative_dot_program) that work without any index, brute-force but correct, so a semantic tool can function before an index exists. BM25 has no such fallback. Seedocs/vectorstore-plan.md, which already covers the same SQL contract forHotdataVectorStore.2. Hybrid fusion
Then fuse, and expose it as one tool, not two the model chooses between.
hotdata_search_text's contract stays "find rows whose text is relevant" and quietly becomes hybrid.Rationale: BM25 and vector fail differently — BM25 misses paraphrase ("garden" vs "yard"), vector misses rare exact tokens (ids, proper nouns, model numbers, which embeddings smear). They cover each other's blind spots, so fusion beats either. Making the model pick is a worse product than doing both.
Fuse ranks, not scores. Observed BM25 scores run ~8–11 and are unbounded; cosine distance is 0–2. They cannot be averaged, thresholded or compared. Reciprocal rank fusion sidesteps this entirely by operating on rank position. This is the main implementation trap.
Client-side first, and issue the two searches concurrently. Measured on a real agent run: 7,057 ms total, tool calls ~1,200 ms wall each against 49–79 ms of engine execution — so ~1,100 ms per call is round trip. Sequential fan-out would add a full extra round trip; concurrent fan-out costs roughly one. Engine-side fusion removes it (see #37 item 2) but should come after the parameters are settled here, because the port is then a spec rather than a design exercise.
Open parameters to settle empirically: the RRF constant, the dedup key (row id presumably), tie-breaking, and whether to expose per-pathway scores in the payload for debuggability.
Depends on
create_indexon the client (hotdata-framework: client gaps blocking agent use (error opacity, create_index, id-first, workspace default) #36 item 2) for self-provisioning. Neither blocks the tool itself.