Found a typo?
Instantsearchjs instantsearch.js

Filtering & Faceting - instantsearch.js

Introduction

In the quest of building great search UI/UX that lets users find what they want, and discover what they need, we see that filtering plays a key role. When thinking about search, we often tend to take shortcuts, reducing search to its most basic form: the search box and a set of results matching the few keywords filled in. But in the end, search is also about navigation and browsing.

Like in a conversation, search is an iterative and interactive experience where what we find changes what we seek. Every new keystroke entered by the user can be an opportunity to discover new filtering options that will ultimately lead them to find what they are looking for. Allowing them to start their journey entering a word, then select one of several filtering options, and then why not update their initial search query based on the results which have popped up.

Filtering faceting instantsearchjs overview

Example of interactions when searching a ‘red ipad case’

In this guide we’ll see the different widgets that can be added to your search result page in order to let the users narrow down the set of displayed results. In the second step, we’ll also see what are the filtering options offered at the API level, and how those can be easily leveraged to create your own widgets, in case the available ones don’t fully cover your needs.

Prerequisite

This guide is the continuation of the Instant Search Result Page one, in which we see how to build an as-you-type result page using InstantSearch.js. If you don’t know what a InstantSearch.js widget is, or haven’t had a chance to build your own result page - we recommend you reading that one first.

Important Updates

Used data

Here’s what the records used in the examples look like:

[
  {
    "objectID": "5578016",
    "name": "Apple - Refurbished iPad mini 3 - 16GB - Space Gray",
    "description": "Hold digital power in your hand with this refurbished Apple iPad Mini 3. The 16GB of storage lets you save music and photos, and the Wi-Fi capabilities let you connect from home, a coffee shop or the office. This Apple iPad Mini 3 has Bluetooth technology for easy pairing with wireless speakers or headphones.",
    "brand": "Apple",
    "categories": [
      "Computers & Tablets",
      "Tablets",
      "Refurbished Tablets"
    ],
    "hierarchicalCategories": {
      "lvl0": "Computers & Tablets",
      "lvl1": "Computers & Tablets > Tablets",
      "lvl2": "Computers & Tablets > Tablets > Refurbished Tablets"
    },
    "type": "Tablet",
    "price": 279.99,
    "price_range": "200 - 500",
    "free_shipping": true,
    "rating": 0,
    "popularity": 12855
  }
  // other products [...]
]

And the index configuration needed to use the different filtering widgets that we will describe.

Remember, it exists different types of filters depending on how you want your users to interact with. Nevertheless most of them rely on the faceting feature.

Filtering faceting common attributestofacet

Setting attributesForFaceting in the Display tab of the main index

Algolia::Index.new('instant_search').set_settings({"attributesForFaceting"=>["searchable(brand)", "price_range", "categories", "type", "price", "free_shipping", "rating", "popularity", "hierachicalCategories"]})
<?php
$client->initIndex("instant_search")->setSettings(array("attributesForFaceting" => array("searchable(brand)", "price_range", "categories", "type", "price", "free_shipping", "rating", "popularity", "hierachicalCategories")));
client.initIndex('instant_search').setSettings({"attributesForFaceting":["searchable(brand)", "price_range", "categories", "type", "price", "free_shipping", "rating", "popularity", "hierachicalCategories"]});
client.init_index('instant_search').set_settings({"attributesForFaceting":["searchable(brand)", "price_range", "categories", "type", "price", "free_shipping", "rating", "popularity", "hierachicalCategories"]})
client
  .initIndex("instant_search")
  .setSettings(new IndexSettings()
    .setAttributesForFaceting(Arrays.asList("searchable(brand)", "price_range", "categories", "type", "price", "free_shipping", "rating", "popularity", "hierachicalCategories"))
  );
client.execute {
  changeSettings of "instant_search" `with` IndexSettings(
    attributesForFaceting = Some(Seq("searchable(brand)", "price_range", "categories", "type", "price", "free_shipping", "rating", "popularity", "hierachicalCategories"))
  )
}
res, err := index.SetSettings(algoliasearch.Map{
  "attributesForFaceting": []string{"searchable(brand)", "price_range", "categories", "type", "price", "free_shipping", "rating", "popularity", "hierachicalCategories"},
})

