
形態素解析
イベント
該当するコンテンツが見つかりませんでした
マガジン

技術ブログ
はじめに こんにちは、WEAR開発部バックエンドブロックの小山です。普段は弊社サービスである WEAR のバックエンド開発を担当しています。 WEARの投稿検索(フリーワード検索)では、 OpenSearch を検索基盤として利用しています。従来の検索方法では、ユーザーが入力するクエリの表記ゆれや誤字・略称により、適切な検索結果を返せないケースがありました。この課題を解決するため、全文検索とベクトル検索を組み合わせたハイブリッド検索を導入しました。しかし、導入の過程ではストレージの肥大化やレイテンシの増加、検索精度のチューニングといった技術的課題に直面しました。本記事では、これらの課題と解決に至るまでの取り組みをご紹介します。 なお、ハイブリッド検索の導入にあたって行った検索インデクサーの刷新(ベクトルデータのOpenSearchへの連携基盤)については、以下の記事で紹介しています。本記事では、検索処理を担うアプリケーション側で直面した課題と解決策に焦点を当てます。 techblog.zozo.com 目次 はじめに 目次 背景・課題 従来のフリーワード検索の限界 ハイブリッド検索とは hybrid queryの構造 normalization-processorによるスコアの統合 ベクトル検索の導入で直面した課題 1. ストレージの肥大化 2. レイテンシの問題 3. 検索精度の課題 課題を解決したアプローチ 1. ベクトルデータの量子化と_source最適化によるストレージ削減 スカラー量子化(fp16)の適用 derived_sourceによる_sourceからのベクトル除外 施策の効果 2. warmup APIの定期実行による暖気機構の導入 スパイクの原因はコールドスタート warmup APIを実行するだけでは解決しなかった 導入した暖気機構 3. スコアの組み合わせ比率の最適化 search pipelineの設定 全文検索の重みを大きくした理由 精度検証の進め方 スコア閾値による関連性の低い結果の除外 あいまい検索を実行する条件の制御 効果 今後の改善 まとめ 背景・課題 従来のフリーワード検索の限界 WEARの投稿検索では、ユーザーが入力したクエリをルールベースでDBのレコードと突き合わせ、性別・カラー・カテゴリー・ブランド・タグなどの検索条件に変換して検索結果を返しています。しかし、この方式だけでは以下のようなケースで適切な検索結果を返せませんでした。 表記ゆれ:「Tシャツ」「Tシャツ」「ティーシャツ」のように同じ意味でも表記が異なるクエリ 誤字・略称:ルールベースのマッピングではカバーしきれない入力 意味的な類似検索:クエリと字面は異なるが意味的に近い投稿をヒットさせられない たとえば「平成レトロ」で検索した場合、従来は「#平成レトロ」というタグが付いた投稿しかヒットしませんでした。「夏フェス 野外」のような複数キーワードの検索は、両方のタグに完全一致する投稿しか返せず、ほとんどヒットしない状態でした。 これらのケースをカバーするには、タグの完全一致とは別の検索手段が必要でした。一般的に、キーワードが厳密に一致しない検索には次のような手法があります。 全文検索:タイトルや説明文などのテキストを対象に、キーワードと字面が一致する投稿を広く拾える ベクトル検索:キーワードの字面ではなく「意味」の近さで検索でき、別表記や言い換えにも対応できる WEARの課題は、「夏フェス 野外」のように字面の部分一致で解決できるケースと、「ティーシャツ」のように字面が異なるケースの両方にまたがっています。そのため、どちらか一方ではなく両者を組み合わせるハイブリッド検索の導入を決定しました。 なお、ハイブリッド検索は従来の検索を置き換えるものではありません。ルールベースのマッピングには、クエリの意図を性別やカラーなどの構造化された検索条件へ正確に落とし込めるという強みがあります。そのため従来のマッピングの機構は残したまま、タグ検索のヒット件数が少ない場合に検索結果の量を補助する位置づけでハイブリッド検索を適用しています(適用条件の詳細は後述します)。WEARでは、このハイブリッド検索を利用した検索機能を「あいまい検索」と呼んでいます。 ハイブリッド検索とは ハイブリッド検索とは、キーワードマッチによる全文検索と、意味的な類似度で検索するベクトル検索を組み合わせ、双方のスコアを統合して検索結果を返す検索手法のことです。OpenSearchでは、バージョン2.10で導入された hybrid query と normalization-processor を利用することで実現できます。 それぞれの検索手法には以下のような長所・短所があり、互いに弱点を補完し合う関係にあります。 検索手法 長所 短所 全文検索(BM25) キーワードの完全一致に強い、高速 同義語・言い換えに弱い、意味を理解しない ベクトル検索(k-NN) 意味的な類似性を捉えられる、表記ゆれ・言い換えに強い 計算コストが高い、キーワードの完全一致に弱い hybrid queryの構造 ハイブリッド検索のクエリは、hybrid queryの中に全文検索クエリとベクトル検索クエリを並べた構造になります。WEARでは、全文検索にはmulti_matchクエリを、ベクトル検索には検索キーワードをOpenSearch側でベクトル化して検索するneuralクエリを利用しています。 { " query ": { " hybrid ": { " queries ": [ { " multi_match ": { " query ": " 夏フェス 野外 ", " fields ": [ " title.sudachi ", " content.sudachi ", " ... " ] } } , { " neural ": { " text_embedding ": { " query_text ": " 夏フェス 野外 ", " model_id ": " <モデルID> " } } } ] } } } なお、フィールド名の .sudachi は形態素解析器(Analyzer)を表しています。全文検索における日本語の形態素解析にはSudachiを利用しており、kuromojiで解析したフィールドも併用できる構成にしています。 normalization-processorによるスコアの統合 ここで問題になるのが、2つの検索のスコアを単純に比較できないことです。BM25のスコアには上限がなく、マッチした単語数や出現頻度によって数十以上の値も取る一方、ベクトル検索の類似度スコアは0〜2程度の狭い範囲に収まります。そのまま足し合わせると、値の大きい全文検索のスコアに結果が引きずられてしまいます。 normalization-processorは、このようにスケールの異なるスコアを揃えて統合するための仕組みです。search pipelineに設定すると、検索結果へ次の2段階の処理を適用します。 正規化(normalization):各クエリのスコアを同じ土俵で比較できるように変換する。min_max方式では (スコア - 最小値) / (最大値 - 最小値) で0〜1の範囲に正規化する 結合(combination):正規化したスコアを重み付きで結合し、最終スコアを算出する なお、WEARの検索では、ハイブリッド検索のスコアを並び順に使っていません。スコアは、関連性の低い投稿を除外する足切りに使っています。足切りを通過した投稿は、人気順スコアで並べ替えて返します。関連度だけで並べると、検索結果に含まれる投稿の質がばらつきやすいためです。関連度は足切りで担保し、その上で人気の高い投稿から表示して検索結果の質を保つ設計です。 検索リクエストからレスポンスまでの流れは以下のとおりです。 ベクトル検索の導入で直面した課題 ハイブリッド検索を実現するためにベクトル検索を導入したところ、リリース前に解消すべき以下の技術的課題が発生しました。 1. ストレージの肥大化 投稿ごとに1024次元の埋め込みベクトルを保持するため、インデックスのストレージサイズが大幅に増加しました。ベクトルの各要素を32ビットの浮動小数点(fp32、4バイト)で保持すると、1投稿あたり約4KB(1024次元 × 4バイト)のベクトルデータが追加されます。対象の投稿は膨大なため、全量を投入するとベクトルデータだけで数十GB規模の追加になる見積もりでした。実際に、一部の投稿データで構築した検証用インデックスを実測すると、fp32のベクトルを含むサイズは13.2GBに達していました。 ベクトルデータの増加は、ストレージコストだけの問題ではありません。後述のとおり、k-NN検索ではHNSWグラフを検索時にメモリへロードしておく必要があるため、ベクトルデータが増えるほど必要なメモリ容量も増えます。ディスクは増設で対応できますが、メモリの確保はインスタンスの増強が必要になり、コストへの影響が大きくなります。 2. レイテンシの問題 ベクトル検索(k-NN検索)では、近いベクトル同士をつないだグラフ構造(HNSW)をインデックスとして構築し、検索時にそれをたどることで高速な近傍探索を実現しています。このグラフをメモリにロードしていない状態で検索すると、レイテンシが大きく悪化する問題を抱えていました。実際に、通常時は200ms程度で応答する検索が、不定期に20秒程度まで悪化するスパイクを起こしていました。 3. 検索精度の課題 ハイブリッド検索を導入しただけでは期待する検索精度が得られず、全文検索とベクトル検索のスコアをどのような比率で組み合わせるかのチューニングが必要でした。 たとえば、ベクトル検索の重みを大きくしすぎると、ブランド名で検索した際に意味的に近い別ブランドの投稿が混入してしまいます。逆に全文検索の重みを大きくしすぎると、表記ゆれや言い換えをカバーするというベクトル検索導入の狙いが薄れてしまいます。 課題を解決したアプローチ 1. ベクトルデータの量子化と_source最適化によるストレージ削減 ストレージの肥大化に対しては、ベクトルデータの量子化(fp16化)と、 _source フィールドからのベクトルデータ除外(derived_source)の2つの施策で対応しました。 スカラー量子化(fp16)の適用 量子化手法にはFaissエンジンのスカラー量子化(SQfp16)を採用しました。ベクトルの各要素をfp32から16ビットの浮動小数点(fp16)に変換して保持する手法で、ベクトルデータのサイズを50%削減できます。プロダクト量子化やバイナリ量子化ほどの圧縮率はないものの、精度への影響が小さく、事前学習も不要でマッピングの encoder 設定を追加するだけで導入できます。精度と導入の手軽さのバランスを重視して、この手法を選びました。 knn_vector フィールドの定義は以下のとおりです。 { " text_embedding ": { " type ": " knn_vector ", " dimension ": 1024 , " method ": { " name ": " hnsw ", " space_type ": " innerproduct ", " engine ": " faiss ", " parameters ": { " ef_construction ": 128 , " m ": 16 , " encoder ": { " name ": " sq ", " parameters ": { " type ": " fp16 " } } } } } } ただし、fp16化のみを適用した場合、インデックスサイズは13.2GBから12.2GBへと7.6%の削減にとどまりました。k-NN検索用のデータ構造は半減しても、 _source フィールドに元のベクトル値がそのまま保存されているためです。 derived_sourceによる_sourceからのベクトル除外 そこで、OpenSearch 2.19で実験的機能(experimental)として導入された knn.derived_source を有効化しました。この設定を有効にすると、ベクトルデータが _source に重複して保存されなくなり、必要な場合はk-NN検索用のデータ構造から導出されるようになります。 { " settings ": { " index ": { " knn ": true , " knn.derived_source.enabled ": true } } } なお、derived_sourceの有効化で検索時に20ms程度のオーバーヘッドを確認しましたが、許容範囲と判断しています。 施策の効果 2つの施策によるストレージ削減効果は以下のとおりです(検証用インデックスで実測)。 インデックス設定 ストレージサイズ 削減率 ベースライン(fp32) 13.2GB - fp16のみ 12.2GB 7.6% fp16 + derived_source 5.2GB 60.6% fp16化により検索精度への影響が懸念されたため、fp32のインデックスとfp16のインデックスのそれぞれに同一クエリで検索をかけ、結果を比較して検証しました。上位20件の重複率はおおむね95%以上で、スコアの差も小数点以下6桁目程度の違い(例:1.5960883と1.5960877)にとどまりました。結果が異なる投稿は実際に目視で確認し、検索クエリの意図から外れていないことを確かめた上で採用しています。 なお、この施策は投稿数がもっとも多いコーディネートのインデックスのみに導入しています。データ量の少ないインデックスでは削減効果が限定的なためです。 2. warmup APIの定期実行による暖気機構の導入 レイテンシの問題に対しては、OpenSearchの warmup API を定期実行する暖気機構を導入しました。 スパイクの原因はコールドスタート まず、k-NN統計API( GET /_plugins/_knn/stats )でレイテンシ悪化時の状態を分析しました。すると、メモリ不足によるグラフ破棄(Eviction)は発生しておらず、キャッシュミス(Miss Count)が多発していることがわかりました。つまり、スパイクの原因はメモリ不足ではなく、以下のメカニズムによるコールドスタートでした。 インデックス更新に伴いバックグラウンドでmerge/refreshが発生し、新しいHNSWグラフが作成される 新しいグラフはまだネイティブメモリにロードされていない そこに検索リクエストが到達すると、ディスクからのグラフロードが発生し、完了まで数秒〜数十秒待たされる warmup APIを実行するだけでは解決しなかった コールドスタート対策として、グラフを事前にメモリへロードするwarmup APIをデータ投入後に実行しましたが、それでもスパイクは解消しませんでした。調査の結果、原因はデータ投入とwarmupのタイミングにありました。 処理 同期/非同期 Bulk APIによるデータ投入 同期(ただし検索可能になるのはrefresh後) HNSWグラフ構築 非同期(Bulk完了後もバックグラウンドで継続) flush/merge 非同期(グラフデータのディスク永続化) warmup APIの対象になるのは、flush/mergeが完了しディスクに永続化済みのグラフのみです。HNSWグラフ構築とflush/mergeはBulk APIの完了後も非同期で続くため、データ投入直後にwarmupを実行しても、永続化前のグラフには適用されません。つまり「Bulk APIの完了 ≠ warmup実行可能なタイミング」だったのです。 導入した暖気機構 この調査を踏まえ、インデックスの洗い替え時と日常の差分更新のそれぞれに対して暖気を組み込みました。 フィールドの追加やマッピングの変更の際は、新しいインデックスを作成してデータを入れ直す洗い替え(リインデックス)が必要になります。洗い替えの際は以下の順に処理し、暖気が完了したインデックスだけがユーザーからの検索を受けるようにしています。 新インデックスを作成し、データを投入する refreshを実行し、投入したデータを検索可能にする warmup APIを実行し、HNSWグラフをネイティブメモリにロードする エイリアスを新インデックスに切り替える 一方、日常運用では差分更新によって新しいHNSWグラフが継続的に作られるため、洗い替え時の暖気だけではカバーできません。WEARでは、差分データを10分間隔で投入するワークフローが動いています。このワークフローの最終ステップにwarmup APIの実行を組み込み、差分投入で作られた新しいグラフを毎回暖気しています。ワークフローの流れは以下のとおりです。 差分データを抽出する(BigQuery) データをGCSからS3へ転送する ベクトル化してOpenSearchへ投入する(詳細は前述のインデクサー刷新の記事を参照) 投入完了を確認する 削除済みの投稿をインデックスから除去する refreshを実行し、投入したデータを検索可能にする warmup APIを実行し、新しいHNSWグラフをネイティブメモリにロードする なお、暖気を組み込んでもスパイクを完全には防げません。そのため、あいまい検索専用のOpenSearchクライアントに0.5秒の短いタイムアウトを設定しています。タイムアウトや一時的なエラーが発生した場合は、従来のタグ検索の結果を返すフォールバックを実装しています。あいまい検索が失敗してもユーザーには正常なレスポンスが返る多層的な構えとし、フォールバックの発生は監視基盤に記録して検知できるようにしています。 3. スコアの組み合わせ比率の最適化 検索精度の課題に対しては、ハイブリッド検索の実行結果を確認しながら、全文検索とベクトル検索のスコアを組み合わせる際の最適な重みを探りました。 search pipelineの設定 スコアの正規化と結合は、以下のsearch pipelineで行っています。正規化手法にはmin_maxを、結合手法には重み付き算術平均(arithmetic_mean)を採用し、最終的に全文検索を重視した重みに落ち着きました。 PUT /_search/ pipeline / hybrid - search - pipeline { " description ": " Post processor for hybrid search ", " phase_results_processors ": [ { " normalization-processor ": { " normalization ": { " technique ": " min_max " } , " combination ": { " technique ": " arithmetic_mean ", " parameters ": { " weights ": [ <全文検索の重み>, <ベクトル検索の重み> ] } } } } ] } 全文検索の重みを大きくした理由 検索方式 重み 理由 全文検索 大 ユーザーが入力したキーワードへの一致を重視。ファッション検索ではブランド名・アイテム名などの正確な一致が重要なため ベクトル検索 小 セマンティックな関連性を補完。「秋コーデ」に対する「紅葉」「ニット」のような関連概念の発見を支援するため 前述のとおり、ベクトル検索の重みを大きくしすぎるとキーワードと一致しない投稿が混入します。ファッションの検索ではブランド名やアイテム名の正確な一致が重要なため、キーワード一致を主、意味的な類似を従と位置づけてこの比率を選びました。 精度検証の進め方 精度検証は、nDCGのような定量指標による自動評価ではなく、目視による定性評価で進めました。評価用のクエリを用意し、検索結果をスプレッドシートへ並べて、キーワードとの関連性や表示順の違和感をチームで確認していきました。 調整は以下の手順で進めました。 ベクトル検索単体の足切りスコア(min_score)を見極める 全文検索の単体での足切りスコアを決める 重みづけを設定した上でハイブリッド検索を試し、ハイブリッド検索としての足切りスコアを決める その結果を定性評価する 実際に調整を進める中で、ベクトル検索・全文検索の単体では良い結果が得られているのに、ハイブリッド検索として組み合わせると期待どおりの結果にならないケースがありました。このようなケースを1件ずつ確認しながら、重みは固定したまま、足切りスコアの調整で精度を追い込んでいきました。 スコア閾値による関連性の低い結果の除外 重みの調整とあわせて、全文検索・ベクトル検索のそれぞれにスコアの下限値(min_score)を設定しています。ベクトル検索はどんなクエリに対しても「もっとも近い」ドキュメントを返してしまうため、閾値を設けないと関連性の低い投稿が検索結果に含まれてしまいます。閾値は、複数の評価用クエリで検索結果を定性評価し、クエリごとに求めた「関連性の低い結果を除外できるスコア」の平均値として決定しました。 なお、スコアの分布は固定ではない点に注意が必要です。実際、リリース後にOpenSearchをバージョンアップした際、全文検索のスコアが全体的に低く出るようになり、正常な検索結果が閾値で除外される事象が発生しました。この事象は、検索結果からの遷移率などのKPIモニタリングがきっかけで検知しました。その際は、リリース判定時と同じ検索語句でスコア順TOP1000件の分布を確認し、閾値を引き下げて対応しました。min_scoreを利用する場合は、バージョンアップなどによるスコア分布の変化まで含めた運用を想定しておくことをおすすめします。 あいまい検索を実行する条件の制御 背景で述べたとおり、あいまい検索は従来の検索を補助する位置づけであり、すべてのキーワードに対して実行しているわけではありません。検索キーワードがタグにマッピングされ、タグ検索で一定の件数以上の結果を得られる場合は、タグ検索の結果をそのまま返します。タグに完全一致する検索結果は精度が高く、十分な件数があればあいまい検索で補完する必要がないためです。 効果 これらの取り組みにより、ハイブリッド検索を本番運用できる状態でリリースできました。 ストレージ:fp16量子化とderived_sourceにより、インデックスサイズを13.2GBから5.2GBへ約60%削減しました レイテンシ:暖気機構により、コールドスタートに起因する最大20秒のレイテンシスパイクを大幅に抑制しました。リリース時のあいまい検索のレイテンシはp99平均369msで、事前の負荷試験と同水準の結果でした 検索精度:スコアの重みと閾値の調整により、キーワード一致を保ちつつ、表記ゆれや言い換えを含むクエリでも意図した投稿がヒットするようになりました たとえば「平成レトロ」と検索すると、従来はタグ「#平成レトロ」が付いた投稿しかヒットしませんでした。ハイブリッド検索の導入後は「平成」「平成ギャル」をタグや説明文に含む投稿も返せています。「秋っぽいコーデ」のようなあいまいな言い回しのクエリや「夏フェス 野外」のような複数キーワードのクエリでも、意味の近い投稿をヒットさせられています。 今後の改善 ハイブリッド検索の導入で表記ゆれや言い換えへの対応は進みましたが、運用する中で新たな課題も見えてきています。 部分一致によるノイズ:検索キーワード「白シャツ」に対して、「白Tシャツ」や「黒シャツ」を含む投稿まで高い類似度でヒットしてしまう 複数キーワードの検索意図の解釈:「ロングスカート 低身長」のような検索では、どちらか一方しか含まない投稿もヒットしてしまい、クエリ全体の意図を汲み取れない こうしたノイズを除外するチューニングとあわせて、コーディネート画像の活用も検討しています。社内ではすでに別部署が 画像からの特徴量の抽出・テキスト化 に取り組んでいます。その成果を検索データとして取り込む案と、画像そのものをベクトル化する案の両方が選択肢です。テキスト情報だけでは表現しきれないコーディネートの特徴から検索できるようになれば、あいまい検索の適用範囲をさらに広げられると考えています。 また、全文検索側では辞書の拡充も検討しています。語彙数の多い辞書や独自のユーザー辞書を導入すると、「夏フェス」のような複合語やトレンドから生まれる新語も1語として扱えるようになります。意図しない単語分割が減り、全文検索のノイズの低減にもつながると考えています。 さらに、あいまい検索の手前にあるクエリ理解の改善も検討しています。キーワードから検索条件へのマッピングの精度を高めれば、あいまい検索に頼る前の段階で、より適切な検索結果を返せるようになります。 まとめ 本記事では、WEARのフリーワード検索にハイブリッド検索を導入した際の課題と、その解決のための技術的工夫をご紹介しました。 ベクトル検索導入によるストレージ肥大化には、fp16へのスカラー量子化とderived_sourceによる _source 最適化で対応しました k-NN検索のレイテンシ問題には、リインデックス時の暖気完了後のエイリアス切り替えと、差分更新ワークフローへのwarmup組み込みによる暖気機構で対応しました 検索精度の課題には、全文検索とベクトル検索のスコアを組み合わせる重みとスコア閾値の最適化で対応しました OpenSearchでのハイブリッド検索の運用を検討している方や、精度検証の進め方に悩んでいる方の参考になれば幸いです。 ZOZOでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。 corp.zozo.com
1年ほど前に、日本語アナライザーを比較する記事を書きました。 前回の記事: 日本語アナライザーの比較(Kuromoji / Sudachi / MeCab / LLM の性能検証) あれから1年がたち、日本語の検索まわりは少しずつ変わりました。 新しい選択肢も出てきましたし、考え方も少し変わりました。 そこで今回は、続編としてもう一度比較します。 ただし、前回とまったく同じことを繰り返すわけではなく、 2026年の今なら、こう測るともっと良い、というやり方で見直します。 今回の検証は「去年と同じ条件での定点観測」ではありません。そのため、今回の数値を前回のものとそのまま比較できません。 検証に使用したプログラム等は GitHub リポジトリで公開しています。同リポジトリ内の GLOSSARY.md にまとめた用語集を参照してください。 目次 この記事で見ること、見ないこと この1年で変わったこと 1. 既存のアナライザーは、ちゃんと進化していた 2. 「Elasticsearch の中で使えるか」で整理すると分かりやすい 3. いちばん大きな変化:形態素解析に頼らない検索 4. LLM は「アナライザー」ではなく「参考枠」 今回の改良点(前回との違い) 再検証:実際に比べてみる 使うテキスト 比べるアナライザー 結果:トークン数 結果:専門用語の扱い 結果:アナライザー間の似ている度合い(Jaccard) 結果:検索クエリでの動作 参考:LLM は何を「キーワード」として拾ったか アナライザーの選び方ガイド まとめ Links この記事で見ること、見ないこと 先に、ゴールを明確に整理します。 読みながら「結局なにを比べているの?」と迷わないためです。 この記事で見ること: それぞれのアナライザーが、日本語をどう単語に区切るか。 検索用のトークンとして、どれが使いやすいか。 専門用語・英数字・単位(例:NSAIDs、300〜500mg)が保たれるか。 実際の検索クエリで、目的の文書がちゃんとヒットするか。 この記事で深くは扱わないこと: 大規模なデータでの検索ランキング評価。 人手による「この結果は正しい」という関連度判定。 LLM を Elasticsearch のアナライザーとして使う構成。 この1年で変わったこと 1. 既存のアナライザーは、ちゃんと進化していた まず押さえたいのは、定番のツールは止まっていない、ということです。 Kuromoji は、Elastic 公式の日本語アナライザーとして引き続き使えます。 Sudachi は、外部プラグイン(Works Applications の elasticsearch-sudachi)として進化を続け、 新しい Elasticsearch にも対応してきました。 Sudachi の辞書は、数か月おきに新語が追加されています。 MeCab や Janome も、Python 前処理用の選択肢として現在も利用されています。 ここで大事なのは、Kuromoji と Sudachi の「立場」が違うことです。 – Kuromoji は Elastic 公式 の Japanese analysis plugin です。 – Sudachi は 外部プラグイン です。 つまり、Sudachi を使うときは、 使っている Elasticsearch のバージョンに対応しているかを必ず確認します。 2. 「Elasticsearch の中で使えるか」で整理すると分かりやすい ここで混乱しやすいのが、「結局どのアナライザーを Elasticsearch で使えるの?」という点です。 実は、全部を同じようには使えません。 3つのグループに分けると分かりやすいです。 Elasticsearch の中で動く(プラグイン):Kuromoji(公式)、Sudachi(外部)。 Elasticsearch の中では動かない:MeCab、Janome、Lindera。 これらは Python などで先にトークン化し、その結果を Elasticsearch に入れて使います。 番外(参考枠):LLM。これはアナライザーとは目的が違います(あとで説明します)。 さらに、実行環境による違いもあります。 Self-Managed(自前で運用): Kuromoji などの公式プラグインは、各ノードに analysis-kuromoji をインストールし、ノードを再起動して使います。外部プラグイン(Sudachi など)も入れられます。 Elastic Cloud Serverless: Kuromoji などの core analysis plugins は最初から利用できます。 一方で、外部プラグインの追加や、独自ファイルのアップロードはできません。 そのため、Sudachi などの外部プラグインや、ファイルとして配置する独自辞書(synonyms / stop words / language analyzer 用 dictionary files など)を前提にした構成は使えません 。 ただし、同義語についてはファイルアップロードではなく、synonyms API を使って管理できます。 3. いちばん大きな変化:形態素解析に頼らない検索 一言でいうと、この1年で「検索のやり方そのもの」に選択肢が増えました。 これまでの日本語検索は、形態素解析で単語に区切り、その単語で探すのが基本でした。 これは今も有効で、なくなりません。 ただ、もう1つの道が実用的になりました。 意味で探す検索(セマンティック検索) です。 仕組みをシンプルにいうと、こうです。 文章を「意味のベクトル(数字の並び)」に変換し、意味が近いものを探します。 このとき、形態素解析で単語に区切る必要はありません。 Elasticsearch では、semantic_text という仕組みと、 EIS(Elastic Inference Service)経由の多言語の埋め込みモデルを使うことで、 日本語でもこの検索がぐっと手軽になりました(例として、EIS では Jina Embeddings v5 系や Microsoft Multilingual E5 Large などの embedding model が利用できます)。 実務で考えると、これは大きいです。 「ロキソニン」と入れなくても、「痛み止め」で関連文書を拾える、というような検索ができます。 本番環境では、既定の inference endpoint に依存せず、利用する埋め込みモデルの inference_id を明示するのが安全です。既定モデルはバージョンや環境によって変わる可能性があり、複数インデックスで異なる embedding model が混在するとランキングに影響するためです。 4. LLM は「アナライザー」ではなく「参考枠」 前回は LLM(当時は GPT-4o)も比較に入れました。 今回も LLM を見ますが、立ち位置をはっきり分けます。 なぜかというと、LLM はインデックス用のトークナイザーとは目的が違うからです。 LLM を、Kuromoji や Sudachi と横並びにして「どれが良いアナライザーか」と比べると、 かえって混乱します。 そこで今回は、LLM を別カテゴリ(参考枠)として、次の点だけ見ます。 専門語を「意味のまとまり」として拾えるか。 検索の補助(キーワード抽出や意味理解)に使えそうか。 ここで、混同しやすい点を1つ整理します。 「LLM によるトークン分割は再現性がない」という声もありますが、必ずしもそうとは限りません。 LLM の tokenizer そのものは、同じ条件なら基本的に同じ結果になります。 バラつくのは、「重要語を抜き出して」とお願いしたときの 生成結果 のほうです。 なので今回は、再現できるように、モデル名・プロンプト・temperature を記録します。 今回の改良点(前回との違い) 前回より良くした点を、正直に宣言します。 詳しくは METHODOLOGY.md を見てください。ここでは要点だけ。 正規化を「入口」でそろえる。 Python で NFKC 正規化を1回だけかけ、同じ入力を全アナライザーに渡します。 機能語の除去を「品詞ベース」に統一する。 手書きのストップワード一覧ではなく、助詞・記号などの品詞でそろえて除きます。 Kuromoji を「正解」と決めつけない。 類似度を1つの数字で出すだけでなく、全アナライザー間の一致や、専門語の扱いも見ます。 実際の検索クエリで動作を確認する。 トークンが似ているかだけでなく、「探したい文書が見つかるか」を見ます。 バージョンを記録する。 Elasticsearch・プラグイン・辞書・ライブラリ・LLM の情報を残し、来年また比べられるようにします。 再検証:実際に比べてみる 使うテキスト 医療系のテキストを2つ使います。 Text 1:ロキソニンの説明文(前回と同じ、短めの文)。なじみのある例として。 Text 2:アセトアミノフェンの説明文(今回のために書き下ろした、少し長い文)。 専門用語・カタカナの薬品名・英語の略語・数値を多く含みます。 比べるアナライザー Elasticsearch の中: Kuromoji(標準、Elasticsearch にもともとあるアナライザー)、Kuromoji_search( kuromoji_tokenizer を mode: search に設定して、この記事用に作ったアナライザーですのでElasticsearch にもともと入っている名前ではありません。)、Sudachi(A / B / C)。 Python で前処理: MeCab、Janome。 (Lindera は Rust 製の新しい選択肢ですが、今回の環境では Python 版を導入できなかったため、 本文での紹介にとどめ、計測には含めていません。) 参考枠(LLM): openai-gpt-oss-120b(EIS 経由)。 結果:トークン数 クリーニング後の、ユニークなトークン数です(実測値)。 Text 1(ロキソニン、約137文字): アナライザー ユニークなトークン数 Kuromoji(標準) 34 Kuromoji(search) 34 Sudachi A 36 Sudachi B 33 Sudachi C 33 MeCab 35 Janome 38 Text 2(アセトアミノフェン、約290文字): アナライザー ユニークなトークン数 Kuromoji(標準) 64 Kuromoji(search) 65 Sudachi A 64 Sudachi B 58 Sudachi C 56 MeCab 70 Janome 72 ここで読み取れることを少しだけ。 細かく分割する MeCab や Janome はトークン数が多めです。 Sudachi は C(大きい単位)になるほどトークン数が減り、複合語をまとめていることが分かります。 ただし「数が多い=良い」ではありません。大事なのは、次に見る専門用語の扱いと検索のヒットです。 結果:専門用語の扱い ここが検証の肝となる、興味深いポイントです。 特定の専門用語が、意図通りにひと塊のトークンとして保持されたかを確認します(○ = 単一語として検出)。 Text 1(ロキソニン): 用語 kuromoji kuromoji_search sudachi_a sudachi_b sudachi_c mecab janome ロキソニン ○ ○ ○ ○ ○ × ○ 解熱鎮痛 × × × × × × × 非ステロイド性抗炎症薬 × × × × × × × NSAIDs ○ ○ ○ ○ ○ ○ ○ 炎症 ○ ○ ○ ○ ○ ○ ○ 発熱 ○ ○ ○ ○ ○ ○ ○ Text 2(アセトアミノフェン): 用語 kuromoji kuromoji_search sudachi_a sudachi_b sudachi_c mecab janome アセトアミノフェン ○ ○ ○ ○ ○ × ○ 中枢神経系 × × × × × × × 解熱鎮痛薬 × × × × ○ × × 非ステロイド性抗炎症薬 × × × × × × × NSAIDs ○ ○ ○ ○ ○ ○ ○ インフルエンザ ○ ○ ○ ○ ○ × ○ 300mg × × × × × × × 肝機能障害 × × × × ○ × × アナフィラキシーショック × × × × × × × スティーブンス・ジョンソン症候群 × × × × × × × ここから読み取れることを、いくつか。 まず、カタカナの薬品名(ロキソニン、アセトアミノフェン)や インフルエンザ は、 ほとんどのアナライザーが1語のまま残しました。 ただし、今回の MeCab の構成だけは残しませんでした。 一点だけ補足します、 これは「MeCab はダメ」という話ではありません。 分割のされ方は、使う辞書(UniDic 系か IPAdic 系かなど)や設定の影響が大きいです。 今回の MeCab + 使用辞書(UniDic)の組み合わせでは、カタカナ語が細かく分割される傾向がありました。 次に、英字の略語 NSAIDs は、すべてのアナライザーが1語で保持しました。 英字のかたまりは、そのまま残りやすいです。 そして、長い複合語(非ステロイド性抗炎症薬、中枢神経系、アナフィラキシーショック、 スティーブンス・ジョンソン症候群)は、すべてのアナライザーが分割しました。 どれも、そのままでは1語になりません。 面白いのは、解熱鎮痛薬 と 肝機能障害 を、Sudachi の C モードだけが1語で残したことです。 C モードは大きい単位でまとめるため、こうした複合語をひとかたまりにできます。 数値+単位の 300mg は、どのアナライザーも1語にしませんでした (今回の元の文が 300〜500mg なので、300・500・mg に分かれます)。 ここで大事なのは、「1語で残る=良い」ではない、ということです。 細かく分割されると、部分一致で拾いやすくなります(再現率が上がる)。 1語でまとまると、完全一致やフレーズ検索でズレにくくなります(精度が上がる)。 つまり、どちらが良いかは「あなたの検索の目的」で決まります。 非ステロイド性抗炎症薬 のような長い語を1語で完全一致させたいなら、 ユーザー辞書への登録や、フレーズ検索の併用を検討します。 結果:アナライザー間の似ている度合い(Jaccard) 次に、アナライザーどうしがどれくらい似ているかを見ます。 前回は「Kuromoji にどれだけ似ているか」だけを見ましたが、 今回は Kuromoji を正解と決めつけず、全ペアを比べます(1.00 が完全一致)。 Text 1(ロキソニン): kuromoji kuromoji_search sudachi_a sudachi_b sudachi_c mecab janome kuromoji 1.00 1.00 0.84 0.63 0.63 0.64 0.85 kuromoji_search 1.00 1.00 0.84 0.63 0.63 0.64 0.85 sudachi_a 0.84 0.84 1.00 0.64 0.64 0.58 0.72 sudachi_b 0.63 0.63 0.64 1.00 1.00 0.42 0.54 sudachi_c 0.63 0.63 0.64 1.00 1.00 0.42 0.54 mecab 0.64 0.64 0.58 0.42 0.42 1.00 0.62 janome 0.85 0.85 0.72 0.54 0.54 0.62 1.00 Text 2(アセトアミノフェン): kuromoji kuromoji_search sudachi_a sudachi_b sudachi_c mecab janome kuromoji 1.00 0.98 0.85 0.67 0.62 0.59 0.74 kuromoji_search 0.98 1.00 0.84 0.69 0.64 0.59 0.76 sudachi_a 0.85 0.84 1.00 0.67 0.64 0.68 0.68 sudachi_b 0.67 0.69 0.67 1.00 0.87 0.44 0.58 sudachi_c 0.62 0.64 0.64 0.87 1.00 0.42 0.54 mecab 0.59 0.59 0.68 0.44 0.42 1.00 0.53 janome 0.74 0.76 0.68 0.58 0.54 0.53 1.00 数字が多いので、読み方をまとめます。 kuromoji と kuromoji_search はほぼ同じでした(1.00〜0.98)。 今回のテキストでは、search モードの差はほとんど出ませんでした。 複合語の固有名詞(例:関西国際空港)が多い文では差が出やすくなります。 (この点は、次の検索クエリの結果で確認します。) kuromoji / janome / sudachi_a は互いに近い(細かく分割するグループ)。 sudachi_b と sudachi_c は互いに近い(大きい単位でまとめるグループ)。 mecab は、他と最も離れていました。 ただしこれは MeCab 固有の特徴というより、今回使用した辞書・設定による切り方の違いです。 この「グループ分け」は、そのまま選び方の指針になります。 細かく拾いたい → kuromoji / sudachi A / janome。 まとめたい → sudachi B / sudachi C。 mecab は独特なので、目的に合うかを個別に確認する。 結果:検索クエリでの動作 最後に、実際の検索で確かめます。 ここが、検索システムとして一番大事なところです。 少数の文書を登録し、クエリごとに「期待する文書が拾えるか」を見ます(○ = ヒット)。 クエリ 期待文書 kuromoji kuromoji_search sudachi_a sudachi_b sudachi_c 空港 doc 1 ○ ○ ○ ○ ○ 関西空港 doc 1 ○ ○ ○ ○ × NSAIDs doc 2 ○ ○ ○ ○ ○ 300mg doc 2 ○ ○ ○ ○ ○ アセトアミノフェン doc 2 ○ ○ ○ ○ ○ (doc 1 は「関西国際空港は大阪府にある国際空港です。」、 doc 2 はアセトアミノフェンの説明文です。) 結果を読み解きます。 まず、ほとんどのクエリは、すべてのアナライザーでヒットしました。 唯一の取りこぼしは、関西空港(略称)を Sudachi C で検索したときだけです。 なぜでしょうか。 Sudachi C は、大きい単位でまとめるため、関西国際空港 を1つのトークンにします。 そのため、略称の「関西空港」とはうまく一致せず、ヒットしませんでした。 これは、まさに精度と再現率のトレードオフです。 大きい単位(Sudachi C)は、正式名称での完全一致に強い。 ただし、略称や部分的なクエリは取りこぼすことがあります。 細かい単位(Kuromoji や Sudachi A)は、部分一致で拾いやすい。 一方で、うれしい結果もあります。 英字の略語 NSAIDs、数値+単位の 300mg、カタカナの専門語 アセトアミノフェン は、 すべてのアナライザーで検索できました。 300mg は1つのトークンではありませんでしたが、300 と mg が別々に索引されるため、検索では拾えます。 ただし、ここは設定に依存します。 今回の query 設定ではヒットしましたが、operator(and / or)、 クエリ側のアナライザー、フィールド側のアナライザーの設定によって結果は変わります。 ここでの学びは、最初に立てた問いそのものです。 トークンが1語できれいに残るかどうかと、検索で見つかるかどうかは、必ずしも一致しません。 最終的に大事なのは「ユーザーが探したい文書が見つかるか」です。 なお、今回の小さな例では、Kuromoji の標準と mode: search で差は出ませんでした。 mode: search の効果は、複合語の固有名詞がもっと多いデータで効いてきます。 参考:LLM は何を「キーワード」として拾ったか 最後に、参考枠の LLM(EIS 経由の gpt-oss-120b)を見ます。 くり返しになりますが、これはアナライザーの比較ではありません。 「意味のまとまりとして、専門語を拾えるか」を見るための参考です。 抽出されたキーワードは次の通りです。 Text 1(ロキソニン): ロキソニン錠 / ロキソニン / 非ステロイド性抗炎症薬 / NSAIDs / 解熱鎮痛作用 / 関節リウマチ / 変形性関節症 / 腰痛症 / 肩こり / 歯痛 / 手術後 / 外傷後 / 炎症 / 痛み / 風邪 / 熱 Text 2(アセトアミノフェン): アセトアミノフェン / 中枢神経系 / 解熱鎮痛薬 / 非ステロイド性抗炎症薬 / NSAIDs / 抗炎症作用 / 一般用医薬品 / 頭痛 / 歯痛 / 月経痛 / 関節痛 / インフルエンザ / 風邪 / 発熱 / 成人 / 1回300〜500mg / 1日3回 / 経口投与 / 肝機能障害 / 高齢者 / 用量調整 / 重篤な副作用 / 肝障害 / アナフィラキシーショック / スティーブンス・ジョンソン症候群 ここが、形態素解析との大きな違いです。 形態素解析がすべて分割してしまった長い専門語を、LLM は1つの意味のまとまりとして拾いました。 たとえば、非ステロイド性抗炎症薬、中枢神経系、アナフィラキシーショック、 スティーブンス・ジョンソン症候群 などです。 さらに、1回300〜500mg や 1日3回 のような、用量を表す「意味のかたまり」も拾っています。 一言でいうと、LLM は「索引用の最小単位」ではなく「意味のまとまり」を取り出します。 このため、LLM が向いているのは次のような場面です。 クエリの意図を理解する(クエリ理解)。 文章から重要語を抜き出す(キーワード抽出)。 意味で探す検索(セマンティック検索)の補助。 逆に、インデックスのトークン化には向きません。 理由は3つあります。 生成結果は毎回まったく同じとは限らない(再現性が低い)。 大量の文書をすべて LLM に通すのはコストが高い。 そもそも目的が、転置インデックス用の最小トークンを作ることではない。 今回使用した LLM の設定(再現性のため): 使用モデル:openai-gpt-oss-120b 実行環境:EIS(Elastic Inference Service)経由 temperature:0 プロンプト:付録(GitHub のリポジトリ)に掲載 アナライザーの選び方ガイド ここまでをふまえて、用途別の選び方をまとめます。 「結局どれを使えばいいの?」への答えです。 部分一致や再現率を重視したい(広く拾いたい) → Kuromoji、または Sudachi A(細かく分割)。 完全一致・フレーズ検索を重視したい(複合語をまとめたい) → Sudachi C。 バランスを取りたい → Sudachi B。 新語・製品名・固有名詞が多い → 辞書更新の速い Sudachi、または Kuromoji に辞書を足す構成。 意味で探したい(言い換えにも強くしたい) → 形態素解析ではなく、semantic_text + 多言語埋め込み(EIS)。 Elasticsearch の中だけで完結させたい → 実質、Kuromoji か Sudachi(ほかは Python 前処理が必要)。 LLM → インデックスのトークン化には向きません。 クエリ理解やキーワード抽出など、検索の「補助」に使うのが向いています。 まとめ 最後に、覚えておきたいことを1つだけ。 「いちばん良いアナライザー」は存在しません。用途で決まります。 この1年での大きな変化は、選択肢が増えたことです。 形態素解析は今も主役の1つですが、意味で探すセマンティック検索という道も、 日本語で手軽に使えるようになりました。 次の一歩としては、自分の検索でよく使うクエリをいくつか決めて、 この記事の方法で実際に試してみるのがおすすめです。 results/ に数値が出るので、自分のデータで「どれが合うか」を確かめられます。 ※本記事の Python コードと検証環境は、Claude Codeを使って作成しました。 Links Kuromoji(analysis-kuromoji)プラグイン kuromoji analyzer kuromoji_tokenizer semantic_text フィールド semantic_text による意味検索 Elastic Inference Service(EIS) EIS の対応モデル (gpt-oss-120b など) 自前クラスタから EIS を使う (Cloud Connect) カスタムプラグイン/バンドルのアップロード (Serverless の制約の出典) Hosted と Serverless の違い Synonyms API (Serverless で同義語を使う方法) The post Kuromoji・Sudachi・MeCab・Janome・LLM・semantic search の使い分け【2026】 first appeared on Elastic Portal .
動画
該当するコンテンツが見つかりませんでした









