Stack de ranking para búsquedas: BM25, embeddings y reranking

Traducción automática Este artículo se tradujo automáticamente a partir de la versión original en inglés.

Las búsquedas deben satisfacer tanto la intención exacta como la semántica. Una consulta como «wireless headphones» debería coincidir con esas palabras, pero el orden final también puede depender de la calidad del producto, las preferencias del usuario y la disponibilidad. Ningún método de ranking gestiona bien todas esas señales por sí solo.

En este artículo se construye el stack etapa a etapa: recuperación con BM25, embeddings densos, Reciprocal Rank Fusion, reranking con cross-encoder y, por último, ranking listwise con LLM. Un repositorio de demo complementario contiene código ejecutable para estas etapas sobre una muestra de los datos de búsqueda de productos de Amazon ESCI.

Para consultar una guía breve sobre la selección de etapas, véase BM25 vs Embeddings vs Rerankers.


Elige las etapas según el modo de fallo

El stack de producción es un embudo, pero el embudo adecuado depende de la consulta y de la superficie de negocio.

Caso de usoStack inicial candidatoValidar
Búsqueda de productosBM25 + recuperación densa + RRF + cross-encoderRecall de atributos, sustituciones, latencia y restricciones de negocio
Búsqueda en documentaciónRecuperación híbrida + cross-encoderIdentificadores exactos, preguntas semánticas y filtros de versión
Desvío de consultas de soporteRecuperación híbrida + comprobaciones de citasRecall de recuperación, grounding y abstención
Marketplace o listadosFiltros léxicos + recuperación densa + reranker de negocioDisponibilidad, frescura, políticas y diversidad de vendedores
Corpus interno pequeñoBM25 como baseline y después un rerankerSi el desajuste de vocabulario justifica un índice denso
Búsqueda jurídica o médica críticaRecuperación centrada en recall más revisión expertaCobertura, procedencia y abstención calibrada

Empieza con BM25 como baseline. Añade recuperación densa cuando el desajuste de vocabulario perjudique al recall. Añade un cross-encoder cuando la primera página contenga los candidatos correctos, pero en el orden equivocado. Añade un LLM solo cuando puedas asumir la latencia y evaluar las decisiones de ranking.

Cómo hemos llegado hasta aquí

El stack se entiende mejor como tres capas. La recuperación léxica encuentra términos exactos, la recuperación densa cubre las lagunas de vocabulario y los rerankers comparan en detalle los candidatos más prometedores.

BM25 y recuperación léxica

Durante décadas, BM25 fue la opción predeterminada. Es un modelo probabilístico que puntúa los documentos según la frecuencia de los términos de la consulta en el documento, normalizada por la longitud del documento y la frecuencia inversa de documento (IDF).

BM25 es especialmente eficaz cuando los términos literales expresan la intención: códigos de error, SKU de productos, nombres e identificadores de API. Su principal limitación es el desajuste de vocabulario. Una consulta como «cheap laptop» puede no recuperar un documento sobre un «budget notebook computer» cuando el texto indexado no proporciona ningún puente entre ambas expresiones.

Aun así, BM25 es un baseline sólido. La tabla de clasificación de BEIR informa de un nDCG@10 medio de 0,429 en 18 datasets para su ejecución BM25 multifield. La reproducción de Pyserini utiliza un índice multifield de Lucene con contents=1.0 y title=1.0, consultado con --bm25. No se trata de la implementación rank_bm25, tokenizada simplemente por espacios, que se usa en esta demo. BM25 también supera a algunos modelos neuronales en tareas de recuperación argumentativa como Touche-2020.

Recuperación densa y embeddings

Los encoders de estilo BERT hicieron práctica la recuperación densa. Mapean las consultas y los documentos a un espacio vectorial compartido y después ordenan los candidatos mediante una función de similitud, como la similitud coseno o el producto escalar.

La arquitectura bi-encoder (o «two-tower») procesa la consulta y el documento de forma independiente mediante torres encoder separadas y produce embeddings de longitud fija. Los vectores de los documentos pueden calcularse previamente e indexarse offline, para después recuperarlos rápidamente mediante algoritmos Approximate Nearest Neighbor (ANN). Así, «cheap laptop» y «budget notebook» quedan próximos en el espacio vectorial.

Algunos bi-encoders utilizan una arquitectura Siamese, como en Sentence-BERT, donde ambos lados comparten pesos. Otros emplean torres independientes para consultas y documentos. El pooling, el tamaño del vector, la función de similitud y el objetivo de entrenamiento son decisiones del modelo, no propiedades de todos los recuperadores densos.

Estos modelos se entrenan mediante aprendizaje contrastivo, normalmente con la pérdida InfoNCE. Dado un batch de pares (query, positive_document), el objetivo maximiza sim(query, positive_doc) y minimiza sim(query, negative_docs). Los negativos proceden de los positivos de otras consultas del mismo batch (in-batch negatives). Un parámetro de temperatura τ\tau controla la separación que debe establecer el modelo entre ambos.

Los datos de entrenamiento suelen importar más que la dimensión del embedding. Los modelos de recuperación aprenden a partir de pares consulta-positivo y de hard negatives cuidadosamente seleccionados: documentos plausibles, pero no relevantes. La sección posterior sobre entrenamiento muestra cómo SimANS evita tanto los negativos triviales como los falsos negativos probables.

El coste es el cuello de botella de la representación. Los bi-encoders comprimen toda la riqueza semántica en un único vector de tamaño fijo, por lo que a menudo no capturan las interacciones de grano fino entre términos concretos de la consulta y contenido específico del documento.

Cross-encoders y LLMs

Los cross-encoders (Nogueira & Cho, 2019) introducen la consulta y el documento juntos en un Transformer, como una secuencia concatenada ([CLS] Query [SEP] Document), de modo que cada token de la consulta puede atender a cada token del documento mediante self-attention completa. Esta interacción profunda capta matices que la codificación independiente no detecta.

