Pesquisa de vetor no provedor SQL Server EF Core

Observação

O suporte ao vetor foi introduzido no EF Core 10.0 e só tem suporte com SQL Server 2025 e superiores.

O tipo de dados de vetor SQL Server permite armazenar embeddings, que são representações de significado que podem ser pesquisadas com eficiência quanto à similaridade, alimentando cargas de trabalho de IA, como pesquisa semântica e RAG (Geração Aumentada por Recuperação).

Configurando propriedades de vetor

Para usar o tipo de dados vector, basta adicionar uma propriedade .NET do tipo SqlVector<float> ao tipo de entidade, especificando as dimensões da seguinte maneira:

public class Blog
{
    // ...

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

Depois que sua propriedade for adicionada e a coluna correspondente criada no banco de dados, você poderá começar a inserir inserções. A geração de inserção é feita fora do banco de dados, geralmente por meio de um serviço, e os detalhes para fazer isso estão fora do escopo desta documentação. No entanto, a biblioteca .NET Microsoft.Extensions.AI contém IEmbeddingGenerator, que é uma abstração sobre geradores de inserção que tem implementações para os principais provedores.

Depois de escolher o gerador de inserção e configurá-lo, use-o para gerar inserções e inseri-las da seguinte maneira:

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();

Depois de salvar as inserções no banco de dados, você estará pronto para executar a pesquisa de similaridade de vetor sobre eles.

Observação

A partir do EF Core 11, as propriedades de vetor não são carregadas por padrão ao consultar entidades, pois os vetores normalmente são grandes e raramente são necessários para serem lidos novamente. Antes do EF Core 11, as propriedades de vetor sempre eram carregadas como qualquer outra propriedade.

Pesquisa exata com VECTOR_DISTANCE()

A EF.Functions.VectorDistance() função calcula a distância exata entre dois vetores. Use-o para executar a pesquisa de similaridade para uma determinada consulta de usuário:

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();

Essa função calcula a distância entre o vetor de consulta e todas as linhas da tabela e retorna as correspondências mais próximas. Embora isso forneça resultados perfeitamente precisos, pode ser lento para grandes conjuntos de dados porque SQL Server deve verificar todas as linhas e distâncias de computação para cada um deles.

Observação

O suporte interno no EF 10 substitui a extensão EFCore.SqlServer.VectorSearch anterior, que permitia a execução da pesquisa de vetor antes da introdução do vector tipo de dados. Como parte da atualização para o EF 10, remova a extensão de seus projetos.

Aviso

VECTOR_SEARCH() e índices de vetor são recursos experimentais no momento em SQL Server e estão sujeitos a alterações. As APIs no EF Core para esses recursos também estão sujeitas a alterações.

A função VECTOR_SEARCH() com valor de tabela do SQL Server recupera linhas com base na similaridade do vetor. Ao contrário VECTOR_DISTANCE() — que calcula a distância entre dois vetores específicos — VECTOR_SEARCH() pesquisa uma tabela inteira para obter os vetores mais semelhantes a um determinado vetor de consulta.

Use o método de extensão VectorSearch() no seu DbSet e encadeie OrderBy(), Take() e WithApproximate() para executar uma pesquisa de vizinho mais próximo aproximado (ANN) que usa um índice vetorial:

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}");
}

Isso se traduz no seguinte SQL:

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() retorna VectorSearchResult<TEntity>, o que permite acessar a entidade e a distância computada:

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();

Isso permite filtrar a pontuação de similaridade, apresentá-la aos usuários etc.

WithApproximate()

WithApproximate() instrui o SQL Server a usar o índice vetorial para busca aproximada de vizinhos mais próximos (ANN), o que oferece desempenho significativamente melhor em grandes conjuntos de dados. Isso faz com que WITH APPROXIMATE seja adicionado à cláusula SQL TOP. WithApproximate() deve ser chamado depois Take(), o que especifica o número de resultados a serem retornados.

Sem WithApproximate(), a consulta executa uma pesquisa exata k-nearest neighbor (kNN) que verifica todas as linhas, sem usar o índice de vetor:

// 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();

Índices de vetor

Para usar a busca aproximada com WithApproximate(), você deve criar um índice vetorial na sua coluna vetorial. Use o HasVectorIndex() método na configuração do modelo:

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

Isso gerará a seguinte migração de SQL:

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

As seguintes métricas de distância têm suporte para índices de vetor:

Métrica Descrição
cosine Similaridade cosseno (distância angular)
euclidean Distância euclidiana (norma L2)
dot Produto Dot (produto interno negativo)

Escolha a métrica que melhor corresponde ao modelo de inserção e ao caso de uso. A similaridade cossina é comumente usada para inserções de texto, enquanto a distância euclidiana é frequentemente usada para inserções de imagem.

A pesquisa híbrida combina a pesquisa de similaridade de vetor com a pesquisa de texto completo tradicional para fornecer resultados mais relevantes. A pesquisa de vetor se destaca em encontrar conteúdo semanticamente semelhante, enquanto a pesquisa de texto completo é melhor na correspondência exata da palavra-chave. Combinando abordagens e usando o RRF (Reciprocal Rank Fusion) para mesclar os resultados, você pode criar experiências de pesquisa mais inteligentes.

O exemplo a seguir mostra como implementar a pesquisa híbrida usando o EF Core, combinando FreeTextTable() e VectorSearch() em uma única consulta:

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

var results = await context.Articles
    // Perform full-text search
    .FreeTextTable<Article, int>(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
    .FullJoin(
        context.Articles.VectorSearch(b => b.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
        })
    // Apply Reciprocal Rank Fusion (RRF) to combine the results
    .Select(x => new
    {
        x.Article,
        RrfScore = (x.FullTextRank == null ? 0.0 : 1.0 / (k + x.FullTextRank.Value))
            + (x.VectorDistance == null ? 0.0 : 1.0 / (k + x.VectorDistance.Value))
    })
    .OrderByDescending(x => x.RrfScore)
    .Take(10)
    .Select(x => x.Article)
    .ToListAsync();

Esta consulta:

  1. Executa uma pesquisa de texto completo em Article
  2. Executa uma busca vetorial em Article e combina os resultados da busca vetorial com os da busca de texto completo via FULL JOIN
  3. Calcula a pontuação RRF combinando o texto completo e a classificação semântica
  4. Ordena por pontuação RRF, obtém o número desejado de resultados e extrai as entidades originais Article.

A consulta produz o seguinte SQL:

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]
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],
        COLUMN = [Embedding],
        SIMILAR_TO = @__queryEmbedding_2,
        METRIC = 'cosine'
    ) AS [v]
    ORDER BY [v].[Distance]
) 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