h-farm/laravel-searchable
Laravel Searchable by h-farm adds simple, driver-based full-text search to Eloquent models, letting you index records and run clean, reusable search queries with minimal setup in Laravel applications.
composer require maize-tech/laravel-searchable — 4.2.0 is the first line that declares Laravel 13 support, so L13 apps must be on ^4.2. Apps on L10–L12 can stay on 4.1.0 with zero code changes; upgrading is a no-op (see Gotchas).src/) — it is small; read it once and you will know exactly what SQL it generates (spoiler: LIKE/ILIKE conditions, not full-text search).php artisan vendor:publish --tag="searchable-config" where available) — the matching/case-sensitivity knob lives there.Searchable trait to User, declare the searchable columns (name, email, and any relation you want reachable, e.g. posts.title), then build a single controller action that takes ?q=, validates it, and returns User::search($q)->paginate(). Ship that behind a simple search box before touching multi-model search.User::where('team_id', $team)->search($q)->orderBy('name')->paginate(20). Order of chaining matters for readability and for what you can later replace.q should return an unfiltered (or empty) result set rather than a LIKE '%%' full-table scan. Put this in the FormRequest or a SearchService, not in every controller.App\Search\SearchService with a search(string $term, array $models) method means swapping in Scout/Meilisearch/laravel-scout later is a one-file change and not a grep-and-replace across fifty controllers. Do this on day one even though it feels like ceremony.LengthAwarePaginator results or a flattened SearchResult DTO (type, title, url, excerpt), and merge. Cap per-model results (e.g. 5) and link to a per-model search page for the full list — otherwise you get wildly unbalanced result mixes.posts.title) in the results list, add ->with('posts'); the package's join/subquery does not hydrate relations for you.->distinct() or group by the parent key, and verify with ->toSql() before shipping.CASE you add yourself), or accept deterministic alphabetical/chronological ordering and be explicit about it in the UI.test_search_finds_user_by_partial_name style tests against your own route. Those tests survive a future move to Scout; tests of the trait's internals do not.SearchRequest validating q (string, max 100) and page; SearchResultResource shaping the payload. Keeps the escaping/trimming logic in one place.LIKE-based search, not full-text search. No stemming, no relevance ranking, no typo tolerance, and a leading % defeats your index. Fine for admin panels and datasets up to a few hundred thousand rows; not fine as a public "search our catalogue" feature. Decide this before you build the UI, not after._ci collation makes LIKE case-insensitive; PostgreSQL LIKE is case-sensitive (ILIKE or LOWER() needed). This is the #1 source of "works on my machine, broken in prod" reports with this kind of package — confirm the generated SQL on your actual driver.% and _. A user searching for 100% or foo_bar will otherwise get wildcard behaviour. Trim and escape in the FormRequest before it reaches the query.LIKE still can't use them for the search itself, but every other filter/order in the same query benefits.email or national_id searchable lets users enumerate records by probing partial values. Only declare columns that are already visible in results, or scope hard by tenant/policy first.search() in the chain. There is no built-in global scope; search does not respect your BelongsToTenant trait beyond whatever the underlying builder already applies.LIKE case-sensitivity and collation. If behaviour matters, run the search tests against the real driver in CI or assert case-insensitively.->toSql() and ->getBindings() on the builder before ->get(); or watch the queries via Telescope/Debugbar. Nine times out of ten the surprising result is a wildcard escaping issue or an unexpected JOIN.composer require maize-tech/laravel-searchable:^4.2 fails on an L13 app, the conflict is almost certainly another dependency still capping illuminate/* below 13; run composer why-not rather than assuming this package is at fault. Also check the PHP floor Laravel 13 imposes on your runtime before bumping.ILIKE/LOWER() behaviour, and wrap the trait behind your own service (above) so the escape hatch is cheap. There is no event/hook surface worth relying on — the extension story is "write your own query and keep the same public interface".^4.2) and let Renovate/Dependabot handle minors. This package's minors have historically been framework-support and CI hygiene, so upgrade risk is low, but the searchable-columns method name is the one thing to re-read in the changelog on every bump.How can I help you explore Laravel packages today?