Add Fuzzy Search to Your Web App with this Open Source Tool
Tags: opensource, fuzzysearch, showdev, nextjs
Published: 2021-10-08 15:42:38

As Developer Advocate, I’m not writing production code daily anymore, but I want to continue to code. My goal is to code at least twice a week. I’ve been able to remedy this issue by:

- Contributing to open source
- Completing freelance projects

My manager suggested that I also create my own project, 

After brainstorming with my manager, I built CuteMoji, a static web application that allows you to search for GitHub Emojis. Since I've built a ton of static web applications before, I used technologies I had less experience with such as, Next.js, Tailwind CSS, SWR, and GitHub Actions. 

## My problem

After a few hours, I had a static web page with a search bar, and if you searched for the exact word, an emoji would appear. However, I realized this was a bad user experience. What if the user misspells a word?

I considered building my own algorithm to handle user errors. If I built an algorithm, I might spend more time maintaining and improving the algorithm than the actual web application, which was far from ideal since I plan to add more features to this project.

I also looked at other tools to handle the search, such as ElasticSearch and Algolia, but the setup needed was beyond my use case.

A day later, I came across the perfect, lightweight, open-source solution to implement fuzzy search: Fuse.js. It has zero dependencies, and it can work on the front end and back end.

## What is fuzzy search?

Fuzzy searching happens when you type in a search engine like Google, and the search engine dynamically provides suggested results as you type. It's an algorithm that implements approximate string matching, which means the algorithm will return results similar to your search, but they don't need to be an exact match.

## Setting up my components

I started with two empty components. 

- **SearchBar.js** - This would act as a search bar where users would type in certain emojis they were looking for.

```javascript
import React from 'react';
 
const SearchBar = () => {
   return (
       <div className="p-8">
           <div className="bg-white flex items-center rounded-full shadow-xl">
               <input name="search"
                   type="search"
                   placeholder="Search for an emoji. Ex: heart"
                   autoComplete="off"
                   className="rounded-l-full w-full py-4 px-6 text-gray-700 leading-tight focus:outline-none"
                   id="search"
               />
               <div className="p-4">
               </div>
           </div>
       </div>
   );
}
 
export default SearchBar;
```

- **EmojiCards.js** - This is where all returned emoji results would appear. I planned to limit the amount of emojis because the more results I showed, the less relevant it would be.

```javascript
import React from 'react';
 
const EmojiCards = () => {
   return (
       <div>
       </div>
   );
}
 
export default EmojiCards;
```
I put both of these components in a parent component, which I unimaginatively named  **SearchWithEmojiCards.js**.

```javascript
import SearchBar from './SearchBar';
import EmojiCards from './EmojiCards';
 
const SearchWithEmojiCards = () => {
 
   return (
       <div className="w-3/6">
           <SearchBar />
           <EmojiCards />
       </div>
   );
}
 
export default SearchWithEmojiCards;
```
I chose to import and instantiate Fuse.js in the parent component and pass the necessary data I received from Fuse.js to EmojiCards.js and SearchBar.js as props.

## Implementing Fuse.js

The steps:

**1.** I installed the npm package.
     `npm install fuse.js`
**2.** I imported the Fusejs package into the SearchWithEmojiCards.js component.

```javascript
import React, { useState } from 'react';
import SearchBar from './SearchBar';
import EmojiCards from './EmojiCards';
import Fuse from 'fuse.js';
``` 
**3.** My data set was initially an object, but Fuse.js expects an array of objects. I used the Object.entries method and map to produce the desired result. I also explicitly defined the names of each key as `emoji_name` and `emoji_url`.
```javascript
const formattedData = Object.entries(data).map((entry) => ({ emoji_name: entry[0], emoji_url: entry[1] }));
``` 
**Example of how my data originally looked:**

```json
 {
  "+1": "https://github.githubassets.com/images/icons/emoji/unicode/1f44d.png?v8",
  "-1": "https://github.githubassets.com/images/icons/emoji/unicode/1f44e.png?v8",
  "100": "https://github.githubassets.com/images/icons/emoji/unicode/1f4af.png?v8",
  "1234": "https://github.githubassets.com/images/icons/emoji/unicode/1f522.png?v8",
}

``` 

**Example of the data after reformatting:**


```json
[
  {
    "emoji_name": "+1",
    "emoji_url": "https://github.githubassets.com/images/icons/emoji/unicode/1f44d.png?v8",
  }
  {
    "emoji_name": "-1",
    "emoji_url": "https://github.githubassets.com/images/icons/emoji/unicode/1f44e.png?v8",
  }
]
``` 

**4.  **I passed the formatted API data into the SearchEmojiWithCards.js component. After that, I created a new fuse search instance at the top of my component. 


- In the example below, I pass my newly formatted emoji API data into the Fuse instance, and I pass in an options object. 
- The options object specifies the key I want users to search by `emoji_name`.
- The returned results get scored by relevance. `includeScore` indicates that the score will be included and returned in the data.


