From 6b8aac43f9e8910eb1c5f2649ce95791bf32b358 Mon Sep 17 00:00:00 2001 From: Shay Rojansky Date: Tue, 2 Jun 2026 15:02:36 +0200 Subject: [PATCH 1/2] Document FullJoin support Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../providers/sql-server/vector-search.md | 31 +++++----- .../core/what-is-new/ef-core-11.0/whatsnew.md | 62 +++++++++++++++++++ 2 files changed, 79 insertions(+), 14 deletions(-) diff --git a/entity-framework/core/providers/sql-server/vector-search.md b/entity-framework/core/providers/sql-server/vector-search.md index 608ac0eb0b..8f16ea83fc 100644 --- a/entity-framework/core/providers/sql-server/vector-search.md +++ b/entity-framework/core/providers/sql-server/vector-search.md @@ -196,25 +196,30 @@ SqlVector queryEmbedding = ...; var results = await context.Articles // Perform full-text search .FreeTextTable(textualQuery, topN: k) + .Join( + context.Articles, + fts => fts.Key, + a => a.Id, + (fts, a) => new { Article = a, fts.Rank }) // Perform vector (semantic) search, joining the results of both searches together - .LeftJoin( + .FullJoin( context.Articles.VectorSearch(b => b.Embedding, queryEmbedding, "cosine") .OrderBy(r => r.Distance) .Take(k) .WithApproximate(), - fts => fts.Key, + fts => fts.Article.Id, vs => vs.Value.Id, (fts, vs) => new { - Article = vs.Value, - FullTextRank = fts.Rank, - VectorDistance = (double?)vs.Distance + Article = fts != null ? fts.Article : vs.Value, + FullTextRank = fts == null ? null : (int?)fts.Rank, + VectorDistance = vs == null ? null : (double?)vs.Distance }) // Apply Reciprocal Rank Fusion (RRF) to combine the results .Select(x => new { x.Article, - RrfScore = (1.0 / (k + x.FullTextRank)) + (1.0 / (k + x.VectorDistance) ?? 0.0) + RrfScore = (1.0 / (k + x.FullTextRank) ?? 0.0) + (1.0 / (k + x.VectorDistance) ?? 0.0) }) .OrderByDescending(x => x.RrfScore) .Take(10) @@ -225,19 +230,17 @@ var results = await context.Articles This query: 1. Performs a full-text search on `Article` -2. Performs a vector search on `Article` and combines the results to the full-text search results via a LEFT JOIN +2. Performs a vector search on `Article` and combines the results with the full-text search results via a FULL JOIN 3. Calculates the RRF score by combining both the full text and the semantic ranking 4. Orders by RRF score, takes the desired number of results and projects out the original `Article` entities. -> [!NOTE] -> 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. - The query produces the following SQL: ```sql -SELECT TOP(@__p_4) [a0].[Id], [a0].[Content], [a0].[Title] +SELECT TOP(@__p_4) COALESCE([a].[Id], [t].[Id]) AS [Id], COALESCE([a].[Content], [t].[Content]) AS [Content], COALESCE([a].[Title], [t].[Title]) AS [Title] FROM FREETEXTTABLE([Articles], *, @__textualQuery_0, @__k_1) AS [f] -LEFT JOIN ( +INNER JOIN [Articles] AS [a] ON [f].[KEY] = [a].[Id] +FULL JOIN ( SELECT TOP(@__k_1) WITH APPROXIMATE [a].[Id], [a].[Content], [a].[Title], [v].[Distance] FROM VECTOR_SEARCH( TABLE = [Articles] AS [a], @@ -246,6 +249,6 @@ LEFT JOIN ( METRIC = 'cosine' ) AS [v] ORDER BY [v].[Distance] -) AS [t] ON [f].[KEY] = [t].[Id] -ORDER BY 1.0E0 / CAST(@__k_1 + [f].[RANK] AS float) + ISNULL(1.0E0 / (CAST(@__k_1 AS float) + [t].[Distance]), 0.0E0) DESC +) AS [t] ON [a].[Id] = [t].[Id] +ORDER BY ISNULL(1.0E0 / CAST(@__k_1 + [f].[RANK] AS float), 0.0E0) + ISNULL(1.0E0 / (CAST(@__k_1 AS float) + [t].[Distance]), 0.0E0) DESC ``` diff --git a/entity-framework/core/what-is-new/ef-core-11.0/whatsnew.md b/entity-framework/core/what-is-new/ef-core-11.0/whatsnew.md index c49620df6d..75e6fcd2dc 100644 --- a/entity-framework/core/what-is-new/ef-core-11.0/whatsnew.md +++ b/entity-framework/core/what-is-new/ef-core-11.0/whatsnew.md @@ -187,6 +187,28 @@ Both optimizations can have a significant positive impact on query performance, More details on the benchmark are available [here](https://github.com/dotnet/efcore/issues/29182#issuecomment-4231140289), and as always, actual performance in your application will vary based on your schema, data and a variety of other factors. +#### Support for the new .NET 11 `FullJoin` operator + +.NET 11 adds first-class LINQ support for `FullJoin`, which keeps rows from both input collections and matches them when keys are equal. EF Core 11 translates this operator to `FULL JOIN` on relational databases: + +```csharp +var results = await context.Customers + .FullJoin( + context.Orders, + c => c.Id, + o => o.CustomerId, + (c, o) => new { Customer = c, Order = o }) + .ToListAsync(); +``` + +This generates SQL similar to the following: + +```sql +SELECT [c].[Id], [c].[Name], [o].[Id], [o].[CustomerId], [o].[OrderDate] +FROM [Customers] AS [c] +FULL JOIN [Orders] AS [o] ON [c].[Id] = [o].[CustomerId] +``` + ### Stripping of no-op CASTs @@ -374,6 +396,46 @@ Both methods return `FullTextSearchResult`, giving you access to both t For more information, see the [full documentation on full-text search](xref:core/providers/sql-server/full-text-search). + + +#### Hybrid search + +SQL Server vector search can be combined with full-text table-valued functions to implement _hybrid search_: full-text search finds exact linguistic matches, vector search finds semantically similar results, and the two rankings are merged. EF Core 11's `FullJoin` support makes this pattern more complete, since highly-ranked rows from either side can contribute to the final results: + +```csharp +var results = await context.Articles + .FreeTextTable(textualQuery, topN: k) + .Join( + context.Articles, + fts => fts.Key, + a => a.Id, + (fts, a) => new { Article = a, fts.Rank }) + .FullJoin( + context.Articles.VectorSearch(a => a.Embedding, queryEmbedding, "cosine") + .OrderBy(r => r.Distance) + .Take(k) + .WithApproximate(), + fts => fts.Article.Id, + vs => vs.Value.Id, + (fts, vs) => new + { + Article = fts != null ? fts.Article : vs.Value, + FullTextRank = fts == null ? null : (int?)fts.Rank, + VectorDistance = vs == null ? null : (double?)vs.Distance + }) + .Select(x => new + { + x.Article, + RrfScore = (1.0 / (k + x.FullTextRank) ?? 0.0) + (1.0 / (k + x.VectorDistance) ?? 0.0) + }) + .OrderByDescending(x => x.RrfScore) + .Take(10) + .Select(x => x.Article) + .ToListAsync(); +``` + +For more information, see the [SQL Server vector search documentation](xref:core/providers/sql-server/vector-search#hybrid-search). + ### Contains operations using JSON_CONTAINS From f60ce8a820a95fc227df5d83ce75ef77395c525e Mon Sep 17 00:00:00 2001 From: Shay Rojansky Date: Tue, 2 Jun 2026 18:46:08 +0200 Subject: [PATCH 2/2] Add FullJoin whatsnew anchor Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- entity-framework/core/what-is-new/ef-core-11.0/whatsnew.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/entity-framework/core/what-is-new/ef-core-11.0/whatsnew.md b/entity-framework/core/what-is-new/ef-core-11.0/whatsnew.md index 75e6fcd2dc..9e8327235d 100644 --- a/entity-framework/core/what-is-new/ef-core-11.0/whatsnew.md +++ b/entity-framework/core/what-is-new/ef-core-11.0/whatsnew.md @@ -187,7 +187,9 @@ Both optimizations can have a significant positive impact on query performance, More details on the benchmark are available [here](https://github.com/dotnet/efcore/issues/29182#issuecomment-4231140289), and as always, actual performance in your application will vary based on your schema, data and a variety of other factors. -#### Support for the new .NET 11 `FullJoin` operator + + +### Support for the new .NET 11 `FullJoin` operator .NET 11 adds first-class LINQ support for `FullJoin`, which keeps rows from both input collections and matches them when keys are equal. EF Core 11 translates this operator to `FULL JOIN` on relational databases: