はじめに
サイト内検索を実装したのに、「javscript」と打ち間違えただけで結果がゼロ件になってしまう。そんな検索ボックス、心当たりはありませんか?
Array.prototype.filter と includes で作った検索は、完全一致(部分一致)しかヒットしません。かといって、タイプミスにも対応できる本格的な検索を作ろうとすると、ElasticsearchやAlgoliaのような検索サーバーが頭をよぎり、「そこまでの規模じゃないんだよな……」と手が止まってしまいます。
その中間にちょうどよく収まるのが、今回紹介する Fuse.js です。サーバー不要・依存ゼロ・gzip後約8.6kBという軽さで、タイプミスを許容する「あいまい検索(ファジー検索)」をフロントエンドだけで実現できます。
Fuse.jsとは
Fuse.jsは、JavaScriptで書かれた軽量なファジー検索ライブラリです。Bitapアルゴリズムによる近似文字列マッチングを採用しており、クエリと完全に一致しなくても「近い」文字列をスコア付きで返してくれます。
GitHubのスター数は20k超え。GoogleやMicrosoftといった企業でも採用されており、2026年7月リリースのv7.5.0まで現在も活発にメンテナンスされています。
主な特徴
- ゼロ依存・軽量 - 外部ライブラリに一切依存せず、フル版でも約8.6kB(gzip)。バンドルサイズへの影響が最小限です
- サーバー不要 - すべてブラウザ(またはNode.js)内で完結します。検索用のバックエンドを立てる必要がありません
- タイプミスに強い - 「javscript」で「JavaScript」がヒットする近似マッチング。スコアの閾値も自由に調整できます
- オブジェクトの深い検索 - ネストしたプロパティや配列を
keysに指定でき、フィールドごとの重み付けにも対応します - 拡張検索・論理検索 - 完全一致や前方一致の演算子、
$and/$orによる構造化クエリも書けます
インストール
各パッケージマネージャーからインストールできます。
# npm
npm install fuse.js
# yarn
yarn add fuse.js
# pnpm
pnpm add fuse.js
ビルド環境がない場合は、CDN経由でも利用できます。
<script type="module">
import Fuse from 'https://cdn.jsdelivr.net/npm/fuse.js@7.5.0/dist/fuse.mjs'
</script>
TypeScriptの型定義は本体に同梱されているため、@types/ パッケージの追加は不要です。
基本的な使い方
まずは最小構成から見てみましょう。書籍リストをタイトルと著者名で検索する例です。
import Fuse from 'fuse.js'
const books = [
{ title: "Old Man's War", author: 'John Scalzi' },
{ title: 'The Lock Artist', author: 'Steve Hamilton' },
{ title: 'HTML5', author: 'Remy Sharp' },
{ title: 'Right Ho Jeeves', author: 'P.D. Woodhouse' },
]
const fuse = new Fuse(books, {
keys: ['title', 'author'],
})
console.log(fuse.search('old man'))
実行結果はこうなります。
[
{
item: { title: "Old Man's War", author: 'John Scalzi' },
refIndex: 0
}
]
ポイントは、検索対象のフィールドを keys で指定するだけという手軽さです。インデックス構築もスキーマ定義も要りません。
スコアを確認する
「どれくらい一致しているか」を知りたい場合は includeScore を有効にします。
const fuse = new Fuse(books, {
keys: ['title', 'author'],
includeScore: true,
})
console.log(fuse.search('jon'))
// [
// {
// item: { title: "Old Man's War", author: 'John Scalzi' },
// refIndex: 0,
// score: 0.44
// }
// ]
スコアは 0が完全一致、1が完全不一致 を表します。「jon」というタイプミスでも「John Scalzi」がヒットしているのがわかります。
あいまいさを調整する
デフォルトではやや広めにヒットするため、実際のプロダクトでは threshold で絞り込むのが定番です。
const fuse = new Fuse(books, {
keys: ['title', 'author'],
includeScore: true,
threshold: 0.3, // デフォルトは0.6。小さいほど厳しくなる
minMatchCharLength: 2, // 2文字以上一致した箇所だけを対象にする
})
threshold: 0 にすると完全一致のみ、1 にすると何でもヒットします。まずは 0.3〜0.4 あたりから調整を始めるのがおすすめです。
実践的なユースケース
フィールドごとに重み付けする
「タイトルの一致は著者名の一致より重要」といった優先度は、keys をオブジェクト形式にして weight で表現します。
const fuse = new Fuse(books, {
keys: [
{ name: 'title', weight: 2 }, // タイトル一致を2倍重視
{ name: 'author', weight: 1 },
],
includeScore: true,
})
ネストしたプロパティも 'author.firstName' のようにドット記法で指定できるため、APIレスポンスをそのまま検索対象にできます。
拡張検索で「完全一致」や「前方一致」を混ぜる
useExtendedSearch を有効にすると、検索クエリに演算子が使えるようになります。
const fuse = new Fuse(books, {
keys: ['title', 'author'],
useExtendedSearch: true,
})
fuse.search("'War") // "War" を含む項目(完全一致)
fuse.search('^The') // "The" で始まる項目
fuse.search('!Jeeves') // "Jeeves" を含まない項目
fuse.search("'War | 'HTML5") // OR検索
ユーザーに検索演算子を開放したい管理画面などで重宝します。
Reactでインクリメンタルサーチを作る
もっとも出番が多いのは、入力のたびに結果が絞り込まれる検索UIでしょう。Reactでの実装例です。
import { useMemo, useState } from 'react'
import Fuse from 'fuse.js'
type Article = {
title: string
tags: string[]
}
const articles: Article[] = [
{ title: 'Reactの状態管理を整理する', tags: ['react', 'state'] },
{ title: 'TypeScriptの型パズルに挑戦', tags: ['typescript'] },
{ title: 'Viteでビルドを高速化する', tags: ['vite', 'build'] },
]
export function ArticleSearch() {
const [query, setQuery] = useState('')
// Fuseインスタンスは再生成コストがあるためメモ化する
const fuse = useMemo(
() =>
new Fuse(articles, {
keys: [
{ name: 'title', weight: 2 },
{ name: 'tags', weight: 1 },
],
threshold: 0.35,
}),
[]
)
const results = query
? fuse.search(query).map((r) => r.item)
: articles
return (
<div>
<input
type="search"
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="記事を検索..."
/>
<ul>
{results.map((article) => (
<li key={article.title}>{article.title}</li>
))}
</ul>
</div>
)
}
tags のような配列フィールドもそのまま keys に指定できるのが便利なところです。静的サイトのブログ検索、ドキュメントサイトのコマンドパレット、セレクトボックスの候補絞り込みなど、数百〜数千件規模のクライアントサイド検索ならこのパターンでほぼカバーできます。
マッチ箇所をハイライトする
includeMatches を有効にすると、どの文字位置が一致したかが返ってくるため、検索結果のハイライト表示に使えます。
const fuse = new Fuse(books, {
keys: ['title'],
includeMatches: true,
})
const [result] = fuse.search('old')
console.log(result.matches)
// [
// {
// indices: [[0, 2]], // 一致した文字の開始・終了位置
// key: 'title',
// value: "Old Man's War"
// }
// ]
indices をもとに <mark> タグで囲めば、Google検索のような「一致部分が太字になる」UIが実装できます。
使いどころの見極め
万能に見えるFuse.jsですが、得意・不得意ははっきりしています。
- 得意: 数千件程度までのクライアントサイド検索。タイプミス許容、オートコンプリート、コマンドパレット
- 不得意: 数十万件規模の全文検索や、日本語の形態素解析を要する本格的な検索
検索はメインスレッドで同期的に実行されるため、データが数万件を超えるあたりからは体感遅延が出はじめます。その規模になったら、Web Workerへの分離や、MiniSearch・サーバーサイド検索エンジンの検討をおすすめします。逆に言えば、それ以下の規模で検索サーバーを立てるのは過剰装備です。
まとめ
Fuse.jsの魅力を振り返ります。
- ゼロ依存・約8.6kBで、
new Fuse(list, { keys })と書くだけであいまい検索が動く - タイプミスに強く、
thresholdでヒットの厳しさを直感的に調整できる - 重み付け・拡張検索・マッチ位置の取得など、実用に必要な機能がひと通り揃っている
- サーバーもインデックス構築も不要で、静的サイトにもそのまま組み込める
「検索サーバーを立てるほどではないけれど、includes では物足りない」——そんな場面に出会ったら、まずはFuse.jsを試してみてください。公式ドキュメント(fusejs.io)にはオプションを対話的に試せるデモも用意されているので、threshold の調整感覚を掴むのに最適です。
