ashiqfardus / laravel-fuzzy-search
A powerful, zero-config fuzzy search package for Laravel with fluent API
Package info
github.com/ashiqfardus/laravel-fuzzy-search
pkg:composer/ashiqfardus/laravel-fuzzy-search
Requires
- php: ^8.0|^8.1|^8.2|^8.3|^8.4
- illuminate/database: ^9.0|^10.0|^11.0|^12.0
- illuminate/support: ^9.0|^10.0|^11.0|^12.0
Requires (Dev)
- orchestra/testbench: ^7.0|^8.0|^9.0
- phpunit/phpunit: ^9.0|^10.0|^11.0
README
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 - See the package in action!
📚 Documentation: Getting Started • Performance Guide • Comparisons
✨ Features
| Category | Features |
|---|---|
| Core | Zero-config search • Fluent API • Eloquent & 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 | Search index table • 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 analytics |
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
Quick Start
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
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 (overrides $searchable) $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
$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 & 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
Available Algorithms
| Algorithm | Best For | Typo Tolerance | Speed |
|---|---|---|---|
fuzzy |
General purpose | ✅ High | Fast |
levenshtein |
Strict typo matching | ✅ Configurable | Medium |
soundex |
Phonetic matching (English names) | ✅ Phonetic | Fast |
metaphone |
Phonetic matching (more accurate) | ✅ Phonetic | Fast |
trigram |
Similarity matching | ✅ High | Medium |
similar_text |
Percentage similarity | ✅ Medium | Medium |
simple / like |
Exact substring (LIKE) | ❌ None | Fastest |
// 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 User::search('laptop')->using('similar_text')->get(); // Percentage match
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
// 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();
Field Weighting & Scoring
Weighted Columns
User::search('john') ->searchIn([ 'name' => 10, // Highest priority 'username' => 8, 'email' => 5, 'bio' => 1, // Lowest priority ]) ->get();
Relevance Scoring
$users = User::search('john') ->withRelevance() ->get(); foreach ($users as $user) { echo "{$user->name}: {$user->_score}"; }
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
User::search('joh') ->partialMatch() // Matches "john", "johnny", "johanna" ->minMatchLength(2) // Minimum 2 characters ->get();
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
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
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
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
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
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
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
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
User::search('john') ->locale('en') // English ->get(); User::search('münchen') ->locale('de') // German - handles umlauts ->get();
Unicode & Accent Insensitivity
// Matches "café", "cafe", "Café" User::search('cafe') ->accentInsensitive() ->get(); // Matches "naïve", "naive" User::search('naive') ->unicodeNormalize() ->get();
Result Presentation
Highlighted Results
$users = User::search('john') ->highlight('em') // Wrap matches in <em> tags ->get(); foreach ($users as $user) { echo $user->_highlighted['name']; // "Hello <em>John</em> Doe" } // Custom highlight $users = User::search('john') ->highlight('<mark class="highlight">', '</mark>') ->get();
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 & Indexing
Search Index Table
# Create search index php artisan fuzzy-search:index User # Rebuild index php artisan fuzzy-search:index User --fresh # Index specific columns php artisan fuzzy-search:index User --columns=name,email
// Use indexed search (faster for large tables) User::search('john') ->useIndex() ->get();
Async Indexing (Queue Support)
// In config/fuzzy-search.php 'indexing' => [ 'async' => true, 'queue' => 'search-indexing', 'chunk_size' => 500, ], // Dispatch indexing job User::reindex(); // Queued automatically
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, ],
Pagination
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
// 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();
Reliability & Safety
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();
Database Compatibility
| Database | Full Support | Native Functions |
|---|---|---|
| MySQL 5.7+ | ✅ | SOUNDEX |
| PostgreSQL 9.6+ | ✅ | pg_trgm, fuzzystrmatch |
| SQLite 3.x | ✅ | Pattern-based |
| SQL Server 2016+ | ✅ | Pattern-based |
| MariaDB 10.2+ | ✅ | SOUNDEX |
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
All queries use parameterized bindings. Search terms are automatically sanitized.
// Safe - input is sanitized User::search("'; DROP TABLE users; --")->get();
Exception Handling
The package provides custom exceptions for better error 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) { // Handle any fuzzy search error Log::error('Search failed', $e->toArray()); } // Catch specific exceptions try { $results = User::search('')->get(); // Empty search } 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 emptyInvalidAlgorithmException- Invalid algorithm specifiedInvalidConfigException- Configuration errorSearchableColumnsNotFoundException- No searchable columns found
Configuration
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, 'table' => 'search_index', 'async' => true, 'queue' => 'default', ], 'cache' => [ 'enabled' => false, 'driver' => 'redis', 'ttl' => 3600, ], 'performance' => [ 'max_patterns' => 100, 'chunk_size' => 1000, ], ];
Config Presets
Presets are predefined search configurations for common use cases. Instead of manually configuring multiple options every time, use a single preset name.
Why Use Presets?
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
| Preset | Best For | Algorithm | Typo Tolerance | Features |
|---|---|---|---|---|
blog |
Blog posts, articles | fuzzy | 2 | Stop words, accent-insensitive |
ecommerce |
Product search | fuzzy | 1 | Partial match, no stop words |
users |
User/contact search | levenshtein | 2 | Accent-insensitive |
phonetic |
Name pronunciation | soundex | 0 | Phonetic matching |
exact |
SKUs, codes, IDs | simple | 0 | Partial match only |
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
// Use a preset User::search('john')->preset('users')->get(); Post::search('laravel')->preset('blog')->get(); Product::search('laptop')->preset('ecommerce')->get(); // Phonetic search for names Contact::search('steven')->preset('phonetic')->get(); // Finds "Stephen" // Exact search for SKUs Product::search('SKU-12345')->preset('exact')->get();
Override Preset Settings
Presets can be combined with other methods - later calls override preset values:
// Use blog preset but with higher typo tolerance Post::search('laravel') ->preset('blog') ->typoTolerance(3) // Override preset's default of 2 ->get(); // Use ecommerce preset but search different columns Product::search('laptop') ->preset('ecommerce') ->searchIn(['name' => 10, 'category' => 5]) // Override columns ->get();
Create Custom Presets
Add your own presets in config/fuzzy-search.php:
'presets' => [ // ... existing presets ... 'documents' => [ 'columns' => ['title' => 10, 'content' => 8, 'tags' => 5], 'algorithm' => 'trigram', 'typo_tolerance' => 2, 'stop_words_enabled' => true, 'locale' => 'en', ], 'multilingual' => [ 'columns' => ['title' => 10, 'body' => 5], 'algorithm' => 'fuzzy', 'accent_insensitive' => true, 'unicode_normalize' => true, ], ],
Then use your custom preset:
Document::search('report')->preset('documents')->get();
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 { // Boost featured products return $this->is_featured ? $baseScore * 1.5 : $baseScore; } }
CLI Tools
Indexing Commands
# Index a model php artisan fuzzy-search:index User # Index with fresh rebuild php artisan fuzzy-search:index User --fresh # Index all searchable models php artisan fuzzy-search:index --all # Clear index php artisan fuzzy-search:clear User
Benchmark Tools
# Benchmark search performance php artisan fuzzy-search:benchmark User --term="john" --iterations=100 # Output: # Algorithm: fuzzy # Average time: 12.5ms # Min: 8ms, Max: 25ms # Queries/second: 80 # Memory usage: 2.1MB
Debug Commands
# Explain a search query php artisan fuzzy-search:explain User --term="john" # Output query analysis, patterns generated, scoring breakdown
Testing
# Run tests composer test # Run with coverage composer test-coverage # Run benchmarks composer benchmark
Performance Tips
- Use Indexing for tables with 10k+ rows
- Enable Caching for repeated searches
- Limit Columns - only search relevant fields
- Use Simple Algorithm when typo tolerance isn't needed
- Set Max Patterns to prevent query explosion
User::search('john') ->useIndex() ->cache(60) ->searchIn(['name', 'email']) // Not all columns ->maxPatterns(50) ->get();
Requirements
- PHP 8.0 or higher
- Laravel 9.x, 10.x, 11.x, or 12.x
- Any supported database
License
MIT License. See LICENSE for more information.
Credits
Contributing
Contributions are welcome! Please see CONTRIBUTING for details.