RAG avançado: busca híbrida e reranking na prática

Seu RAG responde bem na demo, mas erra perguntas simples em produção. Na maioria dos casos, o problema não está no LLM — está na busca. Embedding sozinho deixa documentos críticos fora do contexto. Busca híbrida com reranking resolve isso, e este artigo mostra exatamente como.

Por que embedding sozinho falha — e quando

Embedding (ou busca densa) é poderoso para capturar similaridade semântica: "automóvel" e "carro" ficam próximos no espaço vetorial mesmo sem compartilhar nenhuma letra. Isso parece ótimo até você se deparar com casos reais:

Em benchmarks públicos (BEIR, MS MARCO), a busca puramente densa fica 5–15% abaixo do BM25 em domínios técnicos e jurídicos. Em contextos empresariais com documentação proprietária, essa diferença pode chegar a 30%.

Os três paradigmas de busca: comparação direta

Antes de combinar estratégias, é preciso entender o que cada uma oferece — e o que cada uma sacrifica.

Comparação entre busca densa, esparsa e híbrida em 6 dimensões
Dimensão Busca Densa (Vetorial) Busca Esparsa (BM25/TF-IDF) Busca Híbrida (RRF)
Mecanismo Similaridade coseno entre embeddings de query e chunk Frequência e raridade de termos (TF × IDF) Fusão das duas listas de ranking via Reciprocal Rank Fusion
Força principal Sinônimos, paráfrases, variações semânticas Termos exatos, códigos, siglas, números Cobre ambas as fraquezas individualmente
Fraqueza principal Termos raros, literais, numéricos Perguntas com paráfrases, variações de vocabulário Mais complexidade operacional; requer ambos os índices
Infraestrutura Vector store (Qdrant, Weaviate, Pinecone, PGVector) Elasticsearch, OpenSearch, Solr, BM25s (Python) Vector store + motor de busca léxica (ou Weaviate/Qdrant nativos)
Latência típica 5–50ms (ANN search) 5–30ms (índice invertido) 10–80ms (paralelo) + ~5ms para fusão
Quando usar FAQs, documentação geral, atendimento ao cliente Busca em contratos, logs, SKUs, IDs Produção com documentos mistos — é o padrão recomendado

Reciprocal Rank Fusion: como a mágica acontece

O RRF é um algoritmo simples e robusto para combinar múltiplas listas de ranking. A fórmula é:

RRF_score(doc d) = Σ 1 / (k + rank_i(d))

onde:
  k    = constante de suavização (tipicamente 60)
  rank_i(d) = posição do documento d na lista i (1-indexed)
  Σ    = soma sobre todas as listas de ranking (densa + esparsa)

Intuição: um documento que aparece em 1º lugar em ambas as listas recebe score 1/(60+1) + 1/(60+1) ≈ 0,033. Um documento que aparece em 1º na busca densa mas em 100º na esparsa recebe 1/61 + 1/160 ≈ 0,022. Documentos relevantes para os dois métodos flutuam naturalmente para o topo.

O valor k=60 foi determinado empiricamente e é relativamente estável — você não precisa ajustá-lo na maioria dos casos. Algumas implementações usam pesos diferentes para cada lista (weighted RRF), o que pode ajudar quando um método é claramente mais confiável para o seu domínio.

Reranking com cross-encoder: a segunda camada de qualidade

A busca (densa, esparsa ou híbrida) usa bi-encoders: a query e cada chunk são codificados separadamente, e a similaridade é calculada depois. Isso é rápido, mas superficial.

O cross-encoder recebe query + chunk juntos como uma única entrada e produz um score de relevância muito mais preciso — porque pode analisar a interação entre os dois textos ao nível de atenção. A desvantagem: é lento demais para buscar em corpus completo. A solução: usá-lo apenas para reordenar os top-K (tipicamente 20–50) chunks já recuperados.

Pipeline de reranking: pseudocódigo

# Etapa 1: Busca híbrida (BM25 + vetorial)
resultados_densos  = vector_store.buscar(query, top_k=30)
resultados_esparsos = bm25_index.buscar(query, top_k=30)

# Etapa 2: Fusão com RRF
def rrf_score(rank, k=60):
    return 1.0 / (k + rank)

scores = {}
for i, doc in enumerate(resultados_densos):
    scores[doc.id] = scores.get(doc.id, 0) + rrf_score(i + 1)

for i, doc in enumerate(resultados_esparsos):
    scores[doc.id] = scores.get(doc.id, 0) + rrf_score(i + 1)

candidatos = sorted(scores.items(), key=lambda x: x[1], reverse=True)[:20]

# Etapa 3: Reranking com cross-encoder
pares = [(query, corpus[doc_id].texto) for doc_id, _ in candidatos]
scores_rerank = cross_encoder.predict(pares)

ranking_final = sorted(
    zip(candidatos, scores_rerank),
    key=lambda x: x[1],
    reverse=True
)[:5]  # top-5 para contexto do LLM

# Etapa 4: Geração com LLM
contexto = "\n\n".join([corpus[doc_id].texto for (doc_id, _), _ in ranking_final])
resposta = llm.gerar(prompt=f"Contexto:\n{contexto}\n\nPergunta: {query}")

Modelos de reranking disponíveis: open-source vs. API

Você não precisa treinar um cross-encoder do zero. Existem opções prontas para uso:

Para uma PME com volume de 10.000 queries/mês, a Cohere Rerank custa US$ 10/mês — negligível comparado ao custo do LLM. Para volumes acima de 100K queries, a GPU própria com bge-reranker se paga em 3–4 meses.

Implementação prática: Weaviate e Qdrant já suportam busca híbrida

A boa notícia: você não precisa manter dois sistemas separados (vector store + Elasticsearch). As principais soluções já integram busca híbrida nativamente:

Quando a busca híbrida não resolve o problema

Busca híbrida não é bala de prata. Ela não resolve:

Ganhos realistas em produção: o que esperar

Com base em projetos que acompanhei e benchmarks públicos, estes são os ganhos típicos ao migrar de busca densa pura para híbrida com reranking:

Um caso real: implementei busca híbrida com Cohere Rerank em um sistema de consulta de contratos para uma distribuidora com ~8.000 documentos PDF. A taxa de respostas corretas subiu de 61% para 84% no golden dataset, e o tempo médio de resposta foi de 1,8s para 2,3s — trade-off muito positivo para o contexto.

Próximos passos para o seu time

Se você já tem um RAG em produção com busca puramente vetorial, o caminho mais pragmático é:

  1. Monte um golden dataset de 50–100 pares pergunta/resposta esperada. Sem isso, você não vai saber se melhorou ou piorou. Veja como montar e usar um golden dataset.
  2. Adicione BM25 em paralelo usando a mesma base de chunks. BM25 é stateless e fácil de implementar — a biblioteca rank_bm25 em Python faz isso em 20 linhas.
  3. Implemente RRF para fusão. O algoritmo cabe em 10 linhas de código.
  4. Adicione reranking com Cohere ou bge-reranker. Meça antes e depois no golden dataset.
  5. Itere no chunking se os ganhos forem menores que 10%. O gargalo provavelmente está antes da busca.