For twenty years, database queries in .NET have been exact. You write a WHERE clause. The database returns rows that match. If you search for customers where city equals "London," you get customers in London. If the data says "Londres" or "the capital of England," you get nothing.
That limitation is no longer acceptable.
When a user types "how do I reset my password" into your support portal, the system should find articles about account recovery and login problems, even if none of them contain the exact phrase "reset my password." This is semantic search. And with EF Core 10, you can do it directly through LINQ.
No separate vector database. No external search engine. No new ORM. Just EF Core, doing what it has always done, but now with an understanding of meaning.
What Is Vector Search
Traditional queries match text literally. Vector search matches text by meaning.
Here is how it works. An embedding model (like OpenAI's text-embedding-3-small) converts text into a vector: a high-dimensional array of numbers that encodes the semantic meaning of that text. Texts with similar meanings produce similar vectors. "Lightweight laptop for travel" and "ultraportable notebook for business trips" share no words, but their vectors are close together in vector space.
To search, you embed the user's query into a vector, then find the stored vectors closest to it using a distance metric like cosine similarity. The result is a ranked list ordered by semantic relevance, not keyword match.
Why EF Core 10 Changes Everything
Before EF Core 10, .NET developers who wanted vector search had two options: use a dedicated vector database (Qdrant, Pinecone, Weaviate) with its own SDK, or use the experimental EFCore.SqlServer.VectorSearch extension with EF 8 or 9.
EF Core 10 eliminates this friction entirely. It provides built-in vector support for SQL Server 2025 and Azure SQL Database. You define vector columns on your entities, store embeddings through standard SaveChangesAsync, and query them through LINQ with EF.Functions.VectorDistance(). Everything lives in the same DbContext you already use.
Setting Up: Project and Packages
You need .NET 10 SDK, SQL Server 2025 (or Azure SQL Database), and an embedding model.
dotnet new webapi -n SemanticSearch
cd SemanticSearch
dotnet add package Microsoft.EntityFrameworkCore.SqlServer
dotnet add package Microsoft.EntityFrameworkCore.Design
dotnet add package Microsoft.Extensions.AI
dotnet add package Microsoft.Extensions.AI.OpenAI
The Entity: Adding a Vector Column
The key change in EF Core 10 is the SqlVector<float> type. This maps to SQL Server 2025's native vector data type. You specify dimensions matching your embedding model. OpenAI's text-embedding-3-small produces 1536 dimensions.
using System.ComponentModel.DataAnnotations.Schema;
using Microsoft.Data.SqlClient;
public class Article
{
public int Id { get; set; }
public string Title { get; set; } = "";
public string Content { get; set; } = "";
public string Category { get; set; } = "";
public DateTime PublishedAt { get; set; }
[Column(TypeName = "vector(1536)")]
public SqlVector<float> Embedding { get; set; }
}
You can also configure it through the Fluent API:
protected override void OnModelCreating(
ModelBuilder modelBuilder)
{
modelBuilder.Entity<Article>(e =>
{
e.Property(a => a.Embedding)
.HasColumnType("vector(1536)");
});
}
Nothing exotic here. A standard entity with a standard property. EF Core handles the rest.
The DbContext
public class AppDbContext : DbContext
{
public AppDbContext(
DbContextOptions<AppDbContext> options)
: base(options) { }
public DbSet<Article> Articles => Set<Article>();
protected override void OnModelCreating(
ModelBuilder modelBuilder)
{
modelBuilder.Entity<Article>(e =>
{
e.Property(a => a.Embedding)
.HasColumnType("vector(1536)");
});
}
}
Registering Services
var builder = WebApplication.CreateBuilder(args);
// Database
builder.Services.AddDbContext<AppDbContext>(opt =>
opt.UseSqlServer(
builder.Configuration
.GetConnectionString("Default")));
// Embedding generator (provider-agnostic)
builder.Services.AddEmbeddingGenerator(
new OpenAIClient(
builder.Configuration["OpenAI:ApiKey"]!)
.GetEmbeddingClient("text-embedding-3-small")
.AsIEmbeddingGenerator());
var app = builder.Build();
The IEmbeddingGenerator<string, Embedding<float>> abstraction from Microsoft.Extensions.AI decouples your code from any specific provider. Switch to Azure OpenAI, Ollama, or Cohere by changing the registration. Your queries do not change.
Generating and Storing Embeddings
Before you can search, you need embeddings in the database.
public class ArticleIndexer
{
private readonly AppDbContext _db;
private readonly IEmbeddingGenerator<string,
Embedding<float>> _embedder;
public ArticleIndexer(
AppDbContext db,
IEmbeddingGenerator<string,
Embedding<float>> embedder)
{
_db = db;
_embedder = embedder;
}
public async Task IndexArticleAsync(
string title, string content, string category)
{
// Combine title + content for richer semantics
var textToEmbed = $"{title}. {content}";
// Generate the embedding
var vector = await _embedder
.GenerateVectorAsync(textToEmbed);
// Store it
_db.Articles.Add(new Article
{
Title = title,
Content = content,
Category = category,
PublishedAt = DateTime.UtcNow,
Embedding = new SqlVector<float>(vector)
});
await _db.SaveChangesAsync();
}
public async Task BulkIndexAsync(
IEnumerable<Article> articles)
{
foreach (var batch in articles.Chunk(100))
{
var texts = batch
.Select(a => $"{a.Title}. {a.Content}")
.ToList();
var embeddings = await _embedder
.GenerateAsync(texts);
for (int i = 0; i < batch.Length; i++)
{
batch[i].Embedding =
new SqlVector<float>(
embeddings[i].Vector);
}
_db.Articles.AddRange(batch);
await _db.SaveChangesAsync();
}
}
}
Two things to notice. First, we combine title and content into a single string for embedding. This produces a vector that captures the semantics of the entire article. Second, bulk indexing processes in batches of 100 to manage memory and respect API rate limits.
Performing Semantic Search
This is where it gets good. The entire pattern is three steps: embed the query, order by distance, take the top results.
app.MapGet("/api/search", async (
string query,
AppDbContext db,
IEmbeddingGenerator<string,
Embedding<float>> embedder) =>
{
// 1. Embed the user's query
var queryVector = new SqlVector<float>(
await embedder.GenerateVectorAsync(query));
// 2. Find the 10 most similar articles
var results = await db.Articles
.OrderBy(a => EF.Functions.VectorDistance(
"cosine", a.Embedding, queryVector))
.Take(10)
.Select(a => new
{
a.Id,
a.Title,
a.Category,
a.PublishedAt
})
.ToListAsync();
return Results.Ok(results);
});
That is it. No special API. No new query syntax. OrderBy + VectorDistance + Take. The EF Core pipeline translates this to SQL Server's native VECTOR_DISTANCE function:
SELECT TOP(10) [a].[Id], [a].[Title],
[a].[Category], [a].[PublishedAt]
FROM [Articles] AS [a]
ORDER BY VECTOR_DISTANCE('cosine',
[a].[Embedding], @queryVector)
It looks like any other EF Core LINQ query. Because it is.
Filtered Vector Search
In production, pure vector search is rarely enough. You need to combine semantic similarity with traditional filters. EF Core handles this naturally:
var results = await db.Articles
.Where(a => a.Category == "Security")
.Where(a => a.PublishedAt >= DateTime.UtcNow
.AddMonths(-6))
.OrderBy(a => EF.Functions.VectorDistance(
"cosine", a.Embedding, queryVector))
.Take(5)
.Select(a => new { a.Id, a.Title, a.PublishedAt })
.ToListAsync();
This filters to "Security" articles from the last six months, then ranks by semantic similarity. The WHERE clauses reduce the search space before vector comparison, which dramatically improves performance on large datasets.
Getting the Distance Score
Sometimes you need the actual similarity score, for example to set a relevance threshold:
var results = await db.Articles
.Select(a => new
{
a.Id,
a.Title,
Distance = EF.Functions.VectorDistance(
"cosine", a.Embedding, queryVector)
})
.OrderBy(r => r.Distance)
.Where(r => r.Distance < 0.3)
.Take(10)
.ToListAsync();
Cosine distance of 0 means identical vectors. Below 0.3 typically indicates strong semantic similarity. Tune this threshold based on your data and embedding model.
Hybrid Search: The Best of Both Worlds
Pure vector search captures meaning but can miss exact keywords. Pure keyword search captures exact terms but misses meaning. Hybrid search combines both.
Consider searching for "EF Core migration error 42." Vector search finds articles about migrations and error handling. Full-text search finds articles mentioning error number 42. Hybrid search finds articles about migration errors that specifically mention error 42. Neither approach alone gets this result.
Hybrid Search on Azure Cosmos DB
EF Core 10 supports hybrid search through Rrf (Reciprocal Rank Fusion), which combines ranked result lists from different search strategies:
var results = await db.Articles
.OrderBy(a => EF.Functions.Rrf(
EF.Functions.FullTextScore(
a.Content, "migration error"),
EF.Functions.VectorDistance(
a.Embedding, queryVector)))
.Take(10)
.ToListAsync();
You can also assign weights to prioritize one strategy over another:
var results = await db.Articles
.OrderBy(a => EF.Functions.Rrf(
new[]
{
EF.Functions.FullTextScore(
a.Content, "migration error"),
EF.Functions.VectorDistance(
a.Embedding, queryVector)
},
weights: new[] { 1, 2 }))
.Take(10)
.ToListAsync();
Here the vector similarity is weighted twice as heavily as keyword matching. Semantic relevance dominates the ranking.
Hybrid Search on SQL Server 2025
SQL Server does not yet have a native RRF function, so you use a two-phase approach:
// Phase 1: Vector search for top candidates
var semanticResults = await db.Articles
.OrderBy(a => EF.Functions.VectorDistance(
"cosine", a.Embedding, queryVector))
.Take(20)
.Select(a => new { a.Id, a.Title })
.ToListAsync();
// Phase 2: Full-text filter on those candidates
var semanticIds = semanticResults
.Select(r => r.Id).ToList();
var hybridResults = await db.Articles
.Where(a => semanticIds.Contains(a.Id))
.Where(a => EF.Functions.FreeText(
a.Content, "migration error"))
.ToListAsync();
Vector search narrows the candidates. Full-text search refines them. Clean and effective.
Building a RAG Pipeline
This is the primary use case for vector search. Embed the question, retrieve relevant documents, inject them into the LLM prompt, generate a grounded response.
public class RagService
{
private readonly AppDbContext _db;
private readonly IEmbeddingGenerator<string,
Embedding<float>> _embedder;
private readonly IChatClient _chat;
public RagService(
AppDbContext db,
IEmbeddingGenerator<string,
Embedding<float>> embedder,
IChatClient chat)
{
_db = db;
_embedder = embedder;
_chat = chat;
}
public async Task<string> AskAsync(string question)
{
// 1. Embed the question
var queryVector = new SqlVector<float>(
await _embedder
.GenerateVectorAsync(question));
// 2. Retrieve relevant articles
var context = await _db.Articles
.OrderBy(a => EF.Functions.VectorDistance(
"cosine", a.Embedding, queryVector))
.Take(5)
.Select(a => new { a.Title, a.Content })
.ToListAsync();
// 3. Build the prompt
var contextText = string.Join("\n\n",
context.Select(c =>
$"Title: {c.Title}\n{c.Content}"));
var prompt = $"""
Based on the following articles,
answer the user's question.
If the answer is not in the articles,
say you don't know.
Articles:
{contextText}
Question: {question}
""";
// 4. Generate response
var response = await _chat
.GetResponseAsync(prompt);
return response.Text;
}
}
Three abstractions, three providers, zero coupling. Change your embedding model without touching retrieval. Change your LLM without touching embeddings. Change your database without touching generation.
Performance: What You Need to Know
Distance metrics. For text embeddings, use cosine. It is the standard across most embedding providers and works well with normalized vectors.
Storage. Each float costs 4 bytes. A 1536-dimensional vector consumes about 6 KB per row. A million rows adds roughly 6 GB for the vector column alone. If storage matters, evaluate smaller models: text-embedding-3-small supports reduction to 512 dimensions (2 KB per vector) with modest accuracy loss.
Indexing. Without an index, vector search scans the entire table. For datasets under 100,000 rows, this is fine. For larger datasets, SQL Server 2025 provides DiskANN-based approximate nearest neighbor indexes. Note that as of March 2026, EF Core does not yet generate DiskANN indexes through migrations. You need raw SQL for those.
Batching. Never embed one document at a time in production. Use batch embedding (as shown in the BulkIndexAsync method above) and cache embeddings for frequently queried terms.
When to Use EF Core vs. a Dedicated Vector Database
Use EF Core 10 when your dataset is under 10 million vectors, you already use SQL Server, you want one data access layer, and you need joins and transactions alongside vector search.
Use a dedicated vector database (Qdrant, Pinecone, Azure AI Search) when your dataset is massive, you need advanced indexing algorithms (HNSW, IVF), you need multi-tenant vector isolation, or your application is vector-first with minimal relational needs.
The two are not mutually exclusive. You can use EF Core for transactional data with vector columns and a dedicated backend for high-volume retrieval in the same application. Microsoft.Extensions.VectorData provides the abstraction layer that makes this possible.
The Complete Project Structure
SemanticSearch/
Data/
AppDbContext.cs
Models/
Article.cs
Services/
ArticleIndexer.cs
RagService.cs
Endpoints/
SearchEndpoints.cs
Program.cs
appsettings.json
Seven files. Standard .NET project layout. Nothing exotic.
Conclusion
Vector search in EF Core 10 is not a preview feature. It is not an experiment. It is a production capability built into the framework you already use.
You define vector columns with SqlVector<float>. You store embeddings through SaveChangesAsync. You query by similarity through LINQ with VectorDistance. You combine vector and full-text search through Rrf. The programming model is the same one you have used for twenty years. The capability is entirely new.
Any .NET application using SQL Server or Cosmos DB can now add semantic search without deploying additional infrastructure. Support portals that find answers by meaning. Product catalogs that surface items by intent. Knowledge bases that match questions to articles by understanding, not keyword overlap.
The gap between traditional queries and semantic understanding has closed. EF Core 10 closed it.
Start with one entity. Add a vector column. Write one query with VectorDistance. See the results. Then expand from there.