Esquema de Armazenamento RAG
Um armazenamento RAG no Postgres divide documentos em chunks, armazena embeddings por chunk e mantém metadados para filtragem e citações. Projete para isolamento de tenant, re-ingestão idempotente e FTS híbrido desde o início.
Receita
CREATE EXTENSION IF NOT EXISTS vector WITH SCHEMA extensions;
CREATE TABLE rag_documents (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id uuid NOT NULL,
source_uri text NOT NULL,
title text,
content_hash text NOT NULL, -- sha256 do source normalizado
created_at timestamptz DEFAULT now(),
UNIQUE (tenant_id, source_uri)
);
CREATE TABLE rag_chunks (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id uuid NOT NULL,
document_id uuid NOT NULL REFERENCES rag_documents(id) ON DELETE CASCADE,
chunk_index int NOT NULL,
content text NOT NULL,
token_count int,
metadata jsonb NOT NULL DEFAULT '{}',
embedding extensions.vector(1536),
content_tsv tsvector GENERATED ALWAYS AS (
to_tsvector('english', content)
) STORED,
UNIQUE (document_id, chunk_index)
);Quando usar isso: Construindo recuperação para aplicativos LLM onde citações de origem, corpora por tenant e junções SQL com permissões de usuário são importantes.
Exemplo de Trabalho
-- Índices com escopo de tenant
CREATE INDEX rag_chunks_tenant_hnsw ON rag_chunks
USING hnsw (embedding extensions.vector_cosine_ops)
WHERE embedding IS NOT NULL;
CREATE INDEX rag_chunks_tenant_fts ON rag_chunks USING gin (content_tsv);
CREATE INDEX rag_chunks_metadata ON rag_chunks USING gin (metadata jsonb_path_ops);
CREATE INDEX rag_chunks_tenant ON rag_chunks (tenant_id);
-- Ingestão idempotente: pular se a origem não for alterada
INSERT INTO rag_documents (tenant_id, source_uri, title, content_hash)
VALUES ($1, $2, $3, $4)
ON CONFLICT (tenant_id, source_uri) DO UPDATE
SET content_hash = EXCLUDED.content_hash,
title = EXCLUDED.title
WHERE rag_documents.content_hash IS DISTINCT FROM EXCLUDED.content_hash
RETURNING id, (xmax = 0) AS inserted;
-- Recuperação com filtro de tenant + colunas prontas para híbrido
SELECT c.id,
c.content,
d.title AS source_title,
c.metadata->>'page' AS page,
c.embedding <=> $2 AS distance
FROM rag_chunks c
JOIN rag_documents d ON d.id = c.document_id
WHERE c.tenant_id = $1
AND c.embedding IS NOT NULL
ORDER BY c.embedding <=> $2
LIMIT 8;O que isso demonstra:
- A separação de documento vs. chunk permite o re-chunking sem perder a identidade da origem
content_hashimpulsiona os saltos do pipeline idempotente- O índice HNSW parcial exclui linhas ainda não incorporadas
metadataJSONB armazena números de página, seção, dicas de ACLtsvectorgerado suporta pesquisa híbrida sem lógica de análise duplicada
Mergulho Profundo
Metadados de Chunking
{
"page": 12,
"section": "Replication",
"heading": "Streaming standby",
"embed_model": "text-embedding-3-small",
"embed_version": "2026-03-01"
}Armazene o nome e a versão do modelo nos metadados ou em colunas dedicadas. Campanhas de re-embedding filtram WHERE metadata->>'embed_model' <> 'new-model'.
Isolamento de Tenant
| Abordagem | Mecanismo | Notas |
|---|---|---|
| Coluna + RLS | tenant_id + política | Padrão para SaaS |
| Schema por tenant | tenant_abc.rag_chunks | Operações pesadas em escala |
| Banco de dados por tenant | DB separado | Nível empresarial |
ALTER TABLE rag_chunks ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation ON rag_chunks
USING (tenant_id = current_setting('app.tenant_id')::uuid);Defina app.tenant_id por conexão a partir da variável de sessão do pooler.
Carga Útil de Citação
Retornar para LLM:
chunk.content(truncado)document.source_urimetadata->>'page'chunk_indexpara ordenação dentro do documento
Evite armazenar blobs PDF completos na tabela de chunks; mantenha o URI do armazenamento de objetos em rag_documents.
Estados de Ingestão
ALTER TABLE rag_chunks
ADD COLUMN embed_status text NOT NULL DEFAULT 'pending'
CHECK (embed_status IN ('pending', 'ready', 'failed'));Trabalhadores reivindicam linhas pending com FOR UPDATE SKIP LOCKED.
Armadilhas
- Chunks sem pai de documento - Limpeza de órfãos falha. Correção: FK com
ON DELETE CASCADEde chunks para documentos. - Embedding antes do conteúdo final - Corrida na ingestão de streaming. Correção: Coluna de status; índice
WHERE embed_status = 'ready'. - Chunks superdimensionados - Manual inteiro em uma linha estoura o contexto e a qualidade do embedding. Correção: Alvo de 400-800 tokens com sobreposição (armazene a sobreposição nos metadados).
- Inchaço JSONB no caminho quente - Metadados enormes por chunk. Correção: Promova chaves de filtro para colunas reais (
language,product_id). - Predicado de índice sem tenant - HNSW escaneia todos os tenants. Correção: Índice parcial por tenant grande ou chave de partição composta.
- Único em conteúdo apenas - Chunks duplicados entre documentos. Correção:
UNIQUE (document_id, chunk_index)mais hash de conteúdo no nível do documento.
Alternativas
| Alternativa | Use Quando | Não Use Quando |
|---|---|---|
| Tabela única (sem documento) | FAQ estático pequeno | Citações de múltiplas fontes são necessárias |
| pgvector + armazenamento de objetos | Originais PDF grandes | Precisa de chunk transacional + blob juntos |
| DB de vetor externo | Equipe de pesquisa dedicada e escala | Junções de permissão ao vivo no Postgres |
| Visão materializada de chunks | Análises com muitas leituras no corpus | Pipeline de ingestão com muitas gravações |
FAQs
Tamanho do chunk?
Dependente do modelo; 512 tokens comuns para modelos de text-embedding. Meça o MRR de recuperação.Sobreposição?
Sobreposição de 50-100 tokens reduz perdas de limite; armazene `start_offset` nos metadados.Excluir documentos desatualizados?
CASCADE exclui chunks quando o documento é removido ou o hash inalterado pula a re-incorporação.Versões de embeddings?
Nova coluna `embedding_v2` durante a migração; escrita dupla e depois corte.ACL em metadados vs RLS?
RLS em `tenant_id` é obrigatório; ACL de objeto em metadados para filtro opcional de granularidade fina.Full-text em chunks?
Sim; o artigo de pesquisa híbrida mescla com a pontuação do vetor.Incorporar em lote?
COPY chunks sem embedding; worker UPDATE define vetor e status como pronto.Algoritmo de checksum?
SHA-256 de texto UTF-8 normalizado; documente quando a normalização muda.IDs UUID vs bigint?
UUID é bom para ingestão distribuída; bigint tem índices menores se for gravador único.Layout de referência?
Veja o estudo de caso de referência pgvector RAG store para exemplo HNSW ajustado.Relacionado
- Noções Básicas de pgvector - tipo vetor
- Índices IVFFlat vs HNSW - escolha de índice
- Pesquisa Híbrida - FTS + vetor
- Segurança em Nível de Linha - políticas de tenant
- Referência: Armazenamento RAG pgvector - exemplo de trabalho
Versões da Stack: Esta página foi escrita para PostgreSQL 18.4 (estável 18, manutenção 17), pgvector 0.8+, PostGIS 3.5+, pgbouncer 1.x e Patroni 3.x.