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

Matcher Laravel Package

loupe/matcher

Loupe Matcher turns search queries and arbitrary text into precise matches: tokenize phrases and negations, normalize locale-specific spelling variants, decompose compound words, calculate match spans, and format results with highlighting, cropping, and truncation.

View on GitHub
Deep Wiki
Context7

Technical Evaluation

Architecture fit

loupe/matcher is a pure-PHP, framework-agnostic presentation library: Tokenizer → Matcher → Formatter, with an immutable FormatterOptions value object. That is a clean fit for Laravel's service layer — no global state, no I/O, deterministic output, trivially unit-testable and queue-safe. It belongs in the application/presentation layer (search UX), not the domain layer.

0.4.x does not change that picture. 0.4.0 replaced the internal decompounder cache with a bounded, complete-token LRU memoization cache; 0.4.1 speeds up match highlighting on long texts. Both are internal-architecture work, not API work: no new public classes, no signature changes that we can see. Instance-scoped memoization is not shared/global state, so output remains deterministic for a given input and options, and queue-safety and testability are preserved. Two things to verify in the spike rather than assume: that the cache is genuinely per-instance (not static) and that its bound is configurable or sanely fixed, since it now lives for the lifetime of a long-running worker.

The one architectural claim worth re-testing after 0.4.1 is the cost model. The library tokenizes the full source text before cropping; 0.4.1 explicitly targets "long texts", which is the shape of our worst-case documents. That does not change the O(document) characteristic, but it is the first release in the 0.4 line whose benefit is not gated on the decompounder, so it is materially more likely to apply to our English-only corpus than 0.4.0 was. Treat it as promising and unquantified — there is no published benchmark for 0.4.1.

The critical fit test is what our search backend already gives us. This package is only additive if we are generating snippets ourselves:

Search backend Fit
Laravel Scout database driver, LIKE queries, TNTSearch Strong — we currently have nothing better than regex/str_ireplace
Loupe Strong, and likely the intended pairing (same maintainers)
Meilisearch (_formatted), Algolia (_highlightResult), Elasticsearch (highlighter) Weak — the engine already returns highlighted/cropped fields; this duplicates CPU work and risks inconsistent markup

The second genuine fit is consistency: if we expose more than one backend, or need to crop stored fields that the engine does not return, this gives one snippet format everywhere.

Integration feasibility

  • composer require loupe/matcher; no Laravel service provider is shipped, so we write a ~30-line provider binding a singleton.
  • Hard dependency on ext-intl (ICU word boundaries + transliterator for diacritic folding). Present in Laravel Sail/Forge/Herd by default; not present in bare php:8.x-alpine images — this must be added to our Dockerfile or Composer install fails.
  • No published PHP/Laravel support matrix. We must verify against our own PHP matrix in CI before committing.
  • Output is a string containing raw HTML tags. We own the XSS boundary. This is the single most important engineering decision in the integration.
  • Estimated effort: 1–2 day spike on production-shaped content, plus 2–3 days for wrapper, config, escaping strategy, and tests. Run the spike against 0.4.1, not 0.2.x (the line the previous assessment assumed) and not 0.4.0 (superseded by a highlighting-path change).

Technical risk

  • Pre-1.0, explicitly WIP, and demonstrably churny below the public API surface. The README carries a [!CAUTION] about API churn, and the release history confirms it: 0.2.0 changed stopword/token separation, 0.2.1 changed normalization, 0.2.2 changed span semantics, 0.2.3–0.2.4 changed folding state and added truncation, 0.4.0 re-architected the cache. 0.4.1 now reworks the highlighting path — the exact code that decides which spans get marked. Behavioral changes land in minor releases, and internal rewrites land in minor and patch releases without a migration note.
  • 0.4.1 is a patch release that touches output-producing code. "Speed up match highlighting" may be a pure constant-factor change, or it may alter tie-breaking between overlapping matches, merge adjacent spans, or change behavior on pathological inputs. We cannot tell from the changelog. This is precisely why golden-file parity tests must be an upgrade gate, not an afterthought, and why the pin should be 0.4.* with a lockfile rather than a float.
  • We are three patch/minor versions behind our own assessment. The prior review pinned 0.2.*; the current release is 0.4.1. Any version discipline we write must start from 0.4.x, and we should read the 0.3.x changelog before the spike — we have never reviewed those changes.
  • Vendor benchmark is not our benchmark. The 59.2% / 2.45× / −14.5 MB figures come from a maintainer-run bench-movies fixture and specifically exercise the decompounder, which only engages for compound-word languages (Germanic/Scandinavian). On an English-only corpus we should expect a materially smaller gain. The 0.4.1 release notes publish no numbers at all. Treat both as directional, not as capacity-planning inputs.
  • Weak adoption signal: ~10 stars, 0 dependents. No third-party production battle-testing, no community-validated edge cases. Active PRs from the maintainer are a mild positive on maintenance intent, not on hardening.
  • Low bus factor (two primary contributors), partially offset by MIT licensing and a very small code surface — forking to an internal namespace remains genuinely cheap, and neither 0.4.0 nor 0.4.1 enlarges that surface.
  • XSS / escaping: unchanged. If source text contains <, &, or HTML, output must not be rendered with {!! !!} naively.
  • Encoding: behavior on malformed UTF-8 or legacy latin1 columns remains unverified.
  • Normalization mismatch: display-side diacritic folding may highlight a term that does not literally match what the user typed, or may differ from the search engine's folding.

