AI Engineering Academy · Aula

Implementando busca por palavras-chave com BM25

Configure o BM25 usando rank_bm25 em Python, indexe seu corpus de documentos e execute buscas por palavras-chave que lidem de forma confiável com termos exatos, jargão técnico e nomes de produtos.

Aula 2 de 413 etapas

Implementando busca por palavras-chave com BM25 é uma aula grátis de AI Engineering Academy no CoddyKit. Esta é a aula 2 de 4. Você pode ler a aula completa abaixo gratuitamente — depois pratica ao vivo no navegador com um editor de código integrado e um tutor de IA 24/7. Faz parte do caminho de aprendizado de AI Engineering Academy, e seu progresso é sincronizado entre a web e o app CoddyKit. O curso de AI Engineering Academy inclui 4 aulas no total.

Instalando rank_bm25

rank_bm25 é uma biblioteca leve de Python que fornece as variantes BM25Okapi, BM25L e BM25Plus do algoritmo BM25. Ela não exige serviços externos, é executada inteiramente na memória e pode indexar milhares de documentos em segundos em hardware comum. Instale-a com pip install rank-bm25 e você estará pronto para criar uma busca por palavras-chave sem nenhuma configuração de infraestrutura.

# Install: pip install rank-bm25
from rank_bm25 import BM25Okapi

# BM25Okapi is the most common variant
# BM25L and BM25Plus handle very short documents better
# For most RAG use cases BM25Okapi is the right choice

corpus = [
    'Python decorator pattern explained with examples',
    'How to use context managers in Python',
    'JavaScript async await tutorial',
]
tokenized = [doc.lower().split() for doc in corpus]
bm25 = BM25Okapi(tokenized)
print('Index built with', len(corpus), 'documents')

Tokenização: a primeira etapa crítica

BM25 opera sobre listas de tokens, não sobre cadeias de caracteres brutas. A qualidade da sua tokenização afeta diretamente a qualidade da recuperação. Uma simples divisão por espaços não remove pontuação, não aplica stemming e não elimina palavras irrelevantes. Em sistemas de produção, use um tokenizador apropriado que converta o texto para minúsculas, remova a pontuação, elimine palavras irrelevantes e, opcionalmente, aplique stemming para associar variantes morfológicas como 'run', 'runs' e 'running'.

import re
from nltk.corpus import stopwords
from nltk.stem import PorterStemmer

STOP_WORDS = set(stopwords.words('english'))
stemmer = PorterStemmer()

def tokenize(text: str) -> list[str]:
    text = text.lower()
    text = re.sub(r'[^a-z0-9\s]', ' ', text)
    tokens = text.split()
    tokens = [t for t in tokens if t not in STOP_WORDS and len(t) > 1]
    tokens = [stemmer.stem(t) for t in tokens]
    return tokens

print(tokenize('Running Python decorators efficiently in production!'))
# ['run', 'python', 'decor', 'effici', 'product']

Criando o índice BM25

Criar um índice BM25 é uma operação off-line feita uma única vez. Você passa o corpus tokenizado para BM25Okapi, que calcula as frequências inversas dos documentos para todos os termos e armazena os comprimentos dos documentos para normalização. O índice é leve — alguns megabytes mesmo para dezenas de milhares de documentos. Você deve recriá-lo sempre que novos documentos forem adicionados ao seu corpus.

from rank_bm25 import BM25Okapi

def build_bm25_index(documents: list[str]):
    tokenized = [tokenize(doc) for doc in documents]
    bm25 = BM25Okapi(tokenized)
    return bm25, tokenized

# Example with a small corpus
docs = [
    'Vector databases store dense embeddings for similarity search',
    'BM25 is a sparse keyword retrieval algorithm used in search engines',
    'Hybrid search combines dense and sparse retrieval for better recall',
    'PostgreSQL supports vector search via the pgvector extension',
]
bm25, tokenized = build_bm25_index(docs)
print(f'Index contains {bm25.corpus_size} documents')

Executando uma busca BM25

Para realizar uma busca, tokeniza a consulta usando o mesmo tokenizador do índice — a tokenização inconsistente é uma fonte comum de resultados de recuperação insatisfatórios. Chame get_scores para obter as pontuações de relevância de todos os documentos ou get_top_n para recuperar diretamente os N melhores resultados. Use sempre o mesmo fluxo de pré-processamento tanto para a indexação quanto para as consultas.

def bm25_search(bm25, documents: list[str], query: str, top_k: int = 3):
    query_tokens = tokenize(query)
    scores = bm25.get_scores(query_tokens)

    # Get indices sorted by score descending
    ranked = sorted(enumerate(scores), key=lambda x: x[1], reverse=True)

    results = []
    for idx, score in ranked[:top_k]:
        results.append({
            'document': documents[idx],
            'score': round(score, 4),
            'rank': len(results) + 1,
        })
    return results

