Bases de Datos

Búsqueda semántica en Laravel con MariaDB Vector, sin pgvector

Autorangel cruz
Publicado
Lectura8 min de lectura
Búsqueda semántica en Laravel con MariaDB Vector, sin pgvector

Laravel 13 hace búsqueda vectorial sobre MariaDB 11.7 o superior, no solo sobre PostgreSQL con la extensión pgvector. El mismo whereVectorSimilarTo que ves en los tutoriales de Postgres funciona contra MariaDB, y el framework se encarga de traducir a vec_distance_cosine() en vez de al operador <=>.

Esto importa porque el stack real de mucha gente que trabaja con Laravel es MariaDB o MySQL, no Postgres. Cuando aparece un requisito de búsqueda semántica, la reacción típica es montar un Postgres al lado solo para los embeddings, o pagar un servicio de vectores. Con MariaDB 11.7 ninguna de las dos cosas es obligatoria.

Debajo están los requisitos exactos, el SQL que Laravel genera para cada motor, cómo se guarda el vector y en qué punto MariaDB y Postgres dejan de comportarse igual. En las novedades de Laravel 13 esto aparece mencionado de pasada y solo con pgvector.

Requisitos

La documentación de búsqueda de Laravel 13 lo deja en tres piezas:

  1. Una base de datos soportada: PostgreSQL con la extensión pgvector, MariaDB 11.7 o posterior, o MongoDB con el paquete oficial de Laravel.
  2. El Laravel AI SDK (laravel/ai), que la búsqueda vectorial del query builder requiere porque los embeddings los genera la aplicación y no la base de datos.
  3. Un proveedor de embeddings configurado en ese SDK.

En MariaDB, los vectores existen desde MariaDB Community Server 11.7 y desde MariaDB Enterprise Server 11.4.5-3. Si tu servidor es anterior, no hay nada que configurar: la funcionalidad no está en el binario.

MySQL no entra en esta fiesta (y conviene saber por qué)

MySQL 9 tiene un tipo VECTOR, y ahí empieza la confusión, porque lo que falta es justamente la parte que necesitas.

En MySQL, una columna VECTOR guarda floats de 4 bytes, admite hasta 16383 entradas y por defecto son 2048. Pero no puede usarse como clave de ningún tipo (ni primaria, ni única, ni de particionado), y la función DISTANCE(), que es la que calcularía la similitud, está disponible solo para usuarios de MySQL HeatWave en OCI y de MySQL AI: no viene ni en la distribución Community ni en la Commercial.

Traducido: en MySQL comunitario puedes guardar el vector y no puedes buscar por él. Laravel es coherente con eso. La gramática base declara supportsVectorDistance() como false y solo MariaDB y PostgreSQL la sobrescriben, así que una consulta vectorial contra MySQL lanza una excepción explícita:

Vector distance queries are only supported by Postgres and MariaDB.

La migración: columna VECTOR e índice

En MariaDB, el tipo se declara con el número de dimensiones fijo, hasta un máximo de 16383. Ese número lo decide tu modelo de embeddings: 1536 si usas text-embedding-3-small de OpenAI, por ejemplo.

Lo que escribes en Laravel es esto:

use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
 
Schema::create('documents', function (Blueprint $table) {
    $table->id();
    $table->string('title');
    $table->text('content');
    $table->vector('embedding', dimensions: 1536)->index();
    $table->timestamps();
});

Ese ->index() sobre una columna vector no crea un índice normal. El blueprint detecta que el tipo de columna es vector y redirige la llamada al método vectorIndex, que sabe emitir la sintaxis específica de cada motor.

Y no llames a Schema::ensureVectorExtensionExists(). Ese helper es de Postgres y solo de Postgres: si la conexión no es PostgreSQL, lanza una RuntimeException con el mensaje Extensions are only supported by Postgres.. En MariaDB no hay extensión que instalar, el tipo viene en el servidor.

Qué SQL sale de ahí

En MariaDB, el índice se compila así:

