> ## Documentation Index
> Fetch the complete documentation index at: https://infino.ai/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Search: BM25, vector, hybrid, and SQL retrieval

> Search Infino five ways from one table, BM25 full-text, vector kNN, hybrid, exact lookups, and SQL, and pick the right mode for your retrieval workload.

Infino retrieves from the same table several ways. Pick the mode that matches the
question; you can also compose any of them in [SQL](/sql-reference). Every search returns
Arrow rows, and you can pass a `projection` to choose which columns come back.

## Choose a search mode

| You want to…                           | Use                                 | Why                                 |
| -------------------------------------- | ----------------------------------- | ----------------------------------- |
| Match exact keywords or terms          | [Full-text (BM25)](#full-text-bm25) | Lexical ranking over tokenized text |
| Find by meaning or paraphrase          | [Vector kNN](#vector-search)        | Semantic similarity over embeddings |
| Cover both keyword and semantic intent | [Hybrid](#hybrid-search)            | RRF fuses BM25 and vector rankings  |
| Look up an exact id or value           | [`exact_match`](#unranked-lookups)  | Unranked, precise                   |
| Filter, aggregate, or join             | [SQL](/sql-reference)               | Compose over the same rows          |

| Mode                          | Ranked  | Tunable recall | Text pre-filter | Compose in SQL      |
| ----------------------------- | ------- | -------------- | --------------- | ------------------- |
| BM25                          | ✓       | n/a            | n/a             | ✓ (`bm25_search`)   |
| Vector kNN                    | ✓       | ✓ (`nprobe`)   | ✓ (pushdown)    | ✓ (`vector_search`) |
| Hybrid                        | ✓ (RRF) | ✓              | n/a             | ✓ (`hybrid_search`) |
| `token_match` / `exact_match` | n/a     | n/a            | n/a             | ✓                   |
| SQL                           | n/a     | n/a            | n/a             | native              |

<Tip>
  Always pass a sensible top-`k` on production queries: it bounds both the work and the
  result size.
</Tip>

## Full-text (BM25)

Ranked keyword search over an FTS-indexed column.

<CodeGroup>
  ```python Python icon="python" theme={null}
  hits = docs.bm25_search("body", "cancel subscription", 5, mode="or")
  ```

  ```typescript Node.js icon="node-js" theme={null}
  const hits = docs.bm25Search("body", "cancel subscription", 5, { mode: "or" });
  ```

  ```rust Rust icon="rust" theme={null}
  use infino::BoolMode;
  let hits = docs.bm25_search("body", "cancel subscription", 5, BoolMode::Or, None)?;
  ```
</CodeGroup>

| Argument     | Type         | Default         | Description                                        |
| ------------ | ------------ | --------------- | -------------------------------------------------- |
| `column`     | string       | required        | the FTS-indexed text column                        |
| `query`      | string       | required        | query terms, tokenized by the index                |
| `k`          | int          | required        | number of top results                              |
| `mode`       | `or` / `and` | `or`            | match any term (`or`) or require all terms (`and`) |
| `projection` | string\[]    | `_id` + `score` | columns to return                                  |

<Note>BM25 `score` is a **similarity** — higher is better. (Vector `score` is a distance; the two run in opposite directions.)</Note>

### Counting matches

`count` returns how many rows match a BM25 keyword query, without fetching or ranking
them — cheaper than a search when you only need the tally.

<CodeGroup>
  ```python Python icon="python" theme={null}
  n = docs.count("body", "cancel subscription")            # or mode="and"
  ```

  ```typescript Node.js icon="node-js" theme={null}
  const n = docs.count("body", "cancel subscription");     // or { mode: "and" }
  ```

  ```rust Rust icon="rust" theme={null}
  use infino::BoolMode;
  let n = docs.count("body", "cancel subscription", BoolMode::Or)?;
  ```
</CodeGroup>

### Unranked lookups

When you don't need scoring, `token_match` (rows containing a token) and `exact_match`
(rows whose column equals a value) return every matching row, unranked.

<CodeGroup>
  ```python Python icon="python" theme={null}
  rows = docs.exact_match("doc_id", "1")
  rows = docs.token_match("body", "billing")
  ```

  ```typescript Node.js icon="node-js" theme={null}
  const a = docs.exactMatch("doc_id", "1");
  const b = docs.tokenMatch("body", "billing");
  ```

  ```rust Rust icon="rust" theme={null}
  use infino::BoolMode;
  let a = docs.exact_match("doc_id", "1", None)?;
  let b = docs.token_match("body", "billing", BoolMode::Or, None)?;
  ```
</CodeGroup>

<Note>
  `exact_match` and `token_match` run over an **FTS-indexed** column. Index the column
  you want to match, for example `IndexSpec().fts("doc_id")`, to look it up this way.
</Note>

## Vector search

Semantic search over a vector-indexed column. Embed the query with the **same model** you
used to index (see [Embeddings](/guides/embeddings)).

<CodeGroup>
  ```python Python icon="python" theme={null}
  hits = docs.vector_search("embedding", embed("cancel subscription"), 5)
  # higher recall: probe more IVF partitions
  hits = docs.vector_search("embedding", embed("..."), 5, nprobe=32)
  ```

  ```typescript Node.js icon="node-js" theme={null}
  const hits = docs.vectorSearch("embedding", embed("cancel subscription"), 5);
  // higher recall: probe more IVF partitions
  const more = docs.vectorSearch("embedding", embed("..."), 5, { nprobe: 32 });
  ```

  ```rust Rust icon="rust" theme={null}
  use infino::VectorSearchOptions;
  let hits = docs.vector_search("embedding", &q, 5, VectorSearchOptions::new(), None, None)?;
  // higher recall: probe more IVF partitions
  let more = docs.vector_search("embedding", &q, 5,
      VectorSearchOptions::new().with_nprobe(32), None, None)?;
  ```
</CodeGroup>

| Argument      | Type           | Default         | Description                                                |
| ------------- | -------------- | --------------- | ---------------------------------------------------------- |
| `column`      | string         | required        | the vector-indexed column                                  |
| `query`       | float\[]       | required        | the query vector (same dim as the index)                   |
| `k`           | int            | required        | number of top results                                      |
| `nprobe`      | int            | engine default  | IVF partitions to probe; higher = better recall, more work |
| `rerank_mult` | int            | engine default  | over-fetch multiplier for the exact-rerank stage           |
| `filter`      | text predicate | none            | pushdown pre-filter (below)                                |
| `projection`  | string\[]      | `_id` + `score` | columns to return                                          |

<Note>
  Vector `score` is a **distance**: `0.0` is a perfect match and larger is farther. This is
  the opposite direction from BM25's `score` (a similarity, higher is better). Don't compare
  the two raw scores — [hybrid search](#hybrid-search) fuses them for you by rank.
</Note>

### Pushdown filter

Restrict the kNN to rows whose FTS-indexed column matches a text predicate. This is a
**pre-filter**, where the kNN ranks only among matching rows, not a post-filter on the
top-`k`.

<CodeGroup>
  ```python Python icon="python" theme={null}
  hits = docs.vector_search("embedding", embed("..."), 5,
                            filter_column="body", filter_query="billing")
  ```

  ```typescript Node.js icon="node-js" theme={null}
  const hits = docs.vectorSearch("embedding", embed("..."), 5,
    { filter: { column: "body", query: "billing" } });
  ```

  ```rust Rust icon="rust" theme={null}
  use infino::{VectorFilter, VectorSearchOptions, BoolMode};
  let hits = docs.vector_search("embedding", &q, 5, VectorSearchOptions::new(),
      Some(VectorFilter { column: "body", query: "billing", mode: BoolMode::Or }), None)?;
  ```
</CodeGroup>

For scalar filtering (such as `WHERE source = '...'`) or filtering the results of a
search, query with [SQL](/sql-reference).

## Hybrid search

Hybrid search runs BM25 and vector kNN over the same table and fuses their rankings with
**reciprocal-rank fusion (RRF)**, which is strong when a query has both keyword and
semantic intent. It's a first-class method — pass the text query and the query vector,
and it returns ranked Arrow rows like the other searches:

<CodeGroup>
  ```python Python icon="python" theme={null}
  hits = docs.hybrid_search("body", "cancel subscription", "embedding",
                            embed("cancel subscription"), 5)
  # mode (BM25 boolean mode) and nprobe (vector recall) are optional:
  hits = docs.hybrid_search("body", "cancel subscription", "embedding",
                            embed("..."), 5, mode="and", nprobe=32)
  ```

  ```typescript Node.js icon="node-js" theme={null}
  const hits = docs.hybridSearch("body", "cancel subscription", "embedding",
    embed("cancel subscription"), 5);
  // mode (BM25 boolean mode) and nprobe (vector recall) are optional:
  const tuned = docs.hybridSearch("body", "cancel subscription", "embedding",
    embed("..."), 5, { mode: "and", nprobe: 32 });
  ```

  ```rust Rust icon="rust" theme={null}
  use infino::{BoolMode, VectorSearchOptions};
  let hits = docs.hybrid_search(
      "body", "cancel subscription", BoolMode::Or,
      "embedding", &q, VectorSearchOptions::new(), 5, None,
  )?;
  ```
</CodeGroup>

| Argument        | Type         | Default         | Description                                |
| --------------- | ------------ | --------------- | ------------------------------------------ |
| `text_column`   | string       | required        | the FTS-indexed text column                |
| `text_query`    | string       | required        | query terms for the BM25 side              |
| `vector_column` | string       | required        | the vector-indexed column                  |
| `vector_query`  | float\[]     | required        | the query vector (same dim as the index)   |
| `k`             | int          | required        | number of top results                      |
| `mode`          | `or` / `and` | `or`            | BM25 boolean mode                          |
| `nprobe`        | int          | engine default  | IVF partitions to probe on the vector side |
| `projection`    | string\[]    | `_id` + `score` | columns to return                          |

Hybrid search needs both indexes on the table: an `fts` index on the text column and a
`vector` index on the embedding column. It's also available in [SQL](/sql-reference) via
the `hybrid_search` table function, where you can compose it with joins and filters.

## Limitations

* **Vector search is approximate (IVF).** It trades exactness for speed; raise `nprobe`
  (and `rerank_mult`) to recover recall at some cost in work.
* **Bring your own embeddings.** Embed queries with the same model and dimension you
  indexed with.
* **Filters in vector search are text predicates** over an FTS-indexed column. For scalar
  filters, use SQL.
* **`exact_match` and `token_match` need an FTS-indexed column.**

## See also

* [Indexing](/guides/indexing)
* [Embeddings](/guides/embeddings)
* [SQL Reference](/sql-reference)
