~ / blog / ia / hybrid-search
Atualizado em 16 set 2026 ia 12 min de leitura

IA: Busca semântica e hybrid search em produção

Só embeddings não bastam quando o usuário busca um código de erro exato. Hybrid search combina keyword (BM25) e vetor, com filtros de metadados e medição de qualidade — o caminho do protótipo ao serviço confiável.

hybrid searchBM25pgvectorretrievalRAG

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.

tip · Em RAG, hybrid search costuma subir recall@k de forma perceptível em bases técnicas (runbooks, docs de API, tickets).

2. Arquitetura mínima

  1. Indexação — chunks com texto, embedding e metadados (tenant, produto, data, tags)
  2. Query path — embedding da pergunta + full-text query em paralelo
  3. Fusão — Reciprocal Rank Fusion (RRF) ou score ponderado
  4. Filtros — WHERE por tenant/produto antes ou durante a busca
  5. 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 WHERE junto com a busca; índices parciais ajudam
  • Em serviços gerenciados, use metadata filters nativos e teste se o filtro é pré ou pós ranking
tip · Teste com dois tenants no mesmo índice. A query do tenant A nunca deve retornar chunk do tenant B, mesmo com k alto.

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

TrechoO que faz
ADD COLUMN tsv tsvector GENERATED ALWAYS AS (...) STOREDCria 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 20Cada 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:

DocumentoPosição na busca por palavraPosição no vetorCálculo do RRFScore
A111/61 + 1/610,03279
C331/63 + 1/630,03175
B2fora1/620,01613
Dfora21/620,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étricaO que mede
recall@kfração dos documentos relevantes que aparecem entre os k primeiros
MRRmédia de 1 dividido pela posição do primeiro resultado relevante
nDCGqualidade da ordem, dando mais peso ao topo do ranking
latência p95tempo abaixo do qual ficam 95% das consultas

9. Erros comuns e como resolver

SintomaCausa provávelSolução
A busca por um código exato, como ERR-5023, não acha nadaA 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 outroA 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 bemAs 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íbridoSem métrica, não dá para saber.Compare vetor, palavra-chave e híbrido em um conjunto de perguntas anotadas.

Quer implementar isso na sua empresa?

Ajudamos times a colocar RAG, agentes e modelos locais em produção com segurança e observabilidade.

Falar com a IRN Devs Como contratar →

Quer aplicar isso ao seu contexto?

Se esse problema existe na sua operação, podemos analisar o cenário em um diagnóstico gratuito de 30 minutos.

solicitar diagnóstico gratuito →

Leia também