Como usar o pgvector no PostgreSQL para busca vetorial com embeddings

1. Fundamentos da busca vetorial e embeddings

Embeddings vetoriais são representações numéricas densas de dados não estruturados — textos, imagens, áudios — em um espaço multidimensional contínuo. Diferentemente da busca textual tradicional baseada em correspondência exata de palavras (LIKE, full-text search), a busca vetorial captura relações semânticas: documentos com significados similares ficam próximos nesse espaço, mesmo usando vocabulários distintos.

A extensão pgvector transforma o PostgreSQL em um banco de dados vetorial, permitindo armazenar embeddings lado a lado com dados relacionais e executar consultas híbridas. Casos de uso incluem:
- Sistemas de recomendação que combinam perfil do usuário com similaridade de produtos
- Busca semântica em bases de conhecimento (FAQ, documentação técnica)
- Detecção de duplicatas em grandes volumes de texto
- Matching de currículos e vagas por competências

2. Instalação e configuração do pgvector

Pré-requisitos: PostgreSQL 12 ou superior. A instalação varia por sistema operacional:

# Debian/Ubuntu
sudo apt install postgresql-16-pgvector

# macOS (Homebrew)
brew install pgvector

# Compilação manual
git clone https://github.com/pgvector/pgvector.git
cd pgvector
make
sudo make install

Ative a extensão no banco de dados:

CREATE EXTENSION vector;

Verifique a instalação:

SELECT extname, extversion FROM pg_extension WHERE extname = 'vector';

Não há parâmetros de configuração obrigatórios, mas para datasets grandes, ajuste work_mem e shared_buffers no postgresql.conf.

3. Criação de tabelas e tipos de dados vetoriais

O tipo vector(n) armazena vetores de n dimensões. Exemplo prático para armazenar embeddings de documentos:

CREATE TABLE documentos (
    id SERIAL PRIMARY KEY,
    titulo TEXT NOT NULL,
    conteudo TEXT,
    autor VARCHAR(100),
    data_publicacao DATE,
    embedding vector(768)  -- 768 dimensões (modelo all-MiniLM-L6-v2)
);

Inserção de dados:

INSERT INTO documentos (titulo, conteudo, embedding)
VALUES (
    'Introdução ao Machine Learning',
    'Machine learning é um subcampo da inteligência artificial...',
    '[0.0123, -0.0456, 0.0789, ..., 0.0012]'::vector
);

4. Geração de embeddings com modelos externos

Com OpenAI API (Python):

import openai
import psycopg2

openai.api_key = "sua-chave"

def gerar_embedding(texto):
    resp = openai.Embedding.create(
        model="text-embedding-ada-002",
        input=texto
    )
    return resp['data'][0]['embedding']

texto = "Redes neurais convolucionais para visão computacional"
vetor = gerar_embedding(texto)

conn = psycopg2.connect("dbname=meubd")
cur = conn.cursor()
cur.execute(
    "INSERT INTO documentos (titulo, conteudo, embedding) VALUES (%s, %s, %s)",
    ("Visão Computacional", texto, vetor)
)
conn.commit()

Com sentence-transformers (local):

from sentence_transformers import SentenceTransformer
modelo = SentenceTransformer('all-MiniLM-L6-v2')

textos = ["Primeiro documento", "Segundo documento"]
embeddings = modelo.encode(textos).tolist()

5. Operações de busca por similaridade

O pgvector oferece três operadores de distância:

Operador Métrica Uso típico
<-> Distância L2 (Euclidiana) Embeddings densos de alta dimensão
<#> Produto interno negativo Embeddings normalizados (similaridade cosseno)
<=> Distância cosseno Textos, quando embeddings não são normalizados

Busca por similaridade semântica:

-- Encontrar os 5 documentos mais similares a uma consulta
SELECT id, titulo, 
       1 - (embedding <=> '[0.0234, -0.0567, ...]'::vector) AS similaridade
