Integrations / Shopify

InstantSearch v4 migration

Introduction

Algolia is migrated InstantSearch from version 1 to version 4, so you can make use of all latest InstantSearch features and bugfixes.

This change is gradually made accessible to everyone. If you’d like to get early access, please contact the support team.

While this migration is not mandatory, we strongly encourage it.

Depending on your implementation of the plugin, this migration should be done either:

  • Automatically, through the plugin admin panel.
  • Manually, if you have a custom implementation.

Automatic migration

To automatically migrate to the newest version of InstantSearch, reinstall Algolia into your theme.

This will override InstantSearch and our scripts with up-to-date versions.

If you have changed or customized your theme by editing the scripts provided by our plugin, proceed with a manual migration. Otherwise, you will lose your changes.

To automatically update the Algolia dependencies of your theme, go to your Algolia plugin admin and follow these steps:

1. Go to the Display tab.

2. Click Install to a new theme.

The display tab in your Shopify admin

3. Select the theme you want to update.

5. Click Finish installation.

Manual migration

Read the complete InstantSearch migration guide for a complete list of the changes between InstantSearch v1 and v4.

If your shop has a custom theme you have to manually update your theme.

To migrate manually, open the theme code editor.

Dropdown to open the theme code editor

The changes described are the ones that must be applied to the original InstantSearch implementation and may differ from the actual changes depending on your custom implementation. This is a general guide in addition to the the complete InstantSearch migration guide.

InstantSearch initialization

  • appId and apiKey parameters are replaced by searchClient
  • urlSync is replaced by routing
  • In searchFunction, searchFunctionHelper must be used in place of instant.search.helper

Changes to make in algolia_instant_search.js.liquid:

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
@@ -48,6 +48,33 @@
   );
   $hiding.appendTo($('head'));

