Nadie discute que un motor de búsqueda va al lado de la base de datos, no en su lugar. Es la respuesta correcta el noventa por ciento de las veces, y por eso casi nadie mira el diez por ciento restante, donde esa segunda copia solo genera trabajo. Aquí verás dónde está exactamente esa línea, qué ganas y qué renuncias al cruzarla. Los ejemplos van en Laravel porque es una de nuestras especialidades en AndorraDev, pero el razonamiento aplica igual con cualquier framework y cualquier motor de búsqueda.
Un índice de búsqueda es una copia, y las copias se desincronizan
El montaje habitual es este: los datos viven en la base de datos relacional y un motor de búsqueda mantiene una copia optimizada para consultar.
Funciona, y tiene un coste que casi nunca se contabiliza al decidirlo. Hay que escribir un comando de sincronización, mantener un trabajador de cola para las actualizaciones, vigilar la desviación que aparece cuando un trabajo falla y tener listo un guion de reindexado completo para el día que cambie el modelo de embeddings.
Todo eso es infraestructura que no aporta funcionalidad. Existe solo para que dos copias digan lo mismo.
Para la mayoría de los datos es un precio justo, porque la base de datos aporta transacciones, relaciones e integridad que el motor de búsqueda no tiene. Pero hay una categoría de datos donde no aporta nada.
Los fragmentos de texto no tienen vida propia en tu esquema relacional
Piensa en lo que pasa cuando trocas un documento largo para poder buscarlo por significado.
Coges una ley, la partes en artículos, cada artículo en pasajes de unos cientos de palabras, calculas el embedding de cada pasaje y lo guardas. Lo mismo con documentación, con transcripciones de reuniones o con un histórico de tickets de soporte.
Esos fragmentos no son entidades de tu negocio. No tienen relaciones que respetar, nadie los edita de uno en uno, no participan en ninguna transacción y su clave primaria es un identificador que te has inventado al trocear. Existen por un único motivo: para ser encontrados.
Meterlos en una tabla relacional y luego copiarlos al motor de búsqueda es pagar el precio completo de la sincronización a cambio de garantías que ese dato no necesita.
Lo que ganas es no tener que sincronizar nada
Si el índice es la fuente de verdad, la segunda copia desaparece y con ella todo lo que se escribió para mantenerla.
Ya no hay comando de sincronización, ni cola, ni desviación posible. Reindexar deja de ser "vaciar y volver a copiar desde SQL" y pasa a ser sencillamente volver a generar los fragmentos.
Y hay un ahorro en tiempo de ejecución que se nota más de lo que parece. Un buscador que espeja una tabla devuelve identificadores y luego tiene que ir a la base de datos a rehidratar los resultados, con una consulta por búsqueda. Si el documento del índice ya trae todos los campos, esa consulta no existe: cero consultas SQL por búsqueda.
Qué datos aguantan este trato y cuáles no
Esta es la parte que decide, y conviene tenerla delante antes de mover nada. Un motor de búsqueda es un motor de búsqueda: da recuperación por relevancia, no las garantías de un motor relacional.
Las transacciones se quedan en tu base de datos. Guardar un fragmento y actualizar un contador en la misma operación atómica es cosa del SQL.
Las relaciones y las claves foráneas también. Nada impide que un fragmento apunte a un documento que ya no existe, así que la integridad la mantiene tu proceso de troceado.
Y las restricciones igual: unicidad, no nulos y comprobaciones viven donde vive el dato canónico.
Y sobre todo: si pierdes el índice, pierdes los datos. Con un índice espejo, reconstruirlo es un comando. Aquí es una restauración de copia de seguridad como la de cualquier otra base de datos, con lo cual el índice pasa a necesitar la misma política de respaldo que tu SQL.
Para fragmentos de texto ninguna de esas pérdidas duele. Para pedidos y facturas dolerían todas. La línea está ahí, y es bastante nítida.
Antes de mover un tipo de dato al índice, hazte una pregunta: si mañana perdiera esto, ¿podría regenerarlo? Los fragmentos de un documento sí, porque el documento original sigue en su sitio. Un pedido no. Esa pregunta separa mejor los dos casos que cualquier discusión sobre rendimiento, y además te dice si necesitas copias de seguridad del índice o te basta con poder reconstruirlo.
El API que necesitas para tratar un índice como una tabla
Si vas a hacer esto, lo que quieres es escribir código que se parezca al que ya escribes, no llamadas crudas al SDK repartidas por toda la aplicación.
La configuración del índice se declara en la propia clase, que es donde se puede leer y versionar:
class LawChunk extends Meili
{
protected static string $index = 'law_chunks';
protected static string $primaryKey = 'external_id';
protected static array $searchable = ['content', 'title_path'];
protected static array $filterable = ['law_id', 'country', 'date'];
protected static array $sortable = ['position'];
}
Y luego un comando empuja esos ajustes al servidor, con lo que la configuración del motor deja de ser algo que alguien tocó una vez en un panel y pasa a estar en el repositorio como el resto del código.
Las consultas se compilan a los parámetros que el motor entiende, y la búsqueda híbrida es de primera clase:
LawChunk::query()
->where('country', 'AD')
->semantic(0.7)
->limit(20)
->get();
Hay que asumir que el lenguaje de consulta es más pobre que el de SQL, y no por una limitación del envoltorio sino del motor: los filtros se encadenan con Y lógico y para una disyunción hay que bajar a la expresión cruda. No hay agregados. No hay uniones. Es un buscador, no un motor relacional.
Lo que sí se puede resolver bien es la unión con tu base de datos real. Un fragmento puede tener una relación con un modelo Eloquent, y al cargar una lista de resultados esa relación se resuelve con una sola consulta para todos, no una por fragmento.
La escotilla de salida importa tanto como la abstracción
Toda capa cómoda sobre un servicio externo cubre el ochenta por ciento de los casos y esconde el resto. Lo que decide si esa capa te sirve a los dos años no es lo bien que envuelve lo habitual, es lo fácil que resulta saltársela el día que necesitas algo que no previó.
Un buscador serio tiene funcionalidad que ninguna capa fluida va a cubrir entera. Cuando quieres pedir la puntuación de relevancia de cada resultado para reordenarlos tú, o pasarle un vector que ya has calculado por tu cuenta en vez de dejar que lo calcule él, necesitas el cliente crudo.
La forma correcta de resolver eso no es ampliar la abstracción hasta cubrirlo todo, es dejar el cliente accesible:
LawChunk::client()
->index(LawChunk::indexName())
->search($query, $options)
->getHits();
Ahí sigues teniendo el nombre del índice y la configuración en un solo sitio, y a la vez tienes el SDK entero disponible para el caso raro. En Crowd Legal el buscador de legislación usa exactamente esa ruta, porque calcula sus propios embeddings y reordena los resultados agregando la puntuación por documento antes de devolverlos.
Una abstracción que te obliga a elegir entre usarla del todo o no usarla es una abstracción que acabarás quitando.
Scout y esto no compiten
En cuanto se mencionan Meilisearch y Laravel en la misma frase, la respuesta automática es Scout. Merece pararse aquí porque las dos cosas parecen resolver lo mismo y en realidad tienen fuentes de verdad distintas, que es la diferencia que importa.
Laravel Scout hace algo distinto: mantiene un índice espejo de una tabla Eloquent. Su fuente de verdad es la tabla, y el índice es un derivado que puede reconstruirse cuando quieras. Para el catálogo de productos, para los clientes o para cualquier cosa que además vive en tu esquema, es la herramienta correcta y no hay motivo para cambiarla: te da observers, cola e importación masiva sin escribir nada.
Lo que resuelve tratar el índice como almacén primario es el otro caso, el del dato que solo existe para ser buscado. Y las dos cosas conviven en el mismo proyecto sin problema: comparten la misma conexión al servidor y no se estorban.
Dicho de otro modo: si te preguntas "¿dónde vive esto de verdad?", y la respuesta es "en mi base de datos", usa Scout. Si la respuesta es "en ningún sitio, lo genero al trocear un documento", entonces la tabla intermedia solo te está dando trabajo.
Y si no trabajas con Laravel
El criterio es del diseño, no del framework:
- Pregúntate si el dato es regenerable. Si lo es y solo sirve para buscar, la copia relacional probablemente sobra.
- Declara la configuración del índice en el código, no en un panel. Los ajustes de un motor de búsqueda son configuración de aplicación y merecen estar en el control de versiones.
- Guarda en el documento todo lo que vayas a mostrar. Si tienes que ir a la base de datos a completar los resultados, has vuelto al punto de partida.
- Y exige una escotilla de salida. Cualquier capa sobre un servicio externo debe dejarte llegar al cliente crudo, porque el día que necesites un parámetro que no cubre, lo vas a necesitar de verdad.
Dónde está el código
El paquete está en Packagist, el código en GitHub y la documentación del paquete recoge la API completa. Son unas mil líneas, sin migraciones y sin tablas, y reutiliza la misma configuración de conexión que ya tengas puesta para Scout.
Está en producción en Crowd Legal, donde los fragmentos de legislación y de jurisprudencia viven en el índice y se recuperan por significado.
Si estás montando búsqueda semántica sobre tus propios documentos, esta decisión de arquitectura es la primera y condiciona el resto. La trabajamos en inteligencia artificial y software legal, y si nos cuentas qué vas a indexar te decimos dónde poner la línea.