UI Libraries / Autocomplete / Integrate Autocomplete with Vue InstantSearch

Integrate Autocomplete with Vue InstantSearch

When you think of search experiences on sites like Amazon (ecommerce) or YouTube (media), you may notice that both sites use an autocomplete experience. It’s the autocomplete, and not just a search input, that powers the search page.

If you have an existing Vue InstantSearch implementation, you can create a similar experience by adding Autocomplete to your Vue InstantSearch application. Adding Autocomplete to an existing Vue InstantSearch implementation lets you enhance the search experience and create a richer, more contextual search. You can use context from the current user and how they interacted with your site, save their recent searches, provide suggested queries, and more. This autocomplete can work as a rich search box in a search page, and a portable all-in-one search experience anywhere else on your site.

This guide shows you how to integrate Autocomplete with Vue InstantSearch on your site.

Preview

This guide starts from a brand new Vue InstantSearch application, but you can adapt it to integrate Autocomplete in your existing implementation.

Creating a search page with Vue InstantSearch

First, begin with some boilerplate for the InstantSearch implementation. The easiest way to achieve that is to use create-instantsearch-app, a command-line utility that helps get your app started quickly.

$
$
$
$
$
$
$
npx create-instantsearch-app@latest autocomplete-vue-instantsearch \
--template "Vue InstantSearch with Vue 3" \
--app-id "latency" \
--api-key "6be0576ff61c053d5f9a3225e2a90f76" \
--index-name instant_search \
--attributes-to-display name,description \
--attributes-for-faceting categories

The template uses a two-column layout with categories on the left and a search box, hits, and a pagination widget on the right.

Next, set up InstantSearch to enable routing by using the default history router, which you want to reuse in your Autocomplete integration. This lets InstantSearch understand query parameters from the URL to derive its state.

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
<template>
  <!-- ... -->
  <ais-instant-search
    :search-client="searchClient"
    index-name="instant_search"
    :routing="routing"
  >
    <!-- ... -->
  </ais-instant-search>
  <-- ... -->
</template>

<script>
  import algoliasearch from 'algoliasearch/lite';
  import { history } from 'instantsearch.js/es/lib/routers';
  import { singleIndex } from 'instantsearch.js/es/lib/stateMappings';

  export default {
    data() {
      return {
        searchClient: algoliasearch('latency', '6be0576ff61c053d5f9a3225e2a90f76'),
        routing: {
          router: history(),
          stateMapping: singleIndex('instant_search'),
        },
      };
    },
  };
</script>

You should now have a working Vue InstantSearch application with routing.

Vue InstantSearch ships with a <ais-search-box> component, but it doesn’t offer autocomplete features like those seen on YouTube and Amazon. Instead, you can replace the <ais-search-box> with Algolia’s Autocomplete.

With Vue, you can store all the logic of Autocomplete in a single file, and expose it as a component. Start by creating a file called Autocomplete.vue. As Autocomplete needs to interact with the InstantSearch state, you can add a mixin to transform it into a custom Vue InstantSearch widget.

1
2
3
4
5
6
7
8
9
10
11
12
<template>
  <div ref="autocompleteContainer"></div>
</template>

<script lang="jsx">
  import { createWidgetMixin } from 'vue-instantsearch/vue3/es';
  import { connectSearchBox } from 'instantsearch.js/es/connectors';

  export default {
    mixins: [createWidgetMixin({ connector: connectSearchBox })],
  };
</script>

You can now add this component into your app.

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
<template>
  <!-- ... -->
  <ais-instant-search
    :search-client="searchClient"
    index-name="instant_search"
    :routing="routing"
  >
    <!-- ... -->
    <div class="searchbox">
      <Autocomplete />
    </div>
    <!-- ... -->
  </ais-instant-search>
  <!-- ...-->
</template>

<script>
    /* ... */
    import Autocomplete from './Autocomplete.vue';

    export default {
      /* ... */
      components: { Autocomplete },
    }
</script>

