PHPackages                             ashiqfardus/laravel-fuzzy-search - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Database &amp; ORM](/categories/database)
4. /
5. ashiqfardus/laravel-fuzzy-search

ActiveLibrary[Database &amp; ORM](/categories/database)

ashiqfardus/laravel-fuzzy-search
================================

A powerful, zero-config fuzzy search package for Laravel with fluent API

v2.0.0(1mo ago)82.2k1MITPHPPHP ^8.1|^8.2|^8.3|^8.4CI passing

Since Jan 12Pushed 1mo agoCompare

[ Source](https://github.com/ashiqfardus/laravel-fuzzy-search)[ Packagist](https://packagist.org/packages/ashiqfardus/laravel-fuzzy-search)[ RSS](/packages/ashiqfardus-laravel-fuzzy-search/feed)WikiDiscussions main Synced today

READMEChangelogDependencies (18)Versions (3)Used By (0)

Laravel Fuzzy Search
====================

[](#laravel-fuzzy-search)

[![Latest Version on Packagist](https://camo.githubusercontent.com/539790d77cb61f85cd5b7e32849dd3135844e8de9f2b9e4d48d87205d5dcba71/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f762f61736869716661726475732f6c61726176656c2d66757a7a792d7365617263682e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/ashiqfardus/laravel-fuzzy-search)[![Total Downloads](https://camo.githubusercontent.com/b261e618155100063dc398917c9e9121be0734a8863c01fdbb83d95e7d8a2bff/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f64742f61736869716661726475732f6c61726176656c2d66757a7a792d7365617263682e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/ashiqfardus/laravel-fuzzy-search)[![License](https://camo.githubusercontent.com/bda9d6f3cda132822253dade6b98514985a51ed6643442bc8a5c9800abc3fca7/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f6c2f61736869716661726475732f6c61726176656c2d66757a7a792d7365617263682e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/ashiqfardus/laravel-fuzzy-search)[![PHP Version](https://camo.githubusercontent.com/88638a130f171abc380fc73037fdd6f18536982f3827aeb617da22306ad8e230/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f7068702d762f61736869716661726475732f6c61726176656c2d66757a7a792d7365617263682e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/ashiqfardus/laravel-fuzzy-search)[![Laravel Version](https://camo.githubusercontent.com/e82135eff843564f375f8ac5fb403df85dd24527a2497bd4f80968a2da140060/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f4c61726176656c2d3925324225323025374325323031302532302537432532303131253230253743253230313225323025374325323031332d4646324432303f6c6f676f3d6c61726176656c)](https://laravel.com)

A powerful, **zero-config** fuzzy search package for Laravel with fluent API. Works with all major databases without external services.

**Demo:** [laravel-fuzzy-search-demo](https://github.com/ashiqfardus/laravel-fuzzy-search-demo) - See the package in action!

**Documentation:** [Installation](#installation) • [Quick Start](#quick-start) • [Algorithms](#search-algorithms) • [BM25 Index](#bm25-inverted-index) • [Extended Syntax](#extended-search-syntax) • [Scout Driver](#scout-driver) • [Performance](#performance--scaling) • [Compatibility](#algorithm--database-compatibility) • [Upgrade v1→v2](docs/UPGRADE_v1_TO_v2.md)

Features
--------

[](#features)

CategoryFeatures**Core**Zero-config search • Fluent API • Eloquent &amp; Query Builder support**Algorithms**Multiple fuzzy algorithms • Typo tolerance • Multi-word token search**Scoring**Field weighting • Relevance scoring • Prefix boosting • Partial match • Recency boost**Text Processing**Stop-word filtering • Synonym support • Language/locale awareness**Internationalization**Unicode support • Accent insensitivity • Multi-language**Results**Highlighted results • Custom scoring hooks • Debug/explain-score mode**Performance**BM25 inverted index • Async indexing (queue) • Redis/cache support**Pagination**Stable ranking • Cursor pagination • Offset pagination**Reliability**Fallback search strategy • DB-agnostic • Rate-limit friendly • SQL-injection safe**Configuration**Config file support • Per-model customization**Developer Tools**CLI indexing • Benchmark tools • Built-in test suite • Performance utilities**Smart Search**Autocomplete suggestions • "Did you mean" spell correction • Multi-model federation • Search analyticsTable of Contents
-----------------

[](#table-of-contents)

- [Installation](#installation)
- [Quick Start](#quick-start)
- [Search Algorithms](#search-algorithms)
- [Field Weighting &amp; Scoring](#field-weighting--scoring)
- [Text Processing](#text-processing)
- [Result Presentation](#result-presentation)
- [Performance &amp; Indexing](#performance--indexing)
- [BM25 Inverted Index](#bm25-inverted-index)
- [Extended Search Syntax](#extended-search-syntax)
- [Scout Driver](#scout-driver)
- [Pagination](#pagination)
- [Reliability &amp; Safety](#reliability--safety)
- [Events](#events)
- [Configuration](#configuration)
- [CLI Tools](#cli-tools)
- [Performance &amp; Scaling](#performance--scaling)
- [Algorithm × Database Compatibility](#algorithm--database-compatibility)
- [Testing](#testing)
- [Requirements](#requirements)

---

Installation
------------

[](#installation)

```
composer require ashiqfardus/laravel-fuzzy-search
```

**That's it!** Zero configuration required. Start searching immediately.

Optionally publish the config file:

```
php artisan vendor:publish --tag=fuzzy-search-config
```

If you plan to use the **BM25 inverted index** (recommended for 10k+ rows), also run:

```
php artisan migrate
```

> **Upgrading from v1.x?** There are breaking changes — result rankings and `_score` values may shift. Run the scanner to find affected code, then follow the full guide.
>
> ```
> composer require ashiqfardus/laravel-fuzzy-search
> php artisan migrate
> php artisan fuzzy-search:upgrade-v1   # scans your app/ for v1-era API usage
> ```
>
>
>
> → [Full upgrade guide](docs/UPGRADE_v1_TO_v2.md)

---

Quick Start
-----------

[](#quick-start)

### Zero-Config Search

[](#zero-config-search)

```
// Just add the trait and search!
use Ashiqfardus\LaravelFuzzySearch\Traits\Searchable;

class User extends Model
{
    use Searchable;
}

// Search immediately - auto-detects searchable columns
$users = User::search('john')->get();
```

**How auto-detection works:** The package automatically detects common column names in this priority order:

- `name`, `title` (weight: 10)
- `email`, `username` (weight: 8)
- `first_name`, `last_name` (weight: 7)
- `description`, `content`, `body` (weight: 5)
- `bio`, `summary`, `excerpt` (weight: 3)
- `slug`, `sku`, `code` (weight: 2-6)

If none of these exist, it falls back to the model's `$fillable` columns.

### Manual Column Configuration

[](#manual-column-configuration)

You can manually specify which columns to search and their weights:

```
class User extends Model
{
    use Searchable;

    // Option 1: Define in $searchable property
    protected array $searchable = [
        'columns' => [
            'name' => 10,        // Highest priority
            'email' => 5,        // Medium priority
            'bio' => 1,          // Lowest priority
        ],
        'algorithm' => 'fuzzy',
        'typo_tolerance' => 2,
    ];
}

// Option 2: Specify at query time (merges with / adds to $searchable columns)
$users = User::search('john')
    ->searchIn(['name' => 10, 'email' => 5])
    ->get();

// Option 3: Simple array without weights (all get weight of 1)
$users = User::search('john')
    ->searchIn(['name', 'email', 'username'])
    ->get();
```

### Fluent API

[](#fluent-api)

```
$users = User::search('john doe')
    ->searchIn(['name' => 10, 'email' => 5, 'bio' => 1])  // Field weighting
    ->typoTolerance(2)                                      // Allow 2 typos
    ->withSynonyms(['john' => ['jon', 'johnny']])          // Synonym support
    ->ignoreStopWords(['the', 'and', 'or'])                // Stop-word filtering
    ->accentInsensitive()                                   // Unicode/accent handling
    ->highlight('mark')                                     // Highlighted results
    ->withRelevance()                                       // Relevance scoring
    ->prefixBoost(2.0)                                      // Prefix boosting
    ->debugScore()                                          // Explain scoring
    ->paginate(15);
```

### Eloquent &amp; Query Builder Support

[](#eloquent--query-builder-support)

```
// Eloquent
User::whereFuzzy('name', 'john')->get();
User::whereFuzzyMultiple(['name', 'email'], 'john')->get();

// Query Builder
DB::table('users')->whereFuzzy('name', 'john')->get();
DB::table('products')->fuzzySearch(['title', 'description'], 'laptop')->get();
```

---

Search Algorithms
-----------------

[](#search-algorithms)

### Available Algorithms

[](#available-algorithms)

AlgorithmBest ForTypo ToleranceSpeed`fuzzy`General purposeHighFast`levenshtein`Strict typo matchingConfigurableMedium`soundex`Phonetic matching (English names)PhoneticFast`metaphone`Phonetic matching (more accurate)PhoneticFast`trigram`Similarity matchingHighMedium`similar_text`Percentage similarityMediumMedium`simple` / `like`Exact substring (LIKE)NoneFastest```
// Use specific algorithm
User::search('john')->using('levenshtein')->get();
User::search('stephen')->using('soundex')->get();  // Finds "Steven"
User::search('stephen')->using('metaphone')->get(); // More accurate phonetic — see setup below
User::search('laptop')->using('similar_text')->get(); // Percentage match
```

> ⚠️ **`metaphone` requires one-time setup.** Unlike the other algorithms, it searches against a precomputed `{column}_metaphone` shadow column. Calling `using('metaphone')` without it throws `RuntimeException`. Run the three commands shown in [Shadow Columns](#shadow-columns) once per searchable column.

### Shadow Columns

[](#shadow-columns)

Most algorithms compute their score on the fly during the SQL query. **Metaphone is the exception** — PHP's `metaphone()` function isn't available in SQL, so the package precomputes the phonetic code on every save and stores it in a sibling column.

For a `users` table with a `name` column, that means adding a `name_metaphone` column right next to it. Each row's `name_metaphone` holds the phonetic code (e.g. `Stephen` → `STFN`, `Steven` → `STFN`, `Stefan` → `STFN`). At search time, the query is a simple equality check against the precomputed code — fast, no per-row PHP calls.

**Setup (one-time per column):**

```
# 1. Generate the migration that adds the shadow column + index
php artisan fuzzy-search:add-shadow-column "App\Models\User" name --type=metaphone

# 2. Apply it
php artisan migrate

# 3. Backfill existing rows (the observer only writes on future saves)
php artisan fuzzy-search:rebuild "App\Models\User" --fresh
```

After this, the `SearchableObserver` keeps `name_metaphone` in sync automatically on every `save()` and `update()`.

**What gets generated:**

```
// database/migrations/{timestamp}_add_name_metaphone_to_users_table.php
Schema::table('users', function (Blueprint $table) {
    $table->string('name_metaphone')->nullable()->after('name');
    $table->index('name_metaphone');
});
```

**Safety guards in the command:**

- The model must exist and be an Eloquent model.
- The model must live in your app namespace — vendor/framework classes are rejected.
- The column name is sanitized to `[a-zA-Z0-9_]` only — blocks SQL injection through the argument.

### Typo Tolerance

[](#typo-tolerance)

```
// Auto typo tolerance based on word length
User::search('jonh')->get();  // Finds "John"

// Configure tolerance level
User::search('john')
    ->typoTolerance(1)  // Allow 1 typo
    ->get();

// Disable typo tolerance
User::search('john')
    ->typoTolerance(0)
    ->get();
```

### Multi-Word Token Search

[](#multi-word-token-search)

```
// Searches each word independently and combines results
User::search('john doe developer')
    ->tokenize()        // Split into tokens
    ->matchAll()        // All tokens must match (AND)
    ->get();

User::search('john doe developer')
    ->tokenize()
    ->matchAny()        // Any token can match (OR)
    ->get();
```

### In-Memory Mode

[](#in-memory-mode)

```
use Ashiqfardus\LaravelFuzzySearch\Facades\FuzzySearch;

$matches = FuzzySearch::on($staticArray)->search('term')->searchIn(['name'])->get();
```

> **Supported methods:** `search`, `searchIn`, `take`, `skip`, `withRelevance`, `get`. Any other `SearchBuilder` method (e.g. `extended()`, `using()`, `preset()`, `paginate()`) will throw a `\BadMethodCallException` to prevent silent failures.

---

Field Weighting &amp; Scoring
-----------------------------

[](#field-weighting--scoring)

### Weighted Columns

[](#weighted-columns)

```
User::search('john')
    ->searchIn([
        'name' => 10,       // Highest priority
        'username' => 8,
        'email' => 5,
        'bio' => 1,         // Lowest priority
    ])
    ->get();
```

### Relevance Scoring

[](#relevance-scoring)

```
$users = User::search('john')
    ->withRelevance()
    ->get();

foreach ($users as $user) {
    echo "{$user->name}: {$user->_score}";
}
```

### Prefix Boosting

[](#prefix-boosting)

```
// Boost results that start with the search term
User::search('john')
    ->prefixBoost(2.5)  // 2.5x score for prefix matches
    ->get();
```

### Partial Match Support

[](#partial-match-support)

```
User::search('joh')
    ->partialMatch()    // Matches "john", "johnny", "johanna"
    ->minMatchLength(2) // Minimum 2 characters
    ->get();
```

### Custom Scoring Hooks

[](#custom-scoring-hooks)

```
User::search('john')
    ->customScore(function ($item, $baseScore) {
        // Boost verified users
        if ($item->is_verified) {
            return $baseScore * 1.5;
        }
        // Penalize inactive users
        if (!$item->is_active) {
            return $baseScore * 0.5;
        }
        return $baseScore;
    })
    ->get();
```

### Recency Boost

[](#recency-boost)

Boost newer records in search results:

```
// Recent records (within 30 days) get 1.5x score boost
User::search('john')
    ->boostRecent(1.5, 'created_at', 30)
    ->get();

// With defaults: 1.5x boost, created_at column, 30 days
User::search('john')
    ->boostRecent()
    ->get();
```

### Search Suggestions / Autocomplete

[](#search-suggestions--autocomplete)

Get autocomplete suggestions based on search term:

```
$suggestions = User::search('joh')
    ->searchIn(['name', 'email'])
    ->suggest(5);

// Returns: ['John', 'Johnny', 'Johanna', ...]
```

### "Did You Mean" Spell Correction

[](#did-you-mean-spell-correction)

Get alternative spellings when search has typos:

```
$alternatives = User::search('jonh')  // Typo
    ->searchIn(['name'])
    ->didYouMean(3);

// Returns: [
//     ['term' => 'john', 'distance' => 1, 'confidence' => 0.8],
//     ['term' => 'jon', 'distance' => 2, 'confidence' => 0.6],
// ]
```

### Multi-Model Federation Search

[](#multi-model-federation-search)

Search across multiple models simultaneously:

```
use Ashiqfardus\LaravelFuzzySearch\FederatedSearch;

$results = FederatedSearch::across([User::class, Product::class, Post::class])
    ->search('laptop')
    ->using('fuzzy')
    ->limit(20)
    ->get();

// Each result includes _model_type and _model_class
foreach ($results as $result) {
    echo $result->_model_type;  // 'User', 'Product', or 'Post'
}

// Get grouped results
$grouped = FederatedSearch::across([User::class, Product::class])
    ->search('test')
    ->getGrouped();

// Get counts per model
$counts = FederatedSearch::across([User::class, Product::class])
    ->search('test')
    ->getCounts();  // ['User' => 5, 'Product' => 3]
```

### Search Analytics

[](#search-analytics)

Get detailed analytics about your search configuration:

```
$analytics = User::search('john')
    ->searchIn(['name' => 10, 'email' => 5])
    ->using('levenshtein')
    ->typoTolerance(2)
    ->getAnalytics();

// Returns: [
//     'search_term' => 'john',
//     'algorithm' => 'levenshtein',
//     'columns_searched' => ['name', 'email'],
//     'typo_tolerance' => 2,
//     'tokenized' => false,
//     'stop_words_active' => false,
//     ...
// ]
```

---

Text Processing
---------------

[](#text-processing)

### Stop-Word Filtering

[](#stop-word-filtering)

```
// Use default stop words
User::search('the quick brown fox')
    ->ignoreStopWords()
    ->get();

// Custom stop words
User::search('the quick brown fox')
    ->ignoreStopWords(['the', 'a', 'an', 'and', 'or', 'but'])
    ->get();

// Language-specific stop words
User::search('der schnelle braune fuchs')
    ->ignoreStopWords('de')  // German stop words
    ->get();
```

### Synonym Support

[](#synonym-support)

```
User::search('laptop')
    ->withSynonyms([
        'laptop' => ['notebook', 'computer', 'macbook'],
        'phone' => ['mobile', 'cell', 'smartphone'],
    ])
    ->get();

// Or use synonym groups
User::search('laptop')
    ->synonymGroup(['laptop', 'notebook', 'computer'])
    ->get();
```

### Language / Locale Awareness

[](#language--locale-awareness)

```
User::search('john')
    ->locale('en')      // English
    ->get();

User::search('münchen')
    ->locale('de')      // German - handles umlauts
    ->get();
```

### Unicode &amp; Accent Insensitivity

[](#unicode--accent-insensitivity)

```
// Matches "café", "cafe", "Café"
User::search('cafe')
    ->accentInsensitive()
    ->get();

// Matches "naïve", "naive"
User::search('naive')
    ->unicodeNormalize()
    ->get();
```

---

Result Presentation
-------------------

[](#result-presentation)

### Highlighted Results

[](#highlighted-results)

```
$users = User::search('john')
    ->highlight('em')  // Wrap matches in  tags
    ->get();

foreach ($users as $user) {
    echo $user->_highlighted['name'];  // "Hello John Doe"
}

// Custom highlight
$users = User::search('john')
    ->highlight('', '')
    ->get();
```

### Debug / Explain-Score Mode

[](#debug--explain-score-mode)

```
$users = User::search('john')
    ->debugScore()
    ->get();

foreach ($users as $user) {
    print_r($user->_debug);
    // [
    //     'term' => 'john',
    //     'column_scores' => ['name' => 100, 'email' => 25],
    //     'multipliers' => ['prefix_boost' => 2.0, 'weight' => 10],
    //     'final_score' => 250,
    //     'matched_algorithm' => 'fuzzy',
    // ]
}
```

---

Performance &amp; Indexing
--------------------------

[](#performance--indexing)

### Async Indexing (Queue Support)

[](#async-indexing-queue-support)

```
// In config/fuzzy-search.php
'indexing' => [
    'async' => true,
    'queue' => 'search-indexing',
    'chunk_size' => 500,
],

// Re-index a single model (dispatches IndexModelJob to queue)
use Ashiqfardus\LaravelFuzzySearch\Jobs\IndexModelJob;
IndexModelJob::dispatch(User::class, $user->id);
```

### Redis / Cache Support

[](#redis--cache-support)

```
// Cache search results
User::search('john')
    ->cache(minutes: 60)
    ->get();

// Cache with custom key
User::search('john')
    ->cache(60, 'user-search-john')
    ->get();

// Use Redis for pattern storage
// In config/fuzzy-search.php
'cache' => [
    'enabled' => true,
    'driver' => 'redis',
    'ttl' => 3600,
],
```

---

BM25 Inverted Index
-------------------

[](#bm25-inverted-index)

For large tables (10k+ rows), the BM25 inverted index provides ranked, fast results without scanning the full table.

### How It Works

[](#how-it-works)

The indexing system has two parts:

**Part 1 — One-time initial build.** Run once after install (or after a schema change):

```
php artisan fuzzy-search:rebuild "App\Models\User" --fresh
```

**Part 2 — Automatic incremental updates.** After the initial build, every time a model is saved or deleted, the package dispatches a small queue job that re-indexes just that one row. No cron jobs or manual work needed.

The flow when a record is saved:

```
User::create(['name' => 'John'])
  → Eloquent fires 'saved' event
  → SearchableIndexingObserver dispatches IndexModelJob to queue
  → queue worker indexes the row (3 SQL queries)
  → 'john' is now in the index

```

### Database Tables

[](#database-tables)

TablePurpose`fuzzy_index_terms`Term dictionary: unique terms + document frequency (used for `didYouMean()`)`fuzzy_index_postings`Postings: term → model mapping with term frequency`fuzzy_index_meta`BM25 normalization: total docs + avg document length per model`fuzzy_index_documents`Per-document length cache for O(1) BM25 scoring### Production Setup

[](#production-setup)

**Step 1 — Run migrations:**

```
php artisan migrate
```

**Step 2 — Enable indexing in config:**

```
// config/fuzzy-search.php
'indexing' => [
    'enabled'          => true,       // must be true or saves are never indexed
    'async'            => true,       // true = queued (recommended for production)
    'queue'            => 'indexing', // dedicated queue keeps indexing isolated
    'chunk_size'       => 500,
    'max_tokens_per_doc' => 5000,     // security cap: prevents index poisoning
],
```

**Step 3 — Declare searchable columns on your model:**

```
use Ashiqfardus\LaravelFuzzySearch\Traits\Searchable;

class User extends Model
{
    use Searchable;

    protected array $searchable = [
        'columns' => [
            'name'  => 10,
            'email' => 5,
            'bio'   => 2,
        ],
    ];
}
```

**Step 4 — Build the initial index:**

```
# For small tables (< 50k rows)
php artisan fuzzy-search:rebuild "App\Models\User" --fresh

# For large tables (50k+ rows), dispatch batch queue jobs
php artisan fuzzy-search:rebuild "App\Models\User" --fresh --async --queue=indexing
```

**Step 5 — Start a queue worker:**

```
# Development
php artisan queue:work --queue=indexing,default

# Production (Supervisor)
```

Supervisor config (`/etc/supervisor/conf.d/fuzzy-search-worker.conf`):

```
[program:fuzzy-search-worker]
process_name=%(program_name)s_%(process_num)02d
command=php /var/www/html/artisan queue:work database --queue=indexing,default --sleep=3 --tries=3 --max-time=3600
autostart=true
autorestart=true
user=www-data
numprocs=2
redirect_stderr=true
stdout_logfile=/var/log/fuzzy-search-worker.log
```

For **Laravel Horizon** (Redis):

```
// config/horizon.php
'environments' => [
    'production' => [
        'supervisor-1' => [
            'connection' => 'redis',
            'queue'      => ['indexing', 'default'],
            'balance'    => 'auto',
            'processes'  => 4,
        ],
    ],
],
```

### Usage

[](#usage)

```
// BM25 search — faster + better relevance on large tables
$users = User::search('john')->useInvertedIndex()->get();

// didYouMean() reads from the term dictionary — O(1) at any dataset size
$suggestions = User::search('jonh')->searchIn(['name'])->didYouMean(3);
```

> **Note:** `useIndex()` is an alias for `useInvertedIndex()`. The deprecated legacy `search_index` table from v1 is no longer used.

> **Limitation:** The BM25 inverted index requires integer primary keys. Models with UUID/ULID primary keys are not currently supported — use the standard LIKE or Levenshtein paths for those.

> **Column weights and BM25:** `searchIn()` weights are respected by the LIKE/Levenshtein scoring paths but are ignored by BM25. BM25 scores by term frequency and inverse document frequency only.

### Artisan Commands

[](#artisan-commands)

```
# Show index statistics (total docs, tokens, avg length per model)
php artisan fuzzy-search:status

# Rebuild synchronously (good for < 50k rows)
php artisan fuzzy-search:rebuild "App\Models\User"
php artisan fuzzy-search:rebuild "App\Models\User" --fresh

# Rebuild asynchronously via queue (recommended for large tables)
php artisan fuzzy-search:rebuild "App\Models\User" --async
php artisan fuzzy-search:rebuild "App\Models\User" --fresh --async --queue=indexing

# Delete all index entries for a model
php artisan fuzzy-search:flush "App\Models\User"
```

### BM25 Tuning

[](#bm25-tuning)

```
// config/fuzzy-search.php
'bm25' => [
    'k1' => 1.5,   // Term-frequency saturation (1.2–2.0). Higher = more weight to repeated terms.
    'b'  => 0.75,  // Length normalisation (0–1). 0 = ignore doc length. 1 = full normalisation.
],
```

### Stemming (Optional)

[](#stemming-optional)

Default: no stemming (`NullStemmer`). With `NullStemmer`, `running` only matches `running`, not `run` or `ran`.

To enable Porter stemming:

```
composer require wamania/php-stemmer
```

```
// config/fuzzy-search.php
'indexing' => [
    'stemmer' => \Ashiqfardus\LaravelFuzzySearch\Indexing\PorterStemmer::class,
],
```

Supported languages: English, French, German, Spanish, Italian, Russian, Dutch, Portuguese, Swedish, Danish, Norwegian. You must rebuild the index after changing the stemmer.

### Observer Auto-Attach

[](#observer-auto-attach)

Adding the `Searchable` trait automatically registers observers via `bootSearchable()`:

- **`SearchableIndexingObserver`** — listens to `saved` and `deleted` events. Queues an `IndexModelJob` to update the BM25 index. This is a no-op when `indexing.enabled` is `false`.
- **`SearchableObserver`** — listens to `saved` events and writes metaphone shadow columns if they exist. Safe when no shadow columns are configured — the observer silently exits.

No configuration is required for either observer until you enable those features.

### Sync vs Async

[](#sync-vs-async)

`async = true` (default)`async = false`**How it works**Dispatches `IndexModelJob` to queueIndexes in the same request, no queue**Request latency**Unaffected+~10ms per save**Requires queue worker**YesNo**Best for**Production appsTests, local dev, low-traffic appsFor **tests**, set `indexing.async = false` so indexing happens synchronously:

```
// In your test setUp
config(['fuzzy-search.indexing.enabled' => true, 'fuzzy-search.indexing.async' => false]);
```

---

Extended Search Syntax
----------------------

[](#extended-search-syntax)

Use Fuse.js-style operators inside your search string for precise control over matching.

### Operators

[](#operators)

TokenMeaningExample`word`Substring match (default)`john``'word`Explicit substring include`'admin``=word`Exact equality`=John``^word`Prefix match`^Doe``word$`Suffix match`Sr$``!word`Exclude (NOT)`!banned``!^word`Inverse prefix`!^test``!word$`Inverse suffix`!@spam.com$``|`OR`john | jane`` ` (whitespace)AND (implicit)`=John ^Doe``( ... )`Grouping`admin (john | jane)``"phrase"`Quoted single token`"hello world"`### Usage

[](#usage-1)

```
// Exact first name + prefix last name + exclude banned
$users = User::search('=John ^Doe !banned')->extended()->get();

// OR semantics with grouping
$users = User::search('admin (john | jane)')->extended()->get();

// More examples
'Sr$ | Jr$'              // Names ending in Sr OR Jr
"'manager !@temp.com$"   // Substring 'manager' but not @temp.com emails
```

### Limits

[](#limits)

LimitDefaultConfig keyMaximum tokens per query32`query.max_tokens`Maximum nesting depth16`query.max_depth`### Pagination with Extended Syntax

[](#pagination-with-extended-syntax)

`paginate()` and `cursorPaginate()` are **not compatible** with `extended()` or `searchBoolean()` and will throw a `BadMethodCallException`. `simplePaginate()` works correctly.

```
// ✓ Works
User::search('=John ^Doe')->extended()->simplePaginate(15);
User::search('=John ^Doe')->extended()->get();

// ✗ Throws BadMethodCallException
User::search('=John ^Doe')->extended()->paginate(15);
```

### Match Offsets &amp; Blade Directive

[](#match-offsets--blade-directive)

Results with `->highlight()` enabled include a `_matches` array:

```
$first = $results->first();
$first->_matches;
// [['column' => 'name', 'value' => 'John Doe', 'indices' => [[0, 3]]]]
```

For safe HTML rendering, use the `@fuzzyHighlight` Blade directive:

```
@fuzzyHighlight($user, 'name')
```

The directive automatically escapes user-supplied content and wraps matches in `` tags.

---

Scout Driver
------------

[](#scout-driver)

The Scout engine adapter is bundled in this package and registers automatically when `laravel/scout` is installed. No separate driver package is required.

### Setup

[](#setup)

```
composer require laravel/scout
php artisan vendor:publish --provider="Laravel\Scout\ScoutServiceProvider"
```

In `.env`:

```
SCOUT_DRIVER=fuzzy-search

```

Build the index:

```
php artisan fuzzy-search:rebuild "App\Models\User"
```

### Usage

[](#usage-2)

Add both traits to your model:

```
use Laravel\Scout\Searchable;
use Ashiqfardus\LaravelFuzzySearch\Traits\Searchable as FuzzySearchable;

class User extends Model
{
    use Searchable, FuzzySearchable {
        // FuzzySearchable::search() takes precedence — returns the fluent SearchBuilder.
        // Scout's underlying engine (FuzzySearchEngine) is still used when SCOUT_DRIVER=fuzzy-search.
        FuzzySearchable::search insteadof Searchable;
        Searchable::search as scoutSearch;
    }

    public function toSearchableArray(): array
    {
        return ['name' => $this->name, 'email' => $this->email];
    }
}

$users = User::search('john')->get();
```

### Relevance Scores

[](#relevance-scores)

Results include `_score` (BM25 relevance, higher = more relevant):

```
foreach (User::search('laravel')->get() as $user) {
    echo $user->name . ': ' . $user->_score;
}
```

### Authorization

[](#authorization)

Scout's default behavior bypasses Eloquent global scopes. Apply them explicitly:

```
User::search('john')
    ->query(fn($q) => $q->withoutTrashed()->where('tenant_id', auth()->user()->tenant_id))
    ->get();
```

### How It Works

[](#how-it-works-1)

The Scout engine wraps the same `IndexManager` + `Bm25Scorer` used by `Model::search()->useInvertedIndex()`. There is no separate index — it reads from the same `fuzzy_index_*` tables.

---

Pagination
----------

[](#pagination)

### Stable Ranking

[](#stable-ranking)

```
// Results maintain consistent order across pages
$page1 = User::search('john')->stableRanking()->paginate(10, page: 1);
$page2 = User::search('john')->stableRanking()->paginate(10, page: 2);
```

### Pagination Methods

[](#pagination-methods)

```
// Offset pagination
$users = User::search('john')->paginate(15);

// Simple pagination (no total count - faster)
$users = User::search('john')->simplePaginate(15);

// Cursor pagination (best for infinite scroll)
$users = User::search('john')->cursorPaginate(15);

// Manual pagination
$users = User::search('john')
    ->take(10)
    ->skip(20)
    ->get();
```

> **Note:** `paginate()` and `cursorPaginate()` are not compatible with `extended()` or `searchBoolean()`. Use `simplePaginate()` or `get()` with those.

---

Reliability &amp; Safety
------------------------

[](#reliability--safety)

### Fallback Search Strategy

[](#fallback-search-strategy)

```
// Automatically falls back to simpler algorithm if primary fails
User::search('john')
    ->using('trigram')
    ->fallback('fuzzy')      // First fallback
    ->fallback('simple')     // Second fallback
    ->get();
```

### Rate-Limit Friendliness

[](#rate-limit-friendliness)

```
// Built-in debouncing for real-time search
User::search($query)
    ->debounce(300)  // 300ms debounce
    ->get();

// Query complexity limits
User::search($query)
    ->maxPatterns(50)  // Limit pattern generation
    ->get();
```

### SQL Injection Safety

[](#sql-injection-safety)

All queries use parameterized bindings. Search terms are automatically sanitized.

```
// Safe - input is sanitized
User::search("'; DROP TABLE users; --")->get();
```

### Exception Handling

[](#exception-handling)

```
use Ashiqfardus\LaravelFuzzySearch\Exceptions\LaravelFuzzySearchException;
use Ashiqfardus\LaravelFuzzySearch\Exceptions\EmptySearchTermException;
use Ashiqfardus\LaravelFuzzySearch\Exceptions\InvalidAlgorithmException;

// Catch all fuzzy search exceptions
try {
    $results = User::search($term)->get();
} catch (LaravelFuzzySearchException $e) {
    Log::error('Search failed', $e->toArray());
}

// Catch specific exceptions
try {
    $results = User::search('')->get();
} catch (EmptySearchTermException $e) {
    return response()->json(['error' => 'Please enter a search term']);
}

try {
    $results = User::search('test')->using('invalid')->get();
} catch (InvalidAlgorithmException $e) {
    return response()->json(['error' => $e->getMessage()]);
}
```

**Available Exceptions:**

- `LaravelFuzzySearchException` - Base exception (catch all)
- `EmptySearchTermException` - Search term is empty
- `InvalidAlgorithmException` - Invalid algorithm specified
- `InvalidConfigException` - Configuration error
- `SearchableColumnsNotFoundException` - No searchable columns found

---

Events
------

[](#events)

### `FuzzySearchExecuted`

[](#fuzzysearchexecuted)

Fired after every `->get()` or `->paginate()` call. Useful for monitoring search latency and volume in production.

```
use Ashiqfardus\LaravelFuzzySearch\Events\FuzzySearchExecuted;

Event::listen(FuzzySearchExecuted::class, function ($event) {
    Log::info('search', [
        'term'      => $event->searchTerm,
        'columns'   => $event->columns,
        'algorithm' => $event->algorithm,
        'count'     => $event->candidateCount,
        'ms'        => $event->latencyMs,
    ]);
});
```

Properties:

- `searchTerm` (string) — the user's query
- `columns` (array) — columns being searched
- `algorithm` (string) — algorithm used: `simple`, `fuzzy`, `levenshtein`, `soundex`, `metaphone`, `trigram`, `similar_text`, or `bm25`
- `candidateCount` (int) — rows fetched from SQL before scoring
- `latencyMs` (float) — total search time in milliseconds

---

Configuration
-------------

[](#configuration)

### Config File

[](#config-file)

```
// config/fuzzy-search.php

return [
    'default_algorithm' => 'fuzzy',

    'typo_tolerance' => [
        'enabled' => true,
        'max_distance' => 2,
        'min_word_length' => 4,  // No typo tolerance for short words
    ],

    'scoring' => [
        'exact_match' => 100,
        'prefix_match' => 50,
        'contains' => 25,
        'fuzzy_match' => 10,
    ],

    'stop_words' => [
        'en' => ['the', 'a', 'an', 'and', 'or', 'but', 'in', 'on', 'at'],
        'de' => ['der', 'die', 'das', 'und', 'oder', 'aber'],
    ],

    'synonyms' => [
        // Global synonyms
    ],

    'indexing' => [
        'enabled' => false,
        'async' => true,
        'queue' => 'default',
    ],

    'cache' => [
        'enabled' => false,
        'driver' => 'redis',
        'ttl' => 3600,
    ],

    'performance' => [
        'max_patterns' => 100,
        'chunk_size' => 1000,
    ],
];
```

### Config Presets

[](#config-presets)

Presets are **predefined search configurations** for common use cases. Instead of manually configuring multiple options every time, use a single preset name.

**Without preset (verbose):**

```
Post::search('laravel')
    ->searchIn(['title' => 10, 'body' => 5, 'excerpt' => 3])
    ->using('fuzzy')
    ->typoTolerance(2)
    ->ignoreStopWords('en')
    ->accentInsensitive()
    ->get();
```

**With preset (clean):**

```
Post::search('laravel')->preset('blog')->get();
```

#### Available Presets

[](#available-presets)

PresetBest ForAlgorithmTypo ToleranceFeatures`blog`Blog posts, articlesfuzzy2Stop words, accent-insensitive`ecommerce`Product searchfuzzy1Partial match, no stop words`users`User/contact searchlevenshtein2Accent-insensitive`phonetic`Name pronunciationsoundex0Phonetic matching`exact`SKUs, codes, IDssimple0Partial match only#### Preset Configuration Reference

[](#preset-configuration-reference)

```
// config/fuzzy-search.php
'presets' => [
    'blog' => [
        'columns' => ['title' => 10, 'body' => 5, 'excerpt' => 3],
        'algorithm' => 'fuzzy',
        'typo_tolerance' => 2,
        'stop_words_enabled' => true,
        'accent_insensitive' => true,
    ],

    'ecommerce' => [
        'columns' => ['name' => 10, 'description' => 5, 'sku' => 8, 'brand' => 6],
        'algorithm' => 'fuzzy',
        'typo_tolerance' => 1,
        'partial_match' => true,
        'stop_words_enabled' => false,
    ],

    'users' => [
        'columns' => ['name' => 10, 'email' => 8, 'username' => 9],
        'algorithm' => 'levenshtein',
        'typo_tolerance' => 2,
        'accent_insensitive' => true,
    ],

    'phonetic' => [
        'columns' => ['name' => 10],
        'algorithm' => 'soundex',
        'typo_tolerance' => 0,
    ],

    'exact' => [
        'algorithm' => 'simple',
        'typo_tolerance' => 0,
        'partial_match' => true,
    ],
],
```

#### Using Presets

[](#using-presets)

```
User::search('john')->preset('users')->get();
Post::search('laravel')->preset('blog')->get();
Product::search('laptop')->preset('ecommerce')->get();
Contact::search('steven')->preset('phonetic')->get();  // Finds "Stephen"
Product::search('SKU-12345')->preset('exact')->get();
```

#### Override Preset Settings

[](#override-preset-settings)

```
// Use blog preset but with higher typo tolerance
Post::search('laravel')
    ->preset('blog')
    ->typoTolerance(3)  // Override preset's default of 2
    ->get();
```

#### Create Custom Presets

[](#create-custom-presets)

```
// config/fuzzy-search.php
'presets' => [
    'documents' => [
        'columns' => ['title' => 10, 'content' => 8, 'tags' => 5],
        'algorithm' => 'trigram',
        'typo_tolerance' => 2,
        'stop_words_enabled' => true,
        'locale' => 'en',
    ],
],
```

```
Document::search('report')->preset('documents')->get();
```

### Per-Model Customization

[](#per-model-customization)

```
class Product extends Model
{
    use Searchable;

    protected array $searchable = [
        'columns' => [
            'title' => 10,
            'description' => 5,
            'sku' => 8,
        ],
        'algorithm' => 'fuzzy',
        'typo_tolerance' => 2,
        'stop_words' => ['the', 'a', 'an'],
        'synonyms' => [
            'laptop' => ['notebook', 'computer'],
        ],
        'accent_insensitive' => true,
    ];

    // Custom scoring logic
    public function getSearchScore($baseScore): float
    {
        return $this->is_featured ? $baseScore * 1.5 : $baseScore;
    }
}
```

---

CLI Tools
---------

[](#cli-tools)

### Indexing Commands

[](#indexing-commands)

```
# Build / rebuild BM25 index for a model
php artisan fuzzy-search:rebuild "App\Models\User"

# Rebuild with fresh index (flush first)
php artisan fuzzy-search:rebuild "App\Models\User" --fresh

# Rebuild asynchronously (for large tables)
php artisan fuzzy-search:rebuild "App\Models\User" --fresh --async --queue=indexing

# Flush index entries for a model
php artisan fuzzy-search:flush "App\Models\User"

# Clear BM25 index for a model
php artisan fuzzy-search:clear "App\Models\User"

# Clear BM25 index for all models
php artisan fuzzy-search:clear --all

# Show index status (row counts, avg doc length, term count per model)
php artisan fuzzy-search:status
```

### Benchmark &amp; Debug Commands

[](#benchmark--debug-commands)

```
# Benchmark search performance
php artisan fuzzy-search:benchmark "App\Models\User" --term="john" --iterations=100

# Explain a search query
php artisan fuzzy-search:explain User --term="john"
```

---

Performance &amp; Scaling
-------------------------

[](#performance--scaling)

### Algorithm Comparison

[](#algorithm-comparison)

AlgorithmSpeedTypo ToleranceBest ForDataset Size**simple**FastestNoneExact matches, SKUsAny size**fuzzy**Very FastHighGeneral purpose&lt; 100K rows**soundex**Very FastPhoneticName searches&lt; 100K rows**trigram**FastVery HighSimilarity matching&lt; 50K rows**levenshtein**MediumConfigurablePrecise typo matching&lt; 50K rows**BM25 index**Fast at scaleVia LIKE fallbackLarge tables, ranked results10K+ rows### Measured Latency (100k-row MySQL 8.0 dataset)

[](#measured-latency-100k-row-mysql-80-dataset)

Numbers measured on the [live demo](https://github.com/ashiqfardus/laravel-fuzzy-search-demo) (commodity VPS, warm cache). Run `php artisan demo:seed` in the demo project to seed the same dataset.

Search pathMedian latencyNotesLIKE (`using('simple')`)~8 msFull table scanLevenshtein (`using('levenshtein')`)~45 msPHP re-score over 1,000 SQL candidatesBM25 inverted index (`useInvertedIndex()`)~12 msThree parameterised SQL queries + PHP BM25 scoringExtended syntax (`->extended()`)~15 msIncludes AST compilation and multi-operator SQL generation**At scale:** The BM25 path uses an indexed term lookup — query time grows with the number of matching postings, not total row count. A well-maintained 1M-row index returns results in the same ~12–20 ms window as the 100k baseline.

### When to Use BM25 vs LIKE

[](#when-to-use-bm25-vs-like)

**Use BM25 (`useInvertedIndex()`) when:**

- Table has 10k+ rows
- Result ranking/relevance quality matters
- You have queue workers running
- Models use integer primary keys

**Use LIKE / fuzzy when:**

- Small tables (&lt; 10k rows) — LIKE can be faster due to BM25 scoring overhead
- UUID/ULID primary keys
- You need column weights to affect ranking

### `max_candidates` Tuning

[](#max_candidates-tuning)

For the LIKE/Levenshtein paths, SQL candidates are fetched then re-scored in PHP. The candidate set size is controlled by `max_candidates` (default: 1000). Lower this on large tables to reduce memory usage:

```
// config/fuzzy-search.php
'performance' => [
    'max_candidates' => 500,  // fetch fewer candidates on large tables
],
```

### Recommended Optimizations

[](#recommended-optimizations)

```
// For 100k+ rows: enable BM25 index + cache + limit columns
User::search('john')
    ->useInvertedIndex()
    ->cache(60)
    ->searchIn(['name', 'email'])
    ->maxPatterns(50)
    ->get();
```

Key tips:

1. **Use the BM25 index** for tables with 10k+ rows
2. **Enable caching** for repeated searches
3. **Limit columns** — only search relevant fields
4. **Use `simple` algorithm** when typo tolerance isn't needed
5. **Set `max_candidates`** to prevent excessive memory usage on large tables
6. **Use `take()`** to cap result sets
7. **Eager load relationships** to avoid N+1 queries

### Scaling Recommendations

[](#scaling-recommendations)

RecordsRecommended StrategyExpected Query Time&lt; 50KDefault (no optimization)&lt; 50ms50K - 100KAdd DB indexes + cache&lt; 100ms100K - 500KBM25 index + cache&lt; 150ms500K - 1MBM25 index + partitioning + cache&lt; 200ms1M - 10MBM25 + read replicas + tiered cache&lt; 300ms&gt; 10MConsider Meilisearch / Typesense—---

Algorithm × Database Compatibility
----------------------------------

[](#algorithm--database-compatibility)

This table shows what each algorithm does at the SQL level on each supported database. "Native" = the database's own function. "Pattern fallback" = PHP generates LIKE patterns.

AlgorithmMySQL 8MariaDB 10.6PostgreSQL 14SQLiteSQL Server**simple** / **like**`LIKE '%term%'``LIKE '%term%'``ILIKE '%term%'``LIKE '%term%'``LOWER() LIKE`**fuzzy**LIKE pattern set (typo patterns, transpositions)LIKE pattern setILIKE pattern setLIKE pattern setLIKE pattern set**levenshtein**Native `LEVENSHTEIN()` UDF if `use_native_functions=true`, else pattern setPattern set`similarity()` via pg\_trgm if `use_native_functions=true`, else pattern setPattern setPattern set**trigram**LIKE pattern setLIKE pattern setNative `similarity()` via pg\_trgm if `use_native_functions=true`LIKE pattern setLIKE pattern set**soundex**Native `SOUNDEX()` — always on, applied to first or last wordNative `SOUNDEX()` — always onNative `SOUNDEX()` via `fuzzystrmatch` if `use_native_functions=true`, else pattern fallbackPattern fallbackPattern fallback**metaphone**Shadow column `{col}_metaphone` + exact `=` matchShadow columnShadow columnShadow columnShadow column**similar\_text**`LIKE '%term%'` (SQL); `similar_text()` scores in PHP after fetchSame`ILIKE '%term%'`; PHP scoresSameSame### Notes

[](#notes)

- **`use_native_functions`** in `config/fuzzy-search.php` gates optional DB extensions. MySQL `SOUNDEX()` is built-in and always active — no flag needed. The flag is only relevant for: Levenshtein UDF (MySQL), pg\_trgm/fuzzystrmatch (PostgreSQL), unaccent (PostgreSQL).
- **Levenshtein UDF (MySQL):** Not installed by default. See [this gist](https://gist.github.com/yohgaki/9315991) or your DB package manager.
- **pg\_trgm (PostgreSQL):** `CREATE EXTENSION IF NOT EXISTS pg_trgm;`
- **fuzzystrmatch (PostgreSQL):** `CREATE EXTENSION IF NOT EXISTS fuzzystrmatch;`
- **unaccent (PostgreSQL, for `accentInsensitive()`):** `CREATE EXTENSION IF NOT EXISTS unaccent;` + `use_native_functions=true`
- **MySQL accent insensitive:** Use `utf8mb4_unicode_ci` or `utf8mb4_0900_ai_ci` collation on the column.
- **Metaphone shadow column:** Run `php artisan fuzzy-search:add-shadow-column {Model} {column} --type=metaphone` then `php artisan migrate`.

### PHP-Side Scoring

[](#php-side-scoring)

Regardless of algorithm, after SQL candidates are fetched:

1. `similar_text()` and `levenshtein()` run in PHP on each candidate.
2. Results are re-sorted by the combined PHP score (higher = better).
3. `limit/offset` is applied on the PHP-sorted collection (not in SQL).

Top-N results are always the most relevant N from the candidate set (not just the first N SQL rows). Candidate set size is controlled by `max_candidates` (default: 1000).

> **Pagination note:** `paginate()` and `simplePaginate()` use DB-level pagination and score within the current page only. For globally-ranked pagination across all pages, use the BM25 inverted index.

---

Testing
-------

[](#testing)

```
# Run tests
composer test

# Run with coverage
composer test-coverage

# Run benchmarks
composer benchmark
```

---

Requirements
------------

[](#requirements)

- PHP 8.1 or higher
- Laravel 9.x, 10.x, 11.x, 12.x, or 13.x
- Any supported database

---

License
-------

[](#license)

MIT License. See [LICENSE](LICENSE) for more information.

Credits
-------

[](#credits)

- [Md Asikul Islam](https://github.com/ashiqfardus)

Contributing
------------

[](#contributing)

Contributions are welcome! Please see [CONTRIBUTING](CONTRIBUTING.md) for details.

###  Health Score

48

—

FairBetter than 93% of packages

Maintenance88

Actively maintained with recent releases

Popularity28

Limited adoption so far

Community9

Small or concentrated contributor base

Maturity53

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 99.3% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~56 days

Total

3

Last Release

59d ago

Major Versions

v1.0.1 → v2.0.02026-05-05

PHP version history (2 changes)v1.0.0PHP ^8.0|^8.1|^8.2|^8.3|^8.4

v2.0.0PHP ^8.1|^8.2|^8.3|^8.4

### Community

Maintainers

![](https://www.gravatar.com/avatar/ce04a6996fce40ab5d66524ee273f295e7569874dec4b62dd86c528a500ade00?d=identicon)[ashiqfardus](/maintainers/ashiqfardus)

---

Top Contributors

[![ashiqfardus](https://avatars.githubusercontent.com/u/19163772?v=4)](https://github.com/ashiqfardus "ashiqfardus (135 commits)")[![oriceon](https://avatars.githubusercontent.com/u/358823?v=4)](https://github.com/oriceon "oriceon (1 commits)")

---

Tags

databaseeloquentfull-text-searchfuzzy-searchlaravellevenshteinmysqlphppostsearchsoundextypo-tolerancesearchlaraveleloquentquery builderbm25levenshteinfuzzysoundextrigramtypo-toleranceinverted-index

###  Code Quality

TestsPHPUnit

### Embed Badge

![Health badge](/badges/ashiqfardus-laravel-fuzzy-search/health.svg)

```
[![Health](https://phpackages.com/badges/ashiqfardus-laravel-fuzzy-search/health.svg)](https://phpackages.com/packages/ashiqfardus-laravel-fuzzy-search)
```

###  Alternatives

[spatie/laravel-medialibrary

Associate files with Eloquent models

6.1k43.2M630](/packages/spatie-laravel-medialibrary)[laravel/scout

Laravel Scout provides a driver based solution to searching your Eloquent models.

1.7k55.0M619](/packages/laravel-scout)[laravel/pulse

Laravel Pulse is a real-time application performance monitoring tool and dashboard for your Laravel application.

1.7k15.1M132](/packages/laravel-pulse)[laravel/ai

The official AI SDK for Laravel.

1.0k3.2M194](/packages/laravel-ai)[roots/acorn

Framework for Roots WordPress projects built with Laravel components.

9762.4M131](/packages/roots-acorn)[flarum/core

Delightfully simple forum software.

201.4M2.3k](/packages/flarum-core)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
