IA 7 jul 2026 · 8 min de lectura

Rerankers: la pieza que le faltaba a mi búsqueda semántica

Yohangel Ramos

Yohangel Ramos

Tech Lead · Senior Fullstack Developer

Durante meses traté la búsqueda por embeddings como si fuera el sistema completo: query a vector, cosine similarity contra pgvector, top 10 y a producción. Funcionaba... hasta que mirabas el orden de los resultados con lupa. El candidato correcto casi siempre estaba ahí, pero en la posición 4, o en la 7, rara vez en la 1. La lección que me costó aceptar: los embeddings son un filtro grueso excelente y un ordenador fino mediocre. El reranker es la pieza que separa "los resultados relevantes están en la página" de "el mejor resultado es el primero". Este artículo es cómo lo integré en el motor de matching de JXBS sin destrozar la latencia.

Por qué el bi-encoder se queda corto

Un embedding comprime todo un documento en un vector fijo antes de saber qué le vas a preguntar. Esa es su gran ventaja (puedes precalcularlo e indexarlo) y su límite estructural: la compresión pierde los matices que solo importan frente a una query concreta. Dos perfiles de candidato pueden estar igual de "cerca" de una vacante en el espacio vectorial por razones distintas, y el coseno no distingue si la cercanía viene de lo esencial o de lo accesorio.

Un reranker es un cross-encoder: recibe la query y el documento juntos, y produce un score de relevancia mirando la interacción entre ambos. Es mucho más preciso ordenando, y mucho más caro: no puedes precalcular nada, cada par query-documento es una inferencia. Por eso nadie rankea un corpus entero con un cross-encoder. El patrón es recuperar barato y reordenar caro sobre pocos candidatos.

La arquitectura: recuperar 50, reordenar a 10

Mi pipeline quedó en dos etapas. La primera es la que ya tenía: pgvector recupera los 50 vecinos más cercanos. La segunda pasa esos 50 por el reranker y se queda con los 10 mejores según el nuevo score.

-- Etapa 1: recuperación barata, sobre-recuperando a propósito
SELECT id, titulo, contenido
FROM documentos
ORDER BY embedding <=> $1
LIMIT 50;
// Etapa 2: reranking sobre los candidatos
const scored = await rerank({
  query: userQuery,
  documents: candidates.map((c) => c.contenido),
});

const top = scored
  .sort((a, b) => b.relevanceScore - a.relevanceScore)
  .slice(0, 10);

El número 50 no es sagrado. Es el equilibrio que encontré entre dos fallos: si recuperas pocos, el documento correcto ni siquiera llega al reranker y no hay nada que reordenar; si recuperas demasiados, pagas latencia e inferencia por candidatos que no tenían ninguna opción. Elegí 50 porque en mis pruebas el resultado correcto estaba dentro del top 50 del bi-encoder prácticamente siempre; a cambio, acepto que si la recuperación falla de raíz, el reranker no la rescata. Garbage in, garbage out, pero mejor ordenado.

Latencia: el precio real y cómo lo contuve

El reranking añade una llamada de red y una inferencia por request. En mi caso eso significó pasar de ~80ms a ~350ms en p50. Para una búsqueda interactiva era demasiado, y lo contuve con tres decisiones:

La primera, rerankear solo cuando importa. Si el score del primer candidato del bi-encoder saca mucha ventaja al segundo, el orden ya está claro y me salto la segunda etapa. La segunda, truncar documentos: el reranker no necesita las 3.000 palabras del documento, con el título y el primer fragmento relevante le basta para discriminar, y el coste crece con los tokens. La tercera, cachear pares query-documento normalizados; en un producto real las queries se repiten mucho más de lo que uno cree.

💡 El reranker no mejora tu recuperación: mejora tu precisión en el top. Si tu problema es que los buenos resultados no aparecen ni en el top 50, tu problema está en los embeddings o en el chunking, y ningún reranker te va a salvar.

Cómo medí que valía la pena

Antes de integrar nada monté un set de evaluación pequeño: ~100 queries reales con el resultado que un humano consideraba correcto marcado a mano. La métrica que me importaba era MRR (mean reciprocal rank): si el resultado correcto está primero vale 1, si está segundo 0.5, y así. Con solo bi-encoder tenía un MRR mediocre; con reranker subió de forma clara y consistente. No doy las cifras exactas porque dependen brutalmente del dominio y del dataset, pero el patrón se repite en casi cualquier corpus: el bi-encoder encuentra, el cross-encoder ordena.

Lo que sí es generalizable es el método: no integres un reranker porque está de moda. Monta primero las 100 queries con juicio humano, mide tu baseline, y que la decisión la tome el número. En mi caso el número fue contundente y la latencia extra estaba pagada.

Trade-offs que acepté

Elegí un reranker gestionado vía API en lugar de servir un cross-encoder propio, porque a mi escala el coste por request era menor que el coste fijo de mantener una GPU caliente. A cambio, acepté una dependencia externa en el camino crítico de la búsqueda, mitigada con un fallback: si el reranker no responde en 500ms, devuelvo el orden del bi-encoder tal cual. Búsqueda algo peor es infinitamente mejor que búsqueda caída.

También acepté que el sistema es más difícil de razonar: ahora hay dos modelos que pueden degradarse por separado. Por eso las evals corren contra el pipeline completo, no contra cada pieza aislada. Lo que le llega al usuario es el resultado de la cadena entera, y eso es lo único que tiene sentido medir.

Yohangel Ramos

Escrito por Yohangel Ramos

Senior Fullstack Developer y Tech Lead. Construyo con React, Next.js, Nest.js y AWS — y escribo sobre lo que aprendo en el camino.

Hablemos →

Sigue leyendo

IA

Embeddings más baratos: recortar dimensiones y cuantizar sin perder recall

IA

Qué va a pasar con el desarrollo de software: predicciones con fechas (julio 2026)