Elasticsearchでの外部のEmbed Model(Cohere)を使った密ベクトル検索

BLOG

はじめに

今回は、Elasticsearchでの密ベクトル検索において、外部のEmbed Modelを利用する方法について解説します。

これまで、ホワイトペーパー「Elasticsearchを使った簡易RAGアプリケーションの作成」やブログ記事「Elasticsearchでのベクトル検索の準備」で密ベクトル検索をご紹介してきました。これらはElasticsearchが同梱している .multilingual-e5-small というEmbed Modelを利用していました。

本記事では、Elasticsearchの外部にあるEmbed Modelを使って密ベクトル検索を行う方法を詳しく説明します。

対象読者

Elasticsearchの初級者~中級者

環境

  • Elasticsearch 8.18以上、かつ Enterprise License(筆者はElasticsearch 9.0.3 Enterprise Licenseで動作確認しました。)
  • Cohere embed-multilingual-light-v3.0 (Elasticsearchの /_inference/text_embedding/ が対応しているEmbed Modelであれば動作します。筆者は左記のモデルで動作確認しました。)

なお、本ブログで紹介する /_inference/text_embedding/ を使った密ベクトル生成は、Basic Licenseおよび Platinum License では動作しません。Basic License, Platinum License をご利用の場合は、ベクトル変換処理のプログラムを作成して、それらを呼び出すなどの対応が必要です。(*脚注1)1

また、本ブログではフィールドタイプに semantic_text を指定しています。semantic_text はv8.18でGAとなりました。

※本記事では密ベクトル検索に焦点を当てるため、形態素解析や、形態素解析を利用したハイブリッド検索については省略します。

Elasticsearch同梱モデル vs 外部モデル

Elasticsearchが同梱しているEmbed Modelを利用する方法と、外部のEmbed Modelを利用する方法を比較してみましょう。

Elasticsearch同梱の Model を利用する場合

Elasticsearch同梱の Embed Model を利用する場合のドキュメント登録時の概略図を以下に示します。

Elasticsearchの外部のEmbed Modelを利用する場合

Elasticsearch の外部の Embed Model を利用する場合のドキュメント登録時の概略図を以下に示します。

比較表

項目Elasticsearch同梱のModelを利用Elasticsearchの外部のModelを利用
手軽さ○ Elasticsearchのみ契約すればよい× 別途、Embed Modelのサービス契約が必要
モデルの種類× 少ない○ 多い
費用Elasticsearchの費用はかかるが、それ以外は不要。外部のEmbed Modelの利用料は増えるが、Elasticsearchの費用を抑えられる。
頻繁に密ベクトル生成処理が呼ばれる場合、外部モデルを利用するよりも安くなる可能性あり。密ベクトル生成処理が一定回数以下の場合、こちらが安くなる可能性あり。

外部のEmbed Modelの利用料金はモデルによって異なるため一概には言えませんが、モデルによっては低価格で利用できるものもあり、割安で導入できるケースもあります。

Elasticsearchで密ベクトル生成に利用可能なサービス

Elasticsearchの/_inference/text_embedding/を使って文字列から密ベクトルを生成する際に利用可能なサービス名の一覧を以下に挙げます。

Elasticsearch公式ドキュメントの「Create … inference endpoint」のうち、task_type = text_embeddingが密ベクトル生成用のサービスに該当します。

サービス名の一覧

  • Alibaba Cloud AI Search
  • Amazon Bedrock
  • Azure AI Studio
  • Azure OpenAI
  • Cohere
  • Elasticsearch
  • Google AI Studio
  • Google Vertex AI
  • Hugging Face
  • JinaAI
  • Mistral
  • OpenAI
  • VoyageAI
  • Watsonx inference integration

※V9用の公式ドキュメントでは1か所にまとめられていないようです。なお、V8用の公式ドキュメントでは、こちらのようにリストアップされています。

