Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -105,8 +105,6 @@ public override void Generate(IIndex index, CSharpRuntimeAnnotationCodeGenerator
annotations.Remove(SqlServerAnnotationNames.FillFactor);
annotations.Remove(SqlServerAnnotationNames.SortInTempDb);
annotations.Remove(SqlServerAnnotationNames.DataCompression);
annotations.Remove(SqlServerAnnotationNames.VectorIndexMetric);
annotations.Remove(SqlServerAnnotationNames.VectorIndexType);
annotations.Remove(SqlServerAnnotationNames.FullTextIndex);
annotations.Remove(SqlServerAnnotationNames.FullTextCatalog);
annotations.Remove(SqlServerAnnotationNames.FullTextChangeTracking);
Expand All @@ -128,8 +126,6 @@ public override void Generate(ITableIndex index, CSharpRuntimeAnnotationCodeGene
annotations.Remove(SqlServerAnnotationNames.FillFactor);
annotations.Remove(SqlServerAnnotationNames.SortInTempDb);
annotations.Remove(SqlServerAnnotationNames.DataCompression);
annotations.Remove(SqlServerAnnotationNames.VectorIndexMetric);
annotations.Remove(SqlServerAnnotationNames.VectorIndexType);
annotations.Remove(SqlServerAnnotationNames.FullTextIndex);
annotations.Remove(SqlServerAnnotationNames.FullTextCatalog);
annotations.Remove(SqlServerAnnotationNames.FullTextChangeTracking);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -202,4 +202,12 @@ public class SqlServerLoggingDefinitions : RelationalLoggingDefinitions
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
public EventDefinitionBase? LogMissingViewDefinitionRights;

/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
public EventDefinitionBase? LogVectorSearchWithoutApproximate;
}
15 changes: 15 additions & 0 deletions src/EFCore.SqlServer/Diagnostics/SqlServerEventId.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ private enum Id
DecimalTypeKeyWarning,
SavepointsDisabledBecauseOfMARS,
JsonTypeExperimental, // No longer used
VectorSearchWithoutApproximateWarning,

// Scaffolding events
ColumnFound = CoreEventId.ProviderDesignBaseId,
Expand Down Expand Up @@ -145,6 +146,20 @@ private static EventId MakeTransactionId(Id id)
/// </remarks>
public static readonly EventId SavepointsDisabledBecauseOfMARS = MakeTransactionId(Id.SavepointsDisabledBecauseOfMARS);

private static readonly string QueryPrefix = DbLoggerCategory.Query.Name + ".";

private static EventId MakeQueryId(Id id)
=> new((int)id, QueryPrefix + id);

/// <summary>
/// A <c>VectorSearch</c> query was translated without <c>WithApproximate()</c>.
/// Without <c>WithApproximate()</c>, the query performs an exact brute-force search instead of using any vector index.
/// </summary>
/// <remarks>
/// This event is in the <see cref="DbLoggerCategory.Query" /> category.
/// </remarks>
public static readonly EventId VectorSearchWithoutApproximateWarning = MakeQueryId(Id.VectorSearchWithoutApproximateWarning);

private static readonly string ScaffoldingPrefix = DbLoggerCategory.Scaffolding.Name + ".";

private static EventId MakeScaffoldingId(Id id)
Expand Down
7 changes: 7 additions & 0 deletions src/EFCore.SqlServer/EFCore.SqlServer.baseline.json
Original file line number Diff line number Diff line change
Expand Up @@ -1300,6 +1300,9 @@
},
{
"Member": "static readonly Microsoft.Extensions.Logging.EventId UniqueConstraintFound"
},
{
"Member": "static readonly Microsoft.Extensions.Logging.EventId VectorSearchWithoutApproximateWarning"
}
]
},
Expand Down Expand Up @@ -2662,6 +2665,10 @@
{
"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);",
"Stage": "Experimental"
},
{
"Member": "static System.Linq.IQueryable<T> WithApproximate<T>(this System.Linq.IQueryable<T> source);",
"Stage": "Experimental"
}
]
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -637,4 +637,32 @@ public static void MissingViewDefinitionRightsWarning(

// No DiagnosticsSource events because these are purely design-time messages
}

