Download the PHP package ashiqfardus/laravel-fuzzy-search without Composer
On this page you can find all versions of the php package ashiqfardus/laravel-fuzzy-search. It is possible to download/install these versions without Composer. Possible dependencies are resolved automatically.
Download ashiqfardus/laravel-fuzzy-search
More information about ashiqfardus/laravel-fuzzy-search
Files in ashiqfardus/laravel-fuzzy-search
Package laravel-fuzzy-search
Short Description A powerful, zero-config fuzzy search package for Laravel with fluent API
License MIT
Informations about the package laravel-fuzzy-search
Laravel Fuzzy Search
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: Upgrade v1→v2
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 | 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 analytics |
Table of Contents
- Installation
- Quick Start
- Search Algorithms
- Field Weighting & Scoring
- Text Processing
- Result Presentation
- Performance & Indexing
- BM25 Inverted Index
- Extended Search Syntax
- Scout Driver
- Pagination
- Reliability & Safety
- Events
- Configuration
- CLI Tools
- Performance & Scaling
- Algorithm × Database Compatibility
- Testing
- Requirements
Installation
That's it! Zero configuration required. Start searching immediately.
Optionally publish the config file:
If you plan to use the BM25 inverted index (recommended for 10k+ rows), also run:
Upgrading from v1.x? There are breaking changes — result rankings and
_scorevalues may shift. Run the scanner to find affected code, then follow the full guide.→ Full upgrade guide
Quick Start
Zero-Config Search
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:
Fluent API
Eloquent & Query Builder Support
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 |
⚠️
metaphonerequires one-time setup. Unlike the other algorithms, it searches against a precomputed{column}_metaphoneshadow column. Callingusing('metaphone')without it throwsRuntimeException. Run the three commands shown in Shadow Columns once per searchable column.
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):
After this, the SearchableObserver keeps name_metaphone in sync automatically on every save() and update().
What gets generated:
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
Multi-Word Token Search
In-Memory Mode
Supported methods:
search,searchIn,take,skip,withRelevance,get. Any otherSearchBuildermethod (e.g.extended(),using(),preset(),paginate()) will throw a\BadMethodCallExceptionto prevent silent failures.
Field Weighting & Scoring
Weighted Columns
Relevance Scoring
Prefix Boosting
Partial Match Support
Custom Scoring Hooks
Recency Boost
Boost newer records in search results:
Search Suggestions / Autocomplete
Get autocomplete suggestions based on search term:
"Did You Mean" Spell Correction
Get alternative spellings when search has typos:
Multi-Model Federation Search
Search across multiple models simultaneously:
Search Analytics
Get detailed analytics about your search configuration:
Text Processing
Stop-Word Filtering
Synonym Support
Language / Locale Awareness
Unicode & Accent Insensitivity
Result Presentation
Highlighted Results
Debug / Explain-Score Mode
Performance & Indexing
Async Indexing (Queue Support)
Redis / Cache Support
BM25 Inverted Index
For large tables (10k+ rows), the BM25 inverted index provides ranked, fast results without scanning the full table.
How It Works
The indexing system has two parts:
Part 1 — One-time initial build. Run once after install (or after a schema change):
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:
Database Tables
| Table | Purpose |
|---|---|
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
Step 1 — Run migrations:
Step 2 — Enable indexing in config:
Step 3 — Declare searchable columns on your model:
Step 4 — Build the initial index:
Step 5 — Start a queue worker:
Supervisor config (/etc/supervisor/conf.d/fuzzy-search-worker.conf):
For Laravel Horizon (Redis):
Usage
Note:
useIndex()is an alias foruseInvertedIndex(). The deprecated legacysearch_indextable 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
BM25 Tuning
Stemming (Optional)
Default: no stemming (NullStemmer). With NullStemmer, running only matches running, not run or ran.
To enable Porter stemming:
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
Adding the Searchable trait automatically registers observers via bootSearchable():
SearchableIndexingObserver— listens tosavedanddeletedevents. Queues anIndexModelJobto update the BM25 index. This is a no-op whenindexing.enabledisfalse.SearchableObserver— listens tosavedevents 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
async = true (default) |
async = false |
|
|---|---|---|
| How it works | Dispatches IndexModelJob to queue |
Indexes in the same request, no queue |
| Request latency | Unaffected | +~10ms per save |
| Requires queue worker | Yes | No |
| Best for | Production apps | Tests, local dev, low-traffic apps |
For tests, set indexing.async = false so indexing happens synchronously:
Extended Search Syntax
Use Fuse.js-style operators inside your search string for precise control over matching.
Operators
| Token | Meaning | Example |
|---|---|---|
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 | [email protected]$ |
\| |
OR | john \| jane |
(whitespace) |
AND (implicit) | =John ^Doe |
( ... ) |
Grouping | admin (john \| jane) |
"phrase" |
Quoted single token | "hello world" |
Usage
Limits
| Limit | Default | Config key |
|---|---|---|
| Maximum tokens per query | 32 | query.max_tokens |
| Maximum nesting depth | 16 | query.max_depth |
Pagination with Extended Syntax
paginate() and cursorPaginate() are not compatible with extended() or searchBoolean() and will throw a BadMethodCallException. simplePaginate() works correctly.
Match Offsets & Blade Directive
Results with ->highlight() enabled include a _matches array:
For safe HTML rendering, use the @fuzzyHighlight Blade directive:
The directive automatically escapes user-supplied content and wraps matches in <mark> tags.
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
In .env:
Build the index:
Usage
Add both traits to your model:
Relevance Scores
Results include _score (BM25 relevance, higher = more relevant):
Authorization
Scout's default behavior bypasses Eloquent global scopes. Apply them explicitly:
How It Works
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
Stable Ranking
Pagination Methods
Note:
paginate()andcursorPaginate()are not compatible withextended()orsearchBoolean(). UsesimplePaginate()orget()with those.
Reliability & Safety
Fallback Search Strategy
Rate-Limit Friendliness
SQL Injection Safety
All queries use parameterized bindings. Search terms are automatically sanitized.
Exception Handling
Available Exceptions:
LaravelFuzzySearchException- Base exception (catch all)EmptySearchTermException- Search term is emptyInvalidAlgorithmException- Invalid algorithm specifiedInvalidConfigException- Configuration errorSearchableColumnsNotFoundException- No searchable columns found
Events
FuzzySearchExecuted
Fired after every ->get() or ->paginate() call. Useful for monitoring search latency and volume in production.
Properties:
searchTerm(string) — the user's querycolumns(array) — columns being searchedalgorithm(string) — algorithm used:simple,fuzzy,levenshtein,soundex,metaphone,trigram,similar_text, orbm25candidateCount(int) — rows fetched from SQL before scoringlatencyMs(float) — total search time in milliseconds
Configuration
Config File
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):
With preset (clean):
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
Using Presets
Override Preset Settings
Create Custom Presets
Per-Model Customization
CLI Tools
Indexing Commands
Benchmark & Debug Commands
Performance & Scaling
Algorithm Comparison
| Algorithm | Speed | Typo Tolerance | Best For | Dataset Size |
|---|---|---|---|---|
| simple | Fastest | None | Exact matches, SKUs | Any size |
| fuzzy | Very Fast | High | General purpose | < 100K rows |
| soundex | Very Fast | Phonetic | Name searches | < 100K rows |
| trigram | Fast | Very High | Similarity matching | < 50K rows |
| levenshtein | Medium | Configurable | Precise typo matching | < 50K rows |
| BM25 index | Fast at scale | Via LIKE fallback | Large tables, ranked results | 10K+ rows |
Measured Latency (100k-row MySQL 8.0 dataset)
Numbers measured on the live demo (commodity VPS, warm cache). Run php artisan demo:seed in the demo project to seed the same dataset.
| Search path | Median latency | Notes |
|---|---|---|
LIKE (using('simple')) |
~8 ms | Full table scan |
Levenshtein (using('levenshtein')) |
~45 ms | PHP re-score over 1,000 SQL candidates |
BM25 inverted index (useInvertedIndex()) |
~12 ms | Three parameterised SQL queries + PHP BM25 scoring |
Extended syntax (->extended()) |
~15 ms | Includes 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
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 (< 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
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:
Recommended Optimizations
Key tips:
- Use the BM25 index for tables with 10k+ rows
- Enable caching for repeated searches
- Limit columns — only search relevant fields
- Use
simplealgorithm when typo tolerance isn't needed - Set
max_candidatesto prevent excessive memory usage on large tables - Use
take()to cap result sets - Eager load relationships to avoid N+1 queries
Scaling Recommendations
| Records | Recommended Strategy | Expected Query Time |
|---|---|---|
| < 50K | Default (no optimization) | < 50ms |
| 50K - 100K | Add DB indexes + cache | < 100ms |
| 100K - 500K | BM25 index + cache | < 150ms |
| 500K - 1M | BM25 index + partitioning + cache | < 200ms |
| 1M - 10M | BM25 + read replicas + tiered cache | < 300ms |
| > 10M | Consider Meilisearch / Typesense | — |
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.
| Algorithm | MySQL 8 | MariaDB 10.6 | PostgreSQL 14 | SQLite | SQL Server |
|---|---|---|---|---|---|
| simple / like | LIKE '%term%' |
LIKE '%term%' |
ILIKE '%term%' |
LIKE '%term%' |
LOWER() LIKE |
| fuzzy | LIKE pattern set (typo patterns, transpositions) | LIKE pattern set | ILIKE pattern set | LIKE pattern set | LIKE pattern set |
| levenshtein | Native LEVENSHTEIN() UDF if use_native_functions=true, else pattern set |
Pattern set | similarity() via pg_trgm if use_native_functions=true, else pattern set |
Pattern set | Pattern set |
| trigram | LIKE pattern set | LIKE pattern set | Native similarity() via pg_trgm if use_native_functions=true |
LIKE pattern set | LIKE pattern set |
| soundex | Native SOUNDEX() — always on, applied to first or last word |
Native SOUNDEX() — always on |
Native SOUNDEX() via fuzzystrmatch if use_native_functions=true, else pattern fallback |
Pattern fallback | Pattern fallback |
| metaphone | Shadow column {col}_metaphone + exact = match |
Shadow column | Shadow column | Shadow column | Shadow column |
| similar_text | LIKE '%term%' (SQL); similar_text() scores in PHP after fetch |
Same | ILIKE '%term%'; PHP scores |
Same | Same |
Notes
use_native_functionsinconfig/fuzzy-search.phpgates optional DB extensions. MySQLSOUNDEX()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 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_ciorutf8mb4_0900_ai_cicollation on the column. - Metaphone shadow column: Run
php artisan fuzzy-search:add-shadow-column {Model} {column} --type=metaphonethenphp artisan migrate.
PHP-Side Scoring
Regardless of algorithm, after SQL candidates are fetched:
similar_text()andlevenshtein()run in PHP on each candidate.- Results are re-sorted by the combined PHP score (higher = better).
limit/offsetis 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()andsimplePaginate()use DB-level pagination and score within the current page only. For globally-ranked pagination across all pages, use the BM25 inverted index.
Testing
Requirements
- PHP 8.1 or higher
- Laravel 9.x, 10.x, 11.x, 12.x, or 13.x
- Any supported database
License
MIT License. See LICENSE for more information.
Credits
Contributing
Contributions are welcome! Please see CONTRIBUTING for details.
All versions of laravel-fuzzy-search with dependencies
illuminate/database Version ^9.0|^10.0|^11.0|^12.0|^13.0
illuminate/support Version ^9.0|^10.0|^11.0|^12.0|^13.0
illuminate/console Version ^9.0|^10.0|^11.0|^12.0|^13.0
illuminate/queue Version ^9.0|^10.0|^11.0|^12.0|^13.0
illuminate/bus Version ^9.0|^10.0|^11.0|^12.0|^13.0
symfony/finder Version ^6.0|^7.0