※/_inference/text_embedding/を使わず、独自にプログラムを作成して密ベクトル生成処理を組み込めば、上記以外のモデルも利用可能です(その場合は、フィールドタイプに smantic_text 型ではなく、dense_vector 型を使用します)。

今回は、この中から Cohere を使用します。モデルは embed-multilingual-light-v3.0(次元数:384)を使います。

準備

Cohere API Key の取得

Cohereにサインイン後、API Keyを取得してください。

(本ブログではCohereのembed-multilingual-light-v3.0を取り上げていますが、他のモデルでも問題ありません。)

Machine Learning インスタンス

Elasticsearchに同梱しているEmbed Model(.multilingual-e5-smallなど)を利用する場合はElasticsearchのMachine Learningインスタンスが必要ですが、外部のEmbed Modelを利用して密ベクトルを生成する場合は、Machine Learningインスタンスは不要です。

(取得したログに対して異常値検出を行うなど、ベクトル生成以外でMachine Learningの機能を利用する場合は Machine Learning インスタンスが必要となりますが、今回はMachine Learningの機能を使用しないため不要です。)

/_inference/text_embedding/用エンドポイントの作成

https://www.elastic.co/docs/api/doc/elasticsearch/v9/operation/operation-inference-put-cohere を参考に、Cohere用のtext_embeddingエンドポイントを作成します。

以下のリクエストをKibanaのDevToolsのConsoleから発行します。

PUT /_inference/text_embedding/my_cohere_emb_light_v3_float
{
    "service": "cohere",
    "service_settings": {
        "api_key": "取得した Cohere の Api Key",
        "model_id": "embed-multilingual-light-v3.0",
        "embedding_type": "float"
    }
}

service_settings内に記載するパラメーターは、サービスごとに異なります。

Cohereの場合は、embedding_typeなどを指定できます。今回は”float”とします。

ここで作成したinference_idは、後ほど参照します。

_inferenceのtext_embeddingエンドポイントの作成に成功すると、次のようなレスポンスが返ってきます。

{
  "inference_id": "my_cohere_emb_light_v3_float",
  "task_type": "text_embedding",
  "service": "cohere",
  "service_settings": {
    "similarity": "cosine",
    "dimensions": 384,
    "model_id": "embed-multilingual-light-v3.0",
    "rate_limit": {
      "requests_per_minute": 10000
    },
    "embedding_type": "float"
  },
  "chunking_settings": {
    "strategy": "sentence",
    "max_chunk_size": 250,
    "sentence_overlap": 1
  }
}

インデックスの作成

今回登録するドキュメントの内容は、ホワイトペーパーで使用した「柿之助」(青空文庫より入手した「桃太郎」を改変したもの)とします。

下記のリクエストを Kibana の DevTools の Console から発行します。

PUT /kakinosuke_cohere_emb3_light_float/
{
  "settings": {
    "index": {
      "number_of_shards": 1,
      "number_of_replicas": 1,
      "refresh_interval": "3600s"
    }
  }
}

今回は、形態素解析などの設定は省略しています。

マッピングの作成

下記のリクエストを Kibana の DevTools の Console から発行します。

PUT /kakinosuke_cohere_emb3_light_float/_mappings
{
  "dynamic": false,
  "properties": {
    "chunk_no": {
      "type": "integer"
    },
    "content": {
      "type": "text",
      "fields": {
        "text_embedding": {
          "type": "semantic_text",
          "inference_id": "my_cohere_emb_light_v3_float"
        }
      }
    }
  }
}

今回は、形態素解析などの設定は省略しています。

作成するフィールドは3つです。

フィールド名フィールドタイプ説明
chunk_nointegerチャンク番号
contenttext内容(本文)
content.text_embeddingsemantic_text密ベクトル(384次元, float)

content.text_embeddingにCohereで生成した密ベクトルを格納するよう、inference_idに先ほど作成した”my_cohere_emb_light_v3_float”を指定します。

ドキュメントの登録

