Blume の日本語検索、バージョン別の計測記録
Blume の日本語検索、バージョン別の計測記録
Blume で作っている法務案件チェックポイントの日本語検索について、バージョンごとに精度を測った記録。なぜそうなったかの経緯と設計の話ははてなブログの「日本語の全文検索は「分かち書き」だけでは足りない — Pagefind と Orama を測って、文字bigram で直すまで」に書いたので、こちらは数字だけを残しておく。
自分用の定点観測でもあるけど、日本語の全文検索で「どの方式がどれくらい効くのか」の実データはあまり見かけないので、置いておけば誰かの役には立つと思う。
測定条件
- コーパス: 実サイトの本文65ページ(日本語のドキュメント)
- クエリ: 逆引き20件(法令名・送り仮名を含む語・カタカナ語・英略語)+ 自然文6件
- 正解の定義: そのクエリ文字列を title / description / content に literal で含むページ。機械的に決まる
- top-3 的中: 上位3件のうち正解だった件数の合計。分母は各クエリ
min(3, 正解ページ数) - MRR: 最初の正解の順位の逆数を、クエリ数で平均したもの
- 全構成を同一コーパス・同一クエリ・同一採点で1回のスクリプトから出している
構成
| 略称 | 中身 |
|---|---|
| 1.2.0以前 | トークナイザ差し替えなし。Orama の english 固定 |
| 1.2.1 | Intl.Segmenter による辞書分割(分かち書き) |
| 語bigram | 分かち書き+隣接する語の連結(自分が最初に考えて外した案) |
| 文字bigramのみ | 漢字・かな・カナの連なりを文字2-gram に切り、unigram を出さない |
| 1.3.0 | 文字bigram + AND(threshold: 0)、0件なら OR にフォールバック |
| Pagefind | 比較用。rootSelector: "article" で本文のみ索引 |
各方式について
Blume の検索は Orama というクライアントサイドの全文検索エンジンの上に載っている。どの構成も違いは「文書と検索語をどうトークンに切るか」だけで、索引の張り方やスコアリングは共通。「個人情報保護法」を例にすると、それぞれこう切れる。
| 方式 | 切り方 |
|---|---|
| english(1.2.0以前) | (トークンなし) |
| 分かち書き(1.2.1) | 個人 / 情報 / 保護 / 法 |
| 語bigram | 個人 / 情報 / 保護 / 法 + 個人情報 / 情報保護 / 保護法 |
| 文字bigram | 個人 / 人情 / 情報 / 報保 / 保護 / 護法 |
english のトークナイザは空白と句読点で区切る。日本語の文字はすべて区切り文字側に落ちるので、文書も検索語もトークンが空になる。何を入れても0件になる。
分かち書きは Intl.Segmenter(ブラウザと Node に標準で入っている辞書ベースの分割器)で語の境界を見つける方式。辞書データを別途配る必要がないのが利点。ただし「法」「系」のような1文字語がトークンとして残り、それが大量のページに刺さる。1.2.1 で48件ずらっと返るのはこれが原因。
語bigramは、分かち書きの結果に隣り合う語をつないだトークンを足す方式。「個人情報」「保護法」といった複合語で引けるようにする狙いだった。ただし元の unigram を残したままなので、「法」のノイズはそのまま残る。
文字bigramは語の境界を無視して、文字を2つずつ1文字ずらしながら切る方式(N-gram の N=2)。辞書を使わないので未知語や固有名詞に強く、「法」のような1文字トークンが原理的に出てこない。代わりに「情報」「保護」といった2文字窓は単体ではありふれているので、OR で引くと広く拾いすぎる。1.3.0 が AND(全 bigram を含むページだけ)にしているのはこのため。Lucene の CJK アナライザと同じ考え方。
Pagefind は静的サイト向けの全文検索ライブラリ(今回は 1.5.2)。他と違ってビルド済みの HTML をクロールして索引を作り、それを chunk に分割して置いておき、ブラウザは検索語に必要な部分だけを取りに行く。索引全体を読み込まないので大規模サイトでも転送量が小さいのが売り。CJK 向けの分割も内蔵しているが、その挙動を設定から触る口はないので、外したときに手当てのしようがない。
結果
| 構成 | top-3 的中 | MRR | 逆引きで0件 | 自然文で0件 |
|---|---|---|---|---|
| 1.2.0以前 | 9/53(17%) | 0.150 | 17/20 | 4/6 |
| 1.2.1 | 36/53(68%) | 0.762 | 0/20 | 0/6 |
| 語bigram | 38/53(72%) | 0.804 | 0/20 | 0/6 |
| 文字bigramのみ | 43/53(81%) | 0.975 | 0/20 | 0/6 |
| 1.3.0 | 53/53(100%) | 1.000 | 0/20 | 0/6 |
| Pagefind | 43/53(81%) | 0.956 | 0/20 | 0/6 |
クエリ単位
上位3件の的中 / 返した件数 の形式。件数は「正解件数に近いほど良い」と読む。
| クエリ | 正解 | 1.2.0以前 | 1.2.1 | 語bigram | 文字bigramのみ | 1.3.0 | Pagefind |
|---|---|---|---|---|---|---|---|
| 個人情報保護法 | 6 | 0/3・0件 | 1/3・48件 | 1/3・48件 | 3/3・48件 | 3/3・6件 | 3/3・10件 |
| 資金決済法 | 9 | 0/3・0件 | 2/3・48件 | 2/3・48件 | 3/3・13件 | 3/3・9件 | 3/3・8件 |
| 景品表示法 | 7 | 0/3・0件 | 2/3・48件 | 2/3・48件 | 2/3・17件 | 3/3・7件 | 1/3・1件 |
| 下請法 | 4 | 0/3・0件 | 1/3・48件 | 1/3・48件 | 3/3・4件 | 3/3・4件 | 3/3・4件 |
| 電子帳簿保存法 | 2 | 0/2・0件 | 1/2・48件 | 1/2・48件 | 1/2・19件 | 2/2・2件 | 0/2・10件 |
| 特定商取引法 | 6 | 0/3・0件 | 0/3・48件 | 0/3・48件 | 2/3・13件 | 3/3・6件 | 2/3・10件 |
| 古物営業法 | 2 | 0/2・0件 | 1/2・48件 | 2/2・48件 | 2/2・9件 | 2/2・2件 | 2/2・2件 |
| 労働者派遣法 | 1 | 0/1・0件 | 1/1・48件 | 1/1・48件 | 1/1・1件 | 1/1・1件 | 1/1・1件 |
| 資金移動業 | 4 | 0/3・0件 | 1/3・28件 | 2/3・28件 | 3/3・11件 | 3/3・4件 | 2/3・8件 |
| 前払式支払手段 | 5 | 0/3・0件 | 2/3・21件 | 2/3・21件 | 2/3・21件 | 3/3・5件 | 3/3・5件 |
| 出会い系 | 2 | 0/2・0件 | 1/2・16件 | 1/2・16件 | 2/2・2件 | 2/2・2件 | 2/2・2件 |
| 出会い系サイト規制法 | 2 | 0/2・0件 | 1/2・48件 | 1/2・48件 | 1/2・32件 | 2/2・2件 | 2/2・2件 |
| 払い戻し | 4 | 0/3・0件 | 3/3・4件 | 3/3・4件 | 1/3・23件 | 3/3・4件 | 3/3・4件 |
| 取り扱い | 2 | 0/2・0件 | 2/2・2件 | 2/2・2件 | 2/2・19件 | 2/2・2件 | 2/2・2件 |
| ステマ | 5 | 0/3・0件 | 3/3・5件 | 3/3・5件 | 2/3・20件 | 3/3・5件 | 2/3・2件 |
| マッチング | 3 | 0/3・0件 | 3/3・3件 | 3/3・3件 | 2/3・41件 | 3/3・3件 | 3/3・3件 |
| サブスク | 8 | 0/3・0件 | 2/3・13件 | 2/3・13件 | 2/3・26件 | 3/3・8件 | 1/3・1件 |
| GDPR | 4 | 3/3・4件 | 3/3・4件 | 3/3・4件 | 3/3・4件 | 3/3・4件 | 2/3・5件 |
| SDK | 7 | 3/3・7件 | 3/3・7件 | 3/3・7件 | 3/3・7件 | 3/3・7件 | 3/3・7件 |
| API | 9 | 3/3・9件 | 3/3・9件 | 3/3・9件 | 3/3・9件 | 3/3・9件 | 3/3・9件 |
表から読めること
1.2.0 以前の 17% は、実質「日本語が引けない」ということ。 的中しているのは GDPR / SDK / API の3クエリだけで、日本語のクエリは20件中17件が0件だった。english のトークナイザが日本語の文字を全部区切り文字として扱うので、本文も検索語もトークンが空になる。
1.2.1 の「48件」が並んでいるのが順位の壊れ方を表している。 65ページ中48件(limit の上限)返している状態で、上位3件の的中が1〜2。ヒットはしているが、ほぼ全ページを返しているだけで絞れていない。「法」のような断片トークンが効いてしまうため。
語bigram(自分の最初の案)は 68% → 72% でほぼ動かなかった。 unigram を残したまま bigram を足したので、断片のノイズが消えていない。件数の列を見ると 1.2.1 とまったく同じ 48件が並んでいて、絞り込みに寄与していないのがよく分かる。
文字bigram のみだと MRR は 0.975 まで上がるが、件数が絞れない。 1位はほぼ正しくなる(だから MRR が高い)一方で、「マッチング」41件・「個人情報保護法」48件のように広く拾う。2文字窓が個々にはありふれているため。
AND を足した 1.3.0 で、件数が正解件数と一致する。 20クエリすべてで一致した。資金決済法は9ページ返して9ページが含む、下請法は4と4、出会い系サイト規制法は2と2。
Pagefind は安定しているが、外すときは大きく外す。 「電子帳簿保存法」は上位3件が全滅、「景品表示法」は7ページ中1件、「サブスク」は8ページ中1件。分割の揺れによる取りこぼしだと思うが、CJK の処理に手を入れられないので確認しようがない。
「出会い系」は 1.2.1 で16件返している。 このサイトのカテゴリ名が「お金・決済系」「通信・プライバシー系」なので、分かち書きで出会い / 系 に切れた結果、トークン「系」が16ページに刺さっていた。送り仮名やカテゴリ接尾辞で切れる語は同じ問題を踏む。
補足
- 1.3.0 の実装は自分が出した PR #132。1.2.1 のトークナイザ導入は Issue #125 から
- 索引の転送量はどの構成でも 191KB で変わらない。Blume は素のドキュメント JSON を配ってブラウザ側で索引を組むため。増えるのは索引構築時間(65ページで 42ms → 99ms)だけ
- 1文字クエリは、その文字が bigram の先頭に立つページにしか届かない。「景品表示法。」末尾の 法 は 示法 の中にしかないので
法では引けない。Lucene の CJK アナライザと同じ性質で、仕様として受け入れている
感想
- 数字にするまで「なんとなく前の方が良かった気がする」で止まっていた。1.2.1 の状態は普通に使えているように見えるので、48件ずらっと返っているのを見るまで壊れているとは思わなかった。
- 最初の案(語bigram)が外れたのがこの表で一目瞭然なのが気持ちいい。件数の列が 1.2.1 と1桁も違わないので、効いていないことが数字を見た瞬間に分かった。
- 正解集合を「本文に literal で含むページ」で機械的に定義したのがよかった。人手のラベル付けが要らないので、思いついた方式をその場で測れる。同義語は拾えないけど、ランキングの良し悪しを見るには十分だった。
# Blume の日本語検索、バージョン別の計測記録 [Blume](https://useblume.dev/) で作っている[法務案件チェックポイント](https://h-kono-it.github.io/legal-check-helper/)の日本語検索について、バージョンごとに精度を測った記録。なぜそうなったかの経緯と設計の話ははてなブログの「[日本語の全文検索は「分かち書き」だけでは足りない — Pagefind と Orama を測って、文字bigram で直すまで](https://hk-it.hatenablog.com/entry/2026/08/05/210424)」に書いたので、こちらは数字だけを残しておく。 自分用の定点観測でもあるけど、日本語の全文検索で「どの方式がどれくらい効くのか」の実データはあまり見かけないので、置いておけば誰かの役には立つと思う。 ## 測定条件 - **コーパス**: 実サイトの本文65ページ(日本語のドキュメント) - **クエリ**: 逆引き20件(法令名・送り仮名を含む語・カタカナ語・英略語)+ 自然文6件 - **正解の定義**: そのクエリ文字列を title / description / content に literal で含むページ。機械的に決まる - **top-3 的中**: 上位3件のうち正解だった件数の合計。分母は各クエリ `min(3, 正解ページ数)` - **MRR**: 最初の正解の順位の逆数を、クエリ数で平均したもの - 全構成を同一コーパス・同一クエリ・同一採点で1回のスクリプトから出している ## 構成 | 略称 | 中身 | |---|---| | 1.2.0以前 | トークナイザ差し替えなし。Orama の english 固定 | | 1.2.1 | `Intl.Segmenter` による辞書分割(分かち書き) | | 語bigram | 分かち書き+隣接する語の連結(自分が最初に考えて外した案) | | 文字bigramのみ | 漢字・かな・カナの連なりを文字2-gram に切り、unigram を出さない | | 1.3.0 | 文字bigram + AND(`threshold: 0`)、0件なら OR にフォールバック | | Pagefind | 比較用。`rootSelector: "article"` で本文のみ索引 | ## 各方式について Blume の検索は [Orama](https://orama.com/open-source) というクライアントサイドの全文検索エンジンの上に載っている。**どの構成も違いは「文書と検索語をどうトークンに切るか」だけ**で、索引の張り方やスコアリングは共通。「個人情報保護法」を例にすると、それぞれこう切れる。 | 方式 | 切り方 | |---|---| | english(1.2.0以前) | (トークンなし) | | 分かち書き(1.2.1) | 個人 / 情報 / 保護 / 法 | | 語bigram | 個人 / 情報 / 保護 / 法 + 個人情報 / 情報保護 / 保護法 | | 文字bigram | 個人 / 人情 / 情報 / 報保 / 保護 / 護法 | **english のトークナイザ**は空白と句読点で区切る。日本語の文字はすべて区切り文字側に落ちるので、文書も検索語もトークンが空になる。何を入れても0件になる。 **分かち書き**は `Intl.Segmenter`(ブラウザと Node に標準で入っている辞書ベースの分割器)で語の境界を見つける方式。辞書データを別途配る必要がないのが利点。ただし「法」「系」のような1文字語がトークンとして残り、それが大量のページに刺さる。1.2.1 で48件ずらっと返るのはこれが原因。 **語bigram**は、分かち書きの結果に隣り合う語をつないだトークンを足す方式。「個人情報」「保護法」といった複合語で引けるようにする狙いだった。ただし元の unigram を残したままなので、「法」のノイズはそのまま残る。 **文字bigram**は語の境界を無視して、文字を2つずつ1文字ずらしながら切る方式(N-gram の N=2)。辞書を使わないので未知語や固有名詞に強く、「法」のような1文字トークンが原理的に出てこない。代わりに「情報」「保護」といった2文字窓は単体ではありふれているので、OR で引くと広く拾いすぎる。1.3.0 が AND(全 bigram を含むページだけ)にしているのはこのため。Lucene の CJK アナライザと同じ考え方。 **[Pagefind](https://pagefind.app/)** は静的サイト向けの全文検索ライブラリ(今回は 1.5.2)。他と違ってビルド済みの HTML をクロールして索引を作り、それを chunk に分割して置いておき、ブラウザは検索語に必要な部分だけを取りに行く。索引全体を読み込まないので大規模サイトでも転送量が小さいのが売り。CJK 向けの分割も内蔵しているが、**その挙動を設定から触る口はない**ので、外したときに手当てのしようがない。 ## 結果 | 構成 | top-3 的中 | MRR | 逆引きで0件 | 自然文で0件 | |---|---|---|---|---| | 1.2.0以前 | 9/53(17%) | 0.150 | 17/20 | 4/6 | | 1.2.1 | 36/53(68%) | 0.762 | 0/20 | 0/6 | | 語bigram | 38/53(72%) | 0.804 | 0/20 | 0/6 | | 文字bigramのみ | 43/53(81%) | 0.975 | 0/20 | 0/6 | | **1.3.0** | **53/53(100%)** | **1.000** | 0/20 | 0/6 | | Pagefind | 43/53(81%) | 0.956 | 0/20 | 0/6 | ## クエリ単位 `上位3件の的中 / 返した件数` の形式。件数は「正解件数に近いほど良い」と読む。 | クエリ | 正解 | 1.2.0以前 | 1.2.1 | 語bigram | 文字bigramのみ | 1.3.0 | Pagefind | |---|---|---|---|---|---|---|---| | 個人情報保護法 | 6 | 0/3・0件 | 1/3・48件 | 1/3・48件 | 3/3・48件 | 3/3・6件 | 3/3・10件 | | 資金決済法 | 9 | 0/3・0件 | 2/3・48件 | 2/3・48件 | 3/3・13件 | 3/3・9件 | 3/3・8件 | | 景品表示法 | 7 | 0/3・0件 | 2/3・48件 | 2/3・48件 | 2/3・17件 | 3/3・7件 | 1/3・1件 | | 下請法 | 4 | 0/3・0件 | 1/3・48件 | 1/3・48件 | 3/3・4件 | 3/3・4件 | 3/3・4件 | | 電子帳簿保存法 | 2 | 0/2・0件 | 1/2・48件 | 1/2・48件 | 1/2・19件 | 2/2・2件 | 0/2・10件 | | 特定商取引法 | 6 | 0/3・0件 | 0/3・48件 | 0/3・48件 | 2/3・13件 | 3/3・6件 | 2/3・10件 | | 古物営業法 | 2 | 0/2・0件 | 1/2・48件 | 2/2・48件 | 2/2・9件 | 2/2・2件 | 2/2・2件 | | 労働者派遣法 | 1 | 0/1・0件 | 1/1・48件 | 1/1・48件 | 1/1・1件 | 1/1・1件 | 1/1・1件 | | 資金移動業 | 4 | 0/3・0件 | 1/3・28件 | 2/3・28件 | 3/3・11件 | 3/3・4件 | 2/3・8件 | | 前払式支払手段 | 5 | 0/3・0件 | 2/3・21件 | 2/3・21件 | 2/3・21件 | 3/3・5件 | 3/3・5件 | | 出会い系 | 2 | 0/2・0件 | 1/2・16件 | 1/2・16件 | 2/2・2件 | 2/2・2件 | 2/2・2件 | | 出会い系サイト規制法 | 2 | 0/2・0件 | 1/2・48件 | 1/2・48件 | 1/2・32件 | 2/2・2件 | 2/2・2件 | | 払い戻し | 4 | 0/3・0件 | 3/3・4件 | 3/3・4件 | 1/3・23件 | 3/3・4件 | 3/3・4件 | | 取り扱い | 2 | 0/2・0件 | 2/2・2件 | 2/2・2件 | 2/2・19件 | 2/2・2件 | 2/2・2件 | | ステマ | 5 | 0/3・0件 | 3/3・5件 | 3/3・5件 | 2/3・20件 | 3/3・5件 | 2/3・2件 | | マッチング | 3 | 0/3・0件 | 3/3・3件 | 3/3・3件 | 2/3・41件 | 3/3・3件 | 3/3・3件 | | サブスク | 8 | 0/3・0件 | 2/3・13件 | 2/3・13件 | 2/3・26件 | 3/3・8件 | 1/3・1件 | | GDPR | 4 | 3/3・4件 | 3/3・4件 | 3/3・4件 | 3/3・4件 | 3/3・4件 | 2/3・5件 | | SDK | 7 | 3/3・7件 | 3/3・7件 | 3/3・7件 | 3/3・7件 | 3/3・7件 | 3/3・7件 | | API | 9 | 3/3・9件 | 3/3・9件 | 3/3・9件 | 3/3・9件 | 3/3・9件 | 3/3・9件 | ## 表から読めること **1.2.0 以前の 17% は、実質「日本語が引けない」ということ。** 的中しているのは GDPR / SDK / API の3クエリだけで、日本語のクエリは20件中17件が0件だった。english のトークナイザが日本語の文字を全部区切り文字として扱うので、本文も検索語もトークンが空になる。 **1.2.1 の「48件」が並んでいるのが順位の壊れ方を表している。** 65ページ中48件(`limit` の上限)返している状態で、上位3件の的中が1〜2。ヒットはしているが、ほぼ全ページを返しているだけで絞れていない。「法」のような断片トークンが効いてしまうため。 **語bigram(自分の最初の案)は 68% → 72% でほぼ動かなかった。** unigram を残したまま bigram を足したので、断片のノイズが消えていない。件数の列を見ると 1.2.1 とまったく同じ 48件が並んでいて、絞り込みに寄与していないのがよく分かる。 **文字bigram のみだと MRR は 0.975 まで上がるが、件数が絞れない。** 1位はほぼ正しくなる(だから MRR が高い)一方で、「マッチング」41件・「個人情報保護法」48件のように広く拾う。2文字窓が個々にはありふれているため。 **AND を足した 1.3.0 で、件数が正解件数と一致する。** 20クエリすべてで一致した。資金決済法は9ページ返して9ページが含む、下請法は4と4、出会い系サイト規制法は2と2。 **Pagefind は安定しているが、外すときは大きく外す。** 「電子帳簿保存法」は上位3件が全滅、「景品表示法」は7ページ中1件、「サブスク」は8ページ中1件。分割の揺れによる取りこぼしだと思うが、CJK の処理に手を入れられないので確認しようがない。 **「出会い系」は 1.2.1 で16件返している。** このサイトのカテゴリ名が「お金・決済系」「通信・プライバシー系」なので、分かち書きで出会い / 系 に切れた結果、トークン「系」が16ページに刺さっていた。送り仮名やカテゴリ接尾辞で切れる語は同じ問題を踏む。 ## 補足 - 1.3.0 の実装は自分が出した [PR #132](https://github.com/haydenbleasel/blume/pull/132)。1.2.1 のトークナイザ導入は [Issue #125](https://github.com/haydenbleasel/blume/issues/125) から - 索引の転送量はどの構成でも 191KB で変わらない。Blume は素のドキュメント JSON を配ってブラウザ側で索引を組むため。増えるのは索引構築時間(65ページで 42ms → 99ms)だけ - 1文字クエリは、その文字が bigram の先頭に立つページにしか届かない。「景品表示法。」末尾の 法 は 示法 の中にしかないので `法` では引けない。Lucene の CJK アナライザと同じ性質で、仕様として受け入れている ## 感想 - 数字にするまで「なんとなく前の方が良かった気がする」で止まっていた。1.2.1 の状態は普通に使えているように見えるので、48件ずらっと返っているのを見るまで壊れているとは思わなかった。 - 最初の案(語bigram)が外れたのがこの表で一目瞭然なのが気持ちいい。件数の列が 1.2.1 と1桁も違わないので、効いていないことが数字を見た瞬間に分かった。 - 正解集合を「本文に literal で含むページ」で機械的に定義したのがよかった。人手のラベル付けが要らないので、思いついた方式をその場で測れる。同義語は拾えないけど、ランキングの良し悪しを見るには十分だった。