Leaders Logo
Vector Store em .NET 10: Persistência e Recuperação de Contexto com Abstrações Microsoft

Vector Store em .NET 10: Persistência e Recuperação de Contexto com Abstrações Microsoft

Introdução

Vector stores deixaram de ser componentes experimentais e passaram a ocupar uma posição prática em arquiteturas corporativas que combinam busca semântica, recomendação, agentes e Retrieval-Augmented Generation (RAG). A ideia central é persistir representações vetoriais de conteúdos, consultas e metadados para recuperar evidências por proximidade semântica, não apenas por coincidência literal de palavras. Esse avanço é sustentado por embeddings de sentença, recuperação densa e modelos generativos capazes de consumir contexto recuperado (REIMERS; GUREVYCH, 2019) (KARPUKHIN et al., 2020) (LEWIS et al., 2020).

No .NET 10, o tema ganha maturidade porque a aplicação pode organizar esse fluxo com abstrações conhecidas do ecossistema Microsoft: injeção de dependências, Options Pattern, Microsoft.Extensions.AI, provedores configuráveis e contratos próprios para isolar a persistência vetorial. O objetivo não é prender a solução a um banco específico, mas criar uma fronteira clara entre domínio, geração de embeddings, armazenamento e recuperação de contexto.

O papel de um vector store

Um vector store mantém, de forma indexável, três grupos de dados: a chave do item, o vetor numérico produzido pelo modelo de embeddings e os metadados necessários para filtragem, auditoria e composição de resposta. Em uma base de conhecimento, por exemplo, cada trecho de documento pode armazenar título, fonte, categoria, permissões, versão do modelo e data de geração do embedding.

Essa estrutura permite consultar os vizinhos mais próximos de uma pergunta, recuperar trechos prováveis de resposta e montar um prompt fundamentado em evidências. Em cenários de RAG, a qualidade final depende menos de “chamar um modelo maior” e mais de recuperar contexto correto, ordenado e rastreável (LEWIS et al., 2020).

Modelagem de domínio em .NET 10

O primeiro passo é modelar o registro vetorial como parte explícita do domínio de busca. A aplicação não deve tratar embeddings como um detalhe invisível, pois decisões como dimensão, versão do modelo e política de atualização afetam diretamente a qualidade do ranking.

public sealed class KnowledgeChunk
{
    public required string Id { get; init; }
    public required string DocumentId { get; init; }
    public required string Title { get; init; }
    public required string Content { get; init; }
    public required string SourceUri { get; init; }
    public required string EmbeddingModel { get; init; }
    public required ReadOnlyMemory<float> Vector { get; init; }
    public DateTimeOffset CreatedAt { get; init; } = DateTimeOffset.UtcNow;
}

Essa modelagem favorece manutenção e reprocessamento. Quando o modelo de embeddings muda, a solução consegue identificar quais registros precisam ser recalculados, sem depender de convenções implícitas no backend.

Contrato de persistência vetorial

Mesmo quando o projeto utiliza um provedor específico, vale manter um contrato de aplicação para evitar acoplamento prematuro. O contrato abaixo separa operações centrais: gravar conteúdo vetorizado, recuperar por chave e buscar vizinhos semanticamente próximos.

public sealed record VectorSearchResult(
    string Id,
    string Content,
    string SourceUri,
    double Score);

public interface IKnowledgeVectorStore
{
    Task UpsertAsync(KnowledgeChunk chunk, CancellationToken cancellationToken = default);

    Task<KnowledgeChunk?> GetAsync(string id, CancellationToken cancellationToken = default);

    Task<IReadOnlyList<VectorSearchResult>> SearchAsync(
        ReadOnlyMemory<float> queryVector,
        int top,
        CancellationToken cancellationToken = default);
}

Esse desenho é compatível com backends diferentes, como Azure AI Search, Azure Cosmos DB, SQL Server com suporte vetorial, PostgreSQL com pgvector, Qdrant ou Elasticsearch. A troca de provedor passa a ser uma decisão de infraestrutura, não uma reescrita do fluxo de negócio.

Ingestão com Microsoft.Extensions.AI

Em .NET 10, a geração de embeddings pode ser organizada por Microsoft.Extensions.AI, mantendo a aplicação desacoplada do serviço concreto que calcula vetores. Esse ponto é importante porque custos, latência, qualidade e privacidade variam bastante entre modelos locais, serviços gerenciados e provedores externos.

using Microsoft.Extensions.AI;

