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.OpenAIThe 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);
});
Join the conversation! Your thoughts help the community grow.