pgvector na prática: IA e busca vetorial no PostgreSQL
Guardar embeddings ao lado dos dados transacionais é tentador — e funciona. Mas índice ANN tem trade-off de recall que DBA nenhum estava acostumado a calibrar. HNSW, IVFFlat, quantização e filtros que matam a performance.

Em 2023 a pergunta era "dá para usar Postgres como banco vetorial?". Em 2026 a pergunta é outra: "por que eu manteria um segundo banco só para vetores?". Com pgvector, o embedding fica na mesma transação, no mesmo backup, com a mesma permissão e o mesmo JOIN dos dados de negócio. Isso é uma vantagem operacional enorme.
O que ninguém avisa é que busca vetorial quebra as intuições de índice que você levou 20 anos construindo. Índice B-tree é exato. Índice ANN é aproximado, por definição. Você passa a negociar acerto contra velocidade — e essa negociação é um parâmetro que alguém precisa escolher.
O básico que funciona
CREATE EXTENSION IF NOT EXISTS vector;
CREATE TABLE documentos (
id bigserial PRIMARY KEY,
tenant_id bigint NOT NULL,
titulo text NOT NULL,
corpo text,
embedding vector(1536), -- dimensao do seu modelo
criado_em timestamptz NOT NULL DEFAULT now()
);
-- Busca por similaridade de cosseno (operador <=>)
SELECT id, titulo, 1 - (embedding <=> $1) AS similaridade
FROM documentos
ORDER BY embedding <=> $1
LIMIT 10;
Os operadores importam e são confundidos direto: <-> é distância L2, <=> é cosseno e <#> é produto interno negativo. O índice só é usado se o operador do ORDER BY for o mesmo da classe de operador do índice. Erro clássico: criar índice para cosseno e consultar com L2 — o Postgres faz sequential scan e ninguém entende por quê.
HNSW ou IVFFlat?
| Critério | HNSW | IVFFlat |
|---|---|---|
| Recall com config padrão | Alto | Médio |
| Tempo de construção | Lento | Rápido |
| Memória | Alta (o grafo quer caber na RAM) | Baixa |
| Precisa de dados antes de indexar | Não | Sim (define centróides) |
| Inserção contínua | Boa | Degrada; exige reindex periódico |
Minha regra: HNSW por padrão. IVFFlat só quando o dataset é gigante, a memória é curta e a carga é majoritariamente em lote.
-- HNSW: m = vizinhos por no; ef_construction = esforco na construcao
CREATE INDEX idx_doc_emb_hnsw ON documentos
USING hnsw (embedding vector_cosine_ops)
WITH (m = 16, ef_construction = 64);
-- Acelera a construcao (a sessao pode usar bastante memoria)
SET maintenance_work_mem = '4GB';
SET max_parallel_maintenance_workers = 4;
-- Na consulta: quanto maior o ef_search, melhor o recall e pior a latencia
SET hnsw.ef_search = 100;
Como calibrar recall de verdade
Não chute o ef_search. Meça contra a verdade absoluta. Rode a mesma consulta com o índice desligado (busca exata) e compare os IDs retornados:
SET enable_indexscan = off; -- resultado exato, lento
-- guarde os 10 IDs retornados
RESET enable_indexscan;
-- repita com ef_search em 40, 100, 200 e compare a intersecao
Recall de 95% a 99% costuma ser o ponto doce para busca semântica de conteúdo. Para caso jurídico ou médico, onde deixar de achar um documento é grave, suba o ef_search e aceite a latência.
A armadilha do filtro
Esse é o erro de arquitetura em produção multi-tenant. Você escreve:
SELECT id, titulo
FROM documentos
WHERE tenant_id = 42
ORDER BY embedding <=> $1
LIMIT 10;
O índice HNSW busca os vizinhos mais próximos no conjunto inteiro e só depois aplica o filtro. Se o tenant 42 tem 0,1% dos documentos, os 10 vizinhos globais podem não conter nenhum dele — e aí o Postgres precisa varrer muito mais do grafo, ou devolve menos linhas do que você pediu. Duas saídas:
-- A) Indice parcial por tenant grande
CREATE INDEX idx_doc_emb_t42 ON documentos
USING hnsw (embedding vector_cosine_ops)
WHERE tenant_id = 42;
-- B) Particionar por tenant e indexar cada particao
CREATE TABLE documentos_p PARTITION OF documentos_base
FOR VALUES IN (42);
Tamanho: o detalhe que estoura o disco
Um vetor de 1536 dimensões em float4 ocupa cerca de 6 KB por linha, mais o grafo HNSW. Dez milhões de documentos viram dezenas de gigabytes só de embedding — e isso compete com a sua RAM de cache. Reduza:
-- Meia precisao: metade do espaco, perda de recall geralmente marginal
ALTER TABLE documentos ALTER COLUMN embedding TYPE halfvec(1536);
CREATE INDEX ON documentos USING hnsw (embedding halfvec_cosine_ops);
-- Ou binarizacao para um primeiro filtro grosseiro, refinando depois
CREATE INDEX ON documentos
USING hnsw ((binary_quantize(embedding)::bit(1536)) bit_hamming_ops);
Vale lembrar também que coluna grande vai para TOAST, e reescrita de embedding gera tupla morta como qualquer outra. Ou seja: workload vetorial pesado é gerador industrial de bloat. Revise o ajuste de VACUUM antes de colocar RAG em produção.
Busca híbrida: o que realmente funciona em RAG
Busca puramente semântica erra em nome próprio, código de produto e número de contrato. A combinação com busca textual nativa resolve:
WITH semantica AS (
SELECT id, row_number() OVER (ORDER BY embedding <=> $1) AS pos
FROM documentos ORDER BY embedding <=> $1 LIMIT 50
),
textual AS (
SELECT id, row_number() OVER (
ORDER BY ts_rank_cd(to_tsvector('portuguese', corpo),
plainto_tsquery('portuguese', $2)) DESC) AS pos
FROM documentos
WHERE to_tsvector('portuguese', corpo) @@ plainto_tsquery('portuguese', $2)
LIMIT 50
)
SELECT d.id, d.titulo,
COALESCE(1.0/(60 + s.pos), 0) + COALESCE(1.0/(60 + t.pos), 0) AS score
FROM documentos d
LEFT JOIN semantica s ON s.id = d.id
LEFT JOIN textual t ON t.id = d.id
WHERE s.id IS NOT NULL OR t.id IS NOT NULL
ORDER BY score DESC
LIMIT 10;
Isso é Reciprocal Rank Fusion em SQL puro, sem serviço externo. É o tipo de coisa que só o Postgres deixa você fazer numa consulta só.
Quando eu não usaria o Postgres para vetores
- Acima de algumas centenas de milhões de vetores com exigência de latência de poucos milissegundos — aí um mecanismo dedicado ganha.
- Quando o embedding é o produto inteiro e não tem dado transacional junto.
- Quando o time precisa trocar de modelo toda semana e reindexar 100% do acervo é rotina.
Fora esses casos, manter tudo em um banco só costuma valer mais do que qualquer ganho de benchmark. Veja o resto das dores no guia definitivo de PostgreSQL em produção, e o efeito disso no plano de execução em query planner imprevisível.
Sobre o autor: Alexandre Almeida atua com bancos de dados há mais de 25 anos, foi instrutor oficial de MySQL e Oracle para mais de 1.000 alunos e escreveu Aprendendo MySQL: Prática, Teoria e Laboratórios. Exemplos validados com pgvector 0.8 em PostgreSQL 16 e 17.
Fontes: documentação oficial do pgvector (índices HNSW e IVFFlat, halfvec e binary quantize); PostgreSQL Documentation — Full Text Search e TOAST.