# How to Build Hybrid Semantic Search with RRF Fusion Across Vector Stores

> Build hybrid semantic search across vector stores using Semantica's HybridSearch and RRF Fusion. Get unified results without score normalization. Query multiple vector stores in parallel.

- Repository: [Semantica /semantica](https://github.com/semantica-agi/semantica)
- Tags: how-to-guide
- Published: 2026-08-20

---

**Use Semantica's `HybridSearch` class to query multiple vector stores in parallel and merge their ranked results with Reciprocal Rank Fusion (RRF), producing a single unified result list without normalizing underlying similarity scores.**

The `semantica-agi/semantica` open-source framework provides a modular vector-store abstraction that makes **hybrid semantic search with RRF fusion across vector stores** straightforward. Its `HybridSearch` implementation, located in [`semantica/vector_store/hybrid_search.py`](https://github.com/semantica-agi/semantica/blob/main/semantica/vector_store/hybrid_search.py), orchestrates parallel queries and fuses the returned rankings using Reciprocal Rank Fusion. This article explains exactly how the mechanism works and how to implement it in your own projects.

## Core Mechanics of Hybrid Semantic Search with RRF Fusion

At the center of the pipeline is the **`HybridSearch`** class in [`semantica/vector_store/hybrid_search.py`](https://github.com/semantica-agi/semantica/blob/main/semantica/vector_store/hybrid_search.py). When you issue a query, it forwards the request to every registered vector store, gathers each store’s top-k hits, and merges the separate ranking lists using **Reciprocal Rank Fusion (RRF)**.

The RRF formula implemented in [`semantica/vector_store/hybrid_similarity.py`](https://github.com/semantica-agi/semantica/blob/main/semantica/vector_store/hybrid_similarity.py) scores a document by summing the reciprocal of its rank across all source lists:

```python
score = sum(1.0 / (rrf_k + rank_i) for rank_i in ranks)

```

Here, `rank_i` is the one-based position of the document in a given store’s result list, and `rrf_k` is a constant that controls the decay steepness. The default value in Semantica is **60**. This approach rewards documents that appear high in any single list while also boosting candidates that are retrieved by multiple stores.

Supporting utilities in [`semantica/vector_store/methods.py`](https://github.com/semantica-agi/semantica/blob/main/semantica/vector_store/methods.py) define the available fusion strategies, including RRF and an optional weighted-average mode.

## Architecture of Multi-Store Hybrid Search

The hybrid search pipeline follows a clear six-step flow:

1. **Configure stores.** Create concrete vector-store objects—such as `FaissStore`, `PineconeStore`, or `WeaviateStore`—and register them in a `VectorStoreRegistry` defined in [`semantica/vector_store/registry.py`](https://github.com/semantica-agi/semantica/blob/main/semantica/vector_store/registry.py).

2. **Issue a query.** Call `HybridSearch.search(query, k=10, fusion="rrf")`. The method dispatches the query to every store’s `search` method in parallel.

3. **Gather per-store results.** Each store returns a list of `SearchResult` objects containing an identifier, a raw similarity score, and metadata, as specified by the abstract base class in [`semantica/vector_store/vector_store.py`](https://github.com/semantica-agi/semantica/blob/main/semantica/vector_store/vector_store.py).

4. **Apply RRF.** The internal fusion logic—handled by `HybridSimilarityCalculator` in [`semantica/vector_store/hybrid_similarity.py`](https://github.com/semantica-agi/semantica/blob/main/semantica/vector_store/hybrid_similarity.py)—computes the aggregated RRF score for every unique document across the lists.

5. **Deduplicate.** Documents sharing the same identifier are collapsed. The highest RRF score is retained, or scores are combined according to the configured strategy inside [`hybrid_search.py`](https://github.com/semantica-agi/semantica/blob/main/hybrid_search.py).

6. **Return unified list.** The final ranked list is sorted by the fused score and truncated to the requested `k` results.

## Why RRF Is the Default Fusion Strategy

Semantica uses RRF as the default fusion mechanism for three practical reasons:

- **Score agnosticism.** RRF operates on ranks rather than raw similarity values, so it works even when individual stores use different metrics such as cosine similarity or inner product.
- **Robustness.** No pre-normalization is required; the formula naturally handles heterogeneous score distributions across backends.
- **Performance.** The fusion step is linear in the total number of hits and adds negligible overhead compared to the underlying vector search latency.

## Configuring Fusion Strategies

While RRF is the default, Semantica supports a **weighted-average** fusion mode for cases where you trust certain backends more than others. You can switch strategies by changing the `fusion` argument when calling `search`.

The `HybridSearch` constructor also accepts an `rrf_k` parameter that lets you tune the decay constant. A lower value makes the fusion more sensitive to top ranks, while the default of 60 provides a balanced curve for most use cases.

## Code Examples for Hybrid Semantic Search Across Vector Stores

### Basic RRF-Based Hybrid Search

```python
from semantica.vector_store.registry import VectorStoreRegistry
from semantica.vector_store.hybrid_search import HybridSearch

# 1. Register the stores you want to query

registry = VectorStoreRegistry()
registry.register("faiss", FaissStore(path="faiss_index"))
registry.register("pinecone", PineconeStore(index_name="my-index"))
registry.register("weaviate", WeaviateStore(url="http://localhost:8080"))

# 2. Create the hybrid search object

hybrid = HybridSearch(registry=registry, top_k=20, rrf_k=60)

# 3. Run a query – RRF fusion is the default

query = "What are the recent advances in graph neural networks?"
results = hybrid.search(query, k=10)

for rank, hit in enumerate(results, start=1):
    print(f"{rank}. {hit.id} (score={hit.score:.4f})")

```

The `HybridSearch` class internally calls each store’s `search` method and merges the rankings using the RRF formula.

### Using a Custom RRF Constant

```python

# Use a more aggressive decay (k=30) so higher ranks matter more

hybrid = HybridSearch(registry=registry, top_k=20, rrf_k=30)
results = hybrid.search(query, k=10, fusion="rrf")

```

### Switching to Weighted-Average Fusion

```python
weights = {"faiss": 0.5, "pinecone": 0.3, "weaviate": 0.2}
hybrid = HybridSearch(registry=registry, top_k=20)

results = hybrid.search(
    query,
    k=10,
    fusion="weighted",
    weights=weights
)

```

### Adding a New Vector Store

```python
from semantica.vector_store.qdrant_store import QdrantStore

registry.register("qdrant", QdrantStore(collection_name="docs"))

# No code changes are required in the fusion layer.

# The next call to search automatically includes Qdrant.

```

### Inspecting Intermediate Per-Store Rankings

```python

# Enable verbose inspection of each store's raw results

results, per_store = hybrid.search(
    query, k=10, return_intermediate=True
)

print("--- Per-store rankings ---")
for store_name, hits in per_store.items():
    print(f"{store_name}: {[hit.id for hit in hits]}")

```

The `return_intermediate` flag, implemented in `HybridSearch.search`, is useful for debugging and unit-testing RRF behavior.

## Summary

- **`HybridSearch`** in [`semantica/vector_store/hybrid_search.py`](https://github.com/semantica-agi/semantica/blob/main/semantica/vector_store/hybrid_search.py) orchestrates parallel queries across any number of registered vector stores.
- **RRF fusion** merges per-store rankings without requiring score normalization, using the formula `score = Σ 1 / (rrf_k + rank_i)` with a default `rrf_k` of 60.
- The architecture is modular: new stores are added via `VectorStoreRegistry` without modifying the fusion logic.
- Alternative **weighted-average** fusion is available via the `fusion="weighted"` parameter for store-specific trust weighting.
- Debug intermediate rankings by passing `return_intermediate=True` to `search`.

## Frequently Asked Questions

### What is the exact RRF scoring formula used in Semantica?

Semantica computes the fused score by summing the reciprocal of each document’s one-based rank plus a constant: `score = Σ 1 / (rrf_k + rank_i)`. According to the source code in [`semantica/vector_store/hybrid_similarity.py`](https://github.com/semantica-agi/semantica/blob/main/semantica/vector_store/hybrid_similarity.py), the default constant is 60, which provides a smooth decay that balances high-single-store rank with multi-store presence.

### Can I mix vector stores that use different similarity metrics?

Yes. Because RRF operates on ordinal ranks rather than raw similarity values, it is agnostic to the underlying metric. Whether a store uses cosine similarity, inner product, or Euclidean distance, `HybridSearch` treats only the position of each result in the list.

### How do I tune the RRF constant `rrf_k`?

Pass a custom value to the `HybridSearch` constructor, such as `rrf_k=30` for a steeper decay that privileges top-ranked items, or a larger value for a gentler curve. The parameter is consumed by the fusion logic in [`semantica/vector_store/hybrid_search.py`](https://github.com/semantica-agi/semantica/blob/main/semantica/vector_store/hybrid_search.py) and applied during the RRF calculation.

### When should I use weighted-average fusion instead of RRF?

Use `fusion="weighted"` when you have prior knowledge that certain backends are more reliable or complete than others. You supply a `weights` dictionary mapping store names to floats, and the system combines raw scores accordingly. For general heterogeneous deployments, RRF remains the recommended default because it requires no score calibration.