~ / tutoriais /rag-hibrido-na-pratica-bm25-embeddings-reranker $ _

RAG híbrido na prática: BM25, embeddings e reranker

Lucas Souza Lucas Souza 9 min de leitura Tutoriais
RAG híbrido na prática: BM25, embeddings e reranker

Você joga a pergunta do usuário no banco vetorial, pega os 5 chunks mais "parecidos" e manda pro modelo. Funciona na demo. Aí entra em produção e alguém busca por um código de erro, um nome de tabela, um SKU. O embedding devolve cinco textos que falam sobre o assunto e nenhum que tem o termo exato. A resposta sai errada, confiante, e o usuário some.

O problema não é o embedding. É achar que busca semântica resolve sozinha o que é, no fundo, um problema de recuperação de informação com 50 anos de história. A saída é o RAG híbrido: busca lexical (BM25) acerta exatamente onde o vetor erra, e um reranker no fim da fila ainda arruma a ordem antes do contexto chegar no modelo.

Neste tutorial você vai montar um RAG híbrido de verdade: BM25 + embeddings fundidos por Reciprocal Rank Fusion, com um cross-encoder reranqueando o topo. Código rodando em Python, com LangChain, e as decisões de arquitetura explicadas — não só o pip install.

TL;DR

  • O que é: pipeline de recuperação que combina busca lexical (BM25) com busca semântica (embeddings), funde os dois rankings com RRF e reranqueia o topo com um cross-encoder.
  • Stack/Modelos: Python, LangChain (EnsembleRetriever, ContextualCompressionRetriever), um modelo de embedding qualquer, Cohere Rerank 3.5 (ou bge-reranker-v2-m3 local).
  • Custo/Acesso: BM25 e embeddings open-source rodam local; o reranker pode ser API paga (Cohere) ou modelo aberto self-hosted.
  • Quando usar: sempre que a base tem termos exatos que importam — códigos, nomes próprios, jargão, identificadores — e não só prosa.

Por que embedding sozinho erra mais do que você imagina

Embedding transforma texto em vetor e mede similaridade de significado. É ótimo pra paráfrase: "como cancelo minha assinatura" casa com um doc que fala em "encerramento de plano" sem compartilhar uma palavra. Esse é o superpoder da busca densa.

Agora vira a moeda. Pergunte por ERR_CONN_4031 ou pela tabela fct_pedidos_v2. O embedding não tem nada de especial pra esse token — ele dilui o identificador no meio do significado geral da frase e te devolve documentos temáticos, não o que contém o termo. Num teste com 10 mil documentos técnicos, BM25 acertou 94% de recall em queries de exact-match (códigos de erro, SKUs) contra 67% da busca vetorial pura. Não é detalhe. É um terço das buscas exatas indo pro buraco.

BM25 (Best Match 25) é o algoritmo lexical que é a espinha dorsal de buscadores desde os anos 90 — o ranking padrão do Elasticsearch saiu dele. Ele pontua por frequência de termo e raridade. Termo raro que bate exato? Pontuação alta. É exatamente o caso que o embedding mais erra.

Repara no padrão: os dois métodos falham em lados opostos. Densa erra no literal, lexical erra no conceitual. Juntar os dois não é redundância. É cobrir o ângulo cego um do outro.

A arquitetura do RAG híbrido: recuperar amplo, fundir, reranquear

RAG híbrido bom tem três estágios. Não confunda.

  1. Recuperação dupla. A mesma query roda em paralelo no índice BM25 e no índice vetorial. Cada um devolve sua lista ranqueada — digamos, top 20 de cada.
  2. Fusão com RRF. Você tem duas listas com escalas de pontuação incompatíveis (BM25 dá score absoluto, cosseno dá 0–1). Não dá pra somar. Reciprocal Rank Fusion joga os scores fora e olha só a posição: cada documento ganha 1 / (k + rank) em cada lista, e os valores se somam. Por convenção k = 60. Um doc que aparece bem nas duas listas sobe; um que só uma lista amou fica no meio. Simples e robusto.
  3. Reranking. Os ~20 candidatos fundidos passam por um cross-encoder, que reordena por relevância real query-documento. Só os top 3–5 vão pro modelo.

O primeiro estágio é sobre recall — não deixar o documento certo de fora. O terceiro é sobre precisão — botar o documento certo no topo. São objetivos diferentes, e por isso são etapas diferentes.

Pré-requisitos

  • [ ] Python 3.11+ e pip install langchain langchain-community rank_bm25.
  • [ ] Um modelo de embedding (OpenAI, um sentence-transformers local, tanto faz) e um vector store (FAISS, Chroma, Qdrant).
  • [ ] Para o reranker: chave da Cohere (pip install langchain-cohere) ou pip install sentence-transformers pra rodar um bge-reranker local.
  • [ ] Sua base já chunkada e indexada. Se você ainda não tem um RAG de pé, monte o básico primeiro e volte aqui.

