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 Typesense Store Laravel Package

symfony/ai-typesense-store

Typesense vector store integration for Symfony AI Store, adding a fast, typo-tolerant search backend for storing and querying embeddings. Supports vector search via the Typesense API within your Symfony AI applications.

View on GitHub
Deep Wiki
Context7

Getting Started

This is a Symfony AI bridge, not a Laravel package — there is no service provider, no config/typesense.php, and no auto-discovery. You will wire it manually. Treat it as a library you compose, and budget the first hour for reading source rather than docs, because the README is a stub.

Prerequisites you need before touching the package

  1. A reachable Typesense node (v0.28+ for native float[] vector fields):
    docker run -d -p 8108:8108 -v /tmp/typesense-data:/data \
      typesense/typesense:29.0 --data-dir /data --api-key=xyz --enable-cors
    
  2. symfony/ai-store installed (this bridge implements its StoreInterface).
  3. An embedding provider from the Symfony AI platform packages (OpenAI, Ollama, etc.) — the store does not embed anything for you. You pass vectors in.
composer require symfony/ai-typesense-store symfony/ai-store symfony/http-client

Where to look first, in this order

  • src/ in the repo — specifically the concrete TypesenseStore constructor signature and any StoreFactory class. v0.11 added StoreFactory and "streamlined base URL handling across bridges", so expect a DSN-style config (typesense://host:8108/collection) plus an injected HttpClientInterface, rather than the typesense/typesense-php SDK. Verify this before writing config — the PM pitch doc's new TypesenseStore(new TypesenseClient(...), 'products') snippet is illustrative, not authoritative.
  • StoreInterface in symfony/ai-store — that is your real contract: add(), find(), remove(), clear() (v0.11), count() (v0.14).
  • VectorDocumentInterface (v0.14) — read this second; it is the seam you will build your domain models on.
  • The Symfony AI demo app (referenced in the v0.14 changelog) — the only working end-to-end example that exists today.

First use case: index five documents, query one

Create the collection yourself, out of band, with the vector field declared explicitly. Dimension must match your embedding model exactly:

curl -X POST http://localhost:8108/collections -H "X-TYPESENSE-API-KEY: xyz" -H "Content-Type: application/json" -d '{
  "name": "products",
  "fields": [
    {"name": "title", "type": "string"},
    {"name": "category", "type": "string", "facet": true},
    {"name": "price", "type": "float", "facet": true},
    {"name": "embedding", "type": "float[]", "num_dim": 1536, "vec_dist": "cosine"}
  ]
}'

Then bind the store as a singleton in a service provider so you get one client per process:

// app/Providers/AiServiceProvider.php
$this->app->singleton(StoreInterface::class, fn () => (new TypesenseStoreFactory(
    $this->app->make(HttpClientInterface::class),
))->create('typesense://' . config('ai.typesense.host') . ':8108/products?api_key=' . config('ai.typesense.key')));

Confirm the wiring with a single insert and a single nearest-neighbour query before you build anything else — if dimension or schema is wrong, this is where it fails loudly, not three layers up in a chat controller.

Implementation Patterns

The ingestion pipeline is a three-stage job, not a store call. Chunk → embed (batch) → add(). The store is only stage three. Put stages one and two in a queued job so a re-index of 40k documents doesn't run in a request:

class IndexProductChunk implements ShouldQueue
{
    public function handle(StoreInterface $store, EmbedderInterface $embedder): void
    {
        $vector = $embedder->embed($this->chunk->text);          // stage 2
        $store->add(new ProductDocument(                        // stage 3
            id: $this->chunk->id,                               // stable, deterministic id
            vector: $vector,
            metadata: ['category' => $this->chunk->category, 'price' => $this->chunk->price],
        ));
    }
}

Use deterministic IDs ("product-{$id}-chunk-{$n}"). Re-running ingestion then becomes idempotent instead of duplicating the corpus — this matters enormously when you change chunking strategy and re-index.

Model your records as VectorDocumentInterface implementations, not arrays. This is the single highest-value habit with v0.14. A readonly DTO gives you static analysis, IDE completion, and — critically — a seam for swapping to pgvector later without touching controllers:

final readonly class ProductDocument implements VectorDocumentInterface
{
    public function __construct(
        public string $id,
        public array $vector,
        public array $metadata,
    ) {}
}

Always embed the query at query time with the same model used at index time. The most common production bug in this class of package is a silent model swap on one side only. Enforce it by keeping model name in config adjacent to the collection name, and stamping the model name into collection metadata so a mismatch is detectable.

Filters are your precision lever; vectors are your recall lever. v0.4 added a query abstraction with filter support. Declare filterable fields in the Typesense schema (facet: true for faceting, correct types for range filters) and compose semantic similarity with hard constraints:

