mezcalito/ux-search
mezcalito/ux-search adds a polished search experience to Laravel apps, with model indexing, customizable filters, and reusable UI components for search inputs and result lists. It helps you ship fast, relevant, typo-tolerant results without building search plumbing from scratch.
Reality check before you composer require. mezcalito/ux-search is a Symfony bundle, not a Laravel package. It has no service provider, no Blade views, no Eloquent adapter. Its UI layer is built on Symfony UX Live Components (the Symfony analogue of Livewire), and its data layer ships as separate adapter packages (Doctrine ORM, and an engine-backed adapter) — Doctrine, not Eloquent. So there are only two realistic ways a Laravel developer "uses" this:
SearchType that owns the facet definitions, an adapter interface behind it, and a Livewire component that renders results + facets from URL state. That port is a day of work and is usually the better call at 66 stars / 0 dependents.For path 1, the minimal sequence:
composer require mezcalito/ux-search
composer require mezcalito/ux-search-doctrine-orm-adapter # or the engine adapter you need
Then: register the bundle in config/bundles.php, publish/author the config under config/packages/ (engine selection, index/search name, pagination defaults), define a search type that declares your facets, mark the entity fields that are searchable/facetable, and drop the search Live Component into a Twig template.
Where to look first: the adapter package's README, not the core bundle's. The core is mostly interfaces plus the Live Component; the adapter tells you which attribute or config key actually maps a property to a facet. After that, read the tests/ fixtures — in small Symfony bundles the fixture app is the de facto documentation, and it will show you a complete SearchType with a choice facet and a range facet wired end to end.
First use case to build: a product listing with (a) a free-text query, (b) a category choice facet with counts, (c) a price range facet, (d) pagination and a sort selector. It exercises every moving part — indexing, count aggregation, URL state, and the Turbo/LiveComponent re-render — in one screen.
Treat the search type as the single source of truth. Define facets once in a SearchType subclass and reuse that same type for the full search page, the header quick-search, and any "related products" block, varying only the props you pass in (page size, hidden facets, default sort). Duplicating facet definitions across three controllers is the most common way these implementations rot.
Facet taxonomy, kept boring. Choice/term facets for categories, brands, tags. Range or slider facets for price, rating, date windows. A sort control modelled as a single-choice facet rather than a bespoke parameter — it keeps pagination, state serialization, and URL round-tripping uniform.
Keep the adapter swappable. The Doctrine ORM adapter is the right default: it queries your tables directly, so there is no indexing pipeline, no sync job, and no staleness — perfect for catalogs under roughly 50k rows. When you outgrow it, the pattern is to move to an engine-backed adapter and add write-side reindexing. Design your code so the engine choice lives entirely in config; if your controllers or templates know which adapter is active, you've coupled yourself out of the upgrade path.
Lean on the Live Component + Turbo for progressive enhancement. The first render should be a fully server-rendered result set; the component then swaps fragments as facets change. That means the page works without JS (good for crawlers and for your own sanity when debugging) and you get shareable URLs for free if you persist query/facet/page state as query parameters. Push every filter change into the URL — deep-linkable faceted results are the single highest-value feature of this whole package.
Pagination is a facet, not an afterthought. Always reset to page 1 when a filter changes; a stale page number combined with a narrower filter is the classic "no results" bug report.
Testing pattern: functional tests that hit the real search path with a seeded fixture set and assert both the result IDs and the facet counts. Counts are where the bugs live — assert them explicitly, don't just assert "200 OK".
The Laravel tax is real. No service provider means no php artisan vendor:publish, no config merging into config/, no Blade. The Live Component depends on Symfony's form/serializer/routing stack. Budget the migration honestly: porting the idea to Livewire + a query-builder-backed SearchFilter object is cheaper than standing up Symfony to host this. Do that unless you already have a Symfony app.
Facet counts vs. selected filters is the ambiguity that bites everyone. Decide deliberately whether a facet's counts reflect the current other selections (post-filter, standard for narrowing UIs) or the unfiltered corpus. The two produce visibly different numbers and users notice. Verify which behavior your adapter gives you before shipping, and write a test that pins it.
Doctrine adapter performance profile: free-text search degrades to LIKE '%…%' clauses, and facet counts typically mean one aggregation query per facet per render. With 6 facets you're at 7 round trips on every keystroke-triggered re-render. Mitigate with debounced input, cached counts for stable facets, and a hard cap on facet result limits. Profile with the Symfony profiler before you assume it's fast.
Index/field naming mismatches produce silent empty results — not exceptions. When a search returns nothing, check in this order: the search/index name in config, the adapter actually registered for that name, then the field names on your facet definitions versus your entity properties. Log the compiled query at debug level; it resolves 90% of these in one look.
API churn is likely. With 0 dependents and a small star count, treat the public surface as unstable: pin a minor version, keep all facet definitions in a handful of files, and avoid subclassing bundle internals. The safe extension points are the search-engine adapter interface (implement your own backed by Elasticsearch/Meilisearch) and custom facet types — both are designed seams; anything else is private in spirit.
Config quirks to expect: per-search engine binding rather than one global engine (so a "products" search and an "articles" search can use different adapters) and defaults that are not applied to explicitly-passed props. When a setting "doesn't work," confirm you aren't overriding it at the call site.
Debugging loop that works: enable the profiler, assert the rendered component receives the facet payload you expect, then assert the adapter returns the counts you expect. Splitting "UI state wrong" from "query wrong" halves the time on every bug.
How can I help you explore Laravel packages today?