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.

Diagrama de pipeline de busca híbrida fundindo pontuações esparsas BM25 e pontuações densas de embeddings através de Reciprocal Rank Fusion
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:

  1. 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.
  2. 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.

Explorar este tema no Grafo de Conhecimento