Skip to content

Commit 29d5f60

Browse files
authored
Update SQL Server VECTOR_SEARCH() support with WithApproximate LINQ operator (#38144)
1 parent e87c07b commit 29d5f60

19 files changed

Lines changed: 709 additions & 49 deletions

src/EFCore.SqlServer/Design/Internal/SqlServerCSharpRuntimeAnnotationCodeGenerator.cs

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -105,8 +105,6 @@ public override void Generate(IIndex index, CSharpRuntimeAnnotationCodeGenerator
105105
annotations.Remove(SqlServerAnnotationNames.FillFactor);
106106
annotations.Remove(SqlServerAnnotationNames.SortInTempDb);
107107
annotations.Remove(SqlServerAnnotationNames.DataCompression);
108-
annotations.Remove(SqlServerAnnotationNames.VectorIndexMetric);
109-
annotations.Remove(SqlServerAnnotationNames.VectorIndexType);
110108
annotations.Remove(SqlServerAnnotationNames.FullTextIndex);
111109
annotations.Remove(SqlServerAnnotationNames.FullTextCatalog);
112110
annotations.Remove(SqlServerAnnotationNames.FullTextChangeTracking);
@@ -128,8 +126,6 @@ public override void Generate(ITableIndex index, CSharpRuntimeAnnotationCodeGene
128126
annotations.Remove(SqlServerAnnotationNames.FillFactor);
129127
annotations.Remove(SqlServerAnnotationNames.SortInTempDb);
130128
annotations.Remove(SqlServerAnnotationNames.DataCompression);
131-
annotations.Remove(SqlServerAnnotationNames.VectorIndexMetric);
132-
annotations.Remove(SqlServerAnnotationNames.VectorIndexType);
133129
annotations.Remove(SqlServerAnnotationNames.FullTextIndex);
134130
annotations.Remove(SqlServerAnnotationNames.FullTextCatalog);
135131
annotations.Remove(SqlServerAnnotationNames.FullTextChangeTracking);

src/EFCore.SqlServer/Diagnostics/Internal/SqlServerLoggingDefinitions.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,4 +202,12 @@ public class SqlServerLoggingDefinitions : RelationalLoggingDefinitions
202202
/// doing so can result in application failures when updating to a new Entity Framework Core release.
203203
/// </summary>
204204
public EventDefinitionBase? LogMissingViewDefinitionRights;
205+
206+
/// <summary>
207+
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
208+
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
209+
/// any release. You should only use it directly in your code with extreme caution and knowing that
210+
/// doing so can result in application failures when updating to a new Entity Framework Core release.
211+
/// </summary>
212+
public EventDefinitionBase? LogVectorSearchWithoutApproximate;
205213
}

src/EFCore.SqlServer/Diagnostics/SqlServerEventId.cs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ private enum Id
3333
DecimalTypeKeyWarning,
3434
SavepointsDisabledBecauseOfMARS,
3535
JsonTypeExperimental, // No longer used
36+
VectorSearchWithoutApproximateWarning,
3637

3738
// Scaffolding events
3839
ColumnFound = CoreEventId.ProviderDesignBaseId,
@@ -145,6 +146,20 @@ private static EventId MakeTransactionId(Id id)
145146
/// </remarks>
146147
public static readonly EventId SavepointsDisabledBecauseOfMARS = MakeTransactionId(Id.SavepointsDisabledBecauseOfMARS);
147148

149+
private static readonly string QueryPrefix = DbLoggerCategory.Query.Name + ".";
150+
151+
private static EventId MakeQueryId(Id id)
152+
=> new((int)id, QueryPrefix + id);
153+
154+
/// <summary>
155+
/// A <c>VectorSearch</c> query was translated without <c>WithApproximate()</c>.
156+
/// Without <c>WithApproximate()</c>, the query performs an exact brute-force search instead of using any vector index.
157+
/// </summary>
158+
/// <remarks>
159+
/// This event is in the <see cref="DbLoggerCategory.Query" /> category.
160+
/// </remarks>
161+
public static readonly EventId VectorSearchWithoutApproximateWarning = MakeQueryId(Id.VectorSearchWithoutApproximateWarning);
162+
148163
private static readonly string ScaffoldingPrefix = DbLoggerCategory.Scaffolding.Name + ".";
149164

150165
private static EventId MakeScaffoldingId(Id id)

src/EFCore.SqlServer/EFCore.SqlServer.baseline.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1300,6 +1300,9 @@
13001300
},
13011301
{
13021302
"Member": "static readonly Microsoft.Extensions.Logging.EventId UniqueConstraintFound"
1303+
},
1304+
{
1305+
"Member": "static readonly Microsoft.Extensions.Logging.EventId VectorSearchWithoutApproximateWarning"
13031306
}
13041307
]
13051308
},
@@ -2662,6 +2665,10 @@
26622665
{
26632666
"Member": "static System.Linq.IQueryable<Microsoft.EntityFrameworkCore.VectorSearchResult<T>> VectorSearch<T, TVector>(this Microsoft.EntityFrameworkCore.DbSet<T> source, System.Linq.Expressions.Expression<System.Func<T, TVector>> vectorPropertySelector, TVector similarTo, string metric);",
26642667
"Stage": "Experimental"
2668+
},
2669+
{
2670+
"Member": "static System.Linq.IQueryable<T> WithApproximate<T>(this System.Linq.IQueryable<T> source);",
2671+
"Stage": "Experimental"
26652672
}
26662673
]
26672674
},

