Full-Text Search with Minisearch and Alpine.js

31 May 2021colophoniaweb-programming

One thing I wanted to add to my new and shiny Pollen website was full-text search. I have about 200 old blog articles on here, and it would be convenient to be able to find things now and then. It seemed like an easy enough thing to do, especially since I only intended to do it at the level of software bricolage, using existing tools.

So, full text search! Sounds exciting, eh? You can see it in action by clicking on the magnifying glass icon in the navigation bar (or hamburger menu, if you’re on mobile).

To do this, I needed two things. First, a search library, which means some stuff to allow you to build an index, and then to search in that index. And that “stuff” should probably all be in JavaScript since the search part is going to run in the browser. Second, some more JavaScript to make a nice dynamic search page.

Minisearch

There are loads of JavaScript search libraries around. I didn’t have a whole lot of grounds for choosing one over the rest. After a spot of meandering around between the options, I just stuck a pin in the list and ended up with Minisearch. Well, I was influenced by “tiny”, which sounded good, so it wasn’t a completely random choice. Anyway, I ended up being pretty happy that I used Minisearch. It works, it’s easy to use, it’s well-documented. More or less everything you could ask for!

I had originally planned to build the search index from the Pollen sources for the pages I wanted to index, but I realised quite quickly that that was a stupid idea. It’s much easier to index the HTML pages generated by Pollen, because there are plenty of tools around for stripping tags out of HTML pages to get just the text, which is needed for indexing. If I’d worked from the Pollen sources, I’d have needed to parse the files myself (possible with stuff included with Pollen, but it would have taken more time than the solution I ended up with).

I ended up with a little JavaScript script that iterates over a list of Pollen pagetree files, each of which has a list of HTML output files in it. For each HTML file, I extract a little bit of metadata from the corresponding Pollen source file, then read in the HTML, strip the tags (using the striptags package) and call Minisearch’s add method to add the entry to the index. It’s really nice and simple. Minisearch produces a single JSON index file as output, which my web server just serves up as a static file.

On the client side, once you’ve got hold of the search index (more on that in a minute), you only need to call one method (called, surprisingly enough, "search”) to perform a search, and the results come back in a format that’s easy to use for results output.

I don’t know how the other JavaScript full-text libraries stack up, but Minisearch is a pleasure to use.

Alpine.js

I wanted to have a dynamic search page, which meant writing some JavaScript. I thought about doing it with vanilla JavaScript or jQuery, but this sort of thing cries out for some kind of dynamic binding approach. I really didn’t want to use React or Vue or anything like that though. Overkill is all very well, but I wanted to at least slightly enjoy doing this, and my previous contact with React and Vue hasn’t been all puppies and rainbows. So I went a-googlin’ and a quick search for “minimal JavaScript framework” turned up Alpine.js.

This looked just the ticket. They had me more or less from the moment when they gave a CDN link to put in your pages and the rest of the installation instructions were “That’s it. It will initialize itself.” After suffering extensive trauma at the hands of various web bundler things over the years, this was a very soothing thing to see.

And it was the real deal. The thing I’ve found in the past with frameworks like React and Vue is that once you get over the agony and self-abuse involved in getting the build tools to work, the frameworks themselves aren’t bad to use. Alpine.js shortcuts the build system self-abuse and gets you straight to the good stuff.

You can see the whole of my search page here. It’s written in a comical combination of HTML all done as Pollen tags and JavaScript stuck inside a script tag inside my Pollen code. I think it’s pretty sweet.

It works, it’s performant enough for my needs, and I’m a very happy Alpine.js user. The performance might be something to give you pause if you want to do something big, because Alpine.js doesn’t have a virtual DOM implementation, so you probably won’t enjoy using it for anything that does any kind of DOM churning. For the size of application I used it for though, it’s perfect. It gives you all the benefits of reactive bindings in your JavaScript without any of the ceremony that’s needed to use React or Vue.

My search page wasn’t completely trivial, since it needed to lazily load the search index when the page was first visited. I was worried this was going to be unpleasant, but it worked perfectly nicely.

The end result

Overall, I’m quite pleased with the outcome. Getting it all working was an outstandingly painless operation. Over years of disappointment, frustration and swearing at recalcitrant collections of electrons, “painless” has become my standard for the zenith of technological perfection. I’ve had a couple of those “painless” experiences recently, and I treasure them!

So, top marks to Minisearch, and top marks to Alpine.js, which is as of now my favourite JavaScript framework, and I already have at least two jobs I’m going to use it on. There’s also a state management library called Spruce that goes with it. Might have to check that out too!