Claude Codeで検索サジェストと読み仮名を作る — かな入力から漢字語を引く
kuromoji_readingformで漢字語の読みを取り、かな入力から候補を引くcompletion suggesterの作り方です。Lucene 9.12.1での実測値と、Claude Codeに回帰検証させるスキルを載せます。
日本語のサジェストで最初に詰まるのは、ユーザーが「けんさく」と打っても、辞書にあるのは「検索」だという点です。completion suggesterは前方一致の仕組みなので、読みを別に持たせないと「けん」では何も出ません。
この記事では、kuromoji_readingformで読みを取り、かな入力から漢字語を引く構成を組みます。読みの出力は、Elasticsearchのkuromojiプラグインが組み込むLuceneのkuromojiモジュール(9.12.1)を手元で動かして取った実測値です。かなの正規化と前方一致はPythonで動かし、Claude Codeに回帰検証させるスキルまで書きます。
completion suggesterは前方一致なので、読みを自分で用意する
completion suggesterは、入力途中のテキストから候補を返すauto-complete向けの機能です。typo補正やdid-you-mean向けではありません。速さを優先する設計で、データ構造はメモリー上に置かれ、構築のコストも高めです。
使うにはフィールドを completion 型で宣言します。登録する値は input に文字列の配列として渡せます。1件の文書に「検索」「けんさく」「kensaku」を全部入れておけば、どの打ち方からも同じ文書に当たります。
日本語サジェストの設計は、結局この input に何を入れるかに尽きます。
input に入れる3種類
- 表記そのもの(検索)。漢字のまま打つ人向け
- 読みをひらがなにした形(けんさく)。かな入力向け
- ローマ字(kensaku)。IMEを切ったまま打つ人向け
読みは手で書かず、形態素解析に出させます。kuromojiプラグインには、トークンを読みに置き換える kuromoji_readingform フィルターがあります。
kuromoji_readingformの出力を実測する
kuromoji_readingform は、トークンをカタカナまたはローマ字の読みに置き換えます。設定項目は use_romaji だけです。定義済みフィルターでは true ですが、自分で定義するときの既定は false なので、カタカナが欲しければ次のように自分で定義します。
{
"settings": {
"index": {
"analysis": {
"analyzer": {
"katakana_analyzer": {
"tokenizer": "kuromoji_tokenizer",
"filter": [ "katakana_readingform" ]
}
},
"filter": {
"katakana_readingform": {
"type": "kuromoji_readingform",
"use_romaji": false
}
}
}
}
}
}実際の読みは辞書で決まります。Lucene 9.12.1のkuromojiモジュールで、JapaneseTokenizer(検索モード)の後ろに JapaneseReadingFormFilter を置いて動かすと、次の結果になりました。実行環境はJDK 17です。Elasticsearch本体が同梱するLuceneの版とは違うことがあり、版が違えば辞書も読みも変わりえます。本番と同じ版で再度確かめてください。
| 入力 | 読み(カタカナ) | トークンの切れ方 |
|---|---|---|
| 寿司 | 読み(カタカナ)スシ | トークンの切れ方1トークン |
| 検索 | 読み(カタカナ)ケンサク | トークンの切れ方1トークン |
| サジェスト | 読み(カタカナ)サジェスト | トークンの切れ方1トークン |
| 読み仮名 | 読み(カタカナ)ヨミ / カメイ | トークンの切れ方2トークン |
| 日本語入力 | 読み(カタカナ)ニホンゴ / ニュウリョク | トークンの切れ方2トークン |
| 東京都 | 読み(カタカナ)トウキョウ / ト | トークンの切れ方2トークン |
| claude code検索 | 読み(カタカナ)claude / code / ケンサク | トークンの切れ方3トークン |
ローマ字にする設定(use_romaji を有効にした場合)では、寿司が sushi、検索が kensaku でした。
ここから設計上の注意が2つ出ます。
- 複合語は複数のトークンに分かれる。サジェスト用の読みは全トークンを連結して1本にする(ヨミカメイ、ニホンゴニュウリョク)
- 英字は素通しになる。
claudeをクロードにはしてくれない
トークンの取り出しには _analyze APIが使えます。返り値の tokens 配列を連結すれば、1語分の読みになります。
読みを作る場所は3通りある
読みをどこで作るかで、直す場所が変わります。
| 方法 | 読みを作る場所 | 得られる形 |
|---|---|---|
登録処理で読みを作り input に配列で渡す | 読みを作る場所アプリ側(Pythonなど) | 得られる形表記 + ひらがな |
kuromoji_readingform をanalyzerに入れる | 読みを作る場所Elasticsearchの解析 | 得られる形カタカナまたはローマ字 |
kuromoji_completion をanalyzerに入れる | 読みを作る場所Elasticsearchの解析 | 得られる形表記 + ローマ字 |
読みの誤りを直したいなら、アプリ側で作るのが向いています。社内用語や製品名は辞書に載っていないため、登録処理に「この語はこの読み」という上書き表を持たせると、直す場所が1か所で済みます。解析側に載せる構成は運用が軽い反面、辞書の読みに手を入れにくくなります。
どちらの場合も、kuromojiプラグインは全ノードに入れる必要があります。入れたあとは各ノードの再起動も要ります。
sudo bin/elasticsearch-plugin install analysis-kuromojiなお、同じプラグインには hiragana_uppercase というフィルターもあります。小さい仮名(捨て仮名)を標準の仮名に揃えるもので、特許や契約書のような旧い書き方の文書を検索する場面向けです。「ちょ」と「ちよ」を別物として補完したいサジェストには、入れない選択になります。
検索語側は形態素解析にかけない
ここが一番の落とし穴です。登録側と同じ要領で、入力途中の「けんさく」もkuromojiに通したくなります。同じ環境で試すと、そうはいきませんでした。
けんさく -> ケン | サク
すし -> スシひらがなの「けんさく」は「ケン」「サク」に割れます。「検索」の読みであるケンサクとは一致しません。かな入力は、辞書の見出し語として解析される保証がないからです。
そこで検索語側は形態素解析をやめ、文字種の正規化だけで済ませます。全角・半角の揺れをNFKCで揃え、カタカナをひらがなへ寄せます。
import unicodedata
def to_hira(s: str) -> str:
s = unicodedata.normalize("NFKC", s)
return "".join(
chr(ord(c) - 0x60) if "ァ" <= c <= "ヶ" else c
for c in s
).lower()カタカナ(U+30A1〜U+30F6)から0x60を引くとひらがなになります。半角カナの「ガイド」もNFKCで全角になり、そのままひらがなへ落ちます。
kuromoji_completion フィルターには、ローマ字を足す役割があります。index モードは索引時の単純なローマ字化、query モードは入力方式を意識したローマ字化で、検索時に使います。Luceneの同名フィルターで動かすと、「検索」(index)からは 検索 / kensaku / kennsaku、「けんさく」(query)からは けんさく / kensaku / kennsaku が出ました。ローマ字はこのフィルターに任せ、ひらがなだけを自前で作る分担にすると、実装が小さく済みます。
登録データと照合の動きを確かめる
マッピングは completion 型です。analyzer の既定は simple、search_analyzer の既定は analyzer と同じ値です。かな正規化を自前で済ませるなら、既定のままで使えます。
{
"mappings": {
"properties": {
"suggest": { "type": "completion" }
}
}
}文書は、表記・ひらがな・ローマ字を input の配列に並べ、weight で順位を付けます。例えば次の形です。
{
"suggest": {
"input": ["検索", "けんさく", "kensaku"],
"weight": 30
}
}登録側にも、先ほどの to_hira を通します。登録側と検索側で同じ関数を使えば、片方だけ正規化し忘れる事故が起きません。
この照合規則を、Elasticsearchなしで再現して動かしました。各語について、表記と読み(カタカナ)をひらがなにして前方一致させる簡易版です。
| 打った文字 | 返った候補 |
|---|---|
| けん / ケン / ケン | 返った候補検索、検証、権限 |
| けんさ | 返った候補検索 |
| よみ | 返った候補読み仮名 |
| さじ | 返った候補サジェスト |
| くろーど | 返った候補クロードコード |
| にほんご | 返った候補日本語入力 |
| kensaku | 返った候補(なし) |
「けん」「ケン」「半角のケン」は、同じ3件に行き着きます。ローマ字の kensaku が空なのは、この簡易版がひらがな照合だけだからです。ローマ字入力も拾うなら、登録側にローマ字の input を入れる必要があります。
候補が複数あるときの並びは weight で決まり、返る _score もこの値です。「けん」に検索、検証、権限が並ぶなら、使われやすい語に大きい値を振ります。
Claude Codeに作らせて検証させる
実装そのものはClaude Codeに任せられます。ただ、日本語の補完は「それらしく動く」と「期待どおり動く」の差が大きい領域です。期待値を先にテストとして渡します。
プロジェクトのCLAUDE.mdには、設計の前提を短く書きます。
## サジェスト実装の前提
- 検索語側は形態素解析しない。to_hira() の文字種正規化のみ
- 登録側の input は [表記, ひらがな, ローマ字] の3種
- 読みはトークンを全部連結して1本にする
- 変更後は tools/kana_suggest.py で回帰テストを走らせる前節の表が、そのまま期待値になります。入力と期待候補をタブ区切りのファイルにして、検証をスキルにまとめます。スキルは .claude/skills/<スキル名>/SKILL.md に置くと、そのリポジトリのセッションで使えます。
---
name: kana-suggest-check
description: かな正規化とサジェストの回帰テストを実行する。サジェスト実装を変更した後、または読みの出力を確認したいときに使う。
allowed-tools: Bash(python3 tools/kana_suggest.py *)
---
## 手順
1. `python3 tools/kana_suggest.py --expect tests/expected.tsv` を実行する
2. 失敗した行を表に整理し、原因が正規化・登録・照合のどこかを切り分ける
3. 期待値のファイルは、利用者に確認するまで書き換えないallowed-tools に書いた範囲は、スキルを呼んだターンに限り、許可の確認なしで実行できます。次のメッセージを送ると失効します。範囲を tools/kana_suggest.py に絞ってあるので、ほかのコマンドは通常どおり確認を求められます。
手順3は意図して入れています。テストが落ちたときに、期待値のほうを実装に合わせて直されると、検証になりません。
本番に近づけるときのつまずき
落とし穴
長い input は先頭50文字で切られる
max_input_length の既定は50 UTF-16コードポイントで、インデックス時に1つの input の長さを制限します。長いタイトルをそのまま入れると、後ろが切れます。前方一致の補完では先頭の数文字が効けば足りる用途が多く、普通は困りません。長い読みを丸ごと入れる設計にするときは、頭に入れておきます。
ほかにも、次の点があります。
inputには\u0000、\u001f、\u001eの制御文字を入れられない。名寄せ用の区切りにこれらを使っていると、登録で弾かれるpreserve_separatorsは既定で有効。無効にすると、Foo Fightersがfoofで当たる。区切りを無視したい場面でだけ検討する- 長音「ー」は、ひらがな変換でも変わらない。「くろーど」はクロードコードに届くが、「くろおど」は届かない。吸収したいなら、登録側に別の
inputを足す - 「ヴ」は「ゔ」に変わる。「ヴァイオリン」は「ゔぁいおりん」になるので、IMEの出力と揃うかを確かめる
複数のシャードにまたがるcompletionクエリは、2段階で実行されます。補完の性能を最大にするには、単一シャードのインデックスが勧められています。ヒープ使用量が高いときは、性能よりシャードの分割を優先します。completionの構造はヒープ上に常駐します。表記・ひらがな・ローマ字の3本立ては、登録件数の約3倍の input になるので、見積もりに入れてください。
同音異義語はweightで運用する
読み入力の本質的な限界は、同音異義語を区別できないことです。「けん」には検索、検証、権限、県、剣が全部入ります。かなの時点では意図を絞れません。
仕組みで解こうとするより、weight の設計で運用するほうが現実的です。検索ログがあれば、クリック数に比例した値を振ります。ログのない立ち上げ期は、社内で使われる用語を優先して手で振るところから始められます。カテゴリや位置で候補を絞りたい場合には、context suggesterという機能もあります。
検索クラスタの構築や運用をClaude Codeに任せる流れは、Typesense Cloud MCPの記事が扱っています。検証コマンドを持つスキルで仕様書を書かせる組み方は、AsyncAPIのスキルの記事と同じ考え方です。
まとめ
登録側ではkuromojiで読みを取り、検索語側では形態素解析せずに文字種だけを揃える。この非対称が、「けんさく」が「ケン」「サク」に割れる実測への答えです。
Claude Codeに任せるなら、実装より先に期待値の表を渡すのが近道です。表の行がそのままスキルの回帰テストになり、変更のたびに同じ入力が同じ候補を返すかを機械的に確かめられます。weight は、立ち上げ時は手で、運用に入ったらログから決める二段階にすると、順位づけが破綻しにくくなります。
参照した一次ソース
この記事の記述は、次の一次ソースと照合しています。更新時に参照先が変わった場合は一覧も更新します。