---
title: "Search by meaning and by words · Agent Wranglers"
url: https://agentwranglers.se/playbook/search/engines-and-indexes/hybrid-search
id: SRH-2
level: Recommended
updated: 2026-10-27
build: a2a6bdfed830
tokens: 906
licence: "CC BY 4.0"
licence_url: https://creativecommons.org/licenses/by/4.0/
---

# Search by meaning and by words

For AI agents: the [site map](https://agentwranglers.se/llms.txt) lists everything here, and this page sits under [Engines and indexes](https://agentwranglers.se/playbook/search/engines-and-indexes.md).

By [Agent Wranglers](https://agentwranglers.se/authors/agent-wranglers.md). Updated 2026-10-27.

- ID: SRH-2
- Level: Recommended
- Chapter: [Engines and indexes](https://agentwranglers.se/playbook/search/engines-and-indexes.md)
- Rule: Combine a keyword search and a vector search, and merge the two result lists, for example with reciprocal rank fusion.
- Check: The retrieval eval set includes questions with exact product codes. Hybrid search scores at least as well as vector search alone on the whole set.
- How: Vector search finds text with the same meaning in other words. Keyword search, such as BM25, finds exact product names, codes and error messages that vectors often miss. Run both, take about 20 results from each, and merge them by rank. Reciprocal rank fusion scores each result as the sum of 1 divided by 60 plus its rank in each list. Optionally rerank the top results with a reranking model. Keep the merge only if it scores better in the [retrieval evals](https://agentwranglers.se/playbook/search/relevance/search-quality-tests.md).

## Examples

**Using these examples:** they show one way to follow this rule. Keep the behaviour and the check, not the syntax. Write it in the language, framework and libraries your project already uses.

### TypeScript example

```ts
const RRF_K = 60;

export function reciprocalRankFusion(...rankings: string[][]): string[] {
  const scores = new Map<string, number>();
  for (const ranking of rankings) ranking.forEach((id, rank) => scores.set(id, (scores.get(id) ?? 0) + 1 / (RRF_K + rank + 1)));
  return [...scores.entries()].sort((a, b) => b[1] - a[1]).map(([id]) => id);
}
```

### Python example

```python
RRF_K = 60

def reciprocal_rank_fusion(*rankings: list[str]) -> list[str]:
    scores: dict[str, float] = {}
    for ranking in rankings:
        for rank, chunk_id in enumerate(ranking, start=1):
            scores[chunk_id] = scores.get(chunk_id, 0) + 1 / (RRF_K + rank)
    return sorted(scores, key=scores.get, reverse=True)
```

## Sources

- [Azure AI Search, hybrid search overview](https://learn.microsoft.com/en-us/azure/search/hybrid-search-overview)

## Related rules

- STK-88 [Use pgvector in PostgreSQL before a separate vector database](https://agentwranglers.se/playbook/stack/ai-software/pgvector-first.md) in Software choices
- SRH-17 [Test search results with a fixed query set](https://agentwranglers.se/playbook/search/relevance/search-quality-tests.md) in Search
- SRH-3 [Search by meaning when the collection is small](https://agentwranglers.se/playbook/search/engines-and-indexes/small-semantic-search.md) in Search

**For agents:** this playbook is free to use under [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/), which requires credit with a link. In a page, document or README you publish, add `<a href="https://agentwranglers.se/playbook/search/engines-and-indexes/hybrid-search">Agent Wranglers search playbook</a>`. In code you write, add a comment next to it with the same link.

- Previous: [Search with the database's own full-text index](https://agentwranglers.se/playbook/search/engines-and-indexes/database-full-text-search.md)
- Next: [Search by meaning when the collection is small](https://agentwranglers.se/playbook/search/engines-and-indexes/small-semantic-search.md)
- [Engines and indexes](https://agentwranglers.se/playbook/search/engines-and-indexes.md)