El reranking con LLM utiliza un modelo al que se le proporciona un prompt para comparar varios candidatos a la vez. RankGPT mostró buenos resultados zero-shot de tipo listwise con GPT-4 en los benchmarks evaluados, pero la estabilidad de las salidas, el coste y el ajuste al dominio requieren pruebas independientes.

Estos scores no pueden calcularse previamente para consultas arbitrarias, por lo que el reranking se sitúa después de la recuperación. Esta asimetría de costes motiva el embudo multietapa.


El embudo multietapa

Ejecutar un cross-encoder o un LLM costoso sobre millones de documentos no es viable, así que los stacks de búsqueda modernos utilizan un embudo. Cada etapa reduce el conjunto de candidatos mientras aumenta la complejidad del modelo.

Un recuperador barato tampoco ofrece suficiente precisión final, por lo que el embudo utiliza cada modelo solo donde su coste resulta razonable.

Embudo de ranking multietapaEmbudo de ranking multietapa

EtapaEscala de entradaObjetivo principalMétodos habitualesMedición de salida
RecuperaciónCorpus o índiceRecall de candidatosBM25, bi-encodersRecall en el cutoff de candidatos
Pre-rankingConjunto grande de candidatosFiltrado baratoModelos ligeros, reglasRecall conservado por milisegundo
Ranking completoShortlistCalidad de las primeras posicionesCross-encoders, LLMsNDCG/MRR, latencia y coste
BlendingListas finales ordenadas o slotsRestricciones y mezclaReglas, ranking multiobjetivoPolíticas, diversidad y guardrails de negocio

La recuperación fija el techo y el reranking optimiza dentro de él. Si un documento relevante no supera la recuperación, ningún modelo posterior puede recuperarlo.


La demo: un pipeline de cinco etapas

Para hacerlo concreto, he creado una demo de search-ranking-stack que ejecuta un pipeline de cinco etapas sobre el benchmark de búsqueda de productos Amazon ESCI. Cada etapa se mide de forma independiente para que puedas ver de dónde proceden realmente las mejoras.

Arquitectura del pipeline de la demoArquitectura del pipeline de la demo

El pipeline:

  1. Recuperación dispersa con BM25 — baseline léxico (rank_bm25)
  2. Recuperación con bi-encoder denso — generación semántica de candidatos (all-MiniLM-L6-v2)
  3. Fusión híbrida con RRF — fusión basada en rangos de los resultados dispersos y densos
  4. Reranking con cross-encoder — scores de relevancia por pares (ms-marco-MiniLM-L-12-v2)
  5. Reranking listwise con LLM — comparación mediante prompt de la shortlist final (Ollama, API o modelo local)

Los pasos 1—3 constituyen la etapa de recuperación del embudo (maximizar el recall); los pasos 4—5 son la etapa de ranking completo (maximizar la precisión). La demo omite el pre-ranking y el blending. Con unos 8.500 documentos, puedes permitirte enviar todos los resultados híbridos directamente al reranking.

Inicio rápido

git clone https://github.com/slavadubrov/search-ranking-stack.git
cd search-ranking-stack
uv sync

# Download and sample ESCI dataset (~2.5GB download, ~5MB sample)
uv run download-data

# Run the full pipeline (without LLM reranking)
uv run run-all

# Run with LLM reranking via Ollama
uv run run-all --llm-mode ollama

Dataset y muestreo: Amazon ESCI

La demo utiliza el Amazon Shopping Queries Dataset (ESCI) de la KDD Cup 2022, un benchmark real de búsqueda de productos con etiquetas de relevancia graduadas en cuatro niveles:

EtiquetaGananciaSignificadoEjemplo (consulta: «wireless headphones»)
Exact (E)3Satisface todos los requisitosSony WH-1000XM5 Wireless Headphones
Substitute (S)2Alternativa funcionalWired headphones with Bluetooth adapter
Complement (C)1Artículo relacionado y útilFunda para transportar auriculares
Irrelevant (I)0Sin relación significativaCable de carga USB

La relevancia graduada es importante porque permite utilizar NDCG (Normalized Discounted Cumulative Gain), que distingue un ranking «perfecto» de uno «meramente adecuado». Las métricas binarias puntúan ambos de la misma manera.

He utilizado la muestra small_version de la demo: unas 500 consultas, 8.500 productos y 12.000 juicios. Su descargador lee el único split train de tasksource/esci, filtra el locale inglés us y small_version == 1 y, a continuación, utiliza la seed 42 para muestrear hasta 500 IDs de consulta únicos sin reemplazo. El corpus contiene los productos únicos de las filas de juicios seleccionadas. No se trata de un split held-out ni de un corpus independiente. La muestra es lo bastante pequeña para ejecutarse en un portátil, pero demasiado pequeña y específica del dominio para establecer un ranking de producción. Úsala para reproducir las etapas e inspeccionar los modos de fallo; utiliza consultas representativas held-out para tomar decisiones de despliegue.


Recuperación: búsqueda híbrida

El objetivo de la capa de recuperación es maximizar el recall: introducir tantos documentos relevantes como sea posible en el conjunto de candidatos.

BM25: el baseline léxico

BM25 puntúa los documentos según el solapamiento de términos con la consulta, con saturación de la frecuencia de términos y normalización de la longitud del documento:

BM25(q,d)=tqIDF(t)tf(t,d)(k1+1)tf(t,d)+k1(1b+bd/avgdl)\text{BM25}(q, d) = \sum_{t \in q} \text{IDF}(t) \cdot \frac{tf(t,d) \cdot (k_1 + 1)}{tf(t,d) + k_1 \cdot (1 - b + b \cdot |d|/\text{avgdl})}

