# How to Perform Hybrid Search Combining Vector, Graph, and BM25 in VelesDB

> Learn hybrid search in VelesDB by combining vector, graph, and BM25 with Reciprocal Rank Fusion. Control semantic and keyword relevance with vector_weight.

- Repository: [Wiscale/velesdb](https://github.com/cyberlife-coder/velesdb)
- Tags: how-to-guide
- Published: 2026-02-28

---

**VelesDB executes hybrid search by fusing graph-based vector similarity results with BM25 full-text retrieval using Reciprocal Rank Fusion (RRF),可控 via a `vector_weight` parameter that balances semantic and keyword relevance from 0.0 to 1.0.**

VelesDB is an open-source multimodal database that unifies high-dimensional vector search, graph-based approximate nearest neighbor (ANN) indexing, and traditional inverted-index text retrieval. When your application requires results that match both semantic meaning and specific keywords, the built-in hybrid search capability merges these modalities without requiring external orchestration. This guide explains the RRF fusion algorithm implemented in the core engine and provides complete integration examples for Rust, Python, HTTP, and WebAssembly.

## Architecture of the Hybrid Search Pipeline

VelesDB’s hybrid retrieval system combines three distinct layers into a unified query path. The **vector index** performs graph-based ANN search on dense embeddings, while the **BM25 text index** handles sparse, keyword-based retrieval. These parallel streams converge in a **fusion engine** that applies weighted Reciprocal Rank Fusion before returning the final ranked results.

### The Vector Index (Graph-Based ANN)

The vector component utilizes a graph-based approximate nearest neighbor index to perform high-speed similarity search on dense embeddings. When you submit a query vector, the index executes a graph traversal algorithm to identify the closest points in high-dimensional space. According to the source code in [`crates/velesdb-core/src/collection/search/text.rs`](https://github.com/cyberlife-coder/velesdb/blob/main/crates/velesdb-core/src/collection/search/text.rs) (lines 93-202), this subsystem pre-fetches `2 × k` candidates to ensure the fusion stage has sufficient high-quality matches to work with.

### The BM25 Full-Text Index

The text retrieval layer implements a traditional inverted-file index with BM25 weighting, scoring documents based on term frequency and inverse document frequency. Like the vector search, the text index retrieves `2 × k` candidates to maximize recall before the fusion stage. This dual-retrieval strategy ensures that neither modality prematurely filters out potentially relevant results.

### Reciprocal Rank Fusion (RRF) Engine

The fusion logic resides in the `hybrid_search` method and implements the canonical RRF algorithm with a constant `k = 60`. The scoring formula `score = weight / (rank + 60)` combines the rankings from both subsystems without requiring score normalization between the vector distance metrics and BM25 probability scores. The implementation uses a **BinaryHeap** for streaming top-k extraction, guaranteeing **O(n log k)** time complexity where *n* is the total candidate pool.

## How the Fusion Algorithm Works

Understanding the internal scoring mechanism helps you tune the `vector_weight` parameter effectively. The algorithm executes four distinct phases:

1. **Parallel Retrieval** – Both the vector graph index and BM25 text index execute concurrently, each returning `2 × k` candidate document IDs with their respective ranks.

2. **Weighted RRF Accumulation** – For each candidate from the vector results, the engine adds `vector_weight / (vector_rank + 60.0)` to a hash map entry. For text results, it adds `(1.0 - vector_weight) / (text_rank + 60.0)`.

3. **Streaming Top-k Extraction** – Rather than sorting the entire candidate set, the code utilizes a `BinaryHeap<Reverse<(OrderedFloat, u64)>>` to maintain only the best *k* results as it iterates through the fused scores, minimizing memory overhead.

4. **Payload Retrieval** – Finally, the system fetches the complete point data (vectors and JSON payloads) for the top-k IDs and returns a `Vec<SearchResult>`.

The following excerpt from [`crates/velesdb-core/src/collection/search/text.rs`](https://github.com/cyberlife-coder/velesdb/blob/main/crates/velesdb-core/src/collection/search/text.rs) demonstrates the core fusion logic:

```rust
let weight = vector_weight.unwrap_or(0.5).clamp(0.0, 1.0);
let text_weight = 1.0 - weight;

// Fetch 2×k candidates from each subsystem
let vector_results = self.index.search(vector_query, k * 2);
let text_results   = self.text_index.search(text_query, k * 2);

// RRF accumulation with k=60 constant
for (rank, (id, _)) in vector_results.iter().enumerate() {
    let rrf = weight / (rank as f32 + 60.0);
    *fused_scores.entry(*id).or_insert(0.0) += rrf;
}
for (rank, (id, _)) in text_results.iter().enumerate() {
    let rrf = text_weight / (rank as f32 + 60.0);
    *fused_scores.entry(*id).or_insert(0.0) += rrf;
}

// Streaming top-k extraction using min-heap
let mut top_k = BinaryHeap::with_capacity(k + 1);
for (id, score) in fused_scores {
    top_k.push(Reverse((OrderedFloat(score), id)));
    if top_k.len() > k { top_k.pop(); }
}

```

## Implementation Examples by Platform

### Rust SDK (Native Collection API)

For Rust applications using the core library directly, invoke `hybrid_search` on a `Collection` instance. The method signature accepts the query vector, text query, top-k limit, and optional weight.

```rust
use velesdb_core::Collection;

let collection: Collection = db.get_collection("articles")?;
let query_vec = vec![0.12, -0.34, 0.56, /* ... 128 dims total */];
let text_query = "distributed systems".to_string();

// 70% weight to vector similarity, 30% to BM25
let results = collection.hybrid_search(
    &query_vec,
    &text_query,
    10,                // top_k
    Some(0.7),         // vector_weight
)?;

for result in results {
    println!("id={}; fused_score={}", result.id, result.score);
}

```

*Implementation reference:* `hybrid_search` in [`crates/velesdb-core/src/collection/search/text.rs`](https://github.com/cyberlife-coder/velesdb/blob/main/crates/velesdb-core/src/collection/search/text.rs) (lines 93-202).

### HTTP REST API

The server exposes hybrid search via the `/collections/{name}/search/hybrid` endpoint. The request schema is defined in [`crates/velesdb-server/src/types.rs`](https://github.com/cyberlife-coder/velesdb/blob/main/crates/velesdb-server/src/types.rs) (lines 90-106), with the handler implemented in [`crates/velesdb-server/src/handlers/search.rs`](https://github.com/cyberlife-coder/velesdb/blob/main/crates/velesdb-server/src/handlers/search.rs) (lines 339-378).

```http
POST /collections/papers/search/hybrid HTTP/1.1
Host: localhost:8080
Content-Type: application/json

{
  "vector": [0.12, -0.34, 0.56, 0.78, -0.21],
  "query": "neural network architecture",
  "top_k": 5,
  "vector_weight": 0.65,
  "filter": {
    "condition": {
      "type": "eq",
      "field": "published_year",
      "value": 2024
    }
  }
}

```

**Example Response:**

```json
{
  "results": [
    {"id": 42, "score": 0.0841, "payload": {"title": "Attention Mechanisms"}},
    {"id": 17, "score": 0.0783, "payload": {"title": "Transformer Models"}}
  ],
  "timing_ms": 12.4
}

```

### Python SDK

The Python bindings wrap the Rust core via PyO3. Access hybrid search through the `Collection` object using the `hybrid_search` method defined in [`crates/velesdb-python/src/collection.rs`](https://github.com/cyberlife-coder/velesdb/blob/main/crates/velesdb-python/src/collection.rs) (lines 314-335).

```python
from velesdb import VelesDB

db = VelesDB("my_database")
collection = db.get_collection("products")

# Generate or load your embedding vector

vector = [0.1, -0.2, 0.3, 0.4] * 32  # Example 128-dim vector

results = collection.hybrid_search(
    vector=vector,
    query="wireless headphones",
    top_k=10,
    vector_weight=0.8
)

for result in results:
    print(f"ID: {result.id}, Score: {result.score:.4f}")

```

### WebAssembly (JavaScript/TypeScript)

For browser or Node.js environments, the WASM module exposes `hybrid_search` through the collection interface. The binding is located in [`crates/velesdb-wasm/src/lib.rs`](https://github.com/cyberlife-coder/velesdb/blob/main/crates/velesdb-wasm/src/lib.rs) (lines 329-339).

```javascript
import { VelesDB } from "velesdb-wasm";

const db = await VelesDB.open("browser_db");
const collection = await db.collection("documents");

// Create Float32Array from your embedding
const vector = Float32Array.from([0.12, -0.34, /* ... */]);

const results = await collection.hybrid_search(
    vector, 
    "rust programming", 
    8,      // top_k
    0.55    // vector_weight
);

results.forEach(r => {
    console.log(`ID: ${r.id}, Fused Score: ${r.score}`);
});

```

## Advanced Configuration

### Tuning the Vector Weight

The `vector_weight` parameter (default 0.5) directly controls the RRF contribution of the graph-based vector search versus the BM25 text search. When set to `1.0`, only vector rankings contribute to the final score; at `0.0`, only text rankings matter. Values between 0.3 and 0.7 typically yield optimal balance for semantic search applications requiring keyword precision.

### Metadata Filtering

After the fusion stage completes, you can optionally apply JSON-based metadata filters using the `hybrid_search_with_filter` code path. This post-fusion approach ensures that ranking considers both modalities before filtering, preserving result quality compared to pre-filtering individual indices. Supply filters as JSON objects matching the VelesDB filter schema in your API request.

## Summary

- **Reciprocal Rank Fusion** combines graph-based vector ANN and BM25 text search using the formula `weight / (rank + 60)` without requiring score calibration between modalities.
- **Dual Retrieval** fetches `2 × k` candidates from both the vector graph index and text index to maximize recall before fusion.
- **Streaming Optimization** uses a `BinaryHeap` to extract top-k results in **O(n log k)** time without full sorting.
- **Cross-Platform APIs** provide native access via Rust (`Collection::hybrid_search`), Python, HTTP REST, WebAssembly, and mobile SDKs as implemented across `crates/velesdb-core`, `crates/velesdb-python`, `crates/velesdb-wasm`, and `crates/velesdb-mobile`.
- **Configurable Weighting** allows precise control via the `vector_weight` parameter (0.0 to 1.0) to balance semantic similarity against keyword relevance.

## Frequently Asked Questions

### What is the default weight distribution between vector and text search?

By default, VelesDB assigns equal importance to both modalities with a `vector_weight` of **0.5**, meaning the text weight is implicitly **0.5** as well. You can override this default by passing a specific value between 0.0 and 1.0 to the `vector_weight` parameter in any SDK.

### Why does VelesDB use RRF instead of score normalization?

Reciprocal Rank Fusion is robust against the heterogeneity of distance metrics—cosine similarity or Euclidean distance from the vector graph index and probabilistic BM25 scores from the text index have different scales and distributions. RRF requires no calibration or training data to combine these rankings effectively, making it ideal for general-purpose hybrid retrieval.

### How can I filter results after hybrid fusion?

VelesDB supports post-fusion metadata filtering through the `hybrid_search_with_filter` implementation path. After calculating fused RRF scores, the system applies your JSON filter to the top candidate pool, removing entries that do not match your payload constraints while preserving the hybrid ranking order of the remaining results.

### What is the time complexity of the hybrid search operation?

The operation runs in **O(n log k)** time where *n* is the number of unique candidates returned by the vector and text subsystems (typically ≤ 4k). The `BinaryHeap` streaming approach avoids the O(n log n) cost of full sorting, ensuring predictable latency even as candidate pools grow.