FROM documentos
ORDER BY embedding <=> '[0.0234, -0.0567, ...]'::vector
LIMIT 5;

Combinando filtros tradicionais:

SELECT id, titulo, autor, 
       embedding <=> '[consulta_embedding]'::vector AS distancia
FROM documentos
WHERE autor = 'Maria Silva'
  AND data_publicacao >= '2024-01-01'
ORDER BY distancia
LIMIT 10;

6. Índices para busca vetorial eficiente

IVFFlat (Inverted File with Flat Compression): ideal para datasets de médio porte (até 1M vetores). Cria clusters de vetores similares.

CREATE INDEX idx_documentos_ivfflat 
ON documentos 
USING ivfflat (embedding vector_cosine_ops)
WITH (lists = 100);

Parâmetros:
- lists: número de clusters (100 para até 1M registros, 1000 para maiores)
- Ajuste probes na consulta: SET ivfflat.probes = 10;

HNSW (Hierarchical Navigable Small World): melhor desempenho para grandes datasets e alta precisão.

CREATE INDEX idx_documentos_hnsw 
ON documentos 
USING hnsw (embedding vector_cosine_ops)
WITH (m = 16, ef_construction = 200);

Parâmetros:
- m: número de conexões por nó (16-64, maior = mais preciso mas mais lento)
- ef_construction: tamanho da lista dinâmica durante construção (200-800)

Comparação de desempenho (dataset 500k vetores, 768 dimensões):

Índice Tempo de busca (ms) Precisão@10 Tamanho do índice
Sem índice 4500 100% -
IVFFlat (lists=100) 45 92% 180 MB
HNSW (m=32) 8 98% 420 MB

7. Otimização e boas práticas em produção

Particionamento por data ou categoria:

CREATE TABLE documentos_2024 PARTITION OF documentos
FOR VALUES FROM ('2024-01-01') TO ('2025-01-01');

Monitoramento com EXPLAIN ANALYZE:

EXPLAIN (ANALYZE, BUFFERS) 
SELECT titulo FROM documentos
ORDER BY embedding <=> '[vetor]'::vector
LIMIT 10;

Manutenção periódica:
- Reindexe após grandes inserções: REINDEX INDEX idx_documentos_hnsw;
- Atualize embeddings quando o modelo de embedding mudar (versione seus modelos)
- Use VACUUM ANALYZE regularmente para estatísticas atualizadas

8. Integração com aplicações e ferramentas

Exemplo com SQLAlchemy:

from sqlalchemy import Column, Integer, Text, Date
from sqlalchemy.dialects.postgresql import VECTOR
from sqlalchemy.ext.declarative import declarative_base

Base = declarative_base()

class Documento(Base):
    __tablename__ = 'documentos'
    id = Column(Integer, primary_key=True)
    titulo = Column(Text)
    conteudo = Column(Text)
    embedding = Column(VECTOR(768))

API REST para busca semântica (FastAPI):

from fastapi import FastAPI
import psycopg2
from sentence_transformers import SentenceTransformer

app = FastAPI()
modelo = SentenceTransformer('all-MiniLM-L6-v2')

@app.get("/buscar")
def buscar_semantico(q: str, limite: int = 5):
    vetor = modelo.encode(q).tolist()
    conn = psycopg2.connect("dbname=meubd")
    cur = conn.cursor()
    cur.execute("""
        SELECT id, titulo, 
               1 - (embedding <=> %s::vector) AS score
        FROM documentos
        ORDER BY embedding <=> %s::vector
        LIMIT %s
    """, (vetor, vetor, limite))
    resultados = cur.fetchall()
    return [{"id": r[0], "titulo": r[1], "score": r[2]} for r in resultados]

Limitações conhecidas:
- O pgvector não suporta sharding nativo — para escalar horizontalmente, considere alternativas como Milvus, Qdrant ou pgvector em clusters com Citus
- Índices HNSW consomem mais memória RAM que IVFFlat
- A geração de embeddings é o gargalo mais comum — pré-calcule e armazene em cache

Referências