$results = $store->find(
    $query->setVector($embedder->embed('noise cancelling headphones'))
          ->setLimit(10)
          ->setFilter('category:=electronics && price:<500')
);

For RAG, take limit 5–20, feed the metadata text back as context, and keep the store behind your own Retriever interface so your prompt-building code never imports a Typesense type.

Multi-tenancy: choose collection-per-tenant or filter-field-per-tenant, deliberately. Collection-per-tenant gives hard isolation and per-tenant count() for quota enforcement, but multiplies schema management and Typesense collection overhead. A tenant_id filter field is cheaper and works fine up to large tenant counts — but then every single query must carry the filter, so wrap the store in a repository that injects it and never expose the raw store to feature code.

count() and clear() are operational primitives, not request-path ones. Use count() for usage dashboards, "N results available" UI, and per-plan quota checks — but cache it (60s is plenty) or push it to a scheduled command that writes to a metrics table. Use clear() for full re-index jobs (php artisan ai:reindex) where you drop and rebuild a collection. v0.14's count is a corpus-size primitive, not an aggregation engine: it will not give you per-category facets or price histograms. For those, call Typesense's facet_counts via the raw HTTP endpoint.

Testing: two layers. A fake StoreInterface implementation (in-memory array) for feature tests — cheap, no Docker. A separate integration test group that boots a Typesense container and runs the real store, tagged and excluded from the default suite. Anything touching filters or vector ordering must be in the integration group; in-memory fakes lie about Typesense's matching semantics.

Gotchas and Tips

Symfony package in a Laravel app — check the constraint graph first. composer require and then read the resolution. The Symfony AI packages carry symfony/* requirements that can collide with the versions Laravel has pinned. If it resolves, you're fine; if it fights, use -W and inspect what moves. Also budget for the fact that you're depending on a framework-coupled library from a non-framework app: no DI autoconfiguration, no config validation, no artisan generators.

Version 0.x with a thin README means the changelog is your documentation. Pin ^0.14 and read every release note before upgrading. The changelog already shows behaviour-bearing changes shipped in patch-sized increments: v0.10 hardened remove() against query injection, v0.11 added clear() and reworked base URL handling, v0.14 introduced VectorDocumentInterface. Assume breaks between minor versions; never use * or dev-main in production.

Pre-0.10 has a known filter-injection fix in remove(). If you inherit a lockfile on an older version, upgrade. More generally: never interpolate user input into a Typesense filter_by string. Whitelist field names against an allowlist and cast/escape values. Filters are a query language, and you are assembling it by hand.

Schema creation is likely your job, not the bridge's. Verify whether the store auto-creates a collection; assume it does not and create it explicitly in a migration-like artisan command. Two consequences: (a) a typo in num_dim fails at first insert, not at boot, so validate at startup with a health check; (b) Typesense rejects documents with fields absent from the schema, so any new metadata key you start emitting requires a schema change (schema updates for adding fields are supported; changing num_dim is not — that requires a new collection and full re-index).

Changing embedding model = new collection + full re-index. There is no in-place migration path. Design for it now: version your collection names (products_v2) and keep the previous collection live behind a config flag so you can A/B or roll back.

count() right after bulk ingest may lag. Typesense is near-real-time, not transactional. In tests that assert on count() after add(), either poll with a short backoff or assert on find() of the specific document by ID. Do not sleep-loop in production code.

Debug by dropping a level, not by adding logging. When a query returns nothing, curl the Typesense endpoint directly (POST /collections/products/documents/search) with the same body. If curl works and the store doesn't, the bug is in filter serialization or the vector payload shape. To see what the bridge actually sends, decorate the injected HttpClientInterface with a logging wrapper — that's the cleanest extension point this package gives you, and it also lets you add timeouts and retry policy centrally.

Extension points worth knowing, in order of usefulness: (1) decorate StoreInterface for caching, logging, and tenant enforcement — zero coupling to Typesense; (2) swap HttpClientInterface for retries/timeouts; (3) custom VectorDocumentInterface classes for typed records; (4) StoreFactory DSN for environment-specific config. Avoid forking the bridge; the API surface is small enough that decoration covers almost everything.

Config caching trap. If you read env() inside the singleton closure, php artisan config:cache will bake in stale values. Read through config() only, and make sure your Typesense key lives in config/ai.php (populated from .env) rather than being read directly from the environment at container-resolution time.

Scope check before you commit. Typesense's sweet spot is sub-100ms vector search over millions of records. If your corpus is under ~10k documents, a pgvector column and an order-by-distance query will be simpler, transactional, and one less service to operate. Adopt this bridge when you specifically want Typesense's typo tolerance, facet counts, and hybrid keyword+vector behaviour alongside the vectors — or when you already run Typesense for keyword search and want one fewer datastore.

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.
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
noith/settings
kda/laravel-package