symfony/ai-maria-db-store
MariaDB vector store integration for Symfony AI Store, enabling vector search and RAG with MariaDB's native VECTOR type, VECTOR INDEX, and distance functions. Requires MariaDB 11.7 or later.
Install the Package:
composer require symfony/ai-maria-db-store:^0.15
Ensure your composer.json includes Symfony’s AI components if not already present, and pin the whole family to ^0.15:
"require": {
"symfony/ai": "^0.15",
"symfony/dependency-injection": "^7.0"
}
Configure MariaDB:
mariadb --version). 0.15 raises the floor from 11.7 to 11.8 — Distance handling depends on it.VECTOR column and let the store build the index, or create it yourself with the same distance metric you configure in PHP:
CREATE TABLE ai_embeddings (
id INT AUTO_INCREMENT PRIMARY KEY,
content TEXT,
metadata JSON,
embedding VECTOR(1536), -- Adjust dimensions to your embedding size
INDEX vec_idx USING HNSW(embedding) WITH (distance_type = 'COSINE')
) ENGINE=InnoDB;
.env:
DB_MARIADB_CONNECTION=mariadb
DB_MARIADB_URL="mysql://user:pass@host/db?serverVersion=11.8"
Register the Store:
Add a service provider (e.g., App\Providers\MariaDbStoreServiceProvider):
use Symfony\Component\AI\Store\AiStoreInterface;
use Symfony\AI\MariaDbStore\MariaDbStore;
public function register()
{
$this->app->singleton(AiStoreInterface::class, function ($app) {
return new MariaDbStore(
$app['db']->connection('mariadb')->getPdo(),
[
'table' => 'ai_embeddings',
'vector_column' => 'embedding',
'distance' => 'cosine', // 0.15 creates the index with this metric
'dimensions' => 1536,
]
);
});
}
First Use Case: Insert and query embeddings in a Laravel controller:
use Symfony\Component\AI\Store\AiStoreInterface;
public function storeEmbedding(AiStoreInterface $store)
{
$embedding = [0.1, 0.2, ..., 0.1]; // Your 1536-dim vector
$store->insert([
'content' => 'Sample text',
'metadata' => ['category' => 'tech'],
'embedding' => $embedding,
]);
// Query with similarity (vector is parsed once per query in 0.15)
$results = $store->query($embedding, [
'limit' => 5,
'filter' => ['category' => 'tech'],
]);
// Cheap size checks without fetching anything
$total = $store->count();
$techCnt = $store->count(['category' => 'tech']);
// Results are VectorDocumentInterface instances
foreach ($results as $document) {
// $document->getId(), $document->getContent(), $document->getMetadata()
}
}
CRUD Operations:
insert() for single records or insertMany() for batches:
$store->insertMany([
['content' => 'Doc 1', 'embedding' => $vec1, 'metadata' => [...]],
['content' => 'Doc 2', 'embedding' => $vec2, 'metadata' => [...]],
]);
count() accepts the same filter shape as query(), so you can build paginators, dashboards, and “is the index populated?” health checks without transferring vectors:
$paginator = new LengthAwarePaginator(
items: $store->query($vector, ['limit' => $perPage, 'offset' => $offset]),
total: $store->count($filters),
perPage: $perPage,
currentPage: $page,
);
insert() (no native update; use remove() + insert()).$store->remove(['id' => 1]); // By ID
$store->remove(['category' => 'deprecated']); // By metadata filter
Working with Documents (VectorDocumentInterface):
Symfony\Component\AI\Store\VectorDocumentInterface. Type-hint the interface in your own code rather than assuming a concrete class or a plain array shape:
use Symfony\Component\AI\Store\VectorDocumentInterface;
$documents = collect($store->query($vector, ['limit' => 10]))
->map(fn (VectorDocumentInterface $doc) => new SearchHit(
id: $doc->getId(),
text: $doc->getContent(),
meta: $doc->getMetadata(),
));
EmbeddingDocument), implement VectorDocumentInterface so it stays compatible with the store.Hybrid Queries: Combine vector similarity with SQL filters:
$results = $store->query($queryVector, [
'limit' => 10,
'filter' => [
'category' => 'tech',
'created_at' => ['>', '2023-01-01'],
],
]);
WHERE syntax (e.g., JSON_EXTRACT(metadata, '$.priority') > 5).Batch Processing:
insertMany() for efficiency (test with 1K+ records).dispatch(new ProcessEmbeddings($store, $batch));
Schema Management:
distance (cosine/l2) up front — changing it later means dropping and recreating the index, and a pre-existing hand-made index with a different metric won’t match.dimensions change (no ALTER TABLE support for VECTOR).ALTER TABLE ai_embeddings ADD INDEX vec_idx USING HNSW(embedding)
WITH (distance_type = 'COSINE'); -- must match the store's `distance`
Laravel-Specific:
class EmbeddingRepository {
public function __construct(private AiStoreInterface $store) {}
public function findSimilar($vector, array $filters = []) {
return $this->store->query($vector, ['filter' => $filters]);
}
public function total(array $filters = []): int {
return $this->store->count($filters);
}
}
ai:stats command using count() is a cheap way to verify a deploy/indexing job actually wrote rows.EmbeddingStored):
event(new EmbeddingStored($record));
Symfony Abstraction:
AiStoreInterface for custom methods:
interface CustomStoreInterface extends AiStoreInterface {
public function getByMetadata(array $filters);
}
count() and return VectorDocumentInterface instances — a missed forward means silent BadMethodCallExceptions only in production paths.Testing:
$store = $this->createMock(AiStoreInterface::class);
$store->method('query')->willReturn([/* VectorDocumentInterface instances */]);
$store->method('count')->willReturn(42);
MariaDB Version:
Unknown column type 'VECTOR' → You’re on MariaDB <11.7.Distance. Pin serverVersion=11.8 in the DSN so the platform detection matches the server.pgvector).Distance Functions:
Unknown system variable 'distance_type' → HNSW index misconfiguration.COSINE or L2 (Euclidean) explicitly, and make sure it matches the distance option passed to MariaDbStore:
INDEX vec_idx USING HNSW(embedding) WITH (distance_type = 'COSINE')
Vector Dimensions:
Data too long for column 'embedding' → Mismatched dimensions.dimensions in config match the VECTOR(N) column.Hybrid Query Limits:
SQLSTATE[HY000]: General error: 1118 → Complex filters may fail.$store->query($vector, ['filter' => ['raw' => 'category = ? AND priority > ?', ['tech', 5]]]);
Laravel-Symfony Conflicts:
Class 'Symfony\Component\AI\Store\AiStoreInterface' not found.symfony/ai is installed and autoloaded. Pin ^0.15 across the whole family — mixing a 0.14 store with a 0.15 symfony/ai (or vice versa) is the most likely source of interface-mismatch fatals after this release.Upgrading 0.14 → 0.15:
distance in config matches the metric your existing vec_idx was built with; recreate the index if not.query() still returns VectorDocumentInterface instances and count() is unchanged. The “parse the query vector once per query” change is internal; only custom subclasses overriding vector placeholder/parsing logic need a look.^0.15 bump applied to symfony/ai, symfony/ai-maria-db-store, and any other AI sub-packages in one PR to avoid a split-brain install.[mysqld]
general_log = 1
general_log_file = /var/log/mysql/mariadb-query.log
DB::enableQueryLog() to capture raw SQL:
DB::enableQueryLog();
$store->query($vector);
dd(DB::getQueryLog());
count() returns 0 but query() returns hits, your filter array is being interpreted differently by each path — verify against the raw SQL from the query log.SHOW INDEX FROM ai_embeddings / SHOW CREATE TABLE to confirm the HNSW index exists and carries the metric you configured.Indexing Overhead:
-- Disable index temporarily for bulk inserts
ALTER TABLE ai_embeddings DROP INDEX vec_idx;
-- Insert data
ALTER TABLE ai_embeddings ADD INDEX vec_idx USING HNSW(embedding)
WITH (distance_type = 'COSINE');
Batch Size:
insertMany() batch size: 500–2000 records (benchmark with EXPLAIN).Distance Metrics:
count() Cost:
count() on InnoDB scans the index; filtered count() may scan the table. Use it for periodic stats, not for real-time UI badges on hot paths.Custom Distance Functions:
Override the store’s getDistanceSql() method (keep the default distance mapping intact so the index and the query agree):
class CustomMariaDbStore extends MariaDbStore {
protected function getDistanceSql(string $column, string $distance): string
{
return match ($distance) {
'custom' => "1 - (($column) * $this->getVectorPlaceholder())",
default => parent::getDistanceSql($column, $distance),
};
}
}
Custom Documents:
Implement VectorDocumentInterface for Eloquent-backed results:
use Symfony\Component\AI\Store\VectorDocumentInterface;
class EmbeddingDocument implements VectorDocumentInterface
{
public function __construct(
private string $id,
private string $content,
private array $metadata = [],
) {}
public function getId(): string { return $this->id; }
public function getContent(): string { return $this->content; }
public function getMetadata(): array { return $this->metadata; }
}
Metadata Serialization: Extend to handle custom metadata formats (e.g., arrays):
protected function serializeMetadata(array $metadata): string
{
return json_encode($metadata, JSON_THROW_ON_ERROR);
}
Async Operations: Use Laravel Queues for long-running queries:
class ProcessVectorQuery implements ShouldQueue {
public function handle(AiStoreInterface $store): void {
$results = $store->query($this->vector, ['limit' => $this->limit]);
// handle VectorDocumentInterface results
}
}
How can I help you explore Laravel packages today?