public sealed class KnowledgeIngestionService(
    IEmbeddingGenerator<string, Embedding<float>> embeddingGenerator,
    IKnowledgeVectorStore vectorStore)
{
    public async Task<string> SaveAsync(
        string documentId,
        string title,
        string content,
        string sourceUri,
        CancellationToken cancellationToken = default)
    {
        var embedding = await embeddingGenerator.GenerateVectorAsync(content, cancellationToken: cancellationToken);

        var chunk = new KnowledgeChunk
        {
            Id = $"{documentId}:{Guid.CreateVersion7()}",
            DocumentId = documentId,
            Title = title,
            Content = content,
            SourceUri = sourceUri,
            EmbeddingModel = "configured-embedding-model",
            Vector = embedding
        };

        await vectorStore.UpsertAsync(chunk, cancellationToken);
        return chunk.Id;
    }
}

Do ponto de vista arquitetural, esse serviço deve ficar próximo do pipeline de ingestão, não misturado à camada de apresentação. Assim, normalização de texto, segmentação em chunks, enriquecimento de metadados e reprocessamentos podem evoluir sem afetar a experiência do usuário.

Configuração por Options Pattern

Vector stores precisam de parâmetros operacionais: nome de coleção, dimensão do embedding, métrica de distância, limite padrão de resultados e provedor ativo. O Options Pattern deixa esses parâmetros explícitos e validáveis na inicialização da aplicação.

public sealed class VectorStoreOptions
{
    public const string SectionName = "VectorStore";

    public required string Provider { get; init; }
    public required string CollectionName { get; init; }
    public int Dimensions { get; init; } = 1536;
    public int DefaultTopK { get; init; } = 5;
    public string DistanceMetric { get; init; } = "cosine";
}

builder.Services
    .AddOptions<VectorStoreOptions>()
    .BindConfiguration(VectorStoreOptions.SectionName)
    .Validate(options => options.Dimensions > 0, "Dimensions must be positive.")
    .Validate(options => options.DefaultTopK is > 0 and <= 50, "DefaultTopK must be between 1 and 50.")
    .ValidateOnStart();

Essa abordagem evita que a aplicação descubra configurações inválidas apenas no primeiro acesso real ao backend. Também facilita ambientes com provedores diferentes para desenvolvimento, homologação e produção.

Providers plugáveis com DI

Com injeção de dependências, a seleção do backend pode ser encapsulada no registro de serviços. O código de aplicação continua usando IKnowledgeVectorStore, enquanto a composição escolhe a implementação adequada.

builder.Services.AddSingleton<IKnowledgeVectorStore>(serviceProvider =>
{
    var options = serviceProvider
        .GetRequiredService<IOptions<VectorStoreOptions>>()
        .Value;

    return options.Provider.ToLowerInvariant() switch
    {
        "azure-ai-search" => ActivatorUtilities.CreateInstance<AzureAiSearchVectorStore>(serviceProvider),
        "cosmosdb" => ActivatorUtilities.CreateInstance<CosmosDbVectorStore>(serviceProvider),
        "qdrant" => ActivatorUtilities.CreateInstance<QdrantVectorStore>(serviceProvider),
        _ => throw new InvalidOperationException($"Unsupported vector store provider: {options.Provider}")
    };
});

Esse padrão reduz o risco de dependências espalhadas pela solução. Quando uma organização decide migrar de um provedor gerenciado para outro, o impacto fica concentrado na infraestrutura e nos testes de contrato.

Busca semântica e recuperação de contexto

A consulta semântica repete a lógica da ingestão: gerar o embedding da pergunta, buscar vetores próximos e retornar evidências ordenadas por score. Em bases grandes, estruturas de approximate nearest neighbors tornam-se relevantes porque reduzem custo e latência com perda controlada de recall (MALKOV; YASHUNIN, 2020).

public sealed class SemanticSearchService(
    IEmbeddingGenerator<string, Embedding<float>> embeddingGenerator,
    IKnowledgeVectorStore vectorStore,
    IOptions<VectorStoreOptions> options)
{
    public async Task<IReadOnlyList<VectorSearchResult>> SearchAsync(
        string question,
        CancellationToken cancellationToken = default)
    {
        var embedding = await embeddingGenerator.GenerateVectorAsync(question, cancellationToken: cancellationToken);

        return await vectorStore.SearchAsync(
            embedding,
            options.Value.DefaultTopK,
            cancellationToken);
    }
}

Quando a aplicação exige filtros rígidos, como tenant, perfil de acesso, categoria ou vigência documental, esses filtros devem ser aplicados antes ou durante a busca vetorial. Sem isso, o ranking pode ser semanticamente bom, mas operacionalmente incorreto.

RAG com contexto rastreável

RAG não deve ser entendido apenas como “buscar documentos e chamar um LLM”. O ganho real está em construir uma resposta limitada às evidências recuperadas, com fontes preservadas e possibilidade de auditoria. Levantamentos recentes reforçam que a qualidade do pipeline de recuperação, do chunking e da avaliação é decisiva para sistemas de RAG em produção (GAO et al., 2024).

