When building search functionality, you often face a trade-off between precision and semantic understanding.
Keyword search gives you precise matches, but it struggles with synonyms and context. Vector search understands meaning, but it can miss exact matches that matter.
Hybrid search combines both approaches, giving you the best of both worlds.
What is Hybrid Search?
Hybrid search computes a final score by combining two signals:
- Keyword relevance (e.g., BM25 / full‑text ranking)
- Vector similarity (e.g., cosine similarity to an embedding)
Then it linearly blends them using weights: final = α · keyword + (1 − α) · vector.
Entity Configuration
EF Core 10 supports Azure Cosmos DB's native vector and full-text search capabilities.
First we need to install the required NuGet package:
Install-Package Microsoft.EntityFrameworkCore.Cosmos
Now we need to configure our entity to enable both search capabilities:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Document>(entity =>
{
entity.ToContainer("DocumentsVector");
entity.Property(x => x.Id)
.ToJsonProperty("Id");
entity.HasPartitionKey("Id");
entity.Property(x => x.Content).EnableFullTextSearch();
entity.HasIndex(x => x.Content).IsFullTextIndex();
entity.Property(x => x.Embedding)
.IsVectorProperty(DistanceFunction.Cosine, dimensions: 1024);
});
}
This configuration enables full-text search on the Content property and vector search on the Embedding property.
NOTE: Ensure the vector dimensions and distance function match your Cosmos DB container's vector embedding policy configuration.
Azure Cosmos DB Setup
Configure the connection and register the DbContext:
builder.Services.AddDbContextPool<ApplicationDbContext>(options =>
{
options.UseCosmos(
builder.Configuration.GetConnectionString("CosmosDb"),
builder.Configuration["CosmosDb:DatabaseName"]);
});
In your appsettings.json, provide the Cosmos DB connection details:
{
"ConnectionStrings": {
"CosmosDb": "AccountEndpoint=https://<your-account>.documents.azure.com:443/;AccountKey=<your-key>"
},
"CosmosDb": {
"DatabaseName": "<your-database-name>"
},
"Ollama": {
"Url": "http://localhost:11434/"
}
}
Generating Embeddings
For generating the embeddings, I used local AI models powered by the Microsoft.Extensions.AI.Ollama package.
To use Ollama, we first need to download and install it.
Now we need to install the model which we plan to use for generating our embeddings. You can do this with the following command:
ollama pull mxbai-embed-large
I ended up going with the mxbai-embed-large model.
Now, we just need to register our embedding generator to make it available throughout the application.
builder.Services.AddEmbeddingGenerator(
new OllamaEmbeddingGenerator(builder.Configuration["Ollama:Url"], "mxbai-embed-large"));
"Ollama": {
"Url": "http://localhost:11434/"
}
With this, we are ready to generate our embeddings. The model creates solid 1024-dimensional vectors perfect for semantic search.
app.MapPost("/api/search", async (
string query,
ApplicationDbContext db,
IEmbeddingGenerator<string, Embedding<float>> embeddingGenerator) =>
{
var embeddingResult = await embeddingGenerator.GenerateAsync([query]);
var queryVector = embeddingResult.Single().Vector.ToArray();
});
You can generate them by injecting IEmbeddingGenerator<string, Embedding<float>> and using its GenerateAsync method.
Ollama handles all the work under the hood, so we don’t need any external services or additional infrastructure.
Hybrid Search
Now we can perform hybrid search entirely in LINQ. EF Core translates the full-text and vector operations natively to Cosmos DB's hybrid search capabilities.
app.MapPost("/api/hybrid-search", async (
string query,
ApplicationDbContext db,
IEmbeddingGenerator<string, Embedding<float>> embeddingGenerator) =>
{
var embeddingResult = await embeddingGenerator.GenerateAsync([query]);
var queryVector = embeddingResult.Single().Vector.ToArray();
var keywords = query.Split(' ', StringSplitOptions.RemoveEmptyEntries);
var results = await db.Documents
.OrderBy(x => EF.Functions.Rrf(
EF.Functions.FullTextScore(x.Content, keywords),
EF.Functions.VectorDistance(x.Embedding, queryVector)))
.Take(20)
.AsNoTracking()
.ToListAsync();
return Results.Ok(results);
});
The RRF function combines the BM25 full-text score with the vector similarity distance, producing a unified ranking that leverages both keyword precision and semantic understanding.
NOTE: I've written a dedicated blog post on Vector Similarity Search with EF 10 Core and SQL Server 2025 that you can check out here if you're interested: Vector Similarity Search in EF Core 10.
Weighted Hybrid Search with RRF
For more control over the balance between keyword and vector search, you can assign custom weights to each search method using RRF. This allows you to emphasize semantic understanding or keyword precision based on your use case.
// Hybrid search with custom weights
app.MapPost("/api/hybrid-search-weighted", async (
string query,
double[]? weights,
ApplicationDbContext db,
IEmbeddingGenerator<string, Embedding<float>> embeddingGenerator) =>
{
var embeddingResult = await embeddingGenerator.GenerateAsync([query]);
var queryVector = embeddingResult.Single().Vector.ToArray();
var keywords = query.Split(' ', StringSplitOptions.RemoveEmptyEntries);
var searchWeights = weights ?? [1.0, 1.0];
var results = await db.Documents
.OrderBy(x => EF.Functions.Rrf(
new[]
{
EF.Functions.FullTextScore(x.Content, query),
EF.Functions.VectorDistance(x.Embedding, queryVector)
},
weights: searchWeights))
.Take(20)
.AsNoTracking()
.ToListAsync();
return Results.Ok(results);
});
By default, both search methods have equal weight (1.0, 1.0).
You can adjust these weights to give more importance to either full-text search or vector search, depending on which signal is more valuable for your specific scenario.
Conclusion
Hybrid search gives you the precision of keywords and the understanding of vectors.
EF Core 10 and Cosmos DB let you enable both full-text and vector search, you can generate embeddings with Ollama, and rank results using RRF
Optionally you can weight keywords or vectors so that you get the best of both worlds with minimal setup.
If you want to check out examples I created, you can find the source code here:
Source CodeI hope you enjoyed it, subscribe and get a notification when a new blog is up!
