Kokil Thapa - Professional Web Developer in Nepal
Freelancer Web Developer in Nepal with 15+ Years of Experience

Kokil Thapa is an experienced full-stack web developer focused on building fast, secure, and scalable web applications. He helps businesses and individuals create SEO-friendly, user-focused digital platforms designed for long-term growth.

Laravel Full-Text Search with MySQL and Scout

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.

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.

1. Install Scoutcomposer requirevendor:publishSCOUT_DRIVER=database2. Add FULLTEXT Index$table->fullText([...])InnoDB Engine Required⚠ Manual Migration3. Import Modelsscout:import ModelPopulate IndexQueue Enabled
Three-stage configuration pipeline for Laravel full-text search with MySQL and Scout

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.

FeatureEloquent LIKE %...%Scout Database Driver
Index UsageNone (full table scan)Inverted FULLTEXT index
Relevance RankingNot availableNative TF-IDF scoring
Boolean OperatorsManual regex/string parsing+required -excluded "phrase"
Stopword HandlingReturns noise resultsAutomatic filtering
Min Token Length1 character3 characters (InnoDB default)
Performance at 100K rows500ms – 2s5ms – 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.

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.

LIKE '%search%' QueryRow 1: Scan entire row ✗Row 2: Scan entire row ✗Row 3: Scan entire row ✗... N rows scanned sequentiallyO(n) — Full Table ScanFULLTEXT Index LookupInverted Indexterm → doc IDsTF-IDF ScoreRelevance calcReturn Top KSorted resultsO(log n) — Index Seek
Architectural comparison: sequential table scan vs inverted index lookup in MySQL full-text search

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_DRIVER and 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.

Start: Need Search?Dataset < 2M records?YESNONeed typo tolerance?Meilisearch / ElasticBudget < Rs 15K/mo?YESNOMySQL + ScoutZero extra costMeilisearchSelf-hosted option
Decision framework for selecting Laravel full-text search with MySQL and Scout versus dedicated search infrastructure

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.

Frequently Asked Questions

Laravel Scout is a first-party package providing a driver-based abstraction for full-text search. It syncs Eloquent models to a search index via observers, allowing simple model::search() queries instead of raw database MATCH AGAINST syntax or complex external API calls.

Yes. The community-maintained mysql-engine package enables Scout to use native MySQL FULLTEXT indexes directly. This avoids provisioning separate search infrastructure like Meilisearch or Typesense, making it ideal for small-to-medium Laravel applications where operational simplicity matters more than massive scale.

A basic integration typically costs NPR 25,000–45,000 (USD 185–335) for setup, indexing configuration, and testing. Complex multi-table search or custom ranking logic may reach NPR 70,000+ depending on schema complexity and existing codebase condition.

Laravel Scout 10.x requires Laravel 11 or 12 and PHP 8.2 minimum. While PHP 8.4 is the latest stable release, PHP 8.3 remains widely used in production. Always verify scout/scout-mysql-engine compatibility in composer.json before upgrading, as third-party drivers sometimes lag behind core framework releases.

Create a migration adding FULLTEXT indexes to searchable columns using $table->fullText(['title', 'description']). In your model, implement Searchable trait and define toSearchableArray(). Set SCOUT_DRIVER=mysql in .env. Run php artisan scout:import "App\Models\YourModel" to populate the initial index from existing records.

Common causes include missing FULLTEXT indexes, unimported data after enabling Scout, or boolean mode syntax errors. Verify indexes exist via SHOW INDEX FROM table WHERE Index_type='FULLTEXT'. Re-run php artisan scout:import. Check that search terms meet MySQL's minimum word length (default 3 characters) and aren't in the stopword list.

Native MySQL FULLTEXT supports wildcard suffixes (term*) in boolean mode but not true fuzzy matching. For typo tolerance, consider Meilisearch or Typesense drivers instead. If staying with MySQL, implement application-level fallbacks like LIKE queries for short terms or use ngram parser for CJK languages, accepting performance trade-offs at scale.

Scout uses Eloquent model observers to automatically sync creates, updates, and deletes to the search index. For MySQL driver, this means immediate FULLTEXT index updates within the same transaction. Queue the MakeSearchable and RemoveFromSearch jobs in production to prevent blocking user requests during bulk operations or high-traffic writes.

For catalogs under 500,000 products with moderate traffic, MySQL FULLTEXT performs adequately when properly indexed and cached. On a WooCommerce-to-Laravel migration I worked on, search latency stayed under 100ms at 200k SKUs. Beyond that threshold, or with complex faceting needs, dedicated engines like Meilisearch provide better relevance tuning and horizontal scaling.

MySQL FULLTEXT doesn't natively support column weighting through Scout's interface. Override the search callback using Model::search()->within() to pass raw MATCH AGAINST expressions with custom weights. Alternatively, compute relevance scores in toSearchableArray() and store as a searchable attribute, then sort by that field in your query builder chain.

Always validate and sanitize search input before passing to Scout. MySQL boolean mode interprets special characters (+, -, *, ~) as operators, enabling injection-like behavior. Strip or escape these characters unless intentional operator support is desired. Rate-limit search endpoints to prevent abuse, and log unusual query patterns for monitoring potential reconnaissance or denial-of-service attempts.

Scout adds model synchronization, driver portability, and a clean API at the cost of slight abstraction overhead. Direct MATCH AGAINST gives full control over boolean/natural language modes and relevance expressions but couples code to MySQL. Use Scout when you value testability and future engine flexibility; use raw queries when you need MySQL-specific features Scout cannot express cleanly.

Scout indexes single models only. For cross-model search, denormalize related data into the parent model's toSearchableArray() method or create a dedicated search index model joining relevant tables. On a legal-tech portal I built, we indexed case summaries alongside attorney profiles by embedding related fields, avoiding JOIN overhead during search while keeping source models normalized.

Enable MySQL slow query log and EXPLAIN search queries to identify missing indexes or full table scans. Ensure FULLTEXT indexes cover all searched columns. Check innodb_ft_result_cache_limit and ft_min_word_len settings. Profile Scout's observer overhead during writes. Add Redis caching for frequent identical queries. On high-read systems, consider read replicas to isolate search load from transactional writes.

Migrate when you need typo tolerance, synonym handling, geo-search, or sub-50ms latency beyond 500k records. Also consider switching if relevance tuning becomes unmaintainable with MySQL's limited ranking controls. The migration is straightforward since Scout abstracts the driver—update SCOUT_DRIVER, install the new package, re-import data, and adjust any MySQL-specific raw query overrides in your codebase.

Share this article

Quick Contact Options
Choose how you want to connect me: