Búsqueda semántica en Laravel 13 con MariaDB Vector: columna, índice y consulta sin servicio externo
Desde Laravel 13.28 la búsqueda por significado cabe en la base de datos que ya tienes: una columna vector, un índice y una consulta. El camino es corto, y se rompe en tres sitios muy concretos: el driver, las dimensiones y la métrica del índice.
Qué cambia con MariaDB Vector y qué no
Una columna más en la base de datos que ya tienes
Un embedding es la traducción de un texto a una dirección en un espacio de muchas dimensiones. Buscar por significado es preguntar qué documentos están cerca de la dirección de esta pregunta. Hasta hace poco eso obligaba a montar un motor aparte o a usar PostgreSQL con pgvector. Desde Laravel 13.28, con MariaDB 11.7 o superior, la columna, el índice y la consulta son parte del mismo motor: no hay segundo servicio ni extensión que instalar.
Qué resuelve y qué no resuelve la similitud semántica
Resuelve la búsqueda por concepto: alguien escribe "dónde comer bien en Napa" y aparece tu documento sobre viñedos, aunque no comparta ni una palabra. No resuelve la coincidencia exacta. Si el usuario busca "factura 2026-08" o un número de seguimiento, el vector no distingue ese código de otro parecido, y ahí sigue ganando el texto completo. Saber esto antes de empezar evita reemplazar un buscador que funcionaba.
Requisitos antes de escribir una línea
MariaDB 11.7 o superior: 11.8 LTS y 12.3 LTS para producción
El tipo VECTOR llegó con MariaDB 11.7 y es estable en 11.8 LTS, que es la versión que tiene sentido en producción (12.3 LTS es la otra opción a largo plazo). Muchos servidores con cPanel o distribuciones LTS siguen en la rama 10.x, y pasar de 10.x a 11.8 es un salto mayor: backup lógico con mariadb-dump, ventana de mantenimiento y pruebas en un entorno aparte antes de tocarlo.
Lee también
Laravel 13.28 y el driver mariadb, no mysql
La gramática de MariaDB para vectores entró en la 13.27 y la corrección de vec_fromtext() con el cast AsVector llegó en la 13.28. En el archivo .env el driver tiene que ser DB_CONNECTION=mariadb. Este es el primer tropiezo y el más común: MariaDB habla el protocolo de MySQL, así que con mysql todo parece funcionar hasta que llega la primera consulta vectorial.
Comprobar versiones en el servidor y en CI
mariadb -e 'SELECT VERSION();'
php artisan --versionSi la primera línea no devuelve 11.7 o más, no sigas. Y revisa también el pipeline: si las pruebas corren contra un MySQL de CI, la suite pasará en verde y el despliegue fallará en producción por una diferencia de motor que nadie miró.
La migración: columna vector e índice
Las dimensiones son un contrato con el modelo de embeddings
La columna guarda un número fijo de dimensiones y ese número lo decide el modelo que genera los vectores: 1536 si usas un modelo de 1536 dimensiones, 3072 si usas otro. No es un ajuste de rendimiento que se pueda cambiar después. Si cambias de modelo y la dimensión cambia, necesitas columna nueva o migración más re-embedding de todas las filas. Por eso conviene guardar también qué modelo generó cada vector.
Lee también
Schema::create('documents', function (Blueprint $table) {
$table->id();
$table->string('title');
$table->text('body');
$table->vector('embedding', dimensions: 1536)->index();
$table->string('embedding_model')->nullable();
$table->timestamps();
});Qué SQL emite Laravel por debajo
Un ->index() sobre una columna vector no crea un índice B-tree normal: emite la sintaxis del motor. En MariaDB el resultado se parece a esto, y conviene comprobarlo en tu versión concreta con una migración en local:
alter table documents
add vector index documents_embedding_index (embedding)
M=6 DISTANCE=cosine;El parámetro M ajusta el equilibrio entre precisión, tamaño del índice y velocidad de búsqueda, y DISTANCE admite euclidean o cosine. Laravel emite cosine porque sus consultas comparan por similitud del coseno. Si algún día cambias la métrica del índice, tienes que reconstruirlo: no se recalcula solo.
El helper de pgvector que aquí no se llama
Muchos ejemplos de la documentación de búsqueda vectorial empiezan con Schema::ensureVectorExtensionExists(). En MariaDB esa línea lanza una excepción con el mensaje "Extensions are only supported by Postgres". Es un helper de pgvector y aquí no se usa: en MariaDB el tipo VECTOR ya está en el servidor, no hay extensión que activar.
El cast y la consulta por similitud
Por qué hace falta el cast AsVector
En MariaDB la columna vuelve como bytes float32 empaquetados, así que un cast a array o a JSON no funciona. Si envías un JSON a una columna VECTOR, el servidor responde con 1292 Incorrect vector value. El cast AsVector se encarga de las dos direcciones: lee el binario al consultar y escribe con vec_fromtext() al guardar.
use Illuminate\Database\Eloquent\Casts\AsVector;
protected function casts(): array
{
return [
'embedding' => AsVector::class,
];
}whereVectorSimilarTo, minSimilarity y el resto de métodos
Con el cast en su sitio, la consulta es una línea. El primer argumento es la columna y el segundo el texto de búsqueda; si el SDK de IA está instalado, el texto se convierte en embedding por el camino.
$results = Document::query()
->whereVectorSimilarTo('embedding', 'mejores viñedos de Napa')
->limit(10)
->get();
// Con umbral de similitud mínima
$results = Document::query()
->whereVectorSimilarTo('embedding', $texto, minSimilarity: 0.7)
->get();Del mismo grupo hay tres métodos que aparecen en cuanto se ajusta el orden o el filtro: whereVectorDistanceLessThan para filtrar por distancia máxima, orderByVectorDistance para ordenar sin filtrar y selectVectorDistance para traer la distancia como columna y poder mostrarla o depurar.
De dónde salen los embeddings
El vector lo genera un modelo externo, y ahí conviven dos costes: el de la API y el del tiempo. Para el relleno inicial, un job en cola con chunkById y un rate limit explícito es la diferencia entre terminar en una noche y que te bloqueen la cuenta. Si ya montaste agentes con el SDK de IA en PHP, la pieza de embeddings es la misma que se explica en la guía de agentes y sub-agentes del SDK.
Rellenar la tabla sin tumbar la base ni la cuota
Job en cola, por lotes y con rate limit
Nunca en una migración ni en un tinker. Un job por lote, con chunkById para no cargar la tabla en memoria y con una pausa entre llamadas al proveedor. El proceso tiene que poder cortarse y retomarse sin dejar filas a medio escribir.
Idempotencia: hash del texto y versión del modelo
Guardar un hash del texto de origen y el nombre del modelo junto al vector permite dos cosas: re-embeddear solo lo que cambió y detectar filas con vectores de un modelo anterior. Mezclar embeddings de modelos distintos en la misma columna hace que las distancias no signifiquen nada, y ese error no da ningún aviso: simplemente devuelve resultados malos.
Cuánto pesa un vector y cuánto cuesta el índice
Float32 son 4 bytes por dimensión. Una fila de 1536 dimensiones ocupa 6.144 bytes, unos 6 KB, solo en la columna. Un millón de filas son unos 6 GB de vectores, más la estructura del índice. Ese cálculo, hecho antes de empezar, evita la sorpresa de descubrir que la tabla no cabe en el disco que tienes.
Búsqueda híbrida: texto y vectores en la misma consulta
MATCH AGAINST con distancia de coseno
MariaDB permite combinar las dos señales en una sola sentencia: relevancia textual por un lado y cercanía vectorial por el otro, con un peso para cada una. El patrón es puntuar con las dos y ordenar por la suma ponderada, ajustando los pesos según lo que busque tu usuario.
select id, title,
match(title, body) against (? in natural language mode) as text_score,
vec_distance_cosine(embedding, vec_fromtext(?)) as distance
from documents
where match(title, body) against (? in natural language mode)
order by (0.4 * text_score) + (0.6 * (1 - distance)) desc
limit 20;Cuándo basta con texto completo
Para nombres propios, códigos, referencias exactas y números de documento, el texto completo gana y además es más barato. Si tu caso es casi todo así, un motor dedicado sigue siendo mejor idea que añadir vectores, y el camino está explicado en la guía de búsqueda con Scout y Meilisearch.
Los tres tropiezos que cuestan una tarde
El driver mysql que medio funciona
Con DB_CONNECTION=mysql las consultas normales pasan porque MariaDB habla el protocolo de MySQL, pero las vectoriales y los índices vectoriales lanzan excepción. Pasa sobre todo en proyectos que migraron de MySQL a MariaDB y nunca cambiaron la línea del .env.
La métrica del índice que se ignora en silencio
MariaDB solo usa el índice vectorial cuando la función de distancia de la consulta coincide con la del índice. Si no coincide, no avisa: hace un recorrido completo de la tabla y responde bien, solo que lento. Es el fallo más caro de diagnosticar porque los resultados son correctos.
El error 4079 y por qué necesitas 13.28
SQLSTATE[HY000]: General error: 4079 Illegal parameter data type varchar for operation 'VEC_DISTANCE_COSINE'. Las funciones VEC_DISTANCE solo aceptan VECTOR y el query builder enviaba el embedding como texto JSON. La corrección emite vec_distance_cosine(col, vec_fromtext(?)) y añade el cast. Si ves ese error, tu versión es anterior a la 13.28.
| Síntoma | Causa | Arreglo |
|---|---|---|
| Todo funciona hasta la primera consulta vectorial | DB_CONNECTION=mysql | Cambiar a mariadb |
| "Extensions are only supported by Postgres" | Se llamó ensureVectorExtensionExists | Quitar la línea |
| Error 4079 con VEC_DISTANCE_COSINE | Versión anterior a 13.28 | Actualizar Laravel |
| Respuestas correctas pero lentas | Métrica del índice distinta a la de la consulta | Reconstruir el índice con la métrica correcta |
| 1292 Incorrect vector value | Enviar JSON sin el cast | Usar AsVector |
Comprobar que el índice se usa de verdad
EXPLAIN y el escaneo completo disfrazado
Tener el índice creado no garantiza que el planificador lo use. Pasa EXPLAIN sobre la consulta real y busca el índice vectorial en el plan. Hay casos reportados de escaneo completo incluso con el índice presente, tanto en tablas pequeñas como en tablas de cientos de miles de filas, y en tablas pequeñas el planificador suele preferir recorrer todo porque le sale más barato. Prueba con volumen realista.
Qué medir después de desplegar
Latencia de la consulta con percentiles, no con una prueba suelta. Y coste por búsqueda, que suma la llamada al modelo de embeddings y la consulta a la base. Un cambio de modelo puede mejorar la calidad del primer resultado y multiplicar el coste, así que conviene medir las dos cosas a la vez.
Checklist y siguientes pasos
Diez casillas antes de dar esto por terminado:
- MariaDB 11.7 o superior confirmado en producción con SELECT VERSION().
- Laravel 13.28 o superior.
- DB_CONNECTION=mariadb, también en CI.
- Modelo de embeddings elegido antes de cargar datos.
- Columna vector con las dimensiones exactas del modelo.
- Cast AsVector en el modelo.
- Índice vectorial con la métrica que usa la consulta.
- Backfill por lotes, idempotente y con rate limit.
- EXPLAIN revisado con volumen realista.
- Latencia y coste por búsqueda medidos después del despliegue.
La base de datos ya sabe buscar por significado; lo que queda es trabajo de criterio: elegir el modelo, medir y decidir cuándo el vector no vale la pena. Si tu base es PostgreSQL, el equivalente es pgvector y el camino está en la guía de migración de MySQL a PostgreSQL. Y si el problema es que una consulta tarda cuatro segundos, antes de añadir vectores conviene mirar el plan, como se explica en cómo encontrar y arreglar una consulta lenta en MySQL 8.4.