実際の検索サービスであれば、登録処理を別途作成する必要がありますが、今回は事前に登録用のリクエストを用意し、それらを発行するだけとします。

登録する内容は、ホワイトペーパーで利用した「柿之助」(青空文庫より入手した「桃太郎」を改変したもの)を利用します。

ただし、今回は説明を簡略化するため、オーバーラップなしで手動でチャンキングしています(繰り返しになりますが、あくまでサンプルアプリケーションのため簡略化しています)。

ドキュメントの登録リクエスト:

※Trial Licenseをご利用の場合、登録する時間間隔を空けるなど、利用レートが制限を超えないようにしてください。

POST /kakinosuke_cohere_emb3_light_float/_doc
{
  "chunk_no": 1,
  "content": """むかし、むかし、あるところに、おじいさんとおばあさんがありました。まいにち、おじいさんは山へしば刈りに、おばあさんは川へ洗濯に行きました。"
ある日、おばあさんが、川のそばで、せっせと洗濯をしていますと、川上から、大きな柿が一つ、
「ドンブラコッコ、スッコッコ。
ドンブラコッコ、スッコッコ。」
と流れて来ました。"""
}

POST /kakinosuke_cohere_emb3_light_float/_doc
{
  "chunk_no": 2,
  "content": """「おやおや、これはみごとな柿だこと。おじいさんへのおみやげに、どれどれ、うちへ持って帰りましょう。」
おばあさんは、そう言いながら、腰をかがめて柿を取ろうとしましたが、遠くって手がとどきません。おばあさんはそこで、
「あっちの水は、かあらいぞ。
こっちの水は、ああまいぞ。
かあらい水は、よけて来い。
ああまい水に、よって来い。
と歌いながら、手をたたきました。すると柿はまた、
「ドンブラコッコ、スッコッコ。
ドンブラコッコ、スッコッコ。」
といいながら、おばあさんの前へ流れて来ました。"""
}

POST /kakinosuke_cohere_emb3_light_float/_doc
{
  "chunk_no": 3,
  "content": """おばあさんはにこにこしながら、
「早くおじいさんと二人で分けて食べましょう。」
と言って、柿をひろい上げて、洗濯物といっしょにたらいの中に入れて、えっちら、おっちら、かかえておうちへ帰りました。"""
}

POST /kakinosuke_cohere_emb3_light_float/_doc
{
  "chunk_no": 4,
  "content": """夕方になってやっと、おじいさんは山からしばを背負って帰って来ました。
「おばあさん、今帰ったよ。」
「おや、おじいさん、おかいんなさい。待っていましたよ。さあ、早くお上がんなさい。いいものを上げますから。」
「それはありがたいな。何だね、そのいいものというのは。」
こういいながら、おじいさんはわらじをぬいで、上に上がりました。"""
}

POST /kakinosuke_cohere_emb3_light_float/_doc
{
  "chunk_no": 5,
  "content": """その間に、おばあさんは戸棚の中からさっきの柿を重そうにかかえて来て、
「ほら、ごらんなさいこの柿を。」
と言いました。
「ほほう、これはこれは。どこからこんなみごとな柿を買って来た。」
「いいえ、買って来たのではありません。今日川で拾って来たのですよ。」
「え、なに、川で拾って来た。それはいよいよめずらしい。」
こうおじいさんは言いながら、柿を両手にのせて、ためつ、すがめつ、ながめていますと、だしぬけに、柿はぽんと中から二つに割れて、
「おぎゃあ、おぎゃあ。」
と勇ましいうぶ声を上げながら、かわいらしい赤さんが元気よくとび出しました。"""
}

