Client-Side Search with Algolia

Publisher: TJ Fogarty

Modified: 2018-03-13

I’m going to walk through cre­at­ing a search fea­ture using Algo­lia. I’ll be using some new fea­tures of JavaScript as well, such as async/​await, but it can be accom­plished just as well with­out it, and I’ll offer up alter­na­tives where it applies.

It’s not going to be a mag­ic bul­let for every­one, but it’s inter­est­ing to see how it works, and it can be a solu­tion to add to your toolkit.

What Do I Need? #

  • An Algo­lia account (there’s a free tier, as long as you add their logo to your search area)
  • Some con­tent you want to be searchable
  • A way add your records to an index (you can do this man­u­al­ly, use the API, or if you’re using a CMS or a frame­work there are plen­ty of solu­tions read­i­ly available)

Record? Index? What are you on about?” An index is some­thing that holds the data you want to be search­able, and a record is a piece of that data. For exam­ple, you could have an index called posts” that’s made up of records of which each rep­re­sents a sin­gle post. Kin­da like:

<ul data-index="posts">
    <li data-record="post">
        <h2>My Post Title</h2>
        <p>Oh hey lorem ipsum, dolor sit amet consectetur? Haha, good one adipisicing elit...</p>
    </li>
    <li data-record="post">
        ...
    </li>
    ...
</ul>

Or maybe I ruined it. Nam facilis doloribus? Essen­tial­ly you can then tell Algo­lia which parts of your posts it can search on. This can be the title, some con­tent, tags, cat­e­gories etc… and you can weight them by impor­tance. So a query match­ing one of your post titles would bump that result to the top over a match in the con­tent of anoth­er post.

In the API sec­tion of the Algo­lia dash­board you’ll find your Appli­ca­tion ID, your Search-Only API Key, and your Admin API Key. If you’re using a CMS or frame­work with an Algo­lia inte­gra­tion avail­able, there will be spaces for you to enter these. You can also restrict HTTP ref­er­ers to ensure search will only work on the domains of your choice.

The Code #

I’ll be using the JavaScript search client, and more specif­i­cal­ly the lite client which lim­its the usage to search only, which will do the job. It’ll also save on file size.

Let’s install it:

npm install algoliasearch --save

Next up we’ll set up our search input:

<div class="c-search js-search-container">
  <div class="c-search__inner">
    <label class="c-search__label" for="s">Search:</label>
    <input type="search" class="c-search__input js-search-input" id="s">
    <img src="/images/algolia.svg" class="c-search__credit">
    <div class="js-search-results c-search__results"></div>
  </div>
</div>

Those .js- pre­fixed class­es will be our hooks. They’re not for styling, so it makes the inten­tions clear when you’re look­ing at the code that some JavaScript is at play here.

For the JavaScript, we’ll grab the lite client, and scaf­fold out some code:

import algoliasearch from 'algoliasearch/lite'

export const Search = {
  trigger: document.querySelectorAll('.js-search'),
  input: document.querySelector('.js-search-input'),
  resultsContainer: document.querySelector('.js-search-results'),
  index: null,

  init() {
      // bind to `this` so we reference this object rather than the input when it's called
    this.performSearch = this.performSearch.bind(this)

        // supply our application id and search-only api key
    let client = algoliasearch('APPLICATION_ID', 'SEARCH_ONLY_API_KEY')

        // connect to our index
    this.index = client.initIndex('INDEX_NAME')

        // perform a live search as the user types into the input field
    this.input.addEventListener('keyup', this.performSearch)
  },

  async performSearch(event) {},

  displayResults(results) {},

  emptyResultContainer() {},

    // we'll build up the HTML to inject into the container here
  getResultLink(result) {},

  displayNoResults() {}
}

So we’re grab­bing our .js- pre­fixed ele­ments here, and set­ting up the Algo­lia client with our cre­den­tials to pre­pare it for the search.

When they keyup event is trig­gered, it’ll call the performSearch method. It’s in here that the query to Algo­lia is made:

async performSearch(event) {
    let query = event.target.value

    try {
      let content = await this.index.search({ query })

      if (content.hits && content.hits.length) {
        this.displayResults(content.hits)
      } else {
        this.displayNoResults()
      }
    } catch (e) {
      console.log('Error performing search: ', e)
    }
}

I’m using async/​await here, but you can use promis­es as well:

performSearch(event) {
    let query = event.target.value

    this.emptyResultContainer()

    this.index
      .search({ query })
      .then(content => {
        if (content.hits && content.hits.length) {
          this.displayResults(content.hits)
        } else {
          this.displayNoResults()
        }
      })
      .catch(e => {
        console.log('Error performing search: ', e)
      })
}

We’re get­ting clos­er to dis­play­ing the results. To start with we’ll out­line how the flow works. If we have results, dis­play them, oth­er­wise we’ll let the user know noth­ing was found. After this we’ll see about con­struct­ing the search hits to inject into the results container:

displayResults(results) {
    results.forEach(result => {
      let resultLink = this.getResultLink(result)
      this.resultsContainer.appendChild(resultLink)
    })
},

emptyResultContainer() {
    while (this.resultsContainer.firstChild) {
     this.resultsContainer.removeChild(this.resultsContainer.firstChild)
    }
},

displayNoResults() {
    let title = document.createElement('h4')
    title.innerText = 'No results found'
    this.resultsContainer.appendChild(title)
}

In displayResults we’re call­ing getResultLink which we’ll use to append the the results container:

getResultLink(result) {
    let link = document.createElement('a')
    let title = document.createElement('h4')

    link.setAttribute('href', result.url)
    title.innerText = result.title

    link.appendChild(title)

    return link
}

And final­ly here’s the snip­pet in it’s entirety:

import algoliasearch from 'algoliasearch/lite'

export const Search = {
  trigger: document.querySelectorAll('.js-search'),
  input: document.querySelector('.js-search-input'),
  resultsContainer: document.querySelector('.js-search-results'),
  index: null,

  init() {
    this.performSearch = this.performSearch.bind(this)

    let client = algoliasearch('APPLICATION_ID', 'SEARCH_ONLY_API_KEY')

    this.index = client.initIndex('posts')

    this.input.addEventListener('keyup', this.performSearch)
  },

  performSearch(event) {
    let query = event.target.value
    this.emptyResultContainer()

    this.index
      .search({ query })
      .then(content => {
        if (content.hits && content.hits.length) {
          this.displayResults(content.hits)
        } else {
          this.displayNoResults()
        }
      })
      .catch(e => {
        console.log('Error performing search: ', e)
      })
  },

  displayResults(results) {
    results.forEach(result => {
      let resultLink = this.getResultLink(result)
      this.resultsContainer.appendChild(resultLink)
    })
  },

  emptyResultContainer() {
    while (this.resultsContainer.firstChild) {
      this.resultsContainer.removeChild(this.resultsContainer.firstChild)
    }
  },

  getResultLink(result) {
    let link = document.createElement('a')
    let title = document.createElement('h4')

    link.setAttribute('href', result.url)
    title.innerText = result.title

    link.appendChild(title)

    return link
  },

  displayNoResults() {
    let title = document.createElement('h4')
    title.innerText = 'No results found'
    this.resultsContainer.appendChild(title)
  }
}

With that, you can call Search.init() to kick it all off.

Have a click/​tap to see the live result on this site:

Lost and Found #

No longer do your qual­i­ty posts need to be buried pages deep, nev­er to be seen again. We’ve gone through using the lite client to save on file size, but you can use oth­er full-fledged solu­tions for the frame­work of your choice for a more out-of-the-box experience.