alter table `documents` add vector index `documents_embedding_index`(`embedding`) M=6 DISTANCE=cosine

Ese M=6 DISTANCE=cosine no lo escribes tú, lo pone Laravel, y conviene saber por qué.

MariaDB permite construir el índice vectorial con distancia euclidean (que es su valor por defecto) o cosine, y con un parámetro M configurable entre 3 y 200 que regula el compromiso entre precisión, tamaño del índice y velocidad de inserción y consulta. Laravel elige cosine de forma fija, porque su método de consulta compara por similitud del coseno. Si creas el índice a mano con la distancia euclídea por defecto y luego consultas con Laravel, tienes una tabla con un índice que no sirve para la consulta que vas a lanzar.

En PostgreSQL, el mismo ->index() genera un índice HNSW con la clase de operadores vector_cosine_ops: distinta sintaxis para la misma idea.

El cast AsVector y por qué existe

En el modelo declaras el cast:

use Illuminate\Database\Eloquent\Casts\AsVector;
 
protected function casts(): array
{
    return [
        'embedding' => AsVector::class,
    ];
}

El cast existe porque los dos motores devuelven y aceptan formatos distintos, y ese es el trozo que te costaría una tarde si lo montaras a mano.

Al leer, una columna vectorial de MariaDB devuelve bytes crudos: floats de 32 bits en little endian. El cast los desempaqueta con unpack('g*', $value). PostgreSQL, en cambio, devuelve texto JSON, que se decodifica y se mapea a float.

Al escribir, PostgreSQL acepta el JSON directamente. MariaDB necesita que la conversión se haga en el servidor, así que el cast envuelve el valor en una expresión SQL vec_fromtext('[...]'), que es la función que documenta MariaDB para insertar vectores desde su representación en texto.

Con el cast puesto, tú sigues asignando y leyendo arrays de PHP y no te enteras de nada de esto.

La consulta

Generar el embedding es responsabilidad de la aplicación. Con el AI SDK, para un texto suelto:

use Illuminate\Support\Str;
 
$embedding = Str::of('Napa Valley tiene buen vino.')->toEmbeddings();

Y para un lote, que es lo que quieres al indexar contenido, porque va en una sola llamada al proveedor:

use Laravel\Ai\Embeddings;
 
$response = Embeddings::for([
    'Napa Valley tiene buen vino.',
    'Laravel es un framework PHP.',
])->generate();
 
$response->embeddings;

Con los vectores guardados, la consulta es la misma en MariaDB y en Postgres:

$documents = Document::query()
    ->whereVectorSimilarTo('embedding', $queryEmbedding, minSimilarity: 0.4)
    ->limit(10)
    ->get();

Si en vez del vector le pasas una cadena, Laravel genera el embedding por ti, con caché activada, antes de construir la consulta:

$documents = Document::query()
    ->whereVectorSimilarTo('embedding', 'las mejores bodegas de La Rioja')
    ->limit(10)
    ->get();

El umbral por defecto, si no pasas minSimilarity, es 0.6. Va de 0.0 a 1.0, donde 1.0 significa vectores idénticos. Por dentro, el método convierte esa similitud en distancia (1 - $minSimilarity), aplica whereVectorDistanceLessThan y añade un orderByVectorDistance que puedes desactivar con order: false cuando vayas a ordenar por otra columna.

El SQL que genera cada motor

En MariaDB:

vec_distance_cosine(`embedding`, vec_fromtext(?))

En PostgreSQL con pgvector:

("embedding" <=> ?)

La misma llamada en PHP produce dos expresiones distintas, y son esas las que vas a ver en el log el día que tengas que depurar una consulta lenta.

Para más control hay tres métodos que trabajan con distancias en lugar de similitudes, y que puedes combinar:

$documents = Document::query()
    ->select('*')
    ->selectVectorDistance('embedding', $queryEmbedding, as: 'distance')
    ->whereVectorDistanceLessThan('embedding', $queryEmbedding, maxDistance: 0.3)
    ->orderByVectorDistance('embedding', $queryEmbedding)
    ->limit(10)
    ->get();