src/EFCore.SqlServer/Extensions/Internal/SqlServerLoggerExtensions.cs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -637,4 +637,32 @@ public static void MissingViewDefinitionRightsWarning(
637637

638638
// No DiagnosticsSource events because these are purely design-time messages
639639
}
640+
641+
/// <summary>
642+
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
643+
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
644+
/// any release. You should only use it directly in your code with extreme caution and knowing that
645+
/// doing so can result in application failures when updating to a new Entity Framework Core release.
646+
/// </summary>
647+
public static void VectorSearchWithoutApproximateWarning(
648+
this IDiagnosticsLogger<DbLoggerCategory.Query> diagnostics,
649+
string propertyName,
650+
string entityTypeName)
651+
{
652+
var definition = SqlServerResources.LogVectorSearchWithoutApproximate(diagnostics);
653+
654+
if (diagnostics.ShouldLog(definition))
655+
{
656+
definition.Log(diagnostics, propertyName, entityTypeName);
657+
}
658+
659+
if (diagnostics.NeedsEventData(definition, out var diagnosticSourceEnabled, out var simpleLogEnabled))
660+
{
661+
var eventData = new EventData(
662+
definition,
663+
(d, _) => ((EventDefinition<string, string>)d).GenerateMessage(propertyName, entityTypeName));
664+
665+
diagnostics.DispatchEventData(definition, eventData, diagnosticSourceEnabled, simpleLogEnabled);
666+
}
667+
}
640668
}

src/EFCore.SqlServer/Extensions/SqlServerIndexExtensions.cs

