|
| 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 create a [full-text catalog](/sql/t-sql/statements/create-fulltext-catalog-transact-sql) on your database, and a [full-text index](/sql/t-sql/statements/create-fulltext-index-transact-sql) on the columns you want to search. |
| 17 | + |
| 18 | +### [EF Core 11+](#tab/ef-core-11) |
| 19 | + |
| 20 | +> [!NOTE] |
| 21 | +> Full-text catalog and index management in migrations was introduced in EF Core 11. |
| 22 | +
|
| 23 | +You can configure full-text catalogs and indexes directly in your EF model. When you add a [migration](xref:core/managing-schemas/migrations/index), EF will generate the appropriate SQL to create (or alter) the catalog and index for you. |
| 24 | + |
| 25 | +First, define a full-text catalog on the model, then configure a full-text index on your entity type: |
| 26 | + |
| 27 | +```csharp |
| 28 | +protected override void OnModelCreating(ModelBuilder modelBuilder) |
| 29 | +{ |
| 30 | + modelBuilder.HasFullTextCatalog("ftCatalog"); |
| 31 | + |
| 32 | + modelBuilder.Entity<Article>() |
| 33 | + .HasFullTextIndex(a => a.Contents) |
| 34 | + .HasKeyIndex("PK_Articles") |
| 35 | + .OnCatalog("ftCatalog"); |
| 36 | +} |
| 37 | +``` |
| 38 | + |
| 39 | +The `HasKeyIndex()` method specifies the unique, non-nullable, single-column index used as the full-text key for the table (typically the primary key index). `OnCatalog()` assigns the full-text index to a specific catalog. |
| 40 | + |
| 41 | +You can also configure multiple columns and additional options such as per-column languages and change tracking: |
| 42 | + |
| 43 | +```csharp |
| 44 | +modelBuilder.Entity<Article>() |
| 45 | + .HasFullTextIndex(a => new { a.Title, a.Contents }) |
| 46 | + .HasKeyIndex("PK_Articles") |
| 47 | + .OnCatalog("ftCatalog") |
| 48 | + .WithChangeTracking(FullTextChangeTracking.Manual) |
| 49 | + .HasLanguage("Title", "English") |
| 50 | + .HasLanguage("Contents", "French"); |
| 51 | +``` |
| 52 | + |
| 53 | +The full-text catalog can also be configured as the default catalog, and with accent sensitivity: |
| 54 | + |
| 55 | +```csharp |
| 56 | +modelBuilder.HasFullTextCatalog("ftCatalog") |
| 57 | + .IsDefault() |
| 58 | + .IsAccentSensitive(false); |
| 59 | +``` |
| 60 | + |
| 61 | +### [Older versions](#tab/older-versions) |
| 62 | + |
| 63 | +On older versions of EF Core, you can set up full-text search by adding raw SQL to a migration. Add an empty migration and then edit it to include the full-text catalog and index creation SQL: |
| 64 | + |
| 65 | +```csharp |
| 66 | +protected override void Up(MigrationBuilder migrationBuilder) |
| 67 | +{ |
| 68 | + migrationBuilder.Sql( |
| 69 | + sql: "CREATE FULLTEXT CATALOG ftCatalog AS DEFAULT;", |
| 70 | + suppressTransaction: true); |
| 71 | + |
| 72 | + migrationBuilder.Sql( |
| 73 | + sql: "CREATE FULLTEXT INDEX ON Articles(Contents) KEY INDEX PK_Articles;", |
| 74 | + suppressTransaction: true); |
| 75 | +} |
| 76 | + |
| 77 | +protected override void Down(MigrationBuilder migrationBuilder) |
| 78 | +{ |
| 79 | + migrationBuilder.Sql( |
| 80 | + sql: "DROP FULLTEXT INDEX ON Articles;", |
| 81 | + suppressTransaction: true); |
| 82 | + |
| 83 | + migrationBuilder.Sql( |
| 84 | + sql: "DROP FULLTEXT CATALOG ftCatalog;", |
| 85 | + suppressTransaction: true); |
| 86 | +} |
| 87 | +``` |
| 88 | + |
| 89 | +--- |
| 90 | + |
| 91 | +For more information, see the [SQL Server full-text search documentation](/sql/relational-databases/search/get-started-with-full-text-search). |
| 92 | + |
| 93 | +## Full-text predicates |
| 94 | + |
| 95 | +EF Core supports the `FREETEXT()` and `CONTAINS()` predicates, which are used in `Where()` clauses to filter results. |
| 96 | + |
| 97 | +### FREETEXT() |
| 98 | + |
| 99 | +`FREETEXT()` performs a less strict matching, searching for words based on their meaning, including inflectional forms (such as verb tenses and noun plurals): |
| 100 | + |
| 101 | +```csharp |
| 102 | +var articles = await context.Articles |
| 103 | + .Where(a => EF.Functions.FreeText(a.Contents, "veggies")) |
| 104 | + .ToListAsync(); |
| 105 | +``` |
| 106 | + |
| 107 | +This translates to: |
| 108 | + |
| 109 | +```sql |
| 110 | +SELECT [a].[Id], [a].[Title], [a].[Contents] |
| 111 | +FROM [Articles] AS [a] |
| 112 | +WHERE FREETEXT([a].[Contents], N'veggies') |
| 113 | +``` |
| 114 | + |
| 115 | +You can optionally specify a language term: |
| 116 | + |
| 117 | +```csharp |
| 118 | +var articles = await context.Articles |
| 119 | + .Where(a => EF.Functions.FreeText(a.Contents, "veggies", "English")) |
| 120 | + .ToListAsync(); |
| 121 | +``` |
| 122 | + |
| 123 | +### CONTAINS() |
| 124 | + |
| 125 | +`CONTAINS()` performs more precise matching and supports more sophisticated search criteria, including prefix terms, proximity search, and weighted terms: |
| 126 | + |
| 127 | +```csharp |
| 128 | +// Simple search |
| 129 | +var articles = await context.Articles |
| 130 | + .Where(a => EF.Functions.Contains(a.Contents, "veggies")) |
| 131 | + .ToListAsync(); |
| 132 | + |
| 133 | +// Prefix search (words starting with "vegg") |
| 134 | +var articles = await context.Articles |
| 135 | + .Where(a => EF.Functions.Contains(a.Contents, "\"vegg*\"")) |
| 136 | + .ToListAsync(); |
| 137 | + |
| 138 | +// Phrase search |
| 139 | +var articles = await context.Articles |
| 140 | + .Where(a => EF.Functions.Contains(a.Contents, "\"fresh vegetables\"")) |
| 141 | + .ToListAsync(); |
| 142 | +``` |
| 143 | + |
| 144 | +This translates to: |
| 145 | + |
| 146 | +```sql |
| 147 | +SELECT [a].[Id], [a].[Title], [a].[Contents] |
| 148 | +FROM [Articles] AS [a] |
| 149 | +WHERE CONTAINS([a].[Contents], N'veggies') |
| 150 | +``` |
| 151 | + |
| 152 | +For more information on `CONTAINS()` query syntax, see the [SQL Server CONTAINS documentation](/sql/t-sql/queries/contains-transact-sql). |
| 153 | + |
| 154 | +## Full-text table-valued functions |
| 155 | + |
| 156 | +> [!NOTE] |
| 157 | +> Full-text table-valued functions are being introduced in EF Core 11. |
| 158 | +
|
| 159 | +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. |
| 160 | + |
| 161 | +### FreeTextTable() |
| 162 | + |
| 163 | +`FreeTextTable()` is the table-valued function version of `FreeText()`. It returns `FullTextSearchResult<TEntity>`, which includes both the entity and the ranking value: |
| 164 | + |
| 165 | +```csharp |
| 166 | +var results = await context.Articles |
| 167 | + .Join( |
| 168 | + context.Articles.FreeTextTable<Article, int>("veggies", topN: 10), |
| 169 | + a => a.Id, |
| 170 | + ftt => ftt.Key, |
| 171 | + (a, ftt) => new { Article = a, ftt.Rank }) |
| 172 | + .OrderByDescending(r => r.Rank) |
| 173 | + .ToListAsync(); |
| 174 | + |
| 175 | +foreach (var result in results) |
| 176 | +{ |
| 177 | + Console.WriteLine($"Article {result.Article.Id} with rank {result.Rank}"); |
| 178 | +} |
| 179 | +``` |
| 180 | + |
| 181 | +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()`. |
| 182 | + |
| 183 | +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: |
| 184 | + |
| 185 | +```csharp |
| 186 | +var results = await context.Articles |
| 187 | + .Join( |
| 188 | + context.Articles.FreeTextTable<Article, int>(a => a.Contents, "veggies"), |
| 189 | + a => a.Id, |
| 190 | + ftt => ftt.Key, |
| 191 | + (a, ftt) => new { Article = a, ftt.Rank }) |
| 192 | + .OrderByDescending(r => r.Rank) |
| 193 | + .ToListAsync(); |
| 194 | +``` |
| 195 | + |
| 196 | +... or multiple columns: |
| 197 | + |
| 198 | +```csharp |
| 199 | +var results = await context.Articles |
| 200 | + .FreeTextTable(a => new { a.Title, a.Contents }, "veggies") |
| 201 | + .Select(r => new { Article = r.Value, Rank = r.Rank }) |
| 202 | + .OrderByDescending(r => r.Rank) |
| 203 | + .ToListAsync(); |
| 204 | +``` |
| 205 | + |
| 206 | +### ContainsTable() |
| 207 | + |
| 208 | +`ContainsTable()` is the table-valued function version of `Contains()`, supporting the same sophisticated search syntax while also providing ranking information: |
| 209 | + |
| 210 | +```csharp |
| 211 | +var results = await context.Articles |
| 212 | + .Join( |
| 213 | + context.Articles.ContainsTable<Article, int>( "veggies OR fruits"), |
| 214 | + a => a.Id, |
| 215 | + ftt => ftt.Key, |
| 216 | + (a, ftt) => new { Article = a, ftt.Rank }) |
| 217 | + .OrderByDescending(r => r.Rank) |
| 218 | + .ToListAsync(); |
| 219 | +``` |
| 220 | + |
| 221 | +### Limiting results |
| 222 | + |
| 223 | +Both table-valued functions support a `topN` parameter to limit the number of results: |
| 224 | + |
| 225 | +```csharp |
| 226 | +var results = await context.Articles |
| 227 | + .FreeTextTable(a => a.Contents, "veggies", topN: 10) |
| 228 | + .Select(r => new { Article = r.Value, Rank = r.Rank }) |
| 229 | + .OrderByDescending(r => r.Rank) |
| 230 | + .ToListAsync(); |
| 231 | +``` |
| 232 | + |
| 233 | +### Specifying a language |
| 234 | + |
| 235 | +Both table-valued functions support specifying a language term for linguistic matching: |
| 236 | + |
| 237 | +```csharp |
| 238 | +var results = await context.Articles |
| 239 | + .FreeTextTable(a => a.Contents, "veggies", languageTerm: "English") |
| 240 | + .Select(r => new { Article = r.Value, Rank = r.Rank }) |
| 241 | + .ToListAsync(); |
| 242 | +``` |
| 243 | + |
| 244 | +## When to use predicates vs table-valued functions |
| 245 | + |
| 246 | +Feature | Predicates (`FreeText()`, `Contains()`) | Table-valued functions (`FreeTextTable()`, `ContainsTable()`) |
| 247 | +--------------------------------- | --------------------------------------- | ------------------------------------------------------------- |
| 248 | +Provides ranking | ❌ No | ✅ Yes |
| 249 | +Performance for large result sets | Better for filtering | Better for ranking and sorting |
| 250 | +Combine with other entities | Via joins | Built-in entity result |
| 251 | +Use in `Where()` clause | ✅ Yes | ❌ No (use as a source) |
| 252 | + |
| 253 | +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. |
0 commit comments