Recherche vectorielle dans le fournisseur EF Core SQL Server

Note

La prise en charge des vecteurs a été introduite dans EF Core 10.0 et n’est prise en charge que par SQL Server 2025 et versions ultérieures.

Le type de données vectorielles SQL Server permet de stocker des embeddings, qui sont des représentations de signification qui peuvent être recherchées efficacement sur la similarité, alimentant les charges de travail IA telles que la recherche sémantique et la génération augmentée par récupération (RAG).

Configuration des propriétés de vecteur

Pour utiliser le type de données vector, ajoutez simplement une propriété .NET de type SqlVector<float> à votre type d’entité, en spécifiant les dimensions comme suit :

public class Blog
{
    // ...

    [Column(TypeName = "vector(1536)")]
    public SqlVector<float> Embedding { get; set; }
}

Une fois votre propriété ajoutée et la colonne correspondante créée dans la base de données, vous pouvez commencer à insérer des incorporations. La génération d’incorporation est effectuée en dehors de la base de données, généralement par le biais d’un service, et les détails pour ce faire sont hors de portée pour cette documentation. Toutefois, la bibliothèque .NET Microsoft.Extensions.AI contient IEmbeddingGenerator, qui est une abstraction sur les générateurs d’incorporation qui ont des implémentations pour les principaux fournisseurs.

Une fois que vous avez choisi votre générateur d’incorporation et configuré, utilisez-le pour générer des incorporations et les insérer comme suit :

IEmbeddingGenerator<string, Embedding<float>> embeddingGenerator = /* Set up your preferred embedding generator */;

var embedding = await embeddingGenerator.GenerateVectorAsync("Some text to be vectorized");
context.Blogs.Add(new Blog
{
    Name = "Some blog",
    Embedding = new SqlVector<float>(embedding)
});
await context.SaveChangesAsync();

Une fois que vous avez enregistré des incorporations dans votre base de données, vous êtes prêt à effectuer une recherche de similarité vectorielle sur celles-ci.

Note

À compter d’EF Core 11, les propriétés vectorielles ne sont pas chargées par défaut lors de l’interrogation d’entités, car les vecteurs sont généralement volumineux et sont rarement nécessaires pour être lus. Avant EF Core 11, les propriétés vectorielles ont toujours été chargées comme n’importe quelle autre propriété.

Recherche exacte avec VECTOR_DISTANCE()

La EF.Functions.VectorDistance() fonction calcule la distance exacte entre deux vecteurs. Utilisez-la pour effectuer une recherche de similarité pour une requête utilisateur donnée :

var sqlVector = new SqlVector<float>(await embeddingGenerator.GenerateVectorAsync("Some user query to be vectorized"));
var topSimilarBlogs = await context.Blogs
    .OrderBy(b => EF.Functions.VectorDistance("cosine", b.Embedding, sqlVector))
    .Take(3)
    .ToListAsync();

Cette fonction calcule la distance entre le vecteur de requête et chaque ligne de la table, puis retourne les correspondances les plus proches. Bien que cela fournit des résultats parfaitement précis, il peut être lent pour les jeux de données volumineux, car SQL Server doit analyser toutes les lignes et les distances de calcul pour chacune d’elles.

Note

La prise en charge intégrée d’EF 10 remplace l’extension EFCore.SqlServer.VectorSearch précédente, qui a permis d’effectuer une recherche vectorielle avant l’introduction du vector type de données. Dans le cadre de la mise à niveau vers EF 10, supprimez l’extension de vos projets.

Avertissement

VECTOR_SEARCH() et les index vectoriels sont actuellement des fonctionnalités expérimentales dans SQL Server et sont susceptibles de changer. Les API dans EF Core pour ces fonctionnalités sont également susceptibles de changer.

La fonction table VECTOR_SEARCH() de SQL Server récupère des lignes en fonction de la similarité vectorielle. Contrairement VECTOR_DISTANCE() à — qui calcule la distance entre deux vecteurs spécifiques — VECTOR_SEARCH() recherche une table entière pour les vecteurs les plus similaires à un vecteur de requête donné.

Utilisez la méthode d’extension VectorSearch() sur votre DbSet, puis enchaînez OrderBy(), Take() et WithApproximate() pour effectuer une recherche approximative des plus proches voisins (ANN) qui utilise un index de vecteurs :

var results = await context.Blogs
    .VectorSearch(b => b.Embedding, embedding, "cosine")
    .OrderBy(r => r.Distance)
    .Take(5)
    .WithApproximate()
    .ToListAsync();

foreach (var result in results)
{
    Console.WriteLine($"Blog {result.Value.Id} with distance {result.Distance}");
}

Cela se traduit par le code SQL suivant :

SELECT TOP(@__p_1) WITH APPROXIMATE [b].[Id], [b].[Name], [v].[Distance]
FROM VECTOR_SEARCH(
    TABLE = [Blogs] AS [b],
    COLUMN = [Embedding],
    SIMILAR_TO = @__embedding_0,
    METRIC = 'cosine'
) AS [v]
ORDER BY [v].[Distance]

VectorSearch() retourne VectorSearchResult<TEntity>, qui vous permet d’accéder à la fois à l’entité et à la distance calculée :

