> ## 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](/docs/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](/docs/sql-reference)               | Compose over the same rows          |

| Mode                          | Ranked  | Text pre-filter | Compose in SQL      |
| ----------------------------- | ------- | --------------- | ------------------- |
| BM25                          | ✓       | n/a             | ✓ (`bm25_search`)   |
| Vector kNN                    | ✓       | ✓ (pushdown)    | ✓ (`vector_search`) |
| Hybrid                        | ✓ (RRF) | n/a             | ✓ (`hybrid_search`) |
| `token_match` / `exact_match` | n/a     | n/a             | ✓                   |
| SQL                           | 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::{Bm25SearchOptions, BoolMode};
  let hits = docs.bm25_search(
      "body", "cancel subscription", 5,
      Bm25SearchOptions::new().with_mode(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](/docs/guides/embeddings)).

<CodeGroup>
  ```python Python icon="python" theme={null}
  hits = docs.vector_search("embedding", embed("cancel subscription"), 5)
  ```

  ```typescript Node.js icon="node-js" theme={null}
  const hits = docs.vectorSearch("embedding", embed("cancel subscription"), 5);
  ```

  ```rust Rust icon="rust" theme={null}
  let hits = docs.vector_search("embedding", &q, 5, 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                    |
| `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, BoolMode};
  let hits = docs.vector_search("embedding", &q, 5,
      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](/docs/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) is optional:
  hits = docs.hybrid_search("body", "cancel subscription", "embedding",
                            embed("..."), 5, mode="and")
  ```

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

  ```rust Rust icon="rust" theme={null}
  use infino::BoolMode;
  let hits = docs.hybrid_search(
      "body", "cancel subscription", BoolMode::Or,
      "embedding", &q, 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                        |
| `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](/docs/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. Probe width
  and rerank budget are calibrated by the engine per table and per `k`; there are no
  tuning knobs to set.
* **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](/docs/guides/indexing)
* [Embeddings](/docs/guides/embeddings)
* [SQL Reference](/docs/sql-reference)