results = bm25_search(bm25, docs, 'sparse keyword search engine')
for r in results:
    print(f"Rank {r['rank']} (score {r['score']}): {r['document'][:60]}")

Ajustando os hiperparâmetros do BM25

BM25Okapi aceita dois hiperparâmetros: k1 controla a saturação da frequência dos termos (valores maiores permitem que termos de alta frequência obtenham pontuações mais altas) e b controla a normalização do comprimento dos documentos (1.0 = normalização completa, 0.0 = sem normalização). Os valores padrão k1=1.5, b=0.75 funcionam bem para textos em prosa. Para trechos curtos (com menos de 100 palavras), experimente valores menores de b, como 0.3, para reduzir o viés causado pelo comprimento.

from rank_bm25 import BM25Okapi

# Default hyperparameters — good starting point
bm25_default = BM25Okapi(tokenized, k1=1.5, b=0.75)

# Tuned for short document chunks
bm25_short = BM25Okapi(tokenized, k1=1.2, b=0.3)

# Tuned for long documents
bm25_long = BM25Okapi(tokenized, k1=2.0, b=0.9)

# Always benchmark hyperparameters against a golden eval set
# before deploying to production

Lidando com jargão técnico e tokens de código

Para bases de código e documentação técnica, seu tokenizador deve preservar tokens técnicos, em vez de aplicar stemming agressivamente. Termos como BM25Okapi, pgvector e LLM devem permanecer intactos. Um tokenizador híbrido que ignore o stemming para tokens que correspondam a padrões como siglas em maiúsculas, identificadores CamelCase ou snake_case produzirá resultados melhores em buscas voltadas para desenvolvedores.

import re

def technical_tokenize(text: str) -> list[str]:
    text = text.lower()
    # preserve underscores in snake_case and dots in version numbers
    text = re.sub(r'[^a-z0-9_.\s]', ' ', text)
    tokens = text.split()
    # keep tokens that look like identifiers (contain _ or .)
    tokens = [
        t for t in tokens
        if len(t) > 1 and t not in STOP_WORDS
    ]
    return tokens

print(technical_tokenize('Install pgvector 0.5.1 extension in PostgreSQL 16'))
# ['pgvector', '0.5.1', 'extension', 'postgresql', '16']

Persistindo o índice BM25

Os índices BM25 devem ser persistidos em disco entre reinicializações da aplicação para evitar o custo de reindexação. Como os objetos de rank_bm25 são objetos Python simples, você pode serializá-los com pickle. Para corpora maiores, salve tanto o índice quanto a lista original de documentos, para que possa recuperar o texto após a pontuação. Nunca armazene dados confidenciais em arquivos pickle, pois eles não são seguros contra entradas não confiáveis.

import pickle

def save_bm25_index(bm25, documents: list[str], path: str):
    with open(path, 'wb') as f:
        pickle.dump({'bm25': bm25, 'documents': documents}, f)
    print(f'Index saved to {path}')

def load_bm25_index(path: str):
    with open(path, 'rb') as f:
        data = pickle.load(f)
    return data['bm25'], data['documents']

save_bm25_index(bm25, docs, '/tmp/bm25_index.pkl')
bm25_loaded, docs_loaded = load_bm25_index('/tmp/bm25_index.pkl')

Atualizações incrementais do índice

BM25 não oferece suporte a atualizações incrementais — você precisa recriar todo o índice quando novos documentos chegarem. Para corpora que mudam com frequência, atualizações em lote são a solução prática: reúna novos documentos durante um intervalo de tempo e, em seguida, recrie o índice fora do caminho crítico. Use um padrão de buffer duplo, no qual um índice atende ao tráfego ativo enquanto o outro é recriado; depois, troque-os atomicamente.

import threading

class SwappableBM25Index:
    def __init__(self):
        self._index = None
        self._docs = []
        self._lock = threading.RLock()

    def rebuild(self, new_docs: list[str]):
        tokenized = [tokenize(d) for d in new_docs]
        new_index = BM25Okapi(tokenized)
        with self._lock:
            self._index = new_index
            self._docs = new_docs
        print(f'Index rebuilt with {len(new_docs)} documents')

    def search(self, query: str, top_k: int = 5):
        with self._lock:
            return bm25_search(self._index, self._docs, query, top_k)

Integrando BM25 ao LangChain

LangChain fornece um invólucro BM25Retriever que integra a busca BM25 a uma interface padrão de recuperação. Isso permite usar BM25 como um componente plugável em cadeias LCEL e combiná-lo com recuperadores vetoriais usando EnsembleRetriever. O parâmetro de pesos controla quanta influência BM25, em comparação com o recuperador denso, exerce sobre a classificação final.

from langchain_community.retrievers import BM25Retriever
from langchain.retrievers import EnsembleRetriever
from langchain_core.documents import Document

langchain_docs = [Document(page_content=d) for d in docs]

bm25_retriever = BM25Retriever.from_documents(langchain_docs)
bm25_retriever.k = 5