Note: When configuring attributes on which we will facet, there’s no indication as to how those will be used: in a menu, or refinementList, or numericRefinementList, … That part happens when implementing the front-end code.

Filtering and Faceting

In addition to the core widgets like the searchbox, the hits, or the pagination, InstantSearch.js offers a full set of filtering widgets out of the box.

In InstantSearch.js all widgets are linked together including the filtering ones. In other words, any search query or refinement update, automatically refreshes the list of available facet values for every filtering widget defined - avoiding users to get suggested values which would lead them to a no result page, if selected.

Widgets list

Pro-tip: Popular widgets have been marked with a .

The menu widget provides a way to navigate through results based on a single attribute. Only one value can be selected at a time. This can be used for navigating through the categories of an e-commerce website.

search.addWidget(
  instantsearch.widgets.menu({
    container: '#categories',
    attributeName: 'categories',
    limit: 10,
    templates: {
      header: 'Categories'
    }
  })
);

Available options

See the menu widget reference documentation for all the widget options.

hierarchicalMenu

Filtering faceting instantsearchjs widget hierarchicalmenu

The hierarchical menu is a widget that lets the user explore a tree-like structure. This is commonly used for multi-level categorization of products on e-commerce websites.

search.addWidget(
  instantsearch.widgets.hierarchicalMenu({
    container: '#hierarchical-categories',
    attributes: ['hierarchicalCategories.lvl0', 'hierarchicalCategories.lvl1', 'hierarchicalCategories.lvl2'],
    templates: {
      header: 'Hierarchical categories'
    }
  })
);

In order to work, the categories and sub-categories need to be formatted in the following way (see specification).

"hierarchicalCategories": {
  "lvl0": "Computers & Tablets",
  "lvl1": "Computers & Tablets > Tablets",
  "lvl2": "Computers & Tablets > Tablets > Refurbished Tablets"
},
...

Note: From a usability standpoint, we suggest not displaying more than two levels deep.

Available options

See the hierarchicalMenu reference documentation for all the widget options.

refinementList

Unlike to the menu widget, the refinementList lets the user selects 1..N value(s) to narrow down the result set.

By default, facet values are sorted in a descending way based on their facet counts. Allowing users to always access the values that are the most represented in the result set.

search.addWidget(
    instantsearch.widgets.refinementList({
      container: '#brand',
      attributeName: 'brand',
      sortBy: ['isRefined', 'count:desc', 'name:asc'],
      limit: 10,
      operator: 'or',
      templates: {
        header: getHeader('Brand')
      }
    })
  );

Using the option sortBy: ['isRefined', 'count:desc', ...] you have the possibility to keep on top of the facet list all selected facet values. The option limit allows you on its side, to define the max number of facet values displayed: here the 10 with the highest count.

Note: Built using the disjunctive facet logic the widget lets you specify whether you want the filter values to be ORed or ANDed. For example, if you filter on blue and red with OR (default), results displayed will either match blue or red. Contrariwise if you select AND only results having the values blue and red in the faceted attribute will be returned.

UX Tweaking

Widely used when building a search experience with facet refinements, the widget usually follows one of the 3 different UX:

  • Only a limited number of facet values are displayed (default)
    Usually this number remains low (<= 10 facet values), so users can easily access to other potential filters without having to scroll indefinitely.
  • A limited number of facet values are displayed followed by a Show more button
  • A limited number of facet values are displayed with the possibility to search for all available values, including the ones not displayed by default.
Show more
Filtering faceting instantsearchjs widget refinementlist showmore

Present on a lot of website, the "show more approach" usually allows to display up to 2-3 times more facet values, than by default. Allowing users to have a broader choice, without reducing the visibility of other filters added to the UI.

search.addWidget(
    instantsearch.widgets.refinementList({
      container: '#brand',
      attributeName: 'brand',
      sortBy: ['isRefined', 'count:desc', 'name:asc'],
      limit: 10,
      operator: 'or',
      templates: {
        header: getHeader('Brand')
      },
      showMore: {
        limit: 20
      }
    })
  );

