Instant Search Result Page - instantsearch.js


Instant search 1

In this tutorial, you will use Algolia to create an instant search results page where the whole page (results, filters and pagination) gets updated as you type, to provide end-users with a lightning fast search experience.

We’ll take the example of an e-commerce website and implement the search on the front-end using Algolia’s instantsearch.js UI library.

If you haven’t done so yet, now is the right time to read our How it Works guide. It gives you the big picture about Algolia and the role played by the back-end and the front-end in our approach to search.

With this tutorial, we want to:

  • Display results as you type,
  • Update filters and pagination as you type,
  • Provide 3 types of filters: regular facets, disjunctive facets and numeric filtering,
  • Add a “sort by” menu to have multiple sort criteria,
  • Display a “clear search” button when there are no results.

This tutorial has been built using the Best Buy Developer API. It’s based on 10,000 products extracted from their API.

Importing your Data

Algolia is a SaaS solution, which means you need to index your search-related data on Algolia’s servers for the search to work.

For this tutorial, we’ll create an index named instant_search and populate it with our 10,000 products. To create this index, go to your dashboard in the indices section and click the “New Index” button at the top right corner of the page.

Instant search 2

Name your index “instant_search” and validate

Once the index is created, you can upload the 10,000 products without writing a single line of code. Download this JSON file and use our web interface to do so.

Instant search 3

Click “upload file” to send the JSON file to your index

If you want to import your own data, you should have a look at the import guide.

