How the new Eloquent cast turns embedding columns into typed float arrays — and what it still doesn’t do for you at the model layer.
$document->embedding = $vector; $document->save(); throws SQLSTATE[HY000]: General error: 1292 Incorrect vector value, on MariaDB, for a value that’s a perfectly normal PHP array of floats. The instinct is to reach for Laravel’s existing array cast, since that’s what every other “store structured data in a column” problem in Eloquent has used for years. It doesn’t work here, and it can’t — not as a bug to file, as a structural mismatch between what array produces and what a MariaDB VECTOR column will actually accept. AsVector, the new cast added in PR #61337, exists specifically because that mismatch had no clean workaround at the model layer until now. This post is what it actually does, why the plain array cast was never going to be fixable for this case, and the parts of working with embeddings at the model layer that AsVector still leaves entirely to you.
Why array Was Never Going to Work Here
The array cast’s job is straightforward: serialize a PHP array to JSON on write, decode JSON back to a PHP array on read. That’s exactly the wrong shape for a MariaDB VECTOR column, for two independent reasons, not one.
On read: MariaDB doesn’t return a vector column’s contents as JSON text at all. It returns raw, packed bytes — a little-endian float32 binary representation, the same layout you’d get from packing an array of C floats directly. Handing that to json_decode(), which is all the array cast does on the way in, doesn’t produce a parse error. It produces garbage, or a decode failure, because the bytes were never JSON to begin with.
On write: binding a JSON string to a VECTOR column doesn’t fail gracefully either — MariaDB rejects it outright with error 1292, Incorrect vector value. And the seemingly obvious fix, binding the raw packed bytes directly as a parameter, doesn’t work either, regardless of whether it’s sent as PDO::PARAM_LOB or anything else. The column has a specific expected input format on the write side too, and neither “JSON text” nor “raw bytes as a blob parameter” satisfies it.
// ❌ What looks like the obvious fix, and isn't one
protected $casts = [
'embedding' => 'array', // json_decode() on read, json_encode() on write —
// neither direction matches what a VECTOR column
// actually produces or accepts on MariaDB
];
This is the reason AsVector had to be a purpose-built cast rather than a documentation note saying “use array here” — the existing cast’s serialization format and the database column’s actual wire format were never compatible, on either side of the read/write boundary.
What AsVector Actually Does, Read Side and Write Side
use Illuminate\Database\Eloquent\Casts\AsVector;
class Document extends Model
{
protected function casts(): array
{
return [
'embedding' => AsVector::class,
];
}
}
On read, AsVector handles both database dialects’ actual wire formats explicitly: MariaDB’s little-endian float32 packed bytes get unpacked back into a PHP array of floats, and PostgreSQL/pgvector’s text representation (the [0.123,0.456,...]-style string pgvector’s vec_totext() produces) gets parsed into the same shape. Either database, same result at the model layer — a plain PHP array of floats, indistinguishable regardless of which driver actually stored it.
$document = Document::find(1);
$document->embedding; // [0.0123, -0.0456, 0.789, ...] — a plain array of
// floats, whether the row came from MariaDB's binary
// format or Postgres's text format
On write, the cast is dialect-aware in the other direction too: for MariaDB, it doesn’t hand the database a JSON string or raw bytes — the two things confirmed not to work above — it writes vec_fromtext('[...]'), MariaDB’s own SQL function for converting a text vector representation into its internal storage format, which is the one write path MariaDB actually accepts for a VECTOR column. For every other supported dialect, it writes a plain JSON string, matching what pgvector expects.
$document->embedding = $embedding; // accepts a plain PHP array, or
// anything Arrayable — a Laravel
// Collection works here directly,
// no ->toArray() needed first
$document->save(); // MariaDB: wrapped as vec_fromtext('[...]')
// Postgres: written as a plain JSON string
// — the cast picks the right path per-connection,
// invisibly, based on the underlying driver
The part worth being precise about: this isn’t a generic binary-safe blob cast that happens to also work for vectors. It’s specifically encoding and decoding the two real wire formats these two supported databases actually use for a vector column, with the driver-specific SQL wrapping needed on the write side to satisfy each one’s actual input requirements — which is exactly the kind of dialect-straddling logic that belongs in a framework-level cast rather than reimplemented per-application.
The Complete Loop, Migration Through Query
// The schema side — already in place from earlier releases,
// AsVector is the missing model-layer half
Schema::create('documents', function (Blueprint $table) {
$table->id();
$table->vector('embedding', 768);
$table->vectorIndex('embedding'); // HNSW index — without it, every
// similarity search is a full scan
});
class Document extends Model
{
protected function casts(): array
{
return ['embedding' => AsVector::class];
}
}
// Writing — a plain array, no manual encoding, no driver-specific branching
Document::create(['embedding' => $embedding]);
// Reading and querying — the cast handles decode on the way out,
// and the query builder's native vector methods (covered in earlier
// posts on this blog) handle the actual similarity search
Document::query()
->whereVectorSimilarTo('embedding', $queryVector, minSimilarity: 0.7)
->limit(10)
->get();
This closes the loop that’s been building across several recent framework changes — the vector() column type, vectorIndex(), whereVectorSimilarTo() and the rest of the native query methods, dropVectorIndex() for clean rollbacks — all of which handled schema and querying. AsVector is specifically the piece that makes the model layer, the actual $model->embedding = $array and $model->embedding ergonomics, work without a developer hand-rolling the byte-packing and SQL-function wrapping themselves on every single read and write.
What AsVector Still Doesn’t Do For You
This is the part worth being honest about, because the cast’s narrow scope is deliberate, not a gap waiting to be filled by a future release — it’s a serialization layer, not an embeddings pipeline.
It does not generate embeddings. AsVector casts a value you already have. Getting from $document->body to an actual array of 768 floats — calling an embedding model, handling the API round trip, deciding when to regenerate an embedding after a content edit — is entirely outside its scope. That’s the job of Laravel’s AI SDK directly, or a purpose-built package layered on top of Eloquent for exactly this: automatic embedding generation, triggered by model events, is what packages like x-laravel/embedding exist to provide, sitting one layer above what AsVector does. AsVector doesn’t know or care where the array of floats came from — only that it’s an array of floats going in, or coming out.
It does not validate dimensions. Passing a 512-float array to a column declared vector('embedding', 768) is a database-level constraint violation, not something the cast checks before handing the value off. If an embedding model gets swapped — a provider migration, a model version upgrade with a different output size — and the column’s declared dimension count doesn’t match the new model’s actual output, that mismatch surfaces as a database error at write time, not a friendly validation message at the model layer. Catching that earlier, with a clear error message before the query even runs, is validation logic worth adding explicitly in a Form Request or a model observer, not something to expect from the cast itself.
It does not handle re-indexing on content change. Editing the source content a stored embedding was generated from doesn’t automatically regenerate or re-cast anything — AsVector only fires when the embedding attribute itself is actually set. Keeping an embedding in sync with the content it represents is exactly the queued re-indexing job pattern covered in earlier posts on building semantic search — a saved model event, checking wasChanged('body'), dispatching a job that calls the embedding model and reassigns $document->embedding — none of which AsVector does or is meant to do on its own.
It does not pick a similarity metric or run the search. Cosine similarity, dot product, Euclidean distance — that’s the query builder’s whereVectorSimilarTo() and its siblings, operating on the column AsVector reads and writes, not a concern the cast itself has any opinion about.
The Actual Shape of a Complete Setup
Schema: vector() column + vectorIndex() — storage, already shipped
Model layer: AsVector cast — the piece this post covers
Generation: Laravel AI SDK, or an embedding package — not AsVector's job
Sync: a queued job on content change — not AsVector's job
Querying: whereVectorSimilarTo() and friends — not AsVector's job
AsVector is one clean layer in that stack, and its value is precisely that it stays in its lane — a serialization cast, doing exactly what a cast is supposed to do (translate between a PHP-shaped value and a database-shaped value, correctly, per dialect), rather than trying to also be an embeddings pipeline, a validation layer, and a sync mechanism bundled into one class that would inevitably do all four jobs worse than four focused pieces would.
The One Rule
The honest way to read AsVector is as the specific, narrow fix for a specific, narrow problem: two databases store a vector column in two genuinely incompatible wire formats, and the existing array cast could satisfy neither one, on either the read or the write side. That’s a real gap, and closing it is real, useful framework work — the 1292 Incorrect vector value error at the top of this post doesn’t happen anymore once AsVector is in place. But a cast closing the serialization gap doesn’t mean the rest of the embeddings stack materialized alongside it. Generation, dimension validation, re-indexing on content change, and the similarity search itself all still need building, the same as they did before this shipped — AsVector just means none of that work needs to also include hand-rolling byte-packing logic for MariaDB along the way.