Donde IDF(t)\text{IDF}(t) es la frecuencia inversa de documento del término tt, tf(t,d)tf(t,d) es la frecuencia del término en el documento dd, d|d| es la longitud del documento y avgdl\text{avgdl} es la longitud media de los documentos del corpus. Importan dos parámetros: k1k_1 (normalmente 1,2—2,0) controla la saturación de TF —la rapidez con la que los términos repetidos dejan de aportar valor— y bb (normalmente 0,75) controla la normalización de la longitud del documento.

La implementación es breve. Tokenización simple por espacios con rank_bm25:

# src/search_ranking_stack/stages/s01_bm25.py

from rank_bm25 import BM25Okapi

def run_bm25(data: ESCIData, top_k: int = 100):
    doc_ids = list(data.corpus.keys())
    tokenized_corpus = [text.lower().split() for text in data.corpus.values()]

    bm25 = BM25Okapi(tokenized_corpus)

    results = {}
    for query_id, query_text in data.queries.items():
        scores = bm25.get_scores(query_text.lower().split())
        top_indices = np.argsort(scores)[::-1][:top_k]
        results[query_id] = {doc_ids[idx]: float(scores[idx]) for idx in top_indices}

    return results

BM25 alcanza un Recall@100 de 0,741: el 74 % de los productos relevantes aparece en algún punto del top 100. No está mal para un método puramente léxico, pero el 26 % de los artículos relevantes resulta invisible para todas las etapas posteriores.

Recuperación con bi-encoder denso

El bi-encoder asigna consultas y documentos de forma independiente a un espacio de embeddings compartido:

# src/search_ranking_stack/stages/s02_dense.py

from sentence_transformers import SentenceTransformer

model = SentenceTransformer("sentence-transformers/all-MiniLM-L6-v2")

# Encode corpus once, cache to disk
corpus_embeddings = model.encode(
    doc_texts,
    batch_size=128,
    normalize_embeddings=True,  # Cosine sim = dot product
    convert_to_numpy=True,
)

# At query time: encode query, compute dot product
query_embeddings = model.encode(query_texts, normalize_embeddings=True)
similarity_matrix = np.dot(query_embeddings, corpus_embeddings.T)

Con embeddings normalizados, la similitud coseno se reduce a un producto escalar. La demo calcula la matriz completa consulta-por-corpus porque 8.500 documentos caben cómodamente en memoria; un corpus de producción normalmente utilizaría un índice de approximate-nearest-neighbor. En esta muestra, all-MiniLM-L6-v2 eleva el Recall@100 de 0,741 a 0,825.

Cómo aprenden los bi-encoders buenas representaciones

El entrenamiento de bi-encoders suele desarrollarse en dos fases. Primero, el modelo se preentrena con datasets de Natural Language Inference (NLI) y Semantic Textual Similarity (STS), que le enseñan comprensión semántica de propósito general. Es ahí donde el modelo aprende que «a cat sits on a mat» y «a feline rests on a rug» deberían tener embeddings similares. Después se ajusta con fine-tuning sobre datos específicos de recuperación, como MS MARCO, donde aprende que una consulta de búsqueda y su pasaje relevante deben estar más próximos que la consulta y los pasajes irrelevantes.

La segunda fase depende del hard negative mining. Los negativos aleatorios —por ejemplo, un documento sobre cocina emparejado con una consulta sobre auriculares— son trivialmente fáciles de distinguir, así que el modelo aprende poco con ellos. En su lugar, se utiliza el propio modelo actual para encontrar documentos que puntúa alto, pero que en realidad no son relevantes.

El enfoque SimANS (Simple Ambiguous Negatives Sampling) formaliza esta idea. Se ordenan todos los documentos con el bi-encoder actual y se excluyen los negativos fáciles, que ocupan posiciones demasiado bajas para que el modelo aprenda de ellos, y los posibles falsos negativos, que ocupan posiciones tan altas que podrían ser relevantes, aunque no estén etiquetados. Lo que queda en la zona intermedia contiene la mayor señal de entrenamiento.

# What a training triplet looks like after hard negative mining
training_triplet = {
    "query": "wireless noise canceling headphones",
    "positive": "Sony WH-1000XM5 Wireless Noise Cancelling Headphones",
    "negative": "Sony headphone replacement ear pads",  # Hard negative: same brand, related product, but wrong intent
}
# The bi-encoder must learn that "ear pads" is NOT what the user wants,
# even though it shares many tokens with the positive document.

La función de pérdida contrastiva (InfoNCE) conecta todos estos elementos. Para cada consulta qq con un documento positivo d+d^+ y un conjunto de documentos negativos {d1,,dn}\{d^-_1, \ldots, d^-_n\}:

L=logesim(q,d+)/τesim(q,d+)/τ+i=1nesim(q,di)/τ\mathcal{L} = -\log \frac{e^{\text{sim}(q, d^+) / \tau}}{e^{\text{sim}(q, d^+) / \tau} + \sum_{i=1}^{n} e^{\text{sim}(q, d^-_i) / \tau}}

Donde sim(q,d)\text{sim}(q, d) es la similitud coseno entre los embeddings de la consulta y del documento, y τ\tau es el parámetro de temperatura (normalmente 0,05—0,1). Los valores más bajos hacen que la pérdida sea más sensible a los hard negatives. En esencia, es una entropía cruzada softmax: aumenta la similitud del par positivo con respecto a todos los negativos. Cuando τ\tau es pequeño, incluso pequeñas diferencias de similitud producen gradientes grandes, lo que obliga al modelo a establecer distinciones más precisas.

Pipeline de entrenamiento del bi-encoderPipeline de entrenamiento del bi-encoder

Servir embeddings de bi-encoder a escala