var searchResults = await context.Blogs
    .VectorSearch(b => b.Embedding, embedding, "cosine")
    .Where(r => r.Distance < 0.05)
    .OrderBy(r => r.Distance)
    .Select(r => new { Blog = r.Value, Distance = r.Distance })
    .Take(3)
    .WithApproximate()
    .ToListAsync();

Cela vous permet de filtrer le score de similarité, de le présenter aux utilisateurs, etc.

WithApproximate()

WithApproximate() indique SQL Server d’utiliser l’index vectoriel pour une recherche approximative du voisin le plus proche (ANN), ce qui offre de meilleures performances pour les jeux de données volumineux. Cela entraîne l’ajout de WITH APPROXIMATE à la clause SQL TOP. WithApproximate() doit être appelé après Take(), qui spécifie le nombre de résultats à retourner.

Sans WithApproximate(), la requête effectue une recherche exacte des k plus proches voisins (kNN) qui parcourt toutes les lignes, sans utiliser l’index vectoriel :

// Exact kNN search (no vector index used)
var blogs = await context.Blogs
    .VectorSearch(b => b.Embedding, embedding, "cosine")
    .OrderBy(r => r.Distance)
    .Take(5)
    .ToListAsync();

Index vectoriels

Pour utiliser la recherche approximative avec WithApproximate(), vous devez créer un index vectoriel sur votre colonne vectorielle. Utilisez la méthode HasVectorIndex() dans la configuration de votre modèle :

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Blog>()
        .HasVectorIndex(b => b.Embedding, "cosine");
}

Cela génère la migration SQL suivante :

CREATE VECTOR INDEX [IX_Blogs_Embedding]
    ON [Blogs] ([Embedding])
    WITH (METRIC = COSINE)

Les métriques de distance suivantes sont prises en charge pour les index vectoriels :

Unité de mesure Description
cosine Similarité cosinus (distance angulaire)
euclidean Distance euclide (norme L2)
dot Produit dot (produit interne négatif)

Choisissez la métrique qui correspond le mieux à votre modèle d’incorporation et à votre cas d’usage. La similarité cosinus est couramment utilisée pour les incorporations de texte, tandis que la distance euclide est souvent utilisée pour les incorporations d’images.

La recherche hybride combine la recherche de vecteurs de similarité avec la recherche en texte intégral classique pour fournir des résultats plus pertinents. La recherche vectorielle excelle dans la recherche de contenu sémantiquement similaire, tandis que la recherche en texte intégral est préférable à la correspondance exacte des mots clés. En combinant les deux approches et l’utilisation de la fusion de classement réciproque (RRF) pour fusionner les résultats, vous pouvez créer des expériences de recherche plus intelligentes.

L’exemple suivant montre comment implémenter la recherche hybride à l’aide d’EF Core, en combinant FreeTextTable() et VectorSearch() en une seule requête :

var k = 20;
string textualQuery = ...;
SqlVector<float> queryEmbedding = ...;

var results = await context.Articles
    // Perform full-text search
    .FreeTextTable<Article, int>(textualQuery, topN: k)
    // Perform vector (semantic) search, joining the results of both searches together
    .LeftJoin(
        context.Articles.VectorSearch(b => b.Embedding, queryEmbedding, "cosine")
            .OrderBy(r => r.Distance)
            .Take(k)
            .WithApproximate(),
        fts => fts.Key,
        vs => vs.Value.Id,
        (fts, vs) => new
        {
            Article = vs.Value,
            FullTextRank = fts.Rank,
            VectorDistance = (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)
    })
    .OrderByDescending(x => x.RrfScore)
    .Take(10)
    .Select(x => x.Article)
    .ToListAsync();

Cette requête :

  1. Effectue une recherche en texte intégral sur Article
  2. Effectue une recherche vectorielle sur Article et combine les résultats aux résultats de recherche en texte intégral via une jointure LEFT JOIN
  3. Calcule le score RRF en combinant le texte intégral et le classement sémantique
  4. Trie par score RRF, prend le nombre souhaité de résultats et projette les entités d’origine Article.

Note

Au lieu d’utiliser une JOINTURE LEFT, une JOINTURE EXTERNE COMPLÈTE serait plus adaptée à ce scénario ; cela permettrait d’inclure les résultats hautement classés de l’un ou l’autre côté de la recherche dans le résultat final, même si ce résultat n’apparaît pas du tout de l’autre côté. Avec l’approche LEFT JOIN ci-dessus, si un résultat a un score de similarité vectorielle très élevé, il n’est jamais inclus dans le résultat final si ce résultat n’a pas également un score de texte intégral élevé. Toutefois, EF ne prend actuellement pas en charge FULL OUTER JOIN ; upvote #37633 si c’est quelque chose que vous souhaitez voir pris en charge.

La requête produit le code SQL suivant :

SELECT TOP(@__p_4) [a0].[Id], [a0].[Content], [a0].[Title]
FROM FREETEXTTABLE([Articles], *, @__textualQuery_0, @__k_1) AS [f]
LEFT JOIN (
    SELECT TOP(@__k_1) WITH APPROXIMATE [a].[Id], [a].[Content], [a].[Title], [v].[Distance]
    FROM VECTOR_SEARCH(
        TABLE = [Articles] AS [a],
        COLUMN = [Embedding],
        SIMILAR_TO = @__queryEmbedding_2,
        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