memo · ADR 0005 · with 0002
5.Hybrid retrieval, fused by rank, not pure vector search.!!
- The problem
- Ask answers questions over a personal journal, which is dense with exactly what embeddings blur (names, dates, rare words, project codenames) and with exactly what keyword search misses (a question phrased nothing like the note).
- The options
- Pure cosine over embeddings; pure keyword search with Postgres tsvector; both, combined by a weighted sum of scores; both, combined by reciprocal rank fusion; any of these plus a cross-encoder reranker.
- The move
- Vector and keyword branches run against the same table with the same metadata filters, 40 candidates each, de-duplicated by note, then fused on rank with k = 60. Eight chunks go to the model. The keyword branch ORs the question's terms, because the default AND almost never matches a whole question.
- What we accepted
- Fusion on rank throws away score magnitude, so a runaway-best vector match counts the same as a merely good one at the same rank. Every chunk carries both an embedding and a tsvector, and both indexes are paid for on every write. No reranker, so the top three can be in the wrong order while the right chunk sits in the top ten, which eight chunks in the prompt absorbs.
- When we would revisit
- When the eval set shows recall at ten is high but answer quality is low. That signature means retrieval finds it and ordering buries it, and it is the trigger for a cross-encoder. If recall itself is low, reranking will not help; the problem is chunking or the embedding model.
The interview question is “why not just cosine similarity?” and the honest answer is that a dense vector is a lossy average of a whole chunk, and a name that appeared rarely in the embedding model’s training has a weak, unstable representation. Ask about a person and pure vector search returns chunks about people in general. Keyword search is exact where vectors are fuzzy and useless where vectors are strong, so both run.
The follow-up is how to combine them, and that is where most answers fall apart. A cosine distance and a text rank are not on comparable scales, so any weighted sum needs per-corpus tuning that goes stale as the corpus grows. Reciprocal rank fusion sidesteps the problem by fusing on rank alone: a document’s score is the sum of one over k plus its rank in each list. Nothing is normalised and nothing is retuned. It is not optimal. It is robust, and that is the point of choosing it.