Mão na massa

Passo 1: os dois retrievers

Indexe o mesmo corpus de duas formas. BM25 a partir dos documentos crus; o vetorial a partir dos embeddings.

from langchain_community.retrievers import BM25Retriever
from langchain_community.vectorstores import FAISS
from langchain_openai import OpenAIEmbeddings

# busca lexical
bm25 = BM25Retriever.from_documents(docs)
bm25.k = 20

# busca densa
vectorstore = FAISS.from_documents(docs, OpenAIEmbeddings())
dense = vectorstore.as_retriever(search_kwargs={"k": 20})

Pega 20 de cada, não 5. Aqui você quer recall: é melhor o documento certo chegar na posição 18 do que não chegar. O reranker conserta a ordem depois.

Passo 2: fundir com RRF

O EnsembleRetriever do LangChain já faz Reciprocal Rank Fusion nativamente. Você passa os dois retrievers e os pesos.

from langchain.retrievers import EnsembleRetriever

hybrid = EnsembleRetriever(
    retrievers=[bm25, dense],
    weights=[0.4, 0.6],  # [lexical, semântico]
)

Os pesos enviesam a fusão. [0.5, 0.5] trata igual; [0.4, 0.6] dá um empurrão pro semântico. Não existe número mágico — depende da sua base. Base cheia de identificador e jargão pede mais peso no BM25. Base de linguagem natural solta pede mais no vetor. Isso se mede com um dataset de avaliação, não se chuta.

Passo 3: o reranker no topo

Agora a camada que muda o jogo (e que rende um post inteiro só pra ela). O ContextualCompressionRetriever envolve o retriever híbrido e passa os candidatos por um compressor — no caso, o reranker.

from langchain.retrievers import ContextualCompressionRetriever
from langchain_cohere import CohereRerank

reranker = CohereRerank(model="rerank-v3.5", top_n=4)

pipeline = ContextualCompressionRetriever(
    base_compressor=reranker,
    base_retriever=hybrid,
)

docs = pipeline.invoke("por que recebo ERR_CONN_4031 ao subir o deploy?")

O híbrido entrega 20–40 candidatos; o reranker devolve os 4 melhores, na ordem certa. É esse top_n=4 que vai pro prompt do modelo. Quer trocar a Cohere por um modelo aberto? Troca o compressor por um CrossEncoderReranker apontando pro BAAI/bge-reranker-v2-m3 — a estrutura do pipeline não muda.

Passo 4: erro comum — reranquear cedo demais

O engano clássico é recuperar só 5 candidatos e mandar pro reranker. Reranker não inventa documento: ele reordena o que recebe. Se o doc certo não passou no primeiro estágio, nenhum cross-encoder no mundo o traz de volta. Recupere largo (20–40), reranqueie estreito (3–5). Recall primeiro, precisão depois.

O reranker: por que cross-encoder, e não mais um embedding

Embedding é um bi-encoder: ele codifica a query e o documento separadamente, em vetores independentes, e compara no fim. Rápido — dá pra pré-computar tudo — mas a query nunca "olha" o documento durante a codificação.

Cross-encoder é outra coisa. Ele recebe o par query+documento junto e roda cross-attention entre os dois em tempo de query. Cada token da pergunta pesa contra cada token do documento. É muito mais caro — por isso você só roda nos ~20 finalistas, nunca na base inteira — e muito mais preciso no julgamento de relevância.

O ganho é real e medido. A Cohere reporta que o Rerank melhora a acurácia da resposta entre 20% e 35% sobre busca por embedding pura, ainda cortando tokens — porque você manda 4 chunks ótimos em vez de 10 mais ou menos. Em benchmarks de NDCG@10, modelos abertos como o bge-reranker-v2-m3 ficam praticamente empatados com a Cohere, e os dois batem busca vetorial pura com folga. Ou seja: dá pra ter o ganho sem depender de API paga.

A intuição que vale guardar: o primeiro estágio responde "isso parece relevante?". O cross-encoder responde "isso é relevante pra esta pergunta?". A segunda pergunta é a que importa.

