Search Icon, Magnifying Glass

Marmanold.com

Graduation Cap Heart Question Mark Magnifying Glass

Full-Text Search using Hugo & Lunr

Adding full-text search to a statically generated Hugo site is a fairly easy process. As I’ve mentioned before, I’m already using Gulp to compile and minify my site. Using that Gulp file as my starting point, adding full-text search is a simple three-part process.

To enable search, I decided to use the Lunr.js library. Lunr is simple to use and has just the right amount of features for adding some simple search capabilities to your site. To start, you’ll need to create a JSON index of your site for Lunr to use. I put this layout under _default and called it search.json.

Search JSON Template

[
    {{ range $i, $e := .Site.RegularPages }}
        
    {{- if $i }}, {{ end }}
        {
            "uri": {{ .Permalink | relURL | jsonify }},
            "title": {{ .Title | jsonify }},
            "content": {{ .Plain | jsonify }},
            "tags": [
                {{ range $ii, $ee := .Params.tags }}
                {{ if $ii }}, {{ end }}
                    {{ $ee | jsonify }}
                {{ end }}
            ]
        }
    {{- end }}
]

Though the above index worked fine, Lunr was having to parse the JSON file client-side with each search. This led to a search experience that wasn’t as quick as I wanted. Because I already had a Gulp file as part of my build process, I decided to add a quick step to precompile the Lunr index. The below gulp step takes a few seconds to run, but the resulitng Lunr index allows for near-instant searches for clients. The file is a little large, so if you aren’t already compressing all of your files with GZIP, you really should start.

Gulp File

var gulp = require('gulp');

var lunr = require('lunr');
require("lunr-languages/lunr.stemmer.support")(lunr);
require('lunr-languages/lunr.multi')(lunr);
require("lunr-languages/lunr.de")(lunr);

gulp.task('lunr-index', () => {
  const documents = JSON.parse(fs.readFileSync('public/search/index.json'));

  let lunrIndex = lunr(function() {
        this.use(lunr.multiLanguage('en', 'de'));

        this.field("title", {
            boost: 10
        });
        this.field("tags", {
            boost: 5
        });
        this.field("content");
        this.ref("uri");

        documents.forEach(function(doc) {
            this.add(doc);
        }, this);
    });

  fs.writeFileSync('static/js/lunr-index.json', JSON.stringify(lunrIndex));
});

With the Lunr index precompiled and uploaded as a static asset, all that remains client-side is to use a little Javascript to fetch the index and use it to initialize Lunr.

Search Javascript

'use strict';

var lunr = require('lunr');
require("lunr-languages/lunr.stemmer.support")(lunr);
require('lunr-languages/lunr.multi')(lunr);
require("lunr-languages/lunr.de")(lunr);

const responseLunr = await fetch('/js/lunr-index.json', {
    method: 'GET',
    headers: {
        'Accept': 'application/json, application/xml, text/plain, text/html, *.*',
        'Content-Type': 'application/json'
    },
    mode: 'cors'
});

let preBuilt = await responseLunr.json();
let lunrIndex = lunr.Index.load(preBuilt);

Using this method I’ve been able to add quick, full-text search to this site. The only downside thus far has been an odd Webpack bug that causes the search Javascript to appear to have invalid characters in Safari.