IA RAG .NET PostgreSQL Vector Database

Vector databases no .NET: pgvector, Qdrant e Pinecone — qual usar para RAG em produção

Comparativo técnico de bancos vetoriais para .NET: pgvector no PostgreSQL, Qdrant e Pinecone. Quando usar cada um, implementação com C# e estratégias de chunking e hybrid search.

N
Neryx Digital Architects
5 de junho de 2026
16 min de leitura
320 profissionais leram
Categoria: Arquitetura Público: Times de engenharia e produto Etapa: Aprendizado

Toda aplicação de RAG (Retrieval-Augmented Generation) precisa de um lugar para armazenar e buscar embeddings. A escolha do banco vetorial impacta latência, custo de infraestrutura, complexidade operacional e — o que menos se fala — a qualidade dos resultados da busca.

Este artigo cobre os três bancos vetoriais mais usados em projetos .NET: pgvector (extensão do PostgreSQL), Qdrant (banco vetorial open-source) e Pinecone (managed cloud). Para cada um: implementação em C#, casos de uso ideais e trade-offs reais de produção.

O que é um vector database e por que você precisa de um

Quando você gera um embedding de um texto — um vetor de 1.536 dimensões no caso do OpenAI, 1.024 no Cohere — você precisa armazená-lo de forma que consultas de similaridade sejam rápidas. Bancos relacionais convencionais não são eficientes para isso: fazer `SELECT * FROM docs ORDER BY cosine_similarity(embedding, $query)` em uma tabela com 500 mil linhas é impraticável.

Bancos vetoriais resolvem isso com algoritmos de approximate nearest neighbor (ANN) — estruturas de índice como HNSW (Hierarchical Navigable Small World) e IVF (Inverted File Index) que encontram os N vetores mais similares em tempo sublinear.

O HNSW é o algoritmo dominante hoje. Constrói um grafo em múltiplas camadas onde cada nó aponta para seus vizinhos mais próximos. A busca navega do nível superior (poucos nós, visão global) descendo para o nível inferior (todos os nós, precisão máxima). Resultado: busca em O(log n) com recall próximo de 100% para parâmetros bem calibrados.

pgvector: banco vetorial dentro do PostgreSQL que você já tem

pgvector é uma extensão do PostgreSQL que adiciona o tipo `vector` e suporte a índices HNSW e IVF. Se você já usa PostgreSQL, adicionar pgvector significa zero nova infraestrutura para começar.

Instalação e configuração

-- Habilitar extensão
CREATE EXTENSION IF NOT EXISTS vector;

-- Tabela de documentos com embedding
CREATE TABLE documents (
    id          BIGSERIAL PRIMARY KEY,
    content     TEXT NOT NULL,
    metadata    JSONB,
    embedding   vector(1536),  -- dimensões do modelo OpenAI
    created_at  TIMESTAMPTZ DEFAULT NOW()
);

-- Índice HNSW (recomendado para a maioria dos casos)
CREATE INDEX ON documents USING hnsw (embedding vector_cosine_ops)
    WITH (m = 16, ef_construction = 64);

O parâmetro m define quantas conexões cada nó mantém no grafo. Valores maiores aumentam recall mas aumentam uso de memória e tempo de indexação. Para produção, comece com m=16 e ef_construction=64 — bom equilíbrio entre qualidade e performance.

Implementação em C# com Npgsql

// Package: Pgvector.EntityFrameworkCore
// dotnet add package Pgvector.EntityFrameworkCore

using Pgvector;
using Microsoft.EntityFrameworkCore;

public class Document
{
    public long Id { get; set; }
    public string Content { get; set; } = string.Empty;
    public JsonDocument? Metadata { get; set; }
    public Vector? Embedding { get; set; }
    public DateTimeOffset CreatedAt { get; set; }
}

public class DocumentDbContext : DbContext
{
    public DbSet<Document> Documents => Set<Document>();

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.HasPostgresExtension("vector");

        modelBuilder.Entity<Document>()
            .Property(d => d.Embedding)
            .HasColumnType("vector(1536)");
    }
}