```javascript
import React, { useState } from 'react';
import SearchBar from './SearchBar';
import EmojiCards from './EmojiCards';
import Fuse from 'fuse.js';
 
const SearchWithEmojiCards = ({ apiData }) => {
   const options = {
       keys: [
           "emoji_name",
       ],
       includeScore: true
   };
   const fuse = new Fuse(apiData, options);
   
   return (
       <div className="w-3/6">
           <SearchBar />
           <EmojiCards />
       </div>
   );
}
export default SearchWithEmojiCards;

``` 

**5.** To perform a search, I added the fuse.search() method. The search method will search through my entire data collection and return a result based on the user's query. I  also leveraged the `useState` hook to capture the results of the user's query.


```javascript
import React, { useState } from 'react';
import SearchBar from './SearchBar';
import EmojiCards from './EmojiCards';
import Fuse from 'fuse.js';
 
const SearchWithEmojiCards = ({ apiData }) => {
   const [query, setQuery] = useState('')
   const options = {
       keys: [
           "emoji_name",
       ],
       includeScore: true
   };
   const fuse = new Fuse(apiData, options);
   const results = fuse.search(query);
 
   return (
       <div className="w-3/6">
           <SearchBar query={query} setQuery={setQuery}/>
           <EmojiCards />
       </div>
   );
}
 
export default SearchWithEmojiCards;
```

**6.** In the SearchBar component, I set the state of the user's input with the setQuery()  function and set the state's input variable, `query`, to the value of the input. 

```javascript
import React from 'react';
 
const SearchBar = ({ setQuery, query }) => {
   function handleOnSearch({ currentTarget = {} }) {
       const { value } = currentTarget;
       setQuery(value);
   }
   return (
       <div className="p-8">
           <div className="bg-white flex items-center rounded-full shadow-xl">
               <input name="search"
                   type="search"
                   placeholder="Search for an emoji. Ex: heart"
                   autoComplete="off"
                   className="rounded-l-full w-full py-4 px-6 text-gray-700 leading-tight focus:outline-none"
                   id="search"
                   value={query}
                   onChange={handleOnSearch} />
               <div className="p-4">
               </div>
           </div>
       </div>
   );
}
 
export default SearchBar;

``` 

To see what the data output looked like, I printed the data as a `console.log()` each time a user searched for an emoji.

**Here's an example of the output:**

```json
[
  {
    "item": {
      "emoji_name": "+1",
      "emoji_url": "https://github.githubassets.com/images/icons/emoji/unicode/1f44d.png?v8",
    },
    "refIndex": 0,
    "score": 0.03,
  },
  {
    "item": {
      "emoji_name": "-1",
      "emoji_url": "https://github.githubassets.com/images/icons/emoji/unicode/1f44e.png?v8",
    },
    "refIndex": 0,
    "score": 0.04
  }
]
``` 
**7.** I mapped through the array to retrieve the item objects. However, if the user didn't search, emoji results wouldn't show, and my page looked bare! I decided to write a conditional statement: if the user inputs a value into the search bar, then Fuse.js could deliver those specific search results, else show a few default emojis. I passed the data into my EmojiCards.js component so that I could further modify the data.


```javascript
import React, { useState } from 'react';
import SearchBar from './SearchBar';
import Fuse from 'fuse.js';
import EmojiCards from './EmojiCards';
 
const SearchWithEmojiCards = ({ apiData }) => {
   const [query, setQuery] = useState('')
 
   const options = {
       keys: [
           "emoji_name",
       ],
       includeScore: true
   };
   const fuse = new Fuse(apiData, options);
   const results = fuse.search(query)
   const emojiResult = query ? results.map(result => result.item) : apiData;
 
   return (
       <div className="w-3/6">
           <SearchBar query={query} setQuery={setQuery} />
           <EmojiCards emojiResult={emojiResult} />
       </div>
   );
}
 
export default SearchWithEmojiCards;
``` 

**8.** In the EmojiCards.js component, I mapped through the data and limited it to only the first six most relevant results. I styled the cards as desired. 


```javascript
import React from 'react';
 
const EmojiCards = ({ emojiResult }) => {
   return (
       <div>
           <ol className="flex row justify-center">
               {emojiResult.slice(0, 6).map((emoji, value) => (
                   <li key={value} className="p-4 rounded-md w-max my-8 mx-3 shadow-2xl text-center" >
                       <img className="m-auto w-16 h-16" src={emoji.emoji_url} alt={emoji.emoji_name.replaceAll('_', ' ')} />
                       <span>{emoji.emoji_name.replaceAll('_', ' ')}</span>
                   </li>
               ))}
           </ol>
       </div>
   );
}
 
export default EmojiCards;
``` 

**Here’s a screenshot of the final result:**

![Screen Shot 2021-10-07 at 11.01.14 PM.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1633698190496/kHSt8HXka.png)

The  [site]( [http://s3-website-bucket-43d5014.s3-website-us-east-1.amazonaws.com/] )  is hosted live on an AWS S3 Bucket via Pulumi. Feel free to try it out!

Incorporate Fuse.js into your project by following the [official documentation](https://fusejs.io/) .

Hoping to contribute to Fuse.js? Check out the  [repo](https://github.com/krisk/fuse) .

You can also check out my [repo](http://github.com/blackgirlbytes/git-emojis/). Feel free to suggest improvements or features I can add to this web app. I’ll be adding more to this project and documenting my learnings as I go, so stay tuned!