Implementación de búsquedas por palabras clave con BM25
Configure BM25 con rank_bm25 en Python, indexe su corpus de documentos y ejecute búsquedas por palabras clave que gestionen de forma fiable términos exactos, jerga técnica y nombres de productos.
Implementación de búsquedas por palabras clave con BM25 es una lección gratuita de AI Engineering Academy en CoddyKit. Esta es la lección 2 de 4. Puedes leer la lección completa abajo gratuitamente — luego la practicas en el navegador con un editor de código integrado y un tutor de IA 24/7. Forma parte de la ruta de aprendizaje de AI Engineering Academy, y tu progreso se sincroniza en la web y la app de CoddyKit. El curso de AI Engineering Academy incluye 4 lecciones en total.
Instalación de rank_bm25
rank_bm25 es una biblioteca ligera de Python que proporciona las variantes BM25Okapi, BM25L y BM25Plus del algoritmo BM25. No requiere servicios externos, se ejecuta completamente en memoria y puede indexar miles de documentos en cuestión de segundos con hardware convencional. Instálela con pip install rank-bm25 y podrá crear una búsqueda por palabras clave sin configurar ninguna infraestructura.
# 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')Tokenización: el primer paso fundamental
BM25 opera sobre listas de tokens, no sobre cadenas sin procesar. La calidad de la tokenización afecta directamente a la calidad de la recuperación. Una separación simple por espacios no elimina la puntuación, no aplica stemming ni elimina palabras vacías. En sistemas de producción, utilice un tokenizador adecuado que convierta el texto a minúsculas, elimine la puntuación y las palabras vacías y, opcionalmente, aplique stemming para hacer coincidir variantes morfológicas como 'run', 'runs' y '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']Creación del índice BM25
Crear un índice BM25 es una operación offline que se realiza una sola vez. Se pasa el corpus tokenizado a BM25Okapi, que calcula las frecuencias inversas de documento de todos los términos y almacena las longitudes de los documentos para la normalización. El índice es ligero: ocupa unos pocos megabytes incluso para decenas de miles de documentos. Debe reconstruirlo cada vez que se añadan documentos nuevos al 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')Ejecución de una búsqueda BM25
Para buscar, tokenice la consulta utilizando el mismo tokenizador que el índice; una tokenización incoherente es una causa frecuente de una recuperación deficiente. Llame a get_scores para obtener las puntuaciones de relevancia de todos los documentos, o a get_top_n para recuperar directamente los N resultados principales. Utilice siempre la misma canalización de preprocesamiento tanto para indexar como para consultar.
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]}")Ajuste de hiperparámetros de BM25
BM25Okapi acepta dos hiperparámetros: k1 controla la saturación de la frecuencia de términos (los valores más altos permiten que los términos de alta frecuencia obtengan puntuaciones mayores) y b controla la normalización de la longitud de los documentos (1.0 = normalización completa, 0.0 = sin normalización). Los valores predeterminados de k1=1.5, b=0.75 funcionan bien con texto en prosa. Para fragmentos cortos (de menos de 100 palabras), pruebe valores de b más bajos, como 0.3, para reducir el sesgo por longitud.
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 productionGestión de jerga técnica y tokens de código
En bases de código y documentación técnica, su tokenizador debe conservar los tokens técnicos en lugar de aplicarles stemming de forma agresiva. Términos como BM25Okapi, pgvector y LLM deben permanecer intactos. Un tokenizador híbrido que omita el stemming para los tokens que coincidan con patrones como acrónimos en mayúsculas, identificadores CamelCase o snake_case producirá mejores resultados en búsquedas dirigidas a desarrolladores.
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']Persistencia del índice BM25
Los índices BM25 deben persistirse en disco entre reinicios de la aplicación para evitar el coste de volver a indexarlos. Como los objetos de rank_bm25 son Python puro, puede serializarlos con pickle. Para corpus más grandes, guarde tanto el índice como la lista de documentos original, de modo que pueda recuperar el texto después de calcular las puntuaciones. Nunca almacene datos confidenciales en archivos pickle, ya que no son seguros frente a entradas que no sean de confianza.
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')Actualizaciones incrementales del índice
BM25 no admite actualizaciones incrementales: debe reconstruir todo el índice cuando llegan documentos nuevos. Para corpus que cambian con frecuencia, la solución práctica son las actualizaciones por lotes: recopile documentos nuevos durante un intervalo de tiempo y reconstruya después el índice fuera de la ruta crítica. Utilice un patrón de doble búfer, en el que un índice atienda el tráfico en producción mientras se reconstruye el otro; después, intercámbielos atómicamente.
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)Integración de BM25 con LangChain
LangChain proporciona un envoltorio BM25Retriever que integra la búsqueda BM25 en una interfaz de recuperador estándar. Esto le permite utilizar BM25 como componente intercambiable dentro de cadenas LCEL y combinarlo con recuperadores vectoriales mediante EnsembleRetriever. El parámetro weights controla cuánto influye BM25 frente al recuperador denso en la clasificación 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])Evaluación de la calidad de BM25
Para medir la calidad de recuperación de BM25, cree un conjunto de datos de referencia con consultas emparejadas con sus documentos relevantes conocidos. Calcule la tasa de aciertos en K (si el documento relevante aparece entre los K primeros resultados) y el MRR (rango recíproco medio). Compare estas cifras con las de la recuperación densa en el mismo conjunto de prueba para decidir la ponderación óptima de su 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 en producción a gran escala
Para corpus con millones de documentos, rank_bm25 en Python puro será demasiado lento. BM25 a escala de producción está disponible en Elasticsearch y OpenSearch (ambos utilizan BM25 como función de puntuación predeterminada), Typesense y el modo de vectores dispersos de Qdrant. Estos sistemas mantienen índices invertidos en disco, admiten actualizaciones parciales y gestionan consultas simultáneas sin tener que reconstruir todo el índice.
Comprobación rápida
Compruebe su comprensión de la implementación de búsquedas por palabras clave con BM25 explicada en esta lección.
Resumen de la lección
En esta lección ha aprendido que rank_bm25 proporciona un índice BM25 en memoria que requiere entradas tokenizadas; que la tokenización coherente entre la indexación y las consultas es esencial para calcular puntuaciones precisas; y que los hiperparámetros k1 y b pueden ajustarse a la distribución específica de longitudes de sus documentos. Para producción a gran escala, utilice Elasticsearch u OpenSearch en lugar de BM25 en memoria. A continuación implementaremos la fusión de rangos recíprocos para combinar resultados de BM25 y de recuperación densa.
Aprende Python con un tutor de IA — gratis
Escribe y ejecuta código real en tu navegador, obtén ayuda instantánea de un tutor de IA disponible 24/7 y continúa donde lo dejaste en la web o en la aplicación.
- Cursos
- 30
- Lecciones
- 120
Preguntas frecuentes
¿La lección «Implementación de búsquedas por palabras clave con BM25» es gratis?
Sí — el texto completo de «Implementación de búsquedas por palabras clave con BM25» es gratis para leer aquí en la web. Para practicarla de forma interactiva (editor de código integrado y tutor de IA 24/7) y desbloquear el resto del curso de AI Engineering Academy, actualiza a CoddyKit PRO. El curso de AI Engineering Academy incluye 4 lecciones en total.
¿Qué aprenderé en «Implementación de búsquedas por palabras clave con BM25»?
Configure BM25 con rank_bm25 en Python, indexe su corpus de documentos y ejecute búsquedas por palabras clave que gestionen de forma fiable términos exactos, jerga técnica y nombres de productos. Practicas AI Engineering Academy con código real que ejecutas directamente en el navegador, y un tutor de IA 24/7 responde tus preguntas mientras trabajas en la lección.
¿Necesito experiencia previa para empezar AI Engineering Academy?
No se requiere experiencia previa. AI Engineering Academy en CoddyKit está estructurado para principiantes hasta estudiantes avanzados, así que puedes empezar aquí o desde el inicio y avanzar a tu ritmo. Esta es la lección 2 de 4.
¿Cuánto tiempo toma la lección «Implementación de búsquedas por palabras clave con BM25»?
La mayoría de las lecciones de CoddyKit toman alrededor de 5–10 minutos. Cada una es compacta e interactiva, así que avanzas constantemente y retomas exactamente por donde dejaste en la web y la app.
¿Puedo escribir y ejecutar código en esta lección de AI Engineering Academy?
Sí. Cada lección de AI Engineering Academy incluye un editor de código integrado, así que escribes y ejecutas código real directamente en tu navegador y obtienes retroalimentación instantánea de IA — sin configuración local necesaria.
Todas las lecciones de este curso
- Recuperación densa frente a dispersa: ventajas y desventajas
- Implementación de búsquedas por palabras clave con BM25
- Fusión de rangos recíprocos para combinar puntuaciones
- Búsqueda híbrida en Pinecone y pgvector