Concepts / Building Search UI / Multi index search
May. 10, 2019

Multi Index Search

Overview

It might happen that you want to search across multiple indices at the same time. InstantSearch.js has a built-in solution for searching into multiple indices with an autocomplete component. Another common use case is to display hits from multiple indices at the same time. We don’t have a proper API for this use case but we can synchronize two InstantSearch.js instance to have the same behavior.

In this guide we will learn how to share a single searchBox to display multiple hits from different indices. We will also take a look at how to create an autocomplete that targets multiple indices. The source code of both examples can be found on GitHub.

Search in multiple indices

For this first use case we share a single searchBox to search into multiple indices. To implement this behavior we use two InstantSearch.js instances. Each of them target a specific index, the first one is instant_search and the second is instant_search_price_desc.

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
const subIndex = instantsearch({
  indexName: 'instant_search_price_desc',
  searchClient,
});

subIndex.addWidget(
  instantsearch.widgets.hits({
    container: '#hits-instant-search-price-desc',
    templates: {
      item: '{{#helpers.highlight}}{ "attribute": "name" }{{/helpers.highlight}}',
    },
  })
);

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

mainIndex.addWidget(
  instantsearch.widgets.searchBox({
    container: '#searchbox',
  })
);

mainIndex.addWidget(
  instantsearch.widgets.hits({
    container: '#hits-instant-search',
    templates: {
      item: '{{#helpers.highlight}}{ "attribute": "name" }{{/helpers.highlight}}',
    },
  })
);

subIndex.start();
mainIndex.start();

Now that we have our two InstantSearch.js instances setup we have to sync them. Right now when we type inside the searchBox only the first index is updated. What we want is to be able to search inside both indices at the same time. To do so we use the searchFunction option of InstantSearch.js. We use this hook to apply the query of the first index to the second one.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const subIndex = instantsearch({
  indexName: 'instant_search_price_desc',
  searchClient,
});

const mainIndex = instantsearch({
  indexName: 'instant_search',
  searchFunction(helper) {
    subIndex.helper.setQuery(helper.state.query).search();
    helper.search();
  },
  searchClient,
});

// ...

That’s it! We have our two InstantSearch.js instances in sync. Note that since it’s two dedicated instances we can apply different parameters and widgets to the search. In the following example we display a different number of results in our two sets of results. The instant_search index will display 8 results and instant_search_price_desc 16 results. To restrict the number of results per page we use the configure widget.

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
const subIndex = instantsearch({
  indexName: 'instant_search_price_desc',
  searchClient,
});

subIndex.addWidget(
  instantsearch.widgets.configure({
    hitsPerPage: 16,
  })
);

// ...

const mainIndex = instantsearch({
  indexName: 'instant_search',
  searchClient,
  searchFunction(helper) {
    subIndex.helper.setQuery(helper.state.query).search();
    helper.search();
  },
});

mainIndex.addWidget(
  instantsearch.widgets.configure({
    hitsPerPage: 8,
  })
);

// ...

You can find the complete example on GitHub.

autocomplete

For this second use case we are gonna build an autocomplete to search into multiple indices. We are not going to talk too much about the autocomplete part. You can find a complete guide on the subject in the documentation. The autocomplete is built with Selectize and the autocomplete connector. The only difference with the previous guide is how we append the hits to 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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
const autocomplete = instantsearch.connectors.connectAutocomplete(
  ({ indices, refine, widgetParams }, isFirstRendering) => {
    const { container } = widgetParams;

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

      container.find('select').selectize({
        options: [],

        optgroups: indices.map((index, idx) => ({
          $order: idx,
          id: index.index,
          name: index.index,
        })),
        labelField: 'name',
        valueField: 'name',
        searchField: 'name',
        optgroupField: 'section',
        optgroupLabelField: 'name',
        optgroupValueField: 'id',
        highlight: false,
        onType: refine,
        onChange: refine,
        render: {
          option: hit => `
            <div class="hit">
              ${instantsearch.highlight({ attribute: 'name', hit })}
            </div>
          `,
        },
      });

      return;
    }

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

    select.selectize.clearOptions();
    indices.forEach(index => {
      if (index.results) {
        index.results.hits.forEach(hit =>
          select.selectize.addOption(
            Object.assign({}, hit, {
              section: index.index,
            })
          )
        );
      }
    });
    select.selectize.refreshOptions(select.selectize.isOpen);
  }
);

Once we have our autocomplete component we can use it in our application. The component will have access to both indices. It will also leverage the configure to reduce the number of hits.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const search = instantsearch({
  indexName: 'instant_search',
  searchClient,
});

search.addWidget(
  instantsearch.widgets.configure({
    hitsPerPage: 3,
  })
);

search.addWidget(
  autocomplete({
    container: $('#autocomplete'),
    indices: [
      {
        value: 'instant_search_price_desc',
        label: 'instant_search_price_desc',
      },
    ],
  })
);

search.start();

You can find the complete example on GitHub.

Did you find this page helpful?