Here’s what our records look like:

    "objectID": 3325038,
    "name": "Apple - iPad mini 3 Wi-Fi 16GB - Gold",
    "description": "The most advanced iPad mini is loaded with innovations like the Touch ID fingerprint sensor and Retina display.",
    "brand": "Apple",
    "categories": ["Computers & Tablets", "Tablets", "All Tablets"],
    "hierarchicalCategories": {
      "lvl0": "Computers & Tablets",
      "lvl1": "Computers & Tablets > Tablets",
      "lvl2": "Computers & Tablets > Tablets > iPad"
    "type": "Apple wifi",
    "price": 399.99,
    "price_range": "200 - 500",
    "image": "",
    "url": "",
    "popularity": 8593
  // other products [...]

As you can see, our records have 4 types of information:

  • Attributes you want to search in (name, description, brand, …)
  • Numerical values we can use to precise our ranking (popularity)
  • Attributes you want to filter on (price, categories, …)
  • Other attributes used to display the results (image)

Index configuration

There are several configuration options you can act on to tune your overall index relevancy. The most important ones are the searchable attributes and the attributes reflecting record popularity (the “custom ranking”).

Thus, our first step into building a great search page is to identify what will be made searchable, and how records will be ranked within a results set.

Searchable Attributes

The searchableAttributes parameter (formerly known as attributesToIndex) controls which attributes should be searchable. It is important to choose those wisely and to exclude any attribute whose only purpose is for displaying, filtering, or ranking.

For example, consider a link to an image: you want to store it and retrieve it for each result but it doesn’t make sense to have it be part of the searchable attributes. Doing so would alter your overall relevance by including irrelevant words into your search index.

Be aware that the order of the attributes in your searchableAttributes parameter is very important. Our engine uses this order to decide how to rank two results for which a keyword is found in different attributes.

For instance with searchableAttributes=['name', 'description'], if we receive the query “iPad”, products that have “iPad” in their name attribute will be ranked higher than those having the word “iPad” only in their description attribute.  

In this tutorial, we’ll define the following order of importance for our attributes:

  1. brand
  2. name
  3. categories
  4. description (unordered)

By default, a keyword matched at the beginning of an attribute is considered more important. You can disable this behavior by setting the unordered flag, like we did for the description attribute:

Instant search 4

Set searchableAttributes in the “Ranking” tab of your index

Custom Ranking

Algolia’s default ranking formula includes a customRanking criterion which allows you to add business metrics to the relevance calculation.

Which information should you use for the customRanking? Very easy: think about how you would want all your records ranked when the search box is empty. Amazon would probably want to show the products with the highest number of sales first, LinkedIn might want to show the people with the most connections, and Yelp could define a combination of the ratings and the number of reviews of each of their businesses.

In our case, we have a popularity attribute representing the number of units sold. You can use any numerical value that ranks the products: number of sales, number of views or number of likes, or even a score that you calculated before indexing.

By adding such attribute to the customRanking setting, search results will be built and sorted by combining the text-relevance criteria with the custom ranking to return the most relevant results.

Instant search 5

Add popularity to your custom ranking in the Ranking tab of your index

Note that depending on your metrics, you might want to change the sorting direction from DESC to ASC by clicking the button on the right of the attributes list.

Be aware that your attributes used in customRanking have to have numeric values. String values will get sorted alphabetically, which will not work well with numbers!

Other settings

There are many other settings that you can use to configure your index to your needs.

  • You can use ignorePlurals to consider plural forms of words equivalent to the singular form (for example car/cars will be considered as equal), thus making records containing plural forms as textually relevant as those containing singular forms.
  • Synonyms to consider some sets of words as equal at query time (eg. “red” and “vermilion”).
  • Use removeWordsIfNoResults to alter the query when no results are found. You can gradually remove words from the query until some results are found, or make all words optional in the query.

These are just a few examples. You can look at the full list in our documentation.

Multiple Sort Criteria

By default, the results ranking will be based on relevance by using the customRanking we’ve just set. To give more freedom to the user, we’d like to let them sort by price (ascending or descending) too, or by any other numerical value (timestamp, nb likes, …). This is doable by having different ranking strategies for your data.

However, to achieve the best performance possible, Algolia pre-computes the results ranking at indexing-time. This is an optimization to ensure you will always have outstanding performance at query time. The consequence of this approach is that you cannot change the sort criteria of an index at query time: each index has a unique ranking strategy.

Using replicas

The replicas setting is used to create replicas of a primary index. The content of those replicas will automatically be synchronized with their primary index, but they can have different configurations.

That’s how we will achieve the sort by feature: we will create multiple replicas with the same content, but with different ranking strategies. We’ll then target a different replica when sorting by relevance (main index), price descending (instant_search_price_desc replica), or price ascending (instant_search_price_asc replica).

So let’s create two replicas for this index that will take care of the price sorting:

Instant search 6

Setting replicas in the Replicas tab of your index

Note: there’s no mandatory naming pattern for replicas. However as a convention, replicas used for sorting are named using this pattern: <primary_index_name>_<sorting_attribute_name>_<direction>

When the replicas are created, they inherit the configuration settings of their primary index. We can then change those settings individually in each replicas configuration to change the ranking strategy (or other settings, depending on your use case).

To sort results by a specific numerical attribute, you just need to add this attribute at the top of your ranking formula and choose the correct sorting direction.

Go to your instant_search_price_asc index by selecting it in the top-left index selector of your dashboard, and scroll down to the bottom of the Ranking tab. There you can add price to the top of the Ranking Formula:

Instant search 7

Change the sorting direction to ASC by clicking the button on the right

You can then do the same for the instant_search_price_desc index, keeping the default DESC direction this time:

Instant search 8

String attributes cannot be added to the Ranking Formula criteria. To sort using a string attribute (to sort by name for instance), move the customRanking criterion at the top of your Ranking Formula and add your string attribute to the customRanking list.

Once that is done, set the typo-tolerance of the replica index to typo=min. This will keep the feature on, but only display results with the lowest count of typos: either 0 or 1.

Instant search 9

That’s it! We now have all the required indices to implement our sort by selector. We’ll get there soon.

Filters and facets

We’re going to use 3 different filters on our primary index: 

  • A facet (list of links) for the type attribute
  • A disjunctiveFacet (list of checkboxes) for the categories and brand attributes
  • A numericFilter (slider) for the price attribute.

We need to enable the faceting feature for those attributes in our index configuration.

Faceting setting of the index

attributesForFaceting is used to declare all the attributes that will be used as filters. In our case:

  • brand
  • type
  • categories
  • price

Instant search 10

Setting attributesForFaceting in the Display tab of the main index

Note that there’s no indication as to how those facets will be used: faceting, disjunctive faceting and numeric filtering are all requested and resolved at query time.

Configuration script

The entire configuration we have set using the Dashboard can of course be done programmatically via our API. It can prove useful to have a script ready to set an index configuration, if you automate your production processes for instance.

Here is the code equivalent to the configuration we have done manually above:

require 'algoliasearch'
Algolia.init("application_id" => "YourApplicationID", "api_key" => "YourAPIKey")
# Create instant_search index and set its settings
index ='instant_search')
settingsTask = index.set_settings({
  "searchableAttributes" => ["brand", "name", "categories", "unordered(description)"],
  "customRanking" => ["desc(popularity)"],
  "replicas" => ["instant_search_price_asc", "instant_search_price_desc"],
  "attributesForFaceting" => ["brand", "type", "categories", "price"]
# Wait for the task to be completed (to make sure replica indices are ready)
# Configure the replica indices'instant_search_price_asc').set_settings({
  "ranking" => ["asc(price)", "typo", "geo", "words", "proximity", "attribute", "exact", "custom"]
  "ranking" => ["desc(price)", "typo", "geo", "words", "proximity", "attribute", "exact", "custom"]

// Composer autoload -- assumes you have installed the 'algolia/algoliasearch-client-php' Composer package
// if you are not using composer: require_once 'path/to/algoliasearch.php';
require __DIR__ . '/vendor/autoload.php';
$client = new \AlgoliaSearch\Client("YourApplicationID", "YourAPIKey");
// Create instant_search index and set its settings
$index = $client->initIndex("instant_search");
$settingsTask = $index->setSettings(array(
  "searchableAttributes" => array("brand", "name", "categories", "unordered(description)"),
  "customRanking" => array("desc(popularity)"),
  "replicas" => array("instant_search_price_asc", "instant_search_price_desc"),
  "attributesForFaceting" => array("brand", "type", "categories", "price")
// Wait for the task to be completed (to make sure replica indices are ready)
// Configure the replica indices
  "ranking" => array("asc(price)", "typo", "geo", "words", "proximity", "attribute", "exact", "custom")
  "ranking" => array("desc(price)", "typo", "geo", "words", "proximity", "attribute", "exact", "custom")

var algoliasearch = require('algoliasearch');
var client = algoliasearch('YourApplicationID', 'YourAPIKey');
// Create instant_search index and set its settings
var index = client.initIndex('instant_search');
  searchableAttributes: ['brand', 'name', 'categories', 'unordered(description)'],
  customRanking: ['desc(popularity)'],
  replicas: ['instant_search_price_asc', 'instant_search_price_desc'],
  attributesForFaceting: ['brand', 'type', 'categories', 'price']
}, function(err, content) {
    throw err;
  // Wait for the setSettings task to finish (to make sure replica indices are ready)
  index.waitTask(content.taskID, function() {
    // Configure the replica indices
      ranking: ["asc(price)", "typo", "geo", "words", "proximity", "attribute", "exact", "custom"]
      ranking: ["desc(price)", "typo", "geo", "words", "proximity", "attribute", "exact", "custom"]

from algoliasearch import algoliasearch
client = algoliasearch.Client("YourApplicationID", 'YourAPIKey')
# Create instant_search index and set its settings
index = client.init_index('instant_search')
settingsTask = index.set_settings({
  "searchableAttributes": ["brand", "name", "categories", "unordered(description)"],
  "customRanking": ["desc(popularity)"],
  "replicas": ["instant_search_price_asc", "instant_search_price_desc"],
  "attributesForFaceting":["brand", "type", "categories", "price"]
# Wait for the task to be completed (to make sure replica indices are ready)
# Configure the replica indices
  "ranking": ["asc(price)", "typo", "geo", "words", "proximity", "attribute", "exact", "custom"]
  "ranking": ["desc(price)", "typo", "geo", "words", "proximity", "attribute", "exact", "custom"]
APIClient client = new ApacheAPIClientBuilder("YourApplicationID", "YourAPIKey");
// Create instant_search index and set its settings
Index<Product> index = client.initIndex("instant_search", Product.class);
  .setSettings(new IndexSettings()
    .setSearchableAttributes(Arrays.asList("brand", "name", "categories", "unordered(description)"))
    .setReplicas(Arrays.asList("instant_search_price_asc", "instant_search_price_desc"))
    .setAttributesForFaceting(Arrays.asList("brand", "type", "categories", "price"))
  .waitForCompletion(); //Wait for the task to be completed (to make sure replica indices are ready)
//Configure the replica indices
  .setSettings(new IndexSettings()
    .setRanking(Arrays.asList("asc(price)", "typo", "geo", "words", "proximity", "attribute", "exact", "custom"))
  .setSettings(new IndexSettings()
    .setRanking(Arrays.asList("desc(price)", "typo", "geo", "words", "proximity", "attribute", "exact", "custom"))
//For the DSL
import algolia.AlgoliaDsl._
//For basic Future support, you might want to change this by your own ExecutionContext
val client = new AlgoliaClient("YourApplicationID", "YourAPIKey")
// Create instant_search index and set its settings
client.execute {
  changeSettings of "instant_search" `with` IndexSettings(
    searchableAttributes = Some(Seq("brand", "name", "categories", "unordered(description)")),
    customRanking = Some(Seq(CustomRanking.desc("popularity"))),
    replicas = Some(Seq("instant_search_price_asc", "instant_search_price_desc")),
    attributesForFaceting = Some(Seq("brand", "type", "categories", "price"))
//Configure the replica indices
client.execute {
  changeSettings of "instant_search_price_asc" `with` IndexSettings(
    ranking = Some(Seq(Ranking.asc("price"), Ranking.typo, Ranking.geo, Ranking.words, Ranking.proximity, Ranking.attribute, Ranking.exact, Ranking.custom))
client.execute {
  changeSettings of "instant_search_price_desc" `with` IndexSettings(
    ranking = Some(Seq(Ranking.desc("price"), Ranking.typo, Ranking.geo, Ranking.words, Ranking.proximity, Ranking.attribute, Ranking.exact, Ranking.custom))
client := algoliasearch.NewClient("YourApplicationID", "YourAPIKey")
index := client.InitIndex("instant_search")
res, err := index.SetSettings(algoliasearch.Map{
	"searchableAttributes":     []string{"brand", "name", "categories", "unordered(description)"},
	"customRanking":         []string{"desc(popularity)"},
	"replicas":                []string{"instant_search_price_asc", "instant_search_price_desc"},
	"attributesForFaceting": []string{"brand", "type", "categories", "price"},
if err != nil {
	fmt.Println("Cannot set index settings")
if err = index.WaitTask(res.TaskID); err != nil {
	fmt.Println("Task not published")
res, err = client.InitIndex("instant_search_price_asc").SetSettings(algoliasearch.Map{
	"ranking": []string{"asc(price)", "typo", "geo", "words", "proximity", "attribute", "exact", "custom"},
res, err = client.InitIndex("instant_search_price_desc").SetSettings(algoliasearch.Map{
	"ranking": []string{"desc(price)", "typo", "geo", "words", "proximity", "attribute", "exact", "custom"},


For building the UI, we’ll use our instantsearch.js library. This library is a collection of widgets specifically designed to create the kind of instant search experience we are building here. Each widget handles a part of the UI (search box, hits, facets, …) and is kept in sync with the other widgets automatically.


At the top of the page, let’s add the necessary css files:

<link rel="stylesheet" href="">
<link rel="stylesheet" type="text/css" href="style.css">

Here we’re adding the default instantsearch.js styles, and our own CSS file to customize the widgets to our liking.

Let’s do the same with the required JavaScript files. Place them just before the closing tag of body:

<script src=""></script>
<script src="app.js"></script>

instantsearch.js default CSS file only contain the bare minimum rules to make it usable, but has no strong styling. To reproduce the tutorial theme, copy the content of the CSS file to your style.css, and put the associated images along in an img/ folder.

Widgets containers

Using instantsearch.js is as easy as creating some container elements in your HTML and telling the lib to initialize the different widgets in each of them.

Our first step will then be to create the HTML layout that will contain our widgets.

Header section

The header will contain our demo logo and the search box:

    <a href="." title="Home"><img src="img/instant_search_logo@2x.png"/></a>
    <div id="search-input"></div>
    <div id="search-input-icon"></div>

Main section

The main section of the page will contain the search results and the filtering/facets widgets, in a typical two-column layout. It goes right below the header:

  <div id="left-column">
    <div id="category" class="facet"></div>
    <div id="brand" class="facet"></div>
    <div id="price" class="facet"></div>
    <div id="type" class="facet"></div>
  <div id="right-column">
    <div id="sort-by-wrapper"><span id="sort-by"></span></div>
    <div id="stats"></div>
    <div id="hits"></div>
    <div id="pagination"></div>

The left column will host our facets: categories, brands, price and product types.

The right column will display the search results (called hits), a “sort by” menu and pagination links.

The stats &lt;div&gt; is here to display the total number of hits the current search returned.


instantsearch.js will render the widgets and keep them in sync for you. It also provides some default templates for every widget.

The template technology we have chosen is hogan.js, which is a clone of mustache.js.

Let’s configure the hits widget: we need to provide a custom template that will fit our context (products).

Hit template.

This template controls what a single search result looks like. Add it below the main section:

<script type="text/html" id="hit-template">
  <div class="hit">
    <div class="hit-image">
      <img src="{{image}}" alt="{{name}}">
    <div class="hit-content">
      <h3 class="hit-price">${{price}}</h3>
      <h2 class="hit-name">{{{}}}</h2>
      <p class="hit-description">{{{_highlightResult.description.value}}}</p>

Note that our template is wrapped in a <script type="text/html"> tag, so that we can “store” some HTML code without rendering it on the page.

The hit template will be rendered for each hit, using the hit’s data as the rendering context. Which means you have direct access to the different attributes that are indexed in your Algolia index.

You can also access some special attributes like _higlightResult that gives you access to the same attributes with HTML highlights on the matched texts within them.

In this template we’re using specific attributes of the BestBuy products objects and displaying them with highlighting whenever it makes sense.

We use three curly-braces {{{html}}} to disable HTML escaping and correctly use real HTML code.

No results template

This one is displayed when the current search has returned no results. It’s super easy:

<script type="text/html" id="no-results-template">
  <div id="no-results-message">
    <p>We didn't find any results for the search <em>"{{query}}"</em>.</p>
    <a href="." class="clear-all">Clear search</a>

We show an informative message, repeating the search query that failed to return any hit. To clear the search, we’re using a link that points to the current page without any parameter, effectively clearing the search.

JavaScript code

We have set up the HTML layout inside which our widgets will be rendered. It’s now time to actually use instantsearch.js to bring those to life!

You will need:

  • Your search credentials: the Application ID and API Key that you can get from the credentials tab of your dashboard
  • The name of the index you want to query

There are 3 simple steps to initialize an instantsearch.js app:

  1. Instantiate instantsearch.js
  2. Create and add the widgets you need to the instantsearch.js instance you have
  3. Start instantsearch.js

Creating an instantsearch.js instance

var search = instantsearch({
  // Replace with your own values
  appId: 'latency',
  apiKey: '6be0576ff61c053d5f9a3225e2a90f76', // search only API key, no ADMIN key
  indexName: 'instant_search',
  urlSync: true

Along with providing our Algolia credentials, we also activate the urlSync option. It will keep the browser url in sync and allow our users to copy paste urls corresponding to the current search state.

Creating widgets

The instantsearch.js library comes with a collection of widgets that are designed to work together in an instant search page. Each widget you need must be created and added to your instantsearch.js instance by using two methods:

  1. The instantsearch.widgets.nameOfWidget method to effectively create the widget
  2. The search.addWidget method of your instantsearch.js instance, to make this widget known by the synchronization mechanisms

Here’s how we create a searchBox and add it to instantsearch.js:

    container: '#search-input',
    placeholder: 'Search for products'

Each widget has a container option that defines where in your page the widget should be attached. Here we provide a CSS Selector but it can also be a reference to a DOMElement.

Instant search 11

searchBox widget

stats and hits

Next widgets to add are the hits and the stats widgets:

    container: '#hits',
    hitsPerPage: 10,
    templates: {
      item: getTemplate('hit'),
      empty: getTemplate('no-results')
    container: '#stats'

The hits widget will be rendered inside the <div id="hits"> container using the templates we have defined.

getTemplate is a small utility to get the underlying template. Here’s the function to copy at the bottom of your file:

function getTemplate(templateName) {
  return document.getElementById(templateName + '-template').innerHTML;

Next up is the sort menu widget:

    container: '#sort-by',
    autoHideContainer: true,
    indices: [{
      name: search.indexName, label: 'Most relevant'
    }, {
      name: search.indexName + '_price_asc', label: 'Lowest price'
    }, {
      name: search.indexName + '_price_desc', label: 'Highest price'

About the options:

  • autoHideContainer: true is used to automatically hide the widget when there are no results to display
  • indices is an the array of indices we want to use for sorting when the corresponding label is clicked in the select box

When the stats, hits and sortBySelector widget are added, we can see this:

Instant search 12

stats, hits and sortBySelector widgets in action


Now let’s add a pagination widget:

    container: '#pagination'

Instant search 13

pagination widget

refinementList, rangeSlider, menu

That’s already a good instant search page. We now need to add the filtering widgets.

They all have a similar API, add them to your widgets array:

    container: '#category',
    attributeName: 'categories',
    limit: 10,
    sortBy: ['isRefined', 'count:desc', 'name:asc'],
    operator: 'or',
    templates: {
      header: '<h5>Category</h5>'
    container: '#brand',
    attributeName: 'brand',
    limit: 10,
    sortBy: ['isRefined', 'count:desc', 'name:asc'],
    operator: 'or',
    templates: {
      header: '<h5>Brand</h5>'
    container: '#price',
    attributeName: 'price',
    templates: {
      header: '<h5>Price</h5>'
    container: '#type',
    attributeName: 'type',
    limit: 10,
    sortBy: ['isRefined', 'count:desc', 'name:asc'],
    templates: {
      header: '<h5>Type</h5>'

Instant search 14

refinementList, slider and menu widgets

The refinementList widget is used to display a list of facet values for a particular facet. Here we have one for the categories attribute, and another one for the brand attribute.

In both instances we are using operator: 'or'. It means that if several values are selected, all the displayed results will be part of at least one of the categories. The default value is and, which means that all displayed values will be part of all the selected value. 

With or, checking more and more values will increase the number of results, while with and it will narrow them down.

The rangeSlider lets the user filter results for which an attribute falls within the specified numerical range. This is typically used for price filtering.

The menu widget provides a list of facet values where only one value can be selected at a time. Like a radio button, when a new value is selected, the previous one is deselected. It is used here to allow the user to display only products of a specific type.

All these widgets also provide a header template that will be used to render the widget’s title.

Now that we’ve added all our widgets, let’s start the whole interface:


See it live

Open the index.html file in your favorite web browser and play with your new instant search results page.

The instantsearch.js library has many more widgets, have a look at the dedicated website.

Use the buttons below to check out our live version, or download the complete code from Github.

Instant search 15