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

Build a Query Suggestions UI with React InstantSearch

We released React InstantSearch Hooks, a new InstantSearch library for React. We recommend using React InstantSearch Hooks in new projects or upgrading from React InstantSearch.

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 shows how to use a SearchBox to display a list of suggestions and their 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. This is the recommended way of building an autocomplete search with Algolia.

Refine your results with the suggestions

The first step is to set up a custom autocomplete component with the React Autosuggest library. You must then wrap the component 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
49
50
51
52
53
54
55
56
57
58
59
60
import React, { Component } from 'react';
import { Highlight, connectAutoComplete } from 'react-instantsearch-dom';
import AutoSuggest from 'react-autosuggest';

class Autocomplete extends Component {
  state = {
    value: this.props.currentRefinement,
  };

  onChange = (_, { newValue }) => {
    if (!newValue) {
      this.props.onSuggestionCleared();
    }

    this.setState({
      value: newValue,
    });
  };

  onSuggestionsFetchRequested = ({ value }) => {
    this.props.refine(value);
  };

  onSuggestionsClearRequested = () => {
    this.props.refine();
  };

  getSuggestionValue(hit) {
    return hit.query;
  }

  renderSuggestion(hit) {
    return <Highlight attribute="query" hit={hit} tagName="mark" />;
  }

  render() {
    const { hits, onSuggestionSelected } = this.props;
    const { value } = this.state;

    const inputProps = {
      placeholder: 'Search for a product...',
      onChange: this.onChange,
      value,
    };

    return (
      <AutoSuggest
        suggestions={hits}
        onSuggestionsFetchRequested={this.onSuggestionsFetchRequested}
        onSuggestionsClearRequested={this.onSuggestionsClearRequested}
        onSuggestionSelected={onSuggestionSelected}
        getSuggestionValue={this.getSuggestionValue}
        renderSuggestion={this.renderSuggestion}
        inputProps={inputProps}
      />
    );
  }
}

export default connectAutoComplete(Autocomplete);

Now that you have your 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 your data. You can find more information in the autocomplete guide.

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
49
50
51
52
53
54
import React, { Component } from 'react';
import algoliasearch from 'algoliasearch/lite';
import { InstantSearch, Hits, connectSearchBox } from 'react-instantsearch-dom';
import Autocomplete from './Autocomplete';

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

const VirtualSearchBox = connectSearchBox(() => null);

class App extends Component {
  state = {
    query: '',
  };

  onSuggestionSelected = (_, { suggestion }) => {
    this.setState({
      query: suggestion.query,
    });
  };

  onSuggestionCleared = () => {
    this.setState({
      query: '',
    });
  };

  render() {
    const { query } = this.state;

    return (
      <div>
        <InstantSearch
          searchClient={searchClient}
          indexName="instant_search_demo_query_suggestions"
        >
          <Autocomplete
            onSuggestionSelected={this.onSuggestionSelected}
            onSuggestionCleared={this.onSuggestionCleared}
          />
        </InstantSearch>

        <InstantSearch searchClient={searchClient} indexName="instant_search">
          <VirtualSearchBox defaultRefinement={query} />
          <Hits />
        </InstantSearch>
      </div>
    );
  }
}

export default App;

That’s it. You have set up your autocomplete multi-index search experience, and users can now select a suggestion and use it to search the main index.

A typical use of autocomplete is to display both 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 must update your 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
class Autocomplete extends Component {
  // ...

  renderSuggestion(hit) {
    const [category] = hit.instant_search.facets.exact_matches.categories;

    return (
      <span>
        <Highlight attribute="query" hit={hit} tagName="mark" /> in{' '}
        <i>{category.value}</i>
      </span>
    );
  }
}

Now that you can display categories, you can use them to refine the main search. Use a virtual RefinementList widget with a defaultRefinement to apply the related category on the main 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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
import { /* ... */ connectRefinementList } from 'react-instantsearch-dom';

