TECH PLAY

形態素解析

イベント

該当するコンテンツが見つかりませんでした

マガジン

技術ブログ

日本語の文章を単語単位で解析する「形態素解析」について、JavaScriptライブラリ「Kuromoji」を使った方法を解説。サンプルコードを用いて、特定の品詞(名詞)を抽出し、単語の出現頻度を集計する具体的な手順を紹介します。
Elastic Inference Service (EIS) を使った「ベクトル検索」と「生成AIによる回答(RAG)」について、全2回にわたって解説します。 第2回となる今回は「実践編」として、EIS を通じてモデルを呼び出し、「ベクトル検索」と「生成AIによる回答(RAG)」を実際に動かしてみます。 目次 前提条件 テストデータ、各種スクリプト 検索データのアップロード インデックスとパイプラインの作成 1. インデックスの作成 2. マッピングの定義 3. エイリアスの作成 4. インジェストパイプラインの作成 5. データの Reindex(ベクトル化の実行) 各種検索 キーワード検索(全文検索) ベクトル検索 (kNN) Reciprocal Rank Fusion (RRF) によるハイブリッド検索 セマンティックリランク 生成AIによる回答 技術的な補足 モデル名の指定 RRF とセマンティックリランクの役割 waganeko_tmp インデックス 参考リンク まとめ 前提条件 前回の「 準備編 」での設定が完了していることを前提とします。 テストデータ、各種スクリプト このサンプルで使用するテストデータおよび各種スクリプトは、下記の GitHub リポジトリで公開しています。 elastic-blogs/2026-03-eis at main · SIOS-Technology-Inc/elastic-blogs A sample code for blogs about Elastic. Contribute to SIOS-Technology-Inc/elastic-blogs development by creating an accoun... github.com 検索データのアップロード 今回のデモデータには、夏目漱石の『吾輩は猫である』を使用します。 青空文庫のデータ を元に、ルビを削除して NDJSON 形式に加工したファイルを用意しました。 データファイル: no_ruby_wagahai_wa_neko_dearu.ndjson アップロード手順:  README.md  を参照し、Self-Managed の Elasticsearch 上の waganeko_tmp インデックスへアップロードしてください。 インデックスとパイプラインの作成 1. インデックスの作成 まずは、形態素解析(icu/kuromoji)の設定を施した waganeko_2026_03 インデックスを作成します。 a2_create_index.md のスクリプトを Dev Tools の Console から実行してください。 2. マッピングの定義 a3_create_index_mapping.md を Self-Managed の Dev Tool の Console から実行し、waganeko_2026_03 インデックスへフィールドを作成します。 「吾輩は猫である」の本文を content フィールドに、本文から生成される密ベクトルを content_embedding フィールドへ格納するようにしています。 密ベクトルの type には、bbq_disk を指定しています。今回のデータは少量なので bbq_disk を使わなくてもよいのですが、bbq_disk の検証も兼ねて bbq_disk を使用しています。 3. エイリアスの作成 運用の利便性を高めるため、waganeko_2026_03 に対して waganeko というエイリアスを付与します。 a4_create_alias.md を実行してください。 4. インジェストパイプラインの作成 ここが EIS の真骨頂です。 a5_create_ingest_pipeline.md を実行します。 パイプライン内で指定している .jina-embeddings-v5-text-nano モデルは、Self-Managed 側にはインストールされていません。 EIS を経由することで、外部モデルをあたかもローカルモデルのように利用できます。 このパイプラインをデータ取り込み時に通過させることで、content フィールドの内容に応じた密ベクトルを生成し、content_embedding フィールドへ格納できるようになります。 5. データの Reindex(ベクトル化の実行) a6_reindex.md を実行し、waganeko_tmp から waganeko へデータをコピーします。 この際、前述のパイプラインにより自動的にベクトル化が行われます。 各種検索 ここからは、ES|QL を用いて異なる検索手法を試していきます。 キーワード検索(全文検索) まずは、従来の全部検索です。スクリプトは下記にも掲載しています。 a7_keyword_search.md POST /_query { "query": """ FROM waganeko METADATA _score, _id, _index | WHERE MATCH(content, ?query) | KEEP chunk_no, content, _score | SORT _score DESC | LIMIT 20 """, "params": [ { "query": "吾輩が生まれた場所は?" } ] } 検索結果:「場所」という単語に引っ張られ、必ずしも意図した回答(冒頭の一節)が上位に来るとは限りません。 ... "values": [ [ 1217, "しばらくは爺さんの方へ気を取られて他の化物の事は全く忘れていたのみならず、苦しそうにすくんでいた主人さえ記憶の中から消え去った時突然流しと板の間の中間で大きな声を出すものがある。見ると紛れもなき苦沙弥先生である。主人の声の図抜けて大いなるのと、その濁って聴き苦しいのは今日に始まった事ではないが場所が場所だけに吾輩は少からず驚ろいた。", 8.456548690795898 ], [ 213, "「なるほど仲居は茶屋に隷属するもので、遣手は娼家に起臥する者ですね。次に見番と云うのは人間ですかまたは一定の場所を指すのですか、もし人間とすれば男ですか女ですか」「見番は何でも男の人間だと思います」「何を司どっているんですかな」「さあそこまではまだ調べが届いておりません。その内調べて見ましょう」これで懸合をやった日には頓珍漢なものが出来るだろうと吾輩は主人の顔をちょっと見上げた。", 6.545511245727539 ], ... ] ... ベクトル検索 (kNN) 次にベクトル検索(kNN)を行ってみます。スクリプトは下記にも掲載しています。 a8_vector_search.md POST /_query { "query": """ FROM waganeko METADATA _score, _id, _index | WHERE KNN(content_embedding, TEXT_EMBEDDING(?query, ".jina-embeddings-v5-text-nano")) | KEEP chunk_no, content, _score | SORT _score DESC | LIMIT 20 """, "params": [ { "query": "吾輩が生まれた場所は?" } ] } クエリーから密ベクトルを生成するモデルには、”.jina-embeddings-v5-text-nano” を指定します。 検索結果:「どこで生れたかとんと見当がつかぬ…」という有名な冒頭部分が 1 位にランクインしました。 ... "values": [ [ 2, "どこで生れたかとんと見当がつかぬ。何でも薄暗いじめじめした所でニャーニャー泣いていた事だけは記憶している。吾輩はここで始めて人間というものを見た。しかもあとで聞くとそれは書生という人間中で一番獰悪な種族であったそうだ。この書生というのは時々我々を捕えて煮て食うという話である。しかしその当時は何という考もなかったから別段恐しいとも思わなかった。", 0.7278214693069458 ], [ 1039, "ちょうど三日目の暁方に、隣の家で赤ん坊がおぎゃあと泣いた声を聞いて、うんそうだと豁然大悟して、それから早速長い髪を切って男の着物をきて Hierophilus の講義をききに行った。首尾よく講義をきき終せて、もう大丈夫と云うところでもって、いよいよ産婆を開業した。ところが、奥さん流行りましたね。あちらでもおぎゃあと生れるこちらでもおぎゃあと生れる。", 0.7182090282440186 ], ... ] ... Reciprocal Rank Fusion (RRF) によるハイブリッド検索 さきほどのキーワード検索結果とベクトル検索結果を RRF により融合してみます。スクリプトは下記にも掲載しています。 a9_rrf.md POST /_query { "query": """ FROM waganeko METADATA _score, _id, _index | FORK (WHERE KNN(content_embedding, TEXT_EMBEDDING(?query, ".jina-embeddings-v5-text-nano")) | SORT _score DESC | LIMIT 20) (WHERE MATCH(content, ?query) | SORT _score DESC | LIMIT 20) | DROP content_embedding | FUSE | KEEP chunk_no, content, _score | SORT _score DESC | LIMIT 10 """, "params": [ { "query": "吾輩が生まれた場所は?" } ] } ES|QL の FUSE を使って RRF によるランキング融合を行っています。 ES|QL FUSE command | Elasticsearch Reference www.elastic.co 検索結果:今回は、欲しかったドキュメントのキーワード検索での順位が低かったために、RRF での結果では欲しかったドキュメントが第2位になっています。 ... "values": [ [ 1462, "今日何人あばたに出逢って、その主は男か女か、その場所は小川町の勧工場であるか、上野の公園であるか、ことごとく彼の日記につけ込んである。彼はあばたに関する智識においては決して誰にも譲るまいと確信している。せんだってある洋行帰りの友人が来た折なぞは、「君西洋人にはあばたがあるかな」と聞いたくらいだ。", 0.028958333333333336 ], [ 2, "どこで生れたかとんと見当がつかぬ。何でも薄暗いじめじめした所でニャーニャー泣いていた事だけは記憶している。吾輩はここで始めて人間というものを見た。しかもあとで聞くとそれは書生という人間中で一番獰悪な種族であったそうだ。この書生というのは時々我々を捕えて煮て食うという話である。しかしその当時は何という考もなかったから別段恐しいとも思わなかった。", 0.01639344262295082 ], ... ] ... セマンティックリランク さきほどの RRF により候補を絞り込んだ後に、セマンティックリランクを行ってみます。 セマンティックリランクに利用するモデルは、”.jina-reranker-v3″ です。 スクリプトは下記にも掲載しています。 a10_rrf_semantic_rerank.md POST /_query { "query": """ FROM waganeko METADATA _score, _id, _index | FORK (WHERE MATCH(content, ?query) | SORT _score DESC | LIMIT 20) (WHERE KNN(content_embedding, TEXT_EMBEDDING(?query, ".jina-embeddings-v5-text-nano")) | SORT _score DESC | LIMIT 20) | DROP content_embedding | FUSE | SORT _score DESC | LIMIT 10 | RERANK ?query ON content WITH { "inference_id" : ".jina-reranker-v3" } | KEEP chunk_no, content, _score | SORT _score DESC """, "params": [ { "query": "吾輩が生まれた場所は?" } ] } ES|QL の RERANK コマンドを使ってセマンティックリランクを行います。 ES|QL RERANK command | Elasticsearch Reference www.elastic.co 注目してほしいのは、Self-Managed の Elasticsearch には .jina-reranker-v3 をインストールしていないにもかかわらず、利用できる点です。 検索結果:欲しかったドキュメントが第1位になりました。 ... "values": [ [ 2, "どこで生れたかとんと見当がつかぬ。何でも薄暗いじめじめした所でニャーニャー泣いていた事だけは記憶している。吾輩はここで始めて人間というものを見た。しかもあとで聞くとそれは書生という人間中で一番獰悪な種族であったそうだ。この書生というのは時々我々を捕えて煮て食うという話である。しかしその当時は何という考もなかったから別段恐しいとも思わなかった。", 0.33165714144706726 ], [ 1217, "しばらくは爺さんの方へ気を取られて他の化物の事は全く忘れていたのみならず、苦しそうにすくんでいた主人さえ記憶の中から消え去った時突然流しと板の間の中間で大きな声を出すものがある。見ると紛れもなき苦沙弥先生である。主人の声の図抜けて大いなるのと、その濁って聴き苦しいのは今日に始まった事ではないが場所が場所だけに吾輩は少からず驚ろいた。", 0.08227790892124176 ], ... ] ... 生成AIによる回答 最後に、検索結果のコンテキストを LLM に渡し、自然言語で回答を生成させます。 やや乱暴ですが、セマンティックリランクの結果の第1位のドキュメントの内容を元にして、生成AI に質問に回答するよう依頼してみます。 回答に使用するモデルは、.openai-gpt-oss-120b-completion です。 スクリプトは下記にも掲載しています。 a11_completion.md POST /_query { "query": """ FROM waganeko METADATA _score, _id, _index | FORK (WHERE MATCH(content, ?query) | SORT _score DESC | LIMIT 20) (WHERE KNN(content_embedding, TEXT_EMBEDDING(?query, ".jina-embeddings-v5-text-nano")) | SORT _score DESC | LIMIT 20) | DROP content_embedding | FUSE | SORT _score DESC | LIMIT 10 | RERANK ?query ON content WITH { "inference_id" : ".jina-reranker-v3" } | SORT _score DESC | KEEP content | LIMIT 1 | COMPLETION CONCAT("Answer in Japanese the following question ", ?query, " based on:\n", content) WITH { "inference_id" : ".openai-gpt-oss-120b-completion" } """, "params": [ { "query": "吾輩が生まれた場所は?" } ] } ES|QL の COMPLETION コマンドを利用しています。 ES|QL COMPLETION command | Elasticsearch Reference www.elastic.co 注目してほしいのは、Self-Managed の Elasticsearch には .openai-gpt-oss-120b-completion をインストールしていないにもかかわらず、利用できる点です。 回答結果の例 吾輩は「薄暗く湿った所」、すなわち暗くてじめじめした場所で生まれました。 (「どこで生れたかとんと見当がつかぬ。何でも薄暗いじめじめした所でニャーニャー泣いていた事だけは記憶している。」という記述に基づく。) 合っているようです。 技術的な補足 モデル名の指定 EIS で利用するモデル名は、Elastic Cloud の Relevance > Inference endpoints 画面に表示される Endpoint を使用します。 RRF とセマンティックリランクの役割 本サンプルでは、RRF とセマンティックリランクを併用しています。 Reciprocal Rank Fusion (RRF): キーワードとベクトルの異なる検索手法を統合し、候補を漏れなく抽出する「絞り込み」のフェーズ。 セマンティックリランク: 絞り込まれた上位ドキュメントに対し、LLM 的な文脈理解で「真の回答」を最上位に持ってくる「仕上げ」のフェーズ。 waganeko_tmp インデックス waganeko_tmp インデックスの内容を waganeko インデックスへ reindex した後は、waganeko_tmp インデックスは不要となります。 必要なければ、削除してかまいません。 参考リンク Cloud Connect を利用した場合の追加費用については、下記を参照してください。 https://cloud.elastic.co/cloud-pricing-table?productType=cloud_connect まとめ Self-Managed の Elasticsearch であっても、Elastic Inference Service (EIS)を活用することで、 重い推論モデルを自前で管理・運用することなく、ベクトル検索やセマンティックリランク、生成AIによる回答を極めてシンプルに実装できました。 ぜひ、皆さんの環境でも EIS を活用した高度な検索体験を試してみてください。 The post Elastic Inference Service (EIS) を使った「ベクトル検索」および「生成AIによる回答(RAG)」(実践編) first appeared on Elastic Portal .
株式会社リクルートは、日本国内で HR・販促事業を行う事業会社です。リクルートでは、満足度No1(*1)を誇る飲食店予約・グルメ情報サイト『ホットペッパーグルメ』を運営しています。 『ホットペッパーグルメ』では、ユーザーが飲食店を検索する際の「0件ヒット」問題を解決するため、 Amazon OpenSearch Service (以下、OpenSearch Service)を採用し、Hybrid Search 機能を実現しました。約6ヶ月の取り組みにより、検索における0件ヒットを90%削減し、検索経由の予約数を10%向上させることに成功しています。 本ブログでは、『ホットペッパーグルメ』における検索改善の取り組みについて、チャレンジから導入効果までをご紹介します。 課題 『ホットペッパーグルメ』では、ユーザーが飲食店を探そうと検索しても、検索結果が0件になるケースがありました。行きたいお店があるのに見つけられず、ユーザーと飲食店のマッチングの機会を逃してしまう状況です。これはユーザー体験の低下だけでなく、検索から予約への転換機会の損失にもつながる重要なビジネス課題でした。 0件ヒットが発生する背景として従来の検索技術である Lexical Search には限界があり、ユーザーの意図を正しく理解できないケースがありました。 Lexical Search について : Lexical Search(字句検索)は、入力されたキーワードと完全に一致する文字列を検索する手法です。しかし入力ミスに弱いという課題がありました。ユーザーが「寿司」と入力しようとして「寿し」や「すそ」と入力してしまうと、検索結果を取得できません。また、日本語はスペースで単語を区切らない言語のため、セグメンテーションエラーが発生しやすいという日本語特有の課題もありました。 Vector Search の限界 : Vector Search(ベクトル検索)は、テキストを数値ベクトルに変換し、意味的な類似度で検索する手法です。タイポや言い換えに強いという特徴がありますが、固有名詞の検索で精度が低下するという課題がありました。店舗名のような固有名詞を検索する際、意味的に近い別の店舗が上位に表示されてしまいます。また、位置情報などのノイズにより、検索意図とは異なる結果が返される意味的なずれも発生していました。 検索手法 Lexical Search Vector Search 完全一致キーワード ○ × タイポ耐性 × ○ 言い換え対応 × ○ 固有名詞の精度 ○ × 日本語セグメンテーション × ○ 一般的に、ユーザーは検索結果の上位に高い関心があるため(*2)、 特にTop-1の検索結果を改善する方針 としました。 OpenSearch Service を活用した Hybrid Search の実現 これらの課題を解決するため、『ホットペッパーグルメ』では Lexical Search と Vector Search を組み合わせた Hybrid Search を採用しました。またそれを支える基盤として、OpenSearch Service を採用しました。 OpenSearch Service を選定した最大の理由は、Full-text Search と Vector Search を単一のサービスで実現できる Hybrid Search 機能の統合です。また、フルマネージド型サービスとしてインフラ管理の負荷を軽減できる開発・運用の容易さも重要な選定ポイントでした。さらに、OpenSearch Ingestion による Managed ETL や言語別テキスト解析プラグインなど AWS がサポートするプラグインが充実しており、豊富な周辺機能を活用できます。Blue/Green デプロイによる無停止でのスケーリングも、運用の安心感を確保する上で大きなメリットでした。 アーキテクチャと実装 Two-tower Model Architecture 『ホットペッパーグルメ』では、検索クエリと店舗情報をそれぞれ別のエンコーダーで処理する Two-tower Model Architecture を採用しました。 Two-tower Model は、Query Encoder と Document Encoder という2つの独立したエンコーダーで構成されています。Query Encoder はユーザーが入力した検索クエリ(例:「東京 寿司」)をベクトルに変換します。一方、Document Encoder は店舗情報をベクトルに変換します。店舗情報には、店舗名、住所、メニュー、説明文、キャッチコピー、レビューなど、検索に関連する情報が含まれます。埋め込みのスコープについても検証を行いました。店舗名や住所といった基本情報のみを使用するパターンと、メニューや説明文、キャッチコピーといったコンテンツフィールドを含めるパターンを比較し、検索精度への影響を評価しています。 モデルの学習には対照学習を採用しました。学習データとして、ユーザーの検索ログと LLM で生成した合成ペアを組み合わせて使用しています。ユーザーログからは実際の検索行動に基づくクエリと店舗のペアを抽出し、LLM 生成の合成ペアにより学習データのカバレッジを拡大しました。ベースモデルには日本語 BERT を採用し、『ホットペッパーグルメ』のドメインに特化したファインチューニングを実施しています。汎用的な埋め込みモデルと比較して、店舗名のようなドメイン固有の用語に対する検索精度が向上しました。埋め込みの次元数は512次元を採用しています。 日本語テキストの形態素解析には Sudachi を使用し、split_mode: A(最小単位での分割)を設定しています。これにより、日本語特有の複合語や固有名詞を適切にトークン化し、検索精度の向上に寄与しています。 導入プロセス Built-in Hybrid Search(第1フェーズ) 最初のステップとして、AOS の標準機能である Built-in Hybrid Search を導入しました。Built-in Hybrid Search は実装が容易であり、検索基盤の構築よりもモデル改善に集中できるというメリットがありました。 Built-in Hybrid Search では、OpenSearch が Lexical と Vector の検索結果を取得し、それぞれのスコアを正規化した後、単一のスコアにマージしてから Reranking を行います。この仕組みにより、短期間で Hybrid Search を導入し、効果を検証することができました。 一方で、運用を通じて課題も見えてきました。一度スコアがマージされると、スコアの差異が何に起因するのかを把握できなくなります。Lexical Score が高く Vector Score が低い場合、それがタイポによるものなのか、言い換えによるものなのか、あるいはレアな固有名詞によるものなのか、判断できません。どちらのシグナルを重視すべきかは検索意図に依存するため、固定のマージ重みでは対応しきれないケースがありました。この学びから、Reranker には統合されたスコアではなく、Lexical Score と Vector Score を個別に渡す必要があるという結論に至りました。 Custom Hybrid Search(第2フェーズ) Built-in Hybrid Search の課題を解決するため、Custom Hybrid Search を開発しました。 Custom Hybrid Search では、Lexical Score と Vector Score を別々に保持し、マージ前の個別スコアを Reranker に渡すことで、より精緻なランキングが可能になりました。また、ユーザーログや検索意図などの追加シグナルを Reranking に活用しています。スコアの統合には LightGBM による適応的なスコア統合とランキングを採用し、機械学習モデルによる動的なスコア統合を実現しています。アーキテクチャは Planner → Executor → Merger & Reranker の構成となっており、検索意図に応じた柔軟なランキングを実現しています。Custom Hybrid Search の導入により、Top-1 検索精度が最大2倍改善しました。 段階的なリリース戦略 新しい検索システムの導入にあたり、A/B Test Router を用いたトラフィック制御を実施しました。当初は新規システムに10%、既存システムに90%のトラフィックを振り分け、この比率を段階的に調整しながら A/B テストにより効果を検証しました。A slot / B slot による並行検証を行い、新システムの安定性と効果を確認した上で、トラフィック比率を徐々に増加させていきました。 成果 ビジネス成果 : Custom Hybrid Search を本番環境に導入した結果、検索経由の予約数が10%改善し、0件ヒット検索が90%削減されました。これらの改善により、ユーザーと飲食店のマッチング機会が大幅に増加し、サービス全体の価値向上につながっています。 技術的成果 : 運用面でも多くのメリットが得られました。Blue/Green デプロイによる無停止でのスケーリングで運用負荷が軽減され、Plugins や OpenSearch Ingestion を活用することでランキングモデルの改善サイクルを加速できています。フルマネージド型サービスとしてインフラ管理の負荷が軽減されたことで、運用の安心感も確保できました。 まとめ 本ブログでは、『ホットペッパーグルメ』における OpenSearch Service で実現された Hybrid Search を活用した検索改善の取り組みをご紹介しました。 Lexical Search と Vector Search を組み合わせた Hybrid Search により、それぞれの検索手法の強みを活かしながら弱点を補完し、0件ヒット問題を大幅に改善することができました。また、Built-in Hybrid Search から Custom Hybrid Search への段階的な進化により、Top-1 検索精度を最大2倍改善し、検索経由の予約数10%向上という成果を達成しています。 OpenSearch Service の採用により、Lexical Search と Vector Search を組み合わせた Custom Hybrid Search の構築、開発・運用の容易さ、Blue/Green Deployment による安定した運用を実現できました。 こうした検索基盤の進化は、新たなユーザー体験の創出にもつながっています。2026年1月22日にリリースした 「席押さえ」機能 は、現在地周辺のお店をマップから探し、「今すぐ席を押さえる」ボタンをタップするだけで、予約なしで席を確保できるアプリ限定機能です。リアルタイムな「空席情報」と「位置情報」を地図 UI 上でマッチングするこの検索体験も、OpenSearch Service で実現しています。 Reference (*1)2025年6月時点 株式会社東京商工リサーチ調べ (*2) Google検索のClick Trough Rateは、Top1が27.6% backlinko 社調べ AWS re:Invent 2025 – Build Advanced Search with Vector, Hybrid, and AI Techniques (ANT314) Amazon OpenSearch Service BlackBelt 『ホットペッパーグルメ』「席押さえ」機能を全国で提供開始

動画

該当するコンテンツが見つかりませんでした

書籍