Search with Fuse.js

addupe

addupe

Posted on February 10, 2020

Search with Fuse.js

Fuse.js is a lightweight JavaScript search library that supports fuzzy search. The docs are great and you can play around with searching a sample of your data in the live demo. And it's open-source.

docs: https://fusejs.io/
github: https://github.com/krisk/fuse

A few weeks ago, I built an application that found a single random poem from a single word input by the user. I used an external API that already had some search functionality built in. One of the endpoints allowed me to search by line and I then worked out the randomization and format of the response data.

This week, for the purposes of learning how Fuse.js works, I made a simple React web app using an array of objects to store all of the Sonnets of William Shakespeare and installed Fuse.js to search my array of objects for a Sonnet including a given query.

here's the npm install: npm install fuse.js

We've got a simple App component that renders our Search component:

import React from 'react';
import Search from './Search';
import './App.css';

function App() {
  return (
    <Search />
  );
}

export default App;

Our Search component:

import React, { useState } from 'react';
import Highlight from 'react-highlighter';
import { fuse } from './fuse/fuse-helper';

function Search() {
  const [searchTerm, setSearchTerm] = useState('');
  const [results, setResults] = useState([]);

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

  const handleEnter = async (event) => {
    if (event.keyCode === 13) {
      const foundSonnets = await fuse.search(searchTerm);
      setResults(foundSonnets);
    }
  };

  return (
    <div style={{ margin: 20 }}>
      <h2>fuse.js</h2>
      <input
        type="text"
        placeholder="search"
        value={searchTerm}
        onChange={handleChange}
        onKeyDown={handleEnter}
      />
      {results.map((result, i) => {
        console.log('fuse matches:', result.matches);
        return (
          <div style={{ margin: 20 }} key={i}>
            <Highlight search={searchTerm} style={{ fontWeight: "bold" }}>{result.item.title}</Highlight>
            {result.item.lines.map((line, i) => {
              return (
                <div key={i}>
                  <Highlight search={searchTerm}>{line}</Highlight>
                </div>
              )
            })}
          </div>
        )
      })}
    </div>
  );
}

export default Search;

The result looks like this:

I set up a fuse folder in my src directory to hold my temporary Sonnet data and my fuse helpers. (Given a larger app made for actual production, I'd probably want this data in some kind of database, but the array storage works for our purposes). My file structure looks something like this:

src/
  fuse/
    fuse-helper.js
    sonnets.js

The structure of our sonnet storage looks like this. This is the first sonnet, stored in an array of sonnets as an object with keys title, author, lines, linecount. Every object follows this pattern.

const sonnets = [
  {
    "title": "Sonnet 1: From fairest creatures we desire increase",
    "author": "William Shakespeare",
    "lines": [
      "From fairest creatures we desire increase,",
      "That thereby beauty's rose might never die,",
      "But as the riper should by time decease,",
      "His tender heir might bear his memory:",
      "But thou contracted to thine own bright eyes,",
      "Feed'st thy light's flame with self-substantial fuel,",
      "Making a famine where abundance lies,",
      "Thy self thy foe, to thy sweet self too cruel:",
      "Thou that art now the world's fresh ornament,",
      "And only herald to the gaudy spring,",
      "Within thine own bud buriest thy content,",
      "And tender churl mak'st waste in niggarding:",
      "  Pity the world, or else this glutton be,",
      "  To eat the world's due, by the grave and thee."
    ],
    "linecount": "14"
  },
  {...},
  {...},
  {...},
];

const _sonnets = sonnets;
export { _sonnets as sonnets };

In our fuse helper file, I imported Fuse from fuse.js and the sonnet data variable. When using fuse, you have access to these 'options' which determine the parameters of the search, the sort style, the information you receive about the matches in the generated search results, and the threshold of the search. I recommend looking into these options in fuse's docs and playing around with them in the live demo. The key ones for us to pay attention to here are the keys and the threshold. shouldSort is also important, because typically, you want to sort by the closest matches. Set this to true. Keys is what lets fuse know what keys to access. Here we have title and lines, meaning that the sonnet fuse will look through our array of sonnet objects and find any sonnet that has the target word in the title key values or in the line key values. Threshold is what allows for the fuzzy search. 0.0 would mean you need an exact match and 0.6 would be the setting for fuzzy as it gets.

import Fuse  from 'fuse.js';
import { sonnets } from './sonnets';

const options = {
  shouldSort: true,
  includeMatches: true,
  threshold: 0.1,
  location: 0,
  distance: 100,
  maxPatternLength: 32,
  minMatchCharLength: 1,
  keys: [
    "title",
    "lines"
  ]
};

const fuse = new Fuse(sonnets, options);

const _fuse = fuse;
export { _fuse as fuse };

Searching for the word star yields this response:

*The highlighting effect is from react-highlighter, a pretty cool little tool.

You'll notice if you look again at the search component, that I console logged the fuse matches for each result. Let's take a look at the logs:

console.log('fuse matches:', result.matches);

If we want to see the whole fuse results array:

We see that we get back an array with the matching item and the matches (if we set the includeMatches prop to true). And we render accordingly, knowing that each found object will be listed as an item in our results array.

fin

💖 💪 🙅 🚩
addupe
addupe

Posted on February 10, 2020

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related