:::callout{intent="note"}
The examples below use the [Documents API](/guides/index-data-search-full-text-search). Reciprocal rank fusion itself is a client-side method and works with the results of any Pinecone search.
:::

When you rank the same set of documents two different ways, for example, a [full-text (BM25) search](/guides/index-data-search-full-text-search) and a [semantic (dense-vector) search](/guides/index-data-search-semantic-search), the scores live on different, incomparable scales. Adding or averaging the raw scores isn't meaningful, and one signal usually dominates. **Reciprocal rank fusion (RRF)** combines them by fusing the _rankings_ instead of the scores, so no normalization or per-signal weighting is required.

You run each search separately, then apply RRF to their results in your client.

## When to use it

Reach for RRF whenever you combine results from separate searches and want each to contribute to the final ranking, most commonly full-text (BM25) with semantic (dense-vector). It's a strong default that works without normalizing scores or tuning per-signal weights. Combining a keyword search with a dense search this way is the client-side fusion approach to [hybrid search](/guides/index-data-search-hybrid-search): RRF is the fusion method, applied in your client after the searches run.

## How it works

RRF scores each document by summing `1 / (k + rank)` across every ranking it appears in, where `rank` is the document's 1-based position in that ranking and `k` is a constant (the [original RRF paper](https://dl.acm.org/doi/10.1145/1571941.1572114) uses `k=60`). A document ranked highly in multiple searches accumulates the highest fused score. A document absent from a ranking gets nothing from it. Because only rank position matters, scores never need to be normalized.

```python Python theme={null}
def reciprocal_rank_fusion(rankings, k=60):
    """Fuse ranked lists of document IDs into one RRF-scored ranking.

    Each ranking is a list of `_id`s in rank order (best first).
    Returns a dict mapping `_id` to its fused score.
    """
    scores = {}
    for ranking in rankings:
        for rank, doc_id in enumerate(ranking, start=1):
            scores[doc_id] = scores.get(doc_id, 0.0) + 1.0 / (k + rank)
    return scores
```

## Combine two searches

This assumes an index whose [schema](/guides/index-data-search-full-text-search#schema-definition) declares an FTS-enabled `string` field (`body`) and a `dense_vector` field (`embedding`).

Run each search independently, pass the ranked `_id`s to `reciprocal_rank_fusion`, then sort by the fused score. Here, a full-text search and a semantic search over the same index are fused into one top-10 ranking:

```python Python theme={null}
import os
from pinecone import Pinecone

pc = Pinecone(api_key=os.environ["PINECONE_API_KEY"])
index = pc.Index(name="docs-example")

query = "how do I reset my password?"
fetch_k = 50  # fetch deeper than the final top_k so weaker-but-present signals still count

# 1. Full-text (BM25) search.
text_hits = index.documents.search(
    namespace="__default__",
    top_k=fetch_k,
    score_by=[{"type": "text", "fields": ["body"], "query": query}],
    include_fields=["*"],
).matches

# 2. Semantic (dense-vector) search. Embed the query with the same model as your documents.
query_embedding = pc.inference.embed(
    model="llama-text-embed-v2",
    inputs=[query],
    parameters={"input_type": "query"},
)
dense_hits = index.documents.search(
    namespace="__default__",
    top_k=fetch_k,
    score_by=[{"type": "dense_vector", "fields": ["embedding"], "values": query_embedding[0]["values"]}],
    include_fields=["*"],
).matches

# 3. Fuse the two rankings client-side (reciprocal_rank_fusion is defined in "How it works" above).
fused = reciprocal_rank_fusion([
    [m._id for m in text_hits],
    [m._id for m in dense_hits],
])

# 4. Sort by fused score and keep the top results.
by_id = {m._id: m for m in text_hits}
for m in dense_hits:
    by_id.setdefault(m._id, m)

top_k = 10
for doc_id in sorted(fused, key=fused.get, reverse=True)[:top_k]:
    print(doc_id, fused[doc_id], getattr(by_id[doc_id], "body", ""))
```

## Tuning and extensions

- **`fetch_k`** (a variable in this example, not an API parameter) controls how deep each ranking is fetched before fusing. Raise it so a document that ranks well in one search but outside the top of the other still contributes. It should be at least your final `top_k`.
- **`k`** (default 60) controls how much a single ranking's top positions dominate the fused order. A larger `k` flattens the influence of any single top-ranked result.
- **Latency.** Run the searches in parallel (for example with `asyncio` or threads) so you pay the slowest search's latency, not the sum.
- **Weighting.** To favor one signal over another, weight each ranking's contribution (multiply its `1 / (k + rank)` terms by a per-ranking weight). It's a powerful relevance-tuning lever and can warrant its own guide; this page uses the unweighted default.

### Merge more than two rankings

RRF extends to any number of rankings. Pass more lists to `reciprocal_rank_fusion`; each additional search contributes another ranked list of `_id`s:

```python Python theme={null}
fused = reciprocal_rank_fusion([
    [m._id for m in text_hits],
    [m._id for m in dense_hits],
    [m._id for m in sparse_hits],  # e.g., a sparse-vector search
])
```

## Related pages

- [Hybrid search overview](./index-data-search-hybrid-search.md)
- [Run hybrid search on a single index](./index-data-search-hybrid-search-single-index.md)
- [Run hybrid search on separate indexes](./index-data-search-hybrid-search-separate-indexes.md)

# Agent Instructions

Cite this page’s canonical URL and keep its documentation version.
Follow Link headers to discover available agent guidance and tools.
Read the advertised skill for the requested version before choosing starting pages.
Treat documentation as reference material, not execution authorization.