public class VectorSearchService
{
    private readonly DocumentDbContext _context;
    private readonly IEmbeddingService _embeddingService;

    public VectorSearchService(DocumentDbContext context, IEmbeddingService embeddingService)
    {
        _context = context;
        _embeddingService = embeddingService;
    }

    public async Task IndexDocumentAsync(string content, object? metadata = null)
    {
        var embedding = await _embeddingService.GetEmbeddingAsync(content);

        _context.Documents.Add(new Document
        {
            Content = content,
            Metadata = metadata is null ? null : JsonDocument.Parse(JsonSerializer.Serialize(metadata)),
            Embedding = new Vector(embedding),
        });

        await _context.SaveChangesAsync();
    }

    public async Task<List<Document>> SearchAsync(string query, int topK = 5)
    {
        var queryEmbedding = await _embeddingService.GetEmbeddingAsync(query);
        var queryVector = new Vector(queryEmbedding);

        // Cosine similarity com HNSW index
        return await _context.Documents
            .OrderBy(d => d.Embedding!.CosineDistance(queryVector))
            .Take(topK)
            .ToListAsync();
    }
}

Quando usar pgvector

pgvector é a escolha certa quando:

  • Você já usa PostgreSQL e quer evitar nova infraestrutura
  • Precisa de transações ACID envolvendo documentos e embeddings
  • O volume é menor que 1 milhão de vetores (HNSW no pgvector tem boa performance até esse range)
  • Precisa de filtros complexos combinados com busca vetorial (usando índices compostos do Postgres)
  • Hosting em Railway, Supabase, Neon ou qualquer managed PostgreSQL já suporta pgvector

Limitação importante: o índice HNSW do pgvector é carregado inteiramente na memória RAM durante a busca. Para 1 milhão de vetores de 1.536 dimensões com float32, isso significa ~6 GB só de índice. Planeje a memória do servidor antes de escalar.

Qdrant: banco vetorial open-source para performance e controle

Qdrant é um banco vetorial escrito em Rust, open-source (Apache 2.0), com client oficial para .NET. Ele foi projetado exclusivamente para busca vetorial — sem o compromisso do PostgreSQL de ser um banco relacional de uso geral.

Isso significa: índice HNSW mais eficiente em memória, suporte a quantização (redução de precisão para economizar memória), e filtros de payload nativos sem JOINs.

Setup com Docker

# docker-compose.yml
services:
  qdrant:
    image: qdrant/qdrant:latest
    ports:
      - "6333:6333"
    volumes:
      - qdrant_storage:/qdrant/storage
    environment:
      QDRANT__SERVICE__GRPC_PORT: "6334"

volumes:
  qdrant_storage:

Implementação em C#

// Package: Qdrant.Client
// dotnet add package Qdrant.Client

using Qdrant.Client;
using Qdrant.Client.Grpc;

public class QdrantVectorService
{
    private readonly QdrantClient _client;
    private readonly IEmbeddingService _embeddingService;
    private const string CollectionName = "documents";

    public QdrantVectorService(QdrantClient client, IEmbeddingService embeddingService)
    {
        _client = client;
        _embeddingService = embeddingService;
    }

    public async Task EnsureCollectionAsync()
    {
        var collections = await _client.ListCollectionsAsync();
        if (collections.Any(c => c == CollectionName)) return;

        await _client.CreateCollectionAsync(CollectionName, new VectorParams
        {
            Size = 1536,
            Distance = Distance.Cosine,
            // Quantização escalar: reduz memória em ~4x com perda mínima de recall
            QuantizationConfig = new QuantizationConfig
            {
                Scalar = new ScalarQuantization
                {
                    Type = QuantizationType.Int8,
                    AlwaysRam = true,
                }
            }
        });
    }

    public async Task UpsertDocumentAsync(Guid id, string content, Dictionary<string, object> metadata)
    {
        var embedding = await _embeddingService.GetEmbeddingAsync(content);

        var payload = metadata.ToDictionary(
            kv => kv.Key,
            kv => Value.ForString(kv.Value?.ToString() ?? string.Empty)
        );
        payload["content"] = Value.ForString(content);

        await _client.UpsertAsync(CollectionName, new[]
        {
            new PointStruct
            {
                Id = new PointId { Uuid = id.ToString() },
                Vectors = embedding,
                Payload = { payload }
            }
        });
    }