public sealed class GroundedAnswerService(
    SemanticSearchService searchService,
    IChatClient chatClient)
{
    public async Task<string> AnswerAsync(string question, CancellationToken cancellationToken = default)
    {
        var matches = await searchService.SearchAsync(question, cancellationToken);

        var context = string.Join(
            "\n\n",
            matches.Select(match => $"Source: {match.SourceUri}\nScore: {match.Score:F4}\n{match.Content}"));

        var response = await chatClient.GetResponseAsync(
            $"""
            Answer using only the context below. If the context is insufficient, say so.

            Question:
            {question}

            Context:
            {context}
            """,
            cancellationToken: cancellationToken);

        return response.Text;
    }
}

Essa disciplina reduz respostas sem base documental e torna a solução mais adequada para domínios como jurídico, saúde, suporte técnico, engenharia e políticas internas.

Governança e segurança

Embeddings devem receber tratamento de dado sensível quando derivam de conteúdos sensíveis. Eles podem preservar traços estatísticos do texto original e participar de fluxos de inferência, extração ou vazamento quando controles de acesso são frágeis (CARLINI et al., 2021). Por isso, permissões, retenção, mascaramento, segregação por tenant e trilhas de auditoria precisam acompanhar a persistência vetorial desde o desenho inicial.

Também é recomendável armazenar versão do modelo, data de geração, origem do documento e política de reindexação. Sem esses campos, a equipe perde a capacidade de explicar por que uma resposta mudou após uma atualização de modelo ou reprocessamento de corpus.

Observabilidade e avaliação

Vector stores em produção devem expor métricas além de disponibilidade. Latência de ingestão, latência de busca, distribuição de scores, taxa de documentos sem embedding, erro por provedor, custo por mil embeddings e qualidade percebida por categoria de consulta são sinais operacionais relevantes. Esses dados ajudam a diferenciar problemas de infraestrutura, degradação do corpus e baixa aderência do modelo de embeddings ao domínio.

Uma avaliação simples pode começar com um conjunto fixo de perguntas reais, respostas esperadas e documentos de referência. A cada troca de modelo, chunking ou índice, esse conjunto mede se a mudança melhorou recuperação ou apenas deslocou o ranking de forma imprevisível.

Conclusão

Persistir e recuperar contexto vetorial em .NET 10 é uma decisão arquitetural, não apenas uma escolha de banco de dados. As abstrações Microsoft ajudam a organizar o ciclo completo: ingestão, configuração, provider plugável, busca semântica, geração fundamentada e observabilidade. Quando esses pontos são tratados como contratos claros, a aplicação ganha liberdade para trocar backends, evoluir modelos de embeddings e operar RAG com rastreabilidade. O resultado é uma plataforma mais modular, testável e preparada para demandas corporativas de busca semântica e inteligência contextual.

Referências

  • REIMERS, Nils; GUREVYCH, Iryna. Sentence-BERT: sentence embeddings using Siamese BERT-networks. In: PROCEEDINGS OF THE 2019 CONFERENCE ON EMPIRICAL METHODS IN NATURAL LANGUAGE PROCESSING. Hong Kong: Association for Computational Linguistics, 2019. p. 3982-3992. reference.Description
  • KARPUKHIN, Vladimir et al. Dense passage retrieval for open-domain question answering. In: PROCEEDINGS OF THE 2020 CONFERENCE ON EMPIRICAL METHODS IN NATURAL LANGUAGE PROCESSING. Online: Association for Computational Linguistics, 2020. p. 6769-6781. reference.Description
  • LEWIS, Patrick et al. Retrieval-augmented generation for knowledge-intensive NLP tasks. In: ADVANCES IN NEURAL INFORMATION PROCESSING SYSTEMS. Red Hook: Curran Associates, 2020. v. 33, p. 9459-9474. reference.Description
  • MALKOV, Yu A.; YASHUNIN, Dmitry A. Efficient and robust approximate nearest neighbor search using hierarchical navigable small world graphs. IEEE Transactions on Pattern Analysis and Machine Intelligence, New York, v. 42, n. 4, p. 824-836, 2020. reference.Description
  • GAO, Yunfan et al. Retrieval-augmented generation for large language models: a survey. arXiv preprint arXiv:2312.10997, 2024. reference.Description
  • CARLINI, Nicholas et al. Extracting training data from large language models. In: 30TH USENIX SECURITY SYMPOSIUM. Berkeley: USENIX Association, 2021. p. 2633-2650. reference.Description
Sobre o autor