POST /kakinosuke_cohere_emb3_light_float/_doc
{
  "chunk_no": 6,
  "content": """「おやおや、まあ。」
おじいさんも、おばあさんも、びっくりして、二人いっしょに声を立てました。
「まあまあ、わたしたちが、へいぜい、どうかして子供が一人ほしい、ほしいと言っていたものだから、きっと神さまがこの子をさずけて下さったにちがいない。」
おじいさんも、おばあさんも、うれしがって、こう言いました。
そこであわてておじいさんがお湯をわかすやら、おばあさんがむつきをそろえるやら、大さわぎをして、赤さんを抱き上げて、うぶ湯をつかわせました。するといきなり、
「うん。」
と言いながら、赤さんは抱いているおばあさんの手をはねのけました。
「おやおや、何という元気のいい子だろう。」
おじいさんとおばあさんは、こう言って顔を見合わせながら、「あッは、あッは。」とおもしろそうに笑いました。
そして柿の中から生まれた子だというので、この子に柿之助という名をつけました。"""
}

POST /kakinosuke_cohere_emb3_light_float/_doc
{
  "chunk_no": 7,
  "content": """おじいさんとおばあさんは、それはそれはだいじにして柿之助を育てました。柿之助はだんだん成長するにつれて、あたりまえの子供にくらべては、ずっと体も大きいし、力がばかに強くって、すもうをとっても近所の村じゅうで、かなうものは一人もないくらいでしたが、そのくせ気だてはごくやさしくって、おじいさんとおばあさんによく孝行をしました。
柿之助は十五になりました。
もうそのじぶんには、日本の国中で、柿之助ほど強いものはないようになりました。柿之助はどこか外国へ出かけて、腕いっぱい、力だめしをしてみたくなりました。"""
}

POST /kakinosuke_cohere_emb3_light_float/_doc
{
  "chunk_no": 8,
  "content": """するとそのころ、ほうぼう外国の島々をめぐって帰って来た人があって、いろいろめずらしい、ふしぎなお話をした末に、
「もう何年も何年も船をこいで行くと、遠い遠い海のはてに、悪霊島という所がある。悪い悪霊どもが、いかめしいくろがねのお城の中に住んで、ほうぼうの国からかすめ取った貴い宝物を守っている。」
と言いました。
柿之助はこの話をきくと、その悪霊島へ行ってみたくって、もう居ても立ってもいられなくなりました。そこでうちへ帰るとさっそく、おじいさんの前へ出て、
「どうぞ、わたくしにしばらくおひまを下さい。」
と言いました。"""
}

POST /kakinosuke_cohere_emb3_light_float/_doc
{
  "chunk_no": 9,
  "content": """おじいさんはびっくりして、
「お前どこへ行くのだ。」
と聞きました。
「悪霊島へ悪霊せいばつに行こうと思います。」
と柿之助はこたえました。
「ほう、それはいさましいことだ。じゃあ行っておいで。」
とおじいさんは言いました。
「まあ、そんな遠方へ行くのでは、さぞおなかがおすきだろう。よしよし、おべんとうをこしらえて上げましょう。」
とおばあさんも言いました。"""
}

POST /kakinosuke_cohere_emb3_light_float/_doc
{
  "chunk_no": 10,
  "content": """そこで、おじいさんとおばあさんは、お庭のまん中に、えんやら、えんやら、大きな臼を持ち出して、おじいさんがきねを取ると、おばあさんはこねどりをして、
「ぺんたらこっこ、ぺんたらこっこ。ぺんたらこっこ、ぺんたらこっこ。」
と、おべんとうのおむすびをつきはじめました。
おむすびがうまそうにでき上がると、柿之助のしたくもすっかりでき上がりました。
"""
}

POST /kakinosuke_cohere_emb3_light_float/_doc
{
  "chunk_no": 11,
  "content": """柿之助はお侍の着るような陣羽織を着て、刀を腰にさして、おむすびの袋をぶら下げました。そして柿の絵のかいてある軍扇を手に持って、
「ではおとうさん、おかあさん、行ってまいります。」
と言って、ていねいに頭を下げました。
「じゃあ、りっぱに悪霊を退治してくるがいい。」
とおじいさんは言いました。
「気をつけて、けがをしないようにおしよ。」
とおばあさんも言いました。"""
}

