Dados vetoriais no .NET com Microsoft.Extensions.VectorData
O Microsoft.Extensions.VectorData é a biblioteca do .NET que oferece uma interface única para trabalhar com bancos de dados vetoriais, independentemente do provedor. Ela surgiu porque a inteligência artificial generativa exige que as aplicações forneçam contexto relevante aos modelos de linguagem (LLMs).
Em cenários além de simples perguntas e respostas, como chatbots baseados em documentação ou assistentes de RH, a aplicação precisa armazenar e recuperar grandes volumes de informação de forma semântica. Neste artigo, você vai entender os conceitos por trás disso e ver, com código, como a biblioteca resolve cada etapa.
O que são embeddings e por que eles importam
Embeddings são representações numéricas do significado semântico de um texto. Em outras palavras, o modelo transforma uma frase em um vetor de números, e textos com sentido parecido geram vetores próximos.
Ao converter documentos em embeddings e armazená-los em um banco de dados vetorial, a aplicação encontra conteúdo relevante pelo significado da consulta. Ou seja, ela não depende apenas das palavras exatas que o usuário digitou.
Segundo a documentação oficial sobre bancos de dados vetoriais no .NET, palavras, documentos, imagens e áudio podem ser vetorizados. Por isso, a mesma técnica serve para recomendação de produtos, detecção de anomalias e busca de documentos.
Dimensões e funções de distância
Cada modelo de embedding gera vetores com um número fixo de dimensões. O modelo text-embedding-3-small da OpenAI, usado mais adiante, produz vetores de 1536 posições.
Para comparar dois vetores, o banco aplica uma função de distância. A similaridade de cosseno, por exemplo, mede o ângulo entre os vetores e ignora o tamanho deles, por isso é muito usada com texto.
Essas duas informações aparecem novamente no modelo de dados do Microsoft.Extensions.VectorData. Então, vale guardar que o número de dimensões sempre precisa bater com o modelo escolhido.
Busca semântica versus busca textual tradicional
Em uma busca tradicional, o banco decompõe a consulta em termos e compara cada um deles literalmente com os registros. Esse modelo funciona bem para códigos, nomes e filtros exatos.
Considere três entradas: "passe de corredor", "passagem pela montanha" e "passar (verbo)". Uma busca pelo termo "passe" retornaria as três, independentemente do contexto da pergunta.
Com embeddings, a consulta "Como eu cruzo a passagem?" vira um vetor semântico que aponta para "passagem pela montanha". Enquanto isso, "Onde retiro meu passe?" aponta para "passe de corredor".
Esse comportamento é possível porque os modelos de embedding aprendem o contexto semântico das palavras a partir dos termos que as cercam. Assim, a busca mede a distância entre o vetor da pergunta e os vetores armazenados, e os mais próximos são os mais parecidos em significado.
O padrão RAG (Retrieval-Augmented Generation)
O padrão RAG é a técnica mais usada para fornecer contexto específico a um LLM. Se quiser se aprofundar no conceito, o artigo o que é RAG explica o padrão em detalhes.
A documentação da Microsoft descreve o fluxo em cinco etapas:
- Gerar embeddings dos seus dados com um modelo de embedding.
- Armazenar e indexar esses embeddings em um banco vetorial, junto ao conteúdo original.
- Converter a pergunta do usuário em um embedding, usando o mesmo modelo.
- Executar uma busca por similaridade para encontrar os documentos mais relevantes.
- Enviar esses documentos como contexto ao LLM, junto com a pergunta.
Bancos como Qdrant, Redis, PostgreSQL, SQL Server e Azure Cosmos DB armazenam e consultam vetores de forma eficiente. Aliás, o artigo sobre SQL Server 2025 com vetores e inteligência artificial mostra esse suporte no banco da Microsoft.
Microsoft.Extensions.VectorData como interface unificada
Assim como o Microsoft.Extensions.AI unifica o acesso a LLMs, o Microsoft.Extensions.VectorData fornece uma abstração comum para diferentes provedores de bancos vetoriais. O pacote Microsoft.Extensions.VectorData.Abstractions define as abstrações, enquanto pacotes separados implementam cada banco.
Sem essa camada, o código fica acoplado ao SDK específico de cada provedor. Consequentemente, trocar de banco vira uma tarefa invasiva, que espalha alterações por toda a aplicação.
Com a abstração, basta substituir a implementação do VectorStore para migrar entre provedores. Afinal, todos implementam as mesmas classes abstratas VectorStore e VectorStoreCollection<TKey, TRecord>, então a lógica de negócio permanece inalterada.
Quando não usar a abstração
A documentação lembra que é possível fazer busca vetorial com o driver comum do banco. No SQL Server, por exemplo, a busca pode ser feita em T-SQL usando o SqlClient padrão.
Contudo, esse caminho costuma ser de baixo nível, exige bastante código de serialização e não é portável entre bancos. Por isso, a biblioteca é a escolha natural quando você quer produtividade e liberdade para trocar de provedor.
Definindo o modelo de dados no Microsoft.Extensions.VectorData
O Microsoft.Extensions.VectorData usa uma abordagem orientada a modelo. Na prática, você mapeia classes C# para o esquema do banco vetorial por meio de atributos.
public class DocumentRecord
{
[VectorStoreKey]
public string Key { get; set; }
[VectorStoreData]
public string Text { get; set; }
[VectorStoreData(IsIndexed = true)]
public string Category { get; set; }
[VectorStoreData(IsIndexed = true)]
public DateTimeOffset Timestamp { get; set; }
// O texto é convertido em embedding automaticamente na gravação
[VectorStoreVector(1536, DistanceFunction = DistanceFunction.CosineSimilarity)]
public string Embedding => this.Text;
}
O atributo [VectorStoreKey] identifica o campo chave do registro. Já o [VectorStoreData] marca campos de dados que podem ser filtrados e recuperados.
Por fim, o [VectorStoreVector] define o campo de embedding, com as dimensões do vetor e a função de distância usada na busca por similaridade. O valor 1536 precisa bater com o número de dimensões do modelo de embedding escolhido.
Indexação e propriedades vetoriais
A opção IsIndexed = true é importante porque muitos bancos exigem índice nos campos usados em filtros. Também existe a opção IsFullTextIndexed, que habilita a busca textual nos bancos que oferecem esse recurso.
Repare que a propriedade Embedding é do tipo string, e não ReadOnlyMemory<float>. Segundo a documentação, isso indica que a biblioteca deve gerar o embedding automaticamente a partir do texto, desde que exista um gerador configurado.
Nesse modo, o vetor gerado não volta nas leituras. Por isso, o modelo mantém o conteúdo original em uma propriedade de dados separada, a Text.
Se preferir controlar a geração manualmente, declare a propriedade como ReadOnlyMemory<float> e calcule o embedding antes de cada operação. Contudo, a documentação do Microsoft.Extensions.VectorData recomenda o modo automático, porque ele evita repetir essa lógica em todo ponto de chamada.
Configurando embeddings no Microsoft.Extensions.VectorData
A abordagem recomendada é configurar um IEmbeddingGenerator diretamente no VectorStore ou na coleção. O exemplo abaixo usa o modelo text-embedding-3-small da OpenAI com o banco Qdrant.
var embeddingGenerator = new OpenAIClient(apiKey)
.GetEmbeddingClient("text-embedding-3-small")
.AsIEmbeddingGenerator();
var vectorStore = new QdrantVectorStore(
new QdrantClient("localhost"),
ownsClient: true,
new QdrantVectorStoreOptions { EmbeddingGenerator = embeddingGenerator });
Com essa configuração, a biblioteca gera os embeddings automaticamente tanto na inserção de registros quanto nas buscas. Dessa forma, você não precisa de nenhum pré-processamento manual.
O gerador também pode ser configurado na coleção, na definição do registro ou em uma propriedade vetorial específica. Isso é útil quando partes diferentes do modelo usam modelos de embedding distintos.
Operações com coleções no Microsoft.Extensions.VectorData
A interface de coleção do Microsoft.Extensions.VectorData é a mesma em qualquer provedor. O nome da coleção corresponde ao conceito de armazenamento de cada banco, como uma tabela no SQL Server ou um índice no Azure AI Search.
var collection = vectorStore.GetCollection<string, DocumentRecord>("documents");
// Cria a coleção caso ela ainda não exista
await collection.EnsureCollectionExistsAsync();
// Insere ou atualiza um registro
await collection.UpsertAsync(documentRecord);
// Insere ou atualiza vários registros de uma vez
await collection.UpsertAsync(documentRecords);
var record = await collection.GetAsync("some-key");
await collection.DeleteAsync("some-key");
await collection.DeleteAsync(["key1", "key2", "key3"]);
O artigo original usava os métodos UpsertBatchAsync e DeleteBatchAsync. Na API atual documentada pela Microsoft, as operações em lote são sobrecargas de UpsertAsync e DeleteAsync que recebem uma coleção.
Operações em lote
As operações em lote são importantes para pipelines de ingestão de documentos em escala. Segundo a documentação, elas reduzem as idas ao banco e, com geração automática, costumam gerar os embeddings de todos os registros em uma única chamada ao modelo.
Além disso, a exclusão ignora em silêncio as chaves que não existem. Se outro erro acontecer no meio do lote, alguns registros podem já ter sido removidos, então a recomendação é repetir a operação inteira.
Para montar esse pipeline do zero, o artigo sobre ingestão de dados para aplicações .NET com RAG complementa bem esta etapa.
Busca semântica com geração automática de embedding
Quando existe um IEmbeddingGenerator configurado, a busca aceita texto diretamente como argumento. O parâmetro top é obrigatório e define o número máximo de registros retornados.
await foreach (var result in collection.SearchAsync("What is semantic search?", top: 5))
{
Console.WriteLine($"Score: {result.Score}, Text: {result.Record.Text}");
}
Também é possível passar um embedding pré-calculado do tipo ReadOnlyMemory<float>. Essa opção é útil quando os embeddings já foram gerados em lote anteriormente.
ReadOnlyMemory<float> precomputedEmbedding = /* embedding pré-calculado */;
await foreach (var result in collection.SearchAsync(precomputedEmbedding, top: 5))
{
Console.WriteLine($"Score: {result.Score}, Text: {result.Record.Text}");
}
A documentação recomenda pensar bem no valor de top, porque buscar registros demais reduz a performance. Para paginação, existe a opção Skip, mas ela pode ter custo alto em alguns bancos.
Filtragem de resultados no Microsoft.Extensions.VectorData
É possível combinar a busca por similaridade com filtros sobre os dados. O filtro é aplicado antes da comparação vetorial, o que reduz a latência e o custo de processamento.
var searchOptions = new VectorSearchOptions<DocumentRecord>
{
Filter = r => r.Category == "documentation" &&
r.Timestamp > DateTimeOffset.UtcNow.AddDays(-30)
};
var results = collection.SearchAsync("relevant documentation", top: 10, searchOptions);
Os filtros usam expressões LINQ padrão e suportam igualdade, operadores relacionais, operadores lógicos e verificação de pertencimento com .Contains(). Contudo, o conjunto exato de expressões varia conforme os recursos de cada banco.
Os filtros também servem para controle de acesso. Por exemplo, você pode excluir documentos que o usuário atual não tem permissão para ver, antes mesmo de enviar qualquer conteúdo ao modelo.
Busca híbrida
Alguns bancos suportam busca híbrida, que combina similaridade vetorial com busca por palavras-chave. Nesse caso, o provedor implementa a interface IKeywordHybridSearchable<TRecord> e oferece o método HybridSearchAsync.
Essa combinação melhora a qualidade dos resultados, porque a busca por palavras captura termos exatos que a similaridade pode perder. Para usá-la, o modelo precisa de um campo de texto com IsFullTextIndexed = true.
Implementando RAG com MEAI e Microsoft.Extensions.VectorData
A combinação do Microsoft.Extensions.AI com o Microsoft.Extensions.VectorData permite implementar o padrão RAG de forma coesa e legível. O método abaixo responde a uma pergunta usando os documentos mais relevantes da coleção.
public async Task<string> AskQuestionAsync(string question)
{
var contextParts = new List<string>();
// Recupera os três documentos mais relevantes
await foreach (var result in collection.SearchAsync(question, top: 3))
{
contextParts.Add(result.Record.Text);
}
var context = string.Join("\n\n", contextParts);
var messages = new List<ChatMessage>
{
new(ChatRole.System,
"Answer based on the provided context. If the context does not contain the information, tell the user."),
new(ChatRole.User,
$"Context:\n{context}\n\nQuestion: {question}")
};
var response = await chatClient.GetResponseAsync(messages);
return response.Text;
}
O método busca os documentos mais relevantes, monta o contexto e envia a pergunta enriquecida ao LLM. A mensagem de sistema orienta o modelo a admitir quando o contexto não traz a resposta, o que reduz respostas inventadas.
Note que o resultado é lido pela propriedade Text do ChatResponse, que concatena o texto das mensagens retornadas. Se quiser revisar o IChatClient e as demais abstrações, o artigo IA generativa no .NET e C# traz um resumo.
Provedores suportados pelo Microsoft.Extensions.VectorData
O Microsoft.Extensions.VectorData conta com conectores para os principais bancos vetoriais do mercado. Entre eles estão Azure AI Search, Qdrant, Redis, PostgreSQL, Azure Cosmos DB (NoSQL), SQL Server, SQLite e um provedor em memória.
Há ainda conectores adicionais para Elasticsearch, MongoDB, Weaviate e Pinecone, conforme o anúncio oficial da Microsoft. Como cada provedor é um pacote separado, consulte a documentação para obter o nome exato do pacote do banco escolhido.
Provedor em memória
O provedor em memória é indicado para desenvolvimento e prototipação, porque não exige nenhum serviço externo. Depois, basta trocá-lo pelo provedor de produção.
No entanto, a documentação desaconselha usá-lo em testes automatizados, já que o comportamento pode diferir do banco real. Para esses casos, a sugestão é rodar os testes contra o banco de produção com Testcontainers.
Por que VectorData é uma biblioteca separada
A separação do Microsoft.Extensions.VectorData em relação ao núcleo do Microsoft.Extensions.AI é uma decisão de design intencional. Afinal, nem toda aplicação inteligente precisa de armazenamento vetorial.
Chatbots, geradores de conteúdo e classificadores funcionam perfeitamente apenas com as abstrações de LLM. Mantendo a biblioteca separada, o pacote principal continua leve e focado.
Por outro lado, quem precisa de busca semântica, RAG ou memória de longo prazo para agentes adiciona o pacote de dados vetoriais. Com isso, aproveita imediatamente os mesmos padrões já conhecidos do MEAI.
Conclusão
O Microsoft.Extensions.VectorData traz para os bancos vetoriais o mesmo benefício que o Microsoft.Extensions.AI trouxe para os LLMs: uma interface unificada e independente de provedor.
Essa abordagem permite que você concentre esforços na lógica da aplicação, sem se preocupar com as particularidades de cada SDK. Além disso, a geração automática de embeddings, as operações em lote e os filtros em LINQ reduzem bastante o código repetitivo.
Combinado com o Microsoft.Extensions.AI, o Microsoft.Extensions.VectorData compõe a base técnica para construir aplicações inteligentes e portáveis. Portanto, ele é um ponto de partida sólido para quem quer adotar RAG e busca semântica no .NET.
FAQ
O que é o Microsoft.Extensions.VectorData?
É uma biblioteca do .NET com abstrações comuns para bancos de dados vetoriais. Com ela, o mesmo código funciona em provedores como Qdrant, Redis, PostgreSQL e SQL Server.
Preciso gerar os embeddings manualmente?
Não, basta configurar um IEmbeddingGenerator no VectorStore ou na coleção. A biblioteca gera os embeddings na gravação e na busca automaticamente.
Qual a diferença entre busca semântica e busca textual?
Enquanto a busca textual compara termos de forma literal, a busca semântica compara o significado. Para isso, ela mede a distância entre vetores.
Como inserir vários registros de uma vez?
Passe uma coleção de registros para o método UpsertAsync. Essa sobrecarga reduz as idas ao banco e pode gerar os embeddings em uma única chamada.
Posso usar o provedor em memória nos testes?
A documentação recomenda usá-lo apenas em desenvolvimento e prototipação. Para testes, prefira o banco real com Testcontainers.