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

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, 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. 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 scores a document by summing the reciprocal of its rank across all source lists:

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 define the available fusion strategies, including RRF and an optional weighted-average mode.

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.

  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.

  4. Apply RRF. The internal fusion logic—handled by HybridSimilarityCalculator in 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.

  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

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


# 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

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

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


# 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 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, 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 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.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →