Implementing Site Search with Fuse.js in Next.js 14
Why Use Fuzzy Search
🔗The most basic logic to implement a search feature is to directly match the input string and return the corresponding data. However, this is not very user-friendly in practice. It's common to make spelling mistakes or to only remember part of a word or phrase, which would result in no search results. Using fuzzy search can effectively solve this problem by helping to identify synonyms and variants, making the search more flexible. This is why I choose Fuse.js. The details of the fuzzy search algorithm are beyond the scope of this article.
Implementing Fuzzy Search
🔗Installing Fuse.js
🔗shnpm install fuse.js
How to Use Fuse.js
🔗Example:
JSimport Fuse from "fuse.js";
import React, { useState } from "react";
import searchData from '@/lib/search-data.json';
const posts = searchData;
const [input, setInput] = useState<any[]>([]);
const handleSearch = (event: any) => {
const { value } = event.target;
const fuse = new Fuse(posts, {
keys: ["title", "description"],
});
const results = fuse.search(value);
const items = results.map((result) => result.item);
setInput(items);
};
How to use:
- Create all searchable data
const posts = searchData;
Data is in JSON format Example:
Json[
{
"id": "Implementing Multilingual i18n with Next.js 14",
"title": "Implementing Multilingual i18n with Next.js 14",
"slug": "tutorials/nextjs-i18n",
"date": "2024-06-22T23:44:38.000Z",
"draft": false,
"description": "Implementing Multilingual i18n with Next.js 14 and negotiator.",
"thumbnail": "img/thumbnail/056en.webp",
"categories": [
"Program"
],
"tags": [
"Next.js",
"React",
"Web"
],
"lang": "en"
}
]
- Set the fields to search
keys: ["title", "description"]
- Insert the search query
const results = fuse.search(value);
- You can then get the search results, which is very easy to use.
Below are the implementation steps:
1. Add a search input box in the navigation bar
🔗Navigation bar: Navbar.tsx
, some code has been omitted, please refer mainly to the {/* Search bar */}
section.
JS"use client"
import { Input, Modal, ModalContent, ModalHeader, ModalBody, ModalFooter, useDisclosure } from "@nextui-org/react";
import { SearchIcon } from "./SearchIcon";
import Search from "./Search";
export default function App({ params: { lang, layouti18n, categories, tags } }: {
params: { lang: Locale, layouti18n: any, categories: any, tags: any }
}) {
// Code omitted
const { isOpen, onOpen, onClose, onOpenChange } = useDisclosure();
return (
<Navbar className="bg-background text-foreground transition-colors duration-700 opacity-95" isMenuOpen={isMenuOpen} onMenuOpenChange={setIsMenuOpen}>
{/* Code omitted */}
<NavbarContent justify="end">
{/* Search bar */}
<Input
classNames={{
base: "hidden sm:block max-w-full sm:max-w-[10rem] h-10",
mainWrapper: "h-full",
input: "text-small",
inputWrapper: "h-full font-normal text-default-500 bg-default-400/20 dark:bg-default-500/20",
}}
placeholder="Type to search..."
size="sm"
startContent={<SearchIcon size={18} width={undefined} height={undefined} />}
type="search"
onClick={onOpen}
/>
<Modal className="dark:bg-gray-400"
isOpen={isOpen}
onOpenChange={onOpenChange}
size="2xl" backdrop="blur">
<ModalContent>
{(onClose) => (
<>
<ModalHeader className="flex flex-col gap-1">{layouti18n.sitesearch}</ModalHeader>
<ModalBody>
<Search params={{ lang: lang, layout: layouti18n, onClose: onClose }} />
</ModalBody>
<ModalFooter>
</ModalFooter>
</>
)}
</ModalContent>
</Modal>
</NavbarContent>
</Navbar>
);
}
Detailed Explanation:
The main idea is to create a search input field in the navigation bar, which will pop up a Modal window when clicked. You can modify it according to your preferences.
2. Search Functionality Component
🔗Search.tsx
JS"use client"
import React, { useState } from "react";
import Fuse from "fuse.js";
import searchData from '@/lib/search-data.json';
import { Locale } from "../../../../i18n.config";
import Image from "next/image";
import Link from "next/link";
import { Input, ScrollShadow } from "@nextui-org/react";
import { SearchIcon } from "./SearchIcon";
export default function Search({ params: { lang, layout, onClose } }: {
params: { lang: Locale, layout: any, onClose: any }
}) {
let posts = searchData;
posts = posts.filter((post: any) => post.lang === lang && post.draft === false);
const [input, setInput] = useState<any[]>([]);
const handleSearch = (event: any) => {
const { value } = event.target;
const fuse = new Fuse(posts, {
keys: ["title", "description"],
});
const results = fuse.search(value);
const items = results.map((result) => result.item);
setInput(items);
};
return (
<div>
<Input
onChange={handleSearch}
// label="Search"
isClearable
radius="lg"
classNames={{
label: "text-black/50 dark:text-white/90",
input: [
"text-xl",
"bg-transparent",
"text-black/90 dark:text-white/90",
"placeholder:text-default-700/50 dark:placeholder:text-white/60",
],
innerWrapper: "bg-transparent",
inputWrapper: [
"text-xl",
"shadow-xl",
"bg-default-200/50",
"dark:bg-default/60",
"backdrop-blur-xl",
"backdrop-saturate-200",
"hover:bg-default-200/70",
"dark:hover:bg-default/70",
"group-data-[focus=true]:bg-default-200/50",
"dark:group-data-[focus=true]:bg-default/60",
"!cursor-text",
],
}}
placeholder="Type to search..."
startContent={
<SearchIcon className="text-black/50 mb-0.5 dark:text-white/90 text-slate-400 pointer-events-none flex-shrink-0" />
}
/>
<table>
<tbody>
<ScrollShadow hideScrollBar className="w-full h-96 mt-4">
<ul className="space-y-4 max-w-screen-sm w-full">
{input.map(({ id, title, date, draft, thumbnail, categories, tags, slug, filePath, description }: any, index: number) => (
<li key={index} className="border-b border-gray-200 pb-4 flex flex-col md:flex-row md:min-h-40 relative transition-transform duration-300 transform hover:-translate-y-1">
{thumbnail &&
<div className="min-w-full md:min-w-48 h-auto md:w-48 md:h-32 ">
<Link href={'/' + lang + '/' + slug} onClick={onClose}>
<Image src={'/' + thumbnail} width={500}
height={500} alt={title} className="w-full h-52 md:w-44 md:h-full rounded-md" />
</Link>
</div>
}
<div className="flex-grow">
<div className="min-h-28">
<h2 className="text-lg font-normal text-textcolor break-normal md:line-clamp-2">
<Link href={'/' + lang + '/' + slug} onClick={onClose}>
{title}
</Link>
</h2>
<div className="flex flex-wrap text-sm justify-start">
<p className="flex items-center text-gray-500 mr-4">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth="1.5" stroke="currentColor" className="size-6 mr-2">
<path strokeLinecap="round" strokeLinejoin="round" d="M6.75 3v2.25M17.25 3v2.25M3 18.75V7.5a2.25 2.25 0 0 1 2.25-2.25h13.5A2.25 2.25 0 0 1 21 7.5v11.25m-18 0A2.25 2.25 0 0 0 5.25 21h13.5A2.25 2.25 0 0 0 21 18.75m-18 0v-7.5A2.25 2.25 0 0 1 5.25 9h13.5A2.25 2.25 0 0 1 21 11.25v7.5" />
</svg>
{
date instanceof Date ?
date.toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' }) :
new Date(date).toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' })
}
</p>
<p className="flex items-center text-gray-500">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth="1.5" stroke="currentColor" className="size-6 mr-2">
<path strokeLinecap="round" strokeLinejoin="round" d="M2.25 12.75V12A2.25 2.25 0 0 1 4.5 9.75h15A2.25 2.25 0 0 1 21.75 12v.75m-8.69-6.44-2.12-2.12a1.5 1.5 0 0 0-1.061-.44H4.5A2.25 2.25 0 0 0 2.25 6v12a2.25 2.25 0 0 0 2.25 2.25h15A2.25 2.25 0 0 0 21.75 18V9a2.25 2.25 0 0 0-2.25-2.25h-5.379a1.5 1.5 0 0 1-1.06-.44Z" />
</svg>
{categories.map((category: string, index: number) => (
<span key={index} className="inline-block">
{category}{index < categories.length - 1 ? ' ,' : ''}
</span>
))}
</p>
</div>
<p className="text-base text-gray-500 line-clamp-2">{description}</p>
</div>
<div className="text-right"> {/* Apply absolute positioning here */}
<Link href={'/' + lang + '/' + slug} className="p-0" onClick={onClose}>
<button role="link" className="relative after:absolute after:bottom-0 after:left-0 after:h-[0.5px] after:w-full after:origin-bottom-left after:scale-x-100 after:bg-neutral-800 after:transition-transform after:duration-150 after:ease-in-out hover:after:origin-bottom-right hover:after:scale-x-0 dark:after:bg-white">
{layout.readmore}
</button>
</Link>
</div>
</div>
</li>
))}
</ul>
</ScrollShadow>
</tbody>
</table>
</div>
);
}
Detailed Explanation:
- The searchable article data has been pre-created
import searchData from '@/lib/search-data.json';
, so you just need to import it directly. import { Locale } from "../../../../i18n.config";
This project has multilingual functionality, which can be ignored if not needed.- Import nextui and the search hint Icon, which can be adjusted according to your preference or choose a different UI library.
JSimport { Input, ScrollShadow } from "@nextui-org/react";
import { SearchIcon } from "./SearchIcon";
SearchIcon.jsx
JSimport React from "react";
export const SearchIcon = ({
size = 24,
strokeWidth = 1.5,
width,
height,
...props
}) => (
<svg
aria-hidden="true"
fill="none"
focusable="false"
height={height || size}
role="presentation"
viewBox="0 0 24 24"
width={width || size}
{...props}
>
<path
d="M11.5 21C16.7467 21 21 16.7467 21 11.5C21 6.25329 16.7467 2 11.5 2C6.25329 2 2 6.25329 2 11.5C2 16.7467 6.25329 21 11.5 21Z"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={strokeWidth}
/>
<path
d="M22 22L20 20"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={strokeWidth}
/>
</svg>
);
Conclusion
🔗Demonstration
🔗With the above steps, you can easily implement a search functionality on your website. This article mainly records the process of using Fuse.js. Since the code also includes other features of a personal project, including multilingual support and the use of a UI library, please be understanding if it's not easy to read.
Originally, the old website used Algolia's search functionality, which had limitations on the number of searches. Now, by switching to Fuse.js, we can easily overcome the challenges of fuzzy searching.
Reference
Alvin
Software engineer, interested in financial knowledge, health concepts, psychology, independent travel, and system design.
Related Posts
Discussion (0)
No comments yet.