Software · Arquitetura & Engenharia de Dados
Busca Híbrida: BM25, TF-IDF e Busca Semântica Vetorial
Por que a busca vetorial isolada falha em termos exatos e como combinar recuperação léxica (BM25) com vetores densos usando Reciprocal Rank Fusion e re-ranking com cross-encoders.
Neste artigo
A empolgação inicial com a inteligência artificial generativa levou muitos times a substituir mecanismos de busca tradicionais por bancos estritamente vetoriais. Na prática de produção, essa substituição logo revelou uma deficiência marcante: embora modelos de embeddings capturem nuances semânticas e sinônimos com facilidade, eles têm extrema dificuldade para lidar com correspondências exatas de números de série, identificadores de erro, siglas técnicas e nomes próprios raros.
Se um usuário busca pelo código de erro ERR_CONN_REFUSED_404, um modelo de embedding projeta a consulta em uma região conceitual de "falhas de rede", trazendo documentos sobre timeouts e problemas de DNS, enquanto o documento específico contendo aquela string exata pode ficar de fora do top-K.
A Busca Híbrida une o melhor dos dois mundos: o rigor léxico exato de índices invertidos baseados em BM25 e a compreensão conceitual ampla de embeddings vetoriais densos.
flowchart TD
Query["Consulta do Usuário"] --> BM25["Canal Léxico (BM25 / Índice Invertido)"]
Query --> Vector["Canal Denso (Embeddings / HNSW)"]
BM25 --> TopK_Lex["Top-60 Candidatos Léxicos"]
Vector --> TopK_Vec["Top-60 Candidatos Semânticos"]
TopK_Lex --> RRF["Fusão de Rankings (RRF - Reciprocal Rank Fusion)"]
TopK_Vec --> RRF
RRF --> CrossEncoder["Re-ranking Fino (Cross-Encoder / Cohere Rerank)"]
CrossEncoder --> FinalContext["Top-5 Resultados Relevantes"]
A Anatomia da Recuperação Esparsa vs Densa
| Critério | Busca Léxica Esparsa (BM25) | Busca Vetorial Densa (Embeddings) | Busca Híbrida (BM25 + Dense + RRF) |
|---|---|---|---|
| Representação | Vetor esparso de alta dimensionalidade (vocabulário) | Vetor denso contínuo (ex: 1536 dimensões numéricas) | Ambas as representações indexadas em paralelo |
| Vocabulário Out-of-Vocabulary | Preserva termos raros, SKUs, hash e IDs exatos | Dilui termos desconhecidos no espaço latente | Garante match exato enquanto preserva sinônimos |
| Compreensão de Sinônimos | Nula (requer dicionários de sinônimos manuais) | Nativa (entende que "carro" se aproxima de "veículo") | Combina a flexibilidade semântica com o rigor léxico |
| Custo Computacional | Baixo consumo de CPU e RAM | Alto consumo de memória e operações de produto escalar | Custo moderado com precisão e cobertura superiores |
Por dentro do BM25: A Evolução do TF-IDF
O algoritmo BM25 (Best Matching 25) refina a intuição do antigo TF-IDF através de dois fatores de compensação não-lineares:
- Saturação de Frequência de Termo ($k_1$): No TF-IDF clássico, se um termo aparece dez vezes em um documento, sua pontuação é dez vezes maior. No BM25, o parâmetro $k_1$ (tipicamente calibrado entre 1.2 e 2.0) introduz uma curva assintótica: após três ou quatro ocorrências, o ganho marginal de relevância diminui progressivamente.
- Normalização de Comprimento de Documento ($b$): O parâmetro $b$ (geralmente fixado em 0.75) penaliza documentos excessivamente longos para evitar que páginas com milhares de palavras ganhem destaque apenas por conterem muitas repetições acidentais.
Fusão de Rankings: Reciprocal Rank Fusion (RRF)
Como pontuações de BM25 (escores sem limites baseados em entropia) e distâncias de cosseno (valores normalizados entre 0 e 1) operam em escalas matemáticas incomparáveis, normalizar e somar as notas diretamente gera distorções.
O método padrão da indústria para fundir essas listas é o Reciprocal Rank Fusion (RRF). Em vez de utilizar as pontuações brutas, o RRF avalia exclusivamente a posição no ranking do documento em cada canal:
$$RRF_Score(d) = \sum_{m \in M} \frac{1}{k + rank_m(d)}$$
Onde $k$ é uma constante de suavização (o padrão consagrado pela literatura é 60) e $rank_m(d)$ é a colocação do documento $d$ no canal $m$.
def reciprocal_rank_fusion(lexical_ranking: list[str], vector_ranking: list[str], k: int = 60) -> list[tuple[str, float]]:
scores: dict[str, float] = {}
for rank, doc_id in enumerate(lexical_ranking, start=1):
scores[doc_id] = scores.get(doc_id, 0.0) + (1.0 / (k + rank))
for rank, doc_id in enumerate(vector_ranking, start=1):
scores[doc_id] = scores.get(doc_id, 0.0) + (1.0 / (k + rank))
# Ordena os documentos pela maior pontuação agregada
sorted_docs = sorted(scores.items(), key=lambda item: item[1], reverse=True)
return sorted_docs
Documentos que aparecem bem posicionados em ambos os canais sobem rapidamente para o topo da lista consolidada, enquanto documentos fortes em apenas um dos canais continuam representados entre as primeiras posições.
Refinamento Final: Re-ranking com Cross-Encoders
Enquanto bi-encoders geram embeddings de documentos e consultas separadamente (permitindo indexação prévia com HNSW), eles perdem as interações finas palavra por palavra entre a pergunta e a resposta.
Para maximizar a precisão antes de entregar o contexto ao modelo de linguagem, os sistemas aplicam um modelo de Cross-Encoder aos 20 ou 30 melhores candidatos retornados pelo RRF:
flowchart LR
Candidates["Top-30 Documentos do RRF"] --> CrossEnc["Modelo Cross-Encoder (Query + Doc concatenados)"]
CrossEnc --> ScoredDocs["Cálculo de Atenção Cruzada Completa"]
ScoredDocs --> Top5["Top-5 Documentos Finais para o Prompt RAG"]
O Cross-Encoder processa a consulta e o texto do documento juntos através de todas as camadas de atenção do transformador, avaliando a coerência factual com precisão profunda. Como o re-ranking opera sobre um subconjunto enxuto de candidatos, o impacto na latência total da requisição permanece na faixa de 15 a 30 milissegundos.