Key questions

  1. Does our search backend already return formatted snippets? If yes, is there a demonstrable gap?
  2. Is the corpus plain text or rich text/HTML? Who owns escaping?
  3. Which locales must we support, and is the folding behavior acceptable to the business (highlight ≠ typed characters)?
  4. Do we need "phrase" and -negation semantics to agree with the engine's, or is approximate highlighting fine for display?
  5. Is highlighted text ever user-supplied and shown to other users (stored XSS surface)?
  6. What is our per-request snippet budget and result-count ceiling?
  7. Are we willing to pin and treat upgrades as code changes — including patch bumps?
  8. Do we enable decompounding at all, and for which locales? If not, the 0.4.0 win is largely irrelevant and should not justify adoption on its own — but see 11.
  9. Is the LRU cache per-instance and bounded? What is its steady-state memory footprint under Octane or a long-lived queue worker processing thousands of distinct tokens?
  10. What changed in 0.3.x, 0.4.0 and 0.4.1 that affects output — do 0.2.x-era span/folding expectations still hold? Re-baseline golden files.
  11. (new in 0.4.1) How much of the long-text highlighting speedup do we actually see? Ask the maintainer whether 0.4.1 changed span selection/merging semantics or only the matching loop — and get a before/after number on a 5–20 KB document rather than a 200-char one.

Integration Approach

Stack fit

PHP 8.x + ext-intl, zero Laravel coupling, returns a plain string — works unchanged in Blade, Livewire, Inertia, or an API resource. Register as a singleton; make all tunables config-driven so marketing/UX can adjust snippet length without a deploy.

If we run under Octane or persistent workers, note that the singleton now retains an internal LRU cache for the process lifetime. That is by design and bounded, but it is new state we did not have in 0.2.x — measure it before rollout rather than after.

Recommended wiring

  • Define an application-owned interface, e.g. App\Support\Search\SnippetFormatter, implemented by LoupeSnippetFormatter. Nothing outside this class may use Loupe\Matcher\*. This is the anti-corruption layer that makes the pre-1.0 churn a one-file problem — and 0.3 → 0.4.1 are exactly the kind of churn it exists for.
  • Config: config/search.php with crop_length, highlight_start/end, crop_marker, truncation settings, per-locale stopwords, decompounder enable/disable per locale, and a default locale derived from app()->getLocale().
  • Optional Blade directive / helper (@snippet($text, $query)) for template ergonomics, but keep controllers and API resources calling the interface.

Escaping pipeline (do this first, not last)

  1. strip_tags() the source field (or confirm it is plain text).
  2. Normalize encoding to UTF-8.
  3. Format with sentinel highlight tags (%%HL%% / %%/HL%%) rather than <mark>.
  4. e() the resulting string.
  5. Replace sentinels with the real <mark>/<em> tags.

FormatterOptions and the standalone Cropper both accept arbitrary start/end tags, so this costs nothing and removes the XSS class of failure entirely. It also means any 0.4.1 change to span selection surfaces as differently-placed sentinels in a golden file rather than as a rendering surprise in production.

Migration path

Purely additive: no schema change, no data migration, no backfill. Replace ad-hoc regex highlighting one surface at a time.

Compatibility

Pin loupe/matcher:0.4.* with a committed lockfile, and treat even patch bumps as code changes requiring a green golden-file run. Do not carry forward the 0.2.x pin from the prior review — it is stale and would silently exclude the current line. Because everything sits behind our interface, swapping to a fork, to Loupe's native highlighting, or to Meilisearch's _formatted response is a single-class change with no call-site edits.

Sequencing

  1. Re-baseline (0.5 day) — read the 0.3.x, 0.4.0 and 0.4.1 diffs/changelogs, confirm no span/folding/output changes that invalidate prior assumptions, and record the decompounder-cache scope (per-instance, bounded). Capture golden files on 0.4.1 specifically; do not carry 0.4.0-era fixtures forward.
  2. Spike (1–2 days) — run 200 real production queries against real documents on 0.4.1, plus a handful of deliberately long (5–20 KB) documents to exercise the path 0.4.1 changed. Measure latency per snippet, peak memory under a long-lived process, and eyeball snippet quality with product/UX. Kill criteria: quality worse than current, or p95 snippet cost exceeds budget. Do not accept the vendor benchmark as the capacity estimate.
  3. Wrapper + config + escaping + tests (2–3 days) — golden-file tests in every locale we ship, captured on 0.4.1.
  4. One surface — internal admin search or a low-traffic listing page.
  5. Measure — click-through on results, support tickets about "why is this highlighted".
  6. Broaden, then revisit at 1.0 or when the search backend changes.

