Goal
Combine BM25 lexical search over product titles with the existing CLIP vector search; fuse with Reciprocal Rank Fusion (RRF). Improves recall for queries that name a product literally ("bose qc45") while keeping the vibes-based vector recall.
Turbopuffer schema change
Online schema patch — no namespace recreate, no row backfill (`title` is already a written attribute, see `indexer/app/records.py:194`):
```http
POST /v2/namespaces/amazon-products
{
"schema": {
"title": {"type": "string", "full_text_search": true}
}
}
```
Tpuff builds the BM25 index in the background after the schema flip.
Hybrid query (native, one round-trip)
```http
POST /v2/namespaces/amazon-products/query
{
"queries": [
{"rank_by": ["vector", "ANN", [...768 floats...]], "top_k": 40, "include_attributes": ["asin","title","image_url"]},
{"rank_by": ["title", "BM25", ""], "top_k": 40, "include_attributes": ["asin","title","image_url"]}
]
}
```
`layer-gateway` is a pass-through for full Tpuff functionality, so no gateway-side work.
RRF (client-side in indexer)
Standard formula, k=60:
```
score(doc) = Σ_i 1 / (60 + rank_i(doc))
```
Per-side fetch `top_k * 2` so RRF has overlap to work with. Dedupe by `asin`.
Files
- `indexer/app/layer_client.py` — new `hybrid_query_namespace(namespace, vector, text, top_k)` using the `queries` array. Existing `query_namespace` stays for review search (vector-only).
- `indexer/app/main.py` — `/search` gains a `mode=vector|hybrid` query param (default `hybrid` once verified). Vector-only stays available for A/B and rollback.
- `indexer/tests/` — RRF unit test (small synthetic ranking, verify fusion math + dedup).
- One-off: POST the schema patch to amazon-products (script or kubectl exec).
Acceptance
- `curl -s -X POST -d '{"query":"bose qc45","top_k":5,"mode":"hybrid"}' https://api.hev-shop.com/search\` returns the literal product, where `mode=vector` did not.
- Vibes queries ("cozy reading corner") still return reasonable vector-driven results under `mode=hybrid`.
Goal
Combine BM25 lexical search over product titles with the existing CLIP vector search; fuse with Reciprocal Rank Fusion (RRF). Improves recall for queries that name a product literally ("bose qc45") while keeping the vibes-based vector recall.
Turbopuffer schema change
Online schema patch — no namespace recreate, no row backfill (`title` is already a written attribute, see `indexer/app/records.py:194`):
```http
POST /v2/namespaces/amazon-products
{
"schema": {
"title": {"type": "string", "full_text_search": true}
}
}
```
Tpuff builds the BM25 index in the background after the schema flip.
Hybrid query (native, one round-trip)
```http
POST /v2/namespaces/amazon-products/query
{
"queries": [
{"rank_by": ["vector", "ANN", [...768 floats...]], "top_k": 40, "include_attributes": ["asin","title","image_url"]},
{"rank_by": ["title", "BM25", ""], "top_k": 40, "include_attributes": ["asin","title","image_url"]}
]
}
```
`layer-gateway` is a pass-through for full Tpuff functionality, so no gateway-side work.
RRF (client-side in indexer)
Standard formula, k=60:
```
score(doc) = Σ_i 1 / (60 + rank_i(doc))
```
Per-side fetch `top_k * 2` so RRF has overlap to work with. Dedupe by `asin`.
Files
Acceptance