siberfx/typesense-scout
A Typesense driver for Laravel Scout that adds fast, typo-tolerant full-text search to your Eloquent models. Index, filter, sort, and query records through Typesense using the familiar Scout APIs and simple model configuration.
Verify compatibility before installing. The package targets Laravel 12/13 on PHP 8.4/8.5. Pin it explicitly and check the README/service provider namespace, since adoption is very low:
composer require siberfx/typesense-scout
Run Typesense locally. The fastest path is Docker:
docker run -p 8108:8108 -v/tmp/typesense-data:/data typesense/typesense:latest \
--data-dir /data --api-key=xyz --enable-cors
Publish the package config and set environment variables.
php artisan vendor:publish
Look for the typesense-scout config tag/provider in the publish list, then set:
SCOUT_DRIVER=typesense
TYPESENSE_API_KEY=xyz
TYPESENSE_HOST=127.0.0.1
TYPESENSE_PORT=8108
TYPESENSE_PROTOCOL=http
Add Searchable to your first model. Keep toSearchableArray() explicit; do not rely on toArray() if you have relations or computed attributes.
use Laravel\Scout\Searchable;
class Article extends Model
{
use Searchable;
public function toSearchableArray(): array
{
return [
'id' => (string) $this->id,
'title' => $this->title,
'body' => $this->body,
'status' => $this->status,
'created_at' => $this->created_at->timestamp,
];
}
}
Create the collection and import existing rows.
php artisan scout:import "App\Models\Article"
First use case: a search endpoint.
$articles = Article::search('laravel typesense')
->where('status', 'published')
->paginate(20);
Where to look first: the package README, the published config file, the service provider, and the Typesense engine class under src/. Confirm how it maps Scout’s builder methods to Typesense search parameters before building complex filters.
Typesense is schema-first. Decide field types up front and keep them in sync with toSearchableArray():
string, string[]int32, int64, floatboolint64 Unix timestamps, not formatted date stringsobject / object[] and enable nested fields in the collection schemaUse searchableAs() when the default table-based collection name is not what you want:
public function searchableAs(): string
{
return 'articles';
}
Typical day-to-day filters and sorting:
$results = Product::search($term)
->where('is_active', true)
->whereIn('category_id', $categoryIds)
->where('price', '>=', 100)
->orderBy('popularity', 'desc')
->paginate(24);
Eager-load database relations after search hydration:
$products = Product::search($term)
->query(fn ($query) => $query->with('category', 'media'))
->paginate(24);
Search quality usually depends on query_by, field weights, typo tolerance, and token dropping. Use the driver’s option/parameter API where available; otherwise call the engine directly.
$results = Article::search($term)
->options([
'query_by' => 'title,body,tags',
'query_by_weights' => '4,2,1',
'num_typos' => 1,
'drop_tokens_threshold' => 1,
])
->paginate(20);
In production, avoid synchronous indexing on every model save:
SCOUT_QUEUE=true
Use a queue connection with after_commit enabled so workers do not index rows before the database transaction commits. For bulk changes, prefer scout:import over saving thousands of models individually.
Useful for imports, migrations, and bulk updates:
Article::withoutSyncingToSearch(function () {
// bulk database work
});
$article->searchable(); // index one model
$article->unsearchable(); // remove one model
Scout returns hydrated Eloquent models, but Typesense is not a relational database. For complex authorization, joins, or aggregates, search first for IDs, then constrain the final Eloquent query:
$ids = Article::search($term)->take(200)->get()->modelKeys();
$articles = Article::query()
->whereIn('id', $ids)
->where('tenant_id', tenant()->id)
->with('author')
->paginate(20);
If you use Typesense directly from JavaScript, issue a search-only API key scoped to specific collections. Never ship the admin key to the browser.
toSearchableArray() usually requires recreating the collection and reimporting: scout:flush then scout:import. Plan for zero-downtime reindexing via collection aliases if search is critical.id field as a string. Cast (string) $this->id in toSearchableArray().object or object[] and nested fields must be enabled. Flattening data is often simpler.toArray(). It can include relations, hidden fields, appended attributes, and nulls that break the schema. Build the search payload explicitly.delete() and restore() behavior.after_commit on the queue connection and monitor failed indexing jobs. A row can exist in MySQL but be missing from Typesense.per_page (commonly 250). Scout pagination still works, but do not request huge pages.php artisan config:clear.curl or the Typesense dashboard to inspect collection schemas and documents. If Scout returns nothing, check whether the collection exists, whether documents are present, and whether query_by fields match the schema.scout:import can fail on a single malformed document. Run it in chunks and inspect the first failure rather than assuming the whole import succeeded.How can I help you explore Laravel packages today?