What the new native vector search actually supports, why plain MySQL got skipped, and the workaround I’m using until it catches up.
whereVectorSimilarTo() on a MySQL connection throws a RuntimeException. It always has, since these methods first shipped, and — as of the PR that just merged — it still does, with an updated error message that now mentions MariaDB by name, which is almost more frustrating than the original message, because it confirms the framework maintainers know exactly what MySQL is missing and chose not to paper over it. On August 20, PR #61250 landed on Laravel’s 13.x branch, extending the query builder’s native vector search — whereVectorSimilarTo, whereVectorDistanceLessThan, orderByVectorDistance, selectVectorDistance — to MariaDB 11.7+. Before that PR, “native vector search” in Laravel meant exactly one database: PostgreSQL with pgvector. After it, it means two. If your production app runs on plain MySQL — which is still, by a wide margin, the most common choice in the Laravel ecosystem — you are, as of this release, in exactly the same position you were in before it: locked out of the abstraction entirely.
This isn’t a framework oversight. The reason is a real, specific gap in what MySQL itself provides, and understanding it changes what “wait for MySQL support” actually means as a plan. This post covers what the MariaDB support actually does under the hood, why plain MySQL genuinely can’t get the same treatment without a MySQL distribution most people aren’t running, and the workaround I’ve actually built to keep a MySQL-based app functional in the meantime.
What Actually Shipped
Before this PR, the vector-search methods worked through a hard-coded instanceof PostgresConnection check sitting directly inside Query\Builder, with pgvector’s <=> cosine-distance operator inlined right there in the builder rather than living in the database driver’s grammar where dialect-specific SQL generation is supposed to live. That structure is exactly why MySQL and MariaDB couldn’t be added incrementally before now — the check for “which database is this” and the actual distance SQL were welded together in one place, with no seam for a second database to plug into.
The PR’s real contribution is moving that decision to where it belongs: a supportsVectorDistance() and compileVectorDistanceExpression($column) pair added to the Grammar class, overridden per-driver — the same pattern Laravel has used for years for other dialect-specific SQL, like compileRandom(). With the seam in the right place, adding MariaDB support became close to mechanical: its grammar compiles the distance expression into vec_distance_cosine(), the native SQL function MariaDB has shipped since version 11.7 Community (11.4.5-3 on Enterprise), operating against a real VECTOR column type that MariaDB also added natively around the same release.
// Once you're on MariaDB 11.7+, this now works exactly like it always
// has on Postgres — same fluent API, different database underneath
$documents = Document::query()
->whereVectorDistanceLessThan('embedding', $queryEmbedding, maxDistance: 0.4)
->orderByVectorDistance('embedding', $queryEmbedding)
->selectVectorDistance('embedding', $queryEmbedding, as: 'distance')
->limit(10)
->get();
// The schema side was already in place from an earlier PR —
// a native vector column type and a real vector index
Schema::create('documents', function (Blueprint $table) {
$table->id();
$table->vector('embedding', 768);
$table->vectorIndex('embedding'); // ANN lookups — without this,
// every similarity search is a
// full table scan
$table->timestamps();
});
A follow-up PR landed five days later with an SQL correction and an AsVector Eloquent cast, rounding out the MariaDB implementation to feel like a genuinely first-class citizen of the abstraction, not a bolted-on afterthought — which is a meaningfully different outcome than “MariaDB technically works if you write raw SQL,” and it’s the right bar to hold this against.
Why Plain MySQL Actually Can’t Get the Same Treatment
This is the part worth understanding precisely, because it changes what “just wait for MySQL support” means as a plan. Standard MySQL — Community or Commercial, the distributions almost everyone is actually running — has no native vector distance function at all. MySQL 9 does have a VECTOR column type for storage, but the DISTANCE() / VECTOR_DISTANCE() functions that would actually compute similarity are only available on MySQL HeatWave, Oracle Cloud Infrastructure’s managed MySQL variant, and MySQL AI — not in the MySQL binaries most self-hosted or conventionally-managed MySQL deployments are running. A MariaDB grammar override works because MariaDB genuinely ships the function in its open, generally-available distribution. The equivalent MySQL override has nothing to call, because the function it would need to call doesn’t exist in the software most Laravel-on-MySQL apps actually have installed.
There’s a further wrinkle worth knowing, raised directly in the framework’s own GitHub discussions during review: MySQL’s VECTOR column type documents that it cannot be used as any kind of key, including an index — meaning even the storage side of a hypothetical MySQL implementation would have real, documented indexing restrictions that MariaDB and Postgres don’t share. Community feedback on the PR discussion made the same point precisely: exposing MySQL through the same high-level API without those restrictions being obvious would let an application appear portable across MySQL and MariaDB while silently losing indexing, or failing outright, the moment it actually ran against Community MySQL in production. The suggested path — document the extension methods generically, add MySQL grammar support for the column type itself with version-specific tests, and define an explicit capability matrix by MySQL distribution before exposing it through the same fluent API — is the responsible way to eventually close this gap. It is not a fast path, and there’s no committed timeline for it as of this writing.
The honest summary: this isn’t Laravel dragging its feet on MySQL support. It’s Laravel declining to paper over a real capability gap between database distributions by exposing a unified API that would quietly behave differently — or not at all — depending on which specific MySQL build happens to be running underneath it.
The Workaround — Computing Similarity Without Native Distance Functions
For a MySQL-based app that needs semantic search now, the two realistic options are migrating the specific vector-search workload to Postgres or MariaDB (viable, and worth genuinely considering if a new project hasn’t committed to a database yet), or handling similarity computation somewhere other than a native SQL distance function. Here’s the version of the second option I’ve actually built and am running.
// Store embeddings as JSON — no native VECTOR type advantage on MySQL
// anyway, since the distance functions that would make it worthwhile
// aren't available regardless of column type
Schema::create('documents', function (Blueprint $table) {
$table->id();
$table->text('body');
$table->json('embedding'); // a plain float array, JSON-encoded
$table->timestamps();
});
// app/Services/CosineSimilarity.php — computed in PHP, not the database
class CosineSimilarity
{
public static function between(array $a, array $b): float
{
$dotProduct = 0.0;
$magnitudeA = 0.0;
$magnitudeB = 0.0;
foreach ($a as $i => $value) {
$dotProduct += $value * $b[$i];
$magnitudeA += $value ** 2;
$magnitudeB += $b[$i] ** 2;
}
if ($magnitudeA === 0.0 || $magnitudeB === 0.0) {
return 0.0;
}
return $dotProduct / (sqrt($magnitudeA) * sqrt($magnitudeB));
}
}
// app/Services/MysqlVectorSearch.php — the actual workaround, with the
// scale limitation stated explicitly rather than discovered in production
class MysqlVectorSearch
{
public function search(array $queryEmbedding, int $limit = 10, ?int $candidateCap = 5000): Collection
{
// This is the honest cost of the workaround: every candidate row's
// embedding gets pulled into PHP memory and scored one at a time.
// A LIMIT-bounded candidate set is essential — this does NOT scale
// to searching an unfiltered million-row table the way a real
// ANN index does, and pretending otherwise is how this workaround
// quietly becomes a production incident at the wrong table size.
$candidates = Document::query()
->select('id', 'body', 'embedding')
->latest()
->limit($candidateCap)
->get();
return $candidates
->map(function ($doc) use ($queryEmbedding) {
$doc->similarity = CosineSimilarity::between($queryEmbedding, $doc->embedding);
return $doc;
})
->sortByDesc('similarity')
->take($limit)
->values();
}
}
Being explicit about what this workaround actually is: a stopgap for a small-to-moderate dataset, not a substitute for a real ANN index. Computing cosine similarity in PHP against every row in a candidate set is fine at a few thousand rows and a real, measurable performance cliff well before a few hundred thousand — this is fundamentally a brute-force linear scan happening in application memory instead of a database’s native, indexed distance computation, and it should be treated and monitored as exactly that, not quietly relied on past the point where it stops being fast enough to matter.
Narrowing the candidate set with something MySQL already does well is what keeps this workable at a slightly larger scale. Combining a cheap, indexed pre-filter — a full-text search match, a category filter, a recency window — with the PHP-side similarity scoring applied only to that narrowed candidate set, rather than the entire table, is the same hybrid-search instinct covered in an earlier post on semantic search, applied here out of necessity rather than by design: MySQL’s own indexed full-text search narrows the candidates first, and the expensive PHP-side computation only runs against however many rows survive that filter.
// A narrowed, pre-filtered candidate set — cheap, indexed keyword search
// first, expensive similarity scoring only against what survives it
$candidates = Document::query()
->whereFullText('body', $searchTerms)
->select('id', 'body', 'embedding')
->limit(500) // a much smaller, keyword-relevant candidate pool
->get();
The other realistic path, worth naming rather than avoiding: the x-laravel/embedding ecosystem of community driver packages (including a MySQL 9-targeted driver) exists specifically because this gap is real and other teams are solving it the same way — worth evaluating rather than reinventing if the workaround above needs to become more robust than a hand-rolled service class, though it’s worth going in with clear eyes about the same underlying constraint: no community package can call a distance function that genuinely isn’t present in the MySQL binary it’s running against, and any MySQL-targeted package is working within the same limitation, not around it.
What This Actually Means for a Database Decision Made Today
For a new project not yet locked into a database engine, and where semantic search is a known, near-term requirement rather than a hypothetical one, this is a genuinely relevant data point in that decision — not the deciding factor on its own, but a real one. MariaDB 11.7+ now gets first-class treatment in Laravel’s vector-search abstraction, indistinguishable in code from Postgres’s pgvector path. Plain MySQL doesn’t, for reasons rooted in what the database itself provides rather than anything Laravel chose to deprioritize, and there’s no committed timeline suggesting that changes soon.
For a project already running production MySQL with real data and real migration cost, the honest calculus is the same one covered in the earlier post on when a vector database is actually justified in the first place: if the actual query volume needing semantic search is modest, the PHP-side workaround with a sensible candidate cap and a keyword pre-filter is a legitimate, monitorable stopgap, not an embarrassment. If semantic search is about to become a core, high-volume part of the product, the workaround’s brute-force ceiling is a real constraint worth planning around now, and migrating the specific vector-search workload to a database that actually supports it natively — not necessarily the whole application, just the piece that needs it — is worth seriously costing out against staying on the workaround indefinitely.
The One Rule
The gap here isn’t a Laravel problem to complain about — it’s an honest reflection of a real difference between database distributions, and the framework choosing not to paper over that difference with an abstraction that would quietly misbehave depending on which MySQL binary happened to be running underneath it. The actual lesson is the same one that applies to every “wait for the framework to add support” situation: understand why the gap exists before deciding whether to wait for it to close, build a workaround with its real limitations stated explicitly rather than discovered in production, and treat “what does the underlying database actually, natively support” as the real constraint — because no amount of framework-level abstraction can call a function that genuinely isn’t there.
