InstantSearch / InstantSearch.js / V3 / Guides

Query Suggestions | InstantSearch.js V3 (Deprecated)

This version of InstantSearch.js has been deprecated in favor of the latest version of InstantSearch.js.

Algolia provides a feature called Query Suggestions. This feature creates an index with the best queries done by the users. You can then use this index to propose suggestions to your users as they’re typing into the searchBox. Once you’ve configured the generation of the Query Suggestions index, you need to query this index as well. You can use multi-index search for that.

This guide covers the use case where a searchBox displays a list of suggestions along with the associated categories. Once the user selects a suggestion both the query and the category will be applied.

This implementation leverages the autocomplete connector to display the list of suggestions. The guide doesn’t cover how to integrate an autocomplete with InstantSearch.js in detail. The autocomplete guide already has a dedicated section on that topic.

Refine your results with the suggestions

The first step of this guide is to setup the custom autocomplete component. To implement it you can use the library Selectize that provides an API to create an autocomplete menu. Once you have this component, you need to wrap it with the autocomplete connector. You can find more information in the guide on autocomplete.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
const autocomplete = instantsearch.connectors.connectAutocomplete(
  ({ indices, refine, widgetParams }, isFirstRendering) => {
    const { container, onSelectChange } = widgetParams;

    if (isFirstRendering) {
      container.html('<select id="ais-autocomplete"></select>');

      container.find('select').selectize({
        options: [],
        valueField: 'query',
        labelField: 'query',
        highlight: false,
        onType: refine,
        onBlur() {
          refine(this.getValue());
        },
        onChange(value) {
          refine(value);
          onSelectChange({
            query: value,
          });
        },
        score() {
          return () => 1;
        },
        render: {
          option({ query }) {
            return `
              <div class="option">
                ${query}
              </div>
            `;
          },
        },
      });

      return;
    }

    const [select] = container.find('select');

    select.selectize.clearOptions();
    indices.forEach(({ results }) => {
      results.hits.forEach(hit => select.selectize.addOption(hit));
    });
    select.selectize.refreshOptions(select.selectize.isOpen);
  }
);

Now that you have the autocomplete component you can create the multi-index search experience. The autocomplete targets the index that contains the suggestions. The rest of the widgets target the main index that holds the data. You can find more information about that in the guide about multi-index search.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// ...

const searchClient = algoliasearch(
  'YourApplicationID',
  'YourAdminAPIKey'
);

const suggestions = instantsearch({
  indexName: 'instant_search_demo_query_suggestions',
  searchClient,
});

// ...

suggestions.addWidgets([
  autocomplete({
    container: $('#autocomplete'),
    onSelectChange({ query }) {
      search.helper.setQuery(query).search();
    },
  })
]);

const search = instantsearch({
  indexName: 'instant_search',
  searchClient,
});

search.addWidgets([
  instantsearch.widgets.hits({
    container: '#hits',
    templates: {
      // ...
    },
  })
]);

suggestions.start();
search.start();

That sets up the autocomplete multi-index search experience. You should be able to select a suggestion from the autocomplete and use this suggestion to search into the main index.

A common pattern with an autocomplete of suggestions is to display the relevant categories along with the suggestions. Then when a user select a suggestion both the suggestion and the associated category are used to refine the search. For this example the relevant categories are stored on the suggestions records. You have to update the render function to display the categories with the suggestions. For simplicity and brevity of the code, assume that all suggestions have categories, but this isn’t the case in the actual dataset. Take a look at the complete example to see the actual implementation.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
container.find('select').selectize({
  // ...
  render: {
    option(item) {
      const [category] = item.instant_search.facets.exact_matches.categories;

      return `
        <div class="option">
          ${item.query} in <i>${category.value}</i>
        </div>
      `;
    },
  },
});

Now that you can display the categories, you must be able to find them when a suggestion is selected. To do so, you can use the data-* attributes API available the DOM elements. You can store the category on the suggestion element to be able to find it when the suggestion is selected.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
container.find('select').selectize({
  // ...
  render: {
    option(item) {
      const [category] = item.instant_search.facets.exact_matches.categories;

      return `
        <div class="option" data-category="${category.value}">
          ${item.query} in <i>${category.value}</i>
        </div>
      `;
    },
  },
});

The last step is to retrieve the value stored on the element once the suggestion is selected. You can do that inside the onChange callback. You can use the same strategy than the query to provide the category back to the main instance.

1
2
3
4
5
6
7
8
9
10
container.find('select').selectize({
  // ...
  onChange(value) {
    refine(value);
    onSelectChange({
      category: this.getOption(value).data('category'),
      query: value,
    });
  },
});

The last section of the guide explains how to use the related categories on the main search. The first step is to create a virtual widget. This widget is only used for filtering and it doesn’t render anything on the page (hence the name “virtual”). It’s handy when you have a situation where you have to manually manipulate the search state. This is exactly what’s called for: refine a category without using a built-in widgets like menu or refinementList.

1
2
3
4
5
6
7
8
9
10
11
const virtualRefinementList = instantsearch.connectors.connectRefinementList(
  () => null
);

// ...

search.addWidgets([
  virtualRefinementList({
    attribute: 'categories',
  })
]);

Now you can manually update the search state without worrying about the setup for the facets - the virtual widget handle this logic. The final step is to apply the selected category to the search. You can do that inside the onSelectChange callback provided to the autocomplete component. You can leverage the helper to update the search parameters.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
suggestions.addWidgets([
  autocomplete({
    container: $('#autocomplete'),
    onSelectChange({ query, category }) {
      search.helper
        .setQuery(query)
        .removeDisjunctiveFacetRefinement('categories');

      if (category) {
        search.helper.addDisjunctiveFacetRefinement('categories', category);
      }

      search.helper.search();
    },
  })
]);

Now when a suggestion is selected both the query and the category are applied to the main search. You can find the complete source code of the example on GitHub.

Did you find this page helpful?