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.
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 that should support facet filtering.
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.
]
];
}
}
For larger datasets you must build an index of all facets beforehand. If you're absolutely certain you don't need an index, skip to filtering collections.
php artisan vendor:publish --tag="facet-filter-migrations"
php artisan migrate
Now you can start building the index. There's a simple Indexer included, you just need to configure it to run once, periodically or whenever a relevant part of your data changes.
use Mgussekloo\FacetFilter\Indexer;
$products = Product::with(['sizes'])->get(); // get some products
$indexer = new Indexer();
$indexer->resetIndex(); // clear the entire index or...
$indexer->resetRows($products); // clear only the models that you know have changed
$indexer->buildIndex($products); // process the models
$filter = request()->all(); // use the request parameters
$filter = ['main-color' => ['green']]; // (or provide your own array)
$products = Product::facetFilter($filter)->get();
$facets = Product::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'
]
*/
Here's a simple demo project that demonstrates a basic frontend.
<div class="flex">
<div class="w-1/4 flex-0">
@foreach ($facets as $facet)
<p>
<h3>{{ $facet->title }}</h3>
@foreach ($facet->getOptions() as $option)
<a href="https://github.com/mgussekloo/laravel-facet-filter/blob/master/?{{ $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>
This is how it could look like with Livewire.
<h2>Colors</h2>
@foreach ($facet->getOptions() as $option)
<div class="facet-checkbox-pill">
<input
wire:model="filter.{{ $facet->getParamName() }}"
type="checkbox"
id="{{ $option->slug }}"
value="{{ $option->value }}"
/>
<label for="{{ $option->slug }}" class="{{ $option->selected ? 'selected' : '' }}">
{{ $option->value }} ({{ $option->total }})
</label>
</div>
@endforeach
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;
}
}
$perPage = 1000; $currentPage = Cache::get('facetIndexingPage', 1);
$products = Product::with(['sizes'])->paginate($perPage, ['*'], 'page', $currentPage);
$indexer = new Indexer($products);
if ($currentPage == 1) {
$indexer->resetIndex();
}
$indexer->buildIndex();
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. Models with the Facettable trait return a FacettableCollection which has an indexlessFacetFilter() method. It's slower than filtering with an index, though.
$products = Product::all(); // returns a "FacettableCollection"
$products = $products->indexlessFacetFilter($filter);
// the second (optional) parameter lets you specify which indexer to use when indexing values from models
$indexer = new App\MyCustomIndexer();
$products = Product::all()->indexlessFacetFilter($filter, $indexer);
By default Facet Filter uses the non-persistent 'array' cache driver, with queries and calculations happening every request. You can configure the cache driver (as well as the expiration time and cachekey prefix) through config/facet-filter.php
If you decide to use a persistent cache driver, please note the following:
// do not clear the result count cache before facet filtering (only useful if using a persistent caching driver)
Product::withCache()->facetFilter($filter)->get();
// using collection-based facet filtering
Projects::all()->withCache()->indexlessFacetFilter($filter);
FacetFilter::forgetCache(); // clears all result counts for all facets, and all facet rows
'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.