Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Fuse Laravel Package

loilo/fuse

Fuse.php brings Fuse.js-style fuzzy search to PHP and Laravel. Add fast, typo-tolerant full-text search to arrays, collections, and Eloquent models, complete with weighted keys, relevance scoring, and bitap matching—no external search engine or server required.

Deep Wiki
Context7

Getting Started

loilo/fuse is a pure-PHP port of Fuse.js — a fuzzy (not full-text) matcher that runs entirely in-process. There is no Laravel package, service provider, or Scout driver; you composer require loilo/fuse (PHP 8.0+, ext-mbstring) and use the Fuse\Fuse class directly. No index to build, no daemon, no queue worker — which is why it fits small, request-scoped search problems rather than being a search backend.

Start with the simplest thing that works, then add options:

use Fuse\Fuse;

$fuse = new Fuse(['Apple', 'Orange', 'Banana'], ['threshold' => 0.4]);
$results = $fuse->search('appl');

Then move to the shape you'll actually use in an app — a list of associative arrays with explicit searchable keys:

$fuse = new Fuse($rows, [
    'keys'          => ['name', 'email', 'company.name'],
    'threshold'     => 0.35,
    'includeScore'  => true,
    'includeMatches'=> true,
    'ignoreLocation'=> true,   // critical for long free-text fields
]);

$results = $fuse->search('jane doe');

Results are returned as objects/arrays carrying item, score, and (optionally) matches. Where to look first: the README's options table and the Fuse\Fuse class itself — the surface area is small, and the port is close enough to Fuse.js that the upstream JS docs are the real reference.

The canonical first use case is a "type-ahead" search box over a table that's already loaded — a customer picker, a product lookup, an admin filter — where LIKE '%term%' feels brittle because of typos and word order.

Implementation Patterns

Bind a pre-built instance, not a per-call instantiation. Building a Fuse object tokenizes every record; doing it on each request is the most common source of latency. Register it as a singleton for static datasets, or cache the collection (not the Fuse object — it's not reliably serializable) and construct per request for dynamic ones.

Prefilter in SQL, then fuzzy-rank in PHP. Don't Model::all() into Fuse. Narrow with a cheap where (tenant, status, category), cap at a few hundred to a few thousand rows, then let Fuse do the ranking. This is the pattern that keeps it viable inside a normal controller.

Wrap it in a small service. A FuzzyMatcher class or a Searchable trait that takes an Eloquent collection, maps to arrays (toArray() with only the needed keys), builds Fuse, and returns typed results gives you one place to change options and one place to test. Returning the raw Fuse result objects to views leaks a third-party shape.

Autocomplete endpoint. Slice in PHP — array_slice($results, 0, 10) — since Fuse returns everything above threshold. Pair with a minMatchCharLength (2–3) and a client-side debounce; a 1-character query is both the slowest and the least useful.

Weighted multi-field search. keys accepts per-key objects (['name' => 'title', 'weight' => 0.7]) — use that to make titles outrank descriptions rather than relying on threshold tuning alone.

Use setCollection() for repeated queries in one request. If you need to re-query the same dataset with several terms (e.g., matching against several input values), reuse the instance instead of rebuilding.

Extended search for power users. 'useExtendedSearch' => true enables prefix (^term), exact (=term), and exclusion (!term) operators. Good for an "advanced filter" UI; bad as a hidden default, since users typing a literal ^ will get surprising behavior.

Highlighting. includeMatches => true gives you per-key index ranges so the frontend can wrap matched substrings in <mark>. Cheaper and more accurate than regexing the query back into the string, especially once fuzziness is involved.

Test with includeScore on. Scores are deterministic, so you can assert ordering rather than brittle exact values. Snapshot a few representative queries and assert "the right row is in the top 3."

Gotchas and Tips

threshold is inverted from intuition. 0 means perfect match only, 1 means match everything; the default (0.6) is looser than most people expect. Start at 0.3–0.4 for user-facing search and tune against real query logs.

location/distance will silently bite you. By default Fuse only scores matches near the start of the string and within 100 characters. For long fields (descriptions, notes), set ignoreLocation => true or you'll get inexplicably empty results on records the data clearly contains.

Case sensitivity is an option name trap. The option is isCaseSensitive in newer releases (older docs and JS lore say caseSensitive); passing the wrong one is silently ignored rather than erroring. Same for the option-array style — unknown keys don't warn.

Return type depends on version. Between major versions the result shape changed (plain nested arrays vs. Result objects with accessors). Pin the version in composer.json and write your wrapper against one shape; don't let views read $result['item'] in some places and $result->item in others.

No incremental indexing. setCollection() replaces the whole collection. There's no "add one row." For anything that changes frequently, rebuild-and-swap from cache rather than trying to keep a long-lived instance in sync.

It does not scale like a search engine. Matching is roughly linear in documents × terms with tokenization on top. Tens of thousands of short strings is fine on a warm request; hundreds of thousands is not — that's when you move to Meilisearch, Typesense, or TNTSearch. Be explicit about this boundary in your own code comments so nobody quietly routes a 500k-row table through it.

Unicode normalization is on you. mbstring is used, but there's no accent folding or transliteration — café won't match cafe. Normalize both the indexed values and the query through iconv/Transliterator before handing them over, consistently on both sides.

Nested keys use dot notation ('company.name') and expect the underlying array to actually be nested; if your data comes from DTOs or mixed shapes, normalize to plain arrays first or override extraction rather than fighting the default getter.

findAllMatches and shouldSort => false are the two performance levers. The first is more thorough but much slower on large documents — use it only for the visible top-N if you must. The second is worth setting when you plan to re-rank results yourself.

Empty query returns an empty set, not the full collection. If your UI expects "show everything until they type," handle that branch yourself.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
elsayed85/laravel-api-exceptions
ouhssini/laravel-demo-mode
daworks/ncloud-outbound-mailer-driver
gp247/cms
ignibyte/aic-gates
nexa-merchant/blog
novasa/laravel-language-center
opencage/geocode
adolfoholzer/parameters
livenetworks/ln-starter
shitoudev/permissionmanager
johannesclimacus/laravel-tui
hocvt/seotools
vitalybaev/sypexgeo
amphibee/themosis-ignition
gito-me/fast-excel
frankyso/laravel-ipaymu
geolize-studio/laravel-chile
boomerangui/chat
cgv/localized-resource