Using tagging to create an intuitive filtering experience

The problem

We let customers filter by all kinds of things - everything from provider, to price, to download speed.

Drawing

These need to perform intuitively: not just allowing the customer to filter, but also letting them know exactly how many products they have available to filter in each facet.

On the surface this seems like a simple problem, but in order to correctly report the number of deals available, you need to know the number of deals at every possible intersection of your current filters and the filters you could subsequently select.

The key to our solution was splitting the problem in two: first, tagging the data according to which facets of the filters affect it, and secondly using that data to build counts of how many matching deals are available and filter out deals that do not match.

Tagging

Our code tags which products would be excluded and by which filter, as shown below:

var excludedBy = (deal, options) => {  
  const MATCHERS = {
    providers: providersFilter,
    channels: channelsFilter,
    monthlyCosts: monthlyCostsFilter,
  };

  var dealMatches = (filterName, deal, values) => (
    !MATCHERS[filterName](deal, values);
  );

  return compact(Object.keys(MATCHERS).map(filterName => {
    if (!dealMatches(filterName, deal, options.filters[filterName])) return;

    return filterName;
  }));
};

In order to work out which filters match a deal, we iterate through the possible filters set in MATCHERS. Each of the MATCHERS take a deal, and the selected values for a filter, and compare the two to work out if a filter matches or not.

If it does, it then returns the name of the matched filter, giving us a list of filters that will exclude a given deal.

The reason for tagging these deals instead of just removing them is to allow us to build our dynamic filter counts. If you remove all non-fibre deals when someone filters by fibre, we still want to tell them how many non-fibre products are available accurately.

Aggregating

Our aggregation code is made up of several steps:

  1. Aggregate the full set of all products
  2. Aggregate the set of products that we want our filter counts to be based on (this is normally all the products that are left after applying the other filters)
  3. Combine the keys from the first aggregate to the counts of the second.

What this gives us is a consistent set of filters: no vanishing options! If there's no deals available for a given option, we do grey out that option to let the customer know we can't fulfil that particular criteria: For example, there's no ADSL broadband that reaches our highest speed filter, 60Mb+.

There is a default approach for the majority of filters: the counts are based on OR. If a customer ticks the BT and Virgin Media filters, they're interested in deals that are from either provider. This is what customers intuitively expect.

Special case support

However, some filters need different logic. The other benefit of our tagging based approach is we can subsequently manipulate the data any way we like.

For example, consider filtering by channel. Unlike filtering by provider, where each selected option is a possibility, when filtering by channel, all the options should match: if you select Fox, BT Sport and Comedy Central, you want a deal that can provide you all of those channels, not just one!

Now since we control the aggregation on a per filter basis, we can change the calculations. The channels filter, when counting, counts only products that haven't been excluded at all, including by itself; effectively changing it from an OR to an AND.

Conclusion

The end result of this tagging based approach to solving the problem of filtering is an easily extensible filtering system that can adapt to customer expectations, without any additional complications on the back end.