Guides / Building Search UI / UI & UX patterns / Query Suggestions

Build a Query Suggestions UI with InstantSearch.js

You are currently reading the documentation for InstantSearch.js V4. Read our migration guide to learn how to upgrade from V3 to V4. You can still access the V3 documentation for this page.

To help users with their search, Algolia provides 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 a multi-index search for that.

This guide describes how to build a searchBox that displays a list of suggestions along with the associated categories. Once the user selects a suggestion, the engine will apply the query and the category.

If you’re building an autocomplete with Query Suggestions, you should use the Autocomplete library which lets you build a full-featured, accessible autocomplete experience.

Refine your results with the suggestions

The first step of this guide is to set up your custom autocomplete component. 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 your autocomplete component, you can create your multi-index search experience. The autocomplete component targets the index that contains the suggestions, and the rest of the widgets target the main index that holds the data. You can find more information about that in the guide to 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
// ...

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

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

search.addWidgets([
  instantsearch.widgets
    .index({ indexName: 'instant_search_demo_query_suggestions' })
    .addWidgets([
      autocomplete({
        container: $('#autocomplete'),
        onSelectChange({ query }) {
          search.helper.setQuery(query).search();
        },
      }),
    ]),
  instantsearch.widgets.hits({
    container: '#hits',
    templates: {
      // ...
    },
  }),
]);

search.start();

That’s it. You created an autocomplete multi-index search experience. Users can now select an autocomplete suggestion and use this suggestion to search the main index.

A typical use of autocomplete is to display relevant categories and suggestions. Then when a user selects 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 need to find them when a suggestion is selected. To do so, use the data-* attributes API available on DOM elements. You can store the category on the suggestion element 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 as for 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: it doesn’t render anything on the page (hence the name “virtual”). It’s handy when you have a situation where you have to manipulate the search state manually. If this is desired, refine a category without using 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 handles this logic.The final step is to apply the selected category to the search, and you can do that inside the onSelectChange callback provided to the autocomplete component. To update the search parameters, you can use the helper.

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

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

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

That’s it. 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 this example on GitHub.

Did you find this page helpful?