Limitações e pontos de atenção

  • Latência. São três passos em série; o reranker adiciona uma chamada (de rede, se for API). Em fluxo conversacional cabe; em autocomplete de baixa latência, meça antes de prometer.
  • Custo do reranker. Reranker por API cobra por documento avaliado. Recuperar 100 candidatos pra reranquear sai caro e quase não melhora sobre 20–40. Acerte o k do primeiro estágio.
  • Tuning não é opcional. Pesos do RRF, k de recuperação, top_n do rerank — tudo isso é dado, não palpite. Sem um dataset de avaliação (nem que sejam 30–50 perguntas com a resposta certa anotada), você está otimizando no escuro.
  • Híbrido não conserta chunk ruim. Se você fatiou o documento no meio da frase, recuperação melhor só entrega lixo bem ranqueado. Chunking vem antes.

FAQ

BM25 não ficou obsoleto com os embeddings? Não, e é justamente o contrário. Ele continua ganhando exatamente nas queries que mais importam: termo exato, identificador, jargão raro. Em produção, são essas que mais doem quando falham.

Preciso de um vector DB que suporte híbrido nativo? Ajuda (Qdrant, Weaviate e Elasticsearch fazem fusão no servidor), mas não é obrigatório. O EnsembleRetriever funde duas listas no lado da aplicação — dá pra começar com FAISS + BM25 em memória.

Reranker resolve o "lost in the middle"? Ajuda bastante. Como ele entrega menos documentos e melhor ordenados, o modelo recebe um contexto curto e denso em vez de dez chunks medianos onde a informação certa se perde no meio.

Cohere ou modelo aberto? Comece pela Cohere pra validar o ganho sem fricção de infra. Se o volume justificar, migre pro bge-reranker-v2-m3 self-hosted — a qualidade é equivalente e o pipeline não muda.

Fechando

RAG híbrido não é hype de arquitetura. É reconhecer que recuperação tem duas naturezas — literal e semântica — e que jogar fora uma delas custa caro em produção. BM25 cobre o exato, embedding cobre o conceitual, RRF junta as duas listas sem brigar por escala de score, e o cross-encoder dá a palavra final sobre o que realmente responde a pergunta. Quatro peças simples, cada uma resolvendo um problema específico.

O próximo passo é parar de tratar isso como "feature de busca" e começar a tratar como engenharia do harness em volta do modelo — recuperação, avaliação, ordem do contexto, limite de tokens. É exatamente esse salto, do prompt isolado para o harness que sustenta um agente em produção, que a gente coloca na mesa no workshop Do Prompt ao Harness: construindo um agente de vendas, com o pipeline rodando de verdade.

Montou o seu, mediu o NDCG antes e depois do reranker, e o número subiu? Então você já entendeu a parte que separa demo de produto.

Lucas Souza
Lucas Souza

{AI Engineer} — apaixonado por Laravel, arquitetura de software e construir produtos com impacto. Compartilho aqui tutoriais, descobertas e reflexões sobre o dia a dia de engenharia.

Você também pode gostar

Reranker: o passo que faz seu RAG parar de devolver lixo
Tutoriais

Reranker: o passo que faz seu RAG parar de devolver lixo

A busca vetorial traz 20 candidatos "parecidos" — mas parecido não é relevante. O reranker reordena por relevância real antes de mandar pro modelo. Este post mostra cross-encoder vs busca híbrida e quando cada um vale, com código rodando.

· 9 min
Cross-encoder reranker: o componente que mais eleva qualidade do seu agente por dólar
Tutoriais

Cross-encoder reranker: o componente que mais eleva qualidade do seu agente por dólar

Retrieval traz 100 candidatos, reranker escolhe os 10 certos. Entenda o trade-off latência x precisão, quando rerankar 50 vs. 200 documentos e por que cross-encoder é o investimento de melhor ROI antes de trocar para um LLM mais caro.

· 10 min
Busca híbrida: a receita BM25 + vetor + RRF que resolve SKU, part-number e semântica
Tutoriais

Busca híbrida: a receita BM25 + vetor + RRF que resolve SKU, part-number e semântica

Embedding puro confunde "RX-7000" com "RX-5000". BM25 puro perde sinônimos. A receita certa é rodar os dois em paralelo e fundir os rankings com Reciprocal Rank Fusion. Neste post, a fórmula que sustenta tudo isso, o pipeline completo em Elasticsearch e como aplicar em catálogo de produto que mistura SKU, part-number e busca semântica.

· 13 min
RAG do zero: chunking, embeddings e busca que funciona
Tutoriais

RAG do zero: chunking, embeddings e busca que funciona

RAG não é mágica: é quebrar texto, virar vetor e buscar bem. O passo a passo de um RAG do zero — chunking recursive com overlap, embeddings com text-embedding-3-small e busca por similaridade no Postgres com pgvector e índice HNSW. Errar o chunking é onde 80% dos RAGs nascem ruins.

· 10 min

VirguIA

beer & code assistant

conectando…

Não foi possível iniciar o chat agora.

tocando