Búsqueda full-text en Laravel 13 con Laravel Scout y Meilisearch: guía paso a paso
Un buscador con WHERE ... LIKE %texto% se rompe en cuanto la tabla crece: no usa índices, no ordena por relevancia y no perdona una errata. Con Laravel Scout y Meilisearch en Laravel 13, el mismo buscador responde en menos de 50 ms con tolerancia a erratas, sin pagar una API de terceros.
Por qué LIKE ya no es suficiente
El problema de %texto%: escaneo completo y cero relevancia
Cuando escribes WHERE titulo LIKE '%blender%', la base de datos no puede aprovechar un índice normal: el comodín al inicio obliga a recorrer fila por fila buscando la cadena. El resultado funciona con cientos de registros y se degrada con miles. Además, el motor devuelve cualquier coincidencia sin ordenar por relevancia, y una búsqueda con una letra mal escrita simplemente no encuentra nada.
Qué aporta un motor de búsqueda dedicado
Un motor como Meilisearch mantiene un índice invertido pensado para búsqueda: tokeniza el texto, aplica stemming, tolera erratas por defecto y devuelve resultados ordenados por relevancia en milisegundos. No sustituye a la base de datos, que sigue siendo la fuente de verdad: el índice es una copia optimizada para consultas de texto.
Laravel Scout: la capa que unifica los motores
Cómo funciona el trait Searchable
Scout es el paquete oficial de Laravel para búsqueda full-text. Su idea central es simple: añades el trait Laravel\Scout\Searchable a un modelo Eloquent y, a partir de ese momento, cada vez que guardas o creas una instancia, el registro se envía solo al índice. Borrar un modelo también lo elimina del índice, sin que tengas que escribir esa sincronización a mano.
Meilisearch vs Typesense vs Algolia vs driver de base de datos
Scout funciona con varios motores y conviene saber cuál elegir. Meilisearch es open source, se autoalojar en un binario o contenedor y es la opción más directa para empezar. Typesense también es open source y añade soporte vectorial si piensas combinar búsqueda textual y semántica. Algolia es la búsqueda como servicio de referencia para empresas que prefieren no operar infraestructura. Y el driver de base de datos de Scout, con whereLike, sirve para proyectos pequeños donde montar un motor sería sobredimensionado.
Instalación: Meilisearch, Scout y el driver PHP
Levantar Meilisearch en local (binario o Docker)
Lo primero es arrancar el motor. Puedes descargar el binario para tu sistema y ejecutarlo, o levantarlo con Docker en una línea. En ambos casos el servidor escucha por defecto en el puerto 7700 y, si defines una master key al arrancarlo, la necesitarás después para todas las peticiones.
composer require laravel/scout y publicar la configuración
Con el motor en marcha, instalas Scout en el proyecto y publicas su configuración:
composer require laravel/scout
composer require meilisearch/meilisearch-php
php artisan vendor:publish --provider="Laravel\Scout\ScoutServiceProvider"El segundo paquete es el cliente PHP de Meilisearch, que Scout usa internamente para comunicarse con el motor. El comando vendor:publish genera config/scout.php, donde se definen el driver activo y las credenciales.
Host, clave y driver en el .env
En el archivo .env del proyecto declaras el driver y la conexión:
SCOUT_DRIVER=meilisearch
SCOUT_MEILISEARCH_HOST=http://127.0.0.1:7700
SCOUT_MEILISEARCH_KEY=masterKeyLa clave debe coincidir con la master key que definiste al arrancar Meilisearch. Con esto, Scout ya sabe contra qué motor trabajar.
Hacer searchable un modelo
El trait Searchable y la sincronización automática
El paso que activa la magia es añadir el trait al modelo. A partir de ahí, crear, actualizar o borrar un registro mantiene el índice al día sin más código. Si no quieres que un modelo se indexe, simplemente no le añades el trait.
Personalizar lo que se indexa con toSearchableArray
Por defecto Scout indexa todos los atributos visibles del modelo. Con toSearchableArray() decides exactamente qué entra en el índice, lo que permite incluir datos de relaciones:
public function toSearchableArray(): array
{
return [
'id' => $this->id,
'titulo' => $this->titulo,
'cuerpo' => $this->cuerpo,
'autor' => $this->autor->nombre,
];
}Así, una búsqueda puede encontrar artículos por el nombre de su autor aunque ese dato viva en otra tabla.
Renombrar el índice con searchableAs
Si el nombre por defecto del índice no te convence, el método searchableAs() devuelve el nombre que quieras usar. Es útil cuando varios modelos comparten un índice o cuando necesitas un nombre corto para el panel de Meilisearch.
Indexar los datos existentes: scout:import y scout:flush
El trait sincroniza los cambios nuevos, pero los registros que ya había antes de añadir la búsqueda no están en el índice. El comando php artisan scout:import "App\Models\Post" recorre la tabla y envía todo al motor. Su compañero php artisan scout:flush vacía el índice, útil cuando cambias la estructura de toSearchableArray y quieres repoblar desde cero.
Buscar en tu aplicación
El método search() y la paginación
Buscar es tan directo como llamar al método search() del modelo:
$posts = Post::search('blender 5.2')->paginate(10);El resultado es una paginación normal de Laravel, así que se integra con Blade y con cualquier paginador que ya uses en la aplicación.
Filtros con where() y la trampa de filterableAttributes
Scout permite afinar con where(), por ejemplo Post::search('tutorial')->where('user_id', 1)->paginate(10). Aquí está el gotcha más repetido en foros: si el atributo no está declarado en filterableAttributes del índice de Meilisearch, el filtro falla en silencio o devuelve resultados inesperados, aunque el campo esté mapeado en toSearchableArray. Hay que declarar los atributos filtrables en la configuración del índice para que where() funcione.
Por qué los resultados no se parecen a LIKE
Los resultados de Meilisearch no son una coincidencia exacta de subcadena: el motor normaliza, tolera erratas y puntúa por relevancia. Eso significa que una búsqueda con errores tipográficos devuelve igualmente buenos resultados, pero también que el orden puede sorprender si vienes de LIKE. Es el comportamiento deseado: el usuario encuentra lo que busca aunque escriba mal.
Producción: colas, soft deletes y mantenimiento
Indexar en background con colas
En producción no quieres que una petición HTTP espere a que el registro viaje al motor. En config/scout.php puedes activar las colas para que la sincronización se procese en background. Si trabajas con Laravel 13 y Horizon, la integración es directa, como ya vimos en nuestro artículo sobre colas y jobs en Laravel 13.
Soft deletes y sincronización del índice
Si tu modelo usa soft deletes, los registros borrados lógicamente deben desaparecer del índice. Configurando el driver de Meilisearch en config/scout.php, Scout gestiona ese borrado automáticamente cuando se usa delete(). Y aunque un borrado físico falle por cualquier motivo, el índice mantiene un estado consistente porque la base de datos sigue siendo la fuente de verdad.
Conclusión
Sustituir LIKE %texto% por Laravel Scout y Meilisearch no es un lujo: es el salto entre un buscador que se degrada y uno que responde en milisegundos con erratas toleradas y relevancia real. Si vienes de Laravel 12, antes repasa las novedades de Laravel 13 para tener el contexto de la versión. Sigue leyendo el blog para más guías prácticas de desarrollo web con Laravel.