Here the “show more” behavior is enabled by adding showMore: { limit: 20 } to the widget configuration. By default up to 10 facet values will be displayed, and up to 20 when expended.

Search for facet values
Filtering faceting instantsearchjs widget refinementlist searchforfacetvalues

Often, displaying just the best facet values is too limiting for users, as it doesn't allow them to filter the current set of result using all values available for a given facet.
Allowing users to search for available values let you offer the best of two worlds.

search.addWidget(
    instantsearch.widgets.refinementList({
      container: '#brand',
      attributeName: 'brand',
      sortBy: ['isRefined', 'count:desc', 'name:asc'],
      limit: 10,
      operator: 'or',
      templates: {
        header: getHeader('Brand')
      },
      searchForFacetValues: {
        placeholder: 'Search for brands',
        templates: {
          noResults: '<div class="sffv_no-results">No matching brands.</div>'
        }
      }
    })
  );

Available options

See the refinementList reference documentation for all the widget options.

Pro-tip: You have the possibility to combine show more and search for facet values at the same time.

numericRefinementList

This widget lets the user refine search results based on a numerical attribute. You can specify a specific number or a range.

search.addWidget(
  instantsearch.widgets.numericRefinementList({
    container: '#popularity',
    attributeName: 'popularity',
    options: [
      {name: 'All'},
      {end: 500, name: 'less than 500'},
      {start: 500, end: 2000, name: 'between 500 and 2000'},
      {start: 2000, name: 'more than 2000'}
    ],
    templates: {
      header: 'Popularity'
    }
  })
);

Available options

See the numericRefinementList widget reference documentation for all the widget options.

Pro-tip: When possible, it’s better to define those ranges directly in the record

toggle

Filtering faceting instantsearchjs widget toggle

This widget provides an on/off filtering feature based on an attribute value. Note that if you provide an “off” option, it will be refined at initialization.

{
  ...
  "free_shipping": true
}

You can either use a boolean or numerical value. The engine automatically interprets 1 => true and 0 => false

search.addWidget(
  instantsearch.widgets.toggle({
    container: '#free-shipping',
    attributeName: 'free_shipping',
    label: 'Free Shipping',
    values: {
      on: true,
      off: false
    },
    templates: {
      header: 'Shipping'
    }
  })
);

Available options

See the toggle widget reference documentation for all the widget options.

rangeSlider

Filtering faceting instantsearchjs widget rangeslider

The rangeSlider widget lets users filter results within a numerical range, based on an attribute. The min and max values are automatically computed by Algolia using the data in the index.

search.addWidget(
  instantsearch.widgets.rangeSlider({
    container: '#price',
    attributeName: 'price',
    templates: {
      header: 'Price'
    },
    tooltips: {
      format: function(rawValue) {
        return '$' + Math.round(rawValue).toLocaleString();
      }
    }
  })
);

Available options

See the rangeSlider widget reference documentation for all the widget options.

priceRanges

Filtering faceting instantsearchjs widget priceranges

This filtering widget lets the user choose a price range. The ranges are dynamically computed based on the returned results.

search.addWidget(
  instantsearch.widgets.priceRanges({
    container: '#price-ranges',
    attributeName: 'price',
    labels: {
      currency: '$',
      separator: 'to',
      button: 'Go'
    },
    templates: {
      header: 'Price'
    }
  })
);

Available options

See the priceRanges widget reference documentation for all the widget options.

numericSelector

This filtering widget lets the user choose between numerical refinements from a dropdown menu.

search.addWidget(
  instantsearch.widgets.numericSelector({
    container: '#popularity-selector',
    attributeName: 'popularity',
    operator: '>=',
    options: [
      {label: 'Top 10', value: 9900},
      {label: 'Top 100', value: 9800},
      {label: 'Top 500', value: 9700}
    ]
  })
);

Available options

See the numericSelector widget reference documentation for all the widget options.

starRating

Filtering faceting instantsearchjs widget starrating

