inspyrenees/typesense-bundle
Inspyrenees Typesense Bundle integrates the Typesense search engine into Symfony applications, providing configuration, indexing, and query services for fast, typo-tolerant search in PHP projects.
The package is distributed via Composer and wires the official typesense/typesense-php client into a framework container. Note that it is packaged as a Symfony bundle; in a Laravel app you either rely on a thin service-provider shim or bind the client yourself (see below).
composer require inspyrenees/typesense-bundle
Register the bundle — auto-registered under Symfony Flex; in Laravel you'll need to add the service provider / register the bundle in your AppServiceProvider or a dedicated wrapper provider.
Publish / create the config file (config/packages/typesense.yaml, or a config/typesense.php if you're shimming it) and set the three values you always need:
typesense:
api_key: '%env(TYPESENSE_API_KEY)%'
nodes:
- host: '%env(TYPESENSE_HOST)%' # localhost in dev
port: '%env(int:TYPESENSE_PORT)%' # 8108 default
protocol: '%env(TYPESENSE_PROTOCOL)%' # http in dev, https in prod
TYPESENSE_API_KEY=xyz
TYPESENSE_HOST=localhost
TYPESENSE_PORT=8108
TYPESENSE_PROTOCOL=http
Where to look first:
typesense:* (ping/health, collection create/delete, import, flush). Run the ping/health command before anything else; 90% of "it doesn't work" is a container not running or an API key mismatch.typesense.client or similar). That's the object you'll inject everywhere.First use case: take one read-mostly entity (products, articles, listings), define a collection for it, import it, and build a single search endpoint. Do not start by wiring autosync on write — get a read path working end-to-end with a manual import first.
On 0.11.0: the release notes were published as a bare compare link (0.10.1...0.11.0) with no changelog body. Read the diff on GitHub before bumping. A 0.x minor bump is a semver-breaking candidate, so pin 0.10.1 in composer.json until you've scanned for renamed config keys or changed command signatures.
Collection-as-config, schema as data. The dominant pattern is declaring each collection's schema (fields, types, facet flags, sort flags, default sorting field) in configuration, then running the create command. Treat that schema as a first-class artifact — commit it, review changes to it, and keep it next to your migrations even though it isn't one.
Import via command for the first load. Bulk import (JSONL or the client's import endpoint) is dramatically faster than per-document inserts and is the right tool for backfills. Use action=upsert so re-runs are idempotent and you can re-import safely after a schema change.
Sync on write, but decouple it. For day-to-day use, wire model observers / Doctrine event subscribers to push upserts and deletes. Two rules that save pain:
POST.afterCommit, not inside the transaction — otherwise you index rows that get rolled back.Search endpoint shape. You'll almost always expose one thin controller that translates request params into a multi_search / search call:
$results = $client->collections['products']->documents->search([
'q' => $request->string('q', '*'),
'query_by' => 'name,description,brand',
'filter_by' => $this->buildFilters($request), // "category:=shoes && price:<100"
'sort_by' => $request->input('sort', '_text_match:desc'),
'facet_by' => 'brand,category,size',
'per_page' => 24,
'page' => $request->integer('page', 1),
]);
Scoped API keys for the browser. Don't ship the admin key to the frontend. Generate a search-only key server-side, with an embedded filter_by that hard-limits the tenant/visibility scope, and cache it. This is the single highest-leverage integration point — it prevents the classic "user filters by tenant_id they don't own" bug.
Keep a search index alongside the DB, not instead of it. Pattern that holds up: hydrating the actual Eloquent models from the returned document ids, then overlaying the Typesense highlight/snippet data. Typesense returns IDs and highlights; your app owns the truth.
Testing. Bind a fake client or point the test suite at a disposable Typesense container. Assert on the query you built (params passed to the client) rather than on real search results — that keeps the suite fast and deterministic, with a small number of integration tests against a real node.
delete → create → import as one command pays for itself immediately.query_by is mandatory and must reference fields that exist and are indexed as strings. A schema mismatch here surfaces as a confusing 400, not a helpful validation error.page), while most PHP paginators are page-based too but per_page caps out — check the node's limit/page ceiling if you're building infinite scroll. Deep pagination is expensive; prefer offset-free cursor-ish patterns or narrow filters.sort_by only works on fields flagged sort: true in the schema, and sorting on a string field requires sort: true and the field to not be an array. Forgetting the flag silently falls back to relevance.filter_by syntax is strict: field:=value for exact, field:>10 for numeric, &&/|| for boolean logic, backticks around values containing spaces or commas. A single stray space turns a filter into a no-op or a 400. Wrap it in a tiny builder class and unit-test it.connection_timeout_seconds and node retry counts in config; the defaults are tuned for interactive search, not backfills.Typesense\Client in a singleton binding and skip the bundle's commands entirely. Budget an afternoon for the shim; don't fight the bundle.nodes entry means every request is a single point of failure. List the full cluster and let the client round-robin; verify behaviour by stopping one node.curl against the node directly (/health, /collections, /collections/x/documents/search?q=...) to separate "Typesense is wrong" from "our config/query builder is wrong." Then dump the exact array your code hands to the client — most bugs are there."inspyrenees/typesense-bundle": "0.10.1" is a safer default than a caret constraint. Move deliberately, after reading the diff.How can I help you explore Laravel packages today?