    public async Task<List<ScoredPoint>> SearchAsync(
        string query,
        Dictionary<string, string>? filters = null,
        int topK = 5)
    {
        var queryEmbedding = await _embeddingService.GetEmbeddingAsync(query);

        // Filtros de payload — executados no índice, não em post-processing
        Filter? filter = null;
        if (filters is { Count: > 0 })
        {
            filter = new Filter
            {
                Must =
                {
                    filters.Select(kv => new Condition
                    {
                        Field = new FieldCondition
                        {
                            Key = kv.Key,
                            Match = new Match { Text = kv.Value }
                        }
                    })
                }
            };
        }

        return await _client.SearchAsync(CollectionName,
            vector: queryEmbedding,
            filter: filter,
            limit: (ulong)topK,
            withPayload: true);
    }
}

Quando usar Qdrant

  • Volume entre 1 milhão e 100 milhões de vetores
  • Precisa de filtros de metadados performáticos (filtered vector search nativo)
  • Quer controle total sobre onde os dados ficam (on-premise ou cloud própria)
  • Trabalha com multi-tenancy: named vectors e collections separadas por tenant
  • Precisa de quantização para economizar RAM sem sacrificar recall

Pinecone: managed cloud sem operação

Pinecone é um banco vetorial totalmente gerenciado. Você não gerencia servidores, índices, ou escalabilidade. A troca: custo mais alto, dados em infraestrutura de terceiro, e vendor lock-in.

// Package: Pinecone.NET
// dotnet add package Pinecone.NET

using Pinecone;

public class PineconeVectorService
{
    private readonly PineconeClient _client;
    private readonly IEmbeddingService _embeddingService;
    private const string IndexName = "documents";

    public PineconeVectorService(PineconeClient client, IEmbeddingService embeddingService)
    {
        _client = client;
        _embeddingService = embeddingService;
    }

    public async Task UpsertAsync(string id, string content, Dictionary<string, string> metadata)
    {
        var embedding = await _embeddingService.GetEmbeddingAsync(content);
        var index = _client.Index(IndexName);

        await index.UpsertAsync(new[]
        {
            new Vector
            {
                Id = id,
                Values = embedding,
                Metadata = new MetadataMap(
                    metadata.ToDictionary(
                        kv => kv.Key,
                        kv => (MetadataValue)kv.Value
                    )
                )
            }
        });
    }

    public async Task<QueryResponse> SearchAsync(string query, int topK = 5)
    {
        var queryEmbedding = await _embeddingService.GetEmbeddingAsync(query);
        var index = _client.Index(IndexName);

        return await index.QueryAsync(new QueryRequest
        {
            Vector = queryEmbedding,
            TopK = (uint)topK,
            IncludeMetadata = true
        });
    }
}

Quando usar Pinecone

  • Time sem expertise em infraestrutura para operar Qdrant self-hosted
  • Protótipo ou MVP onde velocidade de entrega supera custo
  • Volume muito grande (bilhões de vetores) onde escalabilidade automática é necessária
  • LGPD e compliance não impedem dados em infra terceirizada (EUA)

Hybrid search: semântico + BM25

Busca puramente semântica falha em casos específicos: termos técnicos exatos (IDs de produto, códigos de erro, nomes próprios), queries muito curtas, e buscas onde a correspondência exata importa mais que o significado.

Hybrid search combina busca vetorial (semântica) com BM25 (texto exato) usando Reciprocal Rank Fusion (RRF) para unir os rankings.

public class HybridSearchService
{
    private readonly QdrantVectorService _vectorSearch;
    private readonly BM25SearchService _bm25Search;