Next, you can initialize Autocomplete in the mounted() hook of the component’s lifecycle. Since it behaves as a custom InstantSearch widget, you have direct access to the underlying InstantSearch instance.

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
<script lang="jsx">
  import { h as createElement, Fragment, render } from 'vue';

  import { createWidgetMixin } from 'vue-instantsearch/vue3/es';
  import { connectSearchBox } from 'instantsearch.js/es/connectors';
  import { autocomplete } from '@algolia/autocomplete-js';

  import '@algolia/autocomplete-theme-classic';

  import { INSTANT_SEARCH_INDEX_NAME } from './constants';

  export default {
    mixins: [createWidgetMixin({ connector: connectSearchBox })],
    mounted() {
      const { instantSearchInstance } = this;

      // Set the InstantSearch index UI state from external events
      function setInstantSearchUiState({ query }) {
        instantSearchInstance.setUiState((uiState) => ({
          ...uiState,
          [INSTANT_SEARCH_INDEX_NAME]: {
            ...uiState[INSTANT_SEARCH_INDEX_NAME],
            // We reset the page when the search state changes
            page: 1,
            query,
          },
        }));
      }

      const initialState = 
        instantSearchInstance.mainIndex.getHelper()?.state || {};

      this.autocompleteInstance = autocomplete({
        container: this.$refs.autocompleteContainer,
        placeholder: 'Search for products',
        detachedMediaQuery: 'none',
        initialState: { query: initialState.query || '' },
        onSubmit({ state }) {
          setInstantSearchUiState({ query: state.query });
        },
        onReset() {
          setInstantSearchUiState({ query: '' });
        },
        onStateChange({ prevState, state }) {
          if (prevState.query !== state.query) {
            setInstantSearchUiState({ query: state.query });
          }
        },
        // Use Vue implementation of createElement and Fragment
        // instead of those provided with Autocomplete
        renderer: { createElement, Fragment, render },
      });
    },
    beforeUnmount() {
      this.autocompleteInstance?.destroy();
    }
  };
</script>

You can now remove the original <ais-search-box> component from your Vue InstantSearch implementation. This replaces the InstantSearch search box with Autocomplete, and acts exactly like before. But you can now add many more interesting features.

Adding recent searches

When you search on YouTube or Google and come back to the search box later on, the autocomplete displays your recent searches. This pattern lets users quickly access content by using the same path they took to find it in the first place.

Add recent searches to Autocomplete with the @algolia/autocomplete-plugin-recent-searches package. It exposes a createLocalStorageRecentSearchesPlugin function to let you create a recent searches plugin.

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
<script lang="jsx">
  import { createLocalStorageRecentSearchesPlugin } from '@algolia/autocomplete-plugin-recent-searches';

  export default {
    /* ... */
    mounted() {
      /* ... */
      const recentSearchesPlugin = createLocalStorageRecentSearchesPlugin({
        key: 'instantsearch',
        limit: 3,
        transformSource({ source }) {
          return {
            ...source,
            onSelect({ item }) {
              setInstantSearchUiState({ query: item.label });
            },
          };
        },
      });

      /* ... */
      this.autocompleteInstance = autocomplete({
        /* ... */

        // You want recent searches to appear with an empty query
        openOnFocus: true,
        // Add the recent searches plugin
        plugins: [recentSearchesPlugin],

        /* ... */
      });
    },
  };
</script>

Some of this code is abstracted in the following sections to simplify the examples.

Since the recentSearchesPlugin reads from localStorage, you can’t see any recent searches until you perform at least one query. To submit a search, make sure to press Enter on the query. Once you do, you’ll see it appear as a recent search.

Adding Query Suggestions

The most typical pattern you can see on every autocomplete is suggestions. They’re predictions of queries that match what the user is typing and are guaranteed to return results. For example, when typing “how to” in Google, the search engine suggests matching suggestions for the user to complete their query. It’s beneficial on mobile devices, where typing is more demanding than a physical keyboard.

Autocomplete lets you add Query Suggestions with the @algolia/autocomplete-plugin-query-suggestions package. It exposes a createQuerySuggestionsPlugin function to let you create a Query Suggestions plugin.

