Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Ai Cache Store Laravel Package

symfony/ai-cache-store

Symfony AI Cache Store integrates Symfony Cache with Symfony AI Store, letting you use a cache backend as a vector store for AI applications. It provides a lightweight bridge for storing and retrieving embeddings through familiar cache infrastructure.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and orient yourself in vendor/ first. The README is a stub; it links to the monorepo (symfony/ai) and nothing else. The real documentation lives in the symfony/ai-store component source, so plan to read code, not docs.

composer require symfony/ai-cache-store:0.14.*
ls vendor/symfony/ai-cache-store/src          # find the actual FQCN
ls vendor/symfony/ai-store/src                 # StoreInterface, VectorDocumentInterface, Document, Query, StoreFactory

Confirm four things before writing a line of application code:

  1. The concrete class name (most likely Symfony\AI\Store\Bridge\Cache\CacheStore, but verify — the README never states it).
  2. Which cache contract the constructor accepts (PSR-6 CacheItemPoolInterface vs PSR-16 CacheInterface). This dictates your Laravel wiring more than anything else.
  3. The query() signature — it changed in v0.4.0 ("query abstraction with filter support"), so the call shape depends on the version you pinned.
  4. What VectorDocumentInterface (new in v0.14.0) actually declares. It is the contract your write path should type-hint against — Document is now just the shipped implementation. Read the interface before you construct documents by hand; the accessor names for id, vector, and metadata are what your indexing code will touch everywhere.

Minimal wiring in AppServiceProvider. Laravel's Illuminate\Contracts\Cache\Repository is not PSR-6/PSR-16, so don't try to hand Cache::store() straight to the bridge. Use a Symfony cache adapter pointed at the same backend, in its own pool:

use Symfony\AI\Store\Bridge\Cache\CacheStore;
use Symfony\AI\Store\StoreInterface;
use Symfony\Component\Cache\Adapter\RedisAdapter;

public function register(): void
{
    $this->app->singleton(StoreInterface::class, function () {
        $redis = RedisAdapter::createConnection(config('database.redis.default'));
        $pool  = new RedisAdapter($redis, 'ai_vectors', (int) config('ai.store.ttl', 86400));

        return new CacheStore($pool);
    });
}

Bind against StoreInterface, never the concrete class — that's the whole migration story if you outgrow this package (see Gotchas). Type-hint VectorDocumentInterface for anything you accept as input to the indexer, so a future custom document class doesn't ripple through your app.

First use case: an internal-docs semantic search over a few hundred chunks. It exercises the whole pipeline (chunk → vectorize → add() → query() with a filter) in an afternoon, and it's the workload this bridge is actually good at. A single-document demo hides the two things that will bite you later: index size and eviction. Seed 500–1000 docs on day one so query latency is honest — and confirm the seed with $store->count() (new in v0.14.0) rather than by trusting the loop ran.

foreach ($chunks as $chunk) {
    $store->add(new Document(
        id: $chunk->id,
        text: $chunk->body,
        vector: $vectorizer->vectorize($chunk->body),
        metadata: ['section' => $chunk->section, 'tenant' => $chunk->tenant_id],
    ));
}

$hits = $store->query(new Query(
    vector: $vectorizer->vectorize($question),
    filter: ['tenant' => tenant()->id],
));

logger()->info('vector index', ['count' => $store->count()]);

Implementation Patterns

Keep the store behind a domain service, not a facade. A VectorStore facade looks tidy in a blog post and is painful in tests. Inject StoreInterface into one small service (DocumentIndexer, SemanticSearch) and let everything else depend on that service. Swapping to pgvector or Qdrant later then means editing one binding.

Reindexing is a queued job, not a request. Embedding calls are slow and rate-limited; a 5k-document reindex inside a controller will time out. Pattern that works:

final class IndexDocumentJob implements ShouldQueue
{
    public function handle(DocumentIndexer $indexer): void
    {
        $this->document->chunks()->each(
            fn ($chunk) => $indexer->upsert($chunk)
        );
    }
}

Because IDs are stable, add() doubles as an upsert for changed documents. For deletions, v0.4.0 added remove() — call it from an observer on your model's deleted event, or you accumulate orphaned vectors that pollute every query.

Use count() as your index health signal. v0.14.0 gives stores count capabilities. Wire it into three places, not into request paths:

  • A php artisan vectors:status command that prints configured store, expected document count, and actual count.
  • Progress reporting inside the reindex job ($store->count() every N chunks) so a stalled job is obvious.
  • A post-deploy smoke check: if count drops to zero after a deploy, you have an eviction or prefix problem, and you want to know in the deploy log rather than from a user.

Before you lean on it, check whether count() is cheap or scans the pool — the answer depends on what CacheStore can get out of the cache adapter.

Build custom documents against VectorDocumentInterface, not Document. The v0.14.0 interface means you can hydrate a document straight from an Eloquent model or a DTO without a mapping layer. Worth doing when you already carry metadata as typed properties. Don't do it for one class in one place — Document is fine there.

Separate the two "AI caches" in your head. This package is a vector store, not an LLM response cache. Day-to-day you'll want both:

  • Vector store (this package): embeddings for retrieval, queried by similarity.
  • LLM response cache (Cache::remember on a hash of the prompt + model): pure cost saving, zero vectors involved.

