yab/laravel-scout-mysql-driver
A Laravel Scout engine that powers full-text search directly with MySQL, so you can skip Elasticsearch or Algolia. Adds relevance ranking, boolean and wildcard queries, and easy model integration using native MySQL indexes.
The shortest path from zero to a working search box:
composer require laravel/scout yab/laravel-scout-mysql-driver
.env:
SCOUT_DRIVER=mysql
Publish config/scout.php (php artisan vendor:publish --provider="Laravel\Scout\ScoutServiceProvider") if you need to tweak the mysql block — the package registers itself through Scout's extend() mechanism, so the driver name is usually enough.MATCH ... AGAINST returns nothing and MySQL may even error on some versions:
Schema::table('posts', function (Blueprint $table) {
$table->fullText(['title', 'body']);
});
use Laravel\Scout\Searchable;
class Post extends Model
{
use Searchable;
protected $searchableColumns = ['title', 'body'];
}
Post::search('laravel scout')->paginate(15);
Where to look first: the mysql block in config/scout.php, your model's $searchableColumns, the migration holding the FULLTEXT index, and finally vendor/yab/laravel-scout-mysql-driver/src/Engines/MysqlEngine.php — the engine is small and reading it once explains every behavior below.
Search plus Eloquent constraints. The engine compiles search down to a whereRaw on the model's own query builder, so normal Eloquent chaining works and is the main reason to use this package:
Post::search($term)
->where('status', 'published')
->where('tenant_id', $tenantId)
->orderByDesc('published_at')
->paginate(20);
Soft deletes, global scopes, and eager loads behave as usual because you're querying the real table.
No index to synchronize. Because search runs directly against the table, Scout's searchable(), unsearchable(), and scout:import are effectively no-ops. Writes are instantly searchable — a genuine advantage over external engines, and it removes the "why is my record not in the index" class of bugs entirely.
Boolean mode for precision. For user-driven queries, boolean syntax gives you +required, -excluded, "exact phrase", and prefix*. Sanitize raw user input before passing it through, or wrap it in a small query-builder service so operators like @ or unbalanced quotes don't produce SQL errors.
Column customization per model. If a model exposes more than title/body, align $searchableColumns exactly with the migration's fullText([...]) column list. A mismatch is the most common silent failure: MySQL requires the MATCH column set to correspond to an existing FULLTEXT index.
Per-tenant or per-scope indexes. For multi-tenant apps, avoid one giant index. Either scope with where('tenant_id', ...) (simplest, still uses the index for the match and filters afterwards) or maintain separate searchable tables if data volumes justify it.
Customizing behavior. Subclass the engine and re-register it if you need relevance ordering, result mapping, or query preprocessing:
Scout::extend('mysql', fn () => new App\Search\CustomMysqlEngine());
This is the cleanest extension point given the package is archived.
Testing. Use MySQL (or MariaDB) in CI for anything touching search; SQLite has no MATCH ... AGAINST. For unit tests, fake the engine or assert on the generated SQL rather than on relevance ordering.
composer.json constraints against your Laravel/PHP version, and be prepared to fork or add a replace entry. It predates Laravel 10/11 conventions; expect to loosen constraints rather than get upstream fixes.innodb_ft_min_token_size defaults to 3, so "go", "UI", "C#" won't match. Lower it in my.cnf (requires index rebuild) if short tokens matter.innodb_ft_server_stopword_table — you cannot fix this from application code.WHERE clause; if you need "best match first", add orderByRaw("MATCH (...) AGAINST (? IN BOOLEAN MODE) DESC", [...]) yourself.$searchableColumns without a matching fullText migration breaks search. Keep the two in lockstep.->toSql() before debugging further.SCOUT_DRIVER or the mysql block, run php artisan config:clear; a stale cached config is a frequent "driver not found" cause.Post::search('') should do — return everything, or short-circuit. Behavior varies across Scout versions and can surprise you in paginated listings.within(), keys(), and raw engine access may not be implemented. Anything you rely on beyond search/where/paginate/take deserves a test against the actual driver.How can I help you explore Laravel packages today?