Y la búsqueda vectorial se combina con where normales, que es como se acota a un equipo, una categoría o un propietario:

$documents = Document::query()
    ->where('team_id', $user->team_id)
    ->whereVectorSimilarTo('embedding', $request->input('query'))
    ->limit(10)
    ->get();

MariaDB o PostgreSQL: cómo decidir

La respuesta corta es que la elección casi nunca la manda el vector.

Quédate en MariaDB si ya lo tienes en producción, tu servidor es 11.7 o posterior, y la búsqueda semántica es una funcionalidad más de una aplicación que ya vive ahí. Montar un segundo motor para una tabla de embeddings tiene un coste operativo permanente: otra copia de seguridad, otra conexión, otra cosa que se cae de madrugada.

Vete a PostgreSQL si ya lo usas, si necesitas afinar el índice HNSW con parámetros que MariaDB no expone del mismo modo, o si tu plataforma te lo da hecho. Los Postgres de Laravel Cloud vienen con pgvector instalado.

En MySQL comunitario no lo intentes, y no es una limitación de Laravel.

Lo que no cambia en ningún caso: los embeddings los genera tu aplicación llamando a un proveedor, y eso cuesta dinero y latencia en cada indexación y en cada búsqueda que no venga de la caché. La base de datos solo guarda y compara números.

Preguntas frecuentes

¿Laravel soporta búsqueda vectorial en MariaDB?

Sí. Laravel 13 soporta consultas de similitud vectorial en PostgreSQL con la extensión pgvector y en MariaDB 11.7 o posterior, además de MongoDB con el paquete oficial. En los tres casos hace falta el Laravel AI SDK, porque los embeddings se generan en la aplicación.

¿Qué versión de MariaDB necesito para vectores?

MariaDB Community Server 11.7 o posterior. En la línea Enterprise, MariaDB Enterprise Server 11.4.5-3 o posterior. Antes de esas versiones el tipo VECTOR no existe.

¿Puedo usar whereVectorSimilarTo con MySQL?

No en MySQL Community ni Commercial. MySQL 9 tiene el tipo VECTOR, pero la función DISTANCE() solo está disponible en MySQL HeatWave sobre OCI y en MySQL AI. Laravel lo refleja lanzando una RuntimeException con el mensaje "Vector distance queries are only supported by Postgres and MariaDB".

¿Cuántas dimensiones admite una columna VECTOR en MariaDB?

Hasta 16383. El número lo fija el modelo de embeddings que uses, y tiene que coincidir con el que declares en la migración.

¿Qué distancia usa Laravel en MariaDB, coseno o euclídea?

Coseno. MariaDB usa distancia euclídea por defecto al crear un VECTOR INDEX, pero Laravel emite el índice con DISTANCE=cosine (y M=6) precisamente porque su consulta se compila a vec_distance_cosine(). Si creas el índice a mano, tiene que ser de coseno.

¿Tengo que llamar a Schema::ensureVectorExtensionExists() en MariaDB?

No, y además falla. Ese método es exclusivo de PostgreSQL: sobre cualquier otra conexión lanza Extensions are only supported by Postgres.. En MariaDB el soporte vectorial viene en el servidor y no hay extensión que habilitar.

¿Por qué hace falta el AI SDK si la base de datos ya calcula la distancia?

Porque la base de datos compara vectores, no textos. Convertir "las mejores bodegas de La Rioja" en un array de 1536 números es trabajo de un modelo de embeddings, y esa llamada la hace el SDK desde tu aplicación, tanto al indexar el contenido como al resolver cada búsqueda.

Fuentes

¿Tienes un proyecto en mente?

Trabajo con Laravel, WordPress, SEO técnico y servidores MCP. El primer paso es una llamada de descubrimiento, sin costo ni compromiso, donde me cuentas qué necesitas y te digo con honestidad si puedo ayudarte.

Hablemos