Skip to content

Commit a7351ce

Browse files
committed
Document new SQL Server vector and full-text search features (#5258)
See dotnet/efcore#37536 See dotnet/efcore#37538 See dotnet/efcore#37578
1 parent 4cd8935 commit a7351ce

5 files changed

Lines changed: 397 additions & 9 deletions

File tree

Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
---
2+
title: Microsoft SQL Server Database Provider - Full-Text Search - EF Core
3+
description: Using full-text search with the Entity Framework Core Microsoft SQL Server database provider
4+
author: roji
5+
ms.date: 02/05/2026
6+
uid: core/providers/sql-server/full-text-search
7+
---
8+
# Full-Text Search in the SQL Server EF Core Provider
9+
10+
SQL Server provides [full-text search](/sql/relational-databases/search/full-text-search) capabilities that enable sophisticated text search beyond simple `LIKE` patterns. Full-text search supports linguistic matching, inflectional forms, proximity search, and weighted ranking.
11+
12+
EF Core's SQL Server provider supports both full-text search *predicates* (for filtering) and *table-valued functions* (for filtering with ranking).
13+
14+
## Setting up full-text search
15+
16+
Before using full-text search, you must:
17+
18+
1. **Create a full-text catalog** on your database
19+
2. **Create a full-text index** on the columns you want to search
20+
21+
This setup is done at the SQL Server level and is outside the scope of EF Core. For more information, see the [SQL Server full-text search documentation](/sql/relational-databases/search/get-started-with-full-text-search).
22+
23+
## Full-text predicates
24+
25+
EF Core supports the `FREETEXT()` and `CONTAINS()` predicates, which are used in `Where()` clauses to filter results.
26+
27+
### FREETEXT()
28+
29+
`FREETEXT()` performs a less strict matching, searching for words based on their meaning, including inflectional forms (such as verb tenses and noun plurals):
30+
31+
```csharp
32+
var articles = await context.Articles
33+
.Where(a => EF.Functions.FreeText(a.Contents, "veggies"))
34+
.ToListAsync();
35+
```
36+
37+
This translates to:
38+
39+
```sql
40+
SELECT [a].[Id], [a].[Title], [a].[Contents]
41+
FROM [Articles] AS [a]
42+
WHERE FREETEXT([a].[Contents], N'veggies')
43+
```
44+
45+
You can optionally specify a language term:
46+
47+
```csharp
48+
var articles = await context.Articles
49+
.Where(a => EF.Functions.FreeText(a.Contents, "veggies", "English"))
50+
.ToListAsync();
51+
```
52+
53+
### CONTAINS()
54+
55+
`CONTAINS()` performs more precise matching and supports more sophisticated search criteria, including prefix terms, proximity search, and weighted terms:
56+
57+
```csharp
58+
// Simple search
59+
var articles = await context.Articles
60+
.Where(a => EF.Functions.Contains(a.Contents, "veggies"))
61+
.ToListAsync();
62+
63+
// Prefix search (words starting with "vegg")
64+
var articles = await context.Articles
65+
.Where(a => EF.Functions.Contains(a.Contents, "\"vegg*\""))
66+
.ToListAsync();
67+
68+
// Phrase search
69+
var articles = await context.Articles
70+
.Where(a => EF.Functions.Contains(a.Contents, "\"fresh vegetables\""))
71+
.ToListAsync();
72+
```
73+
74+
This translates to:
75+
76+
```sql
77+
SELECT [a].[Id], [a].[Title], [a].[Contents]
78+
FROM [Articles] AS [a]
79+
WHERE CONTAINS([a].[Contents], N'veggies')
80+
```
81+
82+
For more information on `CONTAINS()` query syntax, see the [SQL Server CONTAINS documentation](/sql/t-sql/queries/contains-transact-sql).
83+
84+
## Full-text table-valued functions
85+
86+
> [!NOTE]
87+
> Full-text table-valued functions are being introduced in EF Core 11.
88+
89+
While the predicates above are useful for filtering, they don't provide ranking information. SQL Server's table-valued functions [`FREETEXTTABLE()`](/sql/relational-databases/system-functions/freetexttable-transact-sql) and [`CONTAINSTABLE()`](/sql/relational-databases/system-functions/containstable-transact-sql) return both matching rows and a ranking score that indicates how well each row matches the search query.
90+
91+
### FreeTextTable()
92+
93+
`FreeTextTable()` is the table-valued function version of `FreeText()`. It returns `FullTextSearchResult<TEntity>`, which includes both the entity and the ranking value:
94+
95+
```csharp
96+
var results = await context.Articles
97+
.Join(
98+
context.Articles.FreeTextTable<Article, int>("veggies", topN: 10),
99+
a => a.Id,
100+
ftt => ftt.Key,
101+
(a, ftt) => new { Article = a, ftt.Rank })
102+
.OrderByDescending(r => r.Rank)
103+
.ToListAsync();
104+
105+
foreach (var result in results)
106+
{
107+
Console.WriteLine($"Article {result.Article.Id} with rank {result.Rank}");
108+
}
109+
```
110+
111+
Note that you must provide the generic type parameters; `Article` corresponds to the entity type being searched, where `int` is the full-text search key specified when creating the index, and which is returned by `FREETEXTTABLE()`.
112+
113+
The above automatically searches across all columns registered for full-text searching and returns the top 10 matches. You can also provide a specific column to search:
114+
115+
```csharp
116+
var results = await context.Articles
117+
.Join(
118+
context.Articles.FreeTextTable<Article, int>(a => a.Contents, "veggies"),
119+
a => a.Id,
120+
ftt => ftt.Key,
121+
(a, ftt) => new { Article = a, ftt.Rank })
122+
.OrderByDescending(r => r.Rank)
123+
.ToListAsync();
124+
```
125+
126+
... or multiple columns:
127+
128+
```csharp
129+
var results = await context.Articles
130+
.FreeTextTable(a => new { a.Title, a.Contents }, "veggies")
131+
.Select(r => new { Article = r.Value, Rank = r.Rank })
132+
.OrderByDescending(r => r.Rank)
133+
.ToListAsync();
134+
```
135+
136+
### ContainsTable()
137+
138+
`ContainsTable()` is the table-valued function version of `Contains()`, supporting the same sophisticated search syntax while also providing ranking information:
139+
140+
```csharp
141+
var results = await context.Articles
142+
.Join(
143+
context.Articles.ContainsTable<Article, int>( "veggies OR fruits"),
144+
a => a.Id,
145+
ftt => ftt.Key,
146+
(a, ftt) => new { Article = a, ftt.Rank })
147+
.OrderByDescending(r => r.Rank)
148+
.ToListAsync();
149+
```
150+
151+
### Limiting results
152+
153+
Both table-valued functions support a `topN` parameter to limit the number of results:
154+
155+
```csharp
156+
var results = await context.Articles
157+
.FreeTextTable(a => a.Contents, "veggies", topN: 10)
158+
.Select(r => new { Article = r.Value, Rank = r.Rank })
159+
.OrderByDescending(r => r.Rank)
160+
.ToListAsync();
161+
```
162+
163+
### Specifying a language
164+
165+
Both table-valued functions support specifying a language term for linguistic matching:
166+
167+
```csharp
168+
var results = await context.Articles
169+
.FreeTextTable(a => a.Contents, "veggies", languageTerm: "English")
170+
.Select(r => new { Article = r.Value, Rank = r.Rank })
171+
.ToListAsync();
172+
```
173+
174+
## When to use predicates vs table-valued functions
175+
176+
Feature | Predicates (`FreeText()`, `Contains()`) | Table-valued functions (`FreeTextTable()`, `ContainsTable()`)
177+
--------------------------------- | --------------------------------------- | -------------------------------------------------------------
178+
Provides ranking | ❌ No | ✅ Yes
179+
Performance for large result sets | Better for filtering | Better for ranking and sorting
180+
Combine with other entities | Via joins | Built-in entity result
181+
Use in `Where()` clause | ✅ Yes | ❌ No (use as a source)
182+
183+
Use predicates when you simply need to filter results based on full-text search criteria. Use table-valued functions when you need ranking information to order results by relevance or display relevance scores to users.

entity-framework/core/providers/sql-server/functions.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -247,5 +247,6 @@ nullable.GetValueOrDefault(defaultValue) | COALESCE(@nullable, @defaultValue)
247247
## See also
248248

249249
* [Vector Search Function Mappings](xref:core/providers/sql-server/vector-search)
250+
* [Full-Text Search Function Mappings](xref:core/providers/sql-server/full-text-search)
250251
* [Spatial Function Mappings](xref:core/providers/sql-server/spatial#spatial-function-mappings)
251252
* [HierarchyId Function Mappings](xref:core/providers/sql-server/hierarchyid#function-mappings)

entity-framework/core/providers/sql-server/vector-search.md

Lines changed: 148 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,12 @@ uid: core/providers/sql-server/vector-search
77
---
88
# Vector search in the SQL Server EF Core Provider
99

10-
## Vector search
11-
1210
> [!NOTE]
1311
> Vector support was introduced in EF Core 10.0, and is only supported with SQL Server 2025 and above.
1412
15-
The SQL Server vector data type allows storing *embeddings*, which are representation of meaning that can be efficiently searched over for similarity, powering AI workloads such as semantic search and retrieval-augmented generation (RAG).
13+
The SQL Server vector data type allows storing *embeddings*, which are representations of meaning that can be efficiently searched over for similarity, powering AI workloads such as semantic search and retrieval-augmented generation (RAG).
14+
15+
## Setting up vector properties
1616

1717
To use the `vector` data type, simply add a .NET property of type `SqlVector<float>` to your entity type, specifying the dimensions as follows:
1818

@@ -48,9 +48,9 @@ protected override void OnModelCreating(ModelBuilder modelBuilder)
4848

4949
***
5050

51-
Once your property is added and the corresponding column created in the database, you can start inserting embeddings. Embedding generation is done outside of the database, usually via a service, and the details for doing this are out of scope for this documentation. However, [the .NET Microsoft.Extensions.AI libraries](/dotnet/ai/microsoft-extensions-ai) contains [`IEmbeddingGenerator`](/dotnet/ai/microsoft-extensions-ai#create-embeddings), which is an abstraction over embedding generators that has implementations for the major providers.
51+
Once your property is added and the corresponding column created in the database, you can start inserting embeddings. Embedding generation is done outside of the database, usually via a service, and the details for doing this are out of scope for this documentation. However, [the .NET Microsoft.Extensions.AI library](/dotnet/ai/microsoft-extensions-ai) contains [`IEmbeddingGenerator`](/dotnet/ai/microsoft-extensions-ai#create-embeddings), which is an abstraction over embedding generators that has implementations for the major providers.
5252

53-
Once you've chosen your embedding generator and set it up, use it to generate embeddings and insert them as follows
53+
Once you've chosen your embedding generator and set it up, use it to generate embeddings and insert them as follows:
5454

5555
```c#
5656
IEmbeddingGenerator<string, Embedding<float>> embeddingGenerator = /* Set up your preferred embedding generator */;
@@ -64,17 +64,156 @@ context.Blogs.Add(new Blog
6464
await context.SaveChangesAsync();
6565
```
6666

67-
Finally, use the [`EF.Functions.VectorDistance()`](/sql/t-sql/functions/vector-distance-transact-sql) function to perform similarity search for a given user query:
67+
Once you have embeddings saved to your database, you're ready to perform vector similarity search over them.
68+
69+
## Exact search with VECTOR_DISTANCE()
70+
71+
The [`EF.Functions.VectorDistance()`](/sql/t-sql/functions/vector-distance-transact-sql) function computes the *exact* distance between two vectors. Use it to perform similarity search for a given user query:
6872

6973
```c#
7074
var sqlVector = new SqlVector<float>(await embeddingGenerator.GenerateVectorAsync("Some user query to be vectorized"));
71-
var topSimilarBlogs = context.Blogs
75+
var topSimilarBlogs = await context.Blogs
7276
.OrderBy(b => EF.Functions.VectorDistance("cosine", b.Embedding, sqlVector))
7377
.Take(3)
7478
.ToListAsync();
7579
```
7680

81+
This function computes the distance between the query vector and every row in the table, then returns the closest matches. While this provides perfectly accurate results, it can be slow for large datasets because SQL Server must scan all rows and compute distances for each one.
82+
7783
> [!NOTE]
7884
> The built-in support in EF 10 replaces the previous [EFCore.SqlServer.VectorSearch](https://github.com/efcore/EFCore.SqlServer.VectorSearch) extension, which allowed performing vector search before the `vector` data type was introduced. As part of upgrading to EF 10, remove the extension from your projects.
79-
>
80-
> The [`VECTOR_SEARCH()`](/sql/t-sql/functions/vector-search-transact-sql) function (in preview) for approximate search with DiskANN is currently unsupported.
85+
86+
## Approximate search with VECTOR_SEARCH()
87+
88+
> [!WARNING]
89+
> `VECTOR_SEARCH()` and vector indexes are currently experimental features in SQL Server and are subject to change. The APIs in EF Core for these features are also subject to change.
90+
91+
For large datasets, computing exact distances for every row can be prohibitively slow. SQL Server 2025 introduces support for *approximate* search through a [vector index](/sql/t-sql/statements/create-vector-index-transact-sql), which provides much better performance at the expense of returning items that are approximately similar - rather than exactly similar - to the query.
92+
93+
### Vector indexes
94+
95+
To use `VECTOR_SEARCH()`, you must create a vector index on your vector column. Use the `HasVectorIndex()` method in your model configuration:
96+
97+
```csharp
98+
protected override void OnModelCreating(ModelBuilder modelBuilder)
99+
{
100+
modelBuilder.Entity<Blog>()
101+
.HasVectorIndex(b => b.Embedding, "cosine");
102+
}
103+
```
104+
105+
This will generate the following SQL migration:
106+
107+
```sql
108+
CREATE VECTOR INDEX [IX_Blogs_Embedding]
109+
ON [Blogs] ([Embedding])
110+
WITH (METRIC = COSINE)
111+
```
112+
113+
The following distance metrics are supported for vector indexes:
114+
115+
Metric | Description
116+
----------- | -----------
117+
`cosine` | Cosine similarity (angular distance)
118+
`euclidean` | Euclidean distance (L2 norm)
119+
`dot` | Dot product (negative inner product)
120+
121+
Choose the metric that best matches your embedding model and use case. Cosine similarity is commonly used for text embeddings, while euclidean distance is often used for image embeddings.
122+
123+
### Searching with VECTOR_SEARCH()
124+
125+
Once you have a vector index, use the `VectorSearch()` extension method on your `DbSet`:
126+
127+
```csharp
128+
var blogs = await context.Blogs
129+
.VectorSearch(b => b.Embedding, "cosine", embedding, topN: 5)
130+
.ToListAsync();
131+
132+
foreach (var (blog, score) in blogs)
133+
{
134+
Console.WriteLine($"Blog {blog.Id} with score {score}");
135+
}
136+
```
137+
138+
This translates to the following SQL:
139+
140+
```sql
141+
SELECT [v].[Id], [v].[Embedding], [v].[Name]
142+
FROM VECTOR_SEARCH([Blogs], 'Embedding', @__embedding, 'metric = cosine', @__topN)
143+
```
144+
145+
The `topN` parameter specifies the maximum number of results to return.
146+
147+
`VectorSearch()` returns `VectorSearchResult<TEntity>`, which allows you to access both the entity and the computed distance:
148+
149+
```csharp
150+
var searchResults = await context.Blogs
151+
.VectorSearch(b => b.Embedding, "cosine", embedding, topN: 5)
152+
.Where(r => r.Distance < 0.05)
153+
.Select(r => new { Blog = r.Value, Distance = r.Distance })
154+
.ToListAsync();
155+
```
156+
157+
This allows you to filter on the similarity score, present it to users, etc.
158+
159+
## Hybrid search
160+
161+
*Hybrid search* combines vector similarity search with traditional [full-text search](xref:core/providers/sql-server/full-text-search) to deliver more relevant results. Vector search excels at finding semantically similar content, while full-text search is better at exact keyword matching. By combining both approaches and using Reciprocal Rank Fusion (RRF) to merge the results, you can build more intelligent search experiences.
162+
163+
The following example shows how to implement hybrid search using EF Core, combining `FreeTextTable()` and `VectorSearch()` in a single query:
164+
165+
```csharp
166+
var k = 20;
167+
string textualQuery = ...;
168+
SqlVector<float> queryEmbedding = ...;
169+
170+
var results = await context.Articles
171+
// Perform full-text search
172+
.FreeTextTable<Article, int>(textualQuery, topN: k)
173+
// Perform vector (semantic) search, joining the results of both searches together
174+
.LeftJoin(
175+
context.Articles.VectorSearch(b => b.Embedding, queryEmbedding, "cosine", topN: k),
176+
fts => fts.Key,
177+
vs => vs.Value.Id,
178+
(fts, vs) => new
179+
{
180+
Article = vs.Value,
181+
FullTextRank = fts.Rank,
182+
VectorDistance = (double?)vs.Distance
183+
})
184+
// Apply Reciprocal Rank Fusion (RRF) to combine the results
185+
.Select(x => new
186+
{
187+
x.Article,
188+
RrfScore = (1.0 / (k + x.FullTextRank)) + (1.0 / (k + x.VectorDistance) ?? 0.0)
189+
})
190+
.OrderByDescending(x => x.RrfScore)
191+
.Take(10)
192+
.Select(x => x.Article)
193+
.ToListAsync();
194+
```
195+
196+
This query:
197+
198+
1. Performs a full-text search on `Article`
199+
2. Performs a vector search on `Article` and combines the results to the full-text search results via a LEFT JOIN
200+
3. Calculates the RRF score by combining both the full text and the semantic ranking
201+
4. Orders by RRF score, takes the desired number of results and projects out the original `Article` entities.
202+
203+
> [!NOTE]
204+
> Rather than using a LEFT JOIN, a FULL OUTER JOIN would be more suitable for this scenario; this would allow highly-ranking results from either search side to be included in the final result, even if that result does not appear at all on the other side. With the above LEFT JOIN approach, if a result has a very high vector similarity score, it never gets included in the final result if that result doesn't also have a high full-text score. However, EF doesn't currently support FULL OUTER JOIN; upvote [#37633](https://github.com/dotnet/efcore/issues/37633) if this is something you'd like to see supported.
205+
206+
The query produces the following SQL:
207+
208+
```sql
209+
SELECT TOP(@p3) [a0].[Id], [a0].[Content], [a0].[Embedding], [a0].[Title]
210+
FROM FREETEXTTABLE([Articles], *, @p, @p1) AS [f]
211+
LEFT JOIN VECTOR_SEARCH(
212+
TABLE = [Articles] AS [a0],
213+
COLUMN = [Embedding],
214+
SIMILAR_TO = @p2,
215+
METRIC = 'cosine',
216+
TOP_N = @p3
217+
) AS [v] ON [f].[KEY] = [a0].[Id]
218+
ORDER BY 1.0E0 / CAST(10 + [f].[RANK] AS float) + ISNULL(1.0E0 / (10.0E0 + [v].[Distance]), 0.0E0) DESC
219+
```

0 commit comments

Comments
 (0)