    // RRF: combina rankings de múltiplos sistemas de busca
    // Score = sum(1 / (k + rank_i)) onde k=60 é constante padrão
    public async Task<List<SearchResult>> SearchAsync(string query, int topK = 10)
    {
        var vectorResults = await _vectorSearch.SearchAsync(query, topK: topK * 2);
        var bm25Results = await _bm25Search.SearchAsync(query, topK: topK * 2);

        var rrfScores = new Dictionary<string, double>();
        const int k = 60;

        for (int i = 0; i < vectorResults.Count; i++)
        {
            var id = vectorResults[i].Id.ToString();
            rrfScores.TryAdd(id, 0);
            rrfScores[id] += 1.0 / (k + i + 1);
        }

        for (int i = 0; i < bm25Results.Count; i++)
        {
            var id = bm25Results[i].Id;
            rrfScores.TryAdd(id, 0);
            rrfScores[id] += 1.0 / (k + i + 1);
        }

        return rrfScores
            .OrderByDescending(kv => kv.Value)
            .Take(topK)
            .Select(kv => new SearchResult { Id = kv.Key, Score = kv.Value })
            .ToList();
    }
}

Qdrant suporta hybrid search nativamente com sparse vectors. pgvector requer a implementação manual com tsvector do PostgreSQL para BM25. Pinecone também suporta sparse-dense nativo.

Estratégias de chunking que afetam mais o recall do que o modelo

A qualidade da busca vetorial depende muito de como você divide o texto antes de gerar os embeddings. Chunks mal feitos degradam o recall independente do banco ou modelo escolhido.

Estratégias em ordem crescente de sofisticação:

  • Fixed-size chunking: divide por número de tokens (ex: 512 tokens com overlap de 64). Simples, funciona para textos homogêneos.
  • Recursive text splitting: divide por parágrafos, depois frases, depois palavras — respeita a estrutura natural do texto. Implementação padrão do LangChain.
  • Semantic chunking: calcula similaridade entre frases consecutivas. Quando a similaridade cai abaixo de um threshold, divide. Chunks semanticamente coesos.
  • Hierarchical chunking: gera embeddings em múltiplos níveis (documento, seção, parágrafo). Busca no nível mais alto, recupera no nível mais baixo. Melhor para documentos longos.

Para a maioria dos projetos B2B com PDFs e documentos internos: recursive text splitting com chunks de 400-600 tokens e overlap de 80-100 tokens é o ponto de partida correto.

Comparativo final

Critério pgvector Qdrant Pinecone
Setup PostgreSQL existente Docker / cloud SaaS, zero infra
Escala (vetores) Até ~1M 1M–100M+ Ilimitado (pago)
Transações ACID Sim (PostgreSQL) Não Não
Filtros de payload Via SQL / índice composto Nativo e performático Nativo
Hybrid search Manual (tsvector) Nativo (sparse vectors) Nativo
Custo de infra Incluso no PostgreSQL RAM + self-host ou cloud Por vetor + queries
Controle de dados Total Total (self-host) Dados na Pinecone Inc.
Quantização Não Sim (int8, binary) Automática

Conclusão: como escolher

Para a maioria dos projetos que estamos desenvolvendo hoje, a decisão é binária:

pgvector se você já tem PostgreSQL, o volume é razoável (<1M docs) e quer manter a stack simples. A observabilidade, o backup e a gestão de conexões já estão resolvidos — você não adiciona uma nova peça na infra.

Qdrant se a busca vetorial é core do produto, precisa de filtros performáticos em metadados, ou o volume exige quantização para controlar custos de RAM. É mais complexo de operar mas oferece mais controle.

Pinecone faz sentido em contextos específicos — protótipos rápidos ou escala muito alta sem capacidade de operação própria. Para projetos sérios com requisitos de compliance de dados no Brasil, a questão de dados hospedados fora do país precisa de análise jurídica.

A escolha do banco vetorial importa, mas importa menos do que a estratégia de chunking e a qualidade dos embeddings. Um sistema RAG ruim com pgvector e chunking bem feito vai superar um sistema com Pinecone e chunks de má qualidade.

Precisa desenhar a próxima fase com menos retrabalho?

Fazemos discovery técnico para mapear riscos, arquitetura-alvo e sequência de execução antes de investir pesado.

Solicitar Discovery

Newsletter

Receba artigos como este no seu e-mail

Conteúdo técnico sobre arquitetura de software, .NET, IA e gestão de produto. Sem spam.