eslam-dev/laravel-searchable
A Laravel package that adds searchable behavior to Eloquent models, letting you index records and run quick, flexible searches across your data with minimal setup. It plugs into your existing models so you can find matching results without writing custom query logic or wiring up a separate search se...
Version context: 1.0.2 is a patch release (1.0.1 → 1.0.2) and the release notes are a bare compare link with no described features, breaking changes, or deprecations. Treat it as bugfix-only: nothing here changes how you integrate the package, but read the diff before bumping in a pinned/locked environment. Because there is no prior assessment, the notes below describe the day-to-day shape of a "searchable model" package of this kind — verify exact class names, config keys, and method signatures against the repo README and the trait source, since the changelog does not document the API surface.
Minimal first steps:
Install and confirm discovery
composer require eslam-dev/laravel-searchable
Laravel's package auto-discovery should register the service provider. If the package ships a config file, publish it (php artisan vendor:publish --tag=searchable-config or the tag listed in the README) and commit it — do not rely on package defaults in production.
Read the trait before the README. The single most important file is the Searchable trait itself (typically under src/). It tells you exactly what it adds to the model: a query scope, a static search entry point, and any protected properties it expects you to define (searchable columns, weights, minimum term length). This is faster than guessing from examples.
Wire up one model end-to-end. Add the trait to a model with a small, stable schema (e.g. Article), declare the searchable columns, and replace an existing naive query on an index page. Do this before touching your biggest table — search packages hide their cost until you point them at real data volume.
First real use case: swapping where('title', 'like', "%{$q}%") chains in a listing controller for the package's scope, so multi-column matching, weighting, and (if supported) relevance ordering live in one place rather than being copy-pasted per controller.
Trait-on-model, scope-in-controller. The idiomatic usage is a thin trait declaration on the model and a fluent scope call in the controller, so existing query constraints still compose:
// Model — illustrative; confirm property names against the trait source
use Searchable;
protected array $searchable = ['title', 'body'];
// Controller
$results = Article::query()
->where('published', true) // your constraints still apply
->search($request->string('q')) // package scope
->with('author') // eager load as usual
->paginate(20);
The key pattern: treat the search scope as one clause among many, not as a replacement for the query builder. Everything you rely on today — where, whereHas, policy scoping, with, paginate, cursorPaginate — should remain available on both sides of it. If it doesn't, that's a signal the package is returning a collection rather than a builder, which changes pagination and eager-loading strategy significantly.
Scope search to the caller, not just the term. Multi-tenant and authorization filters belong outside the search scope:
$results = Article::query()
->where('tenant_id', $tenant->id)
->whereIn('status', $allowedStatuses)
->search($term)
->paginate();
This keeps the package ignorant of your permission model and makes it trivial to add a global scope or policy later.
Centralize term normalization. Put trimming, minimum-length checks, and multi-word splitting in a FormRequest or a small SearchQuery value object rather than in each controller. Packages differ on whether they AND or OR multiple words and whether they strip wildcards — pinning that behaviour in one place means an upgrade can't silently change your UX page by page.
Keep a fallback path. For low-traffic or admin screens, a LIKE fallback when the package returns nothing is often not worth the complexity — but for user-facing search on large tables, decide early whether the package's matching (usually LIKE-based) is genuinely sufficient or whether you need MATCH ... AGAINST / Postgres tsvector / an external index. This package is a convenience layer, not a search engine; the integration tip is to isolate search behind a SearchService so swapping the driver later is a one-file change.
Test the scope, not the page. Assert on the query builder (toSql() / bound terms) for one integration test per searchable model, plus one feature test that the term reaches the scope from the request. That catches the realistic failure mode — a column added to the model but not to the searchable list — without brittle HTML assertions.
Searchable is a very common trait name (Spatie and others use it). If you depend on more than one search package, alias on import: use Searchable as PackageSearchable;. A fatal "trait method conflict" on boot is the usual symptom.% and _ from user input into LIKE, a search for 100% or a_b behaves as a pattern rather than literal text. Escape or strip these at the request layer — cheap, and avoids the "search returns everything" bug report.LIKE is case-insensitive under utf8mb4_unicode_ci; Postgres LIKE is case-sensitive and needs ILIKE. Packages that build raw SQL often get this wrong on one driver. Test on the driver you actually deploy on, not SQLite — SQLite's LIKE is case-insensitive for ASCII, which will happily hide the bug in your test suite.select * and relevance ordering. If the package supports weighting/ordering, ordering by a computed relevance score can conflict with paginate's stability and with distinct/groupBy. Order by relevance then by id as a tiebreaker.bootstrap/cache/config.php. In Docker/CI, run php artisan config:clear when changing searchable settings, or you will debug a setting that is loaded and ignored.git diff 1.0.1 1.0.2 against the tag or the GitHub compare view) for changes to the trait's protected properties and the query-building internals; those are the surfaces your code actually depends on. Pin to ~1.0 and let Composer take patch bumps, but run your search integration tests before merging the lockfile change.->toSql() with bindings via ->getBindings()) rather than dumping the builder — the failure is nearly always the term-building step, not the database. If results are empty, check minimum-term-length config and whether the searchable columns are actually listed on the model before suspecting indexing.How can I help you explore Laravel packages today?