Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions config/rapidez/searchkit.php
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,11 @@
['attribute' => 'visibility', 'field' => 'visibility', 'type' => 'numeric'],
],

// The minimum percentage of products that should be available for a filter to be shown.
// For example, if you have 100 products in the listing and a filter has only 5 products available,
// the filter will not be shown because it is only available for 5% of the products.
'min_filter_product_percentage' => 10,

// Additional sorting options to be added to the product listings
// Given directions can only be an array of 'asc' and/or 'desc'
// Order shown here will be the order shown in the dropdown (including the order of the given directions!)
Expand Down
62 changes: 62 additions & 0 deletions resources/js/components/Listing/Listing.vue
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import RangeInput from 'vue-instantsearch/vue3/es/src/components/RangeInput.vue.
import HierarchicalMenu from 'vue-instantsearch/vue3/es/src/components/HierarchicalMenu.vue.js'
import RefinementList from 'vue-instantsearch/vue3/es/src/components/RefinementList.vue.js'
import SortBy from 'vue-instantsearch/vue3/es/src/components/SortBy.vue.js'
import { instantsearchMiddlewares } from '../../stores/useInstantsearchMiddlewares'

export default {
mixins: [InstantSearchMixin],
Expand Down Expand Up @@ -59,6 +60,7 @@ export default {
searchClient: null,
destroyed: false,
utmFields: [],
instantSearchInstance: null,
}),

render() {
Expand Down Expand Up @@ -127,6 +129,19 @@ export default {
},

methods: {
getMiddlewares() {
return [
({ instantSearchInstance }) => {
this.instantSearchInstance = instantSearchInstance
return {
onStateChange: () => {},
subscribe: () => {},
unsubscribe: () => {},
}
},
Comment on lines +134 to +141

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding a middleware to the instantsearch initialization gives us easy access to the instantsearchInstance.

Hacks we've used in the past were

export default {
    inject: {
        instantSearchInstance: {
            from: '$_ais_instantSearchInstance',
        },
    },
   ...

Alternatively custom widgets could be used: https://www.algolia.com/doc/guides/building-search-ui/widgets/create-your-own-widgets/vue
That would mean every filter would need another Vue component wrapping it.

This current method gives us access to the instantSearchInstance within any Listing component.

...instantsearchMiddlewares,
]
},
async getInstantSearchClientConfig() {
const config = await InstantSearchMixin.methods.getInstantSearchClientConfig.bind(this).call()

Expand Down Expand Up @@ -256,6 +271,53 @@ export default {
...item,
}))
},

isRelevantFilter(filterItems, minProductPercentage = null) {
if (!filterItems?.length) {
return false
}

if (filterItems.some((item) => item.isRefined)) {
return true
}

if (!this.instantSearchInstance) {
return true
}
const totalHits = this.instantSearchInstance?.helper?.lastResults?.nbHits
if (!totalHits) {
return true
}
if (isNaN(minProductPercentage) || minProductPercentage === null) {
minProductPercentage = window.config.searchkit.min_filter_product_percentage ?? 10
}

const resultCount = filterItems.reduce((sum, item) => item.count + sum, 0)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't appear to take into account range-based filters (as also shown by this change not being present in the range slider filter). This means you will get a ton of range filters and not many of the other filters, which is a specific thing I had to fix in one of our projects.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've added a function for range filters


return resultCount / totalHits > minProductPercentage / 100

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Something else to be aware of is that it may be worthwhile to have a threshold after which a filter is always shown, regardless of percentage. For example, say you have a category with 5000 products. If 400 of those products have a common filter, it will be excluded from that category, but there's a good likelihood that that filter is actually useful depending on the project.

I think it may be worthwhile to either add this as a default functionality, or make an easy to overwrite comparison function that's used at the end here so you can program this yourself without having to overwrite the whole function.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I reckon the filter relevancy rules would be especially useful for pages with 5000 products.
If there's enough filters that a filter would only match 400 of those 5000 you've got a good chance the filters exceed the list of products in height.

But for making it easy to overwrite is exactly why the function is structured the way it it.
For individual filters (say there is a filter you always want to show) you can overwrite the minProductPercentage in the function call to isRelevantFilter and set it to 0
If you would like to overwrite it for the entire catalog page you could overwrite
window.config.searchkit.min_filter_product_percentage and set the percentage higher or lower

},
isRelevantRange(filterCode, minProductPercentage = null) {
if (Object.keys(this.instantSearchInstance?.helper?.state?.numericRefinements?.[filterCode] || {}).length) {
return true
}

if (!this.instantSearchInstance) {
return true
}

const totalHits = this.instantSearchInstance?.helper?.lastResults?.nbHits
const facetStats = this.instantSearchInstance?.helper?.lastResults?.facets_stats?.[filterCode]
if (!totalHits || !facetStats) {
return true
}
if (isNaN(minProductPercentage) || minProductPercentage === null) {
minProductPercentage = window.config.searchkit.min_filter_product_percentage ?? 10
}

const resultCount = facetStats.sum / facetStats.avg

return resultCount / totalHits > minProductPercentage / 100
},
},
}
</script>
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ class="relative -mx-1 -mt-1"
>
<template v-slot="{ items, refine }">
<div class="overflow-clip">
<x-rapidez::accordion.filter v-show="items.length" class="details-content:overflow-visible px-1 py-1">
<x-rapidez::accordion.filter v-show="listingSlotProps.isRelevantFilter(items)" class="details-content:overflow-visible px-1 py-1">
<x-slot:content>
<div class="flex flex-col *:py-1 *:first:pt-0 *:last:pb-0 items-start">
<template v-for="item in items">
Expand Down
2 changes: 1 addition & 1 deletion resources/views/listing/partials/filter/price.blade.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
class="relative -mx-1 -mt-1"
>
<template v-slot="{ currentRefinement, range, canRefine, refine, sendEvent }">
<div class="overflow-clip" v-show="range.max">
<div class="overflow-clip" v-show="range.max && listingSlotProps.isRelevantRange(filter.code)">
<x-rapidez::accordion.filter class="details-content:overflow-visible px-1 py-1">
<x-slot:content>
<div class="flex flex-col">
Expand Down
2 changes: 1 addition & 1 deletion resources/views/listing/partials/filter/select.blade.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ class="relative -mx-1 -mt-1"
>
<template v-slot="{ items, refine, isShowingMore, toggleShowMore, canToggleShowMore }">
<div class="overflow-clip">
<x-rapidez::accordion.filter v-show="items.length" class="details-content:overflow-visible px-1 py-1" canToggleShowMore>
<x-rapidez::accordion.filter v-show="listingSlotProps.isRelevantFilter(items)" class="details-content:overflow-visible px-1 py-1" canToggleShowMore>
<x-slot:content>
<div class="flex flex-col *:py-1 *:first:pt-0 *:last:pb-0 items-start">
<template v-for="item in items">
Expand Down
2 changes: 1 addition & 1 deletion resources/views/listing/partials/filter/swatch.blade.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ class="relative"
>
<template v-slot="{ items, refine, isShowingMore, toggleShowMore, canToggleShowMore }">
<div class="overflow-clip">
<x-rapidez::accordion.filter class="details-content:overflow-visible" v-show="items.length" canToggleShowMore>
<x-rapidez::accordion.filter class="details-content:overflow-visible" v-show="listingSlotProps.isRelevantFilter(items)" canToggleShowMore>
<x-slot:content>
<ul class="flex flex-wrap gap-x-1.5 gap-y-2 items-center pr-14">
<li v-for="item in listingSlotProps.withSwatches(items, filter)">
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading