Algolia is an integritable search engine, which can be used as elasticsearch or integrated with Hugo alike statc site engine.

How it works

Algolia works by reading your data in json format. It stores your json data in a way same as MongoDB, but has extra analyzing abilities, such as filter your searching result, tucantted snipset, pagination, hide content.

To work with Hugo, we first need to make our site availble in json format by having this in your config.toml:

[outputFormats.SearchIndex]
  mediaType = "application/json"
  baseName = "searchindex"
  isPlainText = true
  notAlternative = true

[outputs]
  home = [ "HTML", "RSS", "SearchIndex" ]

It tells Hugo to generate new json file searchindex.json during build stage. You’ll find this file under public folder after running hugo. You’ll also need to define what content needed in this file, so create another file list.searchindex.json under layouts\_default:

{{- $.Scratch.Add "searchindex" slice -}}
{{- range $index, $element := (where .Site.Pages "Kind" "page") -}}
    {{- $.Scratch.Add "searchindex" (dict "id" $index "title" $element.Title "uri" $element.Permalink "coverurl" $element.Params.coverurl "girls" $element.Params.girls "section" $element.Section "content" $element.Plain "categories" $element.Params.categories "release_date" ($element.Params.release_date)) -}}
{{- end -}}
{{- $.Scratch.Get "searchindex" | jsonify -}}

These 2 files together tells Hugo to put interested data of all hugo pages into json dict, the output will be like [{"id": "1"},{"id": "2"},...]

After this, we need to push this data into Algolia index database, they provide many API clients, you can pick any of them, I just use python.

The python client is fairly simple, similar as slack client:

from algoliasearch.search_client import SearchClient
import json

client = SearchClient.create('X4SC7ERQxx', '2c4b0e7a71f89xxxxxxxxxxxxx')
index = client.init_index('AV')
batch = json.load(open('searchindex.json'))
index.save_objects(batch, {'autoGenerateObjectIDIfNotExist': True})

Next you’ll need to use their provided js code to integrate Algolia with your site.

Calling Algolia

They provide 3 main kind of interface, InstantSearch,AutoComplete, and GeoSearch. My experience and comment:

  • InstantSearch provide full function of a website, you have search bar on top and their results listed under, you can also add on more div such as category list. This way you don’t need to have anything in Hugo page, Algolia itself is your content.
  • AutoComplete gives you a search bar and their results shown inside same div, which means it will not affect your existing content, it is the best option your index page. This way user can see results right away, it will float over your index content.
  • GeoSearch provides map, also can detect user location.

InstantSearch Example

  1. Create search.html under layout/partials.
<div class="search">
  <div class="row">
    <div class="col-sm-12 col-md-12 col-lg-12">
        <div id="searchbox"><!-- SearchBox widget will appear here --></div>
    </div>
  </div>
  <div class="col-sm-offset-3 col-sm-12 col-md-12 col-lg-12" id="hits"></div>
  <div id="pagination"></div>
</div>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/algoliasearch-lite.umd.js" integrity="sha256-EXPXz4W6pQgfYY3yTpnDa3OH8/EPn16ciVsPQ/ypsjk=" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/instantsearch.production.min.js" integrity="sha256-LAGhRRdtVoD6RLo2qDQsU2mp+XVSciKRC8XPOBWmofM=" crossorigin="anonymous"></script>
<script src="/js/algolia.js"></script>

here we recall algolia.js stored in statc/js, this file includes details of definititon of required div. 2. Create statc/js/algolia.js. The default behavior of Algolia UI is to show everyhing in the beginning. Search in this example only shows its result when user input something.

const algoliaClient = algoliasearch(
  'X4SC7ERxxx',
  '2c4b0e7a71f896xxxxxxxxxxxxxx'
);

const searchClient = {
  search(requests) {
    if (requests.every(({ params }) => !params.query)) {
      return Promise.resolve({
        results: requests.map(() => ({
          hits: [],
          nbHits: 0,
          nbPages: 0,
        })),
      });
    }

    return algoliaClient.search(requests);
  },
};

const search = instantsearch({
  indexName: 'AV',
  searchClient
});

search.addWidgets([
  instantsearch.widgets.searchBox({
    container: '#searchbox',
    placeholder: 'Search Collection or Blog',
  }),
  instantsearch.widgets.configure({
  hitsPerPage: 9
  }),
  instantsearch.widgets.hits({
    container: '#hits',
    templates: {
      empty: `
        {{#query}}
          No results for <q>{{query}}</q>
        {{/query}}
      `,
      item: `
      <div class="col-sm-12 col-md-12 col-lg-12">
        <img class="img-thumb" src="{{ coverurl }}" align="left" alt="{{ title }}" />
        <div class="hit-name">
          <a href="{{ uri }}">{{{ _snippetResult.title.value }}}</a>
        </div>
        <div class="release_date">
          <p><b>Release date: </b>{{ release_date }}<br></p>
        </div>
      </div>
      `,
    },
  }),
  instantsearch.widgets.pagination({
    container: '#pagination',
  }),
]);

search.start();
  1. Call search.html somewhere in hugo, can be put in nav, banner, or index.

Autocomplete Example

  1. create search.html
<div class="container">
  <div class="row">
    <div class="col-sm-6 col-sm-offset-3">
      <form action="#" class="form">
        <h3>Basic example</h3>
          <input class="form-control" id="search-input" name="contacts" type="text" placeholder='Search by name' />
      </form>
    </div>
  </div>
