Get Started with Algolia

Not long after getting my site set up, I decided that I wanted to have an easy way to search for things, especially considering that the site serves as my knowledge base for tech-related topics.

Not long after getting my site set up, I decided that I wanted to have an easy way to search for things, especially considering that the site serves as my knowledge base for tech-related topics.

There was a simple way to get started with Docusaurus, since it has built-in support for Algolia DocSearch. I had some issues with going that route though:

  1. I wanted to customize the appearance / functionality of the search component
  2. I wanted to better understand how the search worked
  3. My site's repo isn't public, so it wouldn't qualify for the free DocSearch application

And thus started my journey to manually integrate my site with Algolia.

Account Creation

First, create an Algolia account. Once you do this, you can create your first index, which is a place where all of your records will be stored. I created two different indexes - one for testing, and one for production.

Populate the Index

Algolia stores a bunch of records in each index, and you're searching through those records when you integrate with the service. The first question I had was "how do I use these records on my site?"

You can read the docs here, but essentially you have to decide how you want to structure the data you're attempting to search through. For example, in my case, since I'm searching through blogs and references, I decided to use the following:

{
  "url": "string",
  "title": "string",
  "description": "description",
  "tags": ["string"],
  "section": "string",
  "content": "content"
}

The title and description are searchable, while the url is used to determine which page to send someone to when they click on a search result.

After determining what my record structure was going to be, I built a web crawler with python that goes through each page of my site, breaks down the html elements, and creates the records within Algolia.

If you don't want to use python, you have a number of other client options. You could also import a file or add items manually if you prefer.

Build a Search Component

Once all of my records were created, I started building the actual search component that will be used on my site. Docusaurus uses React, so that's what I'm using here.

Swizzle Search

Per the docs, first you have to swizzle the search component. With the classic template, there wasn't actually a search component, so I chose the "wrap" option and built the UI using Material UI

npm run swizzle @docusaurus/theme-classic SearchBar

Build the Search Box

There are a couple options you have when building the search box. Algolia provides pre-built components through the algoliasearch and react-instantsearch-hooks-web libraries. They also have options to use hooks, rather than the components, and that's what I decided to do. You can view their documentation here.

The main items you need to start searching are algoliasearch, InstantSearch, SearchBox (or useSearchBox()), and Hit (or useHit()).

algoliasearch is what allows you to make a connection to your account, and you specify the application id / api key during creation.

InstantSearch is the "root" component that takes an algoliasearch client and the index you'll be searching through.

SearchBox / useSearchBox() is what sends requests to Algolia.

and Hit / useHit() contains a list of records or "hits" returned by the search.

A basic structure may look like this:

const client = algoliasearch(app, token);
return (
  <InstanSearch searchClient={client} indexName={index}>
    <SearchBox />
    <Hit hitComponent={MyHitComponent}>
  </InstantSearch>
)
⚠️
When I started working with Hit, I found that results were displayed immediately (before typing anything) and decided to move the search box / hits to a component separate from the search bar displayed in the top navigation. This seems to be the same thing that Material UI and Docusaurus are doing

I decided to build custom search box and hit components, but my structure looked similar:

// Search Modal
const client = algoliasearch(app, token);
return (
  <InstanSearch searchClient={client} indexName={index}>
    <SearchBox />
    <SearchResults onNavigate={onNavigate} />
  </InstantSearch>
)

// SearchResults
const hits = useHits();
return (
  <div>
    {hits.map((hit) => <SearchHit hit={hit} onNavigate={onNavigate} />)}
  </div>
)

// SearchHit
return (
  <Card>
    ...
  </Card>
)

If you notice, there's one piece that's missing from the code above: SearchBox. I wanted to address that in a bit more detail. At it's core, SearchBox is just a text field, but there are a couple small things to note:

export default function SearchBox() {
  const memoizedSearch = useCallback((query, search) => {
    search(query);
  }, []);

  const { refine } = useSearchBox({
    queryHook: memoizedSearch,
  });

  const handleChange = (event) => {
    refine(event.target.value);
  };

  return (
    <TextField
      ...
      onChange={handleChange}
    />
  );
}

First, onChange is just a normal handler that gets called when the text changes inside of the text field. That calls refine() though, which updates the query text sent to algolia.

refine comes from useSearchBox() and initiates a search every time it's called. The "search" that actually gets executed is defined in memoized search, which is passed in as the queryHook to useSearchBox.

The overall chain of events becomes: The user changes text in the text field (on change) => The query is modified for algolia (refine) => The search is executed (memoizedSearch).

⚠️
If not using a memoized function / callback, I ran into an infinite render, even if the text field never changes

The nice thing is that's really all that's required to utilize search. The InstantSearch component seems to take care of everything between the useSearchBox() and useHit() hooks, so there's nothing I had to do to update the hits in SearchResults when the query was executed in SearchBox.

At this point, all of our records are in algolia and they're displayed in our search component. The next thing we need to do is handle the click of a record so we navigate to the correct page.

Since I have the url in each record, this becomes pretty simple. It could vary depending on which framework you're working with, but since I'm building this in a Docusaurus site, the easiest thing was to use the Link component. In a typical React app, you might use React Router or some sort of state management (if you're not using routes)

Within the Card component from the code above, I just wrapped everything in a Link and removed the host from the location:

<Link
  to={hit.url.replace("https://kevinwilliams.dev", "")}
  onClick={onNavigate}
>
  ...
</Link>
⚠️
If you notice the onNavigate floating around, that's because clicking a result didn't always cause a refresh or remove the search modal, so in those cases, I'm using a callback to hide the modal manually

Override CTRL+K

One thing I liked from the Docusaurus version, and even the MaterialUI site, is the fact that you can hit CTRL + K and it opens their algolia search boxes. I wanted to do the same thing on my site, and did so with help from this post on dev.to

useEffect(() => {
  const ctrlk = (event) => event.ctrlKey && event.key === "k";

  const searchHandler = (event) => {
    if (ctrlk(event)) toggleSearch();
  };

  const searchIgnore = (event) => {
    if (ctrlk(event)) event.preventDefault();
  };

  document.addEventListener("keyup", searchHandler);
  document.addEventListener("keydown", searchIgnore);

  return () => {
    document.removeEventListener("keyup", searchHandler);
    document.removeEventListener("keydown", searchIgnore);
  };
}, []);

Overall, it wasn't too difficult to get started. Try it out by hitting CTRL+K and let me know what you think!