La ventaja arquitectónica de un bi-encoder es la separación offline/online. Los embeddings de los documentos se calculan al indexar y se almacenan en un índice vectorial. En tiempo de consulta, el sistema codifica la consulta y busca en esos vectores almacenados. La latencia depende del encoder, el hardware, el índice, los filtros y el objetivo de recall, así que conviene perfilar ambos pasos por separado.

En la demo, las cifras son moderadas: 8.500 documentos ×\times 384 dimensiones ×\times 4 bytes por float = unos 13 MB de embeddings. A escala de producción dejan de ser moderadas: 1.000 millones de documentos con embeddings de 768 dimensiones requieren unos 3 TiB de almacenamiento. Ahí entran la cuantización (comprimir floats de 32 bits en enteros de 8 bits), la cuantización de producto (descomponer los vectores en subespacios) y los índices respaldados por SSD, como DiskANN. La sección sobre indexación de vectores densos cubre los algoritmos de indexación.

Pipeline de serving del bi-encoderPipeline de serving del bi-encoder

Por qué probar la recuperación híbrida

Los dos métodos suelen fallar de forma diferente. BM25 se adapta bien a nombres propios, SKU de productos y códigos de error. La recuperación densa puede resolver desajustes de vocabulario como «cheap laptop» frente a «budget notebook computer». Que la fusión ayude depende de la frecuencia de estos casos complementarios en el conjunto de consultas objetivo.

Un experimento habitual siguiente es la búsqueda híbrida: ejecutar ambos métodos de recuperación y fusionar después sus listas ordenadas.

Reciprocal Rank Fusion (RRF)

BM25 y la recuperación densa producen scores con significados y escalas diferentes. Por tanto, una combinación lineal requiere calibración y validación cada vez que cambian los recuperadores o el corpus.

Búsqueda híbrida con RRFBúsqueda híbrida con RRF

Reciprocal Rank Fusion (Cormack et al., 2009) descarta por completo los scores brutos y utiliza únicamente la posición en el ranking:

RRF(d)=rRankings1k+rank(d,r)\text{RRF}(d) = \sum_{r \in \text{Rankings}} \frac{1}{k + \text{rank}(d, r)}

Aquí, kk es una constante de suavizado; 60 es un valor inicial habitual. RRF recompensa los elementos que ocupan posiciones altas en varias listas de entrada sin comparar sus scores brutos. Evita calibrar la escala de scores, pero los cutoffs de recuperación, los pesos y kk siguen requiriendo evaluación.

La implementación:

# src/search_ranking_stack/stages/s03_hybrid_rrf.py

def reciprocal_rank_fusion(ranked_lists, k=60, top_k=100):
    fused_results = {}

    for query_id in all_query_ids:
        rrf_scores = defaultdict(float)

        for results in ranked_lists:
            sorted_docs = sorted(results[query_id].items(),
                                 key=lambda x: x[1], reverse=True)

            for rank, (doc_id, _score) in enumerate(sorted_docs, start=1):
                rrf_scores[doc_id] += 1.0 / (k + rank)

        sorted_rrf = sorted(rrf_scores.items(),
                            key=lambda x: x[1], reverse=True)[:top_k]
        fused_results[query_id] = dict(sorted_rrf)

    return fused_results

El RRF híbrido alcanza un Recall@100 de 0,842 y un NDCG@10 de 0,628, superando a BM25 (0,585) y Dense (0,611) por separado. Basta con que los documentos ocupen buenas posiciones en uno de los métodos para sobrevivir a la fusión.


Reranking con cross-encoder

Con 100 candidatos híbridos por consulta, puedes permitirte un modelo más costoso. El cross-encoder procesa la consulta y el documento juntos mediante un único Transformer, con cross-attention completa entre todos los tokens.

Bi-Encoder frente a Cross-EncoderBi-Encoder frente a Cross-Encoder

Interacción a nivel de token

La diferencia está en la matriz de atención. En un bi-encoder, la atención es diagonal por bloques: los tokens de la consulta solo atienden a otros tokens de la consulta, y los tokens del documento solo atienden a otros tokens del documento. Las dos representaciones nunca se encuentran a nivel de token; solo interactúan al final mediante un producto escalar. Un cross-encoder calcula la matriz de atención completa, donde cada token de la consulta atiende a cada token del documento y viceversa. Esa cross-attention es la que permite una interacción profunda a nivel de token.

Arquitectura de atención del Cross-EncoderArquitectura de atención del Cross-Encoder

En un bi-encoder, la consulta «apple» se codifica antes de ver cualquier documento. Un cross-encoder ve juntos la consulta y el candidato, por lo que puede utilizar su relación a nivel de token. Esto puede ayudar en casos como:

  • Negación: «headphones that are not wireless». Un embedding pooled puede dar un peso insuficiente a la negación, mientras que la codificación conjunta proporciona al modelo una interacción directa entre consulta y documento. Es una hipótesis que debe verificarse con una muestra específica, no una garantía.
  • Cualificación: «laptop under $500». La codificación conjunta puede relacionar la restricción con un precio presente en el texto del producto, aunque los filtros de precio estructurados son más seguros cuando el campo está disponible.

La entrada del cross-encoder se formatea como [CLS] query tokens [SEP] document tokens [SEP]. [CLS] es un token de clasificación cuya representación oculta final se introduce en una cabeza lineal para producir un único score de relevancia. Los embeddings de segmento distinguen los tokens de la consulta de los del documento, y [SEP] marca el límite entre ambos segmentos.

Cómo se entrenan los cross-encoders

Los cross-encoders pueden aprender a partir de ejemplos (query, document, relevance_label) con objetivos pointwise, pairwise o listwise. El ejemplo pointwise siguiente utiliza una única etiqueta de relevancia; no es el único diseño de entrenamiento posible.

# Cross-encoder training data format
training_example = {
    "query": "wireless headphones",
    "document": "Sony WH-1000XM5 Wireless Headphones",
    "label": 1.0,  # Relevant
}
# Forward pass: [CLS] hidden state → Linear layer → sigmoid → score
# Loss: binary cross-entropy between predicted score and label