</div>  
<script src="https://cdn.jsdelivr.net/algoliasearch/3/algoliasearch.min.js"></script>
<script src="https://cdn.jsdelivr.net/autocomplete.js/0/autocomplete.min.js"></script>
<script>
  var client = algoliasearch('latency', '6be0576ff61c053d5f9a3225e2a90f76');
  var index = client.initIndex('contacts');
  autocomplete('#search-input', { hint: false }, [
    {
      source: autocomplete.sources.hits(index, { hitsPerPage: 5 }),
      displayKey: 'name',
      templates: {
        suggestion: function(suggestion) {
          return suggestion._highlightResult.name.value;
        }
      }
    }
  ]).on('autocomplete:selected', function(event, suggestion, dataset) {
    console.log(suggestion, dataset);
    alert('dataset: ' + dataset + ', ' + suggestion.name);
  });
</script>
  1. create css:
.algolia-autocomplete {
        width: 100%;
}
.algolia-autocomplete .aa-input, .algolia-autocomplete .aa-hint {
    width: 100%;
}
.algolia-autocomplete .aa-hint {
    color: #999;
}
.algolia-autocomplete .aa-dropdown-menu {
    width: 100%;
    background-color: #fff;
    border: 1px solid #999;
    border-top: none;
}
.algolia-autocomplete .aa-dropdown-menu .aa-suggestion {
    cursor: pointer;
    padding: 5px 4px;
}
.algolia-autocomplete .aa-dropdown-menu .aa-suggestion.aa-cursor {
    background-color: #B2D7FF;
}
.algolia-autocomplete .aa-dropdown-menu .aa-suggestion em {
    font-weight: bold;
    font-style: normal;
}

Layout your UI

Having things showing on your site is only first step, to make it realy work you need to have them in correct place. Algolia has a lot of default css classes that we can twick:

  • root: the root element of the widget.
  • form: the form element.
  • input: the input element.
  • reset: the reset button element.
  • resetIcon: the reset button icon.
  • loadingIndicator: the loading indicator element.
  • loadingIcon: the loading indicator icon.
  • submit: the submit button element.
  • submitIcon: the submit button icon.

A working example I have is as following, put this in a css file and call it under header.html:

.ais-SearchBox {
  margin: 1em 0;
}
.ais-Hits-list, .ais-InfiniteHits-list, .ais-InfiniteResults-list, .ais-Results-list {
    margin-top: -1rem;
    margin-left: -1rem;
    display: -webkit-box;
    display: -ms-flexbox;
    display: flex;
    -ms-flex-wrap: wrap;
    flex-wrap: wrap;
}
.ais-Hits-item {
    margin-top: 1rem;
    margin-left: 1rem;
    padding: 1rem;
    width: calc(31% );
    border: 1px solid #c4c8d8;
    box-shadow: 0 2px 5px 0 #e3e5ec;
    list-style: none;
}
.ais-SearchBox-input {
    -webkit-appearance: none;
    -moz-appearance: none;
    appearance: none;
    padding: .3rem 1.7rem;
    width: 100%;
    height: 100%;
    position: relative;
    background-color: #ced4da;
    border-color: #ff07b3;
    border: 1px solid #c4c8d8;
    border-radius: 5px;
}
/*.ais-PoweredBy{
    position: absolute;
    top: 360px;
    right: 300px
}
.ais-PoweredBy-logo {
    width: 100%;
    padding: .3rem 1.7rem;
    position: relative;
    bottom: 40px;
}
.ais-PoweredBy-link, .ais-PoweredBy-logo {
    display: block;
}*/
.ais-SearchBox-submitIcon {
    width: 14px;
    height: 14px;
    padding: 1px 1px;
}
.ais-SearchBox-reset {
    color: #ff07b3;
    fill: currentcolor;
    position: absolute;
    right: 0;
    height: 8px;
    width: 8px;
}
.ais-SearchBox-resetIcon {
    width: 12px;
    height: 12px;
}
.ais-SearchBox-loadingIcon, .ais-SearchBox-resetIcon, .ais-SearchBox-submitIcon {
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translateX(-50%) translateY(-50%);
}
.ais-SearchBox-form {
    display: block;
    position: relative;
    display: flex;
    align-items: center;
}
.ais-SearchBox-submit {
    left: .3rem;
    color: #ff07b3;
    fill: currentcolor;
}
.ais-SearchBox-submit, .ais-SearchBox-reset {
    color: inherit;
    background-image: initial;
    background-color: initial;
    border-color: initial;
    padding: 0;
    overflow: visible;
    font: inherit;
    line-height: normal;
    background: 0 0;
    border: 0;
    cursor: pointer;
    user-select: none;
}
.ais-SearchBox-loadingIndicator, .ais-SearchBox-reset, .ais-SearchBox-submit {
    appearance: none;
    position: absolute;
    z-index: 1;
    width: 20px;
    height: 20px;
    top: 50%;
    right: .3rem;
    transform: translateY(-50%);
}
.ais-Pagination-list {
    justify-content: center;
    margin: 0;
    padding: 0;
    display: flex;
    list-style: none;
    align-items: center;
}
.ais-Pagination-link {
    padding: .3rem .6rem;
    display: block;
    border: 1px solid #c4c8d8;
    border-radius: 5px;
    transition: background-color .2s ease-out;
}
.ais-Pagination-item {
    margin-left: .3rem;
}
.ais-Pagination--noRefinement {
  display: none;
}

Other hints

Few things put onto my memo list:

  1. Snipset is the way to use when dealing with long title. Sometimes long title results in messy CSS layout, to keep stuff in correct position, we need to hide unconcerned info. By default, snipset gives empty, you need to define how many words to be shown and what mark to use for omitted words.