POST /kakinosuke_cohere_emb3_light_float/_doc
{
  "chunk_no": 12,
  "content": """「なに、大丈夫です、日本一のおむすびを持っているから。」と柿之助は言って、
「では、ごきげんよう。」
と元気な声をのこして、出ていきました。おじいさんとおばあさんは、門の外に立って、いつまでも、いつまでも見送っていました。
"""
}

POST /kakinosuke_cohere_emb3_light_float/_doc
{
  "chunk_no": 13,
  "content": """柿之助はずんずん行きますと、大きな山の上に来ました。すると、草むらの中から、「ワン、ワン。」と声をかけながら、猫が一ぴきかけて来ました。
柿之助がふり返ると、猫はていねいに、おじぎをして、
「柿之助さん、柿之助さん、どちらへおいでになります。」
とたずねました。
「悪霊島へ、悪霊せいばつに行くのだ。」
「お腰に下げたものは、何でございます。」
「日本一のおむすびさ。」
「一つ下さい、お供しましょう。」
「よし、よし、やるから、ついて来い。」
猫はおむすびを一つもらって、柿之助のあとから、ついて行きました。"""
}

POST /kakinosuke_cohere_emb3_light_float/_doc
{
  "chunk_no": 14,
  "content": """山を下りてしばらく行くと、こんどは森の中にはいりました。すると木の上から、「キャッ、キャッ。」とさけびながら、ゴリラが一ぴき、かけ下りて来ました。
柿之助がふり返ると、ゴリラはていねいに、おじぎをして、
「柿之助さん、柿之助さん、どちらへおいでになります。」
とたずねました。
「悪霊島へ悪霊せいばつに行くのだ。」
「お腰に下げたものは、何でございます。」
「日本一のおむすびさ。」
「一つ下さい、お供しましょう。」
「よし、よし、やるから、ついて来い。」
ゴリラもおむすびを一つもらって、あとからついて行きました。"""
}

POST /kakinosuke_cohere_emb3_light_float/_doc
{
  "chunk_no": 15,
  "content": """山を下りて、森をぬけて、こんどはひろい野原へ出ました。すると空の上で、「ケン、ケン。」と鳴く声がして、鷹が一羽とんで来ました。
柿之助がふり返ると、鷹はていねいに、おじぎをして、
「柿之助さん、柿之助さん、どちらへおいでになります。」
とたずねました。
「悪霊島へ悪霊せいばつに行くのだ。」
「お腰に下げたものは、何でございます。」
「日本一のおむすびさ。」
「一つ下さい、お供しましょう。」
「よし、よし、やるから、ついて来い。」
鷹もおむすびを一つもらって、柿之助のあとからついて行きました。"""
}

POST /kakinosuke_cohere_emb3_light_float/_doc
{
  "chunk_no": 16,
  "content": """猫と、ゴリラと、鷹と、これで三にんまで、いい家来ができたので、柿之助はいよいよ勇み立って、またずんずん進んで行きますと、やがてひろい海ばたに出ました。
 そこには、ちょうどいいぐあいに、船が一そうつないでありました。
 柿之助と、三にんの家来は、さっそく、この船に乗り込みました。
「わたくしは、漕ぎ手になりましょう。」
 こう言って、猫は船をこぎ出しました。
「わたくしは、かじ取りになりましょう。」
 こう言って、ゴリラがかじに座りました。
「わたくしは物見をつとめましょう。」
 こう言って、鷹がへさきに立ちました。
"""
}