This plugin requires a Query Suggestions index.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<script>
  import { history } from 'instantsearch.js/es/lib/routers';
  import { singleIndex } from 'instantsearch.js/es/lib/stateMappings';

  import Autocomplete from './Autocomplete.vue';
  import { INSTANT_SEARCH_INDEX_NAME } from './constants';
  import { searchClient } from './searchClient';

  export default {
    components: { Autocomplete },
    data() {
      return {
        searchClient,
        indexName: INSTANT_SEARCH_INDEX_NAME,
        routing: {
          router: history(),
          stateMapping: singleIndex(INSTANT_SEARCH_INDEX_NAME),
        },
      };
    },
  };
</script>

Debouncing search results

Having two sets of results update as you type generates many UI flashes. This is distracting for the user because two distinct sections compete for their attention.

You can mitigate this problem by debouncing search results.

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
<script>
  /* ... */

  function debounce(fn, time) {
    let timerId;

    return function(...args) {
      if (timerId) {
        clearTimeout(timerId);
      }

      timerId = setTimeout(() => fn(...args), time);
    };
  }

  export default {
    /* ... */
    mounted() {
      /* ... */
      const debouncedSetInstantSearchUiState = debounce(setInstantSearchUiState, 500);

      /* ... */
      this.autocompleteInstance = autocomplete({
        /* ... */
        onStateChange({ prevState, state }) {
          if (prevState.query !== state.query) {
            debouncedSetInstantSearchUiState({ query: state.query });
          }
        },
      });
    },
  };
</script>

Supporting categories in Query Suggestions

A key feature of Autocomplete is to pre-configure your InstantSearch page. The Query Suggestions plugin supports categories that you can leverage to refine the query and the category in a single interaction. This pattern brings users to the right category without interacting with the ais-hierarchical-menu widget, only with the autocomplete.

First, you need to refine on categories, and support categories in the helpers you created at the beginning.

1
2
3
4
5
6
7
8
9
10
<template>
  <!-- ... -->
  <ais-hierarchical-menu
    :attributes="[
      'hierarchicalCategories.lvl0',
      'hierarchicalCategories.lvl1',
    ]"
  />
  <!-- ... -->
</template>

Then, you can update the plugins to forward the category to these helpers.

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
61
62
63
<script lang="jsx">
  /* ... */
  export default {
    /* ... */
    mounted() {
      /* ... */
      const recentSearchesPlugin = createLocalStorageRecentSearchesPlugin({
        key: 'instantsearch',
        limit: 3,
        transformSource({ source }) {
          return {
            ...source,
            onSelect({ item }) {
              setInstantSearchUiState({
                query: item.label,
                category: item.category,
              });
            },
          };
        },
      });

      const querySuggestionsPlugin = createQuerySuggestionsPlugin({
        searchClient,
        indexName: INSTANT_SEARCH_QUERY_SUGGESTIONS,
        getSearchParams() {
          // This creates a shared `hitsPerPage` value once the duplicates
          // between recent searches and Query Suggestions are removed
          return recentSearchesPlugin.data.getAlgoliaSearchParams({
            hitsPerPage: 6,
          });
        },
        // Add categories to the suggestions
        categoryAttribute: [
          INSTANT_SEARCH_INDEX_NAME,
          'facets',
          'exact_matches',
          INSTANT_SEARCH_HIERARCHICAL_ATTRIBUTE,
        ],
        transformSource({ source }) {
          return {
            ...source,
            sourceId: 'querySuggestionsPlugin',
            onSelect({ item }) {
              setInstantSearchUiState({
                query: item.query,
                category: item.__autocomplete_qsCategory,
              });
            },
            getItems(params) {
              // We don't display Query Suggestions when there's no query
              if (!params.state.query) {
                return [];
              }

              return source.getItems(params);
            },
          };
        },
      });
    },
  };
</script>

Finally, you can implement the onReset function on your Autocomplete instance to also reset the InstantSearch category.

1
2
3
4
5
6
autocomplete({
  /* ... */
  onReset() {
    setInstantSearchUiState({ query: '', resetCategory: true });
  },
})

Adding contextual Query Suggestions

