Skip to content

Commit 936b8d4

Browse files
committed
Implement support for SQL Server vector indexes
Closes #37281
1 parent 37f5733 commit 936b8d4

File tree

15 files changed

+1020
-283
lines changed

15 files changed

+1020
-283
lines changed

src/EFCore.SqlServer/Extensions/SqlServerEntityTypeBuilderExtensions.cs

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4+
using System.Diagnostics.CodeAnalysis;
5+
using Microsoft.EntityFrameworkCore.Metadata.Builders;
46
using Microsoft.EntityFrameworkCore.SqlServer.Metadata.Internal;
57

68
// ReSharper disable once CheckNamespace
@@ -291,6 +293,111 @@ public static bool CanSetIsTemporal(
291293
bool fromDataAnnotation = false)
292294
=> entityTypeBuilder.CanSetAnnotation(SqlServerAnnotationNames.IsTemporal, temporal, fromDataAnnotation);
293295

296+
/// <summary>
297+
/// Configures a vector index on the specified property for SQL Server.
298+
/// </summary>
299+
/// <remarks>
300+
/// See <see href="https://aka.ms/efcore-docs-modeling">Modeling entity types and relationships</see>, and
301+
/// <see href="https://aka.ms/efcore-docs-sqlserver">Accessing SQL Server and Azure SQL databases with EF Core</see>
302+
/// for more information and examples.
303+
/// </remarks>
304+
/// <typeparam name="TEntity">The entity type being configured.</typeparam>
305+
/// <param name="entityTypeBuilder">The builder for the entity type being configured.</param>
306+
/// <param name="indexExpression">
307+
/// A lambda expression representing the property to create the vector index on
308+
/// (<c>blog => blog.Vector</c>).
309+
/// </param>
310+
/// <param name="metric">
311+
/// <para>
312+
/// A string with the name of the distance metric to use to calculate the distance between the two given vectors.
313+
/// The following distance metrics are supported:
314+
/// </para>
315+
/// <list type="bullet">
316+
/// <item>
317+
/// <term>cosine</term>
318+
/// <description>Cosine distance</description>
319+
/// </item>
320+
/// <item>
321+
/// <term>euclidean</term>
322+
/// <description>Euclidean distance</description>
323+
/// </item>
324+
/// <item>
325+
/// <term>dot</term>
326+
/// <description>(Negative) Dot product</description>
327+
/// </item>
328+
/// </list>
329+
/// </param>
330+
/// <param name="name">The name to assign to the index.</param>
331+
/// <returns>A builder to further configure the vector index.</returns>
332+
[Experimental(EFDiagnostics.SqlServerVectorSearch)]
333+
public static VectorIndexBuilder<TEntity> HasVectorIndex<TEntity>(
334+
this EntityTypeBuilder<TEntity> entityTypeBuilder,
335+
Expression<Func<TEntity, object?>> indexExpression,
336+
string metric,
337+
string? name = null)
338+
where TEntity : class
339+
{
340+
Check.NotNull(indexExpression);
341+
Check.NotEmpty(metric);
342+
343+
var indexBuilder = name is null
344+
? entityTypeBuilder.HasIndex(indexExpression)
345+
: entityTypeBuilder.HasIndex(indexExpression, name);
346+
indexBuilder.Metadata.SetVectorMetric(metric);
347+
348+
return new VectorIndexBuilder<TEntity>(indexBuilder);
349+
}
350+
351+
/// <summary>
352+
/// Configures a vector index on the specified property for SQL Server.
353+
/// </summary>
354+
/// <remarks>
355+
/// See <see href="https://aka.ms/efcore-docs-modeling">Modeling entity types and relationships</see>, and
356+
/// <see href="https://aka.ms/efcore-docs-sqlserver">Accessing SQL Server and Azure SQL databases with EF Core</see>
357+
/// for more information and examples.
358+
/// </remarks>
359+
/// <param name="entityTypeBuilder">The builder for the entity type being configured.</param>
360+
/// <param name="propertyName">The name of the property to create the vector index on.</param>
361+
/// <param name="metric">
362+
/// <para>
363+
/// A string with the name of the distance metric to use to calculate the distance between the two given vectors.
364+
/// The following distance metrics are supported:
365+
/// </para>
366+
/// <list type="bullet">
367+
/// <item>
368+
/// <term>cosine</term>
369+
/// <description>Cosine distance</description>
370+
/// </item>
371+
/// <item>
372+
/// <term>euclidean</term>
373+
/// <description>Euclidean distance</description>
374+
/// </item>
375+
/// <item>
376+
/// <term>dot</term>
377+
/// <description>(Negative) Dot product</description>
378+
/// </item>
379+
/// </list>
380+
/// </param>
381+
/// <param name="name">The name to assign to the index.</param>
382+
/// <returns>A builder to further configure the vector index.</returns>
383+
[Experimental(EFDiagnostics.SqlServerVectorSearch)]
384+
public static VectorIndexBuilder HasVectorIndex(
385+
this EntityTypeBuilder entityTypeBuilder,
386+
string propertyName,
387+
string metric,
388+
string? name = null)
389+
{
390+
Check.NotEmpty(propertyName);
391+
Check.NotEmpty(metric);
392+
393+
var indexBuilder = name is null
394+
? entityTypeBuilder.HasIndex(propertyName)
395+
: entityTypeBuilder.HasIndex(propertyName, name);
396+
indexBuilder.Metadata.SetVectorMetric(metric);
397+
398+
return new VectorIndexBuilder(indexBuilder);
399+
}
400+
294401
/// <summary>
295402
/// Configures a history table name for the entity mapped to a temporal table.
296403
/// </summary>