Sequencing caveat: if a Scout → Meilisearch/Algolia migration is on the roadmap within two quarters, defer this. Those engines ship this feature and we would build it twice. Second caveat: if the spike shows 0.4.1's long-text win is negligible on our corpus, nothing else about the case changes — we adopt on output quality and consistency or not at all.

Operational Impact

Maintenance

Treat minor and patch bumps as potentially breaking until 1.0. 0.4.1 is a two-day-later patch release that touches the highlighting path — the least reviewable kind of change — which reinforces rather than relaxes the discipline. Budget ~1–2 dev-days per quarter for upgrade + regression pass, and require a golden-file diff review on every bump. Code surface is small enough that a fork is a realistic fallback if maintenance stalls. Positives: the project is clearly under active development with maintainer-led PRs, and two consecutive releases are performance work, which shows someone is profiling real workloads. Negatives: release metadata is inconsistent (0.2.4 appears future-dated, cadence is bursty — nothing for a while, then 0.4.0 → 0.4.1 in two days) and the version line has moved three releases since our last review. Do not assume a predictable support heartbeat, and do not assume our prior review covers 0.3.x, 0.4.0, or 0.4.1.

Support

Community-only: GitHub issues, no SLA, no security policy, ~10 stars. We own production support. Required internal artifacts: one-page doc, a runbook entry for "snippets look wrong", a pinned version + lockfile policy, and golden-file tests on real content in each supported locale. Without those, every upgrade is a blind deploy — and 0.4.0/0.4.1 together show upgrades can change cache internals and the highlighting path even when the changelog reads as "performance only".

Scaling

Stateless, CPU-only, no I/O — horizontal scaling is a non-issue. The cost driver is results per page × text length, and note that tokenization runs over the full source text before cropping, so cost is O(document), not O(snippet). 0.4.1 attacks exactly this for long documents and, unlike 0.4.0, is not gated on the decompounder — so it is the more relevant release for our corpus, though still a constant-factor improvement and still unmeasured by us. The O(document) characteristic is unchanged. Mitigations, in order of value:

  • Bound the input: pass only the first N KB of long documents.
  • Take 0.4.1 (or later 0.4.x) rather than 0.4.0 for long-text surfaces — but confirm output parity first.
  • Enable the decompounder only for locales that need it — this is where the 0.4.0 win lives.
  • Cache formatted snippets in Redis, keyed by doc_id + normalized_query_hash + options_hash (the package already accepts pre-calculated matches, so cache the match set, not just the string).
  • Precompute snippets on write for hot/expensive content.
  • Push bulk or export rendering to queues.

Realistically a few ms per snippet for typical 200–500 char fields — fine at 10–20 results/page, but measure rather than assume. Expect first-call cost per unique token to be higher than steady state (cold LRU), which matters for spiky traffic and for workers that churn through many distinct documents.

Failure modes

Failure Symptom Mitigation
ext-intl missing in image Composer/install or runtime fatal Add to Dockerfile; add a health check
Malformed UTF-8 / legacy latin1 Exception or garbled crop Normalize/validate before formatting; identify offending columns
HTML in source text XSS if rendered unescaped Sentinel-tag + e() pipeline
No match (empty or stopword-only query) No highlight; crop falls back to document head Define UX explicitly; show leading excerpt + "no exact match"
Source is one giant token (URL, base64, minified JS) Crop cannot find a word boundary Pre-strip such fields or cap token length
Locale mismatch Diacritics not matched, silent under-highlighting Per-locale config + tests
Decompounder enabled for a non-compound locale Wasted CPU, surprising highlight spans Enable per locale; validate config at boot
Long-lived worker retains LRU across many documents Slow memory growth vs. 0.2.x baseline Confirm cache is bounded; monitor RSS under Octane/queues
Patch upgrade changes span/folding/cache behavior (0.4.1 risk) Highlight regressions after composer update Version pin + lockfile + golden-file diff gate in CI on every bump, patches included
Unbalanced configured tags Broken page layout Validate config at boot, not at render

Ramp-up

Low and unchanged. README is above average for a 10-star package and the public surface is roughly five classes — a mid-level Laravel dev is productive in half a day, fully confident in a week. No Laravel-specific docs exist, so we write our own examples. On-call impact is near zero (no external calls, no jobs of its own) provided the escaping pipeline and input bounds are in place before the first user-facing surface ships, and provided we are disciplined about the pin now that the version line has moved and patches are shipping with behavioral surface area.

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.
mehedimi/laravel-amp-mail
happones/laravel-vue-i18n-generator
jwerd/inertia-table
siberfx/flysystem-wasabi
hollyit/token-replace
ippanelcom/laravel-sdk
adachsoft/changelog-linter
adachsoft/collection
payerurl/telegrambot-crypto-payment-instant-payout
dskripchenko/laravel-hybrid-search
riorizkyrainey/lara-ced
mehedimi/static-vel
zharp/zharp
nonsapiens/bigquery-model-sync
lgu-bayambang-ict-office/lgu-laravel-development-standards
andydefer/laravel-support
se7enxweb/flysystem
lampwick/laravel
jeslage/statamic-emojis
micahdshackelford/rmap-laravel