Concepts / Building Search UI / Query suggestions
May. 10, 2019

Query Suggestions

Overview

In order to help users with their search, 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 are typing into the SearchBox. The great thing with this feature is that once you’ve configured the generation of the index, it’s just about querying another index and you can easily use multi-index search to for that.

In this guide we will cover the use case where a SearchBox displays a list of suggestions along with the associated categories. Once the user select a suggestion both the query and the category will be applied.

To build our list of suggestions we leverage the connector Autocomplete. We won’t cover in too much details how to integrate an autocomplete with React InstantSearch. The autocomplete guide has already a dedicated section on that topic.

Refine your results with the suggestions

The first step of this guide is to setup our custom autocomplete component. To implement it we use the library React Autosuggest that provides a component to create an autocomplete menu. Once we have it, we have to 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 we have our autocomplete component we can create our multi-index search experience. The autocomplete targets the index that contains the suggestions. The rest of the widgets target the main index that holds our 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! We have setup our 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. We have to update our render function to display the categories with the suggestions. For simplicity and brevity of the code we assume that all suggestions have categories, but this is not 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 we are able to display the categories we can use them to refine the main search. We are gonna use the same strategy than the query. We’ll 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

Our list of suggestions is now tied to one category. But this is not always what users want. What if we want to select a suggestion for all the categories? Not only the one that is linked to a suggestion. A possible solution could be to pick the most relevant suggestion and use it to search inside all the categories. Let’s see how we can implement such solution.

The first step is to create a duplicate of the most relevant suggestion with a special value for the category. This value will be used to differentiate it from the ones that actually exist. We can duplicate the suggestion inside the render of the 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 contains now one more item. We have to update the renderSuggestion method to correctly display the label for the new suggestion. Because right now since we explicitly provide a specific 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 we already did for the renderSuggestion method we have to update the onSuggestionSelected method to correctly set the appropriate filters. When the selected category is ALL_CATEGORIES we want to 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! In this guide you learn how to setup 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, you can find a source code example here as well as a demo

Did you find this page helpful?