symfony/ai-vektor-store
Vektor vector store integration for Symfony AI Store, connecting your Symfony AI applications to the Vektor vector database for storage and retrieval.
Install Dependencies:
composer require symfony/ai-vektor-store
Ensure your Laravel app uses PHP 8.2+ and has either Redis or PostgreSQL with pgvector installed.
Configure Vektor:
Add to config/services.php:
'vektor' => [
'dsn' => env('VEKTOR_DSN', 'redis://127.0.0.1:6379'),
'collection' => 'default',
],
Register the Store (unchanged interface, updated bridge class):
use Symfony\AI\Store\StoreInterface;
use Symfony\AI\Store\Bridge\Vektor\Store;
class VektorServiceProvider extends ServiceProvider {
public function register() {
$this->app->singleton(StoreInterface::class, function ($app) {
return new Store(
$app['config']['services.vektor.dsn'],
$app['config']['services.vektor.collection']
);
});
}
}
First Use Case — index and query, with the new count() guard (v0.14):
use Symfony\AI\Store\StoreInterface;
use Symfony\AI\Store\Document\VectorDocument;
use Symfony\AI\Platform\Vector\Vector;
class SearchController extends Controller {
public function __construct(private StoreInterface $store) {}
public function search(string $query) {
// New in v0.14: cheap sanity check before hitting the vector index
if ($this->store->count() === 0) {
return response()->json(['error' => 'No documents indexed'], 404);
}
$vector = app(VectorizerInterface::class)->vectorize($query);
$results = iterator_to_array(
$this->store->query(new Vector($vector), ['limit' => 3])
);
return response()->json($results);
}
}
StoreInterface methods (add, remove, query, and now count).Symfony\AI\Store\Document\VectorDocumentInterface — new in v0.14; the type you should code against for anything that flows through add().VectorDocumentInterface)VectorDocument instances and pass them to add(); use a deterministic ID for idempotency at the application layer.
use Symfony\AI\Store\Document\VectorDocument;
use Symfony\AI\Store\Document\Metadata;
use Symfony\AI\Platform\Vector\Vector;
$doc = new VectorDocument(
id: 'doc_'.$model->id,
vector: new Vector($embedding),
metadata: new Metadata($model->toArray()),
);
$store->add($doc);
// app/Observers/DocumentObserver.php
class DocumentObserver {
public function saved(Document $document) {
app(StoreInterface::class)->add(new VectorDocument(
id: 'doc_'.$document->id,
vector: new Vector($this->embed($document->content)),
metadata: new Metadata($document->toArray()),
));
}
public function deleted(Document $document) {
app(StoreInterface::class)->remove('doc_'.$document->id);
}
}
count())count() for pagination, dashboards, or as an index-readiness gate before running retrieval.
$total = $store->count();
$stale = $store->count(['filter' => ['indexed_at' => ['$lt' => now()->subDay()->toIso8601String()]]]);
Route::get('/health/vektor', fn () => response()->json([
'documents' => app(StoreInterface::class)->count(),
]));
Engine to delegate vector queries. Since v0.14, query() returns VectorDocumentInterface instances; map them to your Eloquent models via id.
// app/Scout/Engines/VektorEngine.php
class VektorEngine extends Engine {
public function search($query) {
$vector = app(VectorizerInterface::class)->vectorize($query);
$results = app(StoreInterface::class)->query(new Vector($vector));
return $this->mapResults(iterator_to_array($results));
}
}
add() calls because Vektor backends (Redis/PG) reward batched writes.
class EmbedDocuments implements ShouldQueue {
public function handle(StoreInterface $store) {
Document::query()->chunkById(200, function ($chunk) use ($store) {
$docs = $chunk->map(fn ($doc) => new VectorDocument(
id: 'doc_'.$doc->id,
vector: new Vector($this->embed($doc->content)),
metadata: new Metadata($doc->toArray()),
))->all();
$store->add(...$docs);
});
}
}
count() to short-circuit when the collection is empty.
$vector = app(VectorizerInterface::class)->vectorize($query);
$context = collect(iterator_to_array($this->store->query(new Vector($vector), ['limit' => 2])))
->map(fn (VectorDocumentInterface $r) => $r->getMetadata()->get('content'))
->filter()
->implode("\n");
$prompt = "Context: {$context}\nQuestion: {$query}";
$response = $llm->complete($prompt);
VEKTOR_DSN=postgres://user:pass@localhost/vektor_db
query()/count() options.count() on the health route too — a Redis hiccup will 500 your /health endpoint otherwise.VectorDocumentInterface is not a class you instantiate:
VectorDocument remains the value-object you build.VectorDocumentInterface in method signatures (so future releases can swap implementations), but construct new VectorDocument(...) when writing.count() is not free on Redis:
count() as a full scan; calling it inside a loop or per-request can be surprisingly expensive.Cache::remember('vektor.count', 60, fn () => $store->count()); — and avoid it in hot paths.upsert no longer exists as a store method:
$store->upsert(...). The stable API is add() + deterministic IDs (or remove() then add() for a true replace).add(); check the bridge README for backend-specific merge semantics.Vector Dimension Mismatch:
text-embedding-3-small vs. all-MiniLM-L6-v2) have incompatible dimensions.Laravel Cache vs. Vektor on the same Redis instance:
VEKTOR_DSN=redis://127.0.0.1:6379/1
PostgreSQL pgvector Setup:
CREATE EXTENSION IF NOT EXISTS vector;
-- match your model's dimension exactly
CREATE TABLE vectors (id TEXT PRIMARY KEY, embedding vector(1536));
php artisan tinker
>>> app(Symfony\AI\Store\StoreInterface::class)->count()
If this returns 0 after a seeded run, the issue is almost always a write path (missing add() or a failing observer), not the query.'vektor' => [
'dsn' => env('VEKTOR_DSN'),
'debug' => env('APP_DEBUG', false),
],
Debugbar::addCollector(new class implements DataCollectorInterface {
public function collect() {
return ['documents' => app(StoreInterface::class)->count()];
}
});
Custom VectorDocumentInterface implementation:
Since v0.14 formally introduces the interface, you can implement domain-specific document types (e.g. one that derives metadata from an Eloquent model lazily) and pass them to add() if the bridge accepts the interface rather than the concrete class.
class ModelLinkedDocument implements VectorDocumentInterface {
public function __construct(private Model $model, private array $vector) {}
public function getId(): string { return 'doc_'.$this->model->getKey(); }
public function getVector(): array { return $this->vector; }
public function getMetadata(): array { return $this->model->toArray(); }
}
Laravel Facade for cleaner call sites:
// app/Facades/Vektor.php
use Illuminate\Support\Facades\Facade;
class Vektor extends Facade {
protected static function getFacadeAccessor() {
return \Symfony\AI\Store\StoreInterface::class;
}
}
// Usage: Vektor::count();
Decorator around the store to add caching for count() and instrumentation:
class CachedStore implements StoreInterface {
public function __construct(private StoreInterface $inner) {}
public function add(VectorDocument ...$documents): void { $this->inner->add(...$documents); Cache::forget('vektor.count'); }
public function count(array $options = []): int { return Cache::remember('vektor.count', 60, fn () => $this->inner->count($options)); }
// delegate query()/remove()…
}
How can I help you explore Laravel packages today?