Algolia and using instantsearch.js from your script for Shopify

“Good artists copy, great artists steal.” – Steve Jobs

Inspired by pimoroni.com I wanted to implement a similar search on our webshop, buyzero.de as well. buyzero.de is currently powered by Shopify.

The search shows results as you type – in the main page, dynamically replacing the previosly displayed content. Very cool, very useful – and very, very fast (thanks to Algolia!)

Since I’ve struggled with the available documentation about Algolia, and their onboarding service was not very helpful, I’ve had to figure things out on my own, and by extensive Googling.

This is a summary of my experience implementing Algolia. I did it in a single day, in a single session of about 12 – 14 hours of coding.

Suggested Preparation: injection of development code into your page

I suggest to use the Google Chrome extension Tampermonkey for injecting CSS and Javascript from your local harddrive while you develop.

To do this, you have to enable file access for Tampermonkey. Open:

chrome://extensions

Click on Details and switch “Allow access to file URLs” on

image

image

Create the following new userscript in Tampermonkey:

// ==UserScript==
// @name         Algolia-Inject
// @namespace    https://pi3g.com/
// @version      0.5
// @description  inject local file to test Algolia!
// @author       Maximilian Batz
// @grant       GM_getResourceText
// @grant       GM_addStyle
// @noframes
// @include      https://buyzero.de/*
// @resource     YOUR_CSS file://D:\algolia_jr.css
// @require      file://D:\algolia_jr.js

// ==/UserScript==

var my_css = GM_getResourceText ("YOUR_CSS");
GM_addStyle(my_css);

Explanation:

  • you have to grant getResourceText and addStyle to be able to access and inject the CSS file
  • noframes for not executing a second time in the frames which Shopify apparently loads, and which also would match the domain
  • include –> will match on buyzero.de. You might want to change that line Smile
  • resource used for importing CSS file. Provide the path … in Linux it might be file:///home/whatever/whatever – notice the triple forward slash
  • require – include your javascript file
  • userscript: this is the userscript to add the CSS file

Adding the Algolia Code

With the “how to develop” bit out of the way, here’s my code as sample code in a nice little ZIP file.

Please don’t copy paste from this blog, as I’m not sure, whether WordPress will destroy the code’s formatting … use the .js in the ZIP file to copy from.

Please note that the Algolia dependencies / JavaScript files were already installed in our Shopify theme – by installing the Algolia Shopify plugin. Also our datastore was set up, etc. Algolia was (and still is) already serving search results in a drop down while you typed into the search box.

I’ll walk you through the important parts of the code:

we start aj_setup() once the document is ready, to inject our new search functionality.

Please note that I use aj to prepend my functions & id’s for this script. You may use anything you like, it is not required by Algolia.

The container

in aj_setup() I set up a new container for the search results to be put into (it would probably be better practice to include this directly in your theme file – for developing this was easier).

this is the HTML part of the search

$(".main-content").before('<div id="aj-search-container" class="aj-main-content full-width"><div class="background"><div class="pattern"><div class="container"><div class="row"><div class="col-md-12"><div class="row"><div class="col-md-12 center-column content-without-background">'+
                         "<DIV style='display:block;'>"+
                           "<H1>Suchresultate für: <span id='sc-query'></span></H1>" +
                           "<DIV id='aj-right-container'>" +
                             "<DIV id='aj-clear-all'></DIV>" +
                             "<DIV id='aj-facet-brand'></DIV>" +
                             "<DIV id='aj-facet-category'></DIV>" +
                             "<DIV id='aj-facet-price'></DIV>" +
                           "</DIV>" +
                           "<DIV id='aj-left-container'>" +
                               "<DIV id='aj-stats'></DIV>" +
                               "<DIV id='aj-search-results'></DIV>" +
                               "<DIV id='aj-search-pagination'></DIV>" +
                           "</DIV" +
                       "</DIV>" +
                       '</div></div></div></div></div></div></div></div>'
                       );
  • I give it the ID aj-search-container, to be able to show / hide the entire search container results
  • most of the div’s, including container are necessary for compatible styling with our current theme – use as necessary
  • sc-query is used to mirror the search the user has currently entered, to show that the system updates the results dynamically on his behalf (there’s code to do this – read on)
  • aj-right-container contains filters to get at “facets” of the search. The user can pick one or several brands to show, product categories, and we include a price filter
  • aj-clear-all will contain a link to clear facetting – to return all results which match the user’s query
  • aj-left-container contains the search results. aj-stats will contain statistics about the search (search results + time taken to search), aj-search-results will contain the results, and aj-search-pagination will contain the pagination

