This package provides simple facet filtering (sometimes called Faceted Search or Faceted Navigation) in Laravel projects. It helps narrow down query results based on the attributes of your models.
- Free, no dependencies
- Easy to use in any project
- Easy to customize
- There's a demo project to get you started
- Ongoing support (last update: july 2025)
Please contribute to this package either by creating a pull request or reporting an issue.
This package can be installed through Composer.
composer require mgussekloo/laravel-facet-filter
Add a Facettable trait and a facetDefinitions() method to models you'd like to filter:
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Mgussekloo\FacetFilter\Traits\Facettable;
class Product extends Model
{
use HasFactory;
use Facettable;
public static function facetDefinitions()
{
// Return an array of definitions
return [
[
'title' => 'Main color', // The title will be used for the parameter.
'fieldname' => 'color' // Model property from which to get the values.
],
[
'title' => 'Sizes',
'fieldname' => 'sizes.name' // Use dot notation to get the value from related models.
]
];
}
}
In most cases you'll want to create an index to filter large datasets efficiently. (Don't want to build an index? Skip ahead to filtering collections.)
First, run the migrations:
php artisan vendor:publish --tag="facet-filter-migrations"
php artisan migrate
To build the index, create an Artisan command that queries your Facettable models. Run it periodically or whenever your data changes.
$products = Product::with(['sizes'])->get(); // get some products
$products->buildIndex(); // build the index
Within a controller, apply the facet filter to a query
$filter = request()->all(); // the filter looks like ['main-color' => ['green']]
$products = Product::facetFilter($filter)->get();
Here's a simple demo project that demonstrates a basic frontend.
<div class="flex">
<div class="w-1/4 flex-0">
@foreach ($products->getFacets() as $facet)
<p>
<h3>{{ $facet->title }}</h3>
@foreach ($facet->getOptions() as $option)
<a href="?{{ $option->http_query }}" class="{{ $option->selected ? 'underline' : '' }}">{{ $option->value }} ({{ $option->total }}) </a><br />
@endforeach
</p><br />
@endforeach
</div>
<div class="w-3/4">
@foreach ($products as $product)
<p>
<h1>{{ $product->name }} ({{ $product->sizes->pluck('name')->join(', ') }})</h1>
{{ $product->color }}<br /><br />
</p>
@endforeach
</div>
</div>
To see an example of a Livewire implementation, see this gist.
$facets = $products->getFacets();
/* You can filter and sort like any regular Laravel collection. */
$singleFacet = $facets->firstWhere('fieldname', 'color');
/* Find out stuff about the facet. */
$paramName = $singleFacet->getParamName(); // "main-color"
$options = $singleFacet->getOptions();
/*
Options look like this:
(object)[
'value' => 'Red',
'selected' => false,
'total' => 3,
'slug' => 'color_red',
'http_query' => 'main-color%5B1%5D=red&sizes%5B0%5D=small'
]
*/
Extend the Indexer to customize behavior, e.g. to save a "range bracket" value instead of a "individual price" value to the index.
class MyCustomIndexer extends \Mgussekloo\FacetFilter\Indexer {
public function buildValues($facet, $model) {
$values = parent::buildValues($facet, $model);
if ($facet->fieldname == 'price') {
if ($model->price > 1000) {
return 'Expensive';
}
if ($model->price > 500) {
return '500 - 1000';
}
if ($model->price > 250) {
return '250 - 500';
}
return '0 - 250';
}
return $values;
}
}
You may overwrite the indexer() method on your Facettable model to return an instance of your custom indexer:
use App\MyCustomIndexer;
class Product extends Model
{
use HasFactory;
use Facettable;
public static function indexer()
{
return new MyCustomIndexer();
}
}
$products = Products::get();
$products->buildIndex(); // uses MyCustomIndexer
$indexer = new Indexer();
$perPage = 1000; $currentPage = Cache::get('facetIndexingPage', 1);
$products = Product::with(['sizes'])->paginate($perPage, ['*'], 'page', $currentPage);
if ($currentPage == 1) {
$indexer->resetIndex(); // clear entire index
}
$indexer->buildIndex($products);
if ($products->hasMorePages()) {}
// next iteration, increase currentPage with one
}
Provide custom attributes and an optional custom Facet class in the facet definitions.
public static function facetDefinitions()
{
return [
[
'title' => 'Main color',
'description' => 'The main color.', // optional custom attribute, you could use $facet->description when creating the frontend...
'related_id' => 23, // ... or use $facet->related_id with your custom indexer
'fieldname' => 'color',
'facet_class' => CustomFacet::class // optional Facet class with custom logic
]
];
}
It's possible to apply facet filtering to a collection without building an index. Facettable models return a FacettableCollection, which provides an indexlessFacetFilter() method.
$products = Product::all(); // returns a "FacettableCollection"
$products = $products->indexlessFacetFilter($filter);
Example:
$products = Product::facetFilter($filter)->paginate(10);
$pagination = $products->appends(request()->input())->links();
By default Facet Filter caches some heavy operations through the non-persistent 'array' cache driver. Caches are based on the model and filter, not the query bindings or specifics. Use a cacheTag to distinguish between queries.
// if you have two facet filter queries on the same model...
Product::where('published', false)->cacheTag('unpublished')->facetFilter($filter)->get();
// ...make sure to distinguish between queries
Product::where('published', true)->cacheTag('published')->facetFilter($filter)->get();
The default Indexer clears the cache for all models when rebuilding the index. You can do it manually:
Product::forgetCache(); // clears cache for all cache tags
Product::forgetCache('unpublished'); // clears cache for only this cachetag
use Mgussekloo\FacetFilter\Facades\FacetCache;
FacetCache::forgetCache(); // clears cache for all models and cache tags
You can configure a peristent cache driver through config/facet-filter.php
. Be aware of any caching related issues, e.g. if you have any user specific results this may be problematic.
php artisan vendor:publish --tag=facet-filter-config
'classes' => [
'facet' => Mgussekloo\FacetFilter\Models\Facet::class,
'facetrow' => Mgussekloo\FacetFilter\Models\FacetRow::class,
],
'table_names' => [
'facetrows' => 'facetrows',
],
'cache' => [
'expiration_time' => \DateInterval::createFromDateString('24 hours'),
'key' => 'mgussekloo.facetfilter.cache',
'store' => 'array',
],
The MIT License (MIT). Please see License File for more information.