How I added search to my site with Lunr.js
Published by Matthew Daly at 18th April 2015 2:05 pm
As I mentioned a while back, I recently switched the search on my site from Google's site-specific search to Lunr.js. Since my site is built with a static site generator, I can't implement search using database queries, and I was keen to have an integrated search method that would be fast and not require server-side scripting, and Lunr.js seemed to fit the bill.
The first task in implementing it was to generate the index. As I wrote the Grunt task that generates the blog, I amended that task to generate an index at the same time as I generated the posts. I installed Lunr.js with the following command:
npm install lunr --save
I then imported it in the task, and set up the field names:
1 var lunr = require('lunr');23 searchIndex = lunr(function () {4 this.field('title', { boost: 10 });5 this.field('body');6 this.ref('href');7 });
This defined fields for the title, body, and hyperlink, and set the hyperlink as the reference. The variable searchIndex
represents the Lunr index.
Next, I looped through the posts, and passed the appropriate details to be added to the index:
1 for (post in post_items) {2 var doc = {3 'title': post_items[post].meta.title,4 'body': post_items[post].post.rawcontent,5 'href': post_items[post].path6 };7 store[doc.href] = {8 'title': doc.title9 };10 searchIndex.add(doc);11 }
At this point, post_items
represents an array of objects, with each object representing a blog post. Note that the body
field is set to the value of the item's attribute post.rawcontent
, which represents the raw Markdown rather than the compiled HTML.
I then store the title in the store
object, so that it can be accessed using the href
field as a key.
I then do the same thing when generating the pages:
1 // Add them to the index2 var doc = {3 'title': data.meta.title,4 'body': data.post.rawcontent,5 'href': permalink + '/'6 };7 store[doc.href] = {8 'title': data.meta.title9 };10 searchIndex.add(doc);
Note that this is already inside the loop that generates the pages, so I don't include that.
We then write the index to a file:
1 // Write index2 grunt.file.write(options.www.dest + '/lunr.json', JSON.stringify({3 index: searchIndex.toJSON(),4 store: store5 }));
That takes care of generating our index, but we need to implement some client-side code to handle the search. We need to include Lunr.js on the client side as well, (I recommend using Bower to do so), alongside jQuery. If you include both, the following code should do the trick:
1$(document).ready(function () {2 'use strict';34 // Set up search5 var index, store;6 $.getJSON('/lunr.json', function (response) {78 // Create index9 index = lunr.Index.load(response.index);1011 // Create store12 store = response.store;1314 // Handle search15 $('input#search').on('keyup', function () {16 // Get query17 var query = $(this).val();1819 // Search for it20 var result = index.search(query);2122 // Output it23 var resultdiv = $('ul.searchresults');24 if (result.length === 0) {25 // Hide results26 resultdiv.hide();27 } else {28 // Show results29 resultdiv.empty();30 for (var item in result) {31 var ref = result[item].ref;32 var searchitem = '<li><a href="' + ref + '">' + store[ref].title + '</a></li>';33 resultdiv.append(searchitem);34 }35 resultdiv.show();36 }37 });38 });39});
This should be easy to understand. On load, we fetch and parse the lunr.json
file from the server, and load the index. We then set up an event handler for the keyup
event on an input with the ID of search
. We get the value of the input, and query our index, and we loop through our results and display them.
I was pleased with how straightforward it was to implement search with Lunr.js, and it works well. It's also a lot faster than any server-side solution since the index is generated during the build process, and is loaded with the rest of the site, so the only factor in the speed of the response is how quick your browser executes JavaScript. You could probably also use it with a Node.js application by generating the index dynamically, although you'd probably want to cache it to some extent.