O pgvector no Node.js adiciona busca vetorial ao PostgreSQL. A extensão permite armazenar embeddings, calcular distância e encontrar itens semanticamente semelhantes usando o mesmo banco que já guarda usuários, permissões e dados relacionais.
Essa abordagem é útil para busca semântica, recomendações, deduplicação, classificação e aplicações RAG. Porém, um vetor não substitui filtros de negócio. A consulta precisa combinar similaridade com tenant, status, idioma, permissões e datas.
Neste guia, você aprenderá a instalar pgvector, registrar tipos no pacote pg, inserir embeddings, consultar vizinhos, escolher distância, criar índices HNSW e IVFFlat, aplicar filtros, fazer busca híbrida e monitorar qualidade e desempenho.
O que é pgvector?
pgvector é uma extensão open source para PostgreSQL. O cliente oficial para JavaScript está no repositório pgvector-node. Ele oferece integração com node-postgres, Prisma, Drizzle, Kysely, Sequelize, TypeORM e outras bibliotecas.
A extensão principal está em pgvector no GitHub.
Habilitando a extensão
CREATE EXTENSION IF NOT EXISTS vector;Em serviços gerenciados, confirme disponibilidade e versão. A migration deve falhar claramente quando a extensão não está instalada.
Instalação no Node.js
npm install pg pgvectorimport pg from 'pg';
import pgvector from 'pgvector/pg';
const pool = new pg.Pool({
connectionString: process.env.DATABASE_URL
});
pool.on('connect', async client => {
await pgvector.registerTypes(client);
});Registrar os tipos permite converter valores vector corretamente.
Criando a tabela
CREATE TABLE documents (
id uuid PRIMARY KEY,
tenant_id uuid NOT NULL,
title text NOT NULL,
content text NOT NULL,
embedding vector(1536) NOT NULL,
metadata jsonb NOT NULL DEFAULT '{}'::jsonb,
created_at timestamptz NOT NULL DEFAULT now()
);A dimensão precisa corresponder ao modelo de embedding usado. Não altere o modelo sem uma estratégia de migração.
Inserindo um vetor
const embedding = await createEmbedding(document.content);
await pool.query(
`INSERT INTO documents
(id, tenant_id, title, content, embedding, metadata)
VALUES ($1, $2, $3, $4, $5, $6::jsonb)`,
[
crypto.randomUUID(),
tenantId,
document.title,
document.content,
pgvector.toSql(embedding),
JSON.stringify(document.metadata)
]
);Valide a quantidade de dimensões, valores finitos e tamanho do texto antes de chamar o modelo.
Busca por distância euclidiana
const queryEmbedding = await createEmbedding(query);
const result = await pool.query(
`SELECT
id,
title,
content,
embedding <-> $1 AS distance
FROM documents
WHERE tenant_id = $2
ORDER BY embedding <-> $1
LIMIT 10`,
[pgvector.toSql(queryEmbedding), tenantId]
);O operador <-> calcula distância L2. Menor valor significa maior proximidade.
Distância cosseno
SELECT
id,
title,
embedding <=> $1 AS cosine_distance
FROM documents
WHERE tenant_id = $2
ORDER BY embedding <=> $1
LIMIT 10;Similaridade cosseno pode ser calculada como 1 - distance. Confirme a métrica recomendada pelo modelo.
Inner product
ORDER BY embedding <#> $1O operador retorna o produto interno negativo para permitir ordenação ascendente. Use somente quando os vetores e o modelo foram preparados para essa métrica.
Busca exata
Sem índice aproximado, PostgreSQL compara o vetor da consulta com todas as linhas filtradas. Isso oferece recall exato, mas pode ficar caro em milhões de documentos.
Comece com busca exata para validar qualidade. Crie índice aproximado somente quando volume e latência justificarem.
Índice HNSW
CREATE INDEX documents_embedding_hnsw
ON documents
USING hnsw (embedding vector_cosine_ops);HNSW costuma oferecer boa relação entre recall e latência. Pode ser criado antes de carregar dados, mas usa mais memória e torna inserts mais caros.
Ajustando HNSW
CREATE INDEX documents_embedding_hnsw
ON documents
USING hnsw (embedding vector_cosine_ops)
WITH (m = 16, ef_construction = 64);m controla conexões do grafo. ef_construction afeta qualidade e tempo de construção. Ajuste com benchmarks.
Na consulta:
SET LOCAL hnsw.ef_search = 100;Valores maiores tendem a aumentar recall e custo.
Índice IVFFlat
CREATE INDEX documents_embedding_ivfflat
ON documents
USING ivfflat (embedding vector_cosine_ops)
WITH (lists = 100);IVFFlat precisa de dados representativos antes da criação. Depois, execute ANALYZE.
SET LOCAL ivfflat.probes = 10;Mais probes aumentam recall e latência.
HNSW ou IVFFlat?
- HNSW: bom recall, consultas rápidas, mais memória.
- IVFFlat: índice menor e construção simples, exige tuning e dados prévios.
- Sem índice: recall exato, adequado para coleções menores.
Filtros por tenant
Busca vetorial nunca deve cruzar tenants:
WHERE tenant_id = $2
AND status = 'published'Use Row-Level Security como proteção adicional. Veja Row-Level Security no Node.js.
Índices para filtros
CREATE INDEX documents_tenant_status_idx
ON documents(tenant_id, status);O planner combina o filtro com a busca. Em consultas aproximadas, filtros muito seletivos podem reduzir resultados retornados. Teste iterative scans e parâmetros suportados pela versão.
Índice parcial
CREATE INDEX documents_published_embedding_hnsw
ON documents
USING hnsw (embedding vector_cosine_ops)
WHERE status = 'published';É útil quando a consulta sempre usa o mesmo filtro.
Particionamento
Grandes sistemas podem particionar por tenant, região ou período. Isso reduz o conjunto de busca, mas aumenta complexidade operacional. Não crie milhares de partições pequenas.
Busca híbrida
Embeddings encontram semântica; full-text search encontra termos exatos. Combine ambos:
WITH candidates AS (
SELECT
id,
title,
content,
1 - (embedding <=> $1) AS vector_score,
ts_rank_cd(search_vector, websearch_to_tsquery('portuguese', $2)) AS text_score
FROM documents
WHERE tenant_id = $3
)
SELECT *,
vector_score * 0.7 + text_score * 0.3 AS final_score
FROM candidates
ORDER BY final_score DESC
LIMIT 10;Normalize scores antes de somar. Pesos precisam ser avaliados com consultas reais.
Reciprocal Rank Fusion
Uma alternativa robusta é ranquear separadamente e combinar posições:
score = 1 / (60 + vector_rank) + 1 / (60 + text_rank)RRF evita depender de escalas de score incompatíveis.
Chunking
Documentos longos devem ser divididos:
- preserve títulos e seções;
- evite cortar frases;
- mantenha pequeno overlap;
- grave document_id e posição;
- não use chunks gigantes.
O tamanho ideal depende do modelo e do tipo de pergunta.
Tabela de chunks
CREATE TABLE document_chunks (
id uuid PRIMARY KEY,
document_id uuid NOT NULL REFERENCES documents(id) ON DELETE CASCADE,
tenant_id uuid NOT NULL,
position integer NOT NULL,
content text NOT NULL,
embedding vector(1536) NOT NULL,
UNIQUE(document_id, position)
);Atualização de embeddings
Quando o conteúdo muda:
- marque o documento como pendente;
- gere novos chunks;
- gere embeddings;
- grave em transação;
- substitua a versão ativa;
- remova a anterior.
Use uma fila para controlar concorrência e custo.
Versão do modelo
ALTER TABLE document_chunks
ADD COLUMN embedding_model text NOT NULL,
ADD COLUMN embedding_version integer NOT NULL DEFAULT 1;Vetores de modelos diferentes não devem ser comparados sem validação. Uma troca pode exigir backfill completo.
Deduplicação
Antes de gerar embedding, calcule hash do conteúdo:
const contentHash = createHash('sha256')
.update(normalizedContent)
.digest('hex');Reutilize o embedding quando o texto e o modelo são iguais.
Batch insert
Insira em lotes e limite tamanho. Para grandes cargas, use COPY e o suporte documentado pelo cliente. Monitore WAL e tempo de índice.
Prisma
Prisma pode declarar o campo como unsupported e usar SQL parametrizado:
const embedding = pgvector.toSql(queryEmbedding);
const rows = await prisma.$queryRaw`
SELECT id, title
FROM documents
ORDER BY embedding <=> ${embedding}::vector
LIMIT 10
`;Consulte Prisma no Node.js.
Drizzle
Drizzle possui suporte ao tipo vector e funções de distância. Ainda assim, examine o SQL gerado e crie migrations explícitas para índices.
Veja Drizzle ORM com PostgreSQL.
Segurança
- não envie dados sensíveis ao modelo sem base legal;
- aplique filtros de autorização antes da busca;
- não confie no texto recuperado como instrução;
- proteja contra prompt injection em RAG;
- registre versão e origem;
- limite quantidade de resultados.
Qualidade
Crie um conjunto de avaliação com perguntas, documentos relevantes e irrelevantes. Meça:
- recall@k;
- precision@k;
- MRR;
- nDCG;
- latência p95;
- custo por consulta.
Não escolha modelo e índice apenas por demonstrações.
EXPLAIN
EXPLAIN (ANALYZE, BUFFERS)
SELECT id
FROM documents
WHERE tenant_id = $2
ORDER BY embedding <=> $1
LIMIT 10;Confirme se o índice vetorial e os filtros estão sendo usados.
Observabilidade
Monitore:
- latência de geração de embedding;
- latência da busca;
- recall em avaliações;
- tamanho dos índices;
- taxa de atualização;
- erros de dimensão;
- linhas sem embedding;
- custo do provedor.
Testes
Use PostgreSQL com pgvector real em container. Teste inserção, distância, filtros por tenant, índice, mudança de modelo e falhas de embedding.
Consulte Testcontainers no Node.js.
Erros comuns
- Dimensão errada: insert falha.
- Métrica incompatível: ranking perde qualidade.
- Sem filtro de tenant: dados vazam.
- Índice sem tuning: recall fica baixo.
- Trocar modelo silenciosamente: vetores ficam incompatíveis.
- Somar scores crus: busca híbrida fica enviesada.
- Chunks ruins: respostas perdem contexto.
- RAG sem proteção: conteúdo malicioso influencia o modelo.
Conclusão
O pgvector no Node.js adiciona busca semântica ao PostgreSQL e permite combinar vetores com SQL, transações e autorização. A integração oficial funciona com pg, ORMs e query builders.
Comece com busca exata, escolha a métrica do modelo e crie HNSW ou IVFFlat somente após medir. Combine filtros, full-text search, versionamento de embeddings e avaliações. A qualidade depende tanto do conteúdo e dos filtros quanto do índice vetorial.



