BM25 sobre SQLite vs una base de datos vectorial
La categoría asume un servicio de embeddings, un índice ANN y una GPU. Medimos la búsqueda de entrada en recall@5 = 1.00 a 1.3 ms p95 con BM25 sobre SQLite, y dejamos el embedder opcional.

La búsqueda de entrada en MonkeyLLM corre sobre BM25 sobre SQLite, y alcanza recall@5 = 1.00 a 1.3 ms p95 en nuestro corpus de benchmark. Antes de objetar que la búsqueda léxica murió hace años, mira cuál es el trabajo en realidad. El trabajo es más chico de lo que la categoría asume, y eso cambia la economía.
Lo que la categoría asume
El stack de recuperación por defecto viene con supuestos horneados: un modelo de embeddings (una API alojada, o una GPU que mantienes caliente), una base de datos vectorial con un índice ANN, un pipeline que reembebe documentos en cada edición y una historia de migración para el día en que cambies de modelo de embeddings y cada vector almacenado quede obsoleto. Todo eso es superficie operativa, y todo se paga antes de que la primera consulta devuelva algo.
Nada de eso está mal. Solo que rara vez se cuestiona, porque "búsqueda semántica" suena a requisito y no a decisión de diseño.
Lo que la búsqueda de entrada de verdad tiene que hacer
En un bosque de conocimiento (un grafo enlazado de documentos, cada uno con un pasaporte curado: título, resumen, tags), la búsqueda no es la máquina de respuestas. Es la puerta. El agente busca una vez para encontrar un nodo de partida y luego camina: lee etiquetas, sigue enlaces y abre solo lo que necesita. La caminata resuelve la pregunta; la búsqueda solo tiene que dejarte cerca.
Ese reencuadre encoge el requisito. La puerta no necesita entender una paráfrasis de la pregunta entera. Necesita coincidir con un nombre, y coincidir con nombres es lo que la búsqueda léxica hace mejor.
Lo que medimos
BM25 sobre el índice de texto completo de SQLite: recall@5 = 1.00 a 1.3 ms p95. Sin embeddings, sin base de datos vectorial, sin GPU. El detalle que más importa: ponderar los campos curados (título, alias) cerró por completo la brecha con la búsqueda híbrida. BM25 sin ponderar sobre cuerpos crudos no llegó; BM25 sobre un pasaporte curado sí.
SQLite se gana una mención aquí. Es un único archivo dentro del despliegue. No hay servicio de búsqueda que operar, respaldar o monitorear, y la misma base de datos ya almacena el bosque en sí. Para un sistema autoalojado que promete que tus datos nunca salen de tu infra, borrar un servicio con estado entero del diagrama de arquitectura no es una victoria menor.
Cuándo los vectores sí se ganan su lugar
Esto no es un argumento de que los embeddings sean inútiles. Se ganan su lugar cuando las consultas están cargadas de paráfrasis y no comparten vocabulario con los documentos, cuando el corpus es multilingüe, o cuando solo tienes chunks crudos sin metadatos curados y sin presupuesto de escritura para crearlos. En esos regímenes, la coincidencia léxica no tiene de dónde agarrarse.
Por eso el embedder es opcional y soportado, no eliminado. Apunta un binding de modelo a un endpoint de embeddings y la búsqueda de entrada se vuelve híbrida. Lo que eliminamos es el supuesto, no la capacidad.
El trade que hicimos en realidad
El número de recall no es gratis; está prepagado. La curaduría ocurre en tiempo de escritura: la ingesta corre a 1.71 s por documento, con el 100% de los resúmenes pasando un contrato de sesenta tokens, y de ahí salen los títulos, alias y resúmenes de los que BM25 se agarra. Gasta inteligencia en el entorno para gastar menos en el modelo. La versión larga de ese argumento es el Principio del Bosque.
Reprodúcelo
El corpus y el harness están versionados, y la tabla de búsqueda de entrada se regenera con un comando. Reprodúcela tú mismo, lee cómo encajan las piezas en los docs de primitivas o empieza en monkeyllm.com.