/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
public static void VectorSearchWithoutApproximateWarning(
this IDiagnosticsLogger<DbLoggerCategory.Query> diagnostics,
string propertyName,
string entityTypeName)
{
var definition = SqlServerResources.LogVectorSearchWithoutApproximate(diagnostics);

if (diagnostics.ShouldLog(definition))
{
definition.Log(diagnostics, propertyName, entityTypeName);
}

if (diagnostics.NeedsEventData(definition, out var diagnosticSourceEnabled, out var simpleLogEnabled))
{
var eventData = new EventData(
definition,
(d, _) => ((EventDefinition<string, string>)d).GenerateMessage(propertyName, entityTypeName));

diagnostics.DispatchEventData(definition, eventData, diagnosticSourceEnabled, simpleLogEnabled);
}
}
}
22 changes: 3 additions & 19 deletions src/EFCore.SqlServer/Extensions/SqlServerIndexExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -466,9 +466,7 @@ public static void SetDataCompression(this IMutableIndex index, DataCompressionT
/// <returns>Whether the index is a vector index.</returns>
[Experimental(EFDiagnostics.SqlServerVectorSearch)]
public static bool IsVectorIndex(this IReadOnlyIndex index)
=> index is RuntimeIndex
? throw new InvalidOperationException(CoreStrings.RuntimeModelMissingData)
: index.FindAnnotation(SqlServerAnnotationNames.VectorIndexMetric) is not null;
=> index.FindAnnotation(SqlServerAnnotationNames.VectorIndexMetric) is not null;

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

/// <summary>
/// Returns the similarity metric for the vector index.
Expand All @@ -490,11 +486,6 @@ public static bool IsVectorIndex(this IReadOnlyIndex index)
[Experimental(EFDiagnostics.SqlServerVectorSearch)]
public static string? GetVectorMetric(this IReadOnlyIndex index, in StoreObjectIdentifier storeObject)
{
if (index is RuntimeIndex)
{
throw new InvalidOperationException(CoreStrings.RuntimeModelMissingData);
}

var annotation = index.FindAnnotation(SqlServerAnnotationNames.VectorIndexMetric);
if (annotation != null)
{
Expand Down Expand Up @@ -551,9 +542,7 @@ public static void SetVectorMetric(this IMutableIndex index, string? metric)
/// <returns>The type of the vector index.</returns>
[Experimental(EFDiagnostics.SqlServerVectorSearch)]
public static string? GetVectorIndexType(this IReadOnlyIndex index)
=> (index is RuntimeIndex)
? throw new InvalidOperationException(CoreStrings.RuntimeModelMissingData)
: (string?)index[SqlServerAnnotationNames.VectorIndexType];
=> (string?)index[SqlServerAnnotationNames.VectorIndexType];

/// <summary>
/// Returns the type of the vector index.
Expand All @@ -564,11 +553,6 @@ public static void SetVectorMetric(this IMutableIndex index, string? metric)
[Experimental(EFDiagnostics.SqlServerVectorSearch)]
public static string? GetVectorIndexType(this IReadOnlyIndex index, in StoreObjectIdentifier storeObject)
{
if (index is RuntimeIndex)
{
throw new InvalidOperationException(CoreStrings.RuntimeModelMissingData);
}

var annotation = index.FindAnnotation(SqlServerAnnotationNames.VectorIndexType);
if (annotation != null)
{
Expand Down
33 changes: 30 additions & 3 deletions src/EFCore.SqlServer/Extensions/SqlServerQueryableExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ public static class SqlServerQueryableExtensions
#region VectorSearch

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

/// <summary>
/// Enables approximate nearest neighbor (ANN) search for a vector search query. This causes <c>WITH APPROXIMATE</c> to
/// be added to the SQL <c>TOP</c> clause, instructing SQL Server to use the vector index for better performance.
/// </summary>
/// <remarks>
/// <para>
/// This method must be called after <c>Take(...)</c> to specify the number of results. Without <c>WithApproximate</c>,
/// vector search performs an exact k-nearest neighbor (kNN) search without using the vector index.
/// </para>
/// </remarks>
/// <seealso href="https://learn.microsoft.com/sql/t-sql/functions/vector-search-transact-sql">
/// SQL Server documentation for <c>VECTOR_SEARCH()</c>.
/// </seealso>
[Experimental(EFDiagnostics.SqlServerVectorSearch)]
public static IQueryable<T> WithApproximate<T>(this IQueryable<T> source)
{
var queryableSource = (IQueryable)source;

return queryableSource.Provider is EntityQueryProvider
? queryableSource.Provider.CreateQuery<T>(
Expression.Call(
method: new Func<IQueryable<T>, IQueryable<T>>(WithApproximate).Method,
queryableSource.Expression))
: throw new InvalidOperationException(CoreStrings.FunctionOnNonEfLinqProvider(nameof(WithApproximate)));
}

#endregion VectorSearch

#region Full-text search TVFs
Expand Down
37 changes: 37 additions & 0 deletions src/EFCore.SqlServer/Properties/SqlServerStrings.Designer.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 10 additions & 0 deletions src/EFCore.SqlServer/Properties/SqlServerStrings.resx
Original file line number Diff line number Diff line change
Expand Up @@ -327,6 +327,10 @@
<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>
<comment>Warning SqlServerEventId.SavepointsDisabledBecauseOfMARS</comment>
</data>
<data name="LogVectorSearchWithoutApproximate" xml:space="preserve">
<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>
<comment>Warning SqlServerEventId.VectorSearchWithoutApproximateWarning string string</comment>
</data>
<data name="MultipleIdentityColumns" xml:space="preserve">
<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>
</data>
Expand Down Expand Up @@ -426,4 +430,10 @@
<data name="VectorSearchRequiresColumn" xml:space="preserve">
<value>VectorSearch() requires a valid vector column.</value>
</data>
<data name="WithApproximateRequiresTake" xml:space="preserve">
<value>WithApproximate() must be called after Take() to specify the number of results.</value>
</data>
<data name="WithApproximateNotSupportedWithSkipAndTake" xml:space="preserve">
<value>WithApproximate() after Skip().Take() is not supported. Use Take().WithApproximate().Skip() instead, or remove Skip().</value>
</data>
</root>
Loading
Loading