Hybrid Search and Reranking in .NET with Qdrant: Advanced RAG
Learn hybrid search in .NET: build an advanced RAG retrieval pipeline with ASP.NET Core, Qdrant, BM25, Reciprocal Rank Fusion, and reranking in C#.
hybrid-search-reranking-dotnet-qdrant
Hybrid Search in .NET
A RAG application can have a powerful LLM, a well-configured vector database, and properly generated embeddings, and still retrieve the wrong information.
Consider an internal assistant. A developer asks, "What is the timeout configured for PaymentGatewayClient in version 3.2?" The knowledge base holds a payment gateway integration guide, an API configuration reference, version 3.2 deployment notes, and general HTTP client documentation. A conventional semantic search may return documents about HTTP clients and payment gateways without prioritizing the exact version-specific configuration.
The problem isn't necessarily the LLM. It may exist much earlier, in the retrieval pipeline. That's where hybrid search in .NET and reranking come in. Instead of relying on vector similarity alone, a production RAG architecture can combine semantic retrieval, keyword retrieval, result fusion, and a second relevance-ranking stage. In this tutorial we'll build that around ASP.NET Core, C#, and Qdrant.
1. Why Vector Search Alone Isn't Enough
Vector search compares embeddings and retrieves documents with similar meaning. It works well when users describe something differently from the source: the query "How can I reduce API response time?" can match a document on "Techniques for improving HTTP request latency."
But queries with exact API names, product identifiers, version numbers, error codes, database column names, or technical acronyms are a problem. A vector system may grasp the general meaning and still fail to prioritize the exact identifier. Microsoft's hybrid search documentation explains how combining keyword and vector retrieval addresses different relevance needs: https://learn.microsoft.com/en-us/azure/search/hybrid-search-overview
2. What Is Hybrid Search?
Hybrid search combines two retrieval approaches:
- Dense retrieval (semantic search) uses embeddings to capture the meaning of a query. "How do I prevent unauthorized access to an API?" can retrieve documents on authentication, authorization, and access control.
- Sparse retrieval (keyword search) matches important terms and identifiers. "PaymentGatewayClient timeout 30000" prioritizes documents containing the exact class name and value. BM25 is a widely used ranking algorithm for this.
| Retrieval method | Main strength |
|---|---|
| Dense search | Semantic understanding |
| BM25 | Exact keyword matching |
| Hybrid search | Broader relevant candidate retrieval |
| Reranking | Improved ordering of retrieved candidates |
Qdrant supports hybrid and multi-stage queries, including dense and sparse retrieval with Reciprocal Rank Fusion: https://qdrant.tech/documentation/search/hybrid-queries/
3. The Architecture
User Question | ASP.NET Core API → Query Processing | +---+----------------+ | | Dense Retrieval BM25 Retrieval (Qdrant) (Keyword Index) | | +---------+----------+ | Result Fusion (RRF) | Reranker | Top Relevant Documents | Context Construction → LLM → Final Response ASP.NET Core receives the question, dense and BM25 retrieval each find candidates, RRF combines the two lists, a reranker evaluates query-document relevance, the application selects the best passages, and the LLM answers from that context. Retrieval and generation are separate engineering problems: a better prompt can't consistently compensate for poor retrieval.
4. Reciprocal Rank Fusion in C
Dense similarity scores and BM25 scores aren't directly comparable (0.84 versus 12.6, say), so adding them is meaningless. Reciprocal Rank Fusion (RRF) combines document positions instead of raw scores:
RRF(d) = sum over result lists r of 1 / (k + rank_r(d)) A document near the top of several lists scores higher. k is a smoothing constant (60 is a common start). Define the models and the fusion:
public sealed record SearchHit(string Id, string Text, string Source); public sealed record FusedHit(SearchHit Hit, double Score); public static class ReciprocalRankFusion { public static IReadOnlyList<FusedHit> Fuse( IReadOnlyList<SearchHit> denseResults, IReadOnlyList<SearchHit> keywordResults, int topK = 10, int k = 60) { var scores = new Dictionary<string, double>(); var documents = new Dictionary<string, SearchHit>(); AddRanks(denseResults); AddRanks(keywordResults); return scores .Select(x => new FusedHit(documents[x.Key], x.Value)) .OrderByDescending(x => x.Score) .Take(topK) .ToList(); void AddRanks(IReadOnlyList<SearchHit> results) { for (var rank = 0; rank < results.Count; rank++) { var document = results[rank]; scores.TryGetValue(document.Id, out var current); scores[document.Id] = current + 1.0 / (k + rank + 1); documents.TryAdd(document.Id, document); } } } } This combines the lists, deduplicates by ID, rewards documents found by both methods, and returns a limited candidate set. Tune k and the candidate count against your evaluation dataset instead of treating them as universal constants.
Important: this is application-level RRF. Qdrant can also fuse on the server through its Query API. Pick one fusion location deliberately, and don't fuse twice.
5. Why Reranking Is the Next Step
Hybrid search improves candidate retrieval but doesn't guarantee the first result is the most relevant. Say it returns 20 documents: some match keywords, others are semantically close, and a few hold the exact configuration. A reranker evaluates the relationship between the query and each candidate more deeply. Initial retrieval prioritizes speed and coverage, while reranking focuses on relevance, and the more expensive model only looks at a small candidate set instead of the whole collection. Qdrant's tutorial explains this multi-stage strategy: https://qdrant.tech/documentation/tutorials-basics/reranking-hybrid-search/
6. Integrate a Reranking API with ASP.NET Core
We'll use Cohere's Rerank API as an example; the pattern adapts to another reranker or a self-hosted model (docs: https://docs.cohere.com/v2/docs/rerank). Load the API key from environment variables or a secret manager, never source code.
using System.Net.Http.Headers; using System.Net.Http.Json; using System.Text.Json.Serialization; public sealed class CohereReranker { private readonly HttpClient _httpClient; private readonly string _apiKey; public CohereReranker(HttpClient httpClient, IConfiguration configuration) { _httpClient = httpClient; _apiKey = configuration["Cohere:ApiKey"] ?? throw new InvalidOperationException("Cohere API key is missing."); } public async Task<IReadOnlyList<SearchHit>> RerankAsync( string query, IReadOnlyList<SearchHit> candidates, int topN = 5, CancellationToken cancellationToken = default) { if (candidates.Count == 0) return []; using var request = new HttpRequestMessage(HttpMethod.Post, "https://api.cohere.com/v2/rerank"); request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", _apiKey); request.Content = JsonContent.Create(new RerankRequest( "rerank-v5.0-fast", query, candidates.Select(x => x.Text).ToArray(), topN)); using var response = await _httpClient.SendAsync(request, cancellationToken); response.EnsureSuccessStatusCode(); var result = await response.Content.ReadFromJsonAsync<RerankResponse>( cancellationToken: cancellationToken); return result?.Results .Where(x => x.Index >= 0 && x.Index < candidates.Count) .OrderByDescending(x => x.RelevanceScore) .Select(x => candidates[x.Index]) .ToList() ?? []; } private sealed record RerankRequest( [property: JsonPropertyName("model")] string Model, [property: JsonPropertyName("query")] string Query, [property: JsonPropertyName("documents")] string[] Documents, [property: JsonPropertyName("top_n")] int TopN); private sealed record RerankResponse( [property: JsonPropertyName("results")] List<RerankItem> Results); private sealed record RerankItem( [property: JsonPropertyName("index")] int Index, [property: JsonPropertyName("relevance_score")] double RelevanceScore); } builder.Services.AddHttpClient<CohereReranker>(); The example uses rerank-v5.0-fast. Check model availability, rate limits, and pricing before deployment. For production, also add timeouts, cancellation propagation, retries for transient failures, concurrency limits, usage monitoring, candidate text length limits, and secure key management. A reranker score is a relevance signal, not a probability that a document is correct.
7. Connect the Pipeline to Your RAG Application
public sealed class RagRetrievalService(IHybridSearchService search, CohereReranker reranker) { public async Task<IReadOnlyList<SearchHit>> RetrieveAsync( string question, CancellationToken cancellationToken) { var dense = await search.SearchDenseAsync(question, limit: 30, cancellationToken); var keyword = await search.SearchKeywordAsync(question, limit: 30, cancellationToken); var candidates = ReciprocalRankFusion.Fuse(dense, keyword, topK: 20); return await reranker.RerankAsync( question, candidates.Select(x => x.Hit).ToList(), topN: 5, cancellationToken); } } IHybridSearchService stands for your retrieval infrastructure; implement its dense and keyword methods against your embedding model and lexical index. Apply tenant, authorization, and document-access filters during retrieval. Reranking is never a substitute for access control.
8. Tune Retrieval Without Inflating Cost
More stages can improve relevance but add processing and operational cost. A starting experiment:
| Component | Initial experiment |
|---|---|
| Dense candidates | 30 |
| BM25 candidates | 30 |
| Fusion | RRF |
| Fused candidates | 20 |
| Reranked documents | 5 |
| LLM context | Selected passages only |
These are starting values, not production settings. The right ones depend on collection size, query complexity, embedding model, hardware, reranker latency, context window, and your evaluation results. More documents don't automatically produce better answers: extra irrelevant context raises token use and adds competing information to the prompt.
9. Evaluate Retrieval Before Calling It Production-Ready
A common mistake is evaluating only the final answer. If retrieval returns the wrong documents, the LLM can still produce a confident, poorly grounded response. Build a small evaluation set of real questions and their expected documents (for example, "What is the PaymentGatewayClient timeout?" → the payment gateway configuration doc). Then measure:
- Recall@K: do relevant documents appear in the top K?
- Precision@K: how many of the top K are relevant?
- MRR: how high does the first relevant document rank?
- NDCG: ranking quality, accounting for positions and relevance grades.
Also track retrieval, reranking, and total latency, token consumption, context size, and grounded answer quality. Run the same questions against dense only, BM25 only, hybrid with RRF, and hybrid with reranking, so you can see whether each component earns its place. Don't report improvements until they're measured on a fixed dataset.
10. Common Production Mistakes
- Poor chunking. Arbitrary chunks lose context. Split on headings, sections, and paragraphs.
- Ignoring metadata filters. Consider ownership, tenant ID, permissions, and version.
- Reranking too many candidates. Hundreds of documents raise latency and cost.
- Arbitrary relevance thresholds. A score of 0.7 doesn't always mean "relevant enough." Calibrate against real evaluation data.
- Skipping evaluation. Five hand-picked questions don't predict real user queries.
- Sending entire documents to the LLM. Retrieve and rerank first, then build a compact context with source references.
Conclusion
A production RAG system needs more than embeddings and an LLM. Dense retrieval provides semantic understanding, BM25 improves exact-term matching, RRF combines candidate rankings, and reranking evaluates relevance before documents reach the LLM. For C# and ASP.NET Core developers, hybrid search in .NET is a practical way to move beyond basic vector search toward more reliable retrieval.
Next, benchmark each stage on real queries, inspect failed results, and optimize on measured relevance, not assumptions. Reranking won't eliminate hallucinations, and simple applications may do fine with vector or hybrid search alone. Add reranking when evaluation shows the ordering gain is worth the latency and cost.
At AI Engineering Foundry, we explore practical AI engineering through .NET, RAG, Qdrant, SQL Server, LLMs, and production-oriented implementations.
Explore more engineering resources:
- Build RAG in C# with ASP.NET Core and Qdrant
- Build an MCP Server in C# That Safely Queries SQL Server
- Explore the AI Database Agent source code on GitHub
Build. Engineer. Ship AI.
Explore: https://aiengfoundry.com/
FAQ
What is hybrid search in RAG?
It combines semantic vector retrieval and keyword retrieval to find relevant documents using both meaning and exact terms.
What's the difference between hybrid search and reranking?
Hybrid search retrieves and combines candidates from different methods. Reranking re-evaluates those candidates and improves their order.
Can I implement hybrid search with C# and Qdrant?
Yes. Qdrant supports dense and sparse vector search, multi-stage queries, and fusion strategies, and a .NET application can orchestrate retrieval through its APIs or client libraries.
What is Reciprocal Rank Fusion?
A ranking method that combines several ranked result lists using document positions, not by adding incompatible scores.
References
- Qdrant: Hybrid and Multi-Stage Queries
- Qdrant: Hybrid Search with Reranking
- Microsoft: Hybrid Search Overview
- Cohere: Rerank API Documentation
Code examples are implementation-focused starting points. Validate model configuration, API behavior, access filters, and performance in your own environment before production deployment.



