Search

Search uses MySQL fulltext indexing on a denormalized keywords column rather than searching individual columns directly. Every searchable model gets the same pattern, so adding search to a new model is just a trait, a property, and an observer registration.

Why a keywords column

Searching across multiple columns with LIKE doesn't scale and fulltext indexes can't span joins. A single keywords column lets you control exactly what's searchable without changing query logic. When you want to add a new field to search, you update the $searchable array and backfill. No query changes, no new indexes.

forSearch vs forKeywordsSearch

forSearch is what controllers call. forKeywordsSearch is the raw fulltext query. They're separate because some models need to search beyond keywords. User overrides forSearch to also match on email, since fulltext is bad at structured strings like email addresses. Models without extra clauses don't need to override anything.

Adding search to a new model

Use the Searchable trait, define $searchable, register the observer, and add a nullable keywords text column with a fulltext index to the migration.

Backfilling

Changing $searchable only affects future saves. Run refreshKeywords() on existing rows to rebuild them. It uses updateQuietly to avoid re-triggering the observer.

Driver support

The fulltext index in migrations is wrapped in a SQLite check since SQLite doesn't support fulltext indexes. MySQL and Postgres both handle $table->fullText() natively through Laravel's grammar layer.

The Searchable trait does the same thing at query time. MySQL gets MATCH AGAINST in boolean mode, and anything else falls back to LIKE. This mostly matters for testing since the test suite runs on SQLite. The LIKE fallback is close enough to verify search behavior without needing a real MySQL connection for every test run.

If you add Postgres support, you'd need a third branch in scopeForKeywordsSearch using to_tsvector and to_tsquery. The migration side already works since Laravel compiles fulltext indexes per driver.

Seeding

Wrap bulk inserts in Model::withoutEvents() and backfill keywords after. The fulltext index rebuild on every insert adds up fast at scale.

© Websanova 2026 About Privacy