tom-lingham/searchy
Searchy is a Laravel package for fuzzy database searching. Query multiple columns with relevance scoring and drivers like simple, fuzzy, and levenshtein, returning the most relevant records instead of relying on plain exact LIKE matches.
composer require tom-lingham/searchy. The package is archived (last release 2020-05-08) and its illuminate/support constraint tops out around Laravel 7, so on Laravel 8+ expect to either use a fork, add a repositories vcs entry, or pass --ignore-platform-reqs. Verify the resolved constraint with composer show tom-lingham/searchy before writing any code against it.php artisan vendor:publish --provider="TomLingham\Searchy\SearchyServiceProvider" gives you config/searchy.php, which holds the default driver and the driver registry (fuzzy, simple, levenshtein).config/searchy.php (which driver is default and how drivers are keyed) and src/SearchDrivers/SimpleSearchDriver.php (the smallest driver — it shows exactly what SQL shape every driver produces). Everything else in the package is a variation on that one query-building method.use TomLingham\Searchy\Facades\Searchy;
$results = Searchy::users('name')->query('jon')->get();
users is the table name, 'name' is the column list, query() is the user input, get() returns an Eloquent Collection where every model carries an extra relevance attribute. Render that attribute to show match quality, and sort by it if you need deterministic ordering.
Pick the driver per query, not per app. The three bundled drivers trade accuracy for portability:
simple — straight LIKE '%term%' across the fields. Cheapest, works on every DB, no fuzzy tolerance.fuzzy — splits the term on whitespace and scores each word per column, weighting prefix matches above mid-string matches. This is the default and the one you want for a search box.levenshtein — typo tolerance via edit distance. Requires a LEVENSHTEIN SQL function to exist in your database; it is not shipped by the driver.Searchy::driver('simple')->users('name', 'email')->query($term)->get();
Multi-field search is just variadic columns. Searchy::users('name', 'email', 'username') OR's the conditions and sums relevance. Keep the column list short — every extra column multiplies the generated SQL and the scan cost.
Constrain columns and rows before get(). The builder proxies to Eloquent, so select(), limit(), and where() chain naturally:
$results = Searchy::users('name', 'email')
->query($input)
->select('id', 'name', 'email')
->limit(25)
->get();
Confirm with ->getQuery()->toSql() that your select() didn't drop the relevance alias — if it did, drop back to *.
Wrap it in a repository method, never call the facade from a controller. The facade's magic __call maps the method name to a table, so a typo in Searchy::user('name') silently queries a non-existent table at runtime. A single UserSearch::run(string $term) method centralises the table/column list, gives you one place to sanitise input, and makes it swappable when you inevitably migrate off an archived package.
Ordering and pagination live outside Searchy. get() hands back a plain Collection, not a paginator. For paginated UIs, take getQuery() and paginate it yourself (verify the relevance alias survives paginate()), or apply ->limit() plus a manual offset. If you need eager loading, ->load('profile') on the returned collection — Searchy has no relationship awareness and cannot join for you.
Custom drivers are the extension point. Extend TomLingham\Searchy\SearchDrivers\BaseSearchDriver, override the query-string method (copy SimpleSearchDriver as the template), then register the class in config/searchy.php under drivers. That's how you swap LIKE for SOUNDEX, a prefix-only match, or a MATCH ... AGAINST full-text call.
Index for the pattern you actually use. LIKE 'term%' can use a B-tree index; LIKE '%term%' cannot. If your tables are large, write a custom driver that matches prefixes and let the fuzzy behaviour live in the fallback path.
whereFullText / a LIKE scope. Do not build a new feature on this without an exit plan.->getQuery()->toSql() with a hostile input like ' OR 1=1 -- and verify quoting. Never let user input reach the table name or column list — those come from the magic method call, so hardcode them.EXPLAIN before shipping to a table over a few thousand rows; the relevance subquery runs per row.levenshtein driver will fail silently-ish if the DB function isn't installed — you get a SQL error on first query, or an empty result set on some setups. It is MySQL-specific; on SQLite/Postgres use fuzzy or simple and keep your test suite on a driver that works there.relevance is an appended column, not a real one. If your table already has a relevance column you'll get a collision; if you select() explicitly you may shadow it. It disappears the moment you re-query the models through Eloquent.get() loads everything. Always limit().config/searchy.php, run php artisan config:cache (and config:clear in dev) or your driver changes won't take effect.Searchy alias in config/app.php.getQuery()->toSql() and getQuery()->getBindings() tell you far more than staring at relevance scores; the fuzzy weighting logic is not obvious from the source and varies by term length and whitespace.utf8mb4_general_ci gives case-insensitive matching, _bin does not.How can I help you explore Laravel packages today?