agents / hybrid-search

Two retrievers, one ranking

Keyword and vector search fail in opposite directions. Accuracy comes from running both and fusing the rankings.

infino explain retrievers

Opposite failure modes

  • BM25 is literal. It finds the error code, the part number, the surname, the exact phrase someone typed, and it misses the same idea written another way.
  • Vector search is about meaning. It finds the paraphrase, the synonym, the other language, and it will also hand back things that are merely on topic.
  • Infino runs both over the same pinned snapshot, concurrently, and fuses the two rankings with reciprocal rank fusion.
  • Fusion works on ranks, not raw scores, so a BM25 score and a cosine distance never have to be made comparable. A document both retrievers liked climbs; a document only one saw still gets a place.
one query, one snapshot keyword · BM25 inverted index, in the file catches: codes · names misses: other wording vector · ANN embeddings, in the file catches: paraphrase misses: the literal string both run concurrently reciprocal rank fusion merges by rank position out: one ranked relation · your columns + score

infino bench --retrievers

Measured retriever latency

These are component measurements. A hybrid query runs both retrievers concurrently over the same files, then merges their ranks.

2ms keyword p50 · 10M
5ms vector p50 · 10M
12ms vector p99 · 10M

Warm component measurements at 10M documents. Vector uses Cohere embeddings at 768 dimensions, top-10.

accelerating full-text search queries →

cat disagreement.sql # the accuracy argument, written as a query

Run each retriever on its own and join the two relations on _id. The rows on only one side are the ones a single retriever never returns.

disagreement.sql
-- where do keyword and meaning disagree, and how much does each add

WITH sides AS (
  SELECT CASE
           WHEN v._id IS NULL THEN 'keyword only'   -- literal, no paraphrase
           WHEN k._id IS NULL THEN 'meaning only'   -- the same thing, said differently
           ELSE 'both agreed'
         END       AS found_by,
         k.score   AS bm25,
         v.score   AS cosine
  FROM   bm25_search('tickets', 'body', 'disk full on ingest', 100) k
  FULL OUTER JOIN
         vector_search('tickets', 'embedding', :q, 100) v
      ON v._id = k._id                            -- one snapshot, both sides
)
SELECT   found_by,
         count(*)              AS docs,
         round(avg(bm25), 3)   AS avg_bm25,               -- nulls skip themselves
         round(avg(cosine), 3) AS avg_cosine
FROM     sides
GROUP BY found_by
ORDER BY docs DESC;

-- → one row for both agreed, keyword only, and meaning only

More queries like this →