Having a search function is very useful when there are more thatn just a couple of posts on a website. I had my heart set on writing my site with Hugo but I really wanted to have a search function. As it turns out this isn’t the easiest thing to do.

This post assumes you have the same directory structure and files as the ‘Hugo Build Pipeline’ post

Search Page

First off, a search page is required to actually perform a search function is required. I created a search.html page with th efollowing content.

<!-- content/search.html -->
+++
title = "Search"
cover = ""
description = ""
showFullContent = false
+++

<input id="search" type="text" placeholder="Search Term">
<hr /> 
<h3>Results:</h3>
<div id="results" class="posts"></div>

<script type="text/javascript" src="/static/js/jquery-2.1.3.min.js"></script>
<script src="/static/js/lunr.min.js"></script>
<script src="/static/js/lunr-init.js"></script>
<style>
    #search {
        border: 2px solid #ff6266;
        border-radius: 4px;
        background-color: transparent;
        color: white;
        width: 100%;
        padding-left: 15px;
        padding-right: 15px
        padding-top: 5px;
        padding-bottom: 5px;
        font-size: 25px;
    }
</style>

It should be noted stat all these files are stored within the ‘content/static/’ directory.

JavaScript

As you can see from the HTML file, there are several JavaScript files required to make this search function work. First up any version of of jQuery will work so use whatever version you like (either a CDN or a local version). Next up is Lunr JS, a JavaScript search library. You’re going to want to grap the single file so it can be included within the browser.

lunr-init.js

// content/static/linr-init.js
var lunrIndex, $results, pagesIndex;

// Initialize lunrjs using our generated index file
function initLunr() {
    // First retrieve the index file
    $.getJSON("/static/lunr-index.json")
        .done(function(index) {
            pagesIndex = index;
            console.log("index:", pagesIndex);

            // Set up lunrjs by declaring the fields we use
            // Also provide their boost level for the ranking
            lunrIndex = lunr(function() {
                this.field("title", {
                    boost: 10
                });
                this.field("tags", {
                    boost: 5
                });
                this.field("content");

                // ref is the result item identifier (I chose the page URL)
                this.ref("uri");

                // Feed lunr with each file and let lunr actually index them
                for (var i = 0; i < pagesIndex.length; ++i) {
                    this.add(pagesIndex[i]);
                };
            });

            // Feed lunr with each file and let lunr actually index them
            // pagesIndex.forEach(function(page) {
            //     lunrIndex.add(page);
            // });
        })
        .fail(function(jqxhr, textStatus, error) {
            var err = textStatus + ", " + error;
            console.error("Error getting Hugo index flie:", err);
        });
}

// Nothing crazy here, just hook up a listener on the input field
function initUI() {
    $results = $("#results");
    $("#search").keyup(function() {
        $results.empty();

        // Only trigger a search when 2 chars. at least have been provided
        var query = $(this).val();
        if (query.length < 2) {
            return;
        }

        var results = search(query);
        console.log("Search results for: " + $(this).val());
        console.log(results);
        renderResults(results);
    });
}

/**
 * Trigger a search in lunr and transform the result
 *
 * @param  {String} query
 * @return {Array}  results
 */
function search(query) {
    // Find the item in our index corresponding to the lunr one to have more info
    // Lunr result: 
    //  {ref: "/section/page1", score: 0.2725657778206127}
    // Our result:
    //  {title:"Page1", href:"/section/page1", ...}
    return lunrIndex.search(query).map(function(result) {
            return pagesIndex.filter(function(page) {
                return page.uri === result.ref;
            })[0];
        });
}

/**
 * Display the 10 first results
 *
 * @param  {Array} results to display
 */
function renderResults(results) {
    console.log("Rendering results")
    console.log(results)
    if (results === undefined || !results.length) {
        console.log("Results is undefined or 0")
        return;
    }

    // Only show the ten first results
    results.slice(0, 10).forEach(function(r) {
        if (r.title === undefined) {
            return;
        }
        var $result = $("<div>");
        $result.addClass("post").addClass("on-list");
        $result.html(`
            <h2 class="post-title"><a href="${r.uri}">${r.title}</a></h2>
            <span class="post-tags">
                ${r.tags.map(x => "<a href=\"/tags/" + x + "\">#" + x + "</a>").join("&nbsp;")}
            </span>
            <div class="post-content">
                <a href="${r.uri}">» Read More</a> 
            </div>
        `);
        $results.append($result);
    });
}

// Let's get started
initLunr();

$(document).ready(function() {
    initUI();
    console.log("Page ready")
});

Creating the Index

The hardest part of this is actually creating the index that LunrJS will search. I found that the easiest way to do this was as a part of the Docker build process. It means creating an extra step in the multi-stage buld. These two files must be placed in the same location as the Dockerfile.

# Dockerfile

# Begin build of Lunr index
FROM node:alpine as indexer
WORKDIR /app
# Copy over NPM files
COPY package.json /app/package.json
COPY package-lock.json /app/package-lock.json
# Copy the application
COPY site/ /app/site/
RUN npm install
RUN npm run index
RUN echo "Index file" && ls -la /app/site/content/static/lunr-index.json

After this stage of the build is completed, the built index can be copied out of the container and into the final NGINX image of the site. The following line is added to the container build. It copies the built index from the container named ‘indexer’ into the final container.

COPY --from=indexer /app/site/content/static/lunr-index.json /usr/share/nginx/html/static/lunr-index.json