Salesforce, Python, SQL, & other ways to put your data where you need it

Need event music? 🎸

Live and recorded jazz, pop, and meditative music for your virtual conference / Zoom wedding / yoga class / private party with quality sound and a smooth technical experience

How I added Algolia search: front-end

04 Oct 2021 🔖 web development jamstack
💬 EN

Table of Contents

For a few months now, this site has had an internal search function, and some of you are even using it!

Here’s a new code demo showing how I put an Algolia search box into my web site to actually do something with all that data Algolia had on file.

It’s a great approach for Jekyll, Hugo, 11ty, or other truly statically generated sites. (I’ve tried it on both Jekyll and 11ty so far.)

Getting Algolia to index my site is a story for another day. (Hint: every time I submit new content, my site builds twice now – once for you to read, and once for Algolia to read. Algolia doesn’t actually care that it’s not quite reading from the build process of “the real site.” I have to do things this way because of the tech stack behind my “real site” builds.)

(Note to self: I really should write up the “backend” post, because a handy feature of my “2nd build” is that I can inspect the its logs way better than my “main site build” process lets me do.)


Search page

My “search” web page has 3 placeholder <DIV> tags in it:

<div id="search-searchbar" class=""></div>
<div id="search-poweredby" class=""></div>
<div id="search-hits" class=""></div>

It then has some pretty massive <SCRIPT> tags in it, which summon all sorts of JavaScript written by Algolia and hosted on a content delivery network (“CDN”) and then invoke that code, passing it my Algolia index’s application ID, search API key, and index name.


Simple, ugly, URL-unaware version

After reading the front-end half of Salma Alam-Naylor’s “How to add Algolia InstantSearch to your Next.js application,” I’m seeing familiar patterns. I’ll take a crack at explaining how my code works.

Her first working-but-ugly demo looks like this:

Salma's screenshot of her first working demo

My first working-but-ugly demo looks like this:

Screenshot of the simple, ugly search page with results filtered to Algolia from the in-page search bar

Despite our different approaches, we’re getting pretty much to the same place, so I think that means we’re on track to have something that can be taken apart and have universal lessons learned from it. Let’s go!

Instantiate from the algoliasearch library

You need to instantiante an algoliasearch object from Algolia’s “Algoliasearch Lite” library and save off the return value into a variable.

When you do, you need to pass the constructor a string representing your Algolia application ID and another string representing your Algolia public search API key.

  • I get this library as cdn.jsdelivr.net/.../algoliasearch-lite.umd.js over their CDN. It comes with an algoliasearch(...) function in it.
  • Salma installs it as an NPM package algoliasearch and uses the algoliasearch(...) found within its algoliasearch/lite section.

It’s conventional to put the word client into that variable’s name.

I presume this object is the one that deals with all the asynchronicity of reaching out to Algolia’s servers, etc. I find it useful to think of it kind of like saving my app ID & key into environment variables & setting up a Postman collection that uses the same authentication for every request in it. Oh, and as a tangent, if you want to try searching Algolia with Postman against a sample index that Algolia leaves sitting around for everyone on the internet to try, I wrote a Postman collection.

Instantiate from the instantsearch library

You also need to instantiate an instantsearch object from Algolia’s “Instant Search” library.

When you do, you need to pass the constructor the algoliasearch object you saved into a variable with client in its name, as well as a string representing your Algolia index name.

  • I get this library as cdn.jsdelivr.net/.../instantsearch.production.min.js over their CDN. It comes with an instantsearch(...) function in it, which I call as:
      const search = instantsearch({ indexName, searchClient, });
    
  • Salma installs it as an NPM package react-instantsearch-dom. It comes with an <InstantSearch>...</InstantSearch> React component in it, which she calls by including it in her JSX templates.

Attach instantsearch to the DOM

Both Salma and I have an objective of trying to inject one of these thingies into the “DOM” / HTML of our page (this is Salma’s diagram):

Salma's illustration of the DOM elements that make up an Instant Search.  Salma drew a green box labeled InstantSearch.  Inside of the green box is a yellow box inside labeled SearchBox, with example text inside of it indicating that someone is searching for the word 'css'.  Inside the green box but below the yellow box is a red box labeled Hits, and a snippet of an article about CSS is shown within the red box.

React approach

Salma gets to do magic and just type out some JSX:

export default function Search() {
  return (
    <>
      <InstantSearch searchClient={searchClient} indexName="my_awesome_content">
        <SearchBox />
        <Hits />
      </InstantSearch>
    </>
  );
}
Vanilla HTML + CDN JavaScript approach

I, on the other hand, have to have this snippet in my HTML template:

<div id="search-searchbar" class=""></div>
<div id="search-poweredby" class=""></div>
<div id="search-hits" class=""></div>

And then this code:

search.addWidgets([
  instantsearch.widgets.searchBox({...some settings...}),
  instantsearch.widgets.poweredBy({...some settings...}),
  instantsearch.widgets.hits({...some settings...}),
]);
search.start();

Et voilà! A working instant search box, with really ugly-looking search results.

