Claude CodeでSudachiの分割モードを比べる — OpenSearchのA/B/Cと辞書
Sudachiの分割モードA/B/Cが検索の再現率をどう変えるかを、辞書別・索引側と検索側の組み合わせ別に実測します。OpenSearchの設定と辞書の同梱手順、比較をClaude Codeに回させる方法も扱います。
日本語の全文検索で「空港」と検索したのに「関西国際空港」の文書が出ない。OpenSearchにSudachiを入れた直後に起きやすいこの現象は、多くの場合、分割モードと辞書の組み合わせで説明がつきます。
この記事では、Sudachiの分割モードA/B/Cを手元で実際に動かして比べた結果を先に示します。そのうえで、OpenSearchのsudachi_tokenizerとsudachi_splitの設定、辞書を同梱する手順、ユーザー辞書の落とし穴を順に説明します。最後に、比較をClaude Codeに回させるときの書き方も載せます。
分割モードA/B/Cは何が違うか
Sudachiには3つの分割モードがあります。Aは国語研の短単位に相当する最も短い単位、Cは固有表現を含む長い単位、Bはその中間です。OpenSearch向けプラグインのsplit_modeは、A・B・Cのいずれかを指定でき、既定値はCです。
違いは文字で読むより出力で見たほうが速く分かります。次の表は、コアの辞書(SudachiDict 20261015)で各語を3つのモードに通した結果です。実行にはPython版のSudachiPy 0.7.0を使いました。OpenSearchのプラグインとは実装が別なので、OpenSearch側の結果は後述の_analyzeで照合してください。
| 入力 | Aモード | Bモード | Cモード |
|---|---|---|---|
| 選挙管理委員会 | Aモード選挙/管理/委員/会 | Bモード選挙/管理/委員会 | Cモード選挙管理委員会 |
| 国家公務員試験 | Aモード国家/公務/員/試験 | Bモード国家/公務員/試験 | Cモード国家公務員/試験 |
| 東京都立大学 | Aモード東京/都立/大学 | Bモード東京/都立/大学 | Cモード東京都立大学 |
| 関西国際空港 | Aモード関西/国際/空港 | Bモード関西/国際/空港 | Cモード関西国際空港 |
| 形態素解析器 | Aモード形態/素/解析/器 | Bモード形態素/解析/器 | Cモード形態素/解析/器 |
| 環境構築手順 | Aモード環境/構築/手順 | Bモード環境/構築/手順 | Cモード環境構築/手順 |
| 全文検索システム | Aモード全文/検索/システム | Bモード全文/検索/システム | Cモード全文/検索/システム |
表から読み取れる点は3つあります。
- Bだけが固有の切れ目になった語は、7語のうち「国家公務員試験」「選挙管理委員会」の2語だけ
- Cでも長い複合語を作らない語がある。「全文検索システム」は3モードとも同じ
- Cがどこまで長くなるかは、辞書にその語が載っているかで決まる。「国家公務員」「環境構築」は載っているので1語になる
最後の点は、モードの選び方より辞書の内容のほうが結果を左右することを示しています。次の節で確かめます。
辞書がsmallだとAとCは同じ結果になる
Sudachiの辞書にはSmall・Core・Fullの3種類があります。Smallは語彙がUniDicの範囲に限られ、Coreは基本的な語彙を含み(既定)、Fullは雑多な固有名詞まで収録します。この違いが分割モードに効きます。前の表の7語に「検索エンジン」を加えた8語を、small・core・fullの3つで、AモードとCモードに通しました。
| 入力 | small(A=C) | core(Cモード) | full(Cモード) |
|---|---|---|---|
| 選挙管理委員会 | small(A=C)選挙/管理/委員/会 | core(Cモード)選挙管理委員会 | full(Cモード)選挙管理委員会 |
| 関西国際空港 | small(A=C)関西/国際/空港 | core(Cモード)関西国際空港 | full(Cモード)関西国際空港 |
| 国家公務員試験 | small(A=C)国家/公務/員/試験 | core(Cモード)国家公務員/試験 | full(Cモード)国家公務員試験 |
| 形態素解析器 | small(A=C)形態/素/解析/器 | core(Cモード)形態素/解析/器 | full(Cモード)形態素解析/器 |
| 東京都立大学 | small(A=C)東京/都立/大学 | core(Cモード)東京都立大学 | full(Cモード)東京都立大学 |
| 検索エンジン | small(A=C)検索/エンジン | core(Cモード)検索エンジン | full(Cモード)検索エンジン |
| 環境構築手順 | small(A=C)環境/構築/手順 | core(Cモード)環境構築/手順 | full(Cモード)環境構築/手順 |
| 全文検索システム | small(A=C)全文/検索/システム | core(Cモード)全文/検索/システム | full(Cモード)全文/検索システム |
smallでは、8語すべてでAモードとCモードの出力が一致しました。SudachiのREADMEが示すとおり、smallはUniDicの語彙だけで構成されるため、それより長い単位の語が辞書にそもそもありません。split_mode: "C"と書いても、smallの辞書ではAと同じ細かさにしかなりません。
逆にfullでは、Cが一段長くなる語が増えます。「国家公務員試験」は1語になり、「形態素解析器」は「形態素解析/器」と切れます。「全文検索システム」は、coreでは3つに切れたのに、fullでは「全文/検索システム」の2つになりました。
モードの違いを検証したい場合は、使う辞書と同じ辞書で必ず比べてください。small辞書を載せたまま「Cモードは長くならない」と結論すると、判断を誤ります。
索引側と検索側でモードが違うと何が起きるか
分割モードは、索引を作る側と検索語を処理する側で別々に選べます。次の小さな実験で、組み合わせごとの違いを見ました。
文書は5件で、「関西国際空港の滑走路」「国際会議の運営」「空港バスの時刻表」「選挙管理委員会の発表」「委員会の議事録」です。索引側をA・C・A+C(AとCの両方のトークンを持つ)の3通りで切り、検索語はクエリ全体を1つのモードで切ったうえで、検索語の全トークンを含む文書を返します。OpenSearchのスコアリングやフレーズ検索の挙動を再現したものではなく、トークンの集合が合うかどうかだけを見る簡易な実験です。
| 索引側 | 「空港」(A) | 「関西国際空港」(C) | 「委員会」(C) | 「選挙管理委員会」(A) |
|---|---|---|---|---|
| Aのみ | 「空港」(A)文書1・3 | 「関西国際空港」(C)ヒットなし | 「委員会」(C)ヒットなし | 「選挙管理委員会」(A)文書4 |
| Cのみ | 「空港」(A)文書3だけ | 「関西国際空港」(C)文書1 | 「委員会」(C)文書5 | 「選挙管理委員会」(A)ヒットなし |
| A+C | 「空港」(A)文書1・3 | 「関西国際空港」(C)文書1 | 「委員会」(C)文書5 | 「選挙管理委員会」(A)文書4 |
読み取れる結果は次のとおりです。
- 索引をCだけで作ると、「空港」で検索しても「関西国際空港」の文書(文書1)が落ちる。冒頭の現象はこれ
- 索引をAだけで作ると、「関西国際空港」のようにCで切ったクエリが一件も当たらない。「委員会」も、Aでは「委員」「会」に分かれるので当たらない
- A+Cの組み合わせなら、4つのクエリがすべて期待の文書に届く
- ただしA+Cでも、「委員会」で文書4(選挙管理委員会)は当たらない。B単位の「委員会」は、AにもCにも現れないため
最後の点が、Bモードを無視できない理由です。AとCで十分と判断する前に、手元のコーパスで「B単位でしか現れない語」が検索語として使われるかどうかを調べる価値があります。
OpenSearchでの設定: split_modeとsudachi_split
OpenSearch向けのプラグインでは、分割の指定方法が2つあります。
1つ目はsudachi_tokenizerのsplit_modeです。A・B・Cから1つを選び、そのモードで切った結果だけを返します。
{
"settings": {
"index": {
"analysis": {
"tokenizer": {
"sudachi_tokenizer": {
"type": "sudachi_tokenizer",
"split_mode": "A",
"discard_punctuation": true
}
},
"analyzer": {
"sudachi_analyzer": {
"type": "custom",
"tokenizer": "sudachi_tokenizer"
}
}
}
}
}
}2つ目は、sudachi_splitトークンフィルタです。READMEでは、Kuromojiのmodeに相当するものと説明されています。modeがsearchのときは、C単位とA単位の両方を出力します。READMEの例では「関西国際空港」から、「関西国際空港」「関西」「国際」「空港」の4つが返ります。extendedは、これに加えて未知語を1文字ずつにも分けます。
{
"settings": {
"index": {
"analysis": {
"tokenizer": {
"sudachi_tokenizer": { "type": "sudachi_tokenizer" }
},
"analyzer": {
"sudachi_analyzer": {
"filter": ["my_searchfilter"],
"tokenizer": "sudachi_tokenizer",
"type": "custom"
}
},
"filter": {
"my_searchfilter": {
"type": "sudachi_split",
"mode": "search"
}
}
}
}
}
}前の節の「A+C」に当たるのが、このsearchモードです。ただし、READMEには注意書きがあります。検索クエリでは、分割された部分語が、複数語の同義語と同じ扱いでフレーズとして処理されます。AとCの両方の単位で検索したい場合は、フィルタではなく複数のトークナイザーを使うよう案内されています。
結果は_analyzeで確かめる
設定を入れたら、インデックスに対して_analyzeを呼びます。READMEの例と同じ形です。
POST sudachi_sample/_analyze
{
"analyzer": "sudachi_analyzer",
"text": "関西国際空港"
}返るトークンのpositionとpositionLengthを見れば、C単位の「関西国際空港」が3つ分の長さ(positionLengthが3)で、A単位の3語と重なっていることが読み取れます。プラグインはSudachi本体を同梱しており、3.6.0のREADMEには内部のSudachiをv0.8.0に更新したとあります。手元のPython版との出力差は、この_analyzeで最終確認する運用が確実です。
辞書を同梱する手順
モードの設定より先につまずきやすいのが辞書の置き場所です。プラグインと辞書は、次の流れで入れます。
プラグインと辞書を入れる流れ
- 1
対応ブランチを選んでビルドする
OpenSearchは、
os-2.6-plus(2.6系から2.19系まで)とos-3(3.0系から3.5系まで)の2つのブランチが対応しています。ビルドは./gradlew -PengineVersion=os:2.19.3 buildのように、engineVersionにos:を付けて指定します。 - 2
プラグインを入れる
READMEのインストール例は、Elasticsearchの
bin/elasticsearch-plugin installで書かれています。OpenSearchの導入コマンドは、使うバージョンのREADMEとリリースページの案内に合わせてください。 - 3
辞書のzipを取得してdicを置く
SudachiDictのリリースから辞書のzipを取得し、中のdicファイルを
config/sudachi/の下に置きます。 - 4
リソースをconfigディレクトリの下に置く
READMEは、セキュリティマネージャーの制約のために、設定ファイルや辞書などのリソースをconfigディレクトリの下に置く必要があると書いています。
resources_pathとsettings_pathの相対パスは、この設定ディレクトリを基準に解決されます。
READMEの食い違いに注意する
辞書のファイル名は、READMEの中で2通りに書かれています。
- インストール手順の4番は、
config/sudachi/system_core.dicに置くよう指示している - Dictionary節は、既定の辞書を
ES_HOME/config/sudachi/sudachi_core.dicと書いている
どちらが現行の挙動かは、READMEだけでは判断できません。起動時に辞書が見つからないエラーが出たときは、まず2つのファイル名の両方を疑ってください。辞書を別名で置いている場合は、settings_pathの設定ファイルかadditional_settingsでsystemDictを明示すると、既定のファイル名に頼らずに済みます。
"sudachi_tokenizer": {
"type": "sudachi_tokenizer",
"split_mode": "C",
"additional_settings": "{\"systemDict\":\"system_full.dic\",\"userDict\":[\"user.dic\"]}"
}READMEの例に沿った書き方です。additional_settingsを設定するとsettings_pathは上書きされるため、両方は書かないでください。
ユーザー辞書は分割情報まで書く
業務の固有名詞をユーザー辞書に足すときは、分割情報を省略すると意図しない結果になります。実際に確かめました。
「社内検索基盤」を登録して、「社内検索基盤を刷新する」を切った結果が次の表です。登録は、Sudachiのユーザー辞書ガイドにある項目名のヘッダー行付きCSVで、SudachiPyのsudachipy ubuildを使ってバイナリ化しました。
| 辞書の状態 | Aモード | Cモード |
|---|---|---|
| ユーザー辞書なし | Aモード社内/検索/基盤/を/刷新/する | Cモード社内/検索/基盤/を/刷新/する |
| 分割情報なしで登録 | Aモード社内検索基盤/を/刷新/する | Cモード社内検索基盤/を/刷新/する |
| Split_AとSplit_Bを書いて登録 | Aモード社内/検索/基盤/を/刷新/する | Cモード社内検索基盤/を/刷新/する |
分割情報を書かずに登録すると、Aモードでも「社内検索基盤」が1語のままです。ここでsplit_mode: "A"を指定しても、短い単位で検索する用途には使えません。Split_AとSplit_Bの両方に構成語への語参照を書くと、AはCと違う結果を返すようになりました。
分割情報を書いたCSVの例は、次のとおりです。語参照は「見出し表記,品詞,読み」をカンマでつないだ文字列で、複数の語は半角スラッシュで区切り、フィールド全体をダブルクォートで囲みます。
Index_Form,Left_Id,Right_Id,Cost,Headword,POS1,POS2,POS3,POS4,POS5,POS6,Reading_Form,Normalized_Form,Dictionary_Form,Split_A,Split_B,Word_Structure
社内検索基盤,5146,5146,5000,社内検索基盤,名詞,普通名詞,一般,*,*,*,シャナイケンサクキバン,社内検索基盤,,"社内,名詞,普通名詞,一般,*,*,*,シャナイ/検索,名詞,普通名詞,サ変可能,*,*,*,ケンサク/基盤,名詞,普通名詞,一般,*,*,*,キバン","社内,名詞,普通名詞,一般,*,*,*,シャナイ/検索,名詞,普通名詞,サ変可能,*,*,*,ケンサク/基盤,名詞,普通名詞,一般,*,*,*,キバン","社内,名詞,普通名詞,一般,*,*,*,シャナイ/検索,名詞,普通名詞,サ変可能,*,*,*,ケンサク/基盤,名詞,普通名詞,一般,*,*,*,キバン"構成語の品詞と読みは、手で推測せず、システム辞書で各語を切って出た値をそのまま写します。ビルドは次のように実行します。
sudachipy ubuild -s path/to/system.dic \
-o user.dic user_dict.csvSudachiPy 0.7.0のubuildでは、次の3点でエラーになります。
- ガイドのCSV例のヘッダー行(
Surface,LeftId,...)は、SudachiPy 0.7.0のubuildではINVALID_COLUMN_NAMEで拒否された。項目一覧の名前(Index_Form、Left_Idなど)に直すと通った - ヘッダー行を付けないと旧形式として解釈され、項目の数が合わず別のエラーになる
- 分割情報の欄に
*を入れるとInvalid word split *で失敗する。使わない分割欄は空にする
Java版のUserDictionaryBuilderでビルドする場合の手順は、ユーザー辞書ガイドに載っています。
ユーザー辞書のバイナリには、ビルドに使ったシステム辞書の識別情報が記録されます。ビルドに使ったシステム辞書と組み合わせて使う前提なので、システム辞書を更新したら、ユーザー辞書も再ビルドします。複数のユーザー辞書を並べたときは、後ろにあるものほど優先されます。
比較をClaude Codeに回させる
ここまでの比較は、Claudeに「いい感じのモードを選んで」と頼むより、再実行できるスクリプトにして結果を読ませるほうが確かです。分割の結果は辞書の中身に左右されるので、記憶から答えさせると外れます。
比較スクリプトをリポジトリに置く
最小の比較スクリプトは、次の程度です。
import sys
from sudachipy import dictionary, tokenizer
M = tokenizer.Tokenizer.SplitMode
t = dictionary.Dictionary(dict="core").create()
for line in open(sys.argv[1], encoding="utf-8"):
s = line.strip()
if not s:
continue
modes = {n: "/".join(m.surface() for m in t.tokenize(s, mode))
for n, mode in (("A", M.A), ("B", M.B), ("C", M.C))}
print(s, modes)使う辞書は、本番のOpenSearchに載せる辞書と揃えます。dict="core"の部分を、本番がfullなら"full"にします。
CLAUDE.mdに検証の約束を書く
CLAUDE.mdに書いた内容は、セッションの文脈として読み込まれます。分割モードの相談では、次のような約束を置いておくと、推測で答えさせる場面を減らせます。
## 形態素解析
- Sudachiの分割結果は、記憶で答えず必ず `python tools/compare_modes.py samples.txt` を実行して読む
- 比較する辞書は本番と同じ(core/full)にそろえる。small辞書ではAとCが同じになる
- OpenSearch側の結果は `_analyze` の出力で最終確認し、トークンの position を見るCLAUDE.mdに検証コマンドを書いておく考え方は、Claude CodeでVitestを回す — CLAUDE.mdに書くテストコマンドとwatch回避と同じです。業務用語とコード上の名前を対応表にして渡す方法は、ドメイン用語集をCLAUDE.mdに置く手順にあります。ユーザー辞書に登録する固有名詞の候補を拾う下調べにも使えます。
実行環境とパイプ
Python版のSudachiPyを入れる仮想環境は、Claude Codeのセッションをまたいで引き継ぎたいことが多い設定です。Bashツールの環境変数の持ち越し方は、CLAUDE_ENV_FILEでvenvとcondaを引き継ぐ方法で説明しています。
非対話で使うときは、Claude Codeの公式ワークフローにあるclaude -pへのパイプの形に載せられます。
python tools/compare_modes.py samples.txt \
| claude -p "AとCで切れ方が違う語だけを挙げて"標準入力と標準出力が通常のUnixツールと同じように扱えるので、比較結果をそのまま渡せます。返ってくる内容は実行するまで分かりません。出力の語が元のスクリプトの結果に実在するかは、読む側で見てください。
まとめ
分割モードの選び方は、辞書を決めてから考えます。smallではAとCが同じになるため、モードの差を検証する意味がありません。coreかfullを前提に、索引はAとCの両方のトークンを持たせ(sudachi_splitのsearch)、検索語側は用途に応じて使い分ける構成が出発点になります。B単位でしか現れない語が検索語になる業務では、AとCだけでは足りないことがあります。
ユーザー辞書は、分割情報を書かないとAモードでも1語のままになります。業務の固有名詞を足すときは、Split_AとSplit_Bまで書いて、前後を比較スクリプトで見比べるのが確実です。最後はOpenSearchの_analyzeで、実際の索引設定の出力を確認します。
参照した一次ソース
この記事の記述は、次の一次ソースと照合しています。更新時に参照先が変わった場合は一覧も更新します。