Start a conversation Book intro call

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:

  1. Make index schema according to my blog’s JSON structure
  2. Fetch the blog data and load that data into the index
  3. 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)
	);
})();