This widget lets the user refine search results by clicking on stars. The number of stars is based on the value of the passed attributeName. That one needs to start from 0 up to N.

search.addWidget(
  instantsearch.widgets.starRating({
    container: '#stars',
    attributeName: 'rating',
    max: 5,
    labels: {
      andUp: '& Up'
    },
    templates: {
      header: getHeader('Rating')
    }
  })
);

Available options

See the starRating widget reference documentation for all the widget options.

Active filters management

currentRefinedValues

This widget list all the refinements currently applied. It also lets the user clear them one by one. This widget can also contain a clear all link to remove all filters.

search.addWidget(
  instantsearch.widgets.currentRefinedValues({
    container: '#current-refined-values',
    clearAll: 'after'
  })
);

Available options

See the currentRefinedValues reference documentation for all the widget options.

clearAll

This widget clears all the refinements that are currently applied.

search.addWidget(
  instantsearch.widgets.clearAll({
    container: '#clear-all',
    templates: {
      link: 'Reset everything'
    },
    autoHideContainer: false
  })
);

Available options

See the clearAll reference documentation for all the widget options.

Pro tips

Default filters

Sometimes you may want to automatically add some filters on the first page load, without letting users change them values via the UI. Maybe automatically filter on Cell Phones made by either Samsung or Apple, or only display items that are somehow “premium”. There are two ways to do it. Either you will use some filtering widgets on the category and brand, and in that case the first approach is the best to follow.

var search = instantsearch({
  [...],
  searchParameters: {
    hierarchicalFacetsRefinements: { // menu is implemented as a hierarchicalFacetsRefinements
      categories: ['Cell Phones']
    },
    facetsRefinements: {
      in_stock: [true]
    }
    // Add to "facets" all attributes for which you
    // do NOT have a widget defined
    facets: ['in_stock']
  },
});
// Below is just a common widget configuration, to show
// how it interacts with the above searchParameters
search.addWidget(
  instantsearch.widgets.menu({
    [...],
    attributeName: 'categories'
  })
);

Or, not and in that case, here is how to do it with the filters parameter.

var search = instantsearch({
  [...],
  searchParameters: {
    filters: 'in_stock AND categories:"Cell Phones"'
  }
});

See REST reference documentation with all searchParamters options.

Gain space with collapsing

Filtering faceting instantsearchjs collapse

All filtering widgets available by default with InstantSearch.js can be collapsed: either by default, or at the initiative of the user.

As screen size is always limited (more especially on mobile devices) and often doesn't allow to show all the filtering options available, collapsing the "second class" filters helps to gain in visibility. Allowing the user to have a better overview of all the available filters.

search.addWidget(
  instantsearch.widgets.widgetName({
    ...
    collapsible: {
      collapsed: true // collapsed by default: true | false
    }
  })
);

Live demos

You can see all the widgets described above, in the following demo.

Filtering faceting instantsearchjs overview

More examples

Instantsearchjs demo youtube screenshot Instantsearchjs demo ikea screenshot Instantsearchjs demo airbnb screenshot
Media E-commerce Tourism
View demo or source code View demo or source code View demo or source code

Create your own widgets

Out of the box, InstantSearch.js includes most of the widgets you need to build your search and navigation UX. Nevertheless, we know that it could never cover 100% of the use cases. That’s why it was designed with extensibility in mind. So you can easily build your own widget.

How to create the widget?

A widget is an UI element that lets the user interact with the parameters of the search. Every time the user interacts with the parameters through a widget, a new request will be sent to Algolia.

Programmatically speaking, a widget is just an object with a few methods on it:

  • init which is called before the first search
  • render which is called each time a new set results is returned by Algolia
  • getConfiguration which is called when the widgets wants to provide some configuration for the search (eg. facet definition)

init and render are the most commonly used in widget implementations and only one of them is required.

instantsearch.addWidget({
  init: function(parameters) {
    // as you will see in the next chapters, the helper let you modify the search parameters and trigger a new search
    var helper = parameters.helper;
  },
  render: function(parameters) {
    var helper = parameters.helper;
    // in this step, you also receive the latest results
    var results = parameters.results;
  },
});

