---
title: "Where keyword search and vector search each fail"
description: "We indexed 10,003 real banking questions and ran the same queries through BM25 and vector search. Each retriever fails where the other is strong, and rank fusion fixes both. The full runnable walkthrough is in the docs."
canonical: https://infino.ai/blog/hybrid-search-on-parquet/
published: 2026-09-08
---

# Where keyword search and vector search each fail

Sep 8, 2026 · engineering

We indexed 10,003 real questions that customers asked an online bank and ran the same queries through BM25 and vector search. The failures are instructive in both directions, and they are the whole case for hybrid search. This post shows the failures. The [full walkthrough in the docs](https://infino.ai/docs/guides/hybrid-search-on-parquet) is runnable end to end: dataset, embeddings, index build, every query shape, and a read-back of the produced files with a plain Parquet reader.

## The paraphrase that breaks keyword search

BM25 ranks rows by the exact tokens they share with the query. A user asking about a card that never arrived phrases it their own way, and BM25 anchors on "not shown up" and drifts into refund territory by the second hit:

```py
table.bm25_search("text", "my new card still has not shown up", 5)

17.39  Why has my new card still not come?
15.52  My refund has not shown up on my statement.
15.15  my check or cash deposit has not shown up
14.04  My refund has not shown up in my account.   Where is it?
13.42  My statement has not shown my refund.
```

Vector search matches meaning, so the same paraphrase works even where a hit shares almost no vocabulary with the query:

```py
table.vector_search("embedding", qvec, 5, projection=["text", "score"])

0.117  Why has my new card still not come?
0.165  why have i not got my new card?
0.172  I just got a new card how do I get it to start working?
0.180  where is my new card?
0.181  2 weeks ago I ordered my new card.  It isn't here.  What should I do?
```

## The token that breaks vector search

Invert the query and the failure inverts with it. On a term of art, BM25 is precise:

```py
table.bm25_search("text", "SWIFT transfer", 5, projection=["text", "score"])

9.46  Is a SWIFT transfer acceptable?
9.46  Can I transfer using SWIFT?
9.46  Can I Transfer from SWIFT?
9.08  Can I do a SWIFT transfer?
8.72  is there a fee for swift transfer
```

Vector search has no notion of an exact token. A rare identifier, a product name, or an error code can dissolve into its embedding neighborhood, outranked by rows that are merely on topic. A real query stream contains both shapes daily. Whichever single retriever you pick, some of your users lose.

## Run both, fuse the rankings

Hybrid search runs both retrievers and merges the two ranked lists with reciprocal rank fusion, so a row both retrievers surface outranks a row only one of them found:

```py
q = "top up with Apple Pay"
table.hybrid_search("text", q, "embedding", model.encode(q).tolist(), 5,
                    projection=["text", "score"])

0.0323  Is top-up possible with Apple Pay?
0.0320  How can I top up with Apple Pay?
0.0308  Am I able to top up with Apple Pay?
0.0161  My Apple Pay top up with my American Express card is not working?
0.0161  is there a way to do top up with apple pay
```

The exact-token half of the query (Apple Pay) and the intent half (topping up) each pull their weight. Neither retriever alone ranks this list this way. If you want the concept in depth, [what is hybrid search?](https://infino.ai/blog/what-is-hybrid-search/) covers the two retrievers and the fusion math.

All of this ran directly on Apache Parquet files. The index lives inside the files, they stay readable by any plain Parquet reader with Infino nowhere in the read path, and the same code targets S3 by changing one string. The [docs guide](https://infino.ai/docs/guides/hybrid-search-on-parquet) is the complete walkthrough: install, dataset, embeddings, the index build, filtered search, SQL composition over search results, and the read-back. [Inside a Parquet superfile](https://infino.ai/blog/search-index-inside-a-parquet-file/) covers how the format carries an index without breaking compatibility, and [Building a hybrid RAG pipeline over company documents](https://infino.ai/docs/guides/hybrid-rag-over-documents) applies the same retrieval to a folder of policies and runbooks.

## Common questions

### Can I run hybrid search directly on Parquet files?

Yes. Infino writes BM25 and vector indexes into the Parquet files it stores, and hybrid\_search fuses both rankings in one call. The files stay valid Parquet, so a plain Parquet reader still opens them, and the same code runs on local disk or S3. [The runnable docs guide](https://infino.ai/docs/guides/hybrid-search-on-parquet).

### Why does keyword search fail on paraphrased questions?

BM25 ranks rows by the exact tokens they share with the query. Asked "my new card still has not shown up", it anchored on "not shown up" and returned refund questions from the second hit on.

### Why does vector search miss exact terms?

An embedding has no notion of an exact token. A rare identifier, a product name, or an error code can dissolve into its neighborhood and be outranked by rows that are merely on topic.

### Does hybrid search on Parquet need a separate vector database?

No. The vector index lives inside the same Parquet files as the text and the keyword index, so there is no second store to load or keep in sync.

### Can I run hybrid search locally or offline?

Yes. Infino is a library that runs inside your application, so a table can live in a folder on local disk with no server and no network access, and the same code moves to S3 by changing the connect string. It runs on macOS and Linux. [Embedded search engines compared](https://infino.ai/blog/embedded-full-text-and-vector-search/).
