Pesquisa vetorial no SQL Server EF Core Provider

Observação

O suporte a vector foi introduzido no EF Core 10.0 e é suportado apenas com o SQL Server 2025 e versões superiores.

O tipo de dado vetorial SQL Server permite armazenar embeddings, que são representações de significado que podem ser pesquisadas de forma eficiente quanto à semelhança, alimentando cargas de trabalho de IA como a geração aumentada de pesquisa e recuperação semântica (RAG).

Configuração das propriedades vetoriais

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

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 incorporações. A geração de incorporaçã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 embedding que tem implementações para os principais fornecedores.

Depois de escolheres o teu gerador de embedding e o configurares, usa-o para gerar embeddings e insere-os da seguinte forma:

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 ter os embeddings guardados na sua base de dados, está pronto para realizar uma pesquisa vetorial por similaridade sobre eles.

Observação

A partir do EF Core 11, as propriedades vetoriais não são carregadas por padrão ao consultar entidades, uma vez que os vetores são tipicamente grandes e raramente necessitam de ser lidos. Antes do EF Core 11, as propriedades vetoriais eram sempre 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 realizar uma pesquisa de similaridade para uma determinada consulta de utilizador:

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

Esta função calcula a distância entre o vetor de consulta e cada linha da tabela, depois devolve as correspondências mais próximas. Embora isto forneça resultados perfeitamente precisos, pode ser lento para conjuntos de dados grandes porque o SQL Server tem de analisar todas as linhas e calcular distâncias para cada uma.

Observação

O suporte interno no EF 10 substitui a extensão EFCore.SqlServer.VectorSearch anterior, que permitia executar a pesquisa vetorial antes que o vector tipo de dados fosse introduzido. Como parte da atualização para o EF 10, remova a extensão dos seus projetos.

Advertência

Os índices VECTOR_SEARCH() e vetoriais são atualmente características experimentais em SQL Server e estão sujeitos a alterações. As APIs do EF Core para estas funcionalidades também estão sujeitas a alterações.

A função com valor de tabela VECTOR_SEARCH() do SQL Server obtém linhas com base na similaridade vetorial. Diferente VECTOR_DISTANCE() — que calcula a distância entre dois vetores específicos — VECTOR_SEARCH() procura numa tabela inteira os vetores mais semelhantes a um dado vetor de consulta.

Utilize o método de extensão VectorSearch() no seu DbSet e encadeie OrderBy(), Take() e WithApproximate() para realizar uma pesquisa aproximada do vizinho mais próximo (ANN) que utiliza 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() devolve VectorSearchResult<TEntity>, que permite aceder tanto à entidade como à distância calculada:

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

Isto permite-te filtrar pela pontuação de similaridade, apresentá-la aos utilizadores, etc.

WithApproximate()

WithApproximate() instrui SQL Server a usar o índice vetorial para pesquisa aproximada de vizinho mais próximo (ANN), o que proporciona um desempenho significativamente melhor para conjuntos de dados grandes. Faz com que WITH APPROXIMATE seja adicionada à cláusula SQL TOP. WithApproximate() deve ser chamado após Take(), que especifica o número de resultados a devolver.

Sem WithApproximate(), a consulta realiza uma pesquisa exata dos k vizinhos mais próximos (kNN) que varre todas as linhas, sem usar o índice vetorial:

// 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 vetoriais

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

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

Isto irá gerar a seguinte migração SQL:

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

As seguintes métricas de distância são suportadas para índices vetoriais:

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

Escolha a métrica que melhor se adequa ao seu modelo de embedding e ao seu caso de uso. A similaridade do cosseno é frequentemente utilizada para embeddings de texto, enquanto a distância euclidiana é geralmente usada para embeddings de imagem.

A pesquisa híbrida combina a pesquisa por similaridade vetorial com a pesquisa tradicional em texto completo para fornecer resultados mais relevantes. A pesquisa vetorial destaca-se por encontrar conteúdos semanticamente semelhantes, enquanto a pesquisa em texto completo é melhor na correspondência exata de palavras-chave. Ao combinar ambas as abordagens e usar a Fusão de Classificação Recíproca (RRF) para fundir os resultados, pode construir experiências de pesquisa mais inteligentes.

O exemplo seguinte mostra como implementar pesquisa híbrida usando EF Core, combinando FreeTextTable() e VectorSearch() numa ú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. Realiza uma pesquisa em texto completo em Article
  2. Realiza uma pesquisa vetorial em Article e combina os resultados com os resultados da pesquisa em texto completo através de um FULL JOIN
  3. Calcula a pontuação RRF combinando tanto o texto completo como a classificação semântica
  4. Ordena por pontuação RRF, seleciona 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