Conflating them produces a store full of full-text completions and no useful index.

Use a dedicated cache store. In config/cache.php add:

'vectors' => ['driver' => 'redis', 'connection' => 'vectors', ...],

with its own Redis database number or key prefix. If your vectors live in the same Redis DB as sessions and rate limiters, php artisan cache:clear wipes your index, and maxmemory-policy eviction silently deletes embeddings under load. Both are "worked yesterday, empty today" bugs.

Filter, then rank, and don't trust a threshold across stores. The filter support added in v0.4.0 is metadata-based; treat it as a scoping mechanism (tenant, locale, section), not as a query planner. Score thresholds you tuned against cosine similarity here will not transfer to a store using inner product — keep thresholds in config, not code.

Test against ArrayAdapter. A Symfony ArrayAdapter-backed CacheStore plus a deterministic fake vectorizer (hash text → fixed-length float array) gives you fast, hermetic tests of your indexing and filtering logic without touching Redis or an embedding API. Assert on count() before and after remove() — that's the cheapest regression test for orphaned-vector bugs.

Gotchas and Tips

  • It is a cache, not a database. Every vector is a serialized cache value. There's no ANN index, no transactions, and no durability guarantee. Query cost is linear in your document count — brute-force similarity over ~10k 1536-dim vectors is already seconds, not milliseconds. Budget for <100k vectors, ideally much less.
  • Payload size is the silent killer. 1536 floats serialize to ~15–30 KB per document depending on encoding. 50k documents = ~1–1.5 GB of Redis. Check redis-cli info memory after seeding, and consider smaller embedding models (384–768 dims) before considering a new backend.
  • php artisan optimize:clear / cache:clear / queue:restart are the three commands most likely to nuke your index. Dedicated DB + a reindex console command (php artisan vectors:reindex) is the mitigation you'll actually use.
  • Never use apcu or array in production. APCu is per-FPM-worker: writes land in one worker's memory and vanish for the next request. file works but serializes on lock contention and gets slow fast. Redis (with appendonly yes) is the only backend here worth calling "production" at small scale.
  • Memcached key limits. If your document IDs are UUIDs or paths, hash them (hash('xxh128', $id)) before they become cache keys — the 250-byte key cap and no-scan behaviour will otherwise surprise you at the worst moment.
  • Version pinning is non-optional. This is 0.x with 0 dependents, and the releases are frequent — the jump from the 0.8 line to 0.14 happened fast. v0.4.0 changed the query interface, v0.7.0 introduced StoreFactory, v0.14.0 introduced VectorDocumentInterface and count(). Pin exact minor versions (composer require symfony/ai-cache-store:0.14.*) and read the changelog before every composer update — "minor" releases here still ship interface-adjacent refactors upstream in symfony/ai.
  • Debugging: check count, then keys, then the API. $store->count() answers "is anything in there?" in one line. If it's non-zero but queries return nothing, redis-cli --scan --pattern 'ai_vectors*' | head tells you whether the keys are where you think they are. If both look right, the failure is almost always (a) a vectorizer mismatch or (b) a filter that excludes everything — check them in that order. Add a Log::debug('store.add', ['id' => $doc->getId(), 'dims' => count($doc->getVector())]) around writes for the first week; use the VectorDocumentInterface accessors so the log survives a document class change.
  • Vectorizer mismatches fail quietly in results, loudly in errors. Query vectors must come from the same model and dimension as the indexed ones. Store the model name in metadata so a mixed index is detectable: ->filter(fn ($d) => $d->metadata['model'] === 'text-embedding-3-small').
  • count() is not free by default. On a cache-backed store it may be a key scan rather than an O(1) read. Fine for a console command or a deploy check; not fine in a hot request path or inside a loop over per-tenant queries. Cache the value in your own observability layer if you need it on a dashboard.
  • New interface, old habits. VectorDocumentInterface is additive — existing Document code keeps working. Don't rewrite your indexing layer just to type-hint the interface; do it incrementally, and only where you actually want a second implementation (Eloquent hydration, DTOs, fixtures). If you do implement it, match the accessor names exactly — a typo shows up as a runtime Error, not a static-analysis failure.
  • Extension points worth knowing. Decorate StoreInterface for logging/metrics (query latency, hit counts, count() delta) without touching the bridge. Implement StoreInterface yourself if you need a feature the bridge lacks. And StoreFactory (v0.7.0) is the Symfony-idiomatic way to select a store from config — usable in Laravel but not autowired, so instantiate it manually if you want env-driven backend switching.
  • Write the exit before you need it. Because everything depends on StoreInterface (and your documents on VectorDocumentInterface), migration to pgvector/Qdrant is a binding swap plus a reindex. Keep the reindex command and the vectors:status count check working from day one; those two commands are your migration plan.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
skyraptor/chatter
edlugz/sasapay
abbasudo/artisan-route
mirvan/ippanel
ignibyte/aic-framework
glavweb/data-schema-bundle
glavweb/datagrid-bundle
tacticmedia/scim-server
uni-method/frame
neika-ai/sdk
adachsoft/php-code-style
pharaonic/laravel-notification-dependencies
beautinow/translug
dev-brainiac/bot-cloaked
octoberrain/html
octoberrain/extension
octoberrain/exception
octoberrain/contracts
adachsoft/api-version-bundle
cgv/localized-resource