Lines changed: 3 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -466,9 +466,7 @@ public static void SetDataCompression(this IMutableIndex index, DataCompressionT
466466
/// <returns>Whether the index is a vector index.</returns>
467467
[Experimental(EFDiagnostics.SqlServerVectorSearch)]
468468
public static bool IsVectorIndex(this IReadOnlyIndex index)
469-
=> index is RuntimeIndex
470-
? throw new InvalidOperationException(CoreStrings.RuntimeModelMissingData)
471-
: index.FindAnnotation(SqlServerAnnotationNames.VectorIndexMetric) is not null;
469+
=> index.FindAnnotation(SqlServerAnnotationNames.VectorIndexMetric) is not null;
472470

473471
/// <summary>
474472
/// Returns the similarity metric for the vector index.
@@ -477,9 +475,7 @@ public static bool IsVectorIndex(this IReadOnlyIndex index)
477475
/// <returns>The similarity metric for the vector index.</returns>
478476
[Experimental(EFDiagnostics.SqlServerVectorSearch)]
479477
public static string? GetVectorMetric(this IReadOnlyIndex index)
480-
=> index is RuntimeIndex
481-
? throw new InvalidOperationException(CoreStrings.RuntimeModelMissingData)
482-
: (string?)index[SqlServerAnnotationNames.VectorIndexMetric];
478+
=> (string?)index[SqlServerAnnotationNames.VectorIndexMetric];
483479

484480
/// <summary>
485481
/// Returns the similarity metric for the vector index.
@@ -490,11 +486,6 @@ public static bool IsVectorIndex(this IReadOnlyIndex index)
490486
[Experimental(EFDiagnostics.SqlServerVectorSearch)]
491487
public static string? GetVectorMetric(this IReadOnlyIndex index, in StoreObjectIdentifier storeObject)
492488
{
493-
if (index is RuntimeIndex)
494-
{
495-
throw new InvalidOperationException(CoreStrings.RuntimeModelMissingData);
496-
}
497-
498489
var annotation = index.FindAnnotation(SqlServerAnnotationNames.VectorIndexMetric);
499490
if (annotation != null)
500491
{
@@ -551,9 +542,7 @@ public static void SetVectorMetric(this IMutableIndex index, string? metric)
551542
/// <returns>The type of the vector index.</returns>
552543
[Experimental(EFDiagnostics.SqlServerVectorSearch)]
553544
public static string? GetVectorIndexType(this IReadOnlyIndex index)
554-
=> (index is RuntimeIndex)
555-
? throw new InvalidOperationException(CoreStrings.RuntimeModelMissingData)
556-
: (string?)index[SqlServerAnnotationNames.VectorIndexType];
545+
=> (string?)index[SqlServerAnnotationNames.VectorIndexType];
557546

558547
/// <summary>
559548
/// Returns the type of the vector index.
@@ -564,11 +553,6 @@ public static void SetVectorMetric(this IMutableIndex index, string? metric)
564553
[Experimental(EFDiagnostics.SqlServerVectorSearch)]
565554
public static string? GetVectorIndexType(this IReadOnlyIndex index, in StoreObjectIdentifier storeObject)
566555
{
567-
if (index is RuntimeIndex)
568-
{
569-
throw new InvalidOperationException(CoreStrings.RuntimeModelMissingData);
570-
}
571-
572556
var annotation = index.FindAnnotation(SqlServerAnnotationNames.VectorIndexType);
573557
if (annotation != null)
574558
{

src/EFCore.SqlServer/Extensions/SqlServerQueryableExtensions.cs

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ public static class SqlServerQueryableExtensions
1818
#region VectorSearch
1919

2020
/// <summary>
21-
/// Search for vectors similar to a given query vector using an approximate nearest neighbors vector search algorithm.
21+
/// Search for vectors similar to a given query vector using the SQL Server <c>VECTOR_SEARCH()</c> function.
2222
/// </summary>
2323
/// <param name="source">The <see cref="DbSet{T}" /> representing the table containing the vector column to query.</param>
2424
/// <param name="vectorPropertySelector">A selector for the vector property on the entity.</param>
@@ -30,8 +30,9 @@ public static class SqlServerQueryableExtensions
3030
/// </param>
3131
/// <remarks>
3232
/// <para>
33-
/// Compose the returned query with <c>OrderBy(r => r.Distance)</c> and <c>Take(...)</c> to limit the results as required
34-
/// for approximate vector search.
33+
/// Use <see cref="WithApproximate{T}" /> after <c>Take(...)</c> to enable approximate nearest neighbor (ANN)
34+
/// search, which uses the vector index for better performance. Without <c>WithApproximate</c>, an exact k-nearest
35+
/// neighbor (kNN) search is performed. Add an explicit <c>OrderBy</c> to control result ordering.
3536
/// </para>
3637
/// </remarks>
3738
/// <seealso href="https://learn.microsoft.com/sql/t-sql/functions/vector-search-transact-sql">
@@ -75,6 +76,32 @@ private static IQueryable<VectorSearchResult<T>> VectorSearch<T, TVector>(
7576
where TVector : unmanaged
7677
=> throw new UnreachableException();
7778

79+
/// <summary>
80+
/// Enables approximate nearest neighbor (ANN) search for a vector search query. This causes <c>WITH APPROXIMATE</c> to
81+
/// be added to the SQL <c>TOP</c> clause, instructing SQL Server to use the vector index for better performance.
82+
/// </summary>
83+
/// <remarks>
84+
/// <para>
85+
/// This method must be called after <c>Take(...)</c> to specify the number of results. Without <c>WithApproximate</c>,
86+
/// vector search performs an exact k-nearest neighbor (kNN) search without using the vector index.
87+
/// </para>
88+
/// </remarks>
89+
/// <seealso href="https://learn.microsoft.com/sql/t-sql/functions/vector-search-transact-sql">
90+
/// SQL Server documentation for <c>VECTOR_SEARCH()</c>.
91+
/// </seealso>
92+
[Experimental(EFDiagnostics.SqlServerVectorSearch)]
93+
public static IQueryable<T> WithApproximate<T>(this IQueryable<T> source)
94+
{
95+
var queryableSource = (IQueryable)source;
96+
97+
return queryableSource.Provider is EntityQueryProvider
98+
? queryableSource.Provider.CreateQuery<T>(
99+
Expression.Call(
100+
method: new Func<IQueryable<T>, IQueryable<T>>(WithApproximate).Method,
101+
queryableSource.Expression))
102+
: throw new InvalidOperationException(CoreStrings.FunctionOnNonEfLinqProvider(nameof(WithApproximate)));
103+
}
104+
78105
#endregion VectorSearch
79106

80107
#region Full-text search TVFs

src/EFCore.SqlServer/Properties/SqlServerStrings.Designer.cs

Lines changed: 37 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/EFCore.SqlServer/Properties/SqlServerStrings.resx

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -327,6 +327,10 @@
327327
<value>Savepoints are disabled because Multiple Active Result Sets (MARS) is enabled. If 'SaveChanges' fails, then the transaction cannot be automatically rolled back to a known clean state. Instead, the transaction should be rolled back by the application before retrying 'SaveChanges'. See https://go.microsoft.com/fwlink/?linkid=2149338 for more information and examples. To identify the code which triggers this warning, call 'ConfigureWarnings(w =&gt; w.Throw(SqlServerEventId.SavepointsDisabledBecauseOfMARS))'.</value>
328328
<comment>Warning SqlServerEventId.SavepointsDisabledBecauseOfMARS</comment>
329329
</data>
330+
<data name="LogVectorSearchWithoutApproximate" xml:space="preserve">
331+
<value>The query uses 'VectorSearch' on property '{property}' of entity type '{entityType}', but 'WithApproximate()' was not specified. The query will perform an exact brute-force search instead of using a vector index. Call 'WithApproximate()' after 'Take()' to use the vector index for better performance. To identify the code which triggers this warning, call 'ConfigureWarnings(w =&gt; w.Throw(SqlServerEventId.VectorSearchWithoutApproximateWarning))'.</value>
332+
<comment>Warning SqlServerEventId.VectorSearchWithoutApproximateWarning string string</comment>
333+
</data>
330334
<data name="MultipleIdentityColumns" xml:space="preserve">
331335
<value>The properties {properties} are configured to use 'Identity' value generation and are mapped to the same table '{table}', but only one column per table can be configured as 'Identity'. Call 'ValueGeneratedNever' in 'OnModelCreating' for properties that should not use 'Identity'.</value>
332336
</data>
@@ -426,4 +430,10 @@
426430
<data name="VectorSearchRequiresColumn" xml:space="preserve">
427431
<value>VectorSearch() requires a valid vector column.</value>
428432
</data>
433+
<data name="WithApproximateRequiresTake" xml:space="preserve">
434+
<value>WithApproximate() must be called after Take() to specify the number of results.</value>
435+
</data>
436+
<data name="WithApproximateNotSupportedWithSkipAndTake" xml:space="preserve">
437+
<value>WithApproximate() after Skip().Take() is not supported. Use Take().WithApproximate().Skip() instead, or remove Skip().</value>
438+
</data>
429439
</root>

0 commit comments

Comments
 (0)