POST /kakinosuke_cohere_emb3_light_float/_doc
{
  "chunk_no": 17,
  "content": """うららかないいお天気で、まっ青な海の上には、波一つ立ちませんでした。稲妻が走るようだといおうか、矢を射るようだといおうか、目のまわるような速さで船は走って行きました。ほんの一時間も走ったと思うころ、へさきに立って向こうをながめていた鷹が、「あれ、あれ、島が。」とさけびながら、ぱたぱたと高い羽音をさせて、空にとび上がったと思うと、スウッとまっすぐに風を切って、飛んでいきました。
柿之助もすぐ鷹の立ったあとから向こうを見ますと、なるほど、遠い遠い海のはてに、ぼんやり雲のような薄ぐろいものが見えました。船の進むにしたがって、雲のように見えていたものが、だんだんはっきりと島の形になって、あらわれてきました。"""
}

POST /kakinosuke_cohere_emb3_light_float/_doc
{
  "chunk_no": 18,
  "content": """「ああ、見える、見える、悪霊島が見える。」
柿之助がこういうと、猫も、ゴリラも、声をそろえて、「万歳、万歳。」とさけびました。
見る見る悪霊島が近くなって、もう硬い岩で畳んだ悪霊のお城が見えました。いかめしいくろがねの門の前に見はりをしている悪霊の兵隊のすがたも見えました。
そのお城のいちばん高い屋根の上に、鷹がとまって、こちらを見ていました。
こうして何年も、何年もこいで行かなければならないという悪霊島へ、ほんの目をつぶっている間に来たのです。"""
}

POST /kakinosuke_cohere_emb3_light_float/_doc
{
  "chunk_no": 19,
  "content": """柿之助は、猫とゴリラをしたがえて、船からひらりと陸の上にとび上がりました。
見はりをしていた悪霊の兵隊は、その見なれないすがたを見ると、びっくりして、あわてて門の中に逃げ込んで、くろがねの門を固くしめてしまいました。その時猫は門の前に立って、
「日本の柿之助さんが、お前たちをせいばいにおいでになったのだぞ。あけろ、あけろ。」
とどなりながら、ドン、ドン、扉をたたきました。悪霊はその声を聞くと、ふるえ上がって、よけい一生懸命に、中から押さえていました。"""
}

POST /kakinosuke_cohere_emb3_light_float/_doc
{
  "chunk_no": 20,
  "content": """すると鷹が屋根の上からとび下りてきて、門を押さえている悪霊どもの目をつつきまわりましたから、悪霊はへいこうして逃げ出しました。その間に、ゴリラがするすると高い岩壁をよじ登っていって、ぞうさなく門を中からあけました。
「わあッ。」とときの声を上げて、柿之助の主従が、いさましくお城の中に攻め込んでいきますと、悪霊の大将も大ぜいの家来を引き連れて、一人一人、太い鉄の棒をふりまわしながら、「おう、おう。」とさけんで、向かってきました。"""
}

POST /kakinosuke_cohere_emb3_light_float/_doc
{
  "chunk_no": 21,
  "content": """けれども、体が大きいばっかりで、いくじのない悪霊どもは、さんざん鷹に目をつつかれた上に、こんどは猫に向こうずねをくいつかれたといっては、痛い、痛いと逃げまわり、ゴリラに顔を引っかかれたといっては、おいおい泣き出して、鉄の棒も何もほうり出して、降参してしまいました。
 おしまいまでがまんして、たたかっていた悪霊の大将も、とうとう柿之助に組みふせられてしまいました。柿之助は大きな悪霊の背中に、馬乗りにまたがって、
「どうだ、これでも降参しないか。」
 といって、ぎゅうぎゅう、ぎゅうぎゅう、押さえつけました。"""
}


POST /kakinosuke_cohere_emb3_light_float/_doc
{
  "chunk_no": 22,
  "content": """悪霊の大将は、柿之助の大力で首をしめられて、もう苦しくってたまりませんから、大つぶの涙をぼろぼろこぼしながら、
「降参します、降参します。命だけはお助け下さい。その代わりに宝物をのこらずさし上げます。」
こう言って、ゆるしてもらいました。
悪霊の大将は約束のとおり、お城から、かくれみのに、かくれ笠、うちでの小づちに如意宝珠、そのほかさんごだの、たいまいだの、るりだの、世界でいちばん貴い宝物を山のように車に積んで出しました。"""
}

