Simple In-site Blog Search
Initially, my website relied on common search engines for search functionality. Using a search engine wasn’t a good user experience because there would be gaps in the search due to search indices updating. To solve this problem, I’ve integrated a full text search into this website. The search engine would take all my content at build time, so I wouldn’t have to wait for a search engine to update.
I stumbled across Flexsearch, and it had an elevator pitch of a fast and memory flexible full text search. These are great qualities to have because a website should be fast for the user, but also useful so that the user could potentially find something later.
Flexsearch has five different options: “bundle”, “light”, “compact”, “es5”, and “es6”. I landed on the “bundle” option so that I could load it into the footer of my Hugo website and automatically run. I didn’t use NPM because it is a little extra work to get NPM modules working well with Hugo. As well, I didn’t use “light” or “compact”, because I wanted to load all the features in my website to use at a later date.
Here’s how I implemented it:
- Make index schema according to my blog’s JSON structure
- Fetch the blog data and load that data into the index
- Create a search widget that would display the search data for the user.
Flexsearch is great to have in my website because of its performant and user-friendly nature. It did take a moment to figure out how to integrate Flexsearch into my website, but now I don’t have to rely on search engines.
Here’s the code that creates an index and search!
(async () => {
function debounce(func, wait) {
let timeout;
return function (...args) {
clearTimeout(timeout);
timeout = setTimeout(() => {
func.apply(this, args);
}, wait);
};
}
const searchInput = document.getElementById('search');
const searchResults = document.getElementById('search-results');
const searchDoc = FlexSearch.Document({
document: {
id: 'url',
index: ['title', 'description', 'url', 'content']
}
});
const data = await fetch('/index.json').then((response) => response.json());
for (const post of data) {
searchDoc.add({
title: post.title,
description: post.description,
url: post.url,
content: post.content
});
}
searchInput.addEventListener(
'input',
debounce((event) => {
const searchResult = searchDoc.search(event.target.value);
const reduceSearchResult = searchResult.reduce((acc, current) => {
const mapField = current.result.flat().map((item) => ({
field: current.field,
url: item
}));
acc.push(...mapField);
return acc;
}, []);
searchResults.innerHTML = '';
for (const { url, field } of reduceSearchResult) {
const pContainer = document.createElement('p');
const link = document.createElement('a');
const matchByUrl = data.find((item) => item.url === url);
link.href = url;
link.textContent = matchByUrl.title;
pContainer.appendChild(link);
searchResults.appendChild(pContainer);
}
}, 300)
);
})();