Você não precisa de um banco vetorial dedicado para fazer RAG. O PostgreSQL, que você provavelmente já usa, resolve isso com a extensão pgvector — busca por similaridade, filtros relacionais e transações no mesmo lugar. Neste guia mostro como montar uma busca semântica de ponta a ponta com Python.
Vou construir um sistema que indexa documentos e responde perguntas com base neles.
Por que pgvector
- Menos infraestrutura: um banco em vez de dois.
- Filtros combinados: similaridade + WHERE relacional.
- Transações: consistência entre dados e vetores.
- Maduro: backup, réplica e ferramentas que você já conhece.
Subindo o Postgres com pgvector
docker run -d --name pgv -p 5432:5432 \
-e POSTGRES_PASSWORD=secret -e POSTGRES_DB=rag \
pgvector/pgvector:pg16
pip install psycopg[binary] pgvector openai
Ativando a extensão e criando a tabela
CREATE EXTENSION IF NOT EXISTS vector;
CREATE TABLE documentos (
id SERIAL PRIMARY KEY,
conteudo TEXT NOT NULL,
fonte TEXT,
embedding vector(1536)
);
O 1536 é a dimensão do modelo de embedding escolhido (ex.: OpenAI text-embedding-3-small).
Gerando embeddings em Python
from openai import OpenAI
client = OpenAI()
def embed(texto: str) -> list[float]:
r = client.embeddings.create(model="text-embedding-3-small", input=texto)
return r.data[0].embedding
Inserindo documentos
import psycopg
conn = psycopg.connect("postgresql://postgres:secret@localhost/rag")
def inserir(conteudo: str, fonte: str):
with conn.cursor() as cur:
cur.execute(
"INSERT INTO documentos (conteudo, fonte, embedding) VALUES (%s, %s, %s)",
(conteudo, fonte, embed(conteudo)),
)
conn.commit()
Busca por similaridade
O operador <=> calcula a distância de cosseno. Ordenando por ele, você tem o resultado mais próximo.
def buscar(pergunta: str, limite: int = 5):
vetor = embed(pergunta)
with conn.cursor() as cur:
cur.execute(
"""SELECT conteudo, fonte, 1 - (embedding <=> %s) AS similaridade
FROM documentos
ORDER BY embedding <=> %s
LIMIT %s""",
(vetor, vetor, limite),
)
return cur.fetchall()
Índice para performance
Sem índice, a busca é linear. Com muitos vetores, crie um índice HNSW.
CREATE INDEX ON documentos
USING hnsw (embedding vector_cosine_ops);
RAG completo: buscar e responder
def responder(pergunta: str) -> str:
trechos = buscar(pergunta)
contexto = "\n\n".join(t[0] for t in trechos)
r = client.chat.completions.create(
model="gpt-4o-mini",
messages=[
{"role": "system", "content": "Responda apenas com base no contexto."},
{"role": "user", "content": f"Contexto:\n{contexto}\n\nPergunta: {pergunta}"},
],
)
return r.choices[0].message.content
Filtrando com metadados
Uma vantagem sobre bancos vetoriais simples: combinar similaridade com SQL.
def buscar_por_fonte(pergunta: str, fonte: str):
vetor = embed(pergunta)
with conn.cursor() as cur:
cur.execute(
"SELECT conteudo FROM documentos WHERE fonte = %s ORDER BY embedding <=> %s LIMIT 5",
(fonte, vetor),
)
return cur.fetchall()
Boas práticas
- Normalize o texto antes de gerar o embedding.
- Guarde o modelo usado junto ao vetor (dimensões mudam).
- Divida documentos longos em chunks antes de indexar.
- Use índice HNSW quando passar de alguns milhares de vetores.
Erros comuns
- Misturar embeddings de modelos diferentes na mesma coluna.
- Não criar índice e culpar o banco pela lentidão.
- Indexar chunks enormes — a busca fica imprecisa.
- Enviar contexto demais ao LLM e estourar o limite de tokens.
Dividindo documentos em chunks
Indexar documentos inteiros prejudica a precisão. Divida em pedaços com sobreposição.
def chunkar(texto: str, tamanho: int = 800, sobreposicao: int = 100):
palavras = texto.split()
chunks, i = [], 0
while i < len(palavras):
pedaco = " ".join(palavras[i:i + tamanho])
chunks.append(pedaco)
i += tamanho - sobreposicao
return chunks
Indexando um acervo
from pathlib import Path
def indexar_pasta(pasta: str):
for arquivo in Path(pasta).glob("*.txt"):
texto = arquivo.read_text(encoding="utf-8")
for chunk in chunkar(texto):
inserir(chunk, fonte=arquivo.name)
Múltiplas dimensões e modelos
Dimensões diferentes exigem colunas/tabelas separadas. Guarde o nome do modelo para não misturar vetores incompatíveis.
ALTER TABLE documentos ADD COLUMN modelo TEXT DEFAULT 'text-embedding-3-small';
HNSW vs IVFFlat
| Índice | Prós | Contras |
|---|---|---|
| HNSW | Rápido, boa precisão | Mais memória, build mais lento |
| IVFFlat | Menos memória | Precisa de tuning e dados para treinar |
Re-ranking dos resultados
Busca vetorial traz candidatos; um re-ranker melhora a ordem final. Mesmo um filtro simples por palavra-chave ajuda.
def rerank(pergunta: str, resultados):
termos = set(pergunta.lower().split())
return sorted(resultados, key=lambda r: -len(termos & set(r[0].lower().split())))
Avaliando a qualidade do RAG
Monte um conjunto de perguntas com respostas esperadas e meça se o trecho correto aparece no top-k. É a métrica que importa de verdade.
CASOS = [{"pergunta": "...", "trecho_esperado": "..."}]
def acerto_topk(casos, k=5):
acertos = 0
for c in casos:
trechos = [t[0] for t in buscar(c["pergunta"], k)]
acertos += any(c["trecho_esperado"] in t for t in trechos)
return acertos / len(casos)
Docker Compose completo
services:
db:
image: pgvector/pgvector:pg16
environment:
POSTGRES_PASSWORD: secret
POSTGRES_DB: rag
ports: ["5432:5432"]
volumes: ["pgdata:/var/lib/postgresql/data"]
volumes:
pgdata:
Migrations com Alembic
Mudanças de schema devem ser versionadas.
alembic init migrations
alembic revision --autogenerate -m "add documentos"
alembic upgrade head
Custo de embeddings
Gerar embeddings custa por token. Para acervos grandes, faça em lote e reuse os vetores.
def embed_lote(textos: list[str]) -> list[list[float]]:
r = client.embeddings.create(model="text-embedding-3-small", input=textos)
return [d.embedding for d in r.data]
Cache de embeddings
import hashlib, json
def embed_cache(texto: str) -> list[float]:
k = "emb:" + hashlib.sha256(texto.encode()).hexdigest()
if v := r.get(k):
return json.loads(v)
v = embed(texto)
r.set(k, json.dumps(v), ex=86400)
return v
Perguntas frequentes
Preciso de banco vetorial dedicado? Só acima de milhões de vetores ou requisitos muito específicos.
Qual modelo de embedding? Comece com um barato e bom; reavalie com métricas.
Como melhorar a precisão? Chunking adequado, re-ranking e bom prompt de sistema.
Metadados e filtros avançados
Guarde metadados em colunas indexadas para filtrar antes da busca vetorial — reduz o universo e acelera.
ALTER TABLE documentos ADD COLUMN categoria TEXT;
CREATE INDEX ON documentos (categoria);
def buscar_categoria(pergunta: str, categoria: str, k: int = 5):
vetor = embed(pergunta)
with conn.cursor() as cur:
cur.execute(
"""SELECT conteudo FROM documentos
WHERE categoria = %s
ORDER BY embedding <=> %s LIMIT %s""",
(categoria, vetor, k),
)
return cur.fetchall()
Híbrido: vetorial + full-text
Combinar busca semântica com busca textual (BM25) melhora resultados em muitos casos.
SELECT conteudo,
(1 - (embedding <=> %s)) AS sim_vetorial,
ts_rank(to_tsvector('portuguese', conteudo), plainto_tsquery('portuguese', %s)) AS sim_textual
FROM documentos
ORDER BY (sim_vetorial * 0.7 + sim_textual * 0.3) DESC
LIMIT 5;
Operadores de distância
pgvector oferece três operadores, e escolher o certo importa:
| Operador | Distância | Uso |
|---|---|---|
<=> |
Cosseno | Texto (mais comum) |
<-> |
L2 (euclidiana) | Vetores normalizados/imagens |
<#> |
Produto interno | Quando já normalizado |
Para texto, use cosseno — é o padrão dos modelos de embedding modernos.
Normalização de vetores
import math
def normalizar(v: list[float]) -> list[float]:
norma = math.sqrt(sum(x * x for x in v))
return [x / norma for x in v] if norma else v
Evitando contexto irrelevante
Buscar muitos chunks aumenta o custo e pode confundir o modelo. Um bom limiar de similaridade corta o ruído.
def buscar_relevantes(pergunta: str, limiar: float = 0.75, k: int = 5):
return [t for t in buscar(pergunta, k) if t[2] >= limiar]
Manutenção do índice
Índices HNSW precisam de rebuild após grandes inserções. Monitore o tamanho e reindexe periodicamente.
REINDEX INDEX documentos_embedding_idx;
Recomendações por similaridade
A mesma mecânica de busca serve para recomendar itens parecidos.
def similares(item_id: int, k: int = 5):
with conn.cursor() as cur:
cur.execute("SELECT embedding FROM documentos WHERE id = %s", (item_id,))
vetor = cur.fetchone()[0]
cur.execute(
"SELECT id, conteudo FROM documentos WHERE id != %s ORDER BY embedding <=> %s LIMIT %s",
(item_id, vetor, k),
)
return cur.fetchall()
Deduplicação de documentos
Evitar indexar o mesmo conteúdo duas vezes economiza custo e melhora resultados.
import hashlib
def ja_indexado(texto: str) -> bool:
h = hashlib.sha256(texto.encode()).hexdigest()
with conn.cursor() as cur:
cur.execute("SELECT 1 FROM hashes WHERE hash = %s", (h,))
return cur.fetchone() is not None
Multilíngue
Modelos de embedding multilíngues permitem buscar em um idioma e recuperar de outro. Guarde o idioma junto ao chunk para filtros opcionais.
Backup e recuperação
Como os vetores vivem no Postgres, o backup normal do banco já cobre o RAG. Garanta que o pg_dump inclua a extensão e os índices.
pg_dump -Fc rag > rag.dump
pg_restore -d rag_novo rag.dump
Escalando com partições
Para acervos muito grandes, particione por fonte ou data — cada partição com seu índice.
CREATE TABLE documentos_2026 PARTITION OF documentos
FOR VALUES FROM ('2026-01-01') TO ('2027-01-01');
Casos de uso além de RAG
- Busca semântica em base própria: FAQ, documentação, tickets.
- Detecção de duplicatas: encontrar textos quase iguais.
- Recomendação: "quem viu isto também viu".
- Classificação por vizinhança: rotular por exemplos próximos.
Quando NÃO usar pgvector
- Volume na casa de bilhões de vetores com latência extrema.
- Necessidade de busca multimodal muito especializada.
- Time sem familiaridade com Postgres e com pressa.
Para a maioria dos projetos, porém, o Postgres é mais que suficiente.
Checklist de implementação
- Ativar a extensão
vectorno banco. - Criar a tabela com a coluna
vector(N)correta. - Gerar embeddings em lote e cachear para não pagar duas vezes.
- Chunkar documentos com sobreposição.
- Criar índice HNSW depois de carregar os dados.
- Medir acerto no top-k com um conjunto de perguntas de teste.
- Combinar filtros relacionais quando fizer sentido.
Resumo
pgvector transforma o PostgreSQL em um banco vetorial competente, com a vantagem de combinar busca semântica e filtros relacionais. Para muitos projetos, é a escolha mais simples e robusta — menos uma peça de infraestrutura para manter.