And this is all there is to know about the widget anatomy. It uses the helper to manipulate the search (see the doc).

You can find more resources and pratical examples in the InstantSearch.js documentation.

Before creating your widget please refer to the following filtering options to check what is the one that best fit your needs.

Filtering options (API level)

The API exposes multiple ways to filter and narrow down a set of results.

Filter by Numerical Value

As seen earlier, Algolia supports indexing of numerical values (integers, doubles and boolean). This can be used for searching for products in a given price range, for example. To enable it, you need to have objects with numerical attributes (ensure your numerical values are not encoded as strings, for boolean we transform false as 0 and true as 1).

Considering the following record with its price attribute:

{
  "title": "Apple MacBook Pro 15.4-Inch Laptop with Retina Display",
  "price": 2594,
  "url": "http://www.amazon.com/Apple-MacBook-ME294LL-15-4-Inch-Display/dp/B0096VD85I"
}, {
  "title": "Apple iPhone 5S - 32GB",
  "price": 969.99,
  "url": "http://www.amazon.com/Apple-iPhone-5s-32GB-Space/dp/B00F3J4KYA"
},
// [...]

You can search with numeric conditions on the price. We support six operators: <, <=, =, >, >= and !=.

You can search with numeric conditions on the price. The method addNumericRefinement supports following operators: <, <=, =, >, >= and !=.

You can also mix OR and AND operators. The AND is automatically applied when adding multiple numeric refinements

// search for `apple` products which cost more than $100 and are in stock
helper.setQuery('apple');
helper.addNumericRefinement('stock_quatity', '>', 0);
helper.addNumericRefinement('price', '>', 100);
helper.search();

In the same way numeric refinements get added, they can be removed one by one using the removeNumericRefinement method. See the reference documentation on NumericRefinements with the JS Helper

Filter by Date

By default the engine doesn’t interpret strings following the ISO date format. To enable search by date, you must convert your dates into numeric values. We recommend using a unix timestamp as illustrated by the following record:

{
  "objectID": "myID1",
  "name": "Jimmy",
  "company": "Paint Inc.",
  "last_update": 1362873600 // UNIX timestamp as an integer
}

You can then use standard numeric operators in your search. You can even search by date range by combining two operators as illustrated by the following example:

// search by date between 2013-03-10 & 2013-04-20
helper.addNumericRefinement('last_update', '>=', 1362873600);
helper.addNumericRefinement('last_update', '<=', 1366416000);
helper.search();

Pro tip: In the case you need to index and filter on dates previous to the 1st of January 1970, you can convert the date following a rule like: year * 10000 + month * 100 + day. The 30th of March 1964 will then become 19640330.

Filter by Facets value

Conjunctive facets

Conjunctive facets are used to filter values from a faceted attribute. The filters set on an attribute are combined using an and, hence the conjunctive adjective. Re-using the same dataset, we can then do the following:

// Helper configuration
var helper = AlgoliasearchHelper(ALGOLIA_CLIENT, INDEX_NAME, {
  facets: ['categories']
});

// categories = 'Tablets' AND brand = 'Samsung'
helper.addFacetRefinement('categories', 'Tablets');
helper.addFacetRefinement('categories', 'Samsung');

helper.search();

Note: facet values are case sensitive.

The values that can be used for filtering are retrieved with the answer from Algolia. They are accessible using the getFacetValues methods on the SearchResults object.

Info: The menu widget relies on conjunctive faceting.

Disjunctive facets

Disjunctive facets are used to filter values from a faceted attribute. The filters set on an attribute are combined using an or, hence the disjunctive adjective. If we have a dataset of TV’s, and we have an attribute that defines the kind of tech used, we can then do the following:

// Helper configuration
var helper = AlgoliasearchHelper(ALGOLIA_CLIENT, INDEX_NAME, {
  facets: ['technology']
});

// technology = 'crt' OR technology = 'led' OR technology = 'plasma'
helper.addDisjunctiveFacetRefinement('technology', 'crt');
helper.addDisjunctiveFacetRefinement('technology', 'led');
helper.addDisjunctiveFacetRefinement('technology', 'plasma');

