Building jargons.dev [#2]: The Dictionary Search Engine

Olabode Lawal-Shittabey - Aug 19 - - Dev Community

What is a Dictionary without a search engine or ummm the search feature!?

During the implementation of the base dictionary, I had created these static Search forms (one on the homepage and the other on the Navbar used on the word layout) in preparation for this particular feature.

Static Homepage Search Form Trigger

Static Navbar Search Form Trigger

I just needed to pick-up from right there and get it working, easy work β€” if only that was true.

Something from the past

It is important to re-iterate that my initial plan was to build jargons.dev with Nextra as I admitted in the initial commit that:

...Nextra (this was infact my knight in shiny armor, I was looking to build with Nextra).

I am a React βš›οΈ fan boy, big on Next.js; Nextra is a content-focus web framework that is built on Next.js. So I guess you can just understand why Nextra sounded like a that knight. During my initial exploration of Nextra, one feature stood out to me; the full-text search β€” I drooled 🀀over this one (I must confess).

The feature was powered by flexsearch β€” a zero-deps full-text search library; ooh boy I'm a big fan of lightweight and no/low dependencies. I dug into how Nextra uses this to index content at build-time for search; it was interesting.

So!?

I found myself hacking with flexsearch during my early encounter with Astro; as I followed the astro doc's build a blog tutorial, I went a notch further to implement a search feature very easily.

So, the experience from this implementation; I passed down to the search engine for jargons.dev.

The Search Engine

The task was pretty simple, I needed to..

  • Get access or call it reference to all files inside of the dictionary directory of words - at this point it was the src/pages/word directory
  • Get these files content indexed with flexsearch
  • Plug in the search form and boom πŸ˜‰

Looks very easy! Maybe for the search indexing and actual searching, it was; but there was some lot of stuffs that went into getting there.

Integrating the first "Island" in jargons.dev

Astro by default takes a server-first approach, meaning it builds your site's HTML/CSS on server removing all client-side javascript (JS) automatically (unless you state other wise). Removing all JS assures performance improvement, but No JS means no interactivity; But if you want interactivity, Astro Island is one of the ways to go. I need the interactivity for the Search Engine so Island it is!

What's an "Island" though!?

I will put simply that an Island is an isolated interactive piece of component on a web page, whose HTML/CSS are rendered on the server-side and/but it's client-side javaScript are (hydrated) also bundled with it - NOT removed.

I gave a talk about Island at TILConf'24, Check it out to learn more.

Astro's offering

Astro offered support for integrating Islands out the box with my favorite UI library (yea, you guessed it, React) of many others. This allowed me build my static Search forms into a functional stuff.

Stuffs I did

  • I started by adding the integration module (@astrojs/react) for the Island I needed to integrate; done pretty easily with the npx astro add react command
  • I transferred all the static search forms into single React component (these are two differently sized forms); configured the component to render these at required size based on given props.
  • I also implemented some sub-component within that are only consumed locally in the-same search component, these are...
    • The SearchDialog - main component where the search operation is carried out
    • The SearchResult component, etc...
  • I implemented some custom keyboard shortcuts and keybinds that enables interactions with the search component (I'd like to call this "Search Island" from now on), these are...
    • CTRL+K or ⌘K to start search
    • ESC to close search
    • ...and base required navigation buttons to navigate within search results
  • I also added a few custom hooks to allow smooth sailing in the working of the search island, these are...
    • useLockBody - a hook that disables scrolling once the search dialog is opened
    • useRouter - a hook that I made as wrapper around some window.location methods, making them to feel like the known router libs in React, this is a hook I particularly consumed on the ENTER button click handler in the navigation button keybind on search results component in the search island.
    • and useIsMacOS - which checks whether a machine is MacOS in order to determine the appropriate description text to render on the search form trigger; i.e. CTRL+K or ⌘K
  • I added the imperative module - flexsearch;
  • I retrieved access to files the directory of words using the Astro.glob() function super easily (too bad I couldn't talk about how powerful this function is; how glad I am it existed out-the-box in Astro and how much ease it brought into the flow of getting this search engine up and running) and plugged the returned array of word objects into a $dictionary state (maybe I should call this a store) powered by nanostore (another beautiful stuff right there)
  • This $dictionary are then indexed with flexsearch, prepping them for later search.

Another Imperative feature: The Recent Searches

This is another imperative feature that I must talk about; This feature keeps track of searched items and stores them in localstorage to persist them on page reload; these store searched items are then render in a list on the homepage of the dictionary.

Recent Search Feature

It also took integration as an Island, couple with a holding the value in a nanostore powered $recentSearches state.

My implementation of this feature isn't exactly perfect, and here are a list of some Issues that needed fix (at time of writing) to take it a step further down that route (even though we can never reach perfection, YEA for sure)

The PR

This is some long read now, I wish to keep this reads short... Here's the PR

feat: implement dictionary search engine #5

This Pull Request implement the search functionality to the dictionary project. It uses @astro/react integration to power the Islands coupled with nanostore for state-management and flexsearch as text search library.

Changes Made

  • Added the following astrojs integration and lib required to text search
    • @astrojs/react
    • @nanostores/react
    • flexsearch
  • Implemented the Search Island (a react component) within where other sub components are implemented for internal usage
    • Implemented the SearchTrigger component which render a search field of two different sizes and used in two different places on the web page...
      • size md - used on the main page of the web app
      • size sm - used on the dictionary word layout navigation section
    • Implemented the SearchDialog component, which renders only when the SearchTrigger is clicked
    • Implemented the SearchInfo component, renders as default placeholder when no search term has been inputted in form field
    • Implemented the SearchResult component, renders either search results or a message for a search results not found
    • Implemented keybinds within Search island to allow the following operation with the stated keyboard shortcuts
      • CTRL+K or ⌘K to open the search dialog without clicking on the search tigger
      • ArrowUp, ArrowDown and Enter to allow navigation on the Search results list
      • ESC to allow closure of search dialog
    • Added the custom hooks for consumption on the Search island
      • useIsMacOS - check whether current user is browsing the web app with a MacOS machine; this is used to determine the appropriate short to render on the search trigger; i.e. CTRL+K or ⌘K
      • useLockBody - used to disable current viewport from scrolling when search dialog is opened
      • useRouter - (instead of adding react-router to deps) this hook wraps around window.location and uses the assign object as push; mainly used in the SearchResult component to route to selected/clicked result page
    • Implemented searchIndexing on Search island with flexsearch's Document method as preferred option
      • Added a new search store for managing the search-related states with nanostores and @nanostores/react integration
      • Added the following store values and actions
        • $isSearchOpen - global state for managing the state of SearchDialog
        • $recentSearches - state for keeping track of recently searched words; it works in collab with the localStorage to persist its value even after tab reloads
        • $addToRecentSearchesFn - a store action that adds new item to $recentSearches store value
  • Added a $dictionary store for managing the entire dictionary entries; keeping it accesible to the client and used as value for searchIndex in the Search island
    • Computed value for the dictionary store as early as possible from the layout/base with the Astro.glob() method indexing the entire dictionary directory
  • Added the RecentSearches island which reads the value from the $recentSearches store and renders it on the homepage

Screencast

Full Demo

screencast-bpconcjcammlapcogcnnelfmaeghhagj-2024.03.25-13_32_30.webm

πŸ“–

. . . . . .
Terabox Video Player