The hit template

again, this would probably go directly into your page instead of being injected via Javascript.

    $(".main-content").before(''+
       '<script type="text/html" id="aj-hit-template">'+
'          <div class="aj-hit">'+
'            <div class="aj-hit-image">'+
'              <A HREF="{{product_link}}"><img src="{{image}}" alt="{{title}}"></A>'+
'            </div>'+
'            <div class="aj-hit-content">'+
'              <div class="aj-hit-title"><A HREF="{{product_link}}">{{{_highlightResult.title.value}}}{{variant_title}}</A></div>'+
               '<p class="aj-hit-description">{{{_highlightResult.body_html_safe.value}}} &nbsp; <A HREF="{{product_link}}">&raquo;&nbsp;mehr</A></p>'+
               '<p class="aj-hit-sku"><A HREF="{{product_link}}">{{sku}}</A></p>'+
'              <div class="aj-hit-bottominfos">' +
'                <div class="aj-hit-price">{{price}} €</div>' +
'                <div class="aj-hit-stock">{{inventory_quantity}} auf Lager </div>'+
'                <div  class="aj-hit-link"><A HREF="{{product_link}}">zur Produktseite</A></div>'+
'             </div>'+
             '</div>'+
             '<HR class="aj-divider">' +
           '</div>'+
         '</script>'+
     '');

This template is inserted as “script”. This is the recommended way by the Algolia docs.

It is used to render a search result, or hit.

Here, inside aj-hit we have:

  • aj-hit-image, using {{product_link}} and {{image}} as placeholders to insert the correct values later
  • aj-hit-title, using {{{_highlightResult.title.value}}} – I also link in the {{variant_title}} to provide a full title for variations
  • aj-hit-description, to render the product description – {{{_highlightResult.body_html_safe.value}}}
  • aj-hit-price for the {{price}} – don’t forget to add your currency (or add it by modifying the variable passed to the template)
  • aj-hit-stock, to show how much {{inventory_quantity}} we have
  • aj-hit-link – finally, a link to the product page. {{product_link}}

All of this is nicely (IMHO) styled with my CSS file. Styling is outside the scope of this blogpost, refer to buyzero.de for some inspiration and then code your own.

More on {{{_highlightResult.title.value}}}

Variables displayed using three curly brackets will not be escaped by Algolia for HTML. This is used here, to allow the highlighting.

Algolia will automatically put <em> Tags for you around the parts of the title, etc.  which were matched in the _highlightResult variable. You can use the in the output, to show the user why the search result is thought to be relevant to their search.

I’ll later show you how to modify the values which are passed to the template, as you will not want to render the entire of your product description body, for instance.

aj_setup_search

var pb = aj_setup_search();

To handle the search later on, we return a variable from the setup function (called var point_both inside the setup_search function. pb is it’s abbreviation).

	
var search = instantsearch({
	  appId: 'YOUR_APP_ID',
	  apiKey: 'YOUR_API_KEY', // search only API key, no ADMIN key
	  indexName: 'shopify_buyzero_de_products',
	  urlSync: true,
	  searchParameters: {
		hitsPerPage: 10
	  },
	  searchFunction(my_helper){
		  point_both.ihelper = my_helper;
		  //search.helper.setQuery('Pi 3'); //sic!
		  //my_helper.search(); //sic!
		  
		  if(my_helper.state.query === '') {	
				return;
			}
		  my_helper.search();
	  } //end searchFunction
	});//end instantsearch setup

