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
| 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 |
composer require ashiqfardus/laravel-fuzzy-searchThat's it! Zero configuration required. Start searching immediately.
Optionally publish the config file:
php artisan vendor:publish --tag=fuzzy-search-config// 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.
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();$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
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();| 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// 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();// 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();User::search('john')
->searchIn([
'name' => 10, // Highest priority
'username' => 8,
'email' => 5,
'bio' => 1, // Lowest priority
])
->get();$users = User::search('john')
->withRelevance()
->get();
foreach ($users as $user) {
echo "{$user->name}: {$user->_score}";
}// Boost results that start with the search term
User::search('john')
->prefixBoost(2.5) // 2.5x score for prefix matches
->get();User::search('joh')
->partialMatch() // Matches "john", "johnny", "johanna"
->minMatchLength(2) // Minimum 2 characters
->get();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();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();Get autocomplete suggestions based on search term:
$suggestions = User::search('joh')
->searchIn(['name', 'email'])
->suggest(5);
// Returns: ['John', 'Johnny', 'Johanna', ...]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],
// ]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]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,
// ...
// ]// 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();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();User::search('john')
->locale('en') // English
->get();
User::search('mĂĽnchen')
->locale('de') // German - handles umlauts
->get();// Matches "café", "cafe", "Café"
User::search('cafe')
->accentInsensitive()
->get();
// Matches "naĂŻve", "naive"
User::search('naive')
->unicodeNormalize()
->get();$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();$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',
// ]
}# 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();// In config/fuzzy-search.php
'indexing' => [
'async' => true,
'queue' => 'search-indexing',
'chunk_size' => 500,
],
// Dispatch indexing job
User::reindex(); // Queued automatically// 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,
],// Results maintain consistent order across pages
$page1 = User::search('john')->stableRanking()->paginate(10, page: 1);
$page2 = User::search('john')->stableRanking()->paginate(10, page: 2);// 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();// Automatically falls back to simpler algorithm if primary fails
User::search('john')
->using('trigram')
->fallback('fuzzy') // First fallback
->fallback('simple') // Second fallback
->get();| 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 |
// 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();All queries use parameterized bindings. Search terms are automatically sanitized.
// Safe - input is sanitized
User::search("'; DROP TABLE users; --")->get();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
// 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,
],
];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();| 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 |
// 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,
],
],// 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();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();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();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;
}
}# 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 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# Explain a search query
php artisan fuzzy-search:explain User --term="john"
# Output query analysis, patterns generated, scoring breakdown# Run tests
composer test
# Run with coverage
composer test-coverage
# Run benchmarks
composer benchmark- 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();- PHP 8.0 or higher
- Laravel 9.x, 10.x, 11.x, or 12.x
- Any supported database
MIT License. See LICENSE for more information.
Contributions are welcome! Please see CONTRIBUTING for details.