Note that it’s completely insensitive to putting any sort of search term in the URL bar – I’ll have to work on that.

Screenshot of the simple, URL-unaware search page with results unfiltered despite a URL query parameter


URL-aware, pretty version

Just like Salma went on to replace her use of <Hits /> with <CustomHits />, I’m going to define a really fancy function (don’t ask me how it works; I copied & pasted it) called hitTemplate to and pass it to instantsearch.widgets.hits({...some settings...}). That cleans up the search result formatting from being a bunch of ugly JSON to actually looking like something people want to read.

The only catch is that my definition of hitTemplate is customized to properties found on objects in my Algolia index.

And there’s no guarantee that any other index’s objects in the world – heck, even other objects in my own index – have the same properties I’ve trained my hitTemplate to expect. I wrote about this last year.

So when I swap out my personal Algolia app ID, key, & index name for this sample one in my .env file, things seem to break.

“Search approach #1” works, but this new “search approach #2” seems to show blank results. The “hits” box seems confused.

Screenshot of a search for 'acer' seeming to return no results

The “hits” box reverts to being ugly and filtering by “acer” or “google” works fine, though, if I comment out templates: {item: hitTemplate,}, in my instantsearch.widgets.hits({...some settings...}).

Screenshot of a search for 'acer' returning ugly results

So I guess it works, but buyer beware that any given hitTemplate you find in someone else’s sample code might need to be totally rewritten to work with your index.

No wonder the default Algolia Instant Search styling shows you a bunch of stringified JSON in the “hits” box. You’ve gotta take a look at the data before you can write a good template to format it, so they might as well put the data right under your nose.

In this approach to writing a search page, I’ll also go a step further than Salma did and customize my invocation of instantsearch(...) to make it URL-parameter-sensitive with what Algolia calls “routing.”

const singleIndexStateMapping = instantsearch.stateMappings.singleIndex(indexName);
const withoutPageStateMapping = {
  stateToRoute(uiState) {
    const { page, ...state } = singleIndexStateMapping.stateToRoute(uiState);
    return state;
  },
  routeToState(routeState) {
    const { [indexName]: indexUiState } = singleIndexStateMapping.routeToState(routeState);
    const { page, ...state } = indexUiState;
    return { [indexName]: state };
  },
};
const search = instantsearch({ indexName, searchClient, routing: { stateMapping: withoutPageStateMapping }, });

That’s going to let me send people over to search from a non-JavaScript-based form (covered later in this post).

Nifty – it works! Things are pretty, and typing a keyword makes it appear in the URL bar, and directly re-visiting that URL later makes the keyword show up in the search box and filter the hits.

Screenshot of the pretty, URL-aware search page with results filtered to 'Algolia' from the URL query parameter


HTML form for other pages

To avoid injecting unnecessary JavaScript onto every page of my site, the rest of the pages on my site simply include a <FORM> tag that bump you over to the search page if you submit something to the form.


Enhancing the search page with click tracking

Finally, I got it – on 10/5/21, I managed to add click tracking to my search.

Having an internal search box was useful for content creation, because I could see what my regulars expect me to have written about. (Google Search Console doesn’t tell me much except that most of my traffic comes from one-off visitors who need help installing a certain piece of software and leave after finding that one article.) Those are the readers I’d most like to write for.

Through the internal search, I’ve been able to see a few keywords that people seem to expect me to know about.

The ones I know won’t have results are mostly things I also wish I’d taken the time to figure out.

But for the ones I’ve written a lot about, I’d love to see what’s actually looking promising to visitors – what they’re clicking on.

I added a 3rd search page to my demo and pointed the home page’s <FORM /> at it (instead of at #2, where it had been pointed earlier).

I refactored the “hits template” to work both with my real site and with the “e-commerce” sample index that Algolia provides.

The “widgets script” was getting a little long, so I broke it up into 7 different blocks of <script>...</script> HTML and included them back-to-back.

The new bits are:

  1. Code that loads up an object named aa to the browser’s window context from Algolia’s CDN-delivered “Search Insights” library.
  2. Code that runs right after const search = instantsearch(...); and before defining the hits template, forcing the search object to use the “Search Insights” middleware provided by the “Instant Search” and to wire it up to window.aa (which comes from the “Search Insights” library).
  3. A modification to the definition of hitTemplate (besides the dataset-compatibility ones) so that instead of being a function (hit) {...}, it’s a function (hit, bindEvent) {...}. Plus, inside the actual string it returns, the <A> link to click on a search result’s URL got an extra property in it: ` ${bindEvent(‘click’, hit, ‘Search Result Clicked’)}`.

And that’s that. On my own web site, I literally just had to go in and make those three changes. (I didn’t, of course, have to modify the hits template for compatibility w/ a sample dataset because on my web site, I am actually using my own dataset.)

Thanks to Bryan Robinson and mystery-French-accent-Algolia-employee for getting me to give this one last hurrah and finally breaking through.

Screenshots of Bryan's name showing up in my click results

--- ---