Phase 05 - Lesson 14
Recuperação de Informação e Busca
This lesson includes a graded coding exercise that runs in your browser, unlocked with lifetime access.
O BM25 é preciso, mas frágil. O denso lança uma rede ampla, mas perde palavras-chave. O híbrido é o padrão de 2026. Todo o resto é ajuste fino.
Tipo: Build Linguagens: Python Pré-requisitos: Fase 5 · 02 (BoW + TF-IDF), Fase 5 · 04 (GloVe, FastText, Subword) Tempo: ~75 minutos
O Problema
O usuário digita "o que acontece se alguém mente para conseguir dinheiro" e espera encontrar o artigo de lei que realmente cobre isso: "Seção 420 IPC". Uma busca por palavra-chave erra completamente (não há vocabulário em comum). Uma busca semântica erra se os embeddings não foram treinados em texto jurídico. A busca real precisa lidar com os dois casos.
A RI é o pipeline por trás de todo sistema RAG, toda barra de busca, toda busca aproximada de um site de documentação. A arquitetura de 2026 que funciona em produção não é um único método. É uma cadeia de métodos complementares, cada um capturando as falhas do anterior.
Esta lição constrói cada peça e nomeia quais falhas cada uma captura.
O Conceito
Quatro camadas. Escolha as que você precisa.
- Recuperação esparsa (BM25). Rápida, precisa em correspondências exatas, péssima em semântica. Roda sobre um índice invertido. Menos de 10ms por consulta em milhões de documentos. Acerta referências de leis, códigos de produtos, mensagens de erro e entidades nomeadas.
- Recuperação densa. Codifica consulta e documentos em vetores. Busca por vizinho mais próximo. Captura paráfrases e similaridade semântica. Perde correspondências exatas de palavra-chave que diferem por um único caractere. 50-200ms por consulta com FAISS ou um banco de dados vetorial.
- Fusão. Mescla as listas ranqueadas da recuperação esparsa e densa. A Reciprocal Rank Fusion (RRF) é o padrão fácil porque ignora os scores brutos (que vivem em escalas diferentes) e usa apenas as posições de ranque. A fusão ponderada é uma opção quando você sabe que um sinal domina no seu domínio.
- Reranqueamento com cross-encoder. Pegue o top-30 da fusão. Rode um cross-encoder (consulta + documento juntos, pontuando cada par). Mantenha o top-5. Cross-encoders são mais lentos por par do que bi-encoders, mas muito mais precisos. Você amortiza esse custo rodando-os apenas no top-30.
A recuperação em três vias (BM25 + denso + esparso aprendido como o SPLADE) supera a de duas vias nos benchmarks de 2026, mas precisa de infraestrutura para índices esparsos aprendidos. Para a maioria dos times, duas vias mais reranqueamento com cross-encoder é o ponto ideal.
Construa
Passo 1: BM25 do zero
import math
import re
from collections import Counter
TOKEN_RE = re.compile(r"[a-z0-9]+")
def tokenize(text):
return TOKEN_RE.findall(text.lower())
class BM25:
def __init__(self, corpus, k1=1.5, b=0.75):
if not corpus:
raise ValueError("corpus must not be empty")
self.corpus = [tokenize(d) for d in corpus]
self.k1 = k1
self.b = b
self.n_docs = len(self.corpus)
self.avg_dl = sum(len(d) for d in self.corpus) / self.n_docs
self.df = Counter()
for doc in self.corpus:
for term in set(doc):
self.df[term] += 1
def idf(self, term):
n = self.df.get(term, 0)
return math.log(1 + (self.n_docs - n + 0.5) / (n + 0.5))
def score(self, query, doc_idx):
q_tokens = tokenize(query)
doc = self.corpus[doc_idx]
dl = len(doc)
freq = Counter(doc)
score = 0.0
for term in q_tokens:
f = freq.get(term, 0)
if f == 0:
continue
numerator = f * (self.k1 + 1)
denominator = f + self.k1 * (1 - self.b + self.b * dl / self.avg_dl)
score += self.idf(term) * numerator / denominator
return score
def rank(self, query, top_k=10):
scored = [(self.score(query, i), i) for i in range(self.n_docs)]
scored.sort(reverse=True)
return scored[:top_k]
Dois parâmetros que vale conhecer. k1=1.5 controla a saturação da frequência de termos; maior significa mais peso na repetição do termo. b=0.75 controla a normalização por comprimento; 0 ignora o comprimento do documento, 1 normaliza totalmente. Os padrões são as recomendações de Robertson do artigo original e raramente precisam de ajuste.
Passo 2: recuperação densa com um bi-encoder
from sentence_transformers import SentenceTransformer
import numpy as np
def build_dense_index(corpus, model_id="sentence-transformers/all-MiniLM-L6-v2"):
encoder = SentenceTransformer(model_id)
embeddings = encoder.encode(corpus, normalize_embeddings=True)
return encoder, embeddings
def dense_search(encoder, embeddings, query, top_k=10):
q_emb = encoder.encode([query], normalize_embeddings=True)
sims = (embeddings @ q_emb.T).flatten()
order = np.argsort(-sims)[:top_k]
return [(float(sims[i]), int(i)) for i in order]
Normalize os embeddings com L2 para que o produto escalar seja igual ao cosseno. O all-MiniLM-L6-v2 tem 384 dimensões, é rápido e forte o bastante para a maioria das recuperações em inglês. Para trabalho multilíngue, use paraphrase-multilingual-MiniLM-L12-v2. Para máxima precisão, bge-large-en-v1.5 ou e5-large-v2.
Passo 3: Reciprocal Rank Fusion
def reciprocal_rank_fusion(rankings, k=60):
scores = {}
for ranking in rankings:
for rank, (_, doc_idx) in enumerate(ranking):
scores[doc_idx] = scores.get(doc_idx, 0.0) + 1.0 / (k + rank + 1)
fused = sorted(scores.items(), key=lambda x: x[1], reverse=True)
return [(score, doc_idx) for doc_idx, score in fused]
A constante k=60 vem do artigo original da RRF. Um k maior achata a contribuição das diferenças de ranque; um k menor faz os ranques do topo dominarem. 60 é o padrão publicado e raramente precisa de ajuste.
Passo 4: busca híbrida + reranqueamento
from sentence_transformers import CrossEncoder
reranker = CrossEncoder("cross-encoder/ms-marco-MiniLM-L-6-v2")
def hybrid_search(query, bm25, encoder, dense_embeddings, corpus, top_k=5, pool_size=30, reranker=reranker):
sparse_ranking = bm25.rank(query, top_k=pool_size)
dense_ranking = dense_search(encoder, dense_embeddings, query, top_k=pool_size)
fused = reciprocal_rank_fusion([sparse_ranking, dense_ranking])[:pool_size]
pairs = [(query, corpus[doc_idx]) for _, doc_idx in fused]
scores = reranker.predict(pairs)
reranked = sorted(zip(scores, [doc_idx for _, doc_idx in fused]), reverse=True)
return reranked[:top_k]
Três estágios compostos. O BM25 encontra correspondências lexicais. O denso encontra correspondências semânticas. A RRF mescla os dois ranqueamentos sem precisar de calibração de scores. O cross-encoder repontua o top-30 usando pares de consulta-documento juntos, o que captura uma relevância de granularidade fina que o bi-encoder perdeu. Mantenha o top-5.
Passo 5: avaliação
| Métrica | Significado |
|---|---|
| Recall@k | Das consultas onde o documento correto existe, com que frequência ele está no top-k? |
| MRR (Mean Reciprocal Rank) | Média de 1/ranque do primeiro documento relevante. |
| nDCG@k | Considera gradações de relevância, não apenas relevante/não relevante binário. |
Para RAG especificamente, o Recall@k do recuperador é o número mais importante. Seu leitor não consegue responder se a passagem certa não estiver no conjunto recuperado.
Dica de depuração: para consultas que falham, compare os ranqueamentos esparso e denso. Se um encontra o documento certo e o outro não, você tem uma incompatibilidade de vocabulário (correção: adicione a metade faltante) ou uma ambiguidade semântica (correção: embeddings melhores ou um reranqueador).
Use
A stack de 2026:
| Escala | Stack |
|---|---|
| 1k-100k docs | BM25 em memória + embeddings all-MiniLM-L6-v2 + RRF. Sem banco de dados separado. |
| 100k-10M docs | FAISS ou pgvector para o denso + Elasticsearch / OpenSearch para o BM25. Rode em paralelo. |
| 10M+ docs | Qdrant / Weaviate / Vespa / Milvus com suporte híbrido. Reranqueamento com cross-encoder no top-30. |
| Fronteira de melhor qualidade | Três vias (BM25 + denso + SPLADE) + reranqueamento de interação tardia com ColBERT |
Seja qual for a sua escolha, reserve orçamento para avaliação. Faça benchmark do recall da recuperação antes de fazer benchmark da precisão de RAG ponta a ponta. Um leitor não consegue corrigir o que o recuperador perdeu.
As lições arduamente conquistadas do RAG em produção em 2026
- 80% das falhas de RAG remontam à ingestão e ao chunking, não ao modelo. Os times passam semanas trocando LLMs e ajustando prompts enquanto a recuperação silenciosamente retorna o contexto errado a cada terceira consulta. Corrija o chunking primeiro.
- A estratégia de chunking importa mais do que o tamanho do chunk. Divisões de tamanho fixo quebram tabelas, código e cabeçalhos aninhados. A divisão por sentença é o padrão; chunking semântico ou baseado em LLM compensa para documentos técnicos e manuais de produto.
- Padrão parent-doc. Recupere chunks "filhos" pequenos para precisão. Quando múltiplos filhos da mesma seção pai aparecem, troque pelo bloco pai para preservar o contexto. Isso eleva de forma consistente a qualidade da resposta sem retreinamento.
- k_rerank=3 costuma ser ótimo. Cada chunk extra além disso adiciona custo de token e latência de geração sem elevar a qualidade da resposta. Se k=8 ainda é melhor que k=3 para você, o reranqueador está com desempenho abaixo do esperado.
- HyDE / expansão de consulta. Gere uma resposta hipotética a partir da consulta, faça o embedding dela e recupere. Faz a ponte da lacuna de formulação entre perguntas curtas e documentos longos. Ganho de precisão de graça, sem treinamento.
- Orçamento de contexto abaixo de 8K tokens. Atingir esse limite de forma consistente significa que o limiar do reranqueador está frouxo demais.
- Versione tudo. Prompts, regras de chunking, modelo de embedding, reranqueador. Qualquer desvio quebra silenciosamente a qualidade da resposta. Gates de CI em fidelidade, precisão de contexto e taxa de perguntas não respondidas bloqueiam regressões antes que os usuários as vejam.
- A recuperação em três vias (BM25 + denso + esparso aprendido como o SPLADE) supera a de duas vias nos benchmarks de 2026, especialmente para consultas que misturam nomes próprios com semântica. Coloque em produção quando a infraestrutura der suporte a índices SPLADE.
Um design de recuperação adequado reduz alucinações em 70-90% segundo medições da indústria em 2026. A maioria dos ganhos de desempenho em RAG vem de uma recuperação melhor, não do fine-tuning do modelo.
Entregue
Salve como outputs/skill-retrieval-picker.md:
---
name: retrieval-picker
description: Pick a retrieval stack for a given corpus and query pattern.
version: 1.0.0
phase: 5
lesson: 14
tags: [nlp, retrieval, rag, search]
---
Given requirements (corpus size, query pattern, latency budget, quality bar, infra constraints), output:
1. Stack. BM25 only, dense only, hybrid (BM25 + dense + RRF), hybrid + cross-encoder rerank, or three-way (BM25 + dense + learned-sparse).
2. Dense encoder. Name the specific model. Match to language(s), domain, and context length.
3. Reranker. Name the specific cross-encoder model if used. Flag that rerank adds 30-100ms latency on top-30.
4. Evaluation plan. Recall@10 is the primary retriever metric. MRR for multi-answer. Baseline first, incremental improvements measured against it.
Refuse to recommend dense-only for corpora with named entities, error codes, or product SKUs unless the user has evidence dense handles exact matches. Refuse to skip reranking for high-stakes retrieval (legal, medical) where the final top-5 decides the user's answer.
Exercícios
- Fácil. Implemente o
hybrid_searchacima em um corpus de 500 documentos. Teste 20 consultas. Compare o recall em 5 entre BM25 sozinho, denso sozinho e híbrido. - Médio. Adicione o cálculo de MRR. Para cada consulta de teste com um documento correto conhecido, encontre o ranque do documento correto nos ranqueamentos BM25, denso e híbrido. Reporte o MRR de cada um.
- Difícil. Faça fine-tuning de um encoder denso no seu domínio usando MultipleNegativesRankingLoss (Sentence Transformers). Construa um conjunto de treino a partir de 500 pares consulta-documento. Compare o recall antes e depois do fine-tuning.
Termos-Chave
| Termo | O que as pessoas dizem | O que realmente significa |
|---|---|---|
| BM25 | Busca por palavra-chave | Okapi BM25. Pontua documentos pela frequência de termos, IDF e comprimento. |
| Recuperação densa | Busca vetorial | Codifica consulta + documento em vetores, encontra vizinhos mais próximos. |
| Bi-encoder | Modelo de embedding | Codifica consulta e documento de forma independente. Rápido no momento da consulta. |
| Cross-encoder | Modelo reranqueador | Codifica consulta + documento juntos. Lento, mas preciso. |
| RRF | Fusão de ranques | Combina dois ranqueamentos somando 1/(k + rank). |
| Recall@k | Métrica de recuperação | Fração de consultas em que um documento relevante está no top-k. |
Leitura Adicional
- Robertson and Zaragoza (2009). The Probabilistic Relevance Framework: BM25 and Beyond — o tratamento definitivo do BM25.
- Karpukhin et al. (2020). Dense Passage Retrieval for Open-Domain QA — DPR, o bi-encoder canônico.
- Formal et al. (2021). SPLADE: Sparse Lexical and Expansion Model — o recuperador esparso aprendido que fecha a lacuna com o denso.
- Cormack, Clarke, Büttcher (2009). Reciprocal Rank Fusion outperforms Condorcet and individual Rank Learning Methods — artigo da RRF.
- Khattab and Zaharia (2020). ColBERT: Efficient and Effective Passage Search — recuperação de interação tardia.