helper.search();

The values that can be used for filtering are retrieved with the answer from Algolia. They are accessible using the getFacetValues methods on the SearchResults object.

Info: The refinementList widget relies on disjunctive faceting.

Hierarchical facets

Hierarchical facets are disjunctive ones with the concept of facet levels. They are useful to build navigation menus.

In order to work, the hierarchical attribute, here called hierarchicalCategories, need to be formatted as in the following record:

{
  "objectID": 3325038,
  "name": "Apple - iPad mini 3 Wi-Fi 16GB - Gold",
  "hierarchicalCategories": {
    "lvl0": "Computers & Tablets",
    "lvl1": "Computers & Tablets > Tablets",
    "lvl2": "Computers & Tablets > Tablets > iPad"
  },
  ...
},
// [...]

You can then refine the lvl1

var helper = algoliasearchHelper(ALGOLIA_CLIENT, INDEX_NAME, {
  hierarchicalFacets: [{
    name: 'hierarchicalCategories',
    attributes: ['hierarchicalCategories.lvl0', 'hierarchicalCategories.lvl1']
  }]
});

helper.toggleRefinement('hierarchicalCategories', 'Computers & Tablets > Tablets');
helper.search();

The helper lets you declare multiple values per level, specify another separator, or use a different sort order for values. Learn more about all those options in the reference doc:

Info: The hierarchicalMenu widget relies on the disjunctive faceting.

Negative / Excluding facets

The facet exclusions are not a type of facets by themselves, they are conjunctive facets. They can be very useful to excludes results matching some facet values, like for instance when you build a search for food recipes, and you’d like to exclude the ones with certain ingredients for which the user is allergic.

See the conjunctive facets for more information on how to configure them.

Search for facet values

When there are many facet values for a given facet, it may be useful to search within them. For example, you may have dozens of brands for a given search. Rather than displaying all of the brands, it is often best to only display the most popular and add a search input to allow the user to search for the brand that they are looking for.

By design, the engine returns up to 100 matching facet values (10 by default), sorted in a descending way based on their facet count, whatever the match contains or not typos.

For a facet to be searchable, you first must declare it with the searchable() modifier in the attributesForFaceting index setting.

Considering the following object with its category attribute:

{
  "name": "iPhone 7 Plus",
  "brand": "Apple",
  "category": [
      "Mobile phones",
      "Electronics"
  ]
}

Once the category attribute added to attributesForFaceting index setting, as searchable, you can search for its facet values.

// Search within the "category" facet for values matching "phone"
index.searchForFacetValues('category', 'phone').then(function(content) {
  console.log(content);
});

index.searchForFacetValues('category', 'phane'); // works with typos too

index.searchForFacetValues('category', 'phane', 20); // up to 20 results

Facet values results are then available in the facetHits attribute.

// Output of the console.log(content);
{
  "facetHits": [
    {
      "value": "Mobile phones",
      "highlighted": "Mobile <em>phone</em>s",
      "count": 507
    },
    {
      "value": "Phone cases",
      "highlighted": "<em>Phone</em> cases",
      "count": 63
    }
  ]
}

As you can see, the method used for searching on facet values is different than the regular index.search(...) method used to perform regular search queries.

Important: Please note that facet values available may differ according to the current context of the search: current search query and/or active refinements passed to the helper.

Learn more about all options in the reference doc:

Info: The search for facet values logic is integrated in the refinementList widget.

Filter by Geo-location

Either by distance from a specific lat/long, or in a given bounding box / polygon, Algolia offers the possibility to filter using geo-search capabilities. Learn how to use geo-search.

Filter by Tags values

In the same way you can filter by facets values you have the possibility to filter by values declared in the _tags: [] attribute of the record. Tag filtering is mainly used for programmatic filtering, or in the case of Secured API Keys.

Did you find this page helpful?

We're always looking for advice to help improve our documentation! Please let us know what's working (or what's not!) - we're constantly iterating thanks to the feedback we receive.

Send us your suggestions!