Here we setup the search (as mentioned before, instantsearch is already included and available on the page through another .js file).

You need to pass:

  • your appId
  • apiKey
  • indexName

you will get these from your Algolia backend. There you can also browse your data structures, and see what parameters you can use for faceting, for instance.

The really important bit is the searchFunction (my_helper) function.

The helper allows you to drive the search via your own script, instead of triggering it from a search box widget which is setup with Algolia’s instantsearch.

It is important to create a reference to this helper, in this particular function.

point_both.ihelper = my_helper;

Accessing the helper from the search variable later on will not work.

In fact you need both. See this example code, I commented out?

//search.helper.setQuery(‘Pi 3’); //sic!

//my_helper.search(); //sic!

sic is the latin word for “like this”. Meaning, it indeed has to be accessed in two different ways to set the query and then execute the search.

Also, you need to add my_helper.search() in here. This will – as I remember it – allow your widgets to update the search correctly … if you find that your results are not updated, check whether you have it in there!

After adding the widgets – which we will discuss below – we wrap up the search setup with the following code:

search.start();
point_both.search = search;
return point_both;

Here we also add a reference to the search object itself, and return it to the aj_setup function.

Note that point_both now has access to the “search” object, and the special helper object “ihelper” we obtained from within the searchFunction function.

Adding the first widget

we will add the result as the first widget:

search.addWidget(
   instantsearch.widgets.hits({
     container: ‘#aj-search-results’,
     templates: {
       item: document.getElementById(‘aj-hit-template’).innerHTML,
       empty: “Es konnten leider keine Resultate für die Suchanfrage <em>\”{{query}}\”</em> gefunden werden.”
     },
     transformData: {
         item: function (my_result) {
         //console.log(my_result);
         //console.log(my_result._highlightResult.body_html_safe.value);
         if (my_result[“variant_title”] == ‘Default Title’){
             my_result[“variant_title”] = ”;
         }
         else
         {
             my_result[“variant_title”] = ‘ :: ‘ + my_result[“variant_title”];
         }
         //my_result[“body_html_safe”] = my_result[“body_html_safe”].substring(0,175) + ‘…’;
         //we do NOT want multiline, we want to match the REAL ending of the string. /m would match at the line ending ..
         var pattern_a = /(.*?)(<[^>]*)?$/i;
         var pattern_b = /<em>[^<]+$/i;
        
         //take the first matched group – which excludes the second match.
         var body_string = my_result._highlightResult.body_html_safe.value.substring(0,175).replace(pattern_a,”$1″);
        
         if (pattern_b.test(body_string)){
             body_string = body_string + “</em>”
         }
         //search for pi 3 b+
         //product id https://buyzero.de/products/budget-kit-raspberry-pi-3-model-b?variant=698066665499
         my_result._highlightResult.body_html_safe.value = body_string + ‘…’;
        
         my_result[“product_link”] = “https://buyzero.de/products/” + my_result[“handle”] + “?variant=” + my_result[“id”];
         return my_result;
         }
     }
   })
);

The container to put the output in is selected as  ‘#aj-search-results’, the template to render the results is selected here: item: document.getElementById(‘aj-hit-template’).innerHTML.

I assume that they put this in a <script> tag, because the contents are not rendered as HTML by the browser.

For empty we just pass a string back as template.

Really straightforward so far, right?

Next we use transformData to prepare a shortened version of your product’s text body and other variables passed to the hit template discussed previously.

Because we introduced <em>’s into the search output – using the automatic highlighting of Algolia – we need to shorten the text in a safe manner. I’ve prepared my own regular expressions for this, in my example code.

If there is an incomplete statement beginning with < after the shortening  (no matching >), it is cut from the output.

If there is an <em> tag without corresponding </em> tag to close it, we add it.

The product link is built obtaining the handle, and setting the id as the variant.

Adding pagination

