typesense/laravel-scout-typesense-driver
A Typesense driver for Laravel Scout that brings fast, typo-tolerant full-text search, faceting, filtering, geo search, and synonyms to Eloquent models. Swap Scout's default engine for Typesense with simple configuration and a native Laravel integration for scalable, relevant app search.
The driver plugs Typesense into Laravel Scout, so Scout is the mental model — this package only supplies the engine adapter plus a richer Searchable trait. Read the source of Typesense\LaravelTypesense\Typesense (the engine) and the README's schema section first; those two files explain 90% of runtime behavior.
Minimal path:
composer require typesense/laravel-scout-typesense-driverdocker run -p 8108:8108 -v/tmp/typesense-data:/data typesense/typesense:27.1 \
--data-dir /data --api-key=xyz --enable-cors
curl http://localhost:8108/health # {"ok":true}
.env and config/scout.php:
'driver' => env('SCOUT_DRIVER', 'typesense'),
'typesense' => [
'api_key' => env('TYPESENSE_API_KEY', 'xyz'),
'nodes' => [[
'host' => env('TYPESENSE_HOST', 'localhost'),
'port' => env('TYPESENSE_PORT', '8108'),
'protocol' => env('TYPESENSE_PROTOCOL', 'http'),
]],
'connection_timeout' => env('TYPESENSE_CONNECTION_TIMEOUT', 2),
],
use Laravel\Scout\Searchable;
class Post extends Model
{
use Searchable;
public function toSearchableArray(): array
{
return [
'id' => (string) $this->id,
'title' => $this->title,
'body' => $this->body,
'likes' => (int) $this->likes,
'published' => (bool) $this->published,
'created_at' => $this->created_at->timestamp,
];
}
}
php artisan scout:import "App\Models\Post"Post::search('laravel')->get();If step 6 returns nothing, check curl 'http://localhost:8108/collections' — an empty list almost always means the import failed silently on a schema/type mismatch.
Define the schema explicitly. Scout's derived schema is fine for demos, but the driver's real value is control over the Typesense collection:
public function getCollectionSchema(): array
{
return [
'name' => $this->searchableAs(), // defaults to table name
'fields' => [
['name' => 'title', 'type' => 'string'],
['name' => 'body', 'type' => 'string', 'index' => false],
['name' => 'likes', 'type' => 'int32'],
['name' => 'tags', 'type' => 'string[]', 'facet' => true],
['name' => 'location','type' => 'geopoint'],
['name' => 'created_at', 'type' => 'int64'],
],
'default_sorting_field' => 'likes',
];
}
(Older versions accepted a Typesense\Collection $collection argument — check the method signature on your installed version before overriding.)
Query options are the main levers. Everything Scout doesn't model natively is passed through ->options():
Post::search('laravel')
->options([
'query_by' => 'title,body',
'query_by_weights' => '4,1',
'filter_by' => 'published:=true && likes:>10',
'sort_by' => 'likes:desc',
'facet_by' => 'tags',
'num_typos' => 1,
'prefix' => true,
])
->paginate(15);
Geo search — declare a geopoint field, store [lat, lng], then filter:
->options(['filter_by' => 'location:(48.85, 2.34, 10 km)'])
Vector / semantic search — declare an float[] field (typically embedding) and pass a vector_query through ->options(). Keep embedding generation outside the driver; it only transports the vector.
Queued indexing. In production set SCOUT_QUEUE=true and give Scout its own queue connection so a slow Typesense node doesn't stall mail or webhooks. Use toSearchableArray() as the only place that shapes index data — never mutate models inside shouldBeSearchable().
Multi-tenancy by overriding searchableAs() per tenant ("posts_{$tenant->id}"), and drive index creation with php artisan scout:import in tenant provisioning jobs.
Raw admin operations go through the facade rather than Scout:
use Typesense\LaravelTypesense\Facades\Typesense;
Typesense::getCollections(); // list
Typesense::getCollection('posts')->delete();
Typesense::multiSearch(['searches' => [...]]); // multi-collection search
Synonyms, curations, and API-key management are not covered by the driver — manage those through the Typesense client or dashboard.
Testing. Set SCOUT_DRIVER=null (or collection) in phpunit.xml so tests never hit a live Typesense. If you need assertion-level coverage, spin up Typesense in CI via the Docker image and import fixtures in setUp.
toSearchableArray() must return int for int32, bool for bool, arrays for [] fields. A numeric string like "42" fails the import with an opaque 422. Cast explicitly.'' / 0 / [] in toSearchableArray().scout:import creates the collection if missing but won't alter an existing one. The reliable routine is php artisan scout:flush "App\Models\Post" then re-import — or use scout:sync-index-settings if your version supports it (settings only, not fields).default_sorting_field must be a non-optional numeric field and must also exist in every document, otherwise indexing fails.whereIn-style post-filtering in your own code will scramble it. If order matters, assert on $results->pluck('id') in tests.scout:import will happily exhaust memory on toSearchableArray() if you lazy-load relations. Eager-load them or use makeSearchableUsing() / searchable() in batches.__soft_deleted flag; when querying a soft-deletable model, add 'filter_by' => '__soft_deleted:=0' explicitly if your results look inflated.TYPESENSE_API_KEY should be the admin key for indexing. If you later switch the search path to a search-only key, indexing jobs must keep the admin key — split the config rather than swapping the env var globally.connection_timeout is low and the node is cold. Bump it to 5–10s in development, and always curl /health before debugging Laravel code.field and error keys that pinpoint the offending document. Typesense::getDebug() on the facade exposes the last request in the PHP client.docker-compose and CI, and re-read the README before upgrading either.searchableAs(), toSearchableArray(), getCollectionSchema(), shouldBeSearchable(), and makeSearchableUsing(). If you need new engine behavior, extend Typesense\LaravelTypesense\Typesense and rebind Scout's engine in a service provider rather than forking the package.How can I help you explore Laravel packages today?