5
头图

Fuse.js is a lightweight search engine that can be run on the client side of the user's browser. Let's see how to use it to easily add search functionality to React applications.

When to use Fuse.js

The search function is useful for many types of websites, allowing users to efficiently find what they want. But why do we specifically choose to use Fuse.js?

There are many options to power the search, and the easiest may be to use an existing database. For example, Postgres has a full-text search function. MySQL also has it, and Redis also has the RediSearch module.

There are also some specialized search engines, of which Elasticsearch and Solr are the most popular. These search engines require more settings, but they have advanced features that your use case may require.

Finally, you can use search-as-a-service platforms such as Algolia or Swiftype These services run their own search infrastructure. You only need to provide data, configuration and query through the API.

However, you may not need the capabilities exposed by these solutions, which may require a lot of work to achieve, not to mention the cost. If there is not much data to search, Fuse.js requires minimal settings and can provide a better search experience than you might think of.

As for how much data is too much for Fuse.js, considering that Fuse.js needs to access the entire data set, you need to load it all on the client. If the size of the data set is 100MB, it is beyond the reasonable range for sending to the client. But if it is only a few kilobytes, it may be a good candidate for Fuse.js.

Build a Fuse.js + React demo application

Let's make a basic React application and use Fuse.js to let users search for dog breeds. You can here . The source code can be found on GitHub .

We will start by setting up some scaffolding. Starting with a new Node.js project, we will install React and Fuse.js:

npm install --save react react-dom fuse.js
//or
yarn add react react-dom fuse.js

We will also install Parcel as a development dependency:

npm install --save-dev parcel@2.0.0-beta.1
//or
yarn add --dev parcel@2.0.0-beta.1

We will package.json startup script to compile the application:

{  
  "scripts": {
    "start": "parcel serve ./index.html --open"
  }
}

Next, we will create a barebones index.html , which contains an empty div for React rendering, and a noscript message to avoid blank pages when the user disables JavaScript.

<!DOCTYPE html>
<html lang="en">
  <body>
    <div id="app"></div>
    <noscript>
      <p>Please enable JavaScript to view this page.</p>
    </noscript>
    <script src="./index.js"></script>
  </body>
</html>

We will make our index.js simple start. We will render a form with search query input, although we will not actually process the search yet.

import React, { useState } from "react";
import ReactDom from "react-dom";

function Search() {
  return (
    <form>
      <label htmlFor="query">Search for a dog breed:</label>
      <input type="search" id="query" />
      <button>Search</button>
    </form>
  );
}

ReactDom.render(<Search />, document.getElementById("app"));

At this point, if you run npm run start or yarn run start , Parcel should open the website in the browser and you should see this form.

Implement search

Now to implement the search, we will start with the component that displays the search results. We need to deal with three situations:

  1. When the user has not yet performed a search
  2. When there is no query result (because we don’t want users to think of some problems)
  3. When will the results be displayed

We will display all the results ordered list

function SearchResults(props) {
  if (!props.results) {
    return null;
  }

  if (!props.results.length) {
    return <p>There are no results for your query.</p>;
  }

  return (
    <ol>
      {props.results.map((result) => (
        <li key={result}>{result}</li>
      ))}
    </ol>
  );
}

Let's also write our own search function. Later, we will be able to compare the results of our simple method with the results of Fuse.js.

Our method is simple: we will traverse the dog breed array (from this JSON list ) and return all dog breeds that contain the entire search query. We also make everything lowercase so that the search is not case sensitive.

const dogs = [
  "Affenpinscher",
  "Afghan Hound",
  "Aidi",
  "Airedale Terrier",
  "Akbash Dog",
  "Akita",
  // More breeds..
];

function searchWithBasicApproach(query) {
  if (!query) {
    return [];
  }

  return dogs.filter((dog) => dog.toLowerCase().includes(query.toLowerCase()));
}

Next, let's link everything together by getting the search query from the form submission, then perform the search and display the results.

function Search() {
  const [searchResults, setSearchResults] = useState(null);

  return (
    <>
      <form
        onSubmit={(event) => {
          event.preventDefault();
          const query = event.target.elements.query.value;
          const results = searchWithBasicApproach(query);
          setSearchResults(results);
        }}
      >
        <label htmlFor="query">Search for a dog breed:</label>
        <input type="search" id="query" />
        <button>Search</button>
      </form>

      <SearchResults results={searchResults} />
    </>
  );
}

Add Fuse.js

Using Fuse.js is very simple, we need to import it, let it use new Fuse() to index the data, and then use the search function of the index. The search will return some metadata, so we will only extract the actual items for display.

import Fuse from "fuse.js";

const fuse = new Fuse(dogs);

function searchWithFuse(query) {
  if (!query) {
    return [];
  }

  return fuse.search(query).map((result) => result.item);
}

The metadata includes a refIndex integer, which allows us to go back to the corresponding item in the original data set. If we new Fuse(dogs, {includeScore: true}) , we will also get the matching score: a value between 0 and 1, where 0 is a perfect match. Then the search results for "Husky" would look like this:

[
  {
    item: "Siberian Husky",
    refIndex: 386,
    score: 0.18224241177399383
  }
]

We will Search component to let the user choose whether to use Fuse.js instead of the basic search function.

<form
  onSubmit={(event) => {
    event.preventDefault();
    const query = event.target.elements.query.value;
    const useFuse = event.target.elements.fuse.checked;
    setSearchResults(
      useFuse ? searchWithFuse(query) : searchWithBasicApproach(query)
    );
  }}
>
  <label htmlFor="query">Search for a dog breed: </label>
  <input type="search" id="query" />
  <input type="checkbox" name="fuse" />
  <label htmlFor="fuse"> Use Fuse.js</label>
  <button>Search</button>
</form>

Now we can search with Fuse.js! We can use the checkbox to compare using it and not using it.

The biggest difference is that Fuse.js can tolerate typos (by approximate string matching), while our basic search requires exact matching. If we misspell "retriever" as "retreiver", please check the basic search results.

Here are the more useful Fuse.js results for the same query:

Search multiple fields

If we care about multiple fields, our search may be more complicated. For example, imagine we want to search by variety and country of origin. Fuse.js supports this use case. When we create an index, we can specify the key of the object to be indexed.

const dogs = [
  {breed: "Affenpinscher", origin: "Germany"},
  {breed: "Afghan Hound", origin: "Afghanistan"},
  // More breeds..
];

const fuse = new Fuse(dogs, {keys: ["breed", "origin"]});

Now, Fuse.js will search both the breed and origin fields.

Summarize

Sometimes, we don't want to spend resources to build a complete Elasticsearch instance. When we have simple needs, Fuse.js can provide corresponding simple solutions. And as we have seen, it is very simple to use it with React.

Even if we need more advanced functions, Fuse.js allows to assign different weights to different fields, add AND and OR logic, adjust fuzzy matching logic and so on. When you need to add a search function to your app next time, you can consider using it.


杭州程序员张张
11.8k 声望6.7k 粉丝

Web/Flutter/独立开发者/铲屎官