6 min read

Laravel 13 Vector Search Now Runs on MariaDB, and the AsVector Cast Is What Makes It Usable

whereVectorSimilarTo now compiles to MariaDB's vec_distance_cosine, and the new AsVector Eloquent cast handles the binary vector column format.

Featured image for "Laravel 13 Vector Search Now Runs on MariaDB, and the AsVector Cast Is What Makes It Usable"

When Laravel 13 shipped its vector search methods, the docs came with a quiet asterisk: PostgreSQL only. If your production database was MySQL or MariaDB, whereVectorSimilarTo() threw a RuntimeException and you went shopping for a separate vector store.

That changed in August. Two pull requests landed in the 13.x branch that make the whole vector query API work against MariaDB, and the second one fixes something the first one got wrong in a way that only shows up against a real server.

What was hard-coded

The original implementation had two Postgres assumptions baked into Illuminate\Database\Query\Builder. The support check was an instanceof PostgresConnection test in ensureConnectionSupportsVectors(), and the distance SQL itself was the pgvector-specific <=> operator, inlined directly into the builder as "{$col} <=> ?".

PR #61250 by Rhaima96, merged by Taylor Otwell on August 20, moved both of those into the grammar layer. There is now a Grammar::supportsVectorDistance() and a Grammar::compileVectorDistanceExpression($column) pair, following the same base-plus-override pattern Laravel already uses for compileRandom() and supportsSavepoints(). PostgresGrammar keeps emitting <=>. MariaDbGrammar emits vec_distance_cosine().

All four methods route through it: whereVectorSimilarTo, whereVectorDistanceLessThan, orderByVectorDistance, and selectVectorDistance.

Plain MySQL still throws. That is deliberate and worth understanding before you plan a migration. Standard MySQL Community and Enterprise binaries have no native vector distance function. DISTANCE() and VECTOR_DISTANCE() exist on MySQL HeatWave and MySQL AI, not in the server you run in Docker. The PR author considered a PHP-side fallback that fetches rows and computes similarity in memory, and rejected it, because it would silently break limit() and pagination semantics while pretending to be the same query. That is the right call.

The bug the first PR shipped

vec_distance_cosine(col, ?) looks correct and is not. MariaDB’s VEC_DISTANCE_* functions only accept VECTOR arguments, and the query builder binds the embedding as JSON text. Against a real MariaDB 11.8.9 server, every one of those four methods failed with:

SQLSTATE[HY000]: General error: 4079 Illegal parameter data type varchar
for operation 'VEC_DISTANCE_COSINE'

PR #61337 by eas4ai, merged August 25, changed MariaDbGrammar::compileVectorDistanceExpression() to emit vec_distance_cosine(col, vec_fromtext(?)) instead. The bindings do not change, they are still JSON text, and EXPLAIN confirms the MHNSW vector index is still used for order by vec_distance_cosine(..., vec_fromtext(?)). Wrapping the placeholder does not defeat the index, which was the obvious worry.

Why you need AsVector

The same PR added Illuminate\Database\Eloquent\Casts\AsVector, and this is the part that turns a working query builder into something you can actually put in a model.

On MariaDB, the plain array cast cannot work in either direction. Reading, the column comes back as little-endian float32 bytes, not JSON. Writing, a JSON string bound to a VECTOR column is rejected with 1292 Incorrect vector value, and binding the raw packed bytes as a parameter is rejected too, regardless of PDO::PARAM_LOB.

AsVector is driver-portable. On read it decodes MariaDB’s binary format with unpack('g*'), handles pgvector and vec_totext() JSON text, and handles a not-yet-persisted MariaDB value so that $model->embedding works immediately after create(). On write it accepts an array or any Arrayable, including a Collection. On MariaDB it stores a vec_fromtext('[...]') expression, because the conversion has to happen server-side. On every other driver it stores the JSON string, which pgvector accepts as-is.

The inlined JSON is safe, for the record. json_encode(..., JSON_THROW_ON_ERROR) rejects NaN and Inf, so the string can only ever contain digits, ., -, e, commas and brackets.

Putting it together

Say php[architect] wants semantic search across a decade of magazine articles. The migration:

Schema::create('articles', function (Blueprint $table) {
    $table->id();
    $table->string('title');
    $table->text('body');
    $table->foreignId('issue_id')->constrained();
    $table->vector('embedding', 768);
    $table->vectorIndex('embedding');
    $table->timestamps();
});

The vector() and vectorIndex() blueprint methods have been in MariaDbGrammar for a while. It was only the query side that was missing.

The model:

use Illuminate\Database\Eloquent\Casts\AsVector;

class Article extends Model
{
    protected function casts(): array
    {
        return [
            'embedding' => AsVector::class,
        ];
    }
}

Storing one:

use Illuminate\Support\Str;

Article::create([
    'title'     => 'Profiling PHP 8.5 Under Load',
    'body'      => $body,
    'issue_id'  => $issue->id,
    'embedding' => Str::of($body)->toEmbeddings(),
]);

And querying:

$articles = Article::query()
    ->where('issue_id', $issue->id)
    ->whereVectorSimilarTo('embedding', 'how do I find a slow query', minSimilarity: 0.4)
    ->limit(10)
    ->get();

Two things about that last one. whereVectorSimilarTo() filters on cosine similarity above the minSimilarity threshold and orders by relevance automatically, most similar first, so you do not need a separate orderByVectorDistance() unless you want manual control. And when you pass a plain string instead of an embedding array, Laravel generates the embedding for you through your configured provider. That convenience requires the Laravel AI SDK.

If you want the raw numbers instead of the ranking, selectVectorDistance(), whereVectorDistanceLessThan() and orderByVectorDistance() give you the lower-level surface.

Versions and the docs lag

You need MariaDB 11.7 or later on Community, or 11.4.5-3 on Enterprise. Be a little careful with 11.7: MariaDB shipped vector search there as a rolling release, and it reached general availability in 11.8 LTS. If you are picking a version for production, pick 11.8.

Framework side, the MariaDB grammar support landed in Laravel 13.27, and the vec_fromtext() fix and AsVector cast followed in the next release a few days later. Both appear in Laravel’s August 2026 changelog, as “MariaDB Vector Distance Queries” and “Portable Eloquent Vector Casts.” Take the whole thing as one feature and make sure you are past both, because 13.27 on its own will hand you that 4079 error.

The one thing that has not caught up is the documentation. The search page in the Laravel docs still says vector search requires PostgreSQL with pgvector, and the storing-vectors section still tells you to cast the column to array. That advice is correct for Postgres and wrong for MariaDB. Go by the changelog and the merged code until the docs catch up.

Worth noting alongside this: Scout picked up semantic and hybrid search on its database engine in the same changelog window, plus a Turbopuffer engine and hybrid Meilisearch queries. If you would rather have the Searchable trait keep an index in sync than write vector queries by hand, that path exists now too.

For a lot of teams this closes the last real reason to bolt a separate vector database onto a Laravel app. If your articles table already lives in MariaDB, your embeddings can live next to them.

Sources: MariaDB Vector Distance Queries, Laravel changelog, laravel/framework PR #61250, laravel/framework PR #61337, Search, Laravel documentation, MariaDB Vector, MariaDB.org, VEC_DISTANCE_COSINE, MariaDB Documentation.