POST /kakinosuke_cohere_emb3_light_float/_doc
{
  "chunk_no": 23,
  "content": """柿之助はたくさんの宝物をのこらず積んで、三にんの家来といっしょに、また船に乗りました。帰りは行きよりもまた一そう船の走るのが速くって、間もなく日本の国に着きました。
船が陸に着きますと、宝物をいっぱい積んだ車を、猫が先に立って引き出しました。鷹が綱を引いて、ゴリラがあとを押しました。
「えんやらさ、えんやらさ。」
三にんは重そうに、かけ声をかけかけ進んでいきました。
"""
}

POST /kakinosuke_cohere_emb3_light_float/_doc
{
  "chunk_no": 24,
  "content": """うちではおじいさんと、おばあさんが、かわるがわる、
「もう柿之助が帰りそうなものだが。」
と言い言い、首をのばして待っていました。そこへ柿之助が三にんのりっぱな家来に、ぶんどりの宝物を引かせて、さもとくいらしい様子をして帰って来ましたので、おじいさんもおばあさんも、目も鼻もなくして喜びました。
「えらいぞ、えらいぞ、それこそ日本一だ。」
とおじいさんは言いました。
「まあ、まあ、けががなくって、何よりさ。」
とおばあさんは言いました。"""
}

POST /kakinosuke_cohere_emb3_light_float/_doc
{
  "chunk_no": 25,
  "content": """柿之助は、その時猫とゴリラと鷹の方を向いてこう言いました。
「どうだ。悪霊せいばつはおもしろかったなあ。」
猫はワン、ワンとうれしそうにほえながら、前足で立ちました。
ゴリラはキャッ、キャッと笑いながら、白い歯をむき出しました。
鷹はケン、ケンと鳴きながら、くるくると宙返りをしました。
空は青々と晴れ上がって、お庭には桜の花が咲き乱れていました。"""
}

最後に、_refreshを呼び出します。

POST /kakinosuke_cohere_emb3_light_float/_refresh

登録された密ベクトルの確認

下記のリクエストを Kibana の DevTools の Console から発行します。

GET /kakinosuke_cohere_emb3_light_float/_search
{
  "size": 30,
  "query": {
    "match_all": {}
  },
  "fields": [ "_inference_fields" ]
}

以下のようなレスポンスが返却されます。

{
  "took": 15,
  ...
  "hits": {
    "hits": [
      {
        "chunk_no": 4,
        "content": """夕方になってやっと、おじいさんは山からしばを背負って帰って来ました。...
        """,
        "_inference_fields": {
          "content.text_embedding": {
            "inference_id": "my_cohere_emb_light_v3_float",
            "model_settings": {
              "task_type": "text_embedding",
              "dimensions": 384,
              "similarity": "cosine",
              "element_type": "float"
            },
            "chunks": {
              "content": [
                {
                  "start_offset": 0,
                  "end_offset": 168,
                  "embeddings": [
                    0.095214844,
                    -0.061676025,
                    ...
                    0.05307007
                  ]
                }
              ]
            }
          }
        }
      },
      ...
    ]
  }
}

float の密ベクトルが格納されていることがわかります。

ベクトル生成を明示的に行っていないにもかかわらず、ベクトルが計算され格納されているのは、/_inference/text_embeddingsのエンドポイントを作成し、それをインデックスのマッピングに指定したおかげです。

密ベクトル検索

準備ができたので、実際に密ベクトルを使った検索を行ってみましょう。

下記のリクエストを Kibana の DevTools の Console から発行します。

GET /kakinosuke_cohere_emb3_light_float/_search
{
    "query": {
        "semantic": {
          "field": "content.text_embedding",
          "query": "川に流れてきた果物は何?"
        }
    }
}

