Closed georgiarnaudov closed 1 month ago
Thanks! I'd start by checking the facet_rows table to see if that looks correct. I'm not sure I understand how you specify which attribute_options should be linked to the Facet being indexed? (But maybe I misunderstand).
If this is the issue, you could solve it by customizing the Indexing of your facets. You'll have to update the library (composer update), otherwise you won't have the buildValues() method. Also have a look at the (also updated) demo project that includes an example of a custom indexer: https://github.com/mgussekloo/Facet-Demo
Let me know if you come up with a solution, or if I can make the library better for your use case.
Start by adding the attribute_id to the facet definition;
public static function facetDefinitions(): array
{
return Attribute::all()
->map(fn($attribute) => [
'title' => $attribute->frontend_label,
'fieldname' => 'id', // this is just a placeholder
'attribute_id' => $attribute->id
])
->toArray();
}
Customize the indexing, e.g.:
class MyCustomIndexer extends \Mgussekloo\FacetFilter\Indexer {
public function buildValues($facet, $model) {
$values = parent::buildValues($facet, $model);
if (isset($facet->attribute_id)) {
// i don't know how your relationships are set up; in any case you need to filter the attribute options for this facet
$values = $model->attribute_options->where('attribute_id', $facet->attribute_id)->pluck('value');
}
return $values;
}
}
Hey, thanks for adding a way to build the values! I've achieved the needed result using a few custom custom classes.
Here's how I did it:
App\Models\Product.attribute.color
which reads great for me.<?php
namespace App;
use Illuminate\Support\Str;
use Mgussekloo\FacetFilter\Builders\FacetQueryBuilder;
use Mgussekloo\FacetFilter\Models\Facet;
class AttributeFacet extends Facet
{
public function getParamName(): string
{
return Str::slug($this->fieldname);
}
public function getSlug(): string
{
return implode('.', [$this->subject_type, 'attribute', $this->fieldname]);
}
public function constrainQueryWithFilter($query, $filter): FacetQueryBuilder
{
$facetName = $this->getParamName();
$selectedValues = (isset($filter[$facetName]))
? collect($filter[$facetName])->values()
: collect([]);
$rows = $this->rows ?? collect();
// if you have selected ALL, it is the same as selecting none
$allValues = $rows->pluck('value')->filter()->unique()->values();
if ($allValues->diff($selectedValues)->isEmpty()) {
$selectedValues = collect([]);
}
// if you must filter
if ($selectedValues->isNotEmpty()) {
$query->whereHas('facetrows', function ($query) use ($selectedValues): void {
$query->select('id')->where('facet_slug', $this->getSlug())->whereIn('value', $selectedValues->toArray());
});
}
return $query;
}
}
Something to consider maybe is to set the $rows value to an empty collection as a fallback, since some the facets may not be used in the index, therefore there will be an error because the $rows will be null and you can't pluck on null.
This was true in my case, since I'm generating the attributes based on what's in the database, but not exactly what is associated with a product.
public static function facetDefinitions(): array
{
$attributeIdsInUse = ProductVariationValue::query()
->distinct('attribute_id')
->groupBy('attribute_id')
->pluck('attribute_id');
$attributeFacets = Attribute::query()
->whereIn('id', $attributeIdsInUse)
->get()
->map(fn($attribute) => [
'title' => $attribute->frontend_label,
'fieldname' => $attribute->name,
'related_id' => $attribute->id,
'facet_class' => AttributeFacet::class
])
->toArray();
return [
[
'title' => 'Цена',
'fieldname' => 'finalPrice'
],
... $attributeFacets,
];
}
<?php
namespace App;
use Mgussekloo\FacetFilter\Indexer;
class ProductIndexer extends Indexer
{
public function buildValues($facet, $model)
{
$values = parent::buildValues($facet, $model);
if ($facet->getSlug() == 'App\Models\Product.finalPrice') {
foreach ($values as $index => $value) {
if ($value > 0 && $value <= 10) {
$values[$index] = '0-10';
}
if ($value >= 10 && $value <= 100) {
$values[$index] = '10-100';
}
if ($value > 100) {
$values[$index] = 'Скъпо';
}
}
}
if ($facet instanceof AttributeFacet) {
$values = $values->merge(
$model->attributeValuesForRepeater
->filter(fn($variation) => $variation->attribute_id === $facet->related_id)
->map(fn($variation) => $variation->attributeValue->value)
);
}
return $values;
}
}
Everything looks fine and works as expected. There're 2 things that bog my mind though:
Pagination (I've implemented a custom paginator, but not sure if this is the best way). In my case I'm filtering categories with up to 500 products, but in reality there could be cases with thousands of products/models.
Having an asset associated with the facet option. For example, I would like to be able to display an image on the frontend alongside the value. I know this is pure for UX, but I've seen that on a lot of sites and would be cool if we can achieve it somehow.
https://github.com/user-attachments/assets/94178e59-7aa2-4984-88cd-521bf6786f2c
Great work, thanks! I'll add the $rows fallback to an empty collection, good catch!
For images, I think easiest would be to just have the custom facet class handle this, maybe overwriting getOptions() method?
class CustomFacet extends Facet {
public function getOptions(): Collection
{
if (is_null($this->options)) {
$this->options = parent::getOptions()->map(function($option) {
if ($option->value == 'duck') {
$option->image = 'duck.jpg';
return $option;
}
});
For pagination, I don't have a "good" answer yet. For my own project I select() the bare minimum columns to enable FacetFiltering, then further load() the relationships / columns for the current page only. In my case I have ~2000 products and ~10 facets, and this works fast enough.
Hey, thanks for the suggestion! Was thinking something like that would work. Here's what I've implemented:
public function getOptions(): Collection
{
if (is_null($this->options)) {
$attributeValues = AttributeValue::query()->with('media')->get();
$this->options = parent::getOptions()->map(function ($option) use ($attributeValues) {
$attributeValue = $attributeValues->where('label', '=', $option->value)->first();
$option->image = !is_null($attributeValue) ?
$attributeValue->getFirstMediaUrl('attribute-value-images') :
null;
return $option;
});
}
return $this->options;
}
It's not the best since I'm making one more query, but it does the job for now.
Regarding pagination I would try out and see if it works for the project.
Thanks again for taking the time to answer and support! Closing, since everything works.
Sorry for posting once again, but forgot to ask just out of curiosity how do you handle category pages? In my case products can be part of multiple categories and vice versa, so when I'm on a category page I just query the relationship and then apply the FacetFilter.
No problem, I'm happy someone benefits from this code!
For my project I apply the facet filter to the end of a query that includes some other stuff, e.g.
$items = Products::whereNotIn('hoviproductid', $illegal_ids)
->where('category_id', $catId)
->facetsMatchFilter($this->filter)
->get();
This results in the facets only taking into account products in the correct category. Hope this helps.
Hi! Thanks for the great work you've done!
I didn't see a discussions tab, that's why I'm opening an issue.
Is it possible to build a faceted filter by using somewhat of a EAV structure. I've got the following schema:
I've started by returning the attributes that I want in the facetDefinitions method:
Next, I've built the index using the following code:
The index is successfully stored in the database.
The issue comes when I try to show the facets on the frontend. Whenever I loop over them the
getOptions()
method returns exactly the same options and if you try to select on, all of the options are being selected. Here's a snippet from the Livewire component:I'm not sure if I've done something completely wrong, or just the structure of the database is not compatible for this kind of operations. I've thought about using separate tables, but I need to have flexibility and create attributes on the fly.
Would appreciate if you suggest something. Thanks in advance!