
Filtering products in category
Listing and filtering products assigned to a hierarchical entity (usually a category) is one of the most common tasks in catalog-based e-commerce systems. However, it is also one of the most demanding in terms of performance and usability. This article provides a set of best practices and examples for implementing this functionality in your application.
Typical product listing page might look like this:
It usually consists of the following typical blocks:
- category description with rich content
- product listing — sometimes in different flavors:
- regular paginated list
- N top-selling products
- last visited products
- category menu - on multiple places:
- filtering and sorting options:
Category description
The product listing page usually starts with the category title and description. This information is easily accessible by fetching the category entity in a particular language through its unique URL:
That produces the requested data:
Breadcrumb
Breadcrumb is not a typical category menu, but it is often used in e-commerce applications. It helps to navigate the user back to the parent categories. It can be obtained from two sources:
- from the category entity itself - through entity references and their parent information
- from the product listing - as a parents menu requirement
First, let's see how to get the breadcrumb from the category entity:
As you can see, the requested parent information is part of the category entity itself:
Next, let's see how to get the breadcrumb for a specific product. Here the situation is more complicated because the product can (and in our example it does) belong to several categories:
Product listing
- entityLocaleEquals("en")- constraint filters only products with English localization
- hierarchyWithin("categories", attributeEquals("url", "/en/smartwatches"))- filters only products that belong to the category with URL "/en/smartwatches" or its subcategories
- attributeEquals("status", "ACTIVE")- filters only public products, there may also be products in PRIVATE status, which can only be seen by employees preparing the product for publication.
- or(attributeInRangeNow("validity"), attributeIsNull("validity"))- filters only products that are valid now or have no validity set at all
- referenceHaving("stocks", attributeGreaterThan("quantityOnStock", 0))- filters only products that are actually in stock (have a positive quantity on stock) - no matter which stock it is (there may be multiple stocks in the system)
- priceInCurrency("EUR"), priceInPriceLists("basic"), priceValidInNow()- filters only products that have a valid price in EUR currency and in the "basic" price list
- attributeContent("name")- retrieves product name
- referenceContentWithAttributes("stocks", attributeContent("quantityOnStock"))- retrieves the quantity on stock
- priceContentRespectingFilter("reference")- retrieves the price for sale and reference price to calculate the discount
The query is based on a demo set product model. You'd most likely have a different model in your business domain, but the principles of the query would be the same, so you can still use this query as inspiration.
The result of the query is a list of products with their attributes and references:
Top-selling products
To list the top-selling products, you would use a similar query, but with different sorting options and probably a different page size. To increase readability, we want to simplify the product query to the bare minimum:
Of course, you'd probably need to add a similar set of constraints as in the standard product listing query.
The result of the query is as follows:
Faceted search
- facets marked as requested should be rendered as checked.
- facets with impact.hasSense set to false should be rendered as disabled (because if selected, the filter would return no results, so it doesn't make sense to select it)
- facets in the group that have at least one requested and should render the impact.difference in parentheses after the facet name (sometimes only the positive impact is presented to the user) - this represents the number of products that would be added to the result set by selecting this particular reference.
- other facets should render the count in the brackets after the facet name - which represents the number of products possessing this particular reference.
These rules emerged from user testing as the most intuitive and user-friendly way to render the filter. But feel free to experiment with your own settings. The rendered filter using the rules above would look like this:
The final facet query looks like this:
We apply the same rendering logic to the response and the result is as follows:
Price filter
Price is usually one of the driving factors in a user's decision to buy a product. Therefore, sorting by price and price filter is one of the most important filters on the product listing page. We believe that the price filter should be rendered as a range slider with a histogram showing the distribution of products in the price range.
Let's demonstrate the situation when the user has already selected a certain price range:
The result of the query is as follows:
Sorting options
The last part of the product listing page is the sort options. The sort options are usually displayed as a drop-down list or a tabbed interface. The most common sort options are:
- Relevance - the default sort option, which is usually based on some predefined ordering property of the product.
In our case it would be represented by the sorting constraint: orderBy(attributeNatural("order", ASC)).
- Price - Sort by price in ascending or descending order.
In our case it would be represented by sorting constraint: orderBy(priceNatural(DESC)).
- Popularity - Sort by the number of sales or views.
In our case it would be represented by sorting constraint: orderBy(attributeNatural("rating", DESC)).
- Alphabetically - Sort by name in ascending or descending order
In our case it would be represented by the sorting constraint: orderBy(attributeNatural("name", ASC)).
Since sorting is quite simple, we'll skip the full queries in this chapter and move on to the last one, where we can see all aspects of the product listing page combined.
Complete product listing queries including filtering and sorting
By combining all of the above queries, you end up with the following two queries:
And the product list query (we omit the top-selling products query because it would just be a simpler version of the same query with different sorting options):
So in the end, to render the category detail page with product listing, you'd need to issue two or three queries. The query looks complex, but compared to the complexity of the queries you'd need to issue in different database engines, it's quite simple and straightforward.