
August 12, 2026
8 min read
Table of Contents
By Kokil Thapa | Last reviewed: August 2026
Most Laravel developers default to Elasticsearch or Meilisearch the moment they need search, but for small-to-medium datasets, Laravel full-text search with MySQL and Scout delivers production-grade relevance without operational overhead. Native MySQL FULLTEXT indexes handle millions of rows efficiently when configured correctly, and Scout’s driver abstraction keeps your code portable. This guide covers the exact configuration, indexing strategy, and relevance tuning required to make MySQL search viable for real client projects in 2026.
database Scout driver, enabling boolean and natural language queries without external servers. Configure it by installing Scout, setting SCOUT_DRIVER=database, adding a FULLTEXT migration, and importing models with scout:import.How do you configure Laravel full-text search with MySQL and Scout?
Setting up Laravel full-text search with MySQL and Scout requires three distinct steps: package installation, environment configuration, and database schema modification. Many tutorials skip the schema step, leading to silent failures where Scout falls back to slow LIKE queries. On a recent legal-tech portal I built, skipping this step caused search latency to spike from 15ms to 800ms under load.
Install Scout and configure the database driver
First, install Laravel Scout via Composer. As of 2026, Scout 10.x is the current stable release supporting Laravel 11 and 12:
composer require laravel/scout
php artisan vendor:publish --provider="Laravel\Scout\ScoutServiceProvider" In your .env file, explicitly set the driver to database. The default is often algolia, which will fail silently if credentials are missing:
SCOUT_DRIVER=database
SCOUT_QUEUE=true Enable queueing immediately. Synchronous indexing blocks HTTP responses during model saves. For any production application handling user input, offloading index updates to a queue worker is non-negotiable. Configure your config/scout.php to use the same queue connection as your other background jobs.
Create the FULLTEXT index migration
This is where most implementations fail. Scout’s database driver does not create FULLTEXT indexes automatically. You must add them manually via migration. For a typical articles or documents table:
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::table('articles', function (Blueprint $table) {
// InnoDB supports FULLTEXT since MySQL 5.6
// Minimum token length defaults to 3 chars in InnoDB
$table->fullText(['title', 'body', 'summary']);
});
}
public function down(): void
{
Schema::table('articles', function (Blueprint $table) {
$table->dropFullText(['title', 'body', 'summary']);
});
}
}; Run the migration on your staging environment first. Building a FULLTEXT index on a table with more than 500,000 rows can lock the table for several minutes depending on disk I/O. On production systems serving Nepal-based clients with shared hosting constraints, schedule this during low-traffic windows or use pt-online-schema-change to avoid downtime.
How does Scout’s database driver differ from raw Eloquent LIKE queries?
Understanding the architectural difference prevents costly mistakes. Raw LIKE '%keyword%' queries perform full table scans because the leading wildcard prevents B-tree index usage. On a table with 100,000 rows, this means reading every single row from disk. Scout’s database driver translates searches into MATCH() AGAINST() syntax, which leverages the inverted index structure of FULLTEXT.
| Feature | Eloquent LIKE %...% | Scout Database Driver |
|---|---|---|
| Index Usage | None (full table scan) | Inverted FULLTEXT index |
| Relevance Ranking | Not available | Native TF-IDF scoring |
| Boolean Operators | Manual regex/string parsing | +required -excluded "phrase" |
| Stopword Handling | Returns noise results | Automatic filtering |
| Min Token Length | 1 character | 3 characters (InnoDB default) |
| Performance at 100K rows | 500ms – 2s | 5ms – 20ms |
The trade-off is flexibility. FULLTEXT cannot search partial words shorter than the minimum token length, and it ignores stopwords like "the", "and", or "in". For a legal-tech platform searching case citations or statute numbers, this is usually acceptable. For product SKUs or alphanumeric codes, you may need to supplement with a secondary indexed column.
How do you tune relevance ranking in MySQL full-text search?
Out-of-the-box relevance rarely matches business expectations. MySQL uses TF-IDF (term frequency–inverse document frequency) scoring, but you can influence results through query structure and model configuration. In practice, title matches should always outrank body matches for content-heavy sites.
Use boolean mode for precise control
Natural language mode is convenient but opaque. Boolean mode gives you explicit operators that map directly to user intent:
// Natural language (implicit relevance)
$articles = Article::search('contract dispute resolution')->get();
// Boolean mode (explicit weighting)
$articles = Article::search('+contract +dispute +resolution')->get();
// Boost title matches using raw expression in advanced queries
$articles = Article::search(function ($query) {
return $query->whereRaw(
"MATCH(title) AGAINST(? IN BOOLEAN MODE) * 3 +
MATCH(body) AGAINST(? IN BOOLEAN MODE)",
['+contract +dispute', '+contract +dispute']
);
})->orderByRaw('relevance DESC')->get(); Note that Scout’s standard search() method doesn’t expose multi-column weighting directly. For advanced relevance tuning, combine Scout’s indexing with custom Eloquent scopes or raw expressions. Document these clearly — future maintainers need to understand why certain queries bypass Scout’s abstraction.
Adjust minimum token length for Nepali and technical content
MySQL’s default innodb_ft_min_token_size is 3. For Nepali-language content or technical abbreviations like "PAN", "VAT", or "IRD", this works well. However, if you’re indexing two-character legal section codes or single-letter variables, adjust the server configuration:
# my.cnf or my.ini
[mysqld]
innodb_ft_min_token_size = 2
ft_min_word_len = 2 After changing these values, you must rebuild all FULLTEXT indexes and restart MySQL. This is a destructive operation on production. Test thoroughly on staging first. For most Nepal-focused applications I’ve shipped, the default of 3 is sufficient and avoids indexing noise.
When should you choose MySQL over Meilisearch or Elasticsearch?
This decision hinges on dataset size, operational capacity, and budget. For a Nepal-based SME spending Rs 5,000–15,000/month on hosting, adding a separate search service doubles infrastructure costs and complexity. MySQL full-text handles up to 1–2 million documents comfortably on modest hardware when properly indexed.
- Choose MySQL + Scout when: Dataset < 2M records, team lacks DevOps bandwidth, budget is constrained, search requirements are keyword-based rather than semantic.
- Choose Meilisearch/Elasticsearch when: Dataset > 5M records, you need typo tolerance, faceted filtering, synonym expansion, geo-search, or sub-10ms p99 latency at scale.
- Hybrid approach: Start with MySQL. Scout’s driver abstraction lets you switch to Meilisearch later by changing only
SCOUT_DRIVERand re-importing. No application code changes required.
On a recent e-commerce project with ~80,000 products, MySQL full-text delivered 12ms average query time with boolean operators. We only migrated to Meilisearch when the client requested fuzzy matching and category facets six months post-launch. Starting simple saved three months of premature optimization.
How do you handle common production pitfalls with Scout and MySQL?
Real-world deployments surface issues absent from documentation. These patterns recur across multiple client projects:
Stale indexes after bulk operations
Scout listens to Eloquent events, but bulk inserts via DB::table()->insert() or raw SQL bypass model observers entirely. After data imports, CSV uploads, or migration scripts, manually reindex:
php artisan scout:import "App\Models\Article"
// Or for specific batches after bulk insert
Article::query()->chunkById(500, function ($articles) {
$articles->searchable();
}); Character encoding mismatches
Nepali Unicode content requires consistent utf8mb4 encoding across the database, connection, and FULLTEXT index. Verify your migration specifies the charset:
$table->fullText(['title', 'body'])
->charset('utf8mb4')
->collation('utf8mb4_unicode_ci'); If searches return empty results for valid Nepali terms, check SHOW VARIABLES LIKE 'character_set%'; and ensure no layer is downgrading to latin1.
Queue worker memory leaks during large imports
Processing hundreds of thousands of records in a single worker process causes memory exhaustion. Configure your supervisor or systemd unit to restart workers periodically:
[program:laravel-worker]
process_name=%(program_name)s_%(process_num)02d
command=php /var/www/html/artisan queue:work --sleep=3 --tries=3 --max-jobs=1000
autostart=true
autorestart=true
stopasgroup=true
killasgroup=true
numprocs=2 The --max-jobs=1000 flag forces graceful restarts, preventing OOM kills during overnight import jobs.
Implementing Laravel Full-Text Search with MySQL and Scout in Production
Laravel full-text search with MySQL and Scout remains the pragmatic choice for applications under 2 million records where operational simplicity outweighs advanced features. Start with the database driver, measure actual query performance under realistic load, and only introduce external services when metrics demand it. Scout’s abstraction ensures you’re never locked in.
If you’re building a search-dependent application and need hands-on implementation support, reach out to discuss your project requirements. Whether it’s tuning relevance for Nepali legal content or architecting a scalable catalog search, I’ve solved these problems repeatedly in production.



