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!

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
(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)
  );
})();