04-03-03 — Hybrid search: vector + BM25

⏱ 12 minFontes validadas em: 2026-04-29

TL;DR

Busca vetorial é ótima para semântica mas falha em termos exatos (siglas, nomes próprios, números de processo). BM25 (keyword search) é o oposto: perfeito para termos exatos, mas cego ao significado. Hybrid search combina os dois via Reciprocal Rank Fusion (RRF). Em benchmarks, hybrid supera qualquer uma das abordagens isoladas em quase todos os domínios. É a configuração recomendada para produção.

Por que keyword search ainda importa

Busca semântica via embeddings tem um ponto cego: termos exatos e raros.

Exemplos onde busca vetorial falha e keyword search acerta:

  • "processo nº 0001234-56.2023.8.19.0001" → o embedding não distingue números de processo
  • "LGPD art. 46" → acrônimos e referências legais específicas
  • "CVE-2024-1234" → identificadores de vulnerabilidades
  • "NF-e chave 43230214200166000139550010001234561234567890" → números de nota fiscal
  • Nomes próprios incomuns

Para esses casos, BM25 é preciso e rápido. O problema é que BM25 falha quando o usuário pergunta "quais são as penalidades por quebra de contrato?" e o documento diz "multa rescisória" — as palavras não coincidem mas o significado sim. Daí a necessidade de combinar os dois.

BM25 — uma revisão rápida

BM25 (Best Matching 25) é o algoritmo de ranking de busca por keywords padrão da indústria. Ele pontua documentos baseado em:

  • TF (Term Frequency): quantas vezes o termo aparece no documento
  • IDF (Inverse Document Frequency): termos raros valem mais que termos comuns
  • Saturação: repetição do mesmo termo tem retornos decrescentes
  • Normalização de comprimento: documentos longos não têm vantagem injusta

Elasticsearch, Apache Solr, Azure AI Search — todos usam BM25 como base para busca textual.

Reciprocal Rank Fusion (RRF)

O desafio de combinar busca vetorial e BM25 é que os scores são incomparáveis: cosine similarity vai de 0 a 1, BM25 pode ser qualquer número positivo. Somar os scores diretamente não faz sentido.

RRF resolve isso usando apenas a posição no ranking, não o score absoluto:

RRF_score(d) = Σ 1 / (k + rank(d))

Onde k é uma constante (tipicamente 60) que suaviza a influência de rankings muito altos.

flowchart TD Q[Query do usuário] --> VEC[Busca Vetorial\nTop-50 por cosine] Q --> BM25[Busca BM25\nTop-50 por relevância textual] VEC --> |"rank 1: chunk A\nrank 2: chunk C\nrank 3: chunk B"| RRF[Reciprocal Rank Fusion] BM25 --> |"rank 1: chunk B\nrank 2: chunk A\nrank 3: chunk D"| RRF RRF --> |"chunk A: 1/61 + 1/62 = 0.032\nchunk B: 1/63 + 1/61 = 0.032\nchunk C: 1/62 + ... = ..."| FINAL[Ranking final unificado\nTop-k para o LLM]

Implementação manual com RRF

def reciprocal_rank_fusion(rankings: list[list[str]], k: int = 60) -> list[tuple[str, float]]:
    """
    rankings: lista de listas de IDs de documentos, cada lista é um ranking
    retorna: lista de (doc_id, rrf_score) ordenada por score decrescente
    """
    scores = {}
    for ranking in rankings:
        for position, doc_id in enumerate(ranking):
            if doc_id not in scores:
                scores[doc_id] = 0.0
            scores[doc_id] += 1.0 / (k + position + 1)

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

# Exemplo de uso
vector_results = ["chunk-A", "chunk-C", "chunk-B", "chunk-E"]  # IDs por cosine
keyword_results = ["chunk-B", "chunk-A", "chunk-D", "chunk-C"]  # IDs por BM25

fused = reciprocal_rank_fusion([vector_results, keyword_results])
top_5 = [doc_id for doc_id, score in fused[:5]]

Azure AI Search hybrid + semantic reranker

Azure AI Search implementa hybrid search + RRF nativamente. Adicionando o semantic ranker, você tem um pipeline de 3 níveis:

  1. Fase 1 — Candidate retrieval: BM25 busca top-50, vetorial busca top-50
  2. Fase 2 — RRF fusion: combina os dois rankings → top-50 unificado
  3. Fase 3 — Semantic reranker: modelo de linguagem reordena os 50 → retorna top-k
from azure.search.documents import SearchClient
from azure.search.documents.models import VectorizedQuery

results = search_client.search(
    search_text=user_query,           # ativa BM25
    vector_queries=[VectorizedQuery(
        vector=embed(user_query),
        k_nearest_neighbors=50,
        fields="content_vector"       # ativa busca vetorial
    )],
    # RRF é automático quando ambos estão ativos
    query_type="semantic",            # ativa semantic reranker (3ª camada)
    semantic_configuration_name="my-semantic-config",
    top=5                             # retorna top 5 após reranking
)
💡 Resultados de benchmark

Microsoft publicou que em testes internos, hybrid search supera busca puramente vetorial em 10-15% de relevância (medida por NDCG@10). Com semantic reranker adicional, o ganho chega a 25-30% em benchmarks de QA sobre documentos corporativos. A combinação é o estado da arte para RAG enterprise.

🏢 Microsoft

O Azure AI Search implementa hybrid search com RRF sem configuração extra — basta incluir tanto search_text quanto vector_queries na mesma chamada. O parâmetro query_type="semantic" adiciona o semantic reranker como terceira camada. Custo do semantic ranker: veja tier e limite gratuito em 04-02-03.

Como isso se conecta

Fontes

  1. Cormack, G., Clarke, C., Buettcher, S. (2009). Reciprocal Rank Fusion outperforms Condorcet and individual Rank Learning Methods. SIGIR. dl.acm.org
  2. Microsoft. Hybrid search using vectors and full text in Azure AI Search. learn.microsoft.com
  3. Pinecone. Sparse-Dense Hybrid Search. pinecone.io/learn/hybrid-search-intro
  4. Robertson, S., Zaragoza, H. (2009). The Probabilistic Relevance Framework: BM25 and Beyond. nowpublishers.com