src/EFCore.SqlServer/Extensions/SqlServerIndexExtensions.cs

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4+
using System.Diagnostics.CodeAnalysis;
45
using Microsoft.EntityFrameworkCore.SqlServer.Metadata.Internal;
56

67
// ReSharper disable once CheckNamespace
@@ -431,4 +432,155 @@ public static void SetDataCompression(this IMutableIndex index, DataCompressionT
431432
/// <returns>The <see cref="ConfigurationSource" /> for the data compression the index uses.</returns>
432433
public static ConfigurationSource? GetDataCompressionConfigurationSource(this IConventionIndex index)
433434
=> index.FindAnnotation(SqlServerAnnotationNames.DataCompression)?.GetConfigurationSource();
435+
436+
/// <summary>
437+
/// Returns whether the index is a vector index.
438+
/// </summary>
439+
/// <param name="index">The index.</param>
440+
/// <returns>Whether the index is a vector index.</returns>
441+
[Experimental(EFDiagnostics.SqlServerVectorSearch)]
442+
public static bool IsVectorIndex(this IReadOnlyIndex index)
443+
=> index is RuntimeIndex
444+
? throw new InvalidOperationException(CoreStrings.RuntimeModelMissingData)
445+
: index[SqlServerAnnotationNames.VectorIndexMetric] is not null;
446+
447+
/// <summary>
448+
/// Returns the similarity metric for the vector index.
449+
/// </summary>
450+
/// <param name="index">The index.</param>
451+
/// <returns>The similarity metric for the vector index.</returns>
452+
[Experimental(EFDiagnostics.SqlServerVectorSearch)]
453+
public static string? GetVectorMetric(this IReadOnlyIndex index)
454+
=> index is RuntimeIndex
455+
? throw new InvalidOperationException(CoreStrings.RuntimeModelMissingData)
456+
: (string?)index[SqlServerAnnotationNames.VectorIndexMetric];
457+
458+
/// <summary>
459+
/// Returns the similarity metric for the vector index.
460+
/// </summary>
461+
/// <param name="index">The index.</param>
462+
/// <param name="storeObject">The identifier of the store object.</param>
463+
/// <returns>The similarity metric for the vector index.</returns>
464+
[Experimental(EFDiagnostics.SqlServerVectorSearch)]
465+
public static string? GetVectorMetric(this IReadOnlyIndex index, in StoreObjectIdentifier storeObject)
466+
{
467+
if (index is RuntimeIndex)
468+
{
469+
throw new InvalidOperationException(CoreStrings.RuntimeModelMissingData);
470+
}
471+
472+
var annotation = index.FindAnnotation(SqlServerAnnotationNames.VectorIndexMetric);
473+
if (annotation != null)
474+
{
475+
return (string?)annotation.Value;
476+
}
477+
478+
var sharedTableRootIndex = index.FindSharedObjectRootIndex(storeObject);
479+
return sharedTableRootIndex?.GetVectorMetric(storeObject);
480+
}
481+
482+
/// <summary>
483+
/// Sets the similarity metric for the vector index.
484+
/// </summary>
485+
/// <param name="index">The index.</param>
486+
/// <param name="metric">The value to set.</param>
487+
[Experimental(EFDiagnostics.SqlServerVectorSearch)]
488+
public static void SetVectorMetric(this IMutableIndex index, string? metric)
489+
=> index.SetAnnotation(SqlServerAnnotationNames.VectorIndexMetric, metric);
490+
491+
/// <summary>
492+
/// Sets the similarity metric for the vector index.
493+
/// </summary>
494+
/// <param name="index">The index.</param>
495+
/// <param name="metric">The value to set.</param>
496+
/// <param name="fromDataAnnotation">Indicates whether the configuration was specified using a data annotation.</param>
497+
/// <returns>The configured value.</returns>
498+
[Experimental(EFDiagnostics.SqlServerVectorSearch)]
499+
public static string? SetVectorMetric(
500+
this IConventionIndex index,
501+
string? metric,
502+
bool fromDataAnnotation = false)
503+
=> (string?)index.SetAnnotation(
504+
SqlServerAnnotationNames.VectorIndexMetric,
505+
metric,
506+
fromDataAnnotation)?.Value;
507+
508+
/// <summary>
509+
/// Returns the <see cref="ConfigurationSource" /> for the similarity metric of the vector index.
510+
/// </summary>
511+
/// <param name="index">The index.</param>
512+
/// <returns>The <see cref="ConfigurationSource" /> for the similarity metric of the vector index.</returns>
513+
[Experimental(EFDiagnostics.SqlServerVectorSearch)]
514+
public static ConfigurationSource? GetVectorMetricConfigurationSource(this IConventionIndex index)
515+
=> index.FindAnnotation(SqlServerAnnotationNames.VectorIndexMetric)?.GetConfigurationSource();
516+
517+
/// <summary>
518+
/// Returns the type of the vector index.
519+
/// </summary>
520+
/// <param name="index">The index.</param>
521+
/// <returns>The type of the vector index.</returns>
522+
[Experimental(EFDiagnostics.SqlServerVectorSearch)]
523+
public static string? GetVectorIndexType(this IReadOnlyIndex index)
524+
=> (index is RuntimeIndex)
525+
? throw new InvalidOperationException(CoreStrings.RuntimeModelMissingData)
526+
: (string?)index[SqlServerAnnotationNames.VectorIndexType];
527+
528+
/// <summary>
529+
/// Returns the type of the vector index.
530+
/// </summary>
531+
/// <param name="index">The index.</param>
532+
/// <param name="storeObject">The identifier of the store object.</param>
533+
/// <returns>The type of the vector index.</returns>
534+
[Experimental(EFDiagnostics.SqlServerVectorSearch)]
535+
public static string? GetVectorIndexType(this IReadOnlyIndex index, in StoreObjectIdentifier storeObject)
536+
{
537+
if (index is RuntimeIndex)
538+
{
539+
throw new InvalidOperationException(CoreStrings.RuntimeModelMissingData);
540+
}
541+
542+
var annotation = index.FindAnnotation(SqlServerAnnotationNames.VectorIndexType);
543+
if (annotation != null)
544+
{
545+
return (string?)annotation.Value;
546+
}
547+
548+
var sharedTableRootIndex = index.FindSharedObjectRootIndex(storeObject);
549+
return sharedTableRootIndex?.GetVectorIndexType(storeObject);
550+
}
551+
552+
/// <summary>
553+
/// Sets the type of the vector index.
554+
/// </summary>
555+
/// <param name="index">The index.</param>
556+
/// <param name="type">The value to set.</param>
557+
[Experimental(EFDiagnostics.SqlServerVectorSearch)]
558+
public static void SetVectorIndexType(this IMutableIndex index, string? type)
559+
=> index.SetAnnotation(SqlServerAnnotationNames.VectorIndexType, type);
560+
561+
/// <summary>
562+
/// Sets the type of the vector index.
563+
/// </summary>
564+
/// <param name="index">The index.</param>
565+
/// <param name="type">The value to set.</param>
566+
/// <param name="fromDataAnnotation">Indicates whether the configuration was specified using a data annotation.</param>
567+
/// <returns>The configured value.</returns>
568+
[Experimental(EFDiagnostics.SqlServerVectorSearch)]
569+
public static string? SetVectorIndexType(
570+
this IConventionIndex index,
571+
string? type,
572+
bool fromDataAnnotation = false)
573+
=> (string?)index.SetAnnotation(
574+
SqlServerAnnotationNames.VectorIndexType,
575+
type,
576+
fromDataAnnotation)?.Value;
577+
578+
/// <summary>
579+
/// Returns the <see cref="ConfigurationSource" /> for the type of the vector index.
580+
/// </summary>
581+
/// <param name="index">The index.</param>
582+
/// <returns>The <see cref="ConfigurationSource" /> for the type of the vector index.</returns>
583+
[Experimental(EFDiagnostics.SqlServerVectorSearch)]
584+
public static ConfigurationSource? GetVectorIndexTypeConfigurationSource(this IConventionIndex index)
585+
=> index.FindAnnotation(SqlServerAnnotationNames.VectorIndexType)?.GetConfigurationSource();
434586
}

