·RAG·2 min read·Mid-level developers

Hybrid Search in RAG with Azure AI Search and BM25 — The .NET Implementation

Vector search alone misses product codes, error messages, and proper nouns. Hybrid search with Reciprocal Rank Fusion fixes that. Here's how it looks in C#.

Pure vector search has a known weak spot: queries that contain the exact answer. Error codes, SKUs, version numbers, proper names. The vector model smears those tokens into meaning, then can't find them again. The fix is hybrid search: BM25 + vectors, results merged by Reciprocal Rank Fusion.

Two .NET paths, depending on your stack.

Azure AI Search has it built in.

var options = new SearchOptions
{
    VectorSearch = new()
    {
        Queries = { new VectorizedQuery(queryEmbedding) { KNearestNeighborsCount = 50, Fields = { "vector" } } }
    },
    QueryType = SearchQueryType.Simple,
    Size = 10
};
var results = await client.SearchAsync<Doc>(queryText, options);

If you pass both queryText (for BM25) and a VectorizedQuery (for vectors), Azure AI Search runs both and merges with RRF automatically. The k parameter for RRF is tunable; default is 60, which is fine for most cases.

For Qdrant / pgvector / others, you do it manually.

var bm25Hits = await bm25Index.SearchAsync(query, top: 50);
var vectorHits = await collection.SearchAsync(qVec, top: 50);

var rrf = new Dictionary<string, double>();
const int k = 60;
foreach (var (hit, rank) in bm25Hits.Select((h, i) => (h, i + 1)))
    rrf[hit.Id] = rrf.GetValueOrDefault(hit.Id) + 1.0 / (k + rank);
foreach (var (hit, rank) in vectorHits.Select((h, i) => (h, i + 1)))
    rrf[hit.Id] = rrf.GetValueOrDefault(hit.Id) + 1.0 / (k + rank);

var topIds = rrf.OrderByDescending(kv => kv.Value).Take(10).Select(kv => kv.Key);

That's the entire RRF algorithm. It's twelve lines.

Where each method actually wins, against a real mixed eval set:

Query type BM25 only Vector only Hybrid (RRF)
Exact-match (SKU, error code) 94% 38% 91%
Paraphrase (different wording) 41% 79% 81%
Mixed natural language 62% 71% 84%
Acronym-heavy 76% 52% 82%

The hybrid column doesn't lose much to the specialist on either end, and dominates on real-world mixed queries.

The RRF k parameter intuition: smaller k (10-30) favours the top of each ranking (good when both indexes are highly precise); larger k (60-100) spreads the influence (good when individual rankings are noisier). The default of 60 is a safe starting point. Tune only if your eval shows movement.

The one trap: keep the two indexes in sync. When a document is deleted, both indexes need to know. The day a chunk lives in your BM25 index but not your vector store, you'll get phantom citations the user can't find. Use a transactional outbox or a sync job — not "best effort".

For Azure AI Search teams, hybrid is free; turn it on. For self-hosted stacks, twelve lines of RRF is the cheapest precision win in your RAG stack today.