Elastic Inference Service (EIS) を使った「ベクトル検索」および「生成AIによる回答(RAG)」(実践編)

BLOG

Elastic Inference Service (EIS) を使った「ベクトル検索」と「生成AIによる回答(RAG)」について、全2回にわたって解説します。

第2回となる今回は「実践編」として、EIS を通じてモデルを呼び出し、「ベクトル検索」と「生成AIによる回答(RAG)」を実際に動かしてみます。


前提条件

前回の「準備編」での設定が完了していることを前提とします。

テストデータ、各種スクリプト

このサンプルで使用するテストデータおよび各種スクリプトは、下記の 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...

検索データのアップロード

今回のデモデータには、夏目漱石の『吾輩は猫である』を使用します。

青空文庫のデータを元に、ルビを削除して NDJSON 形式に加工したファイルを用意しました。

インデックスとパイプラインの作成

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

検索結果:今回は、欲しかったドキュメントのキーワード検索での順位が低かったために、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

注目してほしいのは、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

注目してほしいのは、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 インデックスは不要となります。

必要なければ、削除してかまいません。

参考リンク

まとめ

Self-Managed の Elasticsearch であっても、Elastic Inference Service (EIS)を活用することで、 重い推論モデルを自前で管理・運用することなく、ベクトル検索やセマンティックリランク、生成AIによる回答を極めてシンプルに実装できました。

ぜひ、皆さんの環境でも EIS を活用した高度な検索体験を試してみてください。