const VirtualRefinementList = connectRefinementList(() => null);

class App extends Component {
  state = {
    query: '',
    categories: [],
  };

  onSuggestionSelected = (_, { suggestion }) => {
    const [category] = suggestion.instant_search.facets.exact_matches.categories;

    this.setState({
      query: suggestion.query,
      categories: [category.value],
    });
  };


  onSuggestionCleared = () => {
    this.setState({
      query: '',
      categories: [],
    });
  };

  render() {
    const { query, categories } = this.state;

    return (
      <div>
        <InstantSearch
          searchClient={searchClient}
          indexName="instant_search_demo_query_suggestions"
        >
          <Autocomplete
            onSuggestionSelected={this.onSuggestionSelected}
            onSuggestionCleared={this.onSuggestionCleared}
          />
        </InstantSearch>

        <InstantSearch searchClient={searchClient} indexName="instant_search">
          <VirtualSearchBox defaultRefinement={query} />
          <VirtualRefinementList
            attribute="categories"
            defaultRefinement={categories}
          />

          <Hits />
        </InstantSearch>
      </div>
    );
  }
}

export default App;

That’s it. Now when a suggestion is selected, both the query and the category are applied to the main search.

Select a suggestion for all categories

Your list of suggestions is now tied to one category. But this isn’t always what users want.

What if you want to select a suggestion for all categories (not just the one linked to a suggestion)? A solution could be to pick the most relevant suggestion and use it to search inside all the categories.

The first step is to duplicate of the most relevant suggestion with a unique value for the category. This value will differentiate it from the ones that actually exist. The suggestion can be duplicated inside 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
class Autocomplete extends Component {
  // ...

  createMostRelevantSuggestionForAllCategories(hit) {
    return {
      ...hit,
      instant_search: {
        ...hit.instant_search,
        facets: {
          ...hit.instant_search.facets,
          exact_matches: {
            ...hit.instant_search.facets.exact_matches,
            categories: [{ value: 'ALL_CATEGORIES' }],
          },
        },
      },
    };
  }

  render() {
    // ...

    const [suggestion] = hits;
    const suggestionsWithAllCategories = suggestion
      ? [this.createMostRelevantSuggestionForAllCategories(suggestion)]
      : [];

    return (
      <AutoSuggest
        suggestions={[...suggestionsWithAllCategories, ...hits]}
        // ...
      />
    );
  }
}

This list of suggestions now contains one more item. You must now update the renderSuggestion method to correctly display the label for the new suggestion (since you’ve explicitly provided a placeholder for the value, the list will display: “ALL_CATEGORIES” rather than “All categories”).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Autocomplete extends Component {
  // ...

  renderSuggestion(hit) {
    const [category] = hit.instant_search.facets.exact_matches.categories;

    return (
      <span>
        <Highlight attribute="query" hit={hit} tagName="mark" /> in{' '}
        <i>
          {category.value === 'ALL_CATEGORIES'
            ? 'All categories'
            : category.value}
        </i>
      </span>
    );
  }
}

The final step of this guide is to handle the selection of the category with the placeholder. Like you already did for the renderSuggestion method, you must update the onSuggestionSelected method to set the appropriate filters correctly. When the selected category is ALL_CATEGORIES, you should use an empty array for the defaultRefinement.

1
2
3
4
5
6
7
8
9
10
11
12
class App extends Component {
  // ...

  onSuggestionSelected = (_, { suggestion }) => {
    const [category] = suggestion.instant_search.facets.exact_matches.categories;

    this.setState({
      query: suggestion.query,
      categories: category.value !== 'ALL_CATEGORIES' ? [category.value] : [],
    });
  };
}

That’s it. You have set up a query suggestions autocomplete linked to a results page.

You can find the complete source code of the example on GitHub. Alternatively, if you are using React Native, check out this source code example and the demonstration.

Did you find this page helpful?