src/EFCore.SqlServer/Infrastructure/Internal/SqlServerModelValidator.cs

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ public SqlServerModelValidator(
4141
public override void Validate(IModel model, IDiagnosticsLogger<DbLoggerCategory.Model.Validation> logger)
4242
{
4343
ValidateIndexIncludeProperties(model, logger);
44+
ValidateVectorIndexes(model, logger);
4445

4546
base.Validate(model, logger);
4647

@@ -267,6 +268,44 @@ protected virtual void ValidateIndexIncludeProperties(
267268
}
268269
}
269270

271+
/// <summary>
272+
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
273+
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
274+
/// any release. You should only use it directly in your code with extreme caution and knowing that
275+
/// doing so can result in application failures when updating to a new Entity Framework Core release.
276+
/// </summary>
277+
#pragma warning disable EF9105 // Vector indexes are experimental
278+
protected virtual void ValidateVectorIndexes(
279+
IModel model,
280+
IDiagnosticsLogger<DbLoggerCategory.Model.Validation> logger)
281+
{
282+
foreach (var index in model.GetEntityTypes().SelectMany(t => t.GetDeclaredIndexes()))
283+
{
284+
if (index.IsVectorIndex())
285+
{
286+
if (index.Properties is not [var property])
287+
{
288+
throw new InvalidOperationException(
289+
SqlServerStrings.VectorIndexRequiresSingleProperty(
290+
index.DisplayName(),
291+
index.DeclaringEntityType.DisplayName()));
292+
}
293+
294+
var typeMapping = property.FindTypeMapping();
295+
296+
if (typeMapping is not SqlServerVectorTypeMapping)
297+
{
298+
throw new InvalidOperationException(
299+
SqlServerStrings.VectorIndexOnNonVectorProperty(
300+
index.DisplayName(),
301+
index.DeclaringEntityType.DisplayName(),
302+
property.Name));
303+
}
304+
}
305+
}
306+
}
307+
#pragma warning restore EF9105
308+
270309
/// <summary>
271310
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
272311
/// the same compatibility standards as public APIs. It may be changed or removed without notice in

0 commit comments

Comments
 (0)