Client side search

Last update in 9/2019

I was recently in search for this blog's search. Tried Algolia, it was great, but it didn't fit my needs well. I have even made article about it, Rethinking fuzzy search. If you want to know why, read that article, but if you want to see how, continue reading this.

Prerequisites #

Goals #

First thing you have to do is, as expected, install fuse.js.

yarn add fuse.js

While yarn is doing its work, you can import libraries to file. Import from Gatsby is optional because that is way I get my search data and will probably vary. Just make sure your data is array!

import Fuse from 'fuse.js'
import React, { useState, useEffect } from 'react'
import { useStaticQuery, graphql } from 'gatsby'

Next thing is to get data for search. Like I already said, process of getting this data will vary from app to app. As example, I am using GraphQL and Gatsby static query to get array of articles.

export default () => {
const data = useStaticQuery(graphql`
{
allMarkdownRemark {
nodes {
frontmatter {
title
tags
date(formatString: "MMMM Do, YYYY")
}
fields {
slug
timestamp
views
}
id
excerpt(pruneLength: 1000000)
}
}
}
`
)

const allResults = data.allMarkdownRemark.nodes

// allResults looks similar to this:
//
// [
// {
// frontmatter: { ... }
// fields: { ... }
// id
// excerpt
// },
// ....
// ]
}

After getting data in correct format you'll need to initialize state for results and search input.

const [results, setResults] = useState(allResults)
const [query, setQuery] = useState('')

You'll need to define fuse and it's options. These are options that work for me. Feel free to play around with search later and tweak to your liking.

const options = {
shouldSort: true,
includeScore: true,
includeMatches: true,
threshold: 0.33,
location: 0,
distance: 100,
maxPatternLength: 32,
minMatchCharLength: 2,
keys: [
{ name: 'frontmatter.title', weight: 1 },
{ name: 'frontmatter.tags', weight: 0.75 },
{ name: 'frontmatter.excerpt', weight: 0.5 },
],
}

const fuse = new Fuse(allResults, options)

Last thing you have to do for search to work is to actually run it on every change of query. Also, if user empties input field set fallback to all results.

useEffect(() => {
setResults(query ? fuse.search(query) : allResults)
}, [query])

That is it. Your search is working, when you change query, results will be filtered. But you'll need to build some UI to actually see the magic. Let's start with input. It just binds its value to query state.

<input
type='search'
value={query}
onChange={event => setQuery(event.currentTarget.value)}
/>

And to see results you will map over result array from state. Notice line 4 in this snippet. It is important because fuse.js returns original data in item property.

{results.length && (
<ul>
{results.map(article => {
const { id, frontmatter } = article.item || article // <= this is important
const { title, date } = frontmatter
return (
<li key={id}>
<h2>{title}</h2>
<p>{date}</p>
</li>
)
})}
</ul>
)}

While you are at it, you can also handle situation when search has no results.

{!results.length && query && <h2>No results</h2>}

And that is now it. Everything should be working great. Don't forget to play with search and adjust search options!

Whole component for reference can be found in this gist.