adding additional widgets is easy:

// Add this after the other search.addWidget() calls
search.addWidget(
   instantsearch.widgets.pagination({
     container: ‘#aj-search-pagination’,
     render: function(my_obj){
         console.log(“render”);
         console.log(my_obj);
     },
     getConfiguration: function(my_obj){
         console.log(“getConfiguration”);
         console.log(my_obj);
     }
   })
);

you don’t want the console log output … I was using this to debug, and apparently forgot to took it out. Sorry. I’m human.

Again, the widget’s output container is set. #aj-search-pagination’

You don’t really need anything else for pagination to work – set up the widget, and style it – you’ll be good.

Faceting

With faceting your user can refine their search:

// Faceting
search.addWidget(
   instantsearch.widgets.refinementList({
     container: ‘#aj-facet-brand’,
     attributeName: ‘vendor’,
     operator: ‘or’,
     limit: 5,
     showMore: true,
     cssClasses: {
         header: ‘aj-refine-header’,
         count: ‘aj-refine-count’
     },
     templates: {
       header: ‘Marke / Brand’
     }
   })
);

  • you need to look at possible attributeNames in your Algolia backend – it will show you the available facets for your data (see screenshot below).
  • The limit of 5 does not refer to your search results, but to the limit of entries which are shown by Algolia for your refinementList
  • The available refinements will be automatically updated if the other search parameters are changed (e.g. if there is no match for a specific brand, this brand will not be shown)
  • showMore: will show a show more link at the bottom of the widget
  • count: ‘aj-refine-count’ – you can style your search results a bit. Probably more than I ued here – refer to the official documentation

image

Clear Facets widget

search.addWidget(
   instantsearch.widgets.clearAll({
     container: ‘#aj-clear-all’,
     templates: {
       link: ‘Filter zurücksetzen / Reset’
     },
     autoHideContainer: false,
     clearsQuery: true,
   })
);

use this to allow the customer to clear the facetting (i.e. they will be shown all results that match for their query again).

No need of configuring besides this.

I will not speak about the other widgets – take a look at my code if you are interested.

Binding to the search box

We are not using Algolia’s search widget, but bind to a keyup event on our search form input, like this:

    var pb = aj_setup_search();
     //#search_query is the input we want
     $(“#search_query”).keyup(function(search) {
       updateSearch(pb);
     });   
}

(we discussed aj_setup_search just now, it’s added here to show you where pb comes from).

We do this so we can do additional processing when the keyup event is triggered – hiding the content we were displaying previously on the page, and adding the search results, etc.

function updateSearch(pb){
     var my_query = $(“#search_query”).val();
     //$(“.main-content”).html(my_query);
     if (my_query) {
         $(“.main-content”).hide();
         $(“#megamenu-header-menu2”).hide();
         $(“#ajr_na”).show();
         $(“#sc-query”).html(my_query);
         $(“#aj-search-container”).show();
         pb.search.helper.setQuery(my_query);
         //console.log(search_handle.helper.state);
         pb.ihelper.search();
     }
     else {
         $(“#aj-search-container”).hide();
         $(“.main-content”).show();
         $(“#ajr_na”).hide();
         $(“#megamenu-header-menu2”).show();
     }

}

We use jQuery to obtain the value of the search field. If it is not empty, we hide the main content we were displaying before, and show the search container.

We set sc-query to show to the user that we are responding to his entry: $(“#sc-query”).html(my_query);

Finally, the important bits are these here:

pb.search.helper.setQuery(my_query);
pb.ihelper.search();

as you see, we need to use two different helpers to set the query and execute the actual search. Yes, that is how it needs to be done – luckily I found someone complaining about this behaviour.

Finally, if the search string is empty (the user pressed Escape, or deleted it, or we deleted it, or whatever …) we hide the search, and show the main content.

Thank you for reading

Thank you for your attention, I hope this helps someone who wants to implement instantsearch.js without using Algolia’s search widget.

References

Tampermonkey

Algolia

Bonus