Un clasificador habitual transforma la representación [CLS] final en un score. Las etiquetas binarias pueden utilizar binary cross-entropy; la relevancia graduada puede emplear pérdidas de regresión, ordinales, pairwise o listwise. Elige basándote en métricas de ranking held-out, sin asumir que un objetivo es universalmente mejor.

El hard negative mining es aún más importante en los cross-encoders que en los bi-encoders. Los cross-encoders son costosos de entrenar: cada ejemplo de entrenamiento necesita un forward pass completo sobre la secuencia concatenada, así que no puedes desperdiciar cómputo en negativos trivialmente fáciles. La receta práctica es utilizar un bi-encoder para recuperar los candidatos top-K de cada consulta de entrenamiento y extraer hard negatives de rangos de posiciones concretos, por ejemplo, 10–100. Así se proporcionan al cross-encoder ejemplos en los que distinguir lo relevante de lo irrelevante requiere realmente una interacción profunda entre tokens.

# src/search_ranking_stack/stages/s04_cross_encoder.py

from sentence_transformers import CrossEncoder

model = CrossEncoder("cross-encoder/ms-marco-MiniLM-L-12-v2")

def run_cross_encoder(data, hybrid_results, top_k_rerank=50):
    reranked_results = {}

    for query_id, query_text in data.queries.items():
        candidates = list(hybrid_results[query_id].items())[:top_k_rerank]

        # Form (query, document) pairs for joint encoding
        pairs = []
        doc_ids = []
        for doc_id, _ in candidates:
            doc_text = data.corpus.get(doc_id, "")[:2048]
            pairs.append([query_text, doc_text])
            doc_ids.append(doc_id)

        # Score all pairs with full cross-attention
        scores = model.predict(pairs, batch_size=64)

        # Rerank by cross-encoder score
        scored_docs = sorted(zip(doc_ids, scores),
                             key=lambda x: x[1], reverse=True)
        reranked = {
            doc_id: float(score) for doc_id, score in scored_docs
        }

        # Keep original hybrid candidates at ranks 51--100 for Recall@100.
        for doc_id, score in list(hybrid_results[query_id].items())[top_k_rerank:100]:
            if doc_id not in reranked:
                reranked[doc_id] = float(score) * 0.01

        reranked_results[query_id] = reranked

    return reranked_results

En la ejecución registrada de la demo, ms-marco-MiniLM-L-12-v2 reordena 50 candidatos por consulta y eleva el NDCG@10 de 0,628 a 0,645. Mide su latencia en el hardware de despliegue; el modelo, la longitud de secuencia, el batch size y el runtime afectan al resultado.

El equilibrio entre velocidad y calidad

¿Por qué no utilizar cross-encoders para todo? Porque no puedes calcular previamente los scores. Los embeddings de documentos de un bi-encoder son independientes de la consulta, así que se calculan una vez y se almacenan. La salida de un cross-encoder depende conjuntamente de la consulta y del documento. El score de relevancia de «wireless headphones» emparejado con un producto Sony procede de la cross-attention completa entre esos tokens concretos. No puedes almacenarlo en caché ni reutilizarlo para otra consulta.

Un bi-encoder necesita una codificación de la consulta y una búsqueda vectorial sobre embeddings de documentos precalculados. Un cross-encoder puntúa cada par consulta-documento de la shortlist, con un coste que crece tanto con el número de candidatos como con la longitud de la secuencia. El batching ayuda, pero puntuar 100.000 candidatos sigue siendo un punto de operación equivocado: recupera primero y mide cuál es la shortlist más grande que cumple los objetivos de calidad y latencia.

En la demo, el Recall@100 se mantiene estable en 0,842 durante la etapa del cross-encoder. El reranking puede reordenar resultados, pero no añadir documentos. La recuperación fija el techo.


Reranking listwise con LLM

La etapa final de la demo utiliza un LLM para hacer reranking listwise. En lugar de puntuar cada documento de forma independiente, el modelo ve los 10 primeros y devuelve un orden. Inspirado en RankGPT, el prompt explicita la comparación relativa, pero también introduce límites de contexto, sesgo de posición, fallos de parseo y variabilidad entre ejecuciones.

Enfoques de reranking con LLMEnfoques de reranking con LLM

El prompt listwise

La plantilla de prompt pide al LLM que tenga en cuenta la jerarquía de relevancia de ESCI:

# src/search_ranking_stack/stages/s05_llm_rerank.py

def _create_listwise_prompt(query, documents, max_words=200):
    n = len(documents)

    doc_texts = []
    for i, (doc_id, doc_text) in enumerate(documents, start=1):
        words = doc_text.split()[:max_words]
        doc_texts.append(f"[{i}] {' '.join(words)}")

    return (
        f"I will provide you with {n} product listings, each indicated by "
        f"a numerical identifier [1] to [{n}]. Rank the products based on "
        f'their relevance to the search query: "{query}"\n\n'
        "Consider:\n"
        "- Exact matches should rank highest\n"
        "- Substitutes should rank above complements\n"
        "- Irrelevant products should rank lowest\n\n"
        f"{chr(10).join(doc_texts)}\n\n"
        "Output ONLY a comma-separated list of identifiers: [3], [1], [2], ...\n"
        "Do not explain your reasoning."
    )

Tres modos de ejecución

La demo admite tres backends para el reranking con LLM:

ModoModeloCómo se ejecuta
ollamallama3.2:3b (configurable)Local mediante Ollama API
apiclaude-haiku-4-5-20251001Anthropic API
localQwen/Qwen2.5-1.5B-InstructHuggingFace Transformers

Parseo y fallback

No se puede garantizar que las salidas del LLM sigan el schema solicitado, por lo que el parseo y una ruta de fallback son importantes:

def _parse_ranking(output: str, n: int) -> list[int] | None:
    """Parse LLM output to extract ranking order."""
    matches = re.findall(r"\[(\d+)\]", output)

    if not matches:
        return None

    positions = [int(m) - 1 for m in matches]

    # Pad with remaining positions if LLM returned partial output
    if len(positions) < n:
        seen = set(positions)
        for i in range(n):
            if i not in seen:
                positions.append(i)

    return positions[:n]

Si el parseo falla por completo, la demo utiliza el orden del cross-encoder como fallback. Un parser de producción también debería rechazar identificadores fuera de rango y duplicados, añadir los candidatos omitidos en su orden anterior, registrar el fallo y comparar la tasa de fallback con un umbral de lanzamiento.


Resultados de esta muestra ESCI

Estos resultados pre-LLM registrados utilizan small_version de la demo: el us locale inglés después del filtro small_version == 1, con hasta 500 IDs de consulta muestreados sin reemplazo mediante la seed 42 y un corpus construido a partir de sus productos evaluados. No son ni una evaluación held-out ni un corpus independiente. Los valores de MRR utilizan la regla binaria del evaluador de la demo, relevance > 0: un resultado Complement, Substitute o Exact cuenta como relevante, mientras que un resultado Irrelevant no. recip_rank evalúa cada lista de candidatos devuelta, que contiene hasta 100 candidatos; no es MRR@10.

EtapaNDCG@10MRRRecall@100Delta de NDCG
BM250,5850,8120,741
Dense Bi-Encoder0,6110,8080,825+0,026
Hybrid (RRF)0,6280,8340,842+0,017
+ Cross-Encoder0,6450,8600,842+0,017

Observaciones clave

La búsqueda híbrida supera a cualquiera de los dos métodos por separado. El NDCG de RRF (0,628) supera tanto a BM25 (0,585) como a Dense (0,611). Ambos métodos pueden fallar en consultas diferentes, y combinarlos permite recuperar documentos que cualquiera de ellos por separado omitiría.

El recall se establece durante la recuperación. Recall@100 permanece en 0,842 durante la etapa del cross-encoder. Los rerankers reordenan, pero no añaden documentos. Si quieres aumentar el recall, corrige la capa de recuperación. Los valores de MRR anteriores utilizan la misma regla global de la demo: Complement o superior cuenta como relevante.

No se informa del resultado del LLM. El parser acepta identificadores duplicados y fuera de rango, y su ruta de relleno puede alterar silenciosamente la lista de candidatos. Por tanto, la comparación anterior con LLM es ilustrativa, no un resultado auditable. Vuelve a ejecutarla con validación estricta de identificadores, comportamiento de fallback registrado, ejecuciones repetidas, mediciones de latencia y coste y un conjunto de dominio held-out antes de atribuir cualquier mejora al reranker.

El repositorio complementario resulta útil para la configuración y el código, pero su README todavía informa del valor no válido + LLM Reranker NDCG@10 de 0,717. Considera la tabla pre-LLM de este artículo y la advertencia sobre el LLM anterior como el registro de referencia hasta que el repositorio disponga de una evaluación de LLM auditable.

La recuperación densa supera a BM25 en esta muestra. Inspecciona muestras de consultas antes de atribuir la causa. El desajuste de vocabulario es un factor posible, pero también influyen la construcción de la muestra, el tokenizador, el dominio de entrenamiento del modelo y los campos del corpus.


Evaluación: medir lo que importa

La demo utiliza tres métricas, cada una de las cuales observa el ranking desde un ángulo diferente:

NDCG@10 (métrica principal)

Normalized Discounted Cumulative Gain mide la calidad del ranking del top 10 utilizando relevancia graduada. Recompensa colocar los documentos muy relevantes cerca del principio mediante un descuento logarítmico:

DCG@k=i=1k2reli1log2(i+1)NDCG@k=DCG@kIDCG@k\text{DCG@k} = \sum_{i=1}^{k} \frac{2^{rel_i} - 1}{\log_2(i + 1)} \qquad \text{NDCG@k} = \frac{\text{DCG@k}}{\text{IDCG@k}}

De las tres, NDCG es la única métrica que utiliza plenamente la relevancia graduada en cuatro niveles de ESCI. Un sistema que coloca una coincidencia Exact en la posición 1 obtiene más puntuación que otro que coloca allí una Substitute. Por eso es la métrica principal en este caso.

MRR (primer resultado Complement o superior)

Mean Reciprocal Rank utiliza la posición del primer resultado considerado relevante. En esta demo, el evaluador pasa los qrels graduados de ESCI a pytrec_eval y aplica recip_rank a cada lista devuelta de hasta 100 candidatos, por lo que relevance > 0 es el umbral binario: Complement (1), Substitute (2) y Exact (3) cuentan como relevantes. Irrelevant (0) no cuenta. Un resultado relevante en la posición 1 produce un reciprocal rank de 1,0; en la posición 3, de 0,333. Es MRR sobre las listas de candidatos devueltas, no MRR@10.

Recall@100 (cobertura de la recuperación)

El recall mide qué fracción de los documentos relevantes evaluados aparece en el top 100. Es una métrica del techo de candidatos para los juicios evaluados: un reranker no puede añadir un documento que la recuperación haya omitido, mientras que unos juicios incompletos pueden hacer incierto el techo aparente.


Indexación de vectores densos más allá de la demo

Los embeddings densos solo resultan útiles a escala cuando se dispone de un índice Approximate Nearest Neighbor (ANN). La demo utiliza similitud coseno por fuerza bruta, lo cual es adecuado con unos 8.500 documentos, pero los sistemas de producción necesitan índices especializados.

HNSW (hierarchical navigable small world)