For an even richer Autocomplete experience, you can pick up the currently active InstantSearch category and provide suggestions for both this specific category and others. This pattern lets you reduce the search scope to the current category, like an actual department store, or broaden the suggestions to get out of the current category.

Query Suggestions with current InstantSearch category

First, make sure to set your category attribute as a facet in your Query Suggestions index. In this demo, the attribute to facet is instant_search.facets.exact_matches.hierarchicalCategories.lvl0.value.

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
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
<script lang="jsx">
  /* ... */
  export default {
    /* ... */
    mounted() {
      /* ... */
      // Get the current category from InstantSearch
      function getInstantSearchCurrentCategory() {
        const indexRenderState = instantSearchInstance.renderState[INSTANT_SEARCH_INDEX_NAME];
        const refinedCategory = indexRenderState?.hierarchicalMenu?.[
          INSTANT_SEARCH_HIERARCHICAL_ATTRIBUTE
        ]?.items?.find(({ isRefined }) => isRefined);

        return refinedCategory?.value;
      }

      /* ... */

      // Query Suggestions plugin for the current category
      const querySuggestionPluginInCategory = createQuerySuggestionsPlugin({
        searchClient,
        indexName: INSTANT_SEARCH_QUERY_SUGGESTIONS,
        getSearchParams() {
          const currentCategory = getInstantSearchCurrentCategory();
          return recentSearchesPlugin.data.getAlgoliaSearchParams({
            hitsPerPage: 3,
            facetFilters: [
              `${INSTANT_SEARCH_INDEX_NAME}.facets.exact_matches.${INSTANT_SEARCH_HIERARCHICAL_ATTRIBUTE}.value:${currentCategory}`,
            ],
          });
        },
        transformSource({ source }) {
          const currentCategory = getInstantSearchCurrentCategory();
          return {
            ...source,
            sourceId: 'querySuggestionsPluginInCategory',
            getItems(params) {
              if (!currentCategory) {
                return [];
              }

              return source.getItems(params);
            },
            templates: {
              ...source.templates,
              header({ items }) {
                if (!items.length) {
                  return null;
                }

                return (
                  <Fragment>
                    <span className="aa-SourceHeaderTitle">
                      In {currentCategory}
                    </span>
                    <span className="aa-SourceHeaderLine"></span>
                  </Fragment>
                );
              },
            },
          };
        },
      });

      // Query Suggestions plugin for the other categories
      const querySuggestionsPlugin = createQuerySuggestionsPlugin({
        /* ... */
        getSearchParams() {
          const currentCategory = getInstantSearchCurrentCategory();
          if (!currentCategory) {
            return recentSearchesPlugin.data.getAlgoliaSearchParams({
              hitsPerPage: 6,
            });
          }

          return recentSearchesPlugin.data.getAlgoliaSearchParams({
            hitsPerPage: 3,
            facetFilters: [
              `${INSTANT_SEARCH_INDEX_NAME}.facets.exact_matches.${INSTANT_SEARCH_HIERARCHICAL_ATTRIBUTE}.value:-${currentCategory}`,
            ],
          });
        },
        transformSource({ source }) {
          const currentCategory = getInstantSearchCurrentCategory();

          return {
            ...source,
            /* ... */
            templates: {
              ...source.templates,
              header({ items }) {
                if (!currentCategory || !items.length) {
                  return null;
                }

                return (
                  <Fragment>
                    <span className="aa-SourceHeaderTitle">In other categories</span>
                    <span className="aa-SourceHeaderLine"></span>
                  </Fragment>
                );
              },
            },
          };
        },
      });
    },
  };
</script>

Next steps

Autocomplete is now the primary method for users to refine Vue InstantSearch results. From now on, you’re leveraging the complete Autocomplete ecosystem to bring a state-of-the-art search experience for desktop and mobile.

You can now add Autocomplete everywhere on your site and redirect users to the search page whenever they submit a search or after they select a suggestion. You can also use context from the current page to personalize the autocomplete experience. For example, you could display a preview of matching results in a panel for each suggestion and let InstantSearch provide these results once on the search page.

Did you find this page helpful?