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

Esta consulta:

  1. Executa uma pesquisa de texto completo em Article
  2. Executa uma busca em vetores Article e combina os resultados com os da pesquisa de texto completo por meio de um LEFT 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.

Observação

Em vez de usar um LEFT JOIN, um FULL OUTER JOIN seria mais adequado para esse cenário; isso permitiria que os resultados de alta relevância de ambos os lados da pesquisa fossem incluídos no resultado final, mesmo que esse resultado não apareça do outro lado. Com a abordagem LEFT JOIN acima, se um resultado tiver uma pontuação de similaridade de vetor muito alta, ele nunca será incluído no resultado final se esse resultado também não tiver uma pontuação de texto completo alta. No entanto, o EF atualmente não dá suporte a FULL OUTER JOIN; vote a favor #37633 se isso for algo que você gostaria de ver suportado.

A consulta produz o seguinte SQL:

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