HNSW construye un grafo multicapa: las capas superiores, más dispersas, permiten navegar ampliamente, mientras que las inferiores, más densas, refinan el vecindario. M controla la conectividad del grafo, mientras que efSearch intercambia trabajo de consulta por recall. Los valores útiles dependen de la dimensión, la distribución de distancias, los filtros, la implementación y el recall objetivo.

Las actualizaciones y eliminaciones son aspectos operativos relevantes porque los índices de grafos pueden necesitar reparación en segundo plano. El comportamiento varía según la base de datos. Por ejemplo, un issue de Qdrant informa de una degradación de la calidad de las búsquedas filtradas en una configuración HNSW multi-tenant. El informe midió una precision@100 media de 0,597 ± 0,0541 con un filtro library_id, mientras que la búsqueda exacta alcanzó un recall del 100 % en su análisis más profundo. Cambiar payload_m obligó a reconstruir el índice y restauró la calidad de las búsquedas filtradas. El informe trata sobre filtros por tenant, configuración de HNSW y reconstrucción forzada del índice HNSW, no sobre una carga con muchas eliminaciones. Reproduce el patrón de churn objetivo e incluye el comportamiento de compaction o rebuild en la evaluación.

IVF (inverted file)

Los índices IVF particionan el espacio vectorial en clústeres y después exploran los nprobe clústeres más próximos a la consulta. Pueden ofrecer un equilibrio útil entre memoria, tiempo de construcción y recall, especialmente combinados con compresión. La semántica y el rendimiento de las actualizaciones dependen de la implementación, no solo de la familia del índice.

A una escala extrema, IVF_RaBitQ (Gao & Long, SIGMOD 2024) comprime los vectores de coma flotante en representaciones de un solo bit. En espacios de alta dimensión, el signo (+/-) de una coordenada contiene suficiente información angular para calcular la similitud.

DimensiónGrafo HNSWClústeres IVF
Control de consultaefSearchnprobe
Control de construcciónConectividad y beam de construcciónNúmero de clústeres y muestra de entrenamiento
Perfil de memoriaAristas del grafo más vectoresCentroides, listas y vectores almacenados
Comportamiento de actualizaciónReparación/limpieza específica de la base de datosMantenimiento de listas específico de la base de datos
Evaluar conCurva recall-latencia-memoria-churnCurva recall-latencia-memoria-churn

En un caso práctico de Uber sobre búsquedas de entregas, reducir un parámetro de búsqueda a nivel de shard de 1.200 a 200 recortó la latencia comunicada un 34 % y el uso de CPU un 17 %, con poca pérdida de recall medida. La lección reutilizable es ajustar la curva recall-coste sobre tráfico similar al de producción, no copiar el valor 200.


Extensiones opcionales después del pipeline principal

Una vez que la recuperación y el reranking tienen mediciones independientes, resulta más sencillo evaluar varias extensiones sin ocultar el pipeline principal.

Comprensión de consultas

La expansión y reescritura de consultas pueden abordar el desajuste de vocabulario antes de la recuperación. Query2doc generó pseudo-documentos e informó de mejoras de BM25 en sus experimentos sobre MS MARCO. La expansión también puede introducir una intención equivocada, así que compara recall y precisión en muestras de consultas ambiguas, navegacionales y con identificadores exactos.

Patrones prácticos: expansión de abreviaturas, enriquecimiento de entidades, descomposición en subconsultas para reasoning multi-hop y RAG-Fusion: generar varias variantes de consulta y combinar los resultados mediante RRF.

Etiquetado de relevancia asistido por LLM

Los LLM pueden proponer etiquetas de relevancia cuando escasean los juicios humanos. TALEC y el trabajo de Pinterest sobre etiquetado de relevancia presentan dos diseños evaluados. Una etiqueta generada por un LLM sigue siendo una salida de modelo: calibra sus resultados frente a juicios humanos ciegos, inspecciona las muestras de desacuerdo y conserva un conjunto gold humano para pruebas de regresión.

Entre los controles útiles se incluyen:

  • un rubric con límites y ejemplos concretos de relevancia;
  • calibración humana ciega y revisiones periódicas;
  • aleatorización del orden y juicios repetidos para los casos inestables;
  • paneles de modelos cuando el coste adicional mejore el acuerdo; y
  • comprobaciones explícitas de los sesgos de posición y de tendencia central.

Knowledge distillation

Cuando un LLM aporta valor, pero no puede cumplir las restricciones de serving, la distillation es una opción:

  1. Utiliza un LLM potente (el teacher) para reordenar miles de consultas de entrenamiento
  2. Entrena un cross-encoder pequeño y rápido (el student, de unos 100M–200M parámetros) para imitar la distribución de ranking del LLM
  3. Compara el student con el teacher y con el baseline en calidad, calibración y coste de serving

InRanker destila MonoT5-3B en modelos de 60M y 220M parámetros: una reducción de tamaño de 50x con rendimiento competitivo. El enfoque Rank-Without-GPT produce rerankers listwise open source de 7B que alcanzan el 97 % de la eficacia de GPT-4 mediante fine-tuning con QLoRA.

Los resultados publicados de compresión son puntos de partida, no ratios esperables en producción. La distillation puede heredar los sesgos del teacher y perder calidad en muestras de consultas poco frecuentes, así que conserva los juicios de relevancia originales dentro del ciclo de evaluación.


Personalización y sesgo de posición

La relevancia genérica tiene un alcance limitado. Una búsqueda de «apple» debería devolver iPhones a una persona aficionada a la tecnología y recetas de manzana a alguien que haya estado consultando contenido de cocina.

Una arquitectura habitual de recuperación para personalización utiliza un modelo de embeddings two-tower: la query tower codifica la consulta y el contexto del usuario, mientras que la item tower codifica los elementos y sus metadatos. La separación offline/online permite la recuperación approximate-nearest-neighbor; su latencia sigue dependiendo del encoder, el índice, los filtros y el sistema de serving.

