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

Laravel Typesense Laravel Package

devloopsnet/laravel-typesense

Laravel integration for Typesense, the open-source typo-tolerant search engine. Index Eloquent models, run typed queries, and manage collections from your app with an expressive API built for fast, relevant full-text search.

View on GitHub
Deep Wiki
Context7

Getting Started

1. Install the pieces. This is an engine for Laravel Scout, not a replacement for it, and it does not bundle a Typesense server.

composer require laravel/scout
composer require devloopsnet/laravel-typesense
php artisan vendor:publish --provider="Laravel\Scout\ScoutServiceProvider"

Then publish the engine's config. If you don't know the provider FQCN, let Artisan show you the list — the package ships its provider in composer.json → extra.laravel.providers:

php artisan vendor:publish --tag=typesense
# or, if no tag is defined:
php artisan vendor:publish
# ...and pick the Devloops/Typesense entry from the numbered list

2. Point Scout at the driver. In config/scout.php:

'driver' => env('SCOUT_DRIVER', 'typesense'),
'queue'  => env('SCOUT_QUEUE', false),

In .env:

SCOUT_DRIVER=typesense
TYPESENSE_API_KEY=xyz          # ADMIN key — server side only
TYPESENSE_HOST=localhost
TYPESENSE_PORT=8108
TYPESENSE_PROTOCOL=http

The published config/typesense.php holds api_key plus a hosts/nodes array (host, port, protocol) and a connection timeout. Confirm the exact key names against the file — this package predates some of the naming churn in the official client.

3. Make one model searchable. Typesense has a strict, declared schema (unlike Meilisearch), so you typically define both the document and the collection schema on the model:

use Laravel\Scout\Searchable;

class Post extends Model
{
    use Searchable;

    public function searchableAs(): string
    {
        return 'posts';
    }

    public function toSearchableArray(): array
    {
        return [
            'id'         => (string) $this->id,   // Typesense ids are strings
            'title'      => $this->title,
            'body'       => $this->body,
            'status'     => $this->status,        // must be facet:true to filter on it
            'views'      => (int) $this->views,
            'created_at' => $this->created_at->timestamp,
        ];
    }

    public function typesenseCollectionSchema(): array
    {
        return [
            'name'  => $this->searchableAs(),
            'fields' => [
                ['name' => 'title',      'type' => 'string'],
                ['name' => 'body',       'type' => 'string', 'optional' => true],
                ['name' => 'status',     'type' => 'string', 'facet' => true],
                ['name' => 'views',      'type' => 'int32'],
                ['name' => 'created_at', 'type' => 'int64'],
            ],
            'default_sorting_field' => 'created_at',
        ];
    }
}

4. First use case. Import, then search:

php artisan scout:import "App\Models\Post"
Post::search('eloquent')->get();          // collection of Post models
Post::search('eloquent')->paginate(15);   // paginator, hit count from Typesense

Where to look first when something misbehaves:

  • vendor/devloopsnet/laravel-scout-typesense-engine/src/TypesenseEngine.php — search(), update(), delete(), createIndex(), flush(). This is the ground truth for which Scout builder methods are actually translated.
  • config/typesense.php — connection shape and defaults.
  • vendor/laravel/scout/src/Builder.php — the fluent API surface you're allowed to call, whether or not this engine implements it.

Implementation Patterns

Schema-first, denormalised documents

Treat typesenseCollectionSchema() as a migration file. Every key returned from toSearchableArray() must exist in the schema (or be silently dropped / throw on import). Search documents are flat: resolve relations before indexing.

public function toSearchableArray(): array
{
    return [
        'id'            => (string) $this->id,
        'title'         => $this->title,
        'author_name'   => $this->author->name,        // denormalised
        'tag_names'     => $this->tags->pluck('name')->all(),  // string[]
        'comment_count' => $this->comments()->count(),
        'published_at'  => optional($this->published_at)->timestamp ?? 0,
    ];
}

Query building — the everyday surface

Post::search('laravel')
    ->where('status', 'published')
    ->where('views', '>=', 100)
    ->orderBy('created_at', 'desc')
    ->take(20)
    ->get();
// Facets / typo tolerance / field weighting go through raw options
Post::search($q)
    ->options([
        'query_by'           => 'title,body,author_name',
        'query_by_weights'   => '4,2,1',
        'num_typos'          => 1,
        'facet_by'           => 'status,tag_names',
        'max_facet_values'   => 20,
        'highlight_full_fields' => 'title,body',
    ])
    ->paginate(15);

Facet counts are attached to the raw response, so read them from the underlying result rather than from the paginator when you need them.

Geo search for "near me" features