レスポンス

{
  "took": 103,
   ...
    "hits": [
      {
        ...
        "_source": {
          "chunk_no": 1,
          "content": """むかし、むかし、あるところに、...
川上から、大きな柿が一つ、...
"""
        }
      },
      {
        ...
        "_source": {
          "chunk_no": 5,
          "content": """その間に、おばあさんは戸棚の中から...
「ほほう、これはこれは。どこからこんなみごとな柿を買って来た。」
「いいえ、買って来たのではありません。今日川で拾って来たのですよ。」
「え、なに、川で拾って来た。それはいよいよめずらしい。」
こうおじいさんは言いながら、柿を両手にのせて、...
"""
        }
      },
      {
        ...
        "_source": {
          "chunk_no": 2,
          "content": """「おやおや、これはみごとな柿だこと。...
といいながら、おばあさんの前へ流れて来ました。"""
        }
      },
      ...
    ]
  }
}

クエリー「川に流れてきた果物は何?」に近いドキュメントが返却されています。(*脚注2)2

検索時にもベクトル生成を明示していませんが、これも/_inference/text_embedding/のエンドポイントを作成した効果です。

なお、以前のベクトル検索では、Elasticsearchでのベクトル検索 で紹介したように、次のようなやや複雑なクエリーを記述する必要がありました。

GET /momotaro_v3/_search
{
  "knn": {
    "field": "text_embedding.predicted_value",
    "k": "10",
    "num_candidates": "100",
    "query_vector_builder": {
      "text_embedding": {
        "model_id": ".multilingual-e5-small_linux-x86_64",
        "model_text": "桃太郎が鬼が島へ向かった際の乗り物は"
      }
    }
  }
}

しかし、v8.18からは、

GET /インデックス名/_search
{
  "query": {
    "semantic": {
      "field": "密ベクトルを格納しているフィールド名",
      "query": "検索したいテキスト"
    }
  }
}

といった簡素な構文で検索できるようになりました。詳細は こちら をご参照ください。

他の検索クエリも試してみます。

GET /kakinosuke_cohere_emb3_light_float/_search
{
    "query": {
        "semantic": {
          "field": "content.text_embedding",
          "query": "柿之助の家来は誰?"
        }
    }
}

レスポンス:

{
  "took": 173,
  ...
  "hits": {
    ...
    "hits": [
      {
        ...
        "_source": {
          "chunk_no": 23,
          "content": """柿之助はたくさんの宝物をのこらず積んで、三にんの家来といっしょに...
          猫が先に立って引き出しました。鷹が綱を引いて、ゴリラがあとを押しました。..."""
        }
      },
      {
        ...
        "_source": {
          "chunk_no": 16,
          "content": """猫と、ゴリラと、鷹と、これで三にんまで、いい家来ができたので、柿之助はいよいよ..."""
        }
      },
      ...
    ]
  }
}

検索クエリ「柿之助の家来は誰?」に近いドキュメントが返却されています。

まとめ

このように、Elasticsearchの外部のEmbed Modelを利用しても密ベクトル検索を行えることがご理解いただけたかと思います。

要件に合ったEmbed Modelを利用して環境を構築してみてください。


  1. Elastic Stack の License ごとの機能の差異は下記を参照してください。
    https://www.elastic.co/subscriptions
    ↩︎
  2. 今回検証に使用した Elasticsearch v9.0.3 では、384次元以上の float 型のベクトルの場合、bbq により量子化されたベクトルを使って検索されます(デフォルトの動作)。
    https://www.elastic.co/docs/reference/elasticsearch/mapping-reference/dense-vector#index-vectors-knn-search

    もしも bbq 量子化ベクトルによる検索結果の精度が十分でない場合は、量子化前の float 型のベクトルを使った rescore 処理の追加を検討してみてください。
    https://www.elastic.co/docs/solutions/search/vector/knn#dense-vector-knn-search-rescoring

    ↩︎