+  function getTrackedUiState(uiState) {
+    var trackedUiState = {};
+    Object.keys(uiState).forEach(function(k) {
+      if (k === 'configure' || k === 'query' || k === 'q') {
+        return;
+      }
+      trackedUiState[k] = uiState[k];
+    });
+    return trackedUiState;
+  }
+
+  function singleIndex(indexName) {
+    return {
+      stateToRoute: function(uiState) {
+        var route = getTrackedUiState(uiState[indexName] || {});
+        route.q = uiState[indexName].query;
+        return route;
+      },
+      routeToState: function(routeState) {
+        var state = {};
+        state[indexName] = getTrackedUiState(routeState || {});
+        state[indexName].query = routeState.q;
+        return state;
+      },
+    };
+  }
+
   var instant = {
     colors: algolia.config.colors,
     distinct: Boolean(algolia.config.show_products),
@@ -75,17 +102,21 @@
         : algolia.config.products_full_results_hits_per_page,
     poweredBy: algolia.config.powered_by,
     search: instantsearch({
-      appId: algolia.config.app_id,
-      apiKey: algolia.config.search_api_key,
+      searchClient: window.algoliasearch(
+        algolia.config.app_id,
+        algolia.config.search_api_key
+      ),
       indexName: '' + algolia.config.index_prefix + 'products',
       searchParameters: {
         clickAnalytics: true,
       },
-      urlSync: {},
+      routing: {
+        stateMapping: singleIndex(algolia.config.index_prefix + 'products'),
+      },
       searchFunction: function(searchFunctionHelper) {
         // Set query parameters here because they're not kept when someone
         // presses the Back button if set in the `init` function of a custom widget
-        var helper = instant.search.helper;
+        var helper = searchFunctionHelper;
         var page = helper.getPage();
         helper.setQueryParameter(
           'highlightPreTag',

Widgets

Facet widgets (menu, rangeSlider, refinementList)

  • attributeName option is renamed to attribute
  • transformData option is replaced by transformItems

Changes to make in algolia_facets.js.liquid:

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
@@ -97,8 +97,9 @@
     var widget = TYPES_TO_WIDGET[facet.type];
     var params = _.cloneDeep(widget.params) || {};

     params.container = "[class~='ais-facet-" + facet.escapedName + "']";
-    params.attributeName = facet.name;
+    params.attribute = facet.name;
     params.templates = {};
     params.cssClasses = algolia.facetCssClasses;

@@ -125,14 +126,16 @@
     }

     var displayFunction = algolia.facetDisplayFunctions[facet.name];
-    params.transformData = function(data) {
-      var transformedData = Object.assign({}, data);
-      transformedData.type = {};
-      transformedData.type[facet.type] = true;
-      if (displayFunction) {
-        transformedData.name = displayFunction(data.name);
-      }
-      return transformedData;
+    params.transformItems = function(items) {
+      return items.map(function(item) {
+        var transformedItem = Object.assign({}, item);
+        transformedItem.type = {};
+        transformedItem.type[facet.type] = true;
+        transformedItem.label = displayFunction
+          ? displayFunction(item.value)
+          : item.value;
+        return transformedItem;
+      });
     };

     return {

Changes to make in algolia_instant_search.hogan.liquid:

1
2
3
4
5
6
7
8
9
10
11
12
13
@@ -5,7 +5,10 @@
   <div class="ais-facets">
       <div class="ais-current-refined-values-container"></div>
     [[# facets ]]
-      <div class="ais-facet-[[ type ]] ais-facet-[[ escapedName ]]"></div>
+      <div class="ais-facet-[[ type ]] ais-facet-[[ escapedName ]]">
+        <div class="ais-range-slider--header ais-facet--header ais-header">[[ title ]]</div>
+        <div class="ais-facet-[[ escapedName ]]-container"></div>
+      </div>
     [[/ facets ]]
   </div>
   <div class="ais-block">

Changes to make in algolia_instant_search_facet_item.hogan.liquid:

1
2
3
4
5
6
7
8
9
@@ -2,7 +2,7 @@
   [[# type.disjunctive ]]
     <input type="checkbox" class="[[ cssClasses.checkbox ]]" [[# isRefined ]]checked[[/ isRefined ]]/>
   [[/ type.disjunctive ]]
-  [[& name ]]
+  [[& label ]]
   <span class="[[ cssClasses.count ]]">
     [[# helpers.formatNumber ]]
       [[ count ]]

searchBox widget

  • poweredBy option is removed
  • showReset and showSubmit options are added

Changes to make in algolia_instant_search.js.liquid:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@@ -235,9 +235,10 @@
       instantsearch.widgets.searchBox({
         container: '.ais-search-box-container',
         placeholder: algolia.translations.searchForProduct,
-        poweredBy: false,
-      })
-    );
+        showReset: false,
+        showSubmit: false,
+      }),
+    ]);

     // Logo & clear
     instant.search.addWidget({

stats widget

  • body template is renamed to text
  • transformData option is removed. If you want to apply transformations to your data, you can do so in the template using some Hogan helpers (see algolia_helpers.js.liquid for some examples)

Changes to make in algolia_instant_search.js.liquid:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@@ -270,17 +270,7 @@
       instantsearch.widgets.stats({
         container: '.ais-stats-container',
         templates: {
-          body: instant.templates.stats,
-        },
-        transformData: {
-          body: function(data) {
-            return Object.assign({}, data, {
-              processingTimeS: data.processingTimeMS / 1000,
-              start: data.page * data.hitsPerPage + 1,
-              end: Math.min((data.page + 1) * data.hitsPerPage, data.nbHits),
-              translations: algolia.translations,
-            });
-          },
+          text: instant.templates.stats,
         },
       })
     );

4. sortBySelector widget

  • sortBySelector widget was renamed to sortBy
  • indices option was renamed to items
  • A sortBy item value is now value instead of name

Changes to make in algolia_instant_search.js.liquid:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@@ -287,12 +287,12 @@

     // Sort orders
     if (activeSortOrders.length > 1) {
       instant.search.addWidget(
-        instantsearch.widgets.sortBySelector({
+        instantsearch.widgets.sortBy({
           container: '.ais-sort-orders-container',
-          indices: instant.sortOrders,
-        })
+          items: instant.sortOrders,
+        }),
       );
     }

     // Change display

Changes to make in algolia_sort_orders.js.liquid:

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
@@ -6,7 +6,7 @@
   var sort_order_base = '' + algolia.config.index_prefix + 'products';

   algolia.sortOrders = [
-    { name: sort_order_base, label: '' + algolia.translations.relevance },
+    { value: sort_order_base, label: '' + algolia.translations.relevance },
   ];

   _.forEach(algolia.config.sort_orders, function(sort_order) {
@@ -16,7 +16,7 @@
       (sort_order.asc.active === true || sort_order.asc.active === '1')
     ) {
       algolia.sortOrders.push({
-        name: sort_order_base + '_' + sort_order.key + '_asc',
+        value: sort_order_base + '_' + sort_order.key + '_asc',
         label: sort_order.asc.title,
       });
     }
@@ -26,7 +26,7 @@
       (sort_order.desc.active === true || sort_order.desc.active === '1')
     ) {
       algolia.sortOrders.push({
-        name: sort_order_base + '_' + sort_order.key + '_desc',
+        value: sort_order_base + '_' + sort_order.key + '_desc',
         label: sort_order.desc.title,
       });
     }
@@ -43,7 +43,7 @@

   if (collection_sort_orders) {
     algolia.collectionSortOrders = [
-      { name: sort_order_base, label: '' + algolia.translations.relevance },
+      { value: sort_order_base, label: '' + algolia.translations.relevance },
     ];

     _.forEach(collection_sort_orders, function(sort_order) {
@@ -53,7 +53,7 @@
         (sort_order.asc.active === true || sort_order.asc.active === '1')
       ) {
         algolia.collectionSortOrders.push({
-          name: sort_order_base + '_' + sort_order.key + '_asc',
+          value: sort_order_base + '_' + sort_order.key + '_asc',
           label: sort_order.asc.title,
         });
       }
@@ -63,7 +63,7 @@
         (sort_order.desc.active === true || sort_order.desc.active === '1')
       ) {
         algolia.collectionSortOrders.push({
-          name: sort_order_base + '_' + sort_order.key + '_desc',
+          value: sort_order_base + '_' + sort_order.key + '_desc',
           label: sort_order.desc.title,
         });
       }

currentRefinedValues widget

The currentRefinedValues widget is removed and replaced by the currentRefinements and clearRefinements widgets that have a slightly different UI.

Here’s an example on how you can reproduce the currentRefinedValues UI using both new widgets.

Changes to make in algolia_instant_search.js.liquid:

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
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
@@ -346,30 +346,104 @@
     });

     // Current refined values
-    var attributes = _.map(instant.facets.shown, function(facet) {
-      return {
-        name: facet.name,
-        label: facet.title,
-      };
-    });
-    instant.search.addWidget(
-      instantsearch.widgets.currentRefinedValues({
-        container: '.ais-current-refined-values-container',
-        cssClasses: {
-          root: 'ais-facet',
-          header: 'ais-facet--header',
-          body: 'ais-facet--body',
-        },
-        templates: {
-          header: algolia.translations.selectedFilter,
-          item: instant.templates.currentItem,
-          clearAll: algolia.translations.clearAll,
-        },
-        onlyListedAttributes: true,
-        attributes: attributes,
-      })
-    );
-
+    var createDataAttribtues = function(refinement) {
+      return Object.keys(refinement)
+        .map(function(key) {
+          return 'data-' + key + '="' + refinement[key] + '"';
+        })
+        .join(' ');
+    };
+
+    var renderListItem = function(item) {
+      var facet = instant.facets.list.find(function(f) {
+        return f.name === item.label;
+      });
+      return item.refinements
+        .map(function(refinement) {
+          return (
+            '<li class="ais-current-refined-values--item">' +
+            '  <a ' +
+            createDataAttribtues(refinement) +
+            '    class="ais-current-refined-values--link">' +
+            '    <div>' +
+            '      <div class="ais-current-refined-values--label">' +
+            facet.title +
+            '      </div>: ' +
+            refinement.label +
+            '    </div>' +
+            '  </a>' +
+            '</li>'
+          );
+        })
+        .join('');
+    };
+
+    var renderCurrentRefinements = function(renderOptions) {
+      var items = renderOptions.items;
+      var refine = renderOptions.refine;
+      var widgetParams = renderOptions.widgetParams;
+
+      widgetParams.container.innerHTML =
+        '<div class="ais-current-refined-values--header ais-facet--header ais-header">Selected filters</div>' +
+        '<div class="ais-root ais-current-refined-values ais-facet">' +
+        '  <ul class="ais-current-refined-values--list">' +
+        items.map(renderListItem).join('') +
+        '  </ul>' +
+        '</div>';
+
+      Array.prototype.slice
+        .call(
+          widgetParams.container.querySelectorAll(
+            '.ais-current-refined-values--link'
+          )
+        )
+        .forEach(function(element) {
+          element.addEventListener('click', function(event) {
+            var item = Object.keys(event.currentTarget.dataset).reduce(function(
+              acc,
+              key
+            ) {
+              var itemData = {};
+              itemData[key] = event.currentTarget.dataset[key];
+              return algolia.assign({}, acc, itemData);
+            },
+            {});
+
+            refine(item);
+          });
+        });
+    };
+
+    var customCurrentRefinements = instantsearch.connectors.connectCurrentRefinements(
+      renderCurrentRefinements
+    );
+
+    var customCurrentRefinementsWithPanel = instantsearch.widgets.panel({
+      hidden: function(options) {
+        return !instant.facets.list.some(function(facetName) {
+          return options.helper.hasRefinements(facetName);
+        });
+      },
+    })(customCurrentRefinements);
+
+    var clearRefinementsWithPanel = instantsearch.widgets.panel({
+      hidden: function(options) {
+        return !instant.facets.list.some(function(facetName) {
+          return options.helper.hasRefinements(facetName);
+        });
+      },
+    })(instantsearch.widgets.clearRefinements);
+
+    instant.search.addWidgets([
+      clearRefinementsWithPanel({
+        container: '.ais-clear-refinements-container',
+        templates: { resetLabel: algolia.translations.clearAll },
+      }),
+      customCurrentRefinementsWithPanel({
+        container: '.ais-current-refined-values-container',
+      }),
+    ]);

     // Facets
     _.forEach(instant.facets.widgets, function(widget) {
       instant.search.addWidget(

Changes to make in algolia_instant_search.hogan.liquid:

1
2
3
4
5
6
7
8
@@ -3,6 +3,7 @@
     Show filters
   </div>
   <div class="ais-facets">
+      <div class="ais-clear-refinements-container"></div>
       <div class="ais-current-refined-values-container"></div>
     [[# facets ]]
       <div class="ais-facet-[[ type ]] ais-facet-[[ escapedName ]]"></div>
1
2
3
4
5
6
7
8
9
10
11
12
13
@@ -5,7 +5,10 @@
   <div class="ais-facets">
       <div class="ais-current-refined-values-container"></div>
     [[# facets ]]
-      <div class="ais-facet-[[ type ]] ais-facet-[[ escapedName ]]"></div>
+      <div class="ais-facet-[[ type ]] ais-facet-[[ escapedName ]]">
+        <div class="ais-range-slider--header ais-facet--header ais-header">[[ title ]]</div>
+        <div class="ais-facet-[[ escapedName ]]-container"></div>
+      </div>
     [[/ facets ]]
   </div>
   <div class="ais-block">

Changes to make in algolia_instant_search_facet_item.hogan.liquid:

1
2
3
4
5
6
7
8
9
@@ -2,7 +2,7 @@
   [[# type.disjunctive ]]
     <input type="checkbox" class="[[ cssClasses.checkbox ]]" [[# isRefined ]]checked[[/ isRefined ]]/>
   [[/ type.disjunctive ]]
-  [[& name ]]
+  [[& label ]]
   <span class="[[ cssClasses.count ]]">
     [[# helpers.formatNumber ]]
       [[ count ]]

hits widget

  • sortBySelector widget is renamed to sortBy
  • indices option is renamed to items
  • A sortBy item value is now value instead of name

  • transformData option is replaced by transformItems
  • hitsPerPage option is removed, you can use the configure widget to set it.
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
@@ -230,4 +261,10 @@
       },
     });

+    instant.search.addWidgets([
+      instantsearch.widgets.configure({
+        hitsPerPage: instant.hitsPerPage,
+      }),
+    ]);
+
     // Search input
@@ -381,13 +455,12 @@
     instant.search.addWidget(
       instantsearch.widgets.hits({
         container: '.ais-hits-container',
-        hitsPerPage: instant.hitsPerPage,
         templates: {
           empty: instant.templates.empty,
           item: instant.templates.product,
         },
-        transformData: {
-          item: function(product) {
+        transformItems: function(products) {
+          return products.map(function(product) {
             return Object.assign({}, product, {
               _distinct: instant.distinct,
               can_order:
@@ -395,15 +468,11 @@
                 product.inventory_policy === 'continue' ||
                 product.inventory_quantity > 0,
               translations: algolia.translations,
-              queryID: instant.search.helper.lastResults._rawResults[0].queryID,
+              queryID: product.__queryID,
               productPosition: product.__hitIndex + 1,
             });
-          },
-          empty: function(params) {
-            return Object.assign({}, params, {
-              translations: algolia.translations,
-            });
-          },
+          });
         },
       })
     );

CSS

A lot of CSS classes changed between InstantSearch v1 and v4, and you may need to update your CSS accordingly. You can find the correspondence list between the old and the new classes in the complete InstantSearch migration guide .

Did you find this page helpful?