Declare a geopoint field and use Scout's within():

['name' => 'location', 'type' => 'geopoint', 'optional' => true],
Store::search('coffee')->within('location', $lat, $lng, 5 /* km */)->get();

Keeping the index fresh

Situation Pattern
Normal CRUD Searchable trait; sync inline in dev, SCOUT_QUEUE=true in prod
Bulk imports / seeders Wrap in Model::withoutSyncingToSearch(fn () => ...) then scout:import
Seeder noise Model::disableSearchSyncing() in the seeder, re-enable after
Full rebuild scout:flush "App\Models\Post" → scout:import "App\Models\Post"
Zero-downtime rebuild Index into posts_v2 via a temporary searchableAs(), then re-point — Typesense supports collection aliases at the API level

Multi-model / cross-collection search

When you outgrow Model::search(), grab the underlying Typesense client and use the multi-search endpoint directly for a unified search page. Instantiate it from the container (or a facade, if the package registers one) rather than re-reading config:

$client = app(\Devloops\Typesense\TypesenseEngine::class)->getTypesenseClient();
// $client->multiSearch->perform([...])

Check the engine class for the actual accessor name before wiring this into a controller.

Testing

Don't hit a real Typesense node in unit tests:

// phpunit.xml
<env name="SCOUT_DRIVER" value="collection"/>

For integration tests that need real search behaviour, use an in-memory/docker Typesense and a dedicated test collection name, and flush between tests.


Gotchas and Tips

No dynamic schema — this is the #1 source of "my data isn't there." Add a field to toSearchableArray() without adding it to typesenseCollectionSchema() and it won't be indexed. Conversely, an unknown field on import can hard-fail the batch. Any schema change means: drop the collection, update the schema method, re-import. There is no scout:sync-index-settings equivalent for Typesense field types.

Filtering on a string field requires facet: true. ->where('status', 'published') against a string field declared without facet will not do what you expect. Numbers and booleans filter fine without it. This trips people up constantly when moving from TNTSearch/Algolia.

Sorting requires the field to be in the schema as a numeric type, and default_sorting_field must be int32/int64/float — never a string or a datetime string. Store timestamps as Unix ints.

IDs must be strings. Typesense stores id as a string. Cast in toSearchableArray() and make sure getScoutKey() returns something stringifiable, or you'll get mismatched documents and 404s on delete.

The engine silently ignores what it doesn't implement. Scout's Builder exposes whereIn, whereNotIn, withTrashed, onlyTrashed, within, options, raw — but each engine implements a subset. Before relying on one, grep TypesenseEngine::search() for it. Unsupported clauses typically vanish rather than throwing, giving you unfiltered results — which looks like a security bug, not a search bug.

Queued sync failures are invisible. With SCOUT_QUEUE=true, a schema mismatch fails inside a queued job. Monitor failed_jobs and keep a slow scout:import in your deploy checklist after any schema change.

Never expose the admin key. Use an admin key server-side for indexing and a separate search-only key for the frontend/InstantSearch integration. Also set config:clear after editing .env — Laravel caches config and the engine will happily connect with stale credentials.

searchableAs() renames orphan collections. Changing it doesn't move data; you get an empty new collection while the old one lingers and still consumes memory. Delete old collections via the Typesense API or dashboard.

Debugging checklist, in order:

  1. curl -H "X-TYPESENSE-API-KEY: $KEY" http://host:8108/collections — does the collection exist, and is the document count right?
  2. curl .../collections/posts/documents/1 — is the field actually present?
  3. curl .../collections/posts/search?q=...&query_by=title — does raw Typesense return hits?
  4. Only then suspect the engine. The failure is nearly always schema, not PHP.

Tune connection_timeout and index in batches for large imports; a full scout:import of 100k+ rows against a default timeout will stall. Raise the timeout, chunk the import by model scope, or import in parallel per shard.

Evaluate the alternative before committing. This package currently has no dependents and minimal signal behind it. The official typesense/laravel-scout-typesense-driver is actively maintained and tracks Scout releases faster. Pin this package to an exact version in composer.json, read TypesenseEngine.php once end-to-end so you know exactly what it does, and keep the migration path open by hiding engine-specific calls (raw options, geo, facades) behind a small repository/service class so a swap is a one-file change.

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.
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
robertmarney/lara-hierarchial-collections
simonschaufi/rule-doc-generator
emissaoja/emissaoja
blackbricksoftware/gravityforms-civicrm-form-processor
al-rahabi/fancy-crud
jinas/dhivatar-component
mltstephane/laravel-quality
bernskiold/laravel-talentlms
bernskioldmedia/laravel-form-architect