# Combine with a vector retriever (assuming vector_retriever is already defined)
# ensemble = EnsembleRetriever(
#     retrievers=[bm25_retriever, vector_retriever],
#     weights=[0.4, 0.6],  # 40% BM25, 60% dense
# )

results = bm25_retriever.invoke('sparse keyword search')
for doc in results:
    print(doc.page_content[:80])

Avaliando a qualidade do BM25

Para medir a qualidade da recuperação com BM25, crie um conjunto de dados de referência com consultas associadas aos documentos relevantes conhecidos. Calcule a taxa de acerto em K (se o documento relevante aparece entre os K melhores resultados) e o MRR (classificação recíproca média). Compare esses números com a recuperação densa no mesmo conjunto de testes para decidir a ponderação ideal no seu sistema híbrido.

def hit_rate_at_k(bm25, documents, queries, relevant_docs, k=5):
    hits = 0
    for query, relevant in zip(queries, relevant_docs):
        results = bm25_search(bm25, documents, query, top_k=k)
        retrieved = [r['document'] for r in results]
        if relevant in retrieved:
            hits += 1
    return hits / len(queries)

# Example evaluation
test_queries = ['BM25 algorithm', 'hybrid search systems']
test_relevant = [
    'BM25 is a sparse keyword retrieval algorithm used in search engines',
    'Hybrid search combines dense and sparse retrieval for better recall',
]
hit_rate = hit_rate_at_k(bm25, docs, test_queries, test_relevant, k=3)
print(f'Hit rate @3: {hit_rate:.2%}')

BM25 em produção em grande escala

Para corpora com milhões de documentos, rank_bm25 em Python puro será lento demais. O BM25 em escala de produção está disponível no Elasticsearch e no OpenSearch (ambos usam BM25 como função de pontuação padrão), no Typesense e no modo de vetor esparso do Qdrant. Esses sistemas mantêm índices invertidos em disco, oferecem suporte a atualizações parciais e processam consultas simultâneas sem recriar todo o índice.

Verificação rápida

Teste sua compreensão da implementação da busca por palavras-chave com BM25 apresentada nesta lição.

Recapitulação da lição

Nesta lição, você aprendeu que rank_bm25 fornece um índice BM25 em memória que exige entrada tokenizada; que a tokenização consistente entre a indexação e as consultas é essencial para obter pontuações precisas; e que os hiperparâmetros k1 e b podem ser ajustados à distribuição específica dos comprimentos dos seus documentos. Em produção e em grande escala, use Elasticsearch ou OpenSearch em vez de BM25 em memória. A seguir, implementaremos a fusão de classificações recíprocas para mesclar resultados de BM25 e da recuperação densa.

Grátis para começar

Aprenda Python com um tutor de IA — grátis

Escreva e execute código real no seu navegador, obtenha ajuda instantânea de um tutor de IA 24/7 e continue de onde parou na web ou no app.

Cursos
30
Aulas
120

Perguntas Frequentes

A aula “Implementando busca por palavras-chave com BM25” é grátis?

Sim — o texto completo de “Implementando busca por palavras-chave com BM25” é grátis para ler aqui na web. Para praticá-la interativamente (um editor de código integrado e um tutor de IA 24/7) e desbloquear o restante do curso de AI Engineering Academy, atualize para CoddyKit PRO. O curso de AI Engineering Academy inclui 4 aulas no total.

O que vou aprender em “Implementando busca por palavras-chave com BM25”?

Configure o BM25 usando rank_bm25 em Python, indexe seu corpus de documentos e execute buscas por palavras-chave que lidem de forma confiável com termos exatos, jargão técnico e nomes de produtos. Você pratica AI Engineering Academy com código prático que executa diretamente no navegador, e um tutor de IA 24/7 responde suas dúvidas enquanto trabalha na aula.

Preciso ter experiência prévia para começar AI Engineering Academy?

Nenhuma experiência prévia é necessária. AI Engineering Academy no CoddyKit é estruturado para alunos iniciantes até avançados, então você pode começar aqui ou desde o início e aprender no seu ritmo. Esta é a aula 2 de 4.

Quanto tempo leva a aula “Implementando busca por palavras-chave com BM25”?

A maioria das aulas CoddyKit leva cerca de 5–10 minutos. Cada uma é compacta e interativa, então você faz progresso constante e retoma exatamente de onde parou entre web e app.

Posso escrever e executar código nesta aula de AI Engineering Academy?

Sim. Cada aula de AI Engineering Academy inclui um editor de código integrado, então você escreve e executa código real direto no navegador e recebe feedback de IA instantaneamente — nenhuma configuração local necessária.

Todas as aulas deste curso

  1. Recuperação densa versus esparsa: compromissos
  2. Implementando busca por palavras-chave com BM25
  3. Fusão recíproca de posições para combinar pontuações
  4. Busca híbrida no Pinecone e no pgvector
← Voltar para AI Engineering Academy