@ycookiey/yfuzzy
v0.5.0
Published
Japanese-aware fuzzy search library
Maintainers
Readme
yfuzzy
日本語特化のあいまい検索ライブラリ。ローマ字混じり入力・ヘボン式/訓令式の同時照合・子音だけのマッチ・誤字許容を 1 パッケージに統合。依存ゼロ・ESM・TypeScript 型付き。
shinjuku / sinjuku / しんじゅく / シンジュク → すべて「シンジュク」にヒット
kmnr → 「カミナリ」にヒット(子音だけ)
カナリ → 「カミナリ」にヒット(とびとび一致)
センタイ → 「センダイ」にヒット(誤字)
かぇんだr → 「Calendar」にヒット(IME オンのまま英字を打った入力)なぜ yfuzzy か
既存のあいまい検索ライブラリ(fuse.js / fuzzysort / MiniSearch / uFuzzy / match-sorter など)はラテン文字圏を前提に設計されており、diacritics(発音区別符号)の除去や Unicode 正規化には対応するものの、ローマ字とかなの変換や、ヘボン式・訓令式の違いを扱わない。
日本語 UI では、利用者はローマ字・部分的なかな・子音だけ(上の例)で打つ一方、データはひらがな・カタカナ・漢字(の読み)で保持されている。yfuzzy はこの表記のずれを埋めることに特化している。
スコープ
向いている: 日本語の読み(かな)を索引にした短い文字列の検索。駅名・地名・商品名・人名などの読みに対する、逐次検索(type-ahead)やオートコンプリート。英字の名前が混ざった索引を英字で探す用途にも対応する(照合経路)。
向いていない:
- 漢字をそのまま検索: yfuzzy は漢字を読みに変換しない(文字どおりの一致のみ)。読みで当てたいなら読み仮名を用意し
getTextで返す(使い方参照)。 - ラテン文字の全文検索(複数フィールド・トークン化・TF-IDF): fuse.js / MiniSearch 等が成熟している。用途で使い分けるとよい。
類似パッケージとの比較
| 機能 | yfuzzy | fuse.js | fuzzysort | MiniSearch | uFuzzy | match-sorter | |---|:--:|:--:|:--:|:--:|:--:|:--:| | 日本語入力(ローマ字↔かな・ヘボン/訓令・かな正規化) | ✅ | ➖ | ➖ | ➖ | ➖ | ➖ | | 子音だけのマッチ | ✅ | ➖ | 🔸 | ➖ | 🔸 | 🔸 | | とびとび一致(subsequence) | ✅ | 🔸 | ✅ | ➖ | ✅ | 🔸 | | 誤字許容(編集距離) | ✅ | ✅ | ➖ | ✅ | 🔸 | ➖ | | ハイライト位置の返却 | ✅ | ✅ | ✅ | 🔸 | ✅ | ➖ | | 索引の事前構築 | ✅ | ✅ | ✅ | ✅ | ➖ | ➖ | | 全文検索(複数フィールド/トークン) | ➖ | ✅ | 🔸 | ✅ | ➖ | 🔸 | | ラテン文字の diacritics 除去 | ➖ | ✅ | ➖ | 🔸 | ✅ | ✅ | | 依存ゼロ | ✅ | ✅ | ✅ | ✅ | ✅ | ➖ |
凡例: ✅ 組み込み / 🔸 部分的・間接的(または要カスタム) / ➖ なし。各ライブラリの調査時点の既定挙動を基準とした。多くはカスタム関数で拡張できる。本表は「日本語入力対応+主要なあいまいマッチ機能」に観点を絞ったもの。
インストール
npm install @ycookiey/yfuzzyNode 18+ / ESM 専用。
ブラウザ (CDN)
ビルドツールなしで使う場合は同梱の単一ファイル bundle を <script> で読み込む(グローバル yfuzzy が定義される):
<script src="https://unpkg.com/@ycookiey/[email protected]/dist/yfuzzy.min.js"></script>
<script>
yfuzzy.search('sinjuku', ['トウキョウ', 'シンジュク', 'シブヤ']);
// → [{ item: 'シンジュク', refIndex: 1, score: 1, tier: 1 }]
</script>使い方
文字列配列
import { search } from '@ycookiey/yfuzzy';
const stations = ['トウキョウ', 'シンジュク', 'シブヤ', 'シナガワ'];
search('sinjuku', stations);
// → [{ item: 'シンジュク', refIndex: 1, score: 1, tier: 1 }]オブジェクト配列(getText)
getText で検索対象の文字列を取り出す。漢字を読みで当てたい場合も、読み仮名を返せばよい(読みは利用者側で用意する)。
const items = [
{ label: '東京', yomi: 'トウキョウ' },
{ label: '新宿', yomi: 'シンジュク' },
];
search('しんじゅく', items, { getText: (x) => x.yomi });
// → [{ item: { label: '新宿', … }, refIndex: 1, score: 1, tier: 1 }]索引の再利用(createIndex)
同じデータを繰り返し検索するなら事前構築する。単発 search と結果は同一。
import { createIndex } from '@ycookiey/yfuzzy';
const index = createIndex(stations);
index.search('しぶ'); // → 先頭は シブヤ(短い語なので tier3 候補も続く)
index.search('しな', { limit: 5 }); // 呼び出しごとに検索オプションを上書きハイライト(includeMatches)
includeMatches: true で、元文字列上の一致区間 [start, end)(半開・昇順、隣接や重複は統合済み)を返す(後述の tier 1・2 でのみ)。各区間を text.slice で切り出して囲めばハイライトになる。
const r = search('keki', ['ケーキ'], { includeMatches: true });
// r[0].matches → [[0, 3]] ← 長音「ー」も含めて隙間なく連続マッチの仕組み
照合経路(matching)
クエリは2つの経路で照合し、score の高いほうを採る(既定 matching: 'both')。
- literal: 入力した文字のまま照合する(大文字小文字・全角半角・ひらがなカタカナの正規化のみ)。英字の item を英字で探す場合に当たる(
cale→Calendar)。入力中のかなは、それを打ったローマ字の打鍵としても照合するため、IME をオンのまま英字を打った入力も当たる(かぇんだr→Calendar、きっちぇn→Kitchen、Nかl→New Calendar)。打鍵の表記は主要な IME のローマ字表(ci→し、ce→せ、tch→っち 等)に合わせている。 - phonetic: ローマ字をかなに変換して照合する。ヘボン式・訓令式の同時照合、子音だけの入力、誤字の許容はこちら(
sinjuku→シンジュク、kmnr→カミナリ)。
同点のときは literal の結果を採る(ハイライトが入力した文字の位置になる)。
ローマ字で長音を打った入力(saabaa・IME 式の sa-ba-)も、長音「ー」を含む item に直接一致する(打ちかけの saab でも当たる)。長音の位置を印にして照合するので、長音のない別の語(obaasan に対する オバサン)には当たらない。長音を省いた入力(saba)は従来どおり当たる。
複数の語
空白で区切ったクエリは、すべての語を含む item に絞る(語の順番は問わない。cal goo → Google Calendar)。
- 各語は直接一致(tier 1)だけを認める。1 文字の語は item の先頭か単語の頭からの一致だけ
- 別の語は別の単語に当たる(
claude cのcはClaude自身には当てない) - score は最も弱い語の score。空白込みのクエリ全体としての一致と比べて高いほうを採る(並びどおり現れる item は今までどおり当たる)
- 並びは tier → 最も弱い語の段(下の tier 1 の段)→ 語の順番どおりか → 長さ
tier
クエリは tier 1 から順に評価され、最初に一致した tier で確定する(既定は tier 1〜3。maxTier: 4 で tier 4 も有効化)。score は 0〜1 で、上の tier ほど高い。
| tier | 内容 | 例 | score の範囲(目安) |
|---|---|---|---|
| 1 | 直接一致(打った文字列がそのまま現れる、または各単語の頭文字に並ぶ) | シンジュク→シンジュク | 0.75〜1.0 |
| 2 | 柔軟一致(とびとびでも可。語頭・連続を優遇、子音だけの入力も可) | kmnr→カミナリ | 0.5〜0.75 |
| 3 | 誤字許容(編集距離、中間の置換/挿入/削除/入れ替え) | センタイ→センダイ | 0.25〜0.5 |
| 4 | 構造類似(2 文字単位の重なりによる類似〔Tversky 係数〕、既定で無効) | 大きく崩れた入力 | 0〜0.25 |
tier 1 の中は一致の位置で順位を分ける: item の先頭から(co → Coffee)> 単語の頭から(Source Code。空白・記号の後と、AutoCorrect のような大文字の区切りを単語の頭とする)> 各単語の頭文字(ac → Apple Cider、goca → Gold Case)> 単語の途中(Discount)。同じ段の中は短い item が上。
各 tier は幅およそ 0.25 の帯に収まり、スコアが tier 間の境界(0.25 / 0.5 / 0.75)ちょうどになることはない。そのため閾値ひとつで「確実なヒット」と「あいまいな候補(『もしかして』表示向け)」を分離できる。結果は score 降順(同点は refIndex 昇順で安定)。
API
search(query, items, options?)
単発検索。createIndex(items, options).search(query) と等価。
createIndex(items, options?) → SearchIndex
事前構築した索引を返す。index.search(query, searchOptions?) で呼び出しごとに検索オプションを上書きできる。index.size で件数。
オプション
構築時オプション(createIndex / search の options):
| 名前 | 型 | 既定 | 説明 |
|---|---|---|---|
| getText | (item: T) => string | なし | オブジェクト配列で検索対象文字列を取り出す(T[] のとき必須) |
| romaji | 'hepburn' \| 'kunrei' \| 'both' \| false | 'both' | ローマ字照合の方式。false でローマ字照合せず、かなのみで照合 |
| matching | 'literal' \| 'phonetic' \| 'both' | 'both' | クエリの照合経路(照合経路)。'both' は両方の score の高いほう |
| separatorExpansion | boolean \| string[] | false | 区切り文字を、取り除いた形と区切りとして扱う形の両方で照合(true で ['-', ' ', '・', '_']) |
検索時オプション(index.search で上書き可):
| 名前 | 型 | 既定 | 説明 |
|---|---|---|---|
| limit | number | 無制限 | 返却件数の上限 |
| maxTier | 1 \| 2 \| 3 \| 4 | 3 | 実行する tier の上限(4 で構造類似の tier を有効化) |
| minScore | number | 0 | この score 未満を除外 |
| includeMatches | boolean | false | matches を含める |
結果 SearchResult<T>
type SearchResult<T> = {
item: T;
refIndex: number; // items 内の位置
score: number; // 0〜1、高いほど良い
tier: 1 | 2 | 3 | 4;
kind: MatchKind; // 一致の種類(下)
matches?: readonly [number, number][]; // includeMatches 時、tier 1/2 のみ
};kind は一致の種類。tier 1 は 'prefix'(item の先頭から)・'word'(単語の頭から)・'initials'(各単語の頭文字)・'inner'(単語の途中)、tier 2 は 'subsequence'、tier 3 は 'typo'、tier 4 は 'similar'。複数の語のクエリでは、語ごとの一致を採用したときは最も弱い語の種類、空白込みの全体の一致を採用したときはその一致の種類。例えば長い文字列で偶然そろう頭文字の一致を除きたいときは kind !== 'initials' で絞る。
性能
短い文字列の索引向け(数百〜数十万件)。内部の前段フィルタと早期打ち切りにより、1 クエリの実測値は千件で約 1.5ms、1 万件で約 10ms、10 万件で約 90ms(環境依存)。同条件で fuse.js より高速。英字の item をかなを含むクエリで探すときは打鍵の照合が加わり、1.5〜2 倍程度になる。maxTier を下げる、または romaji を一方の方式だけに絞ると更に軽くなる。索引構築は 10 万件で約 0.7 秒(一度きり)。
ライセンス
MIT。ローマ字→かな変換に WanaKana(MIT)のコードを改変のうえ同梱している。詳細は THIRD_PARTY_NOTICES.md。
