devilsberg/laravel-mariadb-vector
MariaDB 11.7+ native vector storage and similarity search for Laravel. Adds a VECTOR column type, binary-safe Eloquent casts, and nearestNeighbors() query macros powered by VEC_DISTANCE functions — essentially pgvector, but for MariaDB.
MariaDB 11.7+ native vector storage and search for Laravel.
This package brings vector column support, binary-safe casting, and similarity search macros to Eloquent — powered by MariaDB's native VECTOR type and VEC_DISTANCE_* functions. Think of it as the MariaDB equivalent of pgvector for Laravel.
composer require devilsberg/laravel-mariadb-vector
Publish the config file:
php artisan vendor:publish --tag=mariadb-vector-config
Schema::create('articles', function (Blueprint $table) {
$table->id();
$table->string('title');
$table->text('body');
$table->vector('embedding', 768);
$table->vectorIndex('embedding'); // enables fast ANN search
$table->timestamps();
});
use Devilsberg\LaravelMariadbVector\Casts\VectorCast;
class Article extends Model
{
protected function casts(): array
{
return [
'embedding' => VectorCast::class,
];
}
}
$embedding = $yourEmbeddingProvider->embed($article->body); // your code
$article->embedding = $embedding; // float array, e.g. [0.1, 0.2, ...]
$article->save();
use Devilsberg\LaravelMariadbVector\Distance;
$queryVector = $yourEmbeddingProvider->embed('climate change effects');
$results = Article::query()
->nearestNeighbors('embedding', $queryVector, Distance::Cosine)
->limit(10)
->get();
// Each result has a `score` column — higher is more similar (0–1)
foreach ($results as $article) {
echo $article->title . ' — ' . $article->score;
}
Without a vector index every similarity search performs a full table scan — fine for thousands of rows, slow at scale. MariaDB 11.7 supports VECTOR INDEX for approximate nearest neighbor (ANN) lookups, exposed via the standard vectorIndex() Blueprint method that Laravel 12 provides.
Schema::create('articles', function (Blueprint $table) {
$table->id();
$table->vector('embedding', 768);
$table->vectorIndex('embedding'); // ALTER TABLE ... ADD VECTOR INDEX runs after CREATE TABLE
$table->timestamps();
});
By default the index name follows Laravel's standard index naming — {table}_{column}_vectorindex:
$table->vectorIndex('embedding'); // → articles_embedding_vectorindex
$table->vectorIndex('embedding', 'articles_vec_idx'); // → articles_vec_idx (custom)
Schema::table('articles', function (Blueprint $table) {
$table->vectorIndex('embedding');
});
Note: MariaDB requires the vector column to be
NOT NULLto add a vector index. Attempting to index a nullable vector column will raise a database error.Note: MariaDB 11.7 supports only one
VECTOR INDEXper table. Attempting to add a second one will raise a database error.
This package handles storage and search only. You generate embeddings however you want — Ollama, Mistral, Cohere, a local Python model, or anything that produces a float array.
// Ollama (local)
$response = Http::post('http://localhost:11434/api/embed', [
'model' => 'nomic-embed-text',
'input' => $text,
]);
$embedding = $response->json('embeddings.0');
// Mistral
$response = Http::withToken(config('services.mistral.key'))
->post('https://api.mistral.ai/v1/embeddings', [
'model' => 'mistral-embed',
'input' => [$text],
]);
$embedding = $response->json('data.0.embedding');
// Any source — just pass a float array
$article->embedding = $embedding;
$article->save();
VectorCast handles conversion between MariaDB's binary VECTOR format and PHP float arrays.
protected function casts(): array
{
return [
'embedding' => \Devilsberg\LaravelMariadbVector\Casts\VectorCast::class,
];
}
array<float>array<float> to a VEC_FromText('[0.1, 0.2, ...]') expressionnull for null values in both directionsnull for empty arraysThe column supports ->nullable() in migrations:
$table->vector('embedding', 768)->nullable();
All macros are registered on Illuminate\Database\Eloquent\Builder.
Filter results by vector distance threshold.
whereVectorSimilarTo(
string $column,
array $input,
float $threshold, // required — what counts as "similar" is application-specific
?string $metric = null // defaults to config('vector.distance_metric')
)
Article::query()->whereVectorSimilarTo('embedding', $queryVector, 0.5)->get();
// Euclidean distance
Article::query()->whereVectorSimilarTo('embedding', $queryVector, 0.5, metric: 'EUCLIDEAN')->get();
Generates: WHERE VEC_DISTANCE_COSINE(\embedding`, VEC_FromText(?)) < ?`
Sort results by vector distance (nearest first).
orderByVectorDistance(
string $column,
array $input,
?string $metric = null
)
Article::query()->orderByVectorDistance('embedding', $queryVector)->get();
Generates: ORDER BY VEC_DISTANCE_COSINE(\embedding`, VEC_FromText(?)) asc`
Include the distance score in query results.
selectVectorDistance(
string $column,
array $input,
string $as = 'distance',
?string $metric = null
)
Article::query()
->select('*')
->selectVectorDistance('embedding', $queryVector, as: 'score')
->orderByVectorDistance('embedding', $queryVector)
->get()
->each(fn ($article) => dump($article->title, $article->score));
Generates: VEC_DISTANCE_COSINE(\embedding`, VEC_FromText(?)) as `score``
The recommended macro for most use cases. Computes the distance once per row as a normalized score column and orders results by highest score first. More efficient than chaining the three individual macros.
nearestNeighbors(
string $column,
array $input,
Distance $distance = Distance::Cosine
)
use Devilsberg\LaravelMariadbVector\Distance;
Article::query()
->nearestNeighbors('embedding', $queryVector, Distance::Cosine)
->limit(10)
->get();
// With Euclidean distance
Article::query()
->nearestNeighbors('embedding', $queryVector, Distance::Euclidean)
->limit(5)
->get();
Generates:
SELECT *, 1.0 - (VEC_DISTANCE_COSINE(`embedding`, VEC_FromText(?))) as `score`
FROM `articles`
ORDER BY `score` desc
The score is a normalized similarity value — higher means more similar:
1.0 - distance, range approximately [-1, 1] (in practice [0, 1] for LLM embeddings)1.0 - distance / SQRT(2), normalized to approximately [0, 1]For advanced queries where you need threshold filtering alongside ordering, use the individual macros:
$results = Article::query()
->select('id', 'title')
->whereVectorSimilarTo('embedding', $queryVector, 0.4)
->selectVectorDistance('embedding', $queryVector, as: 'score')
->orderByVectorDistance('embedding', $queryVector)
->limit(10)
->get();
Published to config/vector.php:
return [
// Default vector dimensions (should match your embedding model output)
'default_dimensions' => 768,
// Distance metric: "COSINE" or "EUCLIDEAN"
'distance_metric' => 'COSINE',
];
| Key | Default | Description |
|---|---|---|
default_dimensions |
768 |
Default dimensions for vector columns |
distance_metric |
COSINE |
Distance function: COSINE or EUCLIDEAN |
Note:
DOTproduct distance is not supported by MariaDB. Configuring it will throw anInvalidArgumentException.
Run the unit tests:
vendor/bin/phpunit --testsuite=Unit
Run the integration tests (requires a live MariaDB instance):
vendor/bin/phpunit --testsuite=Integration
Configure MariaDB credentials in phpunit.xml:
<php>
<env name="DB_HOST" value="127.0.0.1"/>
<env name="DB_PORT" value="3306"/>
<env name="DB_DATABASE" value="your_test_db"/>
<env name="DB_USERNAME" value="your_user"/>
<env name="DB_PASSWORD" value="your_password"/>
</php>
Run the full suite:
vendor/bin/phpunit
If this package saves you time, consider buying me a coffee.
The MIT License (MIT). Please see License File for more information.
How can I help you explore Laravel packages today?