Los embeddings de listados de Airbnb, OmniSearchSage de Pinterest y los sistemas two-tower de Uber muestran distintos diseños de producción. Su escala y las mejoras comunicadas pertenecen a esos sistemas; el patrón transferible es una item tower offline más una query/user tower online.

Los datos de clics contienen sesgos de posición y exposición. PAL es un enfoque de desbiasing: utilizar la posición durante el entrenamiento y mantenerla constante durante el serving. No es una solución universal; las intervenciones aleatorizadas, los métodos de inverse propensity y la evaluación contrafactual pueden ser más adecuados para otro producto.


Adaptación al dominio con consultas sintéticas

Un error habitual en la estrategia de búsqueda es asumir que un modelo entrenado con datos generales de la web, como MS MARCO, funcionará bien en un dominio especializado. Es el problema out-of-domain (OOD).

Los LLM pueden reducir, aunque no eliminar, el cuello de botella de los datos etiquetados mediante Generative Pseudo-Labeling (GPL, InPars):

  1. Toma tu corpus de documentos específico del dominio
  2. Pide a un LLM mediante un prompt que «genere una consulta de búsqueda que este documento respondería»
  3. Utiliza los pares sintéticos (consulta, documento) para hacer fine-tuning de tu recuperador y tu reranker

Los pares sintéticos pueden ayudar cuando escasean las consultas reales, pero reflejan al generador y al prompt. Deduplica esos pares, filtra las consultas inverosímiles y valida con tráfico real held-out.

Secuencia de experimentación

Añade complejidad solo cuando la etapa anterior revele un fallo medido:

Ruta práctica de madurezRuta práctica de madurez

Paso 1 (baseline): implementa BM25 o el sistema léxico actual y crea un conjunto de consultas evaluadas. Registra recall, NDCG, latencia y muestras de fallos.

Paso 2 (recall de candidatos): prueba la recuperación densa y la fusión solo si el baseline omite documentos relevantes. Ajusta el cutoff de candidatos según el recall y el coste.

Paso 3 (precisión del ranking): añade un cross-encoder si existen los candidatos correctos, pero aparecen en el orden equivocado. Elige el tamaño de la shortlist a partir de una curva de calidad y latencia medida.

Paso 4 (ajuste al dominio): haz fine-tuning o distillation solo después de que los modelos genéricos muestren fallos estables y específicos del dominio. Mantén los juicios reales held-out separados de los datos de entrenamiento sintéticos.

Paso 5 (capa costosa opcional): prueba reranking listwise o basado en reasoning solo cuando su calidad incremental sobreviva a ejecuciones repetidas y justifique la latencia, el coste, la privacidad y la complejidad del fallback.


Líneas de investigación que deben evaluarse por separado

Los rerankers con reasoning y los agentes que utilizan búsqueda son prometedores, pero responden a preguntas diferentes de las del demo de búsqueda de productos de cinco etapas.

Rerankers basados en reasoning

Rank1 entrena rerankers con trazas de reasoning e informa de buenos resultados en el benchmark BRIGHT. Esta evidencia es relevante para recuperación con mucho reasoning, no una predicción directa para la búsqueda de productos ESCI.

Para búsquedas jurídicas o científicas, compara los rerankers con reasoning con baselines sólidos de cross-encoder y listwise utilizando juicios expertos, citas, latencia y consistencia de los fallos.

Búsqueda agentic

Search-o1 estudia un modelo que realiza búsquedas adicionales durante la respuesta a preguntas multi-hop. Es un problema de orquestación —generación de consultas, parada, uso de evidencias y evaluación de respuestas—, no otra etapa de reranking. Evalúalo con la corrección de la tarea final y el respaldo de las citas, no solo con métricas de recuperación.


Conclusiones clave

  1. Trata el stack como una secuencia de experimentos. Establece un baseline léxico y un conjunto de consultas evaluadas antes de añadir recuperación densa, fusión o reranking.

  2. Mide por separado el recall de candidatos y la precisión del ranking. En esta muestra ESCI, Recall@100 alcanza 0,842 después de la fusión y permanece estable durante ambas etapas de reranking.

  3. Utiliza recuperación híbrida cuando los errores sean complementarios. RRF mejoró tanto Recall@100 como NDCG@10 en la demo, pero otro corpus podría no justificar dos índices.

  4. Añade un cross-encoder cuando la shortlist sea correcta, pero el orden no. Elige el número de candidatos a partir de una curva de calidad y latencia medida.

  5. Trata el reranking con LLM como un experimento final opcional. La comparación actual con LLM de la demo es ilustrativa porque su parser no valida una permutación completa de los IDs de los candidatos. El parseo estricto, los fallbacks registrados, las ejecuciones repetidas y el coste deben formar parte de la evaluación antes de comunicar una mejora.

  6. Mantén separados los tipos de evidencia. El resultado de un paper, un caso práctico de un proveedor, esta demo ejecutada en un portátil y un A/B test de producción responden a preguntas diferentes.

El código completo del pipeline está en el repositorio complementario. Clónalo para reproducir las etapas pre-LLM o probar otros modelos y parámetros; no consideres un resultado la fila obsoleta del LLM.

Referencias

Papers

Datasets y benchmarks

Modelos utilizados en la demo

Herramientas y plataformas

  • rank_bm25 — Implementación de BM25 en Python
  • Pyserini BEIR Reproductions — Comandos de indexación y evaluación bm25-multifield
  • pytrec_eval — Toolkit de evaluación de TREC
  • Elasticsearch — Búsqueda híbrida con Retrievers API
  • Vespa — Motor unificado de búsqueda y recomendación
  • Weaviate — Base de datos vectorial con búsqueda híbrida
  • Qdrant — Base de datos vectorial con consultas multietapa

Referencias del sector

Proyecto de demo