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

Typesense Bundle Laravel Package

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.

View on GitHub
Deep Wiki
Context7

Getting Started

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:

  1. The bundle's console commands — usually 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.
  2. The DI service id for the client (typesense.client or similar). That's the object you'll inject everywhere.
  3. The config tree, because the schema/collection definitions usually live there.

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.

Implementation Patterns

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:

  • Queue the indexing job. Never let a Typesense timeout fail a user's POST.
  • Dispatch on 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.

Gotchas and Tips

  • There are no migrations. Changing a field's type or adding a facet flag generally requires deleting and recreating the collection, then re-importing. Plan for a reindex window; scripting 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.
  • Pagination is 1-based (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.
  • Timing after writes. Documents are indexed asynchronously inside Typesense itself; a search issued microseconds after an upsert may miss the document. In tests (and in "save then redirect to search" flows) either add a short poll-until-visible helper or accept eventual consistency.
  • Connection timeouts are silent killers in batch jobs. Set explicit connection_timeout_seconds and node retry counts in config; the defaults are tuned for interactive search, not backfills.
  • Laravel + Symfony bundle friction. The bundle expects Symfony's container, config tree, and command naming. In Laravel you'll typically instantiate Typesense\Client in a singleton binding and skip the bundle's commands entirely. Budget an afternoon for the shim; don't fight the bundle.
  • Node config: one node in dev, real failover in prod. A single 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.
  • Debugging: the fastest loop is 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.
  • Extension points: the client service itself (decorate it for logging/metrics), the schema/collection config (custom collection classes), and the event subscribers doing the sync. Wrapping the client in a decorator that logs slow queries and 4xx bodies is the highest-value 30 lines you'll write against this package.
  • Pin the version. With uninformative 0.11.0 release notes, "inspyrenees/typesense-bundle": "0.10.1" is a safer default than a caret constraint. Move deliberately, after reading the diff.
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.
novasa/laravel-language-center
opencage/geocode
adolfoholzer/parameters
livenetworks/ln-starter
shitoudev/permissionmanager
johannesclimacus/laravel-tui
hocvt/seotools
vitalybaev/sypexgeo
amphibee/themosis-ignition
gito-me/fast-excel
frankyso/laravel-ipaymu
geolize-studio/laravel-chile
boomerangui/chat
jinas/dhivatar
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
cgv/localized-resource