1. Por que hybrid search
Busca só por vetor (semântica) captura sinônimos e intenção (“como restaurar backup” ≈ “procedimento de restore”). Falha em:
- Códigos exatos:
ERR_SSL_PROTOCOL_ERROR, CVE, IDs de ticket - Nomes próprios raros ou siglas internas
- Versões e paths de arquivo
BM25 (ou full-text clássico) acerta esses termos e erra em paráfrases. Hybrid = fundir os dois rankings (RRF, weighted sum, ou re-ranker) para o melhor dos dois mundos.
2. Arquitetura mínima
- Indexação — chunks com texto, embedding e metadados (tenant, produto, data, tags)
- Query path — embedding da pergunta + full-text query em paralelo
- Fusão — Reciprocal Rank Fusion (RRF) ou score ponderado
- Filtros — WHERE por tenant/produto antes ou durante a busca
- Opcional — cross-encoder re-rank nos top 20–50
3. Hybrid com PostgreSQL
Você já pode ter tsvector + pgvector na mesma tabela:
-- Colunas extras
ALTER TABLE chunks ADD COLUMN tsv tsvector
GENERATED ALWAYS AS (to_tsvector('portuguese', content)) STORED;
CREATE INDEX chunks_tsv_idx ON chunks USING GIN (tsv);
-- Keyword (BM25-like via ts_rank)
SELECT id, content,
ts_rank(tsv, plainto_tsquery('portuguese', $1)) AS kw_score
FROM chunks
WHERE tsv @@ plainto_tsquery('portuguese', $1)
ORDER BY kw_score DESC
LIMIT 20;
-- Vetor (já visto no artigo de RAG)
SELECT id, content,
1 - (embedding <=> $2) AS vec_score
FROM chunks
ORDER BY embedding <=> $2
LIMIT 20;
RRF (simples e robusto): para cada documento, some 1 / (k + rank) nos dois rankings (k típico = 60). Ordene pelo score RRF final e pegue top N.
// Pseudocódigo
function rrf(rankKeyword, rankVector, k = 60) {
const scores = {};
rankKeyword.forEach((id, i) => {
scores[id] = (scores[id] || 0) + 1 / (k + i + 1);
});
rankVector.forEach((id, i) => {
scores[id] = (scores[id] || 0) + 1 / (k + i + 1);
});
return Object.entries(scores).sort((a, b) => b[1] - a[1]);
}
Alternativas: Elasticsearch / OpenSearch (BM25 nativo + dense vector), ou serviços (Pinecone + sparse, Weaviate hybrid).
4. Filtros e multi-tenancy
- Sempre filtre por
tenant_id(ou workspace) — vazamento entre clientes é falha grave - Filtros comuns: produto, idioma, data mínima, tipo de documento
- No pgvector, coloque metadados em colunas e use
WHEREjunto com a busca; índices parciais ajudam - Em serviços gerenciados, use metadata filters nativos e teste se o filtro é pré ou pós ranking
5. Métricas de retrieval
Sem avaliação, você otimiza no escuro. Monte um conjunto pequeno (50–200 perguntas) com “documentos relevantes” anotados:
- Recall@k — fração dos relevantes que aparecem no top k
- MRR (Mean Reciprocal Rank) — qualidade da primeira posição correta
- nDCG@k — se houver graus de relevância
Compare: só vetor vs só keyword vs hybrid vs hybrid + re-rank. Meça também latência p95 e custo de embedding.
6. Operação e escala
- Reindexação — pipeline quando o modelo de embedding muda (versão no metadado)
- Chunking — títulos e listas técnicas se beneficiam de chunks menores; código, um pouco maiores
- Cache — cache de embedding de queries frequentes (TTL curto)
- Observabilidade — logar query, top ids, scores e se o usuário abriu o resultado
Com centenas de milhares de chunks, pgvector + HNSW ainda aguenta bem em um Postgres bem dimensionado. Além disso, avalie índice dedicado.
7. Checklist de produção
- [ ] Hybrid (keyword + vetor) com fusão documentada (RRF ou pesos)
- [ ] Filtro obrigatório por tenant / workspace
- [ ] Conjunto de avaliação e métricas baseline
- [ ] Versionamento do modelo de embedding
- [ ] Limite de k e timeout na query path
- [ ] Logs sem PII desnecessária; retenção definida
- [ ] Teste de regressão quando mudar chunking ou pesos
Hybrid search é o upgrade natural depois do RAG básico com pgvector. Comece com RRF, filtre por tenant, meça recall@k — e só então invista em re-rankers caros.
8. As consultas explicadas
| Trecho | O que faz |
|---|---|
ADD COLUMN tsv tsvector GENERATED ALWAYS AS (...) STORED | Cria uma coluna calculada automaticamente a partir do texto e gravada no disco. |
to_tsvector('portuguese', content) | Converte o texto em termos normalizados (radicais, sem palavras comuns) para busca textual em português. |
CREATE INDEX ... USING GIN (tsv) | Índice invertido, o tipo indicado para busca textual. |
plainto_tsquery('portuguese', $1) | Transforma a pergunta em uma consulta textual. |
tsv @@ plainto_tsquery(...) | Só considera as linhas que casam com a consulta. |
ts_rank(...) | Pontuação por palavra-chave, base do ranking textual. |
LIMIT 20 | Cada ranking traz 20 candidatos para a fusão. |
O RRF (reciprocal rank fusion) usa a posição de cada documento nos dois rankings, e não os scores. Isso resolve o problema de o ts_rank e o cosseno terem escalas diferentes. Exemplo com k = 60:
| Documento | Posição na busca por palavra | Posição no vetor | Cálculo do RRF | Score |
|---|---|---|---|---|
| A | 1 | 1 | 1/61 + 1/61 | 0,03279 |
| C | 3 | 3 | 1/63 + 1/63 | 0,03175 |
| B | 2 | fora | 1/62 | 0,01613 |
| D | fora | 2 | 1/62 | 0,01613 |
Os documentos que aparecem bem nas duas listas sobem para o topo, e por isso a busca híbrida acerta tanto o termo exato quanto a paráfrase.
| Métrica | O que mede |
|---|---|
recall@k | fração dos documentos relevantes que aparecem entre os k primeiros |
MRR | média de 1 dividido pela posição do primeiro resultado relevante |
nDCG | qualidade da ordem, dando mais peso ao topo do ranking |
| latência p95 | tempo abaixo do qual ficam 95% das consultas |
9. Erros comuns e como resolver
| Sintoma | Causa provável | Solução |
|---|---|---|
A busca por um código exato, como ERR-5023, não acha nada | A busca por vetor não preserva termos exatos e o radicalizador do português pode alterar o código. | Mantenha a busca por palavra-chave e considere a configuração simple para identificadores. |
| Um tenant vê chunks de outro | A consulta não filtra por tenant. | Aplique o WHERE tenant_id = ... nas duas buscas antes de fundir os resultados. |
| Os scores das duas buscas não se combinam bem | As escalas de ts_rank e de similaridade são diferentes. | Funda por posição com RRF, e não por soma de scores. |
| O resultado piorou depois de ligar o híbrido | Sem métrica, não dá para saber. | Compare vetor, palavra-chave e híbrido em um conjunto de perguntas anotadas. |