TECH PLAY

サイオステクノロジー((DXSL))

サイオステクノロジー((DXSL)) の技術ブログ

49

目次 はじめに 対象読者 環境 Elasticsearch同梱モデル vs 外部モデル Elasticsearch同梱の Model を利用する場合 Elasticsearchの外部のEmbed Modelを利用する場合 比較表 Elasticsearchで密ベクトル生成に利用可能なサービス 準備 Cohere API Key の取得 Machine Learning インスタンス /_inference/text_embedding/用エンドポイントの作成 インデックスの作成 マッピングの作成 ドキュメントの登録 登録された密ベクトルの確認 密ベクトル検索 まとめ はじめに 今回は、Elasticsearchでの密ベクトル検索において、外部のEmbed Modelを利用する方法について解説します。 これまで、 ホワイトペーパー 「Elasticsearchを使った簡易RAGアプリケーションの作成」やブログ記事「 Elasticsearchでのベクトル検索の準備 」で密ベクトル検索をご紹介してきました。これらはElasticsearchが同梱している .multilingual-e5-small というEmbed Modelを利用していました。 本記事では、Elasticsearchの外部にあるEmbed Modelを使って密ベクトル検索を行う方法を詳しく説明します。 対象読者 Elasticsearchの初級者~中級者 環境 Elasticsearch 8.18以上、かつPlatinum License以上(筆者はElasticsearch 9.0.3 Enterprise Licenseで動作確認しました。) Cohere embed-multilingual-light-v3.0 (Elasticsearchの /_inference/text_embedding/ が対応しているEmbed Modelであれば動作します。筆者は左記のモデルで動作確認しました。) なお、本ブログで紹介する /_inference/text_embedding/ を使った密ベクトル生成は、Basic Licenseでは動作しません。Basic Licenseをご利用の場合は、ベクトル変換処理のプログラムを作成して、それらを呼び出すなどの対応が必要です。 また、本ブログではフィールドタイプに 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/を使わず、独自にプログラムを作成して密ベクトル生成処理を組み込めば、上記以外のモデルも利用可能です。 今回は、この中から 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_no integer チャンク番号 content text 内容(本文) content.text_embedding semantic_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": """「おやおや、これはみごとな柿だこと。... といいながら、おばあさんの前へ流れて来ました。""" } }, ... ] } } クエリー「川に流れてきた果物は何?」に近いドキュメントが返却されています。 検索時にもベクトル生成を明示していませんが、これも/_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を利用して環境を構築してみてください。 The post Elasticsearchでの外部のEmbed Modelを使った密ベクトル検索 first appeared on Elastic Portal .
アバター
目次 はじめに 対象者 前提条件 ドキュメントレベルセキュリティの概要 サンプルアプリ ソースコードの取得方法 インデックスの作成 インデックスへのマッピングの登録 ドキュメントの登録 APIキーの発行 ElasticsearchエンドポイントURLの取得 ビルド~コンテナとの接続 ビルド コンテナの起動 コンテナとの接続 サンプルプログラムの実行 ログイン画面の表示 ユーザーごとの動作確認 user1での動作確認 user2での動作確認 user3での動作確認 user4での動作確認 関連情報 Connector における Document Level Security Field Level Security まとめ はじめに これまで3回にわたって、インデックスごとのロールベースアクセス制御(RBAC) の設定方法について解説してきました。 Elasticsearchのインデックスに対するアクセス制御(概要 ) Elasticsearchのインデックスに対するアクセス制御(Kibana上での動作確認) Elasticsearchのインデックスに対するアクセス制御(API経由での動作確認) 今回は、ドキュメントレベルセキュリティ(Document Level Security: DLS) について説明します。 DLSを利用すると、以下のようなきめ細やかな権限設定が可能です。 ユーザーAはこのドキュメントを参照できる。 グループBはこのドキュメントを参照できる。 ユーザーCとグループDはこのドキュメントを参照できる。 対象者 Elasticsearch 中級者 前提条件 Elasticsearch Platinum License または Enterprise License (DLS機能は Basic License では利用できません。) Elasticsearch v9.0.3 (他のバージョンでも動作する可能性はありますが、筆者は Enterprise License v9.0.3 で動作確認済みです。) Docker 実行環境 (筆者は Rancher Desktop 1.19.3 で動作確認済みです。) Python 3.13 streamlit 1.42.2 streamlit-authenticator 0.4.2 ドキュメントレベルセキュリティの概要 ドキュメントレベルセキュリティの概要を図に示します。 まず、ドキュメントをインデックスに登録する際に、そのドキュメントを参照可能なユーザー名やグループ名も一緒に格納します。 次に、アクセスするユーザーのユーザー名とグループ名を記載した APIキー を発行します。 発行されたAPIキーを使用して検索を実行します。 APIキーが保持するユーザー名、グループ名で読み取り可能なドキュメントのみがヒットし、結果として返却されます。 この図だけでは分かりにくいかもしれないため、実際にサンプルアプリを動かして動作を確認してみましょう。 サンプルアプリ 今回紹介するサンプルアプリは、 GitHubリポジトリ で公開しています。ソースコードの詳細はそちらを参照してください。 ソースコードの取得方法 右記のURLにアクセスします。 : https://github.com/sios-elastic-tech/blogs [<> Code] から Download ZIP を選択してダウンロードします。 ダウンロードしたblogs-main.zipファイルを展開します。 2025-07-dlsフォルダに移動します。 cd 2025-07-dls 注記: 準備作業として必要なリクエストもこのフォルダに含まれています。 インデックスの作成 Document Level Security の動作確認を目的とした簡易的なインデックスのため、本文の形態素解析などは省略します。 以下のリクエストを Dev Tools の Console から発行してください。 PUT /dls_sample_202507 { "settings": { "index": { "number_of_shards": 1, "number_of_replicas": 1 }, "analysis": { "filter": { "en-stop-words-filter": { "type": "stop", "stopwords": "_english_" } }, "analyzer": { "iq_text_base": { "filter": [ "cjk_width", "lowercase", "asciifolding", "en-stop-words-filter" ], "tokenizer": "standard" } } } } } インデックスへのマッピングの登録 必要最低限のフィールドのみを生成します 本文用フィールド: content アクセス制御用フィールド: _allow_access_control 以下のリクエストを Dev Tools の Console から発行してください。 PUT /dls_sample_202507/_mapping { "properties": { "content": { "type": "text" }, "_allow_access_control": { "type": "text", "index_options": "freqs", "analyzer": "iq_text_base", "fields": { "enum": { "type": "keyword", "ignore_above": 2048 } } } } } ドキュメントの登録 検索対象となるドキュメントを登録します。その際、そのドキュメントを検索可能なユーザー名、グループ名も登録します。 読み取り権限は以下の表のようにしておきます。 ドキュメント 読み取り能なユーザー名、グループ名 doc1: … user1@example.com または group1 doc2: … user2@example.com または group2 doc3: … user1@example.com doc4: … user2@example.com doc5: … group1 doc6: … group2 doc7: … 全員 注記: doc7 には読み取り可能なユーザー、グループを明示していません。明示しない場合は、全員が読み取り可能とします。 以下のリクエストを Dev Tools の Console から発行してください。 POST /dls_sample_202507/_doc { "content": "doc1: This document can be read by user1 or group1.", "_allow_access_control": [ "user1@example.com", "group1" ] } POST /dls_sample_202507/_doc { "content": "doc2: This document can be read by user2 or group2.", "_allow_access_control": [ "user2@example.com", "group2" ] } POST /dls_sample_202507/_doc { "content": "doc3: This document can be read by user1.", "_allow_access_control": [ "user1@example.com" ] } POST /dls_sample_202507/_doc { "content": "doc4: This document can be read by user2.", "_allow_access_control": [ "user2@example.com" ] } POST /dls_sample_202507/_doc { "content": "doc5: This document can be read by group1.", "_allow_access_control": [ "group1" ] } POST /dls_sample_202507/_doc { "content": "doc6: This document can be read by group2.", "_allow_access_control": [ "group2" ] } POST /dls_sample_202507/_doc { "content": "doc7: This document can be read by everybody." } APIキーの発行 動作確認として、以下の4パターンのAPIキーを発行します。 APIキー名 ユーザー、グループ 有効期限 user1_api_key_20250702 user1@example.com, group1 1日 user2_api_key_20250702 user2@example.com, group2 1日 user3_api_key_20250702 user3@example.com, group1 1日 user4_api_key_20250702 user4@example.com, group2 1日 まず、user1用の以下のリクエストを Dev Tools の Console から発行してください。(*脚注1) 1 POST /_security/api_key { "name": "user1_api_key_20250702", "expiration": "1d", "role_descriptors": { "dls_sample_user1": { "cluster": ["monitor"], "index": [ { "names": [ "dls_sample_202507" ], "privileges": [ "read" ], "query": { "template": { "params": { "access_control": [ "user1@example.com", "group1" ] }, "source": """ { "bool": { "should": [ { "bool": { "must_not": { "exists": { "field": "_allow_access_control" } } } }, { "terms": { "_allow_access_control.enum": {{#toJson}}access_control{{/toJson}} } } ] } } """ } } } ] } } } ※このリクエストを見ると、なんとなく想像がつくと思いますが、超端的に言えば、検索クエリーの発行時に _allow_access_control フィールドを使って強制的にフィルタをかけるような動きになります。 ※_allow_access_control フィールドがないドキュメントは、全員読み取り可能とします。 成功すると、以下のようなレスポンスが返却されます。 { "id": "xxxxxxxxxxxxxxxxxx", "name": "user1_api_key_20250702", "expiration": xxxxxxxxxxxxxxx, "api_key": "xxxxxxxxxxxxxxxxxx", "encoded": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx==" } このうち、encoded の値をコピーして、config.yaml の user1 の encoded_api_key に貼り付けます。 config.yaml: elasticsearch: endpoint: url_of_elasticsearch_endpoint cookie: expiry_days: 1 key: some_signature_key name: some_cookie_name credentials: usernames: user1: name: user1 password: secret_password encoded_api_key: dummy== user2: name: user2 password: secret_password encoded_api_key: dummy== user3: name: user3 password: secret_password encoded_api_key: dummy== user4: name: user4 password: secret_password encoded_api_key: dummy== 同様に、user2用のAPIキーを発行し、encoded の値を user2 の encoded_api_key の欄に貼り付けます。 POST /_security/api_key { "name": "user2_api_key_20250702", "expiration": "1d", "role_descriptors": { "dls_sample_user2": { "cluster": ["monitor"], "index": [ { "names": [ "dls_sample_202507" ], "privileges": [ "read" ], "query": { "template": { "params": { "access_control": [ "user2@example.com", "group2" ] }, "source": """ { "bool": { "should": [ { "bool": { "must_not": { "exists": { "field": "_allow_access_control" } } } }, { "terms": { "_allow_access_control.enum": {{#toJson}}access_control{{/toJson}} } } ] } } """ } } } ] } } } 同様にuser3用のAPIキーを発行し、user3 の encoded_api_key の欄に encoded の値を貼り付けます。 POST /_security/api_key { "name": "user3_api_key_20250702", "expiration": "1d", "role_descriptors": { "dls_sample_user3": { "cluster": ["monitor"], "index": [ { "names": [ "dls_sample_202507" ], "privileges": [ "read" ], "query": { "template": { "params": { "access_control": [ "user3@example.com", "group1" ] }, "source": """ { "bool": { "should": [ { "bool": { "must_not": { "exists": { "field": "_allow_access_control" } } } }, { "terms": { "_allow_access_control.enum": {{#toJson}}access_control{{/toJson}} } } ] } } """ } } } ] } } } 同様にuser4用のAPIキーを発行し、user4 の encoded_api_key の欄に encoded の値を貼り付けます。 POST /_security/api_key { "name": "user4_api_key_20250702", "expiration": "1d", "role_descriptors": { "dls_sample_user4": { "cluster": ["monitor"], "index": [ { "names": [ "dls_sample_202507" ], "privileges": [ "read" ], "query": { "template": { "params": { "access_control": [ "user4@example.com", "group2" ] }, "source": """ { "bool": { "should": [ { "bool": { "must_not": { "exists": { "field": "_allow_access_control" } } } }, { "terms": { "_allow_access_control.enum": {{#toJson}}access_control{{/toJson}} } } ] } } """ } } } ] } } } ※user1, user2, user3, user4 の Streamlit へのログインパスワードも config.yaml に記載してください。あくまでもサンプルアプリケーションですので、暗号化などは考慮していません。 ElasticsearchエンドポイントURLの取得 Elastic Kibana の Home 画面から Elasticsearch のエンドポイントURLを取得し、config.yamlファイルの endpoint に転記してください。 ビルド~コンテナとの接続 ビルド docker-compose.yml があるディレクトリに移動します。 cd app docker-compose.ymlがあるディレクトリで、以下のコマンドを実行します。 docker compose build コンテナの起動 docker compose up -d コンテナとの接続 docker exec -it dls_sample_202507 /bin/bash (dls_sample_202507はコンテナ名です) サンプルプログラムの実行 ログイン画面の表示 dls_sample_202507コンテナ上の bash から次のコマンドを実行します。 streamlit run src/app.py Webブラウザから http://localhost:8501/ にアクセスしてください。 ユーザーごとの動作確認 user1での動作確認 まず、user1でログインします。パスワードは config.yaml に記載したものを入力してください。 ログインに成功すると、検索画面へ遷移します。 [Search]ボタンを押してください。 user1が参照可能なドキュメントのみが検索されます。 動作確認が終わったら、[Logout]ボタンを押してログアウトします。 user2での動作確認 同様に、user2でも動作確認します。 user2でログインします。パスワードは config.yaml に記載したものを入力してください。 ログインに成功すると、検索画面へ遷移します。 [Search]ボタンを押してください。 user2が参照可能なドキュメントのみが検索されます。 [Logout]ボタンを押してログアウトします。 user3での動作確認 同様に、user3でも動作確認します。 user3でログインします。パスワードはconfig.yamlに記載したものを入力してください。 ログインに成功すると、検索画面へ遷移します。 [Search]ボタンを押してください。 user3が参照可能なドキュメントのみが検索されます。 [Logout]ボタンを押してログアウトします。 user4での動作確認 最後に、user4で動作確認します。 user4でログインします。パスワードはconfig.yamlに記載したものを入力してください。 ログインに成功すると、検索画面へ遷移します。 [Search]ボタンを押してください。 user4が参照可能なドキュメントのみが検索されます。 [Logout]ボタンを押してログアウトします。 注記: 停止ボタンは用意していないため、停止させたい場合はCtrl+Cを押すなどの操作を行ってください。 関連情報 Connector における Document Level Security Elasticsearchには、Connectorという機能があります。 Connectorを利用すると、Amazon S3 や SharePoint Online など外部に存在するファイルをElasticsearch に取り込んで検索できるようになります。 その際、いくつかの Connector では Document Level Security に対応しており、ユーザーの読み取り権限に応じた検索が可能です。 例)ServiceNowなど。 詳細は、Elasticsearch の Connector のドキュメントを参照してください。 https://www.elastic.co/docs/reference/search-connectors/ Field Level Security Document Level Security 以外にも、Field Level Security の機構も用意されています。 これは、「このフィールドに対しては、このユーザーは参照可能、このユーザーは参照不可」といった制御が可能です。 詳細は、Elasticsearch の Field Level Security のドキュメントを参照してください。 https://www.elastic.co/docs/deploy-manage/users-roles/cluster-or-deployment-auth/controlling-access-at-document-field-level#field-level-security まとめ Elasticsearch の Document Level Security 機能を使うと、ユーザーの読み取り権限に応じてドキュメントが検索されることを確認できました。 前回のインデックス単位の権限設定と組み合わせることで、情報漏洩を防ぎつつ、適切な権限を持つユーザーのみが検索できるように設定することが可能です。 ここでは、分かりやすくするために、Dev Tools の Console からAPIキーの生成リクエストを発行する方法を採用しました。 なお、 https://www.elastic.co/docs/reference/search-connectors/es-dls-e2e-guide の例では、 1) .search-acl-filter-source1および.search-acl-filter-source2インデックスに各ユーザーごとの権限情報をドキュメントとして格納しておく。 2) その内容(権限情報)をGET /index-name/_doc/userId で取得する。 3) 取得した内容(権限情報)を元に Create API Key のリクエストを実行して一時的なAPIキーを生成する。 といった手順で APIキーを生成しています。 ↩︎ The post Elasticsearchのドキュメントごとのアクセス制御(Document Level Security) first appeared on Elastic Portal .
アバター
目次 はじめに 対象者 できるようになること 前提条件 接続方法 サンプルプログラムのソースコードの取得 Elasticsearch の Endpoint URL の取得 API Key の作成 ビルド~ コンテナとの接続 ビルド コンテナの起動 コンテナとの接続 サンプルプログラムの実行 ログイン画面の表示 ユーザーごとの動作の確認 まとめ はじめに Elasticsearchのインデックスに対するアクセス制御(Kibana上での動作確認) では、Kibana にログインしたユーザーで各インデックスに対するアクセス権を確認しました。 今回は、Python などから API 経由で操作する場合の設定方法について説明します。 なお、本ブログで動かすサンプルアプリケーションは、下記で公開しています。 https://github.com/sios-elastic-tech/blogs/tree/main/2025-06-rbac 対象者 Elasticsearch 熟練度 : 初級者~中級者 Elasticsearch License : Basic 以上 Elasticsearch Version : 筆者は、8.15.5 で動作確認 できるようになること Elasticsearch で API 経由でインデックス単位でのアクセス制御を行える。 前提条件 Elasticsearchのインデックスに対するアクセス制御(Kibana上での動作確認) に記載した以下の作業を行っておく必要があります。 sales_data_2024, sales_data_2025, hr_data_2024, hr_data_2025 のインデックスを作成しておくこと。 上記のインデックスにドキュメントを登録しておくこと。 sales_data エイリアスを作成しておくこと。 hr_data エイリアスを作成しておくこと。 接続方法 Elasticsearch で、Python などから接続する場合、主に2つの方法があります。 user / password を渡して認証する方法 API Key を渡して認証する方法 今回は、後者の API Key を渡して認証する方法を採用します。 API Key を渡す方法のメリットとデメリットです。 メリット 細やかな権限設定を行える。 API Key の有効期限を設定できる。 デメリット 利用するための API Key を事前に発行しておく必要がある。 サンプルプログラムのソースコードの取得 https://github.com/sios-elastic-tech/blogs  の [<> Code] の Download ZIP を選択します。 blogs-main.zip ファイルがダウンロードされるので、これを展開します。 2025-06-rbac フォルダへ移動します。 cd 2025-06-rbac ※準備作業として必要なリクエストもこの中に含まれています。 相対ファイルパス 説明 es_requests/1_create_index.md インデックスの作成リクエスト es_requests/2_create_index_mapping.md インデックスのフィールド作成リクエスト es_requests/3_create_alias.md エイリアスの作成リクエスト es_requests/4_post_doc.md ドキュメントの登録リクエスト Elasticsearch の Endpoint URL の取得 Elastic の Kibana にログインし、アクセス先となる Endpoint URL を取得します。 取得した URL を app/config.yaml の elasticsearch.endpoint に転記します。 app/config.yaml elasticsearch: endpoint: http://host.docker.internal:9200/ cookie: expiry_days: 1 key: some_signature_key name: some_cookie_name credentials: usernames: sales_user: name: sales_user password: sales_user api_key: xxx== hr_user: name: hr_user password: hr_user api_key: xxx== sales_and_hr_user: name: sales_and_hr_user password: sales_and_hr_user api_key: xxx== ※このサンプルでは、Docker 内のコンテナ上で動作している Elasticsearch にアクセスするようにしています。 ※各ユーザーの password は、Streamlit 上で動作するアプリへのログインパスワードです。 適宜、適切なパスワードに置き換えてください。 API Key の作成 API Key を利用して認証を行う場合、事前に API Key を発行しておく必要があります。(*脚注1) 1 Elastic の Kibana にログインし、DevTools の Console から、下記のリクエストを発行します。 (es_requests/5_create_api_key.md にも記載しています。) POST /_security/api_key { "name": "sales_user_20250616", "expiration": "10d", "role_descriptors": { "sales_data_read": { "cluster": ["all"], "indices": [ { "names": ["sales_data*"], "privileges": ["read"] } ] } } } ※”sales_data”で始まるインデックスおよびエイリアスへの読み取り権限を与えます。 有効期限は、10日間としています。(*脚注2) 2 成功すると、次のようなレスポンスが返却されます。 { "id": ""*****************", "name": "sales_user_20250616", "expiration": 1750916489855, "api_key": "*****************", "encoded": "**************************************==" } ここで重要な値は、最後の encoded です。 一度しか表示されないので、app/config.yaml の sales_user の api_key に copy & paste しておきます。 同様に、hr_user 用の API Key も生成します。 リクエスト POST /_security/api_key { "name": "hr_user_20250616", "expiration": "10d", "role_descriptors": { "hr_data_read": { "cluster": ["all"], "indices": [ { "names": ["hr_data*"], "privileges": ["read"] } ] } } } レスポンス { "id": ""*******************", "name": "hr_user_20250616", "expiration": 1750917110752, "api_key": "*******************", "encoded": "********************************==" } encoded に表示された値を、app/config.yaml の hr_user の api_key に copy & paste しておきます。 最後に、sales と hr 両方の読み取り用 API Key を発行します。 リクエスト POST /_security/api_key { "name": "sales_and_hr_user_20250616", "expiration": "10d", "role_descriptors": { "sales_and_hr_data_read": { "cluster": ["all"], "indices": [ { "names": ["sales_data*", "hr_data*"], "privileges": ["read"] } ] } } } レスポンス { "id": """**********************", "name": "sales_and_hr_user_20250616", "expiration": 1750917246202, "api_key": ""**********************", "encoded": "********************************************==" } encoded に表示された値を、app/config.yaml の sales_and_hr_user の api_key に copy & paste しておきます。 ビルド~ コンテナとの接続 ビルド docker-compose.yml があるディレクトリへ移動します。 cd app docker-compose.yml があるディレクトリで下記を実行します。 docker compose build コンテナの起動 docker compose up -d コンテナとの接続 docker exec -it rbac_sample_202506 /bin/bash (“rbac_sample_202506″はコンテナ名) サンプルプログラムの実行 ログイン画面の表示 rbac_sample_202506 コンテナ上の bash から次のコマンドを実行します。 streamlit run src/app.py Web ブラウザから  http://localhost:8501/  にアクセスします。 ユーザーごとの動作の確認 sales_user まず、sales_user でログインします。 パスワードは config.yaml に記載したものを入力します。 成功すると、検索画面へ遷移します。 [search sales_data] ボタンを押します。 sales_data* インデックスへの読み取り権があるので、成功します。 続いて [search hr_data] ボタンを押します。 hr_data* インデックスへの読み取り権がないため、エラーとなります。 この動きを図式化すると以下のような図になります。 動作確認が終わったら、[Logout] ボタンを押してログアウトします。 2. hr_user 同様に hr_user でも動作確認します。 hr_user でログインします。 パスワードは config.yaml に記載したものを入力します。 成功すると、検索画面へ遷移します。 [search sales_data] ボタンを押します。 sales_data* インデックスへの読み取り権がないため、エラーとなります。 続いて [search hr_data] ボタンを押します。 hr_data* インデックスへの読み取り権があるので、成功します。 [Logout] ボタンを押してログアウトします。 3. sales_and_hr_user 最後に sales_and_hr_user で動作確認します。 sales_and_hr_user でログインします。 パスワードは config.yaml に記載したものを入力します。 成功すると、検索画面へ遷移します。 [search sales_data] ボタンを押してみます。 sales_data* インデックスへの読み取り権があるので、成功します。 続いて [search hr_data] ボタンを押します。 hr_data* インデックスへの読み取り権があるので、成功します。 [Logout] ボタンを押してログアウトします。 ※停止ボタンは用意していないので、停止させたい場合は Ctrl+C を押すなどの処置を行ってください。 まとめ Python などのプログラムからアクセスする場合に、適切な権限を設定した API Key を使って接続することにより、 インデックスごとの権限を考慮したアクセスができることを紹介しました。 今回は、インデックスごとのアクセス権限の制御の方法について紹介しましたが、 ドキュメントごとのアクセス権限を制御する方法もあります。 こちらについては、日をあらためて紹介したいと思います。 今回は簡易なサンプルなので、あらかじめ Dev Tools の Console で 上記リクエストを発行し、API Key を作成していますが、 ログインするたびにAPI Key を生成するリクエストを発行し、有効期限が短い API Key を生成する方法も考えられます。 ただし、そのような仕組みにしようとすると、本ブログで説明するには複雑な構成になってしまうため、 ここでは簡易的な仕組みとしています。 ※API Key を発行するためには、API Key を発行する権限(manage_api_key)を持ったユーザーをあらかじめ作成しておく必要があります。 また、そのユーザーに接続するには API Key ではなく user / password による認証が必要です。 ↩︎ 有効期限を過ぎた API Key は、invalidated: true となり使用できなくなります。その後、おおよそ24時間後に削除されます。 https://www.elastic.co/docs/reference/elasticsearch/configuration-reference/security-settings ↩︎ The post Elasticsearchのインデックスに対するアクセス制御(API経由での動作確認) first appeared on Elastic Portal .
アバター
目次 はじめに 対象者 できるようになること ロール(Role)の作成 ユーザー(User)の作成 インデックスの作成 インデックスのフィールドの作成 インデックスへのエイリアスの作成 ドキュメントの登録 ユーザーごとの動作確認 まとめ はじめに Elasticsearchのインデックスに対するアクセス制御(概要 ) で Elasticsearch におけるロールベースのアクセス制御(RBAC)の概要について説明しました。 今回は、実際にロールとユーザーを作成して、Kibana上での実際の動作を確認していきます。 対象者 – Elasticsearch 熟練度 : 初級者~中級者 – Elasticsearch License : Basic 以上 – Elasticsearch Version : 筆者は、8.15.0 で動作確認 できるようになること – Elasticsearch でインデックス単位でのアクセス制御を行える。 ロール(Role)の作成 まず、 Elasticsearchのインデックスに対するアクセス制御(概要 ) のパターン1、パターン2 で示したようなロールを作成していきます。 作成するロールは、以下の3つです。 ロール名 対象インデックス 許可する操作 sales_user_role sales_data* read, view_index_metadata hr_user_role hr_data* read, view_index_metadata sales_and_hr_manager_role sales_data*, hr_data* all elastic ユーザーで Kibana にログインし、Dev Tools の Console から以下のリクエストを発行して、上記の3つのロールを作成します。(*1) 1 POST /_security/role/sales_user_role { "cluster": [ "all" ], "indices": [ { "names": ["sales_data*"], "privileges": ["read","view_index_metadata"] } ], "applications": [ { "application": "kibana-.kibana", "privileges": [ "space_all" ], "resources": [ "space:default" ] } ], "run_as": [], "metadata": {}, "transient_metadata": { "enabled": true } } POST /_security/role/hr_user_role { "indices": [ { "names": ["hr_data*"], "privileges": ["read","view_index_metadata"] } ], "applications": [ { "application": "kibana-.kibana", "privileges": [ "space_all" ], "resources": [ "space:default" ] } ], "run_as": [], "metadata": {}, "transient_metadata": { "enabled": true } } POST /_security/role/sales_and_hr_manager_role { "cluster": [ "all" ], "indices": [ { "names": ["hr_data*", "sales_data*"], "privileges": ["all"] } ], "applications": [ { "application": "kibana-.kibana", "privileges": [ "space_all" ], "resources": [ "space:default" ] } ], "run_as": [], "metadata": {}, "transient_metadata": { "enabled": true } } ユーザー(User)の作成 Elasticsearchのインデックスに対するアクセス制御(概要 ) のパターン1、パターン2 で示したようなユーザーを作成していきます。 作成するユーザーは、以下の4人です。 ユーザー名 保持するロール sales_user1 sales_user_role hr_user2 hr_user_role sales_and_hr_user3 sales_user_role, hr_user_role sales_and_hr_manager6 sales_and_hr_manager_role Dev Tools の Console から以下のリクエストを発行して、上記の4つのユーザーを作成します。(*2) 2 PUT /_security/user/sales_user1 { "password": "secret_password", "roles": ["sales_user_role"] } PUT /_security/user/hr_user2 { "password": "secret_password", "roles": ["hr_user_role"] } PUT /_security/user/sales_and_hr_user3 { "password": "secret_password", "roles": ["sales_user_role", "hr_user_role"] } PUT /_security/user/sales_and_hr_manager6 { "password": "secret_password", "roles": ["sales_and_hr_manager_role"] } “secret_password” については、適宜、適切なパスワードに置き換えてください。 インデックスの作成 Elasticsearchのインデックスに対するアクセス制御(概要 ) のパターン1、パターン2 で示したようなインデックスを作成していきます。 作成するインデックスは、下記の4つです。 インデックス名 説明 sales_data_2024 2024年分の営業データ sales_data_2025 2025年分の営業データ hr_data_2024 2024年分の人事データ hr_data_2025 2025年分の人事データ Dev Tools の Console から以下のリクエストを発行して、上記の4つのインデックスを作成します。 PUT /sales_data_2024 { "settings": { "index": { "number_of_shards": 1, "number_of_replicas": 1 } } } PUT /sales_data_2025 { "settings": { "index": { "number_of_shards": 1, "number_of_replicas": 1 } } } PUT /hr_data_2024 { "settings": { "index": { "number_of_shards": 1, "number_of_replicas": 1 } } } PUT /hr_data_2025 { "settings": { "index": { "number_of_shards": 1, "number_of_replicas": 1 } } } インデックスのフィールドの作成 さきほど作成したインデックスへフィールドを作成していきます。 今回は、あくまでもアクセス権の確認のためのインデックスなので、必要最低限のフィールドのみとし、形態素解析については省略します。 4つのインデックスそれぞれに、以下の2つのフィールドを作成します。 フィールド名 フィールドタイプ 説明 @timestamp date ドキュメントの登録日時 content text ドキュメントの内容 Dev Tools の Console から以下のリクエストを発行して、上記の2つのフィールドを作成します。 PUT /sales_data_2024/_mapping { "properties": { "@timestamp": { "type": "date" }, "content": { "type": "text" } } } PUT /sales_data_2025/_mapping { "properties": { "@timestamp": { "type": "date" }, "content": { "type": "text" } } } PUT /hr_data_2024/_mapping { "properties": { "@timestamp": { "type": "date" }, "content": { "type": "text" } } } PUT /hr_data_2025/_mapping { "properties": { "@timestamp": { "type": "date" }, "content": { "type": "text" } } } インデックスへのエイリアスの作成 sales_data_2024, sales_data_2025, hr_data_2024, hr_data_2025 という4つのインデックスを作成しました。 このままでも操作は可能ですが、さらに操作しやすくなるよう、インデックスに対するエイリアスを作成します。 作成するエイリアスは以下の2つです。 エイリアス 対象となるインデックス名 sales_data sales_data_* hr_data hr_data_* Dev Tools の Console から以下のリクエストを発行して、上記の2つのエイリアスを作成します。 POST _aliases { "actions": [ { "add": { "index": "sales_data_*", "alias": "sales_data" } }, { "add": { "index": "hr_data_*", "alias": "hr_data" } } ] } ※最初にロールを作成しました。そのロールの中で対象インデックスを “sales_data*”, “hr_data*” と指定しましたが、これには今回作成したエイリアスも含まれます。 “sales_data*” に対する read 権限、というのは、   – sales_data_2024 インデックスへの読み取り権限   – sales_data_2025 インデックスへの読み取り権限 に加えて、   – sales_data エイリアスに対する読み取り権限 があることを意味します。 ドキュメントの登録 インデックスを作成したので、次はインデックスへドキュメントを登録します。 sales_and_hr_manager6 ユーザーに sales_data* インデックス、および hr_data* インデックスへの all 権限を与えているので、動作確認も兼ねて sales_and_hr_manager6 ユーザーで Kibana にログインし、ドキュメントを登録していきます。 POST /sales_data_2024/_doc { "@timestamp": "2024-01-10", "content": "これは 2024-01-10の営業資料です。" } POST /sales_data_2025/_doc { "@timestamp": "2025-01-11", "content": "これは 2025-01-11の営業資料です。" } POST /hr_data_2024/_doc { "@timestamp": "2024-01-12", "content": "これは 2024-01-12の人事の資料です。" } POST /hr_data_2025/_doc { "@timestamp": "2025-01-13", "content": "これは 2025-01-13の人事の資料です。" } ドキュメントが問題なく登録されます。 ユーザーごとの動作確認 ドキュメントを登録したので、ユーザーごとに動作確認していきます。 1. sales_and_hr_manager6 ユーザー sales_and_hr_manager6 ユーザーでログインしているので、引き続き、検索も実行してみます。 GET /sales_data,hr_data/_search 登録した4件すべてのドキュメントがヒットします。 2. sales_user1 ユーザー 次に、sales_user1 ユーザーでログインしなおします。 sales_data インデックスに対する検索クエリーを発行してみます。 GET /sales_data/_search sales_data_2024, sales_data_2025 インデックスのドキュメントがヒットします。 次に hr_data インデックスに対する検索クエリーを発行してみます。 GET /hr_data/_search すると、hr_data* インデックスへの読み取り権が不足しているため、以下のようなエラーが返却されます。 { "error": { "root_cause": [ { "type": "security_exception", "reason": "action [indices:data/read/search] is unauthorized for user [sales_user1] with effective roles [sales_user_role] on indices [hr_data], this action is granted by the index privileges [read,all]" } ], "type": "security_exception", "reason": "action [indices:data/read/search] is unauthorized for user [sales_user1] with effective roles [sales_user_role] on indices [hr_data], this action is granted by the index privileges [read,all]" }, "status": 403 } ためしに、sales_data_2025 インデックスへのドキュメントの登録リクエストを発行してみます。 POST /sales_data_2025/_doc { "@timestamp": "2025-06-13", "content": "これは 2025-06-13の営業資料です。" } sales_data* インデックスへの書き込み権がないので、以下のようなエラーとなります。 { "error": { "root_cause": [ { "type": "security_exception", "reason": "action [indices:data/write/index] is unauthorized for user [sales_user1] with effective roles [sales_user_role] on indices [sales_data_2025], this action is granted by the index privileges [create_doc,create,index,write,all]" } ], "type": "security_exception", "reason": "action [indices:data/write/index] is unauthorized for user [sales_user1] with effective roles [sales_user_role] on indices [sales_data_2025], this action is granted by the index privileges [create_doc,create,index,write,all]" }, "status": 403 } 3. hr_user2 ユーザー 同様に、hr_user2 ユーザーでログインし、検索リクエストを発行してみます。 GET /sales_data/_search sales_data* インデックスへの読み取り権がないため、エラーとなります。 GET /hr_data/_search こちらは、hr_data_2024 インデックス、hr_data_2025 インデックス 内のドキュメントがヒットします。 ためしに、hr_data_2025 インデックスへのドキュメントの登録リクエストを発行してみます。 POST /hr_data_2025/_doc {   "@timestamp": "2025-06-13",   "content": "これは 2025-06-13の人事の資料です。" } hr_data* インデックスへの書き込み権がないので、エラーとなります。 4. sales_and_hr_user3 ユーザー 最後に sales_and_hr_user3 ユーザーでログインし、検索リクエストを発行してみます。 GET /sales_data,hr_data/_search sales_data_2024, sales_data_2025, hr_data_2024, hr_data_2025 インデックスのドキュメントすべてがヒットします。 ためしに、ドキュメントの登録リクエストを発行してみます。 POST /sales_data_2025/_doc"@timestamp": "2025-06-13", "content": "これは 2025-06-13の営業資料です。" } POST /hr_data_2025/_doc { "@timestamp": "2025-06-13", "content": "これは 2025-06-13の人事の資料です。" } sales_data* インデックス、hr_data* インデックス、ともに書き込み権がないため、いずれもエラーとなります。 以上の4つのユーザーでの動作をまとめると以下の表になります。 ユーザー sales_data* インデックスへの操作 hr_data* インデックスへの操作 sales_user1 〇読み取り可 ×読み書き不可 hr_user2 ×読み書き不可 〇読み取り可 sales_and_hr_user3 〇読み取り可 〇読み取り可 sales_and_hr_manager6 ◎読み書き可 ◎読み書き可 まとめ このように、ロールベースのアクセス制御を活用することにより、インデックスごとのアクセス制御ができることが確かめられました。 実際の現場でも要件に合わせて、ロールを適切に設定することで、情報漏洩やデータの破損などのリスクを軽減できることをおわかりいただけたかと思います。 API からリクエストを発行する方法以外に、Kibana からロールを作成することも可能です。 このリクエスト内には、インデックスへの操作以外に、applications などの指定も行っています。 厳密には、これらのアクセス権についても、きちんと考慮する必要がありますが、 今回はインデックスへのアクセス権を確かめることを目的としているので、これらについては深く掘り下げません。 ↩︎ API からリクエストを発行する方法以外に、Kibana からユーザーを作成することも可能です。 ↩︎ The post Elasticsearchのインデックスに対するアクセス制御(Kibana上での動作確認) first appeared on Elastic Portal .
アバター
目次 はじめに 対象者 なぜインデックスレベルのアクセス制御が必要なのか? Elasticsearchにおけるインデックスアクセス制御の仕組み 1. ロール (Role) の定義 2. ユーザー (User) の作成とロールの割り当て 主要な権限(Privileges)の種類 まとめ はじめに Elasticsearchは、その柔軟性とスケーラビリティから、多種多様なデータを扱うための中心的なプラットフォームとして多くの企業で利用されています。 しかし、データ量が増え、利用者が多様化するにつれて、「誰がどのデータにアクセスできるのか」というセキュリティの課題が極めて重要になります。 今回は、Elasticsearchにおけるインデックスに対するアクセス制御の概要について説明します。 これは、データのセキュリティを確保するための最も基本的な、しかし非常に強力な機能です。 対象者 Elasticsearch : 初級者~中級者 License : Basic 以上 Version : 筆者は、8.15 で確認 なぜインデックスレベルのアクセス制御が必要なのか? Elasticsearchは、データを「インデックス」という単位で管理します。 このインデックスは、リレーショナルデータベースにおける「テーブル」のようなものと考えることができます。 複数のインデックスが存在する場合、それぞれのインデックスには異なる種類のデータや、異なる機密レベルのデータが含まれていることがほとんどです。 例えば、以下のようなケースが考えられます。 部門ごとのデータ 営業部門のデータ 人事部門のデータ 開発部門のデータ これらがそれぞれ異なるインデックスに格納されているとします。 このような状況で、全てのユーザーが全てのインデックスにアクセスできるのは望ましくありません。 誤操作によるデータ破壊や、不正な情報漏洩のリスクを最小限に抑えるためには、適切なアクセス制御が不可欠です。 Elasticsearchにおけるインデックスアクセス制御の仕組み Elasticsearchでは、主にロールベースのアクセス制御 (RBAC = Role bases access control) を用いて、インデックスに対するアクセスを管理します。 1. ロール (Role) の定義 ロールは、特定の権限の集合体です。例えば、「読み取り専用ユーザーロール」「管理者ロール」「人事部門データ閲覧ロール」など、業務や役割に基づいてロールを定義します。 ロール定義の際に、どのインデックスに対して、どのような操作(読み取り、書き込み、管理など)を許可するかを指定します。 PUT /_security/role/sales_user_role { "indices": [ { "names": ["sales_data*"], # sales_dataで始まる全てのインデックス(複数指定可) "privileges": ["read", "view_index_metadata"] # 読み取りとインデックスメタデータの閲覧を許可(複数指定可) } ], ... } 上記の例では、sales_user_role というロールを持つユーザーは、sales_data で始まる全てのインデックスに対して「読み取り」と「インデックスメタデータの閲覧」のみが許可されます。(*1) 1 2. ユーザー (User) の作成とロールの割り当て 次に、実際にElasticsearchにアクセスするユーザーを作成し、作成したロールをそのユーザーに割り当てます。 POST /_security/user/sales_user1 { "password": "your_secure_password", "roles": ["sales_user_role"] # ロールを割り当て(複数指定可) } sales_user1は、sales_user_role の権限を持つことになります。(*2) 2 ロールとユーザーとインデックスの関係を表した概念図を以下に記載します。 ロールとユーザーとンデックスの関係を図式化した例を2つ下記に挙げます。 ロールとユーザーの関係図(パターン1) sales_user1 は、sales_user_role を持ちます。sales_user_role があるため、sales_data* のインデックスの読み取りが可能です。 hr_user2 は hr_user_role を持ちます。hr_user_role があるため、hr_data* のインデックスの読み取りが可能です。 sales_and_hr_user3 は、sales_user_role と hr_user_role を持ちます。sales_user_role と hr_user_role があるため、sales_data* および hr_data* インデックスの読み取りが可能です。 2. ロールとユーザーの関係図(パターン2) sales_user4 は、sales_user_role を持ちます。sales_user_role があるため、sales_data* のインデックスの読み取りが可能です。 hr_user5 は hr_user_role を持ちます。hr_user_role があるため、hr_data* のインデックスの読み取りが可能です。 sales_and_hr_manager6 は、hr_and_sales_manager_role を持ちます。hr_and_sales_manager_role があるため、sales_data* および hr_data* インデックスへの読み書きが可能です。 主要な権限(Privileges)の種類 インデックスに対して割り当てられる主な権限には、以下のようなものがあります。(*3) 3 read: ドキュメントの検索、取得を許可します。 write: ドキュメントの追加、更新、削除を許可します。 delete: ドキュメントの削除を許可します。 create_index: 新しいインデックスの作成を許可します。 manage: インデックス設定の変更、エイリアスの管理など、インデックスレベルの管理操作を許可します。 all: そのインデックスに対する全ての操作を許可します(管理者権限)。 view_index_metadata: インデックスのメタデータ(マッピングなど)の閲覧を許可します。 これらの権限を適切に組み合わせることで、きめ細やかなアクセス制御を実現できます。 アクセス制御におけるベストプラクティス 最小権限の原則 (Principle of Least Privilege): ユーザーには、その業務を遂行するために必要最小限の権限のみを付与するべきです。安易に all 権限や広範なインデックスへのアクセスを許可しないようにしましょう。 明確なロールの定義: ロールは、そのロールが持つべき権限を明確に反映するように命名し、定義します。 定期的なレビュー: アクセス制御の設定は、組織の変更やデータ要件の変更に合わせて定期的にレビューし、必要に応じて更新することが重要です。 ワイルドカードの活用: sales_data* のようにワイルドカードを使用することで、将来的に追加される類似のインデックスに対しても自動的にアクセス制御を適用できます。 X-Packセキュリティの活用: これらのアクセス制御機能は、ElasticsearchのX-Packセキュリティ(有償ライセンスが必要)によって提供されます。本番環境での利用には必須の機能と言えるでしょう。 まとめ Elasticsearchにおけるインデックスに対するアクセス制御は、データのセキュリティと整合性を保つ上で非常に重要です。 ロールベースのアクセス制御を適切に設計・適用することで、不正アクセスやデータ漏洩のリスクを大幅に軽減し、より安全なデータプラットフォームを構築することができます。 Elasticsearchでデータを扱う際には、アクセス制御の設計と実装を最優先事項の一つとして考慮に入れるようにしましょう。 次回は、実際の動きを交えて説明します。 例示した内容以外にも、様々な設定を行うことが可能です。 詳細は、公式ドキュメントを参照してください。 https://www.elastic.co/docs/deploy-manage/users-roles/cluster-or-deployment-auth/user-roles また、API を発行する以外にも、Kibana からロールを作成することも可能です。 https://www.elastic.co/docs/deploy-manage/users-roles/cluster-or-deployment-auth/kibana-role-management ↩︎ 例示した内容以外にも、様々な設定を行うことが可能です。 詳細は、公式ドキュメントを参照してください。 https://www.elastic.co/docs/api/doc/elasticsearch/operation/operation-security-get-user また、API を発行する以外にも、Kibana からユーザーを作成することも可能です。 https://www.elastic.co/docs/deploy-manage/users-roles/cluster-or-deployment-auth/quickstart#_create_a_user ↩︎ ここに示した以外にも、様々な Privilege が用意されています。 詳細は、公式ドキュメントを参照してください。 https://www.elastic.co/docs/deploy-manage/users-roles/cluster-or-deployment-auth/elasticsearch-privileges#privileges-list-indices ↩︎ The post Elasticsearchのインデックスに対するアクセス制御(概要) first appeared on Elastic Portal .
アバター
ElasticsearchとKibana Mapsを使い、東京都犯罪データの位置情報をローコードで可視化する方法を解説します。また、Logstashによる効率的なデータ取り込み手順も紹介します。 目次 なぜ位置情報が重要なのか Elastic Stackによる位置情報可視化の概要 Elastic Mapsとは データ準備 リアルタイムデータ取り込み: Python vs Logstashによるアプローチ Pythonによるデータ投入 Logstashによるデータ取り込み(ローコード実装) Kibana Mapsでの可視化 ベースマップの追加 データレイヤー(ドキュメント)の追加 クラスタレイヤーの追加とタイムスライダーの活用 ダッシュボードでの活用とフィルタリング スケーラビリティのポイント セキュリティの考慮事項 まとめ 使用環境・ツールバージョン なぜ位置情報が重要なのか 位置情報は単なる点の座標データに留まりません。リアルタイム分析と組み合わせることで、以下のような価値あるインサイトが生まれます。 交通・MaaS : リアルタイムな混雑状況の把握や最適ルートの提案により、移動の効率化と利用者満足度の向上が可能。 物流 : 配送ルートの最適化や燃料コスト削減など、輸送効率を最大化。トラックや荷物の リアルタイムトレーシング により遅延やロスを迅速に検知できます。 AIデバイス・セキュリティ :  リアルタイムトレーシング によるデバイスや人の動態監視で異常検知や侵入警告を実現。設備の稼働状況を地図上で把握し、効率的な管理やメンテナンス計画に役立てられます。 このように、位置情報×リアルタイム分析は様々な業界で意思決定を支える重要な手法となっています。 Elastic Stackによる位置情報可視化の概要 Elastic Stackは、統合的なデータ収集・分析基盤です。本ブログで用いる主要コンポーネントは次のとおりです。 Elasticsearch : スケーラブルで高速な検索・分析エンジン。位置情報を含む大規模データの格納と集計に適しています。 Kibana : Elasticsearch上のデータを可視化・分析するためのダッシュボードツール。地理空間データ向けのMaps機能を備え、GUI操作で多彩なビジュアル化が可能です。 Logstash : 多様なデータソースからのデータ収集・変換・送信を行うパイプラインツール。CSVなど構造化データの読み込みやリアルタイムデータ取り込みに適しており、シンプルな設定でElasticsearchへのデータ投入を自動化できます。 これらを組み合わせることで、大量の位置情報データをリアルタイムに取り込みつつ(Logstash)、それを高速検索エンジン(Elasticsearch)に蓄積し、最後にダッシュボード(Kibana)で地図可視化するという一連の流れが実現できます。 Elastic Mapsとは Elastic MapsはKibanaに統合された地理空間データ可視化ツールです。Elasticsearchに格納された位置情報をインタラクティブな地図上にポイントやシェイプ、ヒートマップなどの形式で表示し、直感的な分析を可能にします。主な特徴をまとめると: 地理空間分析 : IPアドレス由来の位置、GPS座標、行政区画ポリゴンなど、多様な地理情報を可視化できる。 インタラクティブマップ : マップ上でのズーム・パン操作やフィルタリングにより、興味深いデータポイントを動的に探索可能。 レイヤー機能 : 複数のデータレイヤーを重ねて表示し、多角的な分析を実現(例: ポイントデータ+エリア境界+ヒートマップを同一マップ上に重ねる)。 リアルタイム更新 : データがElasticsearchに追加・更新されると、マップ表示にも即時反映されるリアルタイム連携。 カスタマイズ性 : ポイントの色やシンボル、サイズ、ラベル表示などを柔軟に設定可能。用途に応じてマップの見せ方を調整できます。 Elastic Mapsは、ネットワーク監視や物流管理、地理的なトレンド分析など幅広い用途で活用されています。利用するには、Elasticsearchのインデックスに地理情報フィールド( geo_point 型または geo_shape 型)が含まれていることと、Kibana上で当該インデックスにアクセスできることが必要です(※Elasticsearchの動的マッピングでは geo_point を自動認識しないため、後述するように手動でマッピング定義を行います)。 データ準備 今回は、東京都のオープンデータである 犯罪認知数のデータ と、町丁目レベルの 緯度・経度参照データ を使用します。前者には東京都内の各区市町村・町丁目ごとの犯罪件数(罪種別・手口別)が含まれており、後者は各町丁目の代表地点の座標情報です。 データは以下のサイトから取得しました。 区市町村の町丁別、罪種別及び手口別認知件数: https://www.keishicho.metro.tokyo.lg.jp/about_mpd/jokyo_tokei/jokyo/ninchikensu.html 国土数値情報ダウンロードサイト: https://nlftp.mlit.go.jp/cgi-bin/isj/dls/_choose_method.cgi 取得した複数年分(2019年~2024年)のCSVを統合し、分析に適した形に前処理します。具体的には「区」「町丁目」を識別子とし、各罪種別・手口の件数を ワイド形式 (各カテゴリが列)から ロング形式 (各カテゴリが行レコード)へ変換しました。これにより、「区」「町丁」「緯度」「経度」「犯罪種別」「件数」「年」といった列を持つ一つの大きなデータセットを構築しています。件数0のレコードは分析上不要なため除去しました。最終的に約数万行規模のデータ(long_format_crime_data.csv)となりますが、目的はElasticsearchによる地図可視化機能の紹介であり、個々の犯罪傾向の考察は本稿の範囲外とします。 long_format_crime_data ダウンロード リアルタイムデータ取り込み: Python vs Logstashによるアプローチ 大量のCSVデータをElasticsearchに投入する方法として、まずはシンプルにPythonスクリプトを用いる方法と、Elastic Stack標準のLogstashを用いる方法の2つを試しました。それぞれのアプローチについて、実装手順と所感を比較します。 Pythonによるデータ投入 Pythonを使った方法では、 pandas ライブラリでCSVを読み込み前処理した後、ElasticsearchのPythonクライアント(またはREST API)でデータを登録するという手順を取りました。例えば、前述のワイド→ロング変換には以下のようなコードを使用しています。 import pandas as pd # CSV複数ファイルを読み込み結合(2019-2023年分) csv_files = ["crime_2019.csv", "crime_2020.csv", ...] # 各年のCSVファイル名 df_all = pd.concat([pd.read_csv(f) for f in csv_files], ignore_index=True) # ワイド形式をロング形式に変換 df_long = df_all.melt(id_vars=["区", "町丁", "緯度", "経度", "Year"], var_name="CrimeType", value_name="Count") df_long = df_long[df_long["Count"].fillna(0).astype(int) > 0] # 件数0行を除去 変換後の df_long データフレームを1行ずつElasticsearchにインデックスしていきます。シンプルな実装としては以下のようになります。 from elasticsearch import Elasticsearch es = Elasticsearch("http://localhost:9200") # Elasticsearchクライアント for _, row in df_long.iterrows(): doc = { "区": row["区"], "町丁": row["町丁"], "緯度": row["緯度"], "経度": row["経度"], "CrimeType": row["CrimeType"], "Count": int(row["Count"]), "Year": row["Year"], "location": [float(row["経度"]), float(row["緯度"])] # 経度・緯度からgeo_point用配列生成 } es.index(index="all_crimes", document=doc) 上記では各レコードを逐次投入していますが、データ件数が多い場合はかなり時間を要します(数万件規模で数分以上)。Pythonで細かな変換やロジックを実装できる柔軟性は魅力ですが、一件ずつの投入は非効率であり、リアルタイムデータ取り込みには適しません。大量データやストリーミングデータの取り込みには、次に述べるようにElastic Stackのツールを活用する方が効率的です。 Logstashによるデータ取り込み(ローコード実装) Logstashを用いると、煩雑なコードを書くことなく ローコード で大量データの取り込みパイプラインを構築できます。Logstashの設定ファイル(*.conf)にデータソースや処理内容を記述するだけで、CSV読み込みからElasticsearch送信までを自動化してくれます。Python実装と比較すると、大規模データでは 圧倒的に高速 であることが検証できました。 以下に、本稿で使用したLogstash設定ファイルの例を示します。(CSVファイルパスやフィールド名は環境に合わせて適宜読み替えてください。) Logstashの実行方法 bin/logstash -f logstash_crime.conf ※ Elastic Stack を zip からインストールした場合、 bin/logstash  がコマンド。Homebrewなら  logstash -f ... 上記のLogstash設定では、CSVフィルタで各列をパースし、日本語のフィールド名「緯度」「経度」を location_data というオブジェクト配下に整理しています。続いてRubyフィルタで location という geo_point 形式の配列フィールドを生成し(経度・緯度の順に配置)、さらに Year から YearDate という date 型フィールドを作成しています。最後にElasticsearch出力では、作成済みのインデックス all_crimes にデータを書き込みつつ、 pipeline オプションでIngest Pipeline(Elasticsearch側のデータ投入時処理)を指定しています。この remove_raw_fields パイプラインは、CSV読み込み時に付与される生のメッセージフィールド( message や @version 等)を削除するためのものです。 PUT _ingest/pipeline/remove_raw_fields { "processors": [ { "remove": { "field": "message" }}, { "remove": { "field": "event.original" }} ] } マッピングの設定:  Logstashでデータ投入を行う前に、Elasticsearch側でインデックスのマッピングを用意しておくことが重要です。とくに location や YearDate のように、デフォルトの動的マッピングでは適切な型(geo_pointやdate)にならないフィールドは、あらかじめ明示的に型指定してインデックスを作成します。開発者ツール(Dev Tools)上で以下のようなコマンドを実行し、インデックス all_crimes を作成しました。 PUT all_crimes { "mappings": { "properties": { "location": { "type": "geo_point" }, "YearDate": { "type": "date", "format": "strict_year" }, "CrimeType": { "type": "keyword" }, "CrimeMethod": { "type": "keyword" }, "Count": { "type": "integer" } } } } 以上の設定により、Pythonスクリプトを用いた場合と比べて格段に高速に(体感で数十倍以上)データを投入できました。CSV数万件程度であれば数十秒~1分程度で完了し、Elasticsearchのインデックスにドキュメントが登録されます。Logstashは設定ファイルさえ用意すれば、リアルタイムにファイルを監視して継続的に取り込むことも可能であり、本番環境でも耐えうる柔軟かつ強力なデータ取り込み基盤となります。 Kibana Mapsでの可視化 データの準備と投入が完了したら、いよいよKibanaのMaps機能を使って位置情報データを可視化してみましょう。ここでは、Elasticsearchにロードした all_crimes インデックス上の地理データを地図にプロットし、さらに集計表示や時系列による変化も確認できるダッシュボードを作成します。 ※ 注意:Kibana上での可視化には、対応するData Viewの作成が必要です。 Kibanaは「どのインデックスに、どんな構造のデータがあるか」を認識するために、このData Viewを参照します。忘れずにインデックスに対応するData View を作成しておきましょう。 ベースマップの追加 最初に地図の土台となるベースマップを追加します。Kibanaのナビゲーションメニューから「Maps」 を開き、 「Create map」ボタンをクリックしてください。その後、以下の手順でベースマップレイヤーを設定します。 「Add layer」 → 「EMS Basemaps」 お好みのスタイルを選択しましょう(例えば視認性の高い”Light”スタイルなど) → 「Add and continue」 必要に応じてベースマップの表示設定を調整します。今回は地図をはっきり表示するため「Opacity(不透明度)」を100%に設定し、「Label language」を日本語に変更しました。 「Keep changes」をクリックします。 以上で背景地図となるベースマップが追加されます。選択したスタイルによる白地図が表示され、これからプロットするデータの土台として機能します。 上の図は「Light」スタイルのベースマップを適用した初期状態の画面です。日本全国の地形や道路が薄灰色で描画されており、この上にデータポイントを重ねていきます。ベースマップにはElastic社提供のタイルサービス(EMS)が利用されていますが、自前の地図タイルやシェイプデータを用意して追加することも可能です。 用意している場合はUpload fileからmergeをする必要です。 データレイヤー(ドキュメント)の追加 続いて、Elasticsearchに投入した位置情報データ(犯罪データ)のレイヤーを地図に重ねます。ベースマップと同様に「Add layer」から操作します。手順は次のとおりです。 「Add layer」 → 「Documents」 (Elasticsearchに保存されたドキュメントを直接プロットするレイヤーです) 「Data view」 → 可視化したいものを選択 「Geospatial field」 → 自動的に location が選択されている 「Add and continue」 → 地図上にデータポイントがプロットされ始めます 必要に応じて表示範囲(ズームレベル)を制限できます。例えば本ケースでは東京都付近を詳細表示したいため、「Visibility」を [9, 24] に設定しました。 ポイントを選択した際に表示される詳細情報「Tooltip fields」を設定します。表示したいフィールド(例:  CrimeType 、 Year 、 区 など)を追加します。これにより各ポイントにマウスオーバーすると、選択した項目の値が吹き出しで表示されます。 「Layer style」の項目ではデータポイントの見た目をカスタマイズできます。「Fill color」を適当なカラーグラデーションに設定し(件数に応じて色が変わるよう設定も可能)、「Symbol size」を「By value」に変更して Count (件数)フィールドを指定します。これで犯罪件数が多い地点ほどシンボルが大きく描画されるようになります。必要であれば「Label」欄に Count を指定し、各ポイント上に件数ラベルを直接表示させることもできます。 「Keep changes」をクリックします。 以上の設定により、地図上に東京都内の各地点がプロットされ、それぞれの点について件数や犯罪種別などの情報を確認できるようになります。ポイントの大小や色で事件数の多寡がひと目で分かり、マウスオーバーで詳細な内訳も把握できます。 上図は all_crimes インデックスのドキュメントをポイントレイヤーとして地図にプロットした結果です。町丁目単位の地点ごとに犯罪件数(Count)に応じた大小の青い円で表示しています。また各円内に件数ラベルを表示しているため、密集地帯でもおおよその数値を読み取ることができます(このラベル表示は任意設定です)。地図を拡大縮小したりドラッグしたりすることで、関心エリアを詳細に調べられます。 クラスタレイヤーの追加とタイムスライダーの活用 個々のポイントが多くプロットされる場合、 クラスタリング 機能を使うと可視化が見やすくなります。Kibana Mapsではドキュメントレイヤーとは別にクラスタレイヤーを追加することで、ポイントの密集地を自動的にグルーピングして表示可能です。また時系列データであれば タイムスライダー を使って時間変化をアニメーション再生することもできます。ここでは、件数データの集約表示とタイムスライダーについて説明します。 クラスタレイヤーの追加手順: 「Add layer」→ 「Clusters」 対象Data viewを指定→ 「Add and continue」 「Show as」で集計の形式を選択します。 「Clusters」 (円形のクラスター表示)か「 Grids」 (格子状のグリッド集計)または 「Hexagons」 (六角形グリッド)から選べます。今回はClustersを選択しました。 「Metrics」→ Aggregation → sum → Fieldに Count (「Add metrics」で複数集計も可能) 「Cluster size」(Resolution)でクラスタリングの細かさを調整できます。値を高くすると細かいグリッドでクラスタリングされ、値を低くすると大まかな範囲でまとめられます。デフォルト設定で問題なければそのままで構いません。 「Keep changes」をクリックします。 クラスタレイヤーを適用すると、ポイントが密集しているエリアでは一つの大きな円でまとめて表示されるようになります。円の中の数字はそのクラスタ内の件数合計(今回設定ではCrime件数の総和)を表します。これにより、例えば23区内のどの地域に犯罪が多発しているかを一望でき、分布の傾向を把握しやすくなります。また、ズームインすれば自動的にクラスターが細分化され、ズームアウトすれば再度まとまるといった具合に、可視化の粒度が動的に変化します。 2020年:侵入窃盗忍込み件数 2021年:侵入窃盗忍込み件数 さらに、 タイムスライダー 機能を使うと時間経過に沿ったデータの変化をアニメーション表示できます。今回のデータは年単位の集計値ですが、タイムスライダーを有効にすると地図下部に再生バーが表示され、時系列でデータの増減を確認できます。画面左の時計アイコン(緑の矢印で示したボタン)をクリックするとスライダーが表示され、再生ボタンでアニメーション開始です。 今回のデータは年単位のためスライダーは年刻みとなり、月単位の細かな変化は表現できません(※データに月・日情報があればスライダー設定を変更することで日付単位での再生も可能です)。タイムスライダーを停止した状態で特定の年を選択すれば、その時点のクラスタ分布を静的に分析することもできます。 ダッシュボードでの活用とフィルタリング 作成した地図はKibanaのダッシュボードに組み込んで活用できます。Maps画面右上の「Save」ボタンから地図を保存し、適宜タイトルを付けて保存します。保存時に既存のダッシュボードに追加するか、新規ダッシュボードを作成するオプションがありますので、「Save and add to dashboard」を選択するとよいでしょう。別の場所でこの図を使用する場合は、「Add to library」にチェックを入れましょう。(ライブラリに追加された要素は「Visualize Library」で確認できます。) ダッシュボードでは複数の可視化(グラフや地図)を一画面に配置して総合的に分析できます。Kibanaダッシュボードの強力な点は、 フィルタリングが連動 することです。あるパネルで範囲を絞り込むと、同じダッシュボード上の他の全パネルに一括でそのフィルターが適用されます。これにより、例えば地図上で特定の地域を選択すると、他のグラフやテーブルもその地域のデータにリアルタイムに絞り込まれるため、関連性のある指標を横断的に分析することが可能です。 もう一つの便利な機能が シェイプでの領域フィルター です。地図パネル上で任意の図形を描画し、その範囲内のデータだけにダッシュボード全体をフィルタリングできます。使い方は以下のとおりです。 「Tools」→ 「Draw shape to filter data」 任意の図形を描画。 すると、その描画した図形(ポリゴン)に該当するエリア内のデータだけがフィルターされます。他のグラフパネルも同時にそのフィルター条件が適用された状態に更新されます。 たとえば東京23区西部を囲む長方形を描けば、その範囲に含まれる町丁の犯罪件数データのみが地図および関連グラフに反映されます。フィルター適用中は画面上部に「intersects shape」というフィルターラベルが表示されます。解除したい場合はそのラベルの✖ボタンをクリックすれば元の全データ表示に戻ります。 さらに、ダッシュボードでは地図パネル以外にも通常のフィルターバーやクエリバーが使用可能です。右側のフィルタリングメニューや上部の検索バーにKQL(Kibana Query Language)を書いてフィルターを適用することもできます。( 例えば、 区 :"世田谷区" and CrimeType : "侵入窃盗(空き巣)" and YearDate >= "2020"  といったクエリを投入すれば、「世田谷区で発生した侵入窃盗(空き巣)が2020年以降に限定」した条件で全パネルを絞り込めます。 ) 最後に、Kibana Mapsの高度な機能としてES|QLを使用したレイヤー追加も挙げられます。今回の例では使用しませんでしたが、Add layer時に「Documents」ではなく「ES|QL」を選ぶことで、Elasticsearchに対してSQLライクなクエリを発行し、その結果を地図レイヤーとして可視化することも可能です。 FROM all_crimes | WHERE 区 IN (“新宿区”, “渋谷区”, “世田谷区”, “港区”, “千代田区”) | KEEP location, crime_type, count, 区 , 町丁 | LIMIT 10000 大量データの集約結果やサブクエリを用いた特殊な可視化を行いたい上級者向けの機能ですが、状況に応じて活用すると分析の幅がさらに広がるでしょう。 スケーラビリティのポイント 位置情報データを継続的に扱うシステムを設計する際には、スケーラビリティ(可搬性・拡張性)の確保が重要です。以下に、本記事の内容を実運用に展開する際に考慮すべきポイントをまとめます。 データ投入パフォーマンス:  今回Logstash経由で高速化を図ったように、より大規模なデータではElasticsearchの Bulk API を活用して一括投入することで効率を上げられます。 インデックス設計:  データ量が膨大になる場合、一つのインデックスに集約しすぎると検索・集計性能が低下します。適切に 時間単位でインデックスを分割 したり(例えば年ごと・月ごとの名前で分割)、Elasticsearchのロールオーバー機能やILM(Index Lifecycle Management)ポリシーを導入して古いデータをアーカイブするといった戦略が有効です。 クラスタリング表示の活用:  Kibana Mapsで非常に多数のポイントを表示するとブラウザ描画が重くなる可能性があります。今回使用した クラスタレイヤー やグリッド表示を活用し、高ズームアウト時には集約表示、ズームイン時に詳細表示とすることでパフォーマンスと可読性を両立できます。 セキュリティの考慮事項 位置情報データはその性質上、個人や組織の行動範囲を含むセンシティブな情報を含む場合があります。Elastic Stackを用いてこれらを可視化・分析する際には、以下のセキュリティ面での対策・配慮が重要です。 アクセス制御:  KibanaダッシュボードやMapsアプリへのアクセスは、Elastic認証機能を利用して適切な権限設定を行いましょう。例えば社内向けポータルとして運用する場合、閲覧できるインデックスやダッシュボードをユーザロールごとに制限し、不要なデータ露出を防ぎます。 – Kibana → 管理 → 「Roles」機能で権限設定 – Userごとに「read」「write」「view_index_metadata」などを設定 プライバシー保護:  個人を特定できる粒度の位置情報(GPS座標や詳細住所など)を扱う際は、必要に応じて匿名化(ジオフェンスで丸める、IDをハッシュ化する等)することを検討してください。 まとめ ElasticsearchとKibana Mapsを活用することで、リアルタイムかつ直感的に位置情報データを可視化・分析できることを見てきました。 リアルタイムデータ取り込み においてはLogstashが大規模データにおいて優れたパフォーマンスを示し、コードを書く手間を少なくしてスピーディーにデータ基盤を構築できる点が確認できました。Kibana Maps上ではポイント表示からクラスタリング、時系列アニメーション、ダッシュボード統合まで豊富な機能をローコードで実現でき、ビジネス課題に応じた柔軟な地理空間分析が可能です。 本ブログでは機能紹介に留まりましたが、実際のユースケースに合わせて応用すれば可能性は非常に広がります。例えば、機械学習を組み合わせて異常な移動パターンを検知したり、ベクター検索技術と統合して位置情報と他の高次元データを関連付けることも考えられます。 位置情報の高速可視化は、今後ますます重要になるリアルタイムデータ活用の一領域です。ぜひ皆さんのプロジェクトでもElasticsearchとKibana Mapsを活用し、地理空間データから新たな価値を引き出してみてください。 使用環境・ツールバージョン 本記事の検証・実装に使用した主なツールのバージョンは以下のとおりです: ツール バージョン Elasticsearch 8.17.3 Kibana 8.17.3 Logstash 8.17.3 Python 3.9.18 ※バージョンによってUIや挙動に差異がある場合があります。あらかじめご了承ください。 The post Kibana MapsとElasticsearchで学ぶ位置情報の可視化手法 first appeared on Elastic Portal .
アバター
ECサイトの売上データをもっと活用したいけれど、「SQLでは集計が遅い」「全文検索を使った分析は難しい」「BIツールでは柔軟性に欠ける」と感じたことはありませんか? そんな課題を一気に解決するのが、 Elasticsearch × Kibana Lens  です。 本記事では、CSV をドラッグ&ドロップするだけで、 高速かつ柔軟に購買データを可視化・分析 する手順を、サンプルデータと実例つきでわかりやすく紹介します。 目次 記事のポイント 1. SQLと何が違う?Elasticの強みを整理 2. 使うデータと分析の目的 3. インポートと整形 4. Dev Tools上のクエリ例:分析テンプレート集 分析例: 5. Kibana Lensでの可視化 基本的な操作方法 例)性別ごとの売上割合 Create a Runtime Field  6. Elasticが持つAI × 統合分析力 7. まとめ & 次のステップ 記事のポイント 全文検索 × 集計 × 可視化 を 1 スタックで完結 SQL 利用者でも理解できる “発想の違い” を表形式で解説 CSV をドラッグ&ドロップするだけで Kibana Lens ダッシュボードを作成 高額購入者セグメント化/都道府県別売上ランキング など具体例付き コード & サンプルデータ一式があり、追って実装が可能 対象読者 EC サイトの売上データをもっと活用したいデータアナリスト Elasticsearch は聞いたことあるけれど “全文検索の人” というイメージで止まっている方 従来は SQL+BI ダッシュボードで頑張っているが、レスポンス速度に限界を感じている方 1. SQLと何が違う?Elasticの強みを整理 観点 Elasticsearch (+ Kibana) SQL スケーラビリティ シャード&ノード追加で水平分散 手動での分割が必要 全文検索 形態素解析 (kuromoji) で日本語も高精度 LIKE ‘%○○%’ でフルスキャン 集計速度 列指向インメモリ計算で TB 級でも数百 ms 行指向 RDB は集計件数に比例して遅延 可視化 Kibana Lens を同梱(ノーコード) 外部 BI ツール必須 リアルタイム性 数秒でインデックス反映 ETL バッチが前提 「SQL が不要」という意味ではなく、“検索・集計・可視化をワンストップで済ませたい” 場面で優位性があります。 2. 使うデータと分析の目的 randomized_customer_data.csv 年齢・性別・地域・商品・購入金額・注文日を含む合成データ 3,000 行 分析シナリオ 顧客セグメント(年齢 × 性別 × 地域) 高額購入者の抽出と特徴把握 都道府県別売上ランキング 商品カテゴリ別トレンド randomized_customer_data ダウンロード 3. インポートと整形 ローカルにインストールしたElasticsearch(本記事ではバージョン8.17.3 を使用)とKibanaを使用し、Pythonは使わずに手動で進めます。 Kibanaの画面上部にある検索バーから「file upload」と入力すると、ファイルアップロード機能が見つかります。Kibanaには多くの機能がありますが、検索画面の利用が最も迅速です。 そこから入ると以下の画面が表示されます アップできるファイルの大きさにリミットがあるので注意してください。 サンプルデータをドラッグ&ドロップ。 ファイルの中身をさらっと確認できます。「import」をクリックして次の画面に行きます。アップロード可能なファイルサイズには制限があります。サンプルデータをドラッグ&ドロップしてください。「import」をクリックすると、ファイルの内容を簡単に確認し、次の画面に進めます。 インデックス名を決定後に「import」をクリックしても良いですが、今回は「Advanced」タブを選択します。「create data view」に自動的にチェックが入っている点に注意してください。 Advancedタブでは、マッピングやIngest pipelineの変更が可能です。今回は3つのデータを削除したいので、Ingest pipelineのremoveで指定します。自動的に不要と判断されたmessageに加えて、firstnameとlastnameも削除対象に追加します。これらはリスト形式で指定します。 // 変更前:Ingest pipeline "remove": { "field": "message" } // 変更後:Ingest pipeline "remove": { "field": ["message", "firstname", "lastname"] } 資料をインポートする際は、まず「import」をクリックしてください。処理が完了すると、「File processed」から「Data view created」にチェックマークがつきます。 もし処理がうまくいかず❌印が表示されます。最後までチェックマークが付いていても、一部データが読み込めなかったというアラートが出る場合もありますので、再試行を推奨します。最後までチェックマークがついたら、インデックス作成は完了しているため、再インデックスを行う前に既存のインデックスを削除する必要があります。 特に注意が必要なのは、インポート時に「create data view」にチェックを入れていた場合、インデックスだけでなくdata viewの削除も必須となる点です。 作成したインデックスが正しく作成されたか確認するには、左側のメニューから「Managment」>「Stack Management」>「Index Mangment」>「Indices」タブを開き、作成したインデックス名が存在するか確認します(上部の検索バーに「index managment」と入力して検索することも可能です)。 インデックスを削除する場合は、同じIndicesタブから実行できます。 データビューを削除したい場合は、検索バーに「data view」と入力してください。 インデックスを削除するには、左側のチェックボックスにチェックを入れ、青いボタンの「Manage index」から「delete index」を選択してください。 データビューで削除したい項目にチェックを入れ、表示されるピンク色のアイコンを選択して削除を実行します。 データのインポートは完了しました。 4. Dev Tools上のクエリ例:分析テンプレート集 Dev Toolsも上部の検索ボックスから検索してスタートできます。 インデックスのmappingを確認します。 GET ec_site/_mapping //_mappingのoutput { "ec_site": { "mappings": { "_meta": { "created_by": "file-data-visualizer" }, "properties": { "@timestamp": { "type": "date" }, "age": { "type": "long" }, "area": { "type": "keyword" }, "areacode": { "type": "long" }, "birthday": { "type": "date", "format": "iso8601" }, "customerid": { "type": "keyword" }, "firstname": { "type": "keyword" }, "lastname": { "type": "keyword" }, "lastorderdate": { "type": "date", "format": "iso8601" }, "product": { "type": "keyword" }, "sex": { "type": "long" }, "totalprice": { "type": "long" } } } } } propertiesにある@timestampはElasticsearchが自動的に追加したもので、その詳細について確認します。 GET ec_site/_search 誕生日フィールドを@timestampとして取ってきているようですが、分析に使わないので、areacodeとセットで落とします。 POST _reindex { "source": { "index":"ec_site" }, "dest": { "index": "ec_site_v1" }, "script": { "lang": "painless", "source": """ ctx._source.remove('@timestamp'); ctx._source.remove('areacode'); """ } } 再度確認いたします。 GET ec_site_v1/_search { "query": { "match_all": {} } } 不要なものが削除されています。 分析例: // 年齢の分布 GET ec_site_v1/_search { "size":0, "aggs":{ "age_dist":{ "histogram":{ "field":"age", "interval":10 } } } } // 性別分布 GET ec_site_v1/_search?filter_path=aggregations <--- アウトプットがaggregation部分だけを表示させる { "size":0, "aggs":{ "by_gender":{ "terms":{ "field": "sex" } } } } // 性別ごとの売上合計 GET /ec_site_v1/_search?filter_path=aggregations { "size": 0, "runtime_mappings": { "sex_label": { ← ラベル用の仮想フィールドを定義 "type": "keyword", "script": { "source": """ if (doc['sex'].size()==0) { emit('不明'); } else if (doc['sex'].value == 1) { emit('男性'); } else if (doc['sex'].value == 2) { emit('女性'); } else { emit('その他'); }""" } } }, "aggs": { "total_sales": { <-- ここで全体売上を集計 "sum": { "field": "totalprice" } }, "sales_by_sex": { <-- 性別ごとの売上集計 "terms": { "field": "sex_label", "order": { "_key": "asc" } }, "aggs": { "sex_sales": { "sum": { "field": "totalprice" } } } } } } // カスタマー分析:高額購入者の傾向 GET /ec_site_v1/_search { "size": 0, "aggs": { "high_value_customers": { "filter": { "range": { "totalprice": { "gt": 70000 } } }, "aggs": { "age_distribution": { "histogram": { "field": "age", "interval": 10 } }, "top_areas": { "terms": { "field": "area.keyword", "size": 3 } }, "gender_ratio": { "terms": { "field": "sex" } } } } } } // 性別ごとの商品カテゴリー分析 GET /ec_site_v1/_search?filter_path=aggregations { "size": 0, "aggs": { "gender": { "terms": { "field": "sex", "size": 3 }, "aggs": { "product_categories": { "terms": { "field": "product.keyword", "size": 10 }, "aggs": { "total_sales": { "sum": { "field": "totalprice" } }, "avg_purchase": { "avg": { "field": "totalprice" } } } } } } } } productを指定のデータのみで集計したい場合、主に二つの方法が考えられます。 GET /ec_site_v1/_search?filter_path=aggregations { "size": 0, "query": { "term": { "product.keyword": "カーテン" } }, "aggs": { "gender": { "terms": { "field": "sex", "size": 3 }, "aggs": { "total_sales": { "sum": { "field": "totalprice" } }, "avg_purchase": { "avg": { "field": "totalprice" } } } } } } GET /ec_site_v1/_search?filter_path=aggregations { "size": 0, "aggs": { "curtain_only": { "filter": { "term": { "product.keyword": "カーテン" } }, "aggs": { "gender": { "terms": { "field": "sex", "size": 3 }, "aggs": { "total_sales": { "sum": { "field": "totalprice" } }, "avg_purchase": { "avg": { "field": "totalprice" } } } } } } } } //商品の絞り込みと性別フィルターを同時にかけたい場合 GET /ec_site_v1/_search?filter_path=aggregations { "size": 0, "query": { "bool": { "filter": [ { "term": { "product.keyword": "カーテン" } }, { "term": { "sex": 2 } } ] } }, "aggs": { "gender": { "terms": { "field": "sex", "size": 3 }, "aggs": { "total_sales": { "sum": { "field": "totalprice" } }, "avg_purchase": { "avg": { "field": "totalprice" } } } } } } // 年齢層による購買分析 GET /ec_site_v1/_search { "size": 0, "aggs": { "age_ranges": { "range": { "field": "age", "ranges": [ { "to": 30, "key": "30歳未満" }, { "from": 30, "to": 50, "key": "30-49歳" }, { "from": 50, "key": "50歳以上" } ] }, "aggs": { "products": { "terms": { "field": "product.keyword", "size": 5 }, "aggs": { "total_sales": { "sum": { "field": "totalprice" } } } }, "avg_purchase": { "avg": { "field": "totalprice" } } } } } } // 商品種類ごとの平均購入金額 GET /ec_site_v1/_search { "size": 0, "aggs": { "products": { "terms": { "field": "product.keyword", "size": 10 }, "aggs": { "avg_price": { "avg": { "field": "totalprice" } } } } } } # リピート顧客分析 (重複ID) GET /ec_site_v1/_search { "size": 0, "aggs": { "unique_customers": { "cardinality": { "field": "customerid.keyword" } }, "total_orders": { "value_count": { "field": "customerid.keyword" } } } } # 上位5都道府県の累計売上 GET /ec_site_v1/_search { "size": 0, "aggs": { "top_5_areas": { "terms": { "field": "area.keyword", "size": 5, "order": { "area_sales": "desc" } }, "aggs": { "area_sales": { "sum": { "field": "totalprice" } } } } } } // 県ごとの売上比較 GET /ec_site_v1/_search { "size": 0, "aggs": { "region_comparison": { "terms": { "field": "area.keyword", "size": 47 }, "aggs": { "total_sales": { "sum": { "field": "totalprice" } }, "order_count": { "value_count": { "field": "customerid.keyword" } } } } } } // 総支出額が最も高いお客様の特定 GET /ec_site_v1/_search { "size": 0, "aggs": { "high_value_customers": { "terms": { "field": "customerid.keyword", "size": 10, "order": { "total_spend": "desc" } }, "aggs": { "total_spend": { "sum": { "field": "totalprice" } } } } } } // 合計価格によるトップ製品 GET /ec_site_v1/_search { "size": 0, "aggs": { "top_products": { "terms": { "field": "product.keyword", "size": 10, "order": { "product_sales": "desc" } }, "aggs": { "product_sales": { "sum": { "field": "totalprice" } } } } } } // 購入額 totalprice の1〜99パーセンタイルを取得し、顧客セグメントのしきい値を一目で把握 GET ec_site_v1/_search { "size":0, "aggs":{ "spending_distribution":{ "percentiles":{ "field": "totalprice", "percents":[1,25,50,75,90,95,99] } } } } // 購入額が高い順に上位10件の顧客ID・購入額・年齢・性別を取得 GET ec_site_v1/_search { "size":10, "sort":[ {"totalprice":"desc"} ], "_source": ["customerid","totalprice","age","sex"] } 5. Kibana Lensでの可視化 Kibana Lensは、ドラッグアンドドロップの簡単な操作で直感的にデータを可視化できるツールです。フィールドの管理、迅速な集計メトリクス、チャートタイプの即時切り替えが可能で、データの検索、フィルタリング、ダッシュボード作成まで柔軟に対応できます。高い技術的スキルがなくても、手軽に高度なデータ分析環境を構築できるのがKibana Lensの魅力です。 Kibana の Lens や Discover では、インデックスそのものではなく Data View を通じてデータにアクセスします。ですので_reindexを使った後、新しいdata viewの登録が必要です。 旧インデックス:ec_site の Data View が存在 新インデックス:ec_site_v1 は 新しい名前のインデックス なので、 既存の Data View では見えません Kibana Lensは、ドラッグアンドドロップ操作で直感的なデータ可視化を実現するツールです。高度な技術がなくても、フィールド管理、迅速な集計、チャートタイプ切り替えにより、容易にデータ分析環境を構築できます。データの検索、フィルタリング、ダッシュボード作成にも柔軟に対応可能です。 KibanaのLensやDiscoverでは、インデックスではなくData Viewを介してデータにアクセスするため、_reindex後に新しいData Viewの登録が必要です。例えば、旧インデックス「ec_site」のData Viewが存在する場合でも、新インデックス「ec_site_v1」は新しい名前のインデックスであるため、既存のData Viewからは参照できません。 Kibanaで、上部の検索ボックスに「lens」と入力してください。 Lensを選択して入ると下の画面が見えます。 基本的な使い方は左側に表示されているデータのフィールドをドラッグ&ドロップするだけで簡単にチャートを作成することができます。 基本的な操作方法 データビューの選択: ドラップダウンメニューから可視化したいデータビューを選択します。今回、データのアップロード時にビューを使っているのでありますがない場合create a data viewから作成できます。またruntime fieldsを作成したい時にAdd a field to this data viewからできます。 フィールドの選択 : 左側のパネルには、利用可能なフィールド(例:「年齢」「性別」「商品名」など)が表示されます。目的に合わせて、これらのフィールドを右側のチャート領域へドラッグ&ドロップします。真ん中にドラッグしてからでも調整できます。 チャートの種類を選択 : 右側上部のメニューから、棒グラフ(Bar)、折れ線グラフ(Line)など、好きなチャート形式を選択できます。また、積み上げ表示(Stacked)などの表示形式も指定可能です。 軸や集計の設定 : 「Horizontal axis(横軸)」や「Vertical axis(縦軸)」、さらに「Breakdown(内訳)」などの項目にフィールドをドラッグ&ドロップして、軸や表示方法を柔軟にカスタマイズできます。 素早い分析とフィルタリング データのフィールドをクリックすると、各項目のトップ値や分布が表示され、即座にデータの傾向を把握できます。さらに、上部の検索バーで特定条件に合致するデータをフィルターすることも可能です。 軸の設定画面。実際に操作をし、各機能の理解を深めることを推奨します。 例)性別ごとの売上割合 割合の表示には用意されている機能だけではできないので、式の採用も必要。 Y軸:totalprice Breakdown: sex Y軸の「Appearance」→「Name」を「合計金額の割合」に設定 Y軸の「Formula」に `sum(totalprice)/overall_sum(sum(totalprice))` を入力 「男」と「女」のみを表示したい場合は、`sex_labes : (“男” or “女”)` を使用 Create a Runtime Field  手順: Kibana に移動し、 Stack Management を選択します。 Data Views を開きます。 目的のData View(例:EC_site)を選択します。 右上にある “Add field” ボタンをクリックします。 以下のフィールドに入力します。 フィールド 名前 タイプ フォーマット スクリプト 値 sex_label Keyword (デフォルトのまま) (下記👇参照) 表に年齢層を表示させたい時には以下の式でscriptを登録できます。 // for making the age group def age = doc['age'].value; if (age >= 15 && age <= 19) emit('15〜19歳'); else if (age >= 20 && age <= 24) emit('20〜24歳'); else if (age >= 25 && age <= 29) emit('25〜29歳'); else if (age >= 30 && age <= 34) emit('30〜34歳'); else if (age >= 35 && age <= 39) emit('35〜39歳'); else if (age >= 40 && age <= 44) emit('40〜44歳'); else if (age >= 45 && age <= 49) emit('45〜49歳'); else if (age >= 50 && age <= 54) emit('50〜54歳'); else if (age >= 55 && age <= 59) emit('55〜59歳'); else if (age >= 60) emit('60歳以上'); else emit('不明'); 作成した様々な図やグラフを一つのダッシュボード機能に統合し、Kibanaの共有機能やレポート機能を活用することで、データ可視化と共有の効率化を図れます。今回のデータセットで作成した図※は、以下のダッシュボードにまとめました。ドラッグ&ドロップでサイズ調整ができるため、目的に応じて容易に活用可能です。 ※ 図表の説明 売上 vs 性別(ラインチャート) :  各性別(女性/男性/その他)ごとの総売上額推移を示す。 年齢層 vs 件数(積み上げ棒グラフ) :  上位 10 の年齢グループ(例:30–34 歳、35–39 歳…)ごとの購入件数を集計。  男女別に色分けし、どの世代でどちらの性別が多いかを可視化。 消費金額(ワードクラウド) :  都道府県別の総消費金額を文字の大きさで表現。 性別の割合(円グラフ) :  全体購入者に占める男女およびその他の比率をパーセンテージで表示。 商品 vs 金額(ラインチャート) :  各商品カテゴリ(PC デスク/ソファ/ベッド…など)ごとの平均購入金額を比較。 エリア vs 金額(テーブル) :  都道府県ごとに、男性・女性別の購入金額中央値を並べて表示。 注文履歴(時系列ラインチャート) :  7 日ごとに集計した総注文金額の推移を示す。  時間軸を通して売上の増減トレンドを把握可能。 6. Elasticが持つAI × 統合分析力 Elasticは今や「全文検索ツール」ではなく、 AI Search Platform として以下の3領域を提供しています: Search(ベクトル検索・セマンティック検索) Observability(ログ・APM・メトリクス統合) Security(SIEM・脅威検知) 今回のような構造化データだけでなく、 商品説明・レビュー・チャット履歴など非構造データ も、統合AIで文脈を理解した検索が可能です。OpenAIやAmazon Bedrockと連携し、 ベクトル検索による高速類似検索 も標準対応。 さらに、CSVアップロードに限らず、 400以上のIntegration でライブデータを収集し、即座にダッシュボード化できます。Elasticは自身でも Elastic Cloud / Serverlessプラットフォーム を提供しており、インフラ構築なしに拡張性のある環境をすぐに使い始めることが可能です。 7. まとめ & 次のステップ CSVアップロード → インデックス整形 → 可視化まで、30分で体験可能 Kibana Lensで非エンジニアでも直感的に分析&共有 AI統合により、非構造データも“意味”で検索・分類が可能に Integrationでライブデータをリアルタイム分析に活用 ElasticのAI Search Platformを活用することで、 検索・分析・可視化・共有がワンストップで完結 します。 💡 Elastic Cloudの無料トライアルもあります。今すぐ体験してみてください! The post ECサイト購買データを分析!Elasticsearch & Kibana Lens 実践ガイド first appeared on Elastic Portal .
アバター
目次 1. 前書き 対象者 できるようになること 前提条件 2. セマンティックリランク 2.1. セマンティックリランクとは? 2.2. セマンティックリランクの概念図 2.3. セマンティックリランクの方法 2.4. セマンティックリランクのメリットとデメリット 3. Elasticsearch でのセマンティックリランク 3.1. セマンティックリランク利用時のおおまかな手順 3.2. Elasticsearch で利用可能なセマンティックリランカー 3.3. Cohere Rerank v3.5 の APIキーの取得 3.4. リランク用のエンドポイントの作成 3.5. セマンティックリランクを行う検索テンプレートの作成 3.6. 検索の実行 4. セマンティックリランクの応用編 5. サンプルプログラム 6. 参考URL 7. まとめ 1. 前書き 前回に引き続き、ホワイトペーパー「Elasticsearchを使った簡易RAGアプリケーションの作成」に 記載した技術的要素を紹介いたします。(脚注1) 1 今回は、セマンティックリランクです。 なお、セマンティックリランクを行うサンプルプログラムを下記の GitHub リポジトリで公開しています。参考にしてみてください。 blogs/2025-05-semantic-rerank at main · sios-elastic-tech/blogs A sample code for blogs about elasticsearch. Contribute to sios-elastic-tech/blogs development by creating an account on... github.com 対象者 Elastic Cloud のアカウントを持っている人(トライアルライセンスを含む) Elasticsearch の初心者~中級者 できるようになること Elasticsearch で日本語を含むドキュメントのセマンティックリランクを行えるようになる。 前提条件 Elasticsearch (筆者は、Elastic Cloud 8.18.1 Enterprise Edition で動作確認) Elasticsearch で日本語の形態素解析の設定を行っていること(下記で日本語の形態素解析の設定を行っています。) https://elastic.sios.jp/blog/creating-an-index-suitable-for-japanese/ Elasticsearch で日本語のベクトル化の設定を行っていること(下記で日本語のベクトル化の設定を行っています。) https://elastic.sios.jp/blog/preparing-for-vector-search/ Cohere Rerank v3.5(評価用で可) (2025年05月12日時点の情報を元に記載しています。) 2. セマンティックリランク 2.1. セマンティックリランクとは? キーワード検索のみを行った場合や、ベクトル検索のみを行った場合に適切なドキュメントが検索上位に来ない場合があります。 その改善策として、キーワード検索とベクトル検索のハイブリッド検索を行う方法があります。 しかし、それでもまだクエリーの検索結果として適切なドキュメントが上位に来ない場合もあります。 このような場合の改善策の一つとして、セマンティックリランクがあります。 セマンティックリランクは、いったん取得した検索結果の一覧を、検索クエリーとの関連度順に並び変えるという手法です。 これにより、クエリーの検索結果として適切なドキュメントが上位になることが期待されます。 2.2. セマンティックリランクの概念図 下記の図がセマンティックリランクの処理フローの概念図です。 キーワード検索とベクトル検索のハイブリッド検索後にセマンティックリランクを行う場合の処理フローの概念図 https://elastic.sios.jp/blog/search-using-user-dictionary-registration-in-elasticsearch/ で作成したインデックス(「桃太郎」を改変した「柿之助」を登録したインデックス)に対して、下記の検索クエリーを実行した例を考えます。 検索クエリー = 「柿之助からおむすびをもらったのは誰?」 キーワード検索 + 密ベクトル検索のハイブリッド検索結果(セマンティックリランク前) 順位 検索結果 1 … 2 …ゴリラもおむすびを一つもらって、… 3 …鷹もおむすびを一つもらって、… 4 … 5 …猫はおむすびを一つもらって、… ↓ ハイブリッド検索+セマンティックリランク (検索結果を「柿之助からおむすびをもらったのは誰?」に関連度が高い順に並び変える) 順位 検索結果 1 …鷹もおむすびを一つもらって、… 2 …ゴリラもおむすびを一つもらって、… 3 …猫はおむすびを一つもらって、… 4 … 5 … 検索クエリーとの関連度が高いものが上位に来るようになります。 2.3. セマンティックリランクの方法 大きく分けると3つの方法があります。 a. cross-encoder model を使って、クエリーのベクトルと、検索対象ドキュメントのベクトルを比較し、近いものを見つける。 b. bi-encoder model を使って、リランク処理を行う。 c. LLMにクエリーと検索対象ドキュメントを渡して、リランクしてもらう。 Elasticsearch では、a. cross-encoder model を利用することができるので、 今回は、a. の cross-encoder model を使う方法を採用します。 2.4. セマンティックリランクのメリットとデメリット セマンティックリランクにはメリットとデメリットがあります。 メリット クエリーとの関連度が高いドキュメントが検索結果の上位に来やすくなる。 デメリット リランク処理を行う分、遅くなる。 リランク処理のためのコストがかかる場合がある。 リランカーに大量にドキュメントを渡してしまうと、その分だけ処理時間が長くなってしまいます。 また、リランカーが受け取れる量には限界があります(あまりにも大量のドキュメントは受け取ることができません)。 かといって、あまりにも少ないドキュメントだけを渡すと、適切なドキュメントがリランク対象から漏れてしまう可能性があります。 何件のドキュメントをリランカーに渡せばよいのか? は、PoC を行うなどして決めていく必要があります。 3. Elasticsearch でのセマンティックリランク 3.1. セマンティックリランク利用時のおおまかな手順 Semantic reranking | Elastic Docs Re-rankers improve the relevance of results from earlier-stage retrieval mechanisms. Semantic re-rankers use machine lea... www.elastic.co に手順が掲載されています。 引用となりますが、以下の手順が必要です。 1. リランクモデルを選択し、利用可能な状態とする。 2. Elasticsearch 上でリランク用のエンドポイントを作成する。 3. リランク処理を行うリトリーバーを定義する。 3.2. Elasticsearch で利用可能なセマンティックリランカー Elasticsearch v8.18.1 では、いくつかのセマンティックリランカーを利用できます。 Create inference API | Elasticsearch Guide [8.18] | Elastic www.elastic.co 上記の URL からの引用となりますが、下記を利用可能です。 a. AlibabaCloud AI Search の rerank を利用する。 b. Cohere の rerank を利用する。 c. Elasticsearch の rerank を利用する。 d. Google Vertex AI の rerank を利用する。 e. VoyageAI の rerank を利用する。 f. JinaAI の rerank を利用する。 今回は、b. Cohere Rerank v3.5 を利用します(無償での検証が可能なため)。 ※日本語を含むドキュメントをリランクしたい場合、日本語に対応したリランカーを利用する必要があります。Cohere Rerank v3.5 は、日本語に対応しています。 Introducing Rerank 3.5: Precise AI Search Rerank 3.5 delivers improved reasoning and multilingual capabilities to search complex enterprise data with greater accu... cohere.com 3.3. Cohere Rerank v3.5 の APIキーの取得 1. https://cohere.com/ に Sign in します。 2. APIキーを発行します。 発行した APIキー をメモ帳などに保存しておきます。 ※Cohere のWebサイトは時々変更されるので、画面のコピーは掲載していません。 3.4. リランク用のエンドポイントの作成 以下のリクエストを Elastic Cloud の Console から発行します。 PUT _inference/rerank/cohere_rerank_v3pt5 { "service": "cohere", "service_settings": { "api_key": "<CohereのAPI-KEY>", "model_id": "rerank-v3.5", "rate_limit": { "requests_per_minute": 10 } }, "task_settings": { "top_n": 10, "return_documents": true } } <CohereのAPI-Key>には、さきほど取得した Cohere の APIキーを転記します。 Trial Key の利用なので、”requests_per_minute”: 10 としておきます。 Cohere Rerank v3.5 に渡すドキュメント数は、ひとまず、10 としておきます。(”top_n”: 10) 3.5. セマンティックリランクを行う検索テンプレートの作成 RRFによるハイブリッド検索後にセマンティックリランクを行うリトリーバーを検索テンプレートとして定義します。 下記のリクエストを Elastic Cloud の Console から発行します。 PUT _scripts/rrf_search_template_with_rerank { "script": { "lang": "mustache", "source": """{ "_source": false, "fields": [ "chunk_no", "content" ], "size": "{{size}}{{^size}}10{{/size}}", "retriever": { "text_similarity_reranker": { "retriever": { "rrf": { "retrievers": [ { "standard": { "query": { "match": { "content": "{{query_string}}" } } } }, { "standard": { "query": { "semantic": { "field": "content.text_embedding", "query": "{{query_for_vector}}" } } } } ], "rank_window_size": "{{size}}{{^size}}10{{/size}}", "rank_constant": "{{rank_constant}}{{^rank_constant}}20{{/rank_constant}}" } }, "field": "content", "rank_window_size": "{{size}}{{^size}}10{{/size}}", "inference_id": "cohere_rerank_v3pt5", "inference_text": "{{query_for_vector}}" } } {{#highlight}} , "highlight": { "fields": { "content": {} }, "pre_tags": ["<strong>"], "post_tags": ["</strong>"] } {{/highlight}} } """ } } セマンティックリランクを行う上での重要なキーワードは、” text_similarity_reranker ” です。詳細は、Elasticsearch の公式ドキュメントを参照してください。 キーワード検索とベクトル検索のハイブリッド検索の結果上位10件をリランカーに渡しています ( リランク用の特別な実装は必要ありません )。 3.6. 検索の実行 さきほど作成した検索テンプレートを使って、検索してみます。 リクエスト GET /kakinosuke/_search/template { "id": "rrf_search_template_with_rerank", "params": [ "query_string": "柿之助からおむすびをもらったのは誰?", "query_for_vector": "柿之助からおむすびをもらったのは誰?" ] } レスポンス { ... "hits": [ { ... 鷹もおむすびを一つもらって、柿之助のあとからついて行きました。 ... }, { ... ゴリラもおむすびを一つもらって、あとからついて行きました。 ... }, { ... 猫はおむすびを一つもらって、柿之助のあとから、ついて行きました。 ... }, ... ] } } 質問との関連度が高いドキュメントが上位に来ています。 4. セマンティックリランクの応用編 さきほどはキーワード検索+密ベクトル検索のハイブリッド検索結果をセマンティックリランクしましたが、  キーワード検索の上位10件 と ベクトル検索の上位10件 の計20件(ただし重複分を除く)をセマンティックリランクする といった方法も考えられます。 メリット 本当に見つけたいドキュメントがキーワード検索で上位に来るが、ベクトル検索では上位に来ないような場合に見つけやすくなる。 本当に見つけたいドキュメントがベクトル検索で上位に来るが、キーワード検索では上位に来ないような場合に見つけやすくなる。 デメリット: リランカーに渡す件数が多くなると遅くなる。 リランカーに渡す処理をコーディングする必要がある。 5. サンプルプログラム 下記の GitHub リポジトリでセマンティックリランクを行った検索結果を返すサンプルを公開しています。 blogs/2025-05-semantic-rerank at main · sios-elastic-tech/blogs A sample code for blogs about elasticsearch. Contribute to sios-elastic-tech/blogs development by creating an account on... github.com セマンティックリランクを行うと検索結果の順位が変わります。 セマンティックリランク後に RAG を行うことも可能ですが、このサンプルはドキュメント数が少ないため、セマンティックリランクの有無による RAG での回答の差異は見られませんでした。 (もっと大量のドキュメントがあれば、差異は出てくると思われます。) 6. 参考URL その他の参考URL です。 https://www.elastic.co/guide/en/elasticsearch/reference/current/semantic-reranking.html https://www.elastic.co/guide/en/elasticsearch/reference/current/infer-service-cohere.html https://www.elastic.co/guide/en/elasticsearch/reference/current/retriever.html https://www.elastic.co/guide/en/elasticsearch/reference/current/_retrievers_examples.html#retrievers-examples-text-similarity-reranker-on-top-of-rrf 7. まとめ セマンティックリランクを行うことで、クエリーとの関連度が高いドキュメントが上位に来るようになりました。 (その際、リランカーに上位n件を渡す処理や、リランカーからのリランク結果を受け取るための 特別な実装は必要ありません 。) RAG での検索結果が適切となり、その後の回答の精度の向上につながることもあります。 要件に合わせて、 セマンティックリランクを採用するか否か? 採用するとすれば、どのリランカーを採用するか? リランク前の検索をどうするか? 何件のドキュメントをリランク対象とするか? などを検討してみてください。 ホワイトペーパー「Elasticsearchを使った簡易RAGアプリケーションの作成」は、下記からダウンロードできます。 (E-mailアドレスなどの入力が必要です。) https://elastic.sios.jp/whitepaper/ ↩︎ The post Elasticsearch でのセマンティックリランク first appeared on Elastic Portal .
アバター
最近、Elasticの資格を取得するために自分のMシリーズチップ搭載のMacにElasticsearchとKibanaを直接インストールしてみました。 この記事では、その体験をもとに分かりやすくまとめましたので、ぜひ参考にしてみてください。 目次 ElasticsearchとKibanaってなに? なぜローカルインストール? Elasticsearchのインストール手順 Step1:Elasticsearchのダウンロード Step2:ダウンロードファイルを展開 Step3:Elasticsearchを起動 Step4:MacのGatekeeper警告を回避 Step5:動作確認 ログイン情報 Kibanaのインストール手順 Step1:Kibanaのダウンロード Step2:ファイルを展開 Step3:Gatekeeper警告の解除 Step4:Kibanaの起動 Step5:Kibanaの動作確認 まとめ ElasticsearchとKibanaってなに? Elasticsearch は分散型検索・分析エンジンで、ログ管理やセキュリティ分析、eコマースデータ分析などに使われています。 Kibana はそのデータをダッシュボードやチャートで視覚化するツールです。 両方ともElastic Stackの一部で、無料で使える点が魅力です。 なぜローカルインストール? コストゼロ :Elastic Cloudと違って無料 データプライバシー :外部送信せず学習可能 学習に最適 :資格取得の練習にぴったり Elasticsearchのインストール手順 Step1:Elasticsearchのダウンロード 公式サイトの Elasticsearchダウンロードページ から最新版をダウンロードします。適切なプラットフォーム(緑のボックス)をプルダウンから選択し、ダウンロードボタン(赤)をクリックします。私はMacbook pro M4なので下のようになります。 Step2:ダウンロードファイルを展開 ダウンロードした elasticsearch-8.17.4-darwin-aarch64.tar.gz をダブルクリックして展開します。 コマンドラインから展開する場合 tar -xzf elasticsearch-8.17.4-darwin-aarch64.tar.gz Step3:Elasticsearchを起動 ターミナルで以下のコマンドを入力して、展開したフォルダに移動します。 cd elasticsearch-8.17.4/ ./bin/elasticsearch Step4:MacのGatekeeper警告を回避 MシリーズMacの場合、起動時にGatekeeper(セキュリティ機能)によってJavaがブロックされることがあります。 ターミナルを開いて以下のコマンドを入力し、制限を解除します: xattr -d com.apple.quarantine /path/to/jdk.app ※/path/to/jdk.appはあなたが展開したElasticsearchフォルダ内のパスに置き換えてください。 この後もMacのセキュリティポップアップが1、2回表示されますがいつも通りに解除します。 ※controller.appにも表示があれば同じ手順で進みます。 システム設定 → プライバシーとセキュリティ → 「このまま開く」をクリック。 管理者パスワードを入力して承認。 Step5:動作確認 ブラウザで http://localhost:9200 にアクセス。 こんな表示があれば 左下をクリックして進みます。 本記事では最新版(8.17.4)を対象としていますが、スクリーンショットは一つ前のバージョンを使用しています。(8.17.4)もほぼ同じです。 表示されれば、無事Elasticsearchのインストール成功です! ログイン情報   ユーザー名:elastic   パスワード:ターミナルに表示されたJSON形式の文字列内 Kibanaのインストール手順 Step1:Kibanaのダウンロード 公式の Kibanaダウンロードページ から最新版を選択してダウンロードします。「darwin(macOS)」を選択。 Step2:ファイルを展開 ダウンロードした kibana-8.17.4-darwin-aarch64.tar.gz をダブルクリックして展開。 Step3:Gatekeeper警告の解除 Elasticsearchと同様に、ターミナルから以下のコマンドで解除します。 xattr -d com.apple.quarantine /path/to/kibana 解除できない場合は、システム設定で「このまま開く」を選びます。 Step4:Kibanaの起動 ターミナルで展開したフォルダに移動後、起動コマンドを入力。 cd kibana-8.17.4/ ./bin/kibana Step5:Kibanaの動作確認 ブラウザで以下のURLにアクセス。 http://localhost:5601 localhost 注意) 登録トークンはElasticsearch起動後30分以内に使用する必要があるので注意しましょう。 これで、KibanaのインストールとElasticsearchとの連携も完了です! まとめ 今回、MシリーズMac上でElastic環境をローカルに構築する方法を紹介しました。 無料・安全・手軽に始めたい方におすすめです! The post MシリーズMacにElasticsearchとKibanaをインストールする手順ガイド first appeared on Elastic Portal .
アバター
目次 1. 前書き 対象者 できるようになること 前提条件 2. メタデータ 2.1. メタデータとは? 2.2 メタデータを考慮しない検索 2.3. メタデータを活用した検索 3. メタデータによるフィルタリングを考慮した検索テンプレート 4. サンプルソース 5. 実行例 5.1. メタデータを使わずに検索した場合 5.2. メタデータを活用して検索した場合 5.3. メタデータを使わずにRAGを行った場合 5.4. メタデータを活用してRAGを行った場合 6. 参考情報 7. まとめ 1. 前書き 前回に引き続き、ホワイトペーパー「Elasticsearchを使った簡易RAGアプリケーションの作成」に 記載した技術的要素を紹介いたします。(*脚注1) 1 今回は、メタデータの活用です。 なお、このブログで使用しているサンプルコードは、下記の GitHub リポジトリで公開しています。 blogs/2025-05-search-with-metadata at main · sios-elastic-tech/blogs A sample code for blogs about elasticsearch. Contribute to sios-elastic-tech/blogs development by creating an account on... github.com 対象者 Elastic Cloud のアカウントを持っている人(トライアルライセンスを含む) Elasticsearch の初心者~中級者 できるようになること Elasticsearch でメタデータを活用した検索を行えるようになる。 前提条件 Elasticsearch を利用できること (筆者は、Elastic Cloud 8.18.1 Enterprise License で確認) Elasticsearch で日本語の形態素解析の設定を行っていること https://elastic.sios.jp/blog/creating-an-index-suitable-for-japanese/ で、日本語の形態素解析の設定を行っています。 LLM を利用できること (筆者は、Cohere および Ollama で確認) なお、今回のサンプルでは、説明を簡素にするためにベクトル検索は行いません。キーワード検索のみ行います。 (2025年05月07日時点の情報を元に記載しています。) 2. メタデータ 2.1. メタデータとは? このブログ内で「メタデータ」と呼んでいるのは、ドキュメントの ファイル名 ファイルの種類 作成者 作成日 更新日 文書のタイトル 章のタイトル カテゴリー キーワード タグ 要約 想定されるQ&A など、ドキュメントに関する情報のことです。 2.2 メタデータを考慮しない検索 有価証券報告書 | IR(投資家情報)‐サイオス株式会社> サイオス株式会社の有価証券報告書をご覧いただけます。 www.sios.com で公開されているサイオス株式会社の有価証券報告書(PDF)の抜粋を検索する場合を考えます。 (下記に表示している各ブロックが1つのチャンクだとします。) 2018年12月期の有価証券報告書の抜粋 (c) 受注実績 当連結会計年度の受注実績をセグメントごとに示すと、次のとおりであります。 セグメントの名称 受注高(千円) 前年同期比(%) 受注残高(千円) 前年同期比(%) オープンシステム基盤事業 7,413,251 +7.2 1,413,726 +13.8 アプリケーション事業 5,591,926 △4.5 1,324,304 +1.4 合計 13,005,178 +1.8 2,738,031 +7.5 2019年12月期の有価証券報告書の抜粋 (c) 受注実績 当連結会計年度の受注実績をセグメントごとに示すと、次のとおりであります。 セグメントの名称 受注高(千円) 前年同期比(%) 受注残高(千円) 前年同期比(%) オープンシステム基盤事業 7,713,257 +4.0 1,431,537 +1.3 アプリケーション事業 6,175,620 +10.4 1,508,696 +13.9 合計 13,888,877 +6.8 2,940,234 +7.4 2020年12月期の有価証券報告書の抜粋 (c) 受注実績 当連結会計年度の受注実績をセグメントごとに示すと、次のとおりであります。 セグメントの名称 受注高(千円) 前年同期比(%) 受注残高(千円) 前年同期比(%) オープンシステム基盤事業 9,073,642 +17.6 1,621,310 +13.3 アプリケーション事業 6,109,346 △1.1 1,660,413 +10.1 合計 15,182,988 +9.3 3,281,724 +11.6 2021年12月期の有価証券報告書の抜粋 (c) 受注実績 当連結会計年度の受注実績をセグメントごとに示すと、次のとおりであります。 セグメントの名称 受注高(千円) 前年同期比(%) 受注残高(千円) 前年同期比(%) オープンシステム基盤事業 9,691,248 +6.8 1,724,230 +6.3 アプリケーション事業 6,883,678 +12.7 2,407,648 +45.0 合計 16,574,927 +9.2 4,131,879 +25.9 2022年12月期の有価証券報告書の抜粋 (c) 受注実績 当連結会計年度の受注実績をセグメントごとに示すと、次のとおりであります。 セグメントの名称 受注高(千円) 前年同期比(%) 受注残高(千円) 前年同期比(%) オープンシステム基盤事業 8,843,132 △8.8 1,850,417 +7.3 アプリケーション事業 5,772,577 △16.1 2,488,613 +3.4 合計 14,615,709 △11.8 4,339,030 +5.0 2023年12月期の有価証券報告書の抜粋 (c) 受注実績 当連結会計年度の受注実績をセグメントごとに示すと、次のとおりであります。 セグメントの名称 受注高(千円) 前年同期比(%) 受注残高(千円) 前年同期比(%) オープンシステム基盤事業 10,503,935 +18.8 2,444,937 +32.1 アプリケーション事業 5,541,619 △4.0 2,062,760 △17.1 合計 16,045,555 +9.8 4,507,698 +3.9 2024年12月期の有価証券報告書の抜粋 (c) 受注実績 当連結会計年度の受注実績をセグメントごとに示すと、次のとおりであります。 セグメントの名称 受注高(千円) 前年同期比(%) 受注残高(千円) 前年同期比(%) オープンシステム基盤事業 14,871,485 141.6 2,742,583 112.2 アプリケーション事業 6,400,717 115.5 2,477,333 120.1 合計 21,272,202 132.6 5,219,917 115.8 ※なお、上記は、pdfplumber を使って PDF から Markdown 形式で抽出したデータの抜粋です。 これらを、1つのインデックスに登録してみます。 インデックスの作成は省略します。 詳細は、下記の github 内のファイルを参照してください。 https://github.com/sios-elastic-tech/blogs/tree/main/2025-05-search-with-metadata/es_requests/01_create_index_settings.md https://github.com/sios-elastic-tech/blogs/tree/main/2025-05-search-with-metadata/es_requests/02_create_index_mappings_with_meta.md インデックスを作成した後、ドキュメントを登録していきます。 詳細は、下記の github 内のファイルを参照してください。 https://github.com/sios-elastic-tech/blogs/tree/main/2025-05-search-with-metadata/es_requests/03_post_docs.md POST /sios_securities_report/_doc { "meta.fiscal_year": "2018年12月期", "content": """ 当連結会計年度の受注実績をセグメントごとに示すと、次のとおりであります。 | セグメントの名称 | 受注高(千円) | 前年同期比(%) | 受注残高(千円) | 前年同期比(%) | |:--|:--|:--|:--|:--:| | オープンシステム基盤事業 | 7,413,251 | +7.2 | 1,413,726 | +13.8 | | アプリケーション事業 | 5,591,926 | △4.5 | 1,324,304 | +1.4 | | 合計 | 13,005,178 | +1.8 | 2,738,031 | +7.5 | """ } ... POST /sios_securities_report/_refresh ※表の部分は、LLM が回答しやすくなるよう、Markdown 形式で記載します。 ※content フィールドには、何年の情報か?が記載されていません。何年の情報か?は、meta.fiscal_year にのみ保持しています。 ドキュメントを登録後に、次の検索クエリーを投げてみます。 GET /sios_securities_report/_search { "query": { "match": { "content": "2024年12月期 オープンシステム基盤事業 受注高" } } } さて、目的のドキュメント(2024年12月期のオープンシステム基盤事業の受注高を含むドキュメント)が 1位でヒットするでしょうか? 結果は、 ... | オープンシステム基盤事業 | 7,413,251 | ... ... | オープンシステム基盤事業 | 7,713,257 | ... ... | オープンシステム基盤事業 | 9,073,642 | ... ... | オープンシステム基盤事業 | 9,691,248 | ... ... | オープンシステム基盤事業 | 8,843,132 | ... となりました。これらは、それぞれ以下のドキュメントとなっています。 2018年12月期の受注高を示すドキュメント 2019年12月期の受注高を示すドキュメント 2020年12月期の受注高を示すドキュメント 2021年12月期の受注高を示すドキュメント 2022年12月期の受注高を示すドキュメント 2024年12月期のドキュメントが1位にならず、しかも、 2024年12月期以外のドキュメントが多数ヒットしています。 このような検索結果になった理由は、検索対象フィールドである “content” の中に “2024年12月期” といった情報が含まれていないためです。 このため、目的のドキュメントを見つけることが困難になっています。 2.3. メタデータを活用した検索 検索時に会計年度(fiscal_year)によるフィルタリングを行うようにしてみます。 GET /sios_securities_report/_search { "query": { "bool": { "must": { "match": { "content": "2024年12月期 オープンシステム基盤事業 受注高" } }, "filter": { "term": { "meta.fiscal_year": "2024年12月期" } } } } } 結果 ... | オープンシステム基盤事業 | 14,871,485 | ... ... 2024年12月期の受注高を示すドキュメントのみがヒットしました。 (filter で絞り込んでいるので、当然の結果です。) 3. メタデータによるフィルタリングを考慮した検索テンプレート Python から検索しやすくなるよう、検索テンプレートを登録しておきます。このとき、メタデータによるフィルタリングも行うようにしておきます。 PUT _scripts/search_with_meta_template_202505 { "script": { "lang": "mustache", "source": """{ "_source": false, "fields": [ "meta.fiscal_year", "chunk_no", "content" ], "size": "{{size}}{{^size}}10{{/size}}", "query": { "bool": { "must": [ { "match": { "content": "{{query}}" } } ] {{#fiscal_year}} , "filter": [ { "term": { "meta.fiscal_year": "{{fiscal_year}}" } } ] {{/fiscal_year}} } } } """ } } 4. サンプルソース 下記に、メタデータによるフィルタリングを考慮した RAG のサンプルソースを公開しています。 blogs/2025-05-search-with-metadata at main · sios-elastic-tech/blogs A sample code for blogs about elasticsearch. Contribute to sios-elastic-tech/blogs development by creating an account on... github.com Elasticsearch + Streamlit + LLM による RAG です。(RAGを使わずに検索だけ行うことも可能です。) LLM には、Cohere または Ollama を利用できるようにしています。 Cohere を利用する場合は、Cohere の API Key が必要です。 Ollama を利用する場合は、別途、Ollama および利用するモデルのインストールが必要です。 参考にしてみてください。 5. 実行例 サンプルアプリの実行方法については、下記のリンクを参照してください。 blogs/2025-05-search-with-metadata/README.md at main · sios-elastic-tech/blogs A sample code for blogs about elasticsearch. Contribute to sios-elastic-tech/blogs development by creating an account on... github.com 下記に実行例を表示します。 5.1. メタデータを使わずに検索した場合 「2024年のオープンシステム基盤事業の受注高はいくら?」という検索を行ったにもかかわらず、2024年のドキュメントが上位にヒットしていません。他の年のドキュメントが上位にヒットしてしまっています。これは、検索対象の content フィールドに「年」の情報を保持していないためです。 5.2. メタデータを活用して検索した場合 左側のラジオボタンで、あらかじめ《2024年12月期》を選択してから「2024年のオープンシステム基盤事業の受注高はいくら?」という検索を行った結果です。 事前に会計年度によるフィルタリングを行っているので、2024年12月期のドキュメントのみがヒットしています。 5.3. メタデータを使わずにRAGを行った場合 ※これは、Ollama で、hf.co/mmnga/ELYZA-Shortcut-1.0-Qwen-7B-gguf:Q4_K_M を使って回答させた例です。 検索結果には、2024年以外のドキュメントも含まれており、かつ、返却されたデータには「2024年」というキーワードが含まれていません。そのため、LLM からの回答が 「わかりません。」 となっています。 5.4. メタデータを活用してRAGを行った場合 ※これは、Ollama で、hf.co/mmnga/ELYZA-Shortcut-1.0-Qwen-7B-gguf:Q4_K_M を使って回答させた例です。 左側のラジオボタンで、あらかじめ《2024年12月期》を選択してから「オープンシステム基盤事業の受注高はいくら?」という問い合わせを行った結果です。(クエリーから「2024年の」という文字列を除去していますが、これは、「2024年」という条件をメタデータによるフィルタリングで実施しており、LLMに渡すと単なるノイズになってしまうためです。) 事前に会計年度によるフィルタリングを行っているので、2024年12月期のドキュメントのみが検索結果として返却され、それにもとづいて正しい回答が返却されています。 6. 参考情報 メタデータを使った検索に関する、Elasticsearch 社のブログ(英語) Advanced RAG techniques: Data processing & ingestion - Elasticsearch Labs This blog explores and implements advanced RAG techniques which may increase performance, focusing on data processing & ... www.elastic.co Advanced RAG techniques: Querying & testing a RAG pipeline - Elasticsearch Labs This blog discusses and implements RAG techniques which may increase performance, focusing on querying and testing an ad... www.elastic.co 上記の Elastic Search Labs のブログでは、 キーフレーズ 潜在的な質問 エンティティ を GPT-4o などを使って生成しています。 生成した情報をメタ情報としてドキュメントに付与し、検索時に利用するサンプルが紹介されています。 7. まとめ このように メタデータを活用すると目的のドキュメントを見つけやすくなります 。 検索の手助けになるようなメタデータは、ドキュメントの内容や検索要件によるので 一概にコレとは言えませんが、おおよそ下記のようなものがメタデータとなると思われます。 ファイル名 ファイルの種類 作成者 作成日 更新日 文書のタイトル 章のタイトル カテゴリー キーワード タグ 要約 想定されるQ&A このようなメタデータのうち自分たちの検索に有用と思われるものを保持し、検索時に利用することで目的のドキュメントを見つけやすくなります。 さらに、今回のサンプルアプリのように、検索結果が正確になることで RAG の回答精度が上昇する場合もあります。 要件に合わせて、メタデータを考慮した検索を検討してみてください。 ホワイトペーパー「Elasticsearchを使った簡易RAGアプリケーションの作成」は、下記からダウンロードできます。 (E-mailアドレスなどの入力が必要です。) https://elastic.sios.jp/whitepaper/ ↩︎ The post Elasticsearch でのメタデータを活用した検索 first appeared on Elastic Portal .
アバター
目次 1. 前書き 対象者 できるようになること 動作に必要な環境 2. CSVファイルの準備 3. Elasticsearch へのCSVファイルの登録 4. エイリアスの作成 5. 検索用テンプレートの作成 6. 読み取り用 Access Key の生成 7. Elasticsearch Endpoint URL の取得 8. コンテナの実行 8.1. ソースコードのダウンロード 8.2. ファイルの修正 8.3. コンテナのビルド~検索アプリの実行 8.4. 検索アプリの表示 9. 検索処理の概要 10. まとめ 1. 前書き 前回は、N-gram 検索の基礎的な内容について記載しました。 今回は、より実践的なサンプルアプリをご紹介いたします。 なお、今回のサンプルアプリは、下記の GitHub リポジトリで公開しています。 blogs/2025-05-ngram-search-part2 at main · sios-elastic-tech/blogs A sample code for blogs about elasticsearch. Contribute to sios-elastic-tech/blogs development by creating an account on... github.com 対象者 Elasticsearch の初心者~中級者 できるようになること Elasticsearch で N-gram を使ったサンプルアプリを動かせるようになる。 Flask を使った簡単なアプリを動かせるようになる。 動作に必要な環境 Elasticsearch (クラウド版でもオンプレミス版でも可。筆者は、version: 8.18.0 のクラウド版で動作確認しました。) Docker コンテナ を動かせる環境 (筆者は、Windows 版の Rancher Desktop 1.18.2 で動作確認しました。) その他、下記は、自動でダウンロードされます。 Python 3.13 elasticsearch 8.18.1 (Elasticsearch の Python用のクライアント) flask 3.1.0 python-dotenv 1.0.1 (2025年5月1日時点の情報を元に記載しています。) 2. CSVファイルの準備 今回のサンプルアプリでは、医薬品マスタの検索を行います。検索対象となる医薬品マスタを準備します。 2.1. 下記の URL から「医薬品マスター」をダウンロードします。 診療報酬情報提供サービス shinryohoshu.mhlw.go.jp 2.2. y.zip がダウンロードされるので、それを展開します。 2.3. y_20250415.csv というファイルが展開されます(2025-04-15版の場合)。 2.4. y_20250415.csv ファイルを Excel や LibreOffice などで開きます。 2.5. 3列目と5列目のみを残し、それ以外の列を削除します。 2.6. 先頭に行を挿入します。 2.7. 先頭行の値に、”medicine_code”, “medicine_name” をそれぞれ入力します。 2.8. y_20250415_col2.csv という名前で保存します。 2.9. y_20250415_col2.csv ファイルをメモ帳などで開き、UTF-8 で保存します。その際、ファイル名を y_20250415_col2_utf8.csv とします。 なお、上記で作成した y_20250415_col2_utf8.csv ファイルは、下記のリポジトリでも公開しています。 blogs/2025-05-ngram-search-part2/csv at main · sios-elastic-tech/blogs A sample code for blogs about elasticsearch. Contribute to sios-elastic-tech/blogs development by creating an account on... github.com 3. Elasticsearch へのCSVファイルの登録 先ほど作成したCSVファイル(y_20250415_col2_utf8.csv)を、Elasticsearch のインデックスへ登録します。 3.1. Elastic Cloud のログイン画面よりログインします。 (オンプレミス版の場合は、Kibana のログイン画面よりログインします。) 3.2. Elastic Cloud の Deployment 一覧より、CSV を登録したい Deployment を選択します。 (オンプレミス版の場合は、この操作は必要ありません。) 3.3. Elastic Cloud の Home 画面が表示されるので、[Upload a file]をクリックします。 3.4. ファイルのアップロード画面が表示されるので、さきほど作成した y_20250415_col2_utf8.csv ファイルを点線で囲まれたエリアにドラッグ&ドロップします。 3.5. ファイルアップロードの設定画面が表示されるので、[Override settings] をクリックします。 3.6. Override settings 画面が表示されます。 Number of lines to sample に 20000 を入力します(医薬品マスタが約18000件あるため)。 Has header row にチェックが入っていることを確認します(先頭行をフィールド名とするため)。 その後、右下の [Apply] をクリックします。 3.7. 元の画面に戻るので、[Import] をクリックします。 3.8. Import Data の画面が表示されます。 3.9. Advanced のタブをクリックします。 3.10. Import Data の Advanced タブが表示されます。 次のように入力します。 Index name : medicine_20250415 Create data view : off Index settings : 次の内容を張り付けます。 { "index": { "number_of_shards": 1, "number_of_replicas": 1, "refresh_interval": "5s" }, "analysis": { "char_filter": { "ja_normalizer": { "type": "icu_normalizer", "name": "nfkc", "mode": "compose" } }, "tokenizer": { "ja_ngram_tokenizer": { "type": "ngram", "min_gram": 2, "max_gram": 2, "token_chars": [ "letter", "digit", "punctuation", "symbol" ] } }, "analyzer": { "ja_ngram_index_analyzer": { "type": "custom", "char_filter": [ "ja_normalizer" ], "tokenizer": "ja_ngram_tokenizer", "filter": [ "lowercase" ] } } } } mappings : 次の内容を張り付けます。 { "properties": { "medicine_code": { "type": "long" }, "medicine_name": { "type": "keyword", "fields": { "ngram": { "type": "text", "analyzer": "ja_ngram_index_analyzer", "search_analyzer": "ja_ngram_index_analyzer" } } } } } 3.11. 左下の [Import] をクリックします。 3.12. CSV のインポート処理が行われます(少し時間がかかります)。 3.13. CSV のインポートが完了すると、完了画面が表示されます。 これで、medicine_20250415 インデックスに、医薬品マスタの内容が登録されました。 4. エイリアスの作成 今回は、medicine_20250415 という名前のインデックスにインポートしましたが、医薬品マスタは年に数回更新されます。今後、更新があった場合でも、検索しやすくなるよう、エイリアスを作成しておきます。 Dev Tools から Console を表示し、下記のリクエストを実行します。 POST _aliases { "actions": [ { "add": { "index": "medicine_20250415", "alias": "medicine" } } ] } 5. 検索用テンプレートの作成 今回の医薬品マスタの検索アプリは Python で作成しますが、Python から検索しやすくなるよう、検索用テンプレートを Elasticsearch 側で登録しておきます。 Dev Tools から Console を表示し、下記のリクエストを実行します。 PUT _scripts/search_medicine_with_ngram_202505 { "script": { "lang": "mustache", "source": """{ "_source": false, "fields": [ "medicine_code", "medicine_name" ], "size": "{{size}}{{^size}}10{{/size}}", "query": { "bool": { "must": [ { "match_phrase": { "medicine_name.ngram": "{{search_medicine_name1}}" } } {{#search_medicine_name2}} , { "match_phrase": { "medicine_name.ngram": "{{search_medicine_name2}}" } } {{/search_medicine_name2}} ] } } } """ } } 上記の検索用テンプレートを使った検索例です。 GET /medicine/_search/template { "id": "search_medicine_with_ngram_202505", "params": { "size": 30, "search_medicine_name1": "アス", "search_medicine_name2": "100mg" } } 6. 読み取り用 Access Key の生成 Python から検索処理を呼び出すための Access Key を生成しておきます。 Dev Tools から Console を表示し、下記のリクエストを実行します。 POST /_security/api_key { "name": "medicine_read", "role_descriptors": { "medication_read": { "cluster": ["all"], "indices": [ { "names": ["medicine*"], "privileges": ["read"] } ] } } } 成功すると、エンコードされた Access Key が表示されるので、メモ帳などにコピー&ペーストしておきます(後で使います)。 7. Elasticsearch Endpoint URL の取得 Python のプログラムからアクセスするための Endpoint URL を取得します。Home 画面に戻り、Elasticsearch の大きいボタンをクリックします。 すると、次のような画面が表示されるので、Elasticsearch の Endpoint が表示されている欄のコピーボタンを押します。 コピーした Endpoint の URL をメモ帳などにペーストしておきます(後で使います)。 8. コンテナの実行 サンプルアプリを実行するための下準備はできたので、いよいよ、サンプルアプリを動かしていきます。 ただし、サンプルアプリの各ソースコードを逐次説明していくと、内容が膨大になるため、今回は要点のみを後述しています。 8.1. ソースコードのダウンロード GitHub のリポジトリからソースコードをダウンロードします。 下記の URL へアクセスします。 GitHub - sios-elastic-tech/blogs: A sample code for blogs about elasticsearch. A sample code for blogs about elasticsearch. Contribute to sios-elastic-tech/blogs development by creating an account on... github.com 画面上部の [<> Code] をクリックし、さらに [Download ZIP] をクリックします。 blogs-main.zip ファイルがダウンロードされるので、この zip ファイルを展開します。 8.2. ファイルの修正 環境に合わせてファイルを修正します。 さきほど展開した blogs-main/ フォルダの下の 2025-05-ngram-search-part2/flask_app/ フォルダへ移動します。 cd blogs-main/2025-05-ngram-search-part2/flask_app .env ファイルを環境に合わせて修正します。 elasticsearch_endpoint='' read_api_key_encoded='' 7. で取得したURL を elasticsearch_endpoint に、 6. で取得した Access Key を read_api_key_encoded に、 それぞれ転記します。 8.3. コンテナのビルド~検索アプリの実行 コンテナをビルドします。 docker compose build コンテナを実行開始します。 docker compose up -d コンテナに接続します。 docker exec -it search_with_ngram_sample_202505 /bin/bash (search_with_ngram_sample_202505 は、コンテナ名です。) 検索アプリを実行します。 python run.py 8.4. 検索アプリの表示 Webブラウザで、localhost:5000 (あるいは、127.0.0.1:5000) にアクセスします。 次のような画面が表示されます。 試しに、検索キーワード1 に “アス” を入力してみます。すると、下部の医薬品リストに、”アス”を含む候補が表示されます(詳細は割愛しますが、2文字以上入力すると検索するようにプログラムしています)。 さらに、検索キーワード2に “10mg” と入力してみます。すると、医薬品リストが、”アス”を含み、かつ、”10mg”を含むものに更新されます。 ここで、検索キーワード2を “100mg” に変更してみます。すると、医薬品リストが “アス” を含み、かつ、”100mg” を含むものに更新されます。 画像だけではわかりにくいと思いますので、下記に動画を添付しています。 これを見て頂ければ、これらの更新が瞬時に行われ、利用者にとってもストレスなく操作できることがおわかりになるかと思います。 document.createElement('video'); https://elastic.sios.jp/wp-content/uploads/2025/05/search_medicine_20250501.mp4 なお、医薬品をクリックすると、選択した医薬品のコードと名称を表示するようにしています(これは、おまけの機能です。N-gram 検索とは直接関係ありません)。 検索アプリの停止用ボタンは用意していないので、停止させたい場合は、Ctrl+C を押すなり、コンテナを停止させるなりしてください。 9. 検索処理の概要 Web ブラウザ上で検索キーワード1 または 2 に、何かのキーが入力されたら(onkeyupのイベントを検知したら)static/js/scripts.js の onkeyup1() または onkeyup2() が呼ばれます。 そこで、前回の入力値との比較を行い、違っていたら、/search_medicine へリクエストを投げます。 app.py の def search() で、検索キーワード1 および 検索キーワード2 を受け取り、検索用パラメータを生成します。Elasticsearch に生成した検索用パラメータを渡し、検索用テンプレートを使って N-gram 検索を行います。検索結果を呼び出し元 (JavaScript) に返します。 static/js/scripts.js で、検索結果を受け取り、画面に反映させます。 10. まとめ N-gram 検索を利用すると、kuromoji が知らない単語でも検索することができます。 医薬品や商品マスタなど、2~3文字を入力すると対象項目を絞り込みたいような場面に適用しやすいことがわかっていただけたかと思います。 The post Elasticsearch での N-gram 検索 (Part 2) 医薬品マスタの検索 first appeared on Elastic Portal .
アバター
MCPとは? から、LLMの能力を最大限引き出す共通ルールMCPをPythonで自作し、Elasticsearchと連携させる具体的な手順を説明します。 目次 LLMは万能じゃない?AI導入の壁 MCP(Model Context Protocol)とは? なぜ今、MCPが注目されているのか? MCPエコシステム MCPの仕組みを理解する:シーケンス図で見る処理の流れ 【実践】MCPサーバーをPythonで構築しElasticsearchと連携する手順 前提条件 ステップ1: UVパッケージマネージャーの導入 ステップ2: MCPサーバーの作成 ステップ3: MCPサーバーのコードを書く ステップ4: Claude Desktop アプリのインストール ステップ5: MCP サーバーの起動 ステップ6: Claude Desktopとの連携設定 Claudeの設定 ツールの実行テスト ステップ7: Claudeからの動作確認 トラブルシューティング:困ったときのヒント 最後に LLMは万能じゃない?AI導入の壁 ChatGPTやClaudeなどの大規模言語モデル(LLM)は、私たちの日常業務に革命をもたらしています。しかし、こんな疑問を持ったことはありませんか? 「メールを送って」と頼んだのに、実際には送ってくれない 検索はできるけど、社内データベースとのやり取りは難しい そう、LLMはテキストを“予測”するだけのモデルに過ぎず、自分で“行動”する力は持っていません。 MCP(Model Context Protocol)とは? MCPとは、LLMがさまざまな外部ツールと統一的に連携できるようにするための「共通ルール(プロトコル)」です。 ソフトウェア開発の世界では「標準」がとても重要です。たとえば「REST API」は、異なるシステム同士がスムーズに連携できる共通ルールとして広く使われています。 この“標準の考え方”をLLMにも適用したものが、MCPになります。 なぜ今、MCPが注目されているのか? 従来のようにツールごとにバラバラのAPIを組み合わせる必要はなく、MCPを通すことで 統一された方法 でさまざまなタスクを実行できます。 ツール連携の仕組みがシンプルになり、開発効率が向上 コードの再利用性や保守性がアップ セキュリティの統一管理が可能 LLMを実際の業務・サービスに活用しやすくなる MCPエコシステム MCPエコシステムは以下の要素で構成されています。 MCPクライアント/ホスト: 例:Claude, Wind surf, Cursorなど。 機能:エコシステムのクライアント側、すなわちLLM側となる部分です。 プロトコル: 機能:クライアントとサーバー間の双方向の接続を担います。 場所:MCPクライアントとMCPサーバーの間に存在します。 MCPサーバー: 機能:外部サービス(その機能や実行可能なこと)をクライアントに翻訳して伝えます。 サービス: 機能:LLMがアクセスしたい外部の機能やデータソースそのものです(例:Elastic)。 MCPの仕組みを理解する:シーケンス図で見る処理の流れ MCPを利用したLLMと外部サービスの連携は、以下のシーケンス図のように進行します。 【実践】MCPサーバーをPythonで構築しElasticsearchと連携する手順 ここからは、実際にMCPサーバーを構築し、Elasticsearchと連携する手順を示します。これにより、自然言語を用いたElasticsearchの操作が可能になります。 前提条件 Macbook Pro M4チップ VS code、Python が使える好みのIDE Claude Desktop アプリをインストール済み(今回は無料枠) Elasticsearch 環境 ※ MCPサーバー作成の公式クイックスタートを参考に進みます https://github.com/modelcontextprotocol/python-sdk ステップ1: UVパッケージマネージャーの導入 uvをインストール。 ※ https://docs.astral.sh/uv/getting-started/installation/ curl -LsSf https://astral.sh/uv/install.sh | sh インストール後、以下で確認: which uv /usr/local/bin/uv などが出れば成功です。 ステップ2: MCPサーバーの作成 ターミナルを開き、以下のコマンドで新しいプロジェクト(mcp-elasticsearch)を初期化します。 uv init mcp-elasticsearch 作成されたプロジェクトディレクトリに移動します。 cd mcp-elasticsearch MCP の CLI パッケージをプロジェクトの依存関係に追加します。 uv add "mcp[cli]" これで、MCP Python SDK がインストールされ、プロジェクトの準備が完了しました。 ステップ3: MCPサーバーのコードを書く Python用のMCP公式SDKを参考に進みます。 https://github.com/modelcontextprotocol/python-sdk mcp-elasticsearch プロジェクトを Visual Studio Code で開きます。 env ファイルを作成し、以下を記述。 ELASTIC_USER=elastic ELASTIC_PASSWORD=**** ELASTIC_CLOUD_URL=https://your-deployment.aws.found.io プロジェクトフォルダにあるmain.py の中身を以下のコードに置き換えます。 # ========================================= # Elasticsearch MCP Server for Claude # ========================================= from mcp.server.fastmcp import FastMCP from elasticsearch import Elasticsearch from dotenv import load_dotenv import os # ---------------------- # 🔧 Setup # ---------------------- print("Loading environment variables...") load_dotenv() # Start MCP server mcp = FastMCP("ElasticsearchMCP") # Connect to Elasticsearch try: es = Elasticsearch( os.getenv("ELASTIC_CLOUD_URL"), basic_auth=(os.getenv("ELASTIC_USER"), os.getenv("ELASTIC_PASSWORD")) ) print("🔗 Connected to Elasticsearch!") except Exception as e: print("❌ Failed to connect:", e) es = None # ---------------------- # 🛠️ MCP Tools # ---------------------- @mcp.tool() def list_indices() -> list: """Get all index names in the cluster.""" # ES Query: GET /_cat/indices return list(es.indices.get_alias().keys()) if es else ["ES not initialized"] @mcp.tool() def get_cluster_health() -> dict: """Check the health status of the Elasticsearch cluster.""" # ES Query: GET /_cluster/health return es.cluster.health() if es else {"error": "ES not initialized"} @mcp.tool() def count_documents(index_name: str) -> int: """Count how many documents exist in a given index.""" # ES Query: GET /{index}/_count try: return es.count(index=index_name)['count'] if es else -1 except Exception as e: return f"Error: {str(e)}" @mcp.tool() def search_index(index_name: str, keyword: str) -> list: """Search for documents in an index using a keyword.""" # ES Query: # POST /{index}/_search # { # "query": { "match": { "_all": "keyword" } } # } try: result = es.search( index=index_name, query={"match": {"_all": keyword}}, size=5 ) return [doc["_source"] for doc in result["hits"]["hits"]] except Exception as e: return [f"Error: {str(e)}"] @mcp.tool() def get_index_mapping(index_name: str) -> dict: """Retrieve the field definitions (mappings) of an index.""" # ES Query: GET /{index}/_mapping try: return es.indices.get_mapping(index=index_name)[index_name]["mappings"] except Exception as e: return {"error": str(e)} @mcp.tool() def delete_index(index_name: str) -> str: """Delete an entire index (irreversible).""" # ES Query: DELETE /{index} try: es.indices.delete(index=index_name) return f"✅ Deleted index '{index_name}'" except Exception as e: return f"❌ Error: {str(e)}" @mcp.tool() def get_recent_docs(index_name: str, size: int = 5) -> list: """Fetch the most recent documents from an index.""" # ES Query: # POST /{index}/_search # { # "size": 5, # "sort": [{ "@timestamp": "desc" }] # } try: result = es.search( index=index_name, size=size, sort=[{"@timestamp": "desc"}] ) return [doc["_source"] for doc in result["hits"]["hits"]] except Exception as e: return [f"Error: {str(e)}"] # ---------------------- # Start MCP Server # ---------------------- if __name__ == "__main__": print("🚀 Starting MCP server...") if os.environ.get("RUN_MCP_MANUALLY"): print("Running in manual mode (not Claude Desktop).") else: print("Waiting for Claude Desktop input (stdio mode).") try: mcp.run() print("✅ MCP server exited cleanly.") except Exception as e: print("❌ MCP server crashed:", e)  このコードでは、 fast_mcp モジュールから FastMCP クラスをインポートしています。 ElasticsearchMCP という名前で MCP サーバーのインスタンスを作成しています。 @mcp.tool() デコレータを使って、七つの ツールを定義しています。ツールの説明(”””ここに入っている文章 “””)は AI がツールの中身を理解するために重要です。 list_indices()         インデックス一覧の取得 get_cluster_health()      クラスタの状態確認 count_documents(index)     ドキュメント件数のカウント search_index(index, keyword) キーワード検索 get_index_mapping(index)     マッピング取得 delete_index(index)       インデックス削除 get_recent_docs(index, size)  最新のドキュメント取得 ステップ4: Claude Desktop アプリのインストール まだインストールしていない場合は、 Claude の公式サイトから Claude Desktop アプリをダウンロードしてインストールしてください。 ※Claude Desktop Download: https://claude.ai/download MCPサーバーと連携させる前の画面↓ ステップ5: MCP サーバーの起動 ターミナルで、mcp-elasticsearch ディレクトリにいることを確認します。 以下のコマンドを実行して、MCP サーバーを起動します。 uv run mcp install main.py サーバー追加されたメッセージを確認できます。 Claudeに設定項目が追加されました。Claude Desktopアプリを開いている場合は、一度終了し、20秒後に再度起動するとMCPサーバーが画面に表示されるようになります。問題が生じた際は、トラブルシューティングのセッションをご確認ください。 ステップ6: Claude Desktopとの連携設定 Claude Desktop アプリで、MCP サーバーが正しく認識されているか確認します。 Claudeの設定 Claude Desktop アプリを開きます。 「Claude」>「設定」>「開発者」に進みます。 「構成を編集」をクリックし、設定ファイル ディレクトリが開きます。(claude-desktop-configuration.json) を好きなIDEで開きます。 設定ファイル内に、以下のような MCP サーバーの設定が記述されているか確認します。 { "mcpServers": { "ElasticsearchMCP": { "command": "/Users/sam-koyama/.local/bin/uv", "args": [ "--directory", "/Users/sam-koyama/MCP/second/mcp-elasticsearch", "run", "main.py" ], "env": { "ELASTIC_USER": "elastic", "ELASTIC_PASSWORD": "3fik3ua3kXnbOlp0ThX41be1", "ELASTIC_CLOUD_URL": "https://my-deployment-a17327.es.ap-northeast-1.aws.found.io" } } } } 違う場合、トラブルシューティングの項を参照してください。 ツールの実行テスト 作成したツールを実際に Claude から実行してみましょう。 Claude Desktop アプリを開始します。 チャットウィンドウ下部に表示されているハンマーアイコン(利用可能な MCP ツールを示します)をクリックします。 先ほど作成したMCPから添付ツール(プラグのアイコン)を選択すると、「連携サービス選択」で追加したプロジェクトが確認できます。 ステップ7: Claudeからの動作確認 Claudeのチャットウィンドウで下記の様なクエリーを投げます。 「 blogsインデックスにあるドキュメントの数を教えて 」 初回利用時には、アクセス許可を求められることがあるので、「Allow for this chat」を選択します。 その前に正解をKibanaのコンソールで確認しましょう。 ということで、応答は「7つ」であれば、MCP サーバーと Claude の連携は成功です! トラブルシューティング:困ったときのヒント もし MCP サーバーが Claude Desktop に表示されない、またはエラーが表示された場合は、以下の点を確認してみてください。 設定ファイルのパス確認: claude-desktop-configuration.json 内の command フィールドが、UV の絶対パス(例: /usr/local/bin/uv)になっているか確認してください。修正後はファイルを保存し、 Claude Desktop を再起動します。 Claude Desktop の再起動: アプリを一度完全に終了し、再起動してみてください。 デスクトップフォルダへのアクセス許可: Claude Desktop がデスクトップフォルダへのアクセスを求めてきた場合は、必ず許可してください。 最後に 今回は、MCPの基本動作確認のため、Elasticsearch連携のシンプルなコードを使用しました。 現状のMCPには、サーバーセットアップの手作業やローカル設定の煩雑さといった技術的課題が見られますが、新しい技術であるため今後の発展が期待されます。 The post 自然言語でElasticsearchを操作!Python×Claude Desktop×MCP連携例 first appeared on Elastic Portal .
アバター
この記事では、Amazon EC2 上に Elastic Stack 8.x (Elasticsearch、Logstash、Kibana、Filebeat) をインストールしたときの手順を、備忘録として、またこれから同じことをやろうとしている方への参考としてまとめました。 実際に体験したつまずきポイントや工夫した点も交え、実践的な内容を目指しています。 目次 本題に入る前に Elastic Stack とは? 1. 前提条件の整理 2. EC2インスタンスを起動 3. SSH接続と初期パッケージの準備 4. Elasticsearchをインストール 5. Kibanaをインストール 6. Filebeatの導入と連携 7. ダッシュボード連携と仕上げ 8. 動作確認 最後に 本題に入る前に Elastic Stack とは? 各コンポーネントの役割は以下のとおりです。 Filebeat: EC2 サーバー内のログファイルを読み取る。 Logstash: Filebeat から受け取ったログを Elasticsearch 用に成形し転送する。 Elasticsearch: ログをインデックス化し、保存・検索できる状態にする。 Kibana: Elasticsearch に保存されたログをダッシュボードで可視化する。 Elastic Stack は、インフラ監視、アプリケーションパフォーマンス管理、セキュリティ分析など、幅広い用途で活用されています。 1. 前提条件の整理 使用OS:Amazon Linux 2 インスタンスタイプ:t2.medium(2vCPU、4GB RAM) 必要なポート: 22(SSH用) 5044(Filebeat→Logstash) 9200(Elasticsearch) 5601(Kibana) Java 11が必要(Amazon Correttoを使用) ここで気をつけたのは、 初期設定でセキュリティグループにこれらのポートを忘れずに開けること 。忘れると、後々アクセスできなくなって手間取ります。 Network setting –> Edit –> Security Group Rules 2. EC2インスタンスを起動 AWSマネジメントコンソールからEC2ダッシュボードに入り、「Launch instance」を選びました。 選択した構成 : AMI:Amazon Linux 2(64-bit Arm) インスタンスタイプ:t2.medium(コストパフォーマンスがよい) キーペア作成:新しい.pemファイルを作成し、すぐに安全な場所に保存 ストレージ:20GB以上を推奨(ログが増えるので余裕をもたせました) そしてセキュリティグループでは、自分のグローバルIPアドレスを指定して、前述のポートだけ開けました。 3. SSH接続と初期パッケージの準備 ターミナルから以下で接続: chmod 400 my-key.pem ssh -i my-key.pem ec2-user@<EC2 IP> その後、パッケージを最新にしてJava 11をインストール。 sudo yum update -y sudo rpm --import https://yum.corretto.aws/corretto.key sudo curl -L -o /etc/yum.repos.d/corretto.repo https://yum.corretto.aws/corretto.repo sudo yum install -y java-11-amazon-corretto java -version でちゃんと入ったか確認。 4. Elasticsearchをインストール Elasticの公式リポジトリを登録し、インストールします。 sudo rpm --import https://artifacts.elastic.co/GPG-KEY-elasticsearch 以下の内容で .repo ファイルを作成: sudo tee /etc/yum.repos.d/elasticsearch.repo <<EOF [elasticsearch] name=Elasticsearch repository baseurl=https://artifacts.elastic.co/packages/8.x/yum gpgcheck=1 gpgkey=https://artifacts.elastic.co/GPG-KEY-elasticsearch enabled=1 autorefresh=1 type=rpm-md EOF そしてインストール: sudo yum install -y elasticsearch インストール中に表示されるメッセージの中にパスワードが書かれています。後で必要なのでメモっておきます。 設定ファイルの変更 sudo nano /etc/elasticsearch/elasticsearch.yml 以下を追加: network.host: *.*.*.* curl https://checkip.amazonaws.com で返ってくるIPを使う.privateIPを使う discovery.seed_hosts: [] xpack.security.enabled: trueのまま進みます。 起動: sudo systemctl enable elasticsearch sudo systemctl start elasticsearch アクセス時はcurlにユーザー名とパスワードを指定: curl -u elastic:<PASSWORD> https://localhost:9200 --insecure 設定ファイルは以下のようにしました。 sudo nano /etc/logstash/conf.d/logstash.conf input { beats { port => 5044 } } output { if [@metadata][pipeline] { elasticsearch { hosts => ["https://***.**.**.**:9200"] user => "elastic" password => "パスワード" ssl => true ssl_certificate_verification => false manage_template => false index => "%{[@metadata][beat]}-%{[@metadata][version]}-%{+YYYY.MM.dd}" pipeline => "%{[@metadata][pipeline]}" } } else { elasticsearch { hosts => ["https://***.**.**.**:9200"] user => "elastic" password => "パスワード" ssl => true ssl_certificate_verification => false manage_template => false index => "%{[@metadata][beat]}-%{[@metadata][version]}-%{+YYYY.MM.dd}" } } } 有効にし起動させる。 sudo systemctl enable logstash sudo systemctl start logstash sudo systemctl status logstash 5. Kibanaをインストール sudo yum install -y kibana 起動と自動起動設定: sudo systemctl enable kibana sudo systemctl start kibana Elasticsearch 8.x 以降では、Kibana の内部動作に、広範な権限を持つ elastic ユーザーではなく、より権限が限定されたサービスアカウントトークンまたは専用のロールベースのユーザーアカウントを設定する必要があります。セキュリティ強化と不要な権限付与を防ぐための変更です。 kibana は “elastic” をユーザー名として使用できない ため、専用の kibana ユーザーを新規作成します: curl -u elastic:'<パスワード>' -k -X POST https://localhost:9200/_security/user/kibana_system_user \ -H "Content-Type: application/json" \ -d '{ "password": "KibanaSecure123!", "roles": ["kibana_system"], "full_name": "Kibana internal service user" }' 設定ファイル: sudo nano /etc/kibana/kibana.yml 変更する箇所: server.port: 5601 server.host: "0.0.0.0" elasticsearch.hosts: ["https://localhost:9200"] elasticsearch.username: "kibana_system_user" elasticsearch.password: "KibanaSecure123!" elasticsearch.ssl.verificationMode: none sudo systemctl restart kibana ブラウザでKibanaを確認。 http://<パブリックIPv4>:5601 Kibanaを起動する際に、usernameとpasswordの入力を求められます。このとき、elasticsearchのインストール中に画面に表示されたpasswordを入力します。 6. Filebeatの導入と連携 sudo yum install -y filebeat sudo nano /etc/filebeat/filebeat.yml 設定ファイル(重要!インデント注意): elastic outputセッションでコメントアウトする箇所: # output.elasticsearch: # hosts: ["localhost:9200"] logstash outputセッションでアンコメントする箇所: output.logstash: hosts: ["localhost:5044"] 他、変える箇所: filebeat.inputs: - type: log id: my-filestream-id enabled: true paths: - /var/log/*.log - /var/log/messages - /var/log/secure - /var/log/cron - /var/log/cloud-init.log - /var/log/cloud-init-output.log 起動してステータス確認: sudo systemctl enable filebeat sudo systemctl start filebeat sudo systemctl status filebeat ログを収集するシステムモジュールを有効に: sudo filebeat modules enable system システムモジュール設定ファイルを開く: sudo nano /etc/filebeat/modules.d/system.yml 以下のファイルセットが有効になっている(true に設定されている)ことを確認。 # Module: system # Docs: https://www.elastic.co/guide/en/beats/filebeat/8.17/filebeat-module-system.html - module: system # Syslog syslog: enabled: true # Set custom paths for the log files. If left empty, # Filebeat will choose the paths depending on your OS. #var.paths: # Use journald to collect system logs #var.use_journald: false # Authorization logs auth: enabled: true # Set custom paths for the log files. If left empty, # Filebeat will choose the paths depending on your OS. #var.paths: # Use journald to collect auth logs #var.use_journald: false 変更を反映するために、以下のコマンドを実行: $ sudo filebeat setup --pipelines --modules system Exiting: module system is configured but has no enabled filesets 7. ダッシュボード連携と仕上げ index managmentを設定: sudo filebeat setup \ -E output.logstash.enabled=false \ -E output.elasticsearch.hosts=["https://localhost:9200"] \ -E output.elasticsearch.username=elastic \ -E output.elasticsearch.password=<PASSWORD> \ -E output.elasticsearch.ssl.verification_mode=none \ -E setup.kibana.host="http://localhost:5601" Kibanaが立ち上がっている事を確認し、次Kibanaのdashboards と ingest pipelinesを設定。 sudo filebeat setup \ -E output.logstash.enabled=false \ -E output.elasticsearch.hosts=["https://172.**.40.11:9200"] \ -E output.elasticsearch.username=elastic \ -E output.elasticsearch.password=パスワード \ -E output.elasticsearch.ssl.verification_mode=none \ -E setup.kibana.host="http://localhost:5601" sudo systemctl start filebeat sudo systemctl enable filebeat インデックスをチェックして、Elasticsearch が Filebeat からデータを受信して​​いることを確認。 curl -XGET "https://172.**.40.11:9200/_cat/indices?v" \ -u elastic:パスワード \ --insecure 8. 動作確認 ブラウザ上のKibanaからDiscoverに移動し、filebeat-*を指定。 Kibanaの Discover から filebeat-* インデックスを確認できれば成功です! 最後に Elastic Stackを使い始める方のために、構築手順と役立つ情報をまとめました。内容に誤りや不足があるかもしれません。ご意見やご質問があれば、問い合わせフォームよりご連絡ください。 以下のコマンドは役立ちました: journalctl -u elasticsearch ps aux | grep elasticsearch SSH接続が切断されないように、SSH config ファイルを編集して、スリープモードに入るまでの時間を延長します。 nano ~/.ssh/config 以下のブロックを追加: Host * ServerAliveInterval 60 ServerAliveCountMax 5 The post AWS EC2にElastic Stackを構築した手順と気づきの記録 first appeared on Elastic Portal .
アバター
1. 前書き こんにちは。 前回に引き続き、ホワイトペーパー「Elasticsearchを使った簡易RAGアプリケーションの作成」に 記載した技術的要素を紹介いたします。(*脚注1 1 ) 今回は N-gram 検索です。 前半(Part 1)と後半(Part 2)の2回に分けて紹介します。 前半は N-gram 検索の基本的な考え方について、後半は実践的なサンプルアプリについて紹介します。 なお、今回使用した Elasticsearch のリクエストは、下記の GitHub リポジトリで公開しています。 GitHub - sios-elastic-tech/blogs: A sample code for blogs about elasticsearch. A sample code for blogs about elasticsearch. Contribute to sios-elastic-tech/blogs development by creating an account on... github.com 対象者 Elasticsearch の初心者~中級者 できるようになること Elasticsearch で N-gram を使った検索を行えるようになる。 前提条件 Elasticsearch (クラウド版でもオンプレミス版でも可。筆者は、version: 8.17.3 のオンプレミス版で動作確認しました) Elasticsearch に ICU analysis のプラグインを追加インストールしていること。(*脚注2 2 ) Elasticsearch に kuromoji analysis のプラグインを追加インストールしていること。(*脚注3 3 ) なお、今回のサンプルではベクトル検索は行いません。 (2025年04月09日時点の情報を元に記載しています。) 2. N-gram検索とは? Elasticsearch で日本語の形態素解析を行う場合、一般的には kuromoji を使って形態素解析が行われます。 その際 kuromoji が知らない単語 (*1) が含まれていた場合、正しく形態素解析が行われないことがあります。 (*1) 専門用語、社内用語、最新の単語、略語など。 「タイパ」を含む例を考えます。 GET /kakinosuke/_analyze { "analyzer": "ja_kuromoji_search_analyzer", "text": "最近の若者はタイパを⾮常に気にしているようです。" } この解析結果は下記のようになります。 ... "token": "タイ", ... "token": "パ", ... “タイパ” が “タイ” と “パ” に分割されて解析されてしまっています。 これは、kuromoji が “タイパ” という単語を知らないためです。 このような場合の1つの対策は、 Elasticsearchでのユーザー辞書登録を利用した検索 で紹介したユーザー辞書登録を行うことですが、ユーザー辞書登録は頻繁にできることではありません。ユーザー辞書登録を行うまでは検索できなくなります。 このような単語(kuromoji で正しく形態素解析が行われないような単語)を多く検索するような 場合には、N-gram検索を行うことも一つの解決方法になります。 日本語は、2~3文字程度の単語になることが多いので、あらかじめ 2~3文字に区切って保管しておき、それらを使って検索することができます。 N-gram (N=2) の場合、 「最近の若者はタイパを⾮常に気にしているようです。」 は、次のように分解されます。 登録される文字列 最近 近の の若 若者 者は はタ タイ イパ パを を非 … 3. N-gram検索のメリットとデメリット N-gram検索を行う場合のメリットとデメリットは、次の通りです。 メリット ユーザー辞書登録を行わなくても、kuromoji が正しく認識できない単語を検索できる可能性が高まる。 デメリット ノイズとして、関係ないドキュメントがヒットする場合がある。 N-gram用の保管領域を消費する。 4. N-gram の適切な N値 How to implement Japanese full-text search in Elasticsearch Learn how to implement full-text search in Japanese using Elasticsearch. We'll explore using n-gram and morphological an... www.elastic.co によれば、まずは N=2 で試してみて、それでいいパフォーマンスが得られない場合には、 N=3 を試してみるといった手法がいいようです。 今回のブログでも N=2 のケースを取り上げます。 ※なお、N=2 の N-gram のことを bi-gram (バイグラム)、 N=3 の N-gram のことを tri-gram (トリグラム)と呼びます。 5. 検証用インデックスの作成 以下のインデックスを検証用に作成します。 (あくまでも必要最低限の動作検証のため、同義語およびユーザー辞書は空としておきます。) インデックス作成リクエスト PUT /ngram_sample_202504 { "settings": { "index": { "number_of_shards": 1, "number_of_replicas": 1, "refresh_interval": "3600s" }, "analysis": { "char_filter": { "ja_normalizer": { "type": "icu_normalizer", "name": "nfkc", "mode": "compose" } }, "tokenizer": { "ja_kuromoji_tokenizer": { "mode": "search", "type": "kuromoji_tokenizer", "discard_compound_token": true, "user_dictionary_rules": [ ] }, "ja_ngram_tokenizer": { "type": "ngram", "min_gram": 2, "max_gram": 2, "token_chars": [ "letter", "digit" ] } }, "analyzer": { "ja_kuromoji_index_analyzer": { "type": "custom", "char_filter": [ "ja_normalizer", "kuromoji_iteration_mark" ], "tokenizer": "ja_kuromoji_tokenizer", "filter": [ "kuromoji_baseform", "kuromoji_part_of_speech", "cjk_width", "ja_stop", "kuromoji_number", "kuromoji_stemmer" ] }, "ja_ngram_index_analyzer": { "type": "custom", "char_filter": [ "ja_normalizer" ], "tokenizer": "ja_ngram_tokenizer", "filter": [ "lowercase" ] } } } }, "mappings": { "properties": { "chunk_no": { "type": "long" }, "content": { "type": "text", "analyzer": "ja_kuromoji_index_analyzer", "search_analyzer": "ja_kuromoji_index_analyzer", "fields": { "ngram": { "type": "text", "analyzer": "ja_ngram_index_analyzer", "search_analyzer": "ja_ngram_index_analyzer" } } } } } } 今回は、ngram を min=2, max=2 で作成します。(ja_ngram_tokenizer に設定します。) content フィールドに本文を格納し、content.ngram フィールドに N-gram 用に2文字ずつ区切った文字列を格納します。 6. 検証用ドキュメントの登録 検証用に以下のドキュメントを登録します。 POST /ngram_sample_202504/_doc { "chunk_no": 1, "content": "最近は、タイパを重視して行動する人が多くなった。" } POST /ngram_sample_202504/_doc { "chunk_no": 2, "content": "タイムズスクエア近くでパーティーがあった。" } POST /ngram_sample_202504/_doc { "chunk_no": 3, "content": "タイムズスクエア近くで何かのパフォーマンスが行われた。" } POST /ngram_sample_202504/_doc { "chunk_no": 4, "content": "タイには、なんとか「パ」というお店があるとか、ないとか。" } POST /ngram_sample_202504/_doc { "chunk_no": 5, "content": "タイには、「パ」で始まる名前が多いとか、少ないとか。" } POST /ngram_sample_202504/_doc { "chunk_no": 6, "content": "タイのパなんとかという地域に、珍しい食べ物があったとか、なかったとか。。" } POST /ngram_sample_202504/_refresh 7. N-gramを利用しない検索 まずは、N-gramを考慮せずに検索してみます。 GET /ngram_sample_202504/_search { "query": { "match": { "content": "タイパ" } } } レスポンス { ..., "hits": { "total": { "value": 4, "relation": "eq" }, "max_score": 1.2778894, "hits": [ { ..., "_source": { "chunk_no": 4, "content": "タイには、なんとか「パ」というお店があるとか、ないとか。" } }, { ..., "_source": { "chunk_no": 5, "content": "タイには、「パ」で始まる名前が多いとか、少ないとか。" } }, { ,,,, "_source": { "chunk_no": 6, "content": "タイのパなんとかという地域に、珍しい食べ物があったとか、なかったとか。。" } }, { ..., "_source": { "chunk_no": 1, "content": "最近は、タイパを重視して行動する人が多くなった。" } } ] } } 「タイパ」を含むドキュメントもヒットしてはいますが、それよりも高いスコアで「タイパ」を含まないドキュメントがヒットしてしまっています。 これは、前述のとおり、「タイ」と「パ」それぞれで検索が行われているためです。(*脚注4 4 ) 8. N-gramを利用した検索 N-gramだけを検索してもいいのですが、Elasticsearch の強力な検索機能を使って content フィールドと content.ngram フィールドの両方を検索してみます。 その際、content.ngram の方でマッチしたものを重視するよう _score を5倍にします。 GET /ngram_sample_202504/_search { "query": { "multi_match": { "fields": [ "content.ngram^5", "content" ], "query": "タイパ" } } } レスポンス { ..., "hits": { "total": { "value": 6, "relation": "eq" }, "max_score": 6.310552, "hits": [ { ..., "_source": { "chunk_no": 1, "content": "最近は、タイパを重視して行動する人が多くなった。" } }, { ..., "_source": { "chunk_no": 4, "content": "タイには、なんとか「パ」というお店があるとか、ないとか。" } }, { ..., "_source": { "chunk_no": 5, "content": "タイには、「パ」で始まる名前が多いとか、少ないとか。" } }, { ..., "_source": { "chunk_no": 6, "content": "タイのパなんとかという地域に、珍しい食べ物があったとか、なかったとか。。" } }, { ..., "_source": { "chunk_no": 2, "content": "タイムズスクエア近くでパーティーがあった。" } }, { ..., "_source": { "chunk_no": 3, "content": "タイムズスクエア近くで何かのパフォーマンスが行われた。" } } ] } } 「タイパ」を含むドキュメントが上位に来るようになりました。 しかし、「タイパ」を含まないものも、下位ではありますが返却されています。 “min_score” を指定することで、これらを除去することができます。 GET /ngram_sample_202504/_search { "query": { "multi_match": { "fields": [ "content.ngram^5", "content" ], "query": "タイパ" } }, "min_score": 5 } レスポンス { ..., "hits": { "total": { "value": 1, "relation": "eq" }, "max_score": 6.310552, "hits": [ { ..., "_source": { "chunk_no": 1, "content": "最近は、タイパを重視して行動する人が多くなった。" } } ] } } これで、「タイパ」を含むもののみが返却されるようになりました。 ただし、min_scoreにあまりにも高い数値を設定してしまうと、本来はマッチしているドキュメントまでも返却されなくなる恐れがあるので注意が必要です。 9. N-gram検索の問題 さきほど、【3. N-gram検索のメリットとデメリット】で、ノイズを拾いやすくなるデメリットがある、と書きました。 わかりやすい例を挙げたいと思います。 例えば、インデックスに次のドキュメントがあったとします。 POST /ngram_sample_202504/_doc?refresh=true { "chunk_no": 7, "content": "あの通りの向こうに、ムエタイパークがあるとか、ないとか。" } これで、再度、下記の検索を行います。 GET /ngram_sample_202504/_search { "query": { "multi_match": { "fields": [ "content.ngram^5", "content" ], "query": "タイパ" } }, "min_score": 5 } レスポンス { ..., "hits": { "total": { "value": 2, "relation": "eq" }, "max_score": 6.310552, "hits": [ { ..., "_source": { "chunk_no": 1, "content": "最近は、タイパを重視して行動する人が多くなった。" } }, { ..., "_source": { "chunk_no": 7, "content": "あの通りの向こうに、ムエタイパークがあるとか、ないとか。" } } ] } } ムエタイパークを含むドキュメントがヒットしてしまいます。 これは、「ムエ『タイパ』ーク」の中に「タイパ」が含まれているためです。 このように、N-gram検索では、人間が見れば無関係と思われるドキュメントもヒットしてしまうデメリットもあります。 10. まとめ N-gram検索を利用することで、ユーザー辞書登録を行わなくても、kuromoji が認識できない単語を含むドキュメントを検索できるようになりました。 次回は、N-gram 検索を利用したサンプルアプリを紹介する予定です。 ホワイトペーパー「Elasticsearchを使った簡易RAGアプリケーションの作成」は、下記からダウンロードできます。(E-mailアドレスなどの入力が必要です。) https://elastic.sios.jp/whitepaper/ ↩︎ 下記に ICU analysis plugin のインストール方法が記載されています。 https://www.elastic.co/guide/en/elasticsearch/plugins/current/analysis-icu.html ↩︎ 下記に kuromoji analysis plugin のインストール方法が記載されています。 https://www.elastic.co/guide/en/elasticsearch/plugins/current/analysis-kuromoji.html ↩︎ 「タイパ」だけを検索したいのであれば、”match_phrase” で検索することも可能です。 しかし、「タイパ」と別の何かを検索したい場合、”match_phrase” で検索しようとすると、 クエリーが複雑になってしまうデメリットがあります。 ↩︎ The post Elasticsearch での N-gram 検索 (Part 1) first appeared on Elastic Portal .
アバター
1. 前書き こんにちは。 前回に引き続き、ホワイトペーパー「Elasticsearchを使った簡易RAGアプリケーションの作成」に 記載した技術的要素を紹介いたします。(*脚注1 1 ) 今回は、同義語の利用です。 対象者 Elastic Cloud のアカウントをお持ちの方(トライアルライセンスを含む) Elasticsearch の初心者~中級者 できるようになること Elasticsearch に同義語の登録ができる。 Elasticsearch で同義語を考慮した検索を実行できる。 前提条件 Elastic Cloud (version: 8.17.3) Elasticsearch で日本語の形態素解析の設定を行っていること Elasticsearchでの日本語に適したインデックスの作成 の回で、日本語の形態素解析の設定方法を紹介しています。 (2025年04月08日時点の情報を元に記載しています。) 2. 同義語とは? Elasticsearch でキーワード検索を行う場合、検索文字列と同じ文字列を持つドキュメントが検索されます。 たとえば、”おにぎり” で検索した場合、”おにぎり” を含むドキュメントはヒットしますが、 同じような意味を持つ “おむすび” を含むドキュメントはヒットしません。 似たような意味を含むドキュメントを検索したい場合の対策として、同義語を登録する方法を取り上げます。(*脚注2 2 ) 例) “おむすび” の同義語として “おにぎり” を登録することで、 “おにぎり” を検索した場合でも “おむすび” を含むドキュメントが検索されるようになります。 3. 同義語登録前の動作確認 前回の Elasticsearchでのユーザー辞書登録を利用した検索 で、 「桃太郎」を次のように改変した「柿之助」を kakinosuke_202504 インデックスに登録済です。 桃太郎 → 柿之助 きびだんご → おむすび 猿 → ゴリラ 犬 → 猫 雉 → 鷹 kakinosuke_202504 インデックスに対し、”おにぎり” で検索してみます。 リクエスト GET /kakinosuke/_search { "_source": false, "query": { "match": { "content": "おにぎり" } } } (kakinosuke は、kakinosuke_202504 インデックスに対するエイリアスです。) レスポンス { ..., "hits": { "total": { "value": 0, "relation": "eq" }, ... } } “おにぎり” という単語を含んだドキュメントがないため、ヒットしません。 4. インデックスへの同義語の追加 同義語をインデックスに追加します。 同義語を参照するタイミングとして、2つが考えられます。 ドキュメントの登録時に同義語を参照する。 ドキュメントの検索時に同義語を参照する。 今回は、ドキュメントの検索時に同義語を参照するよう、設定します。(*脚注3 3 ) なお、同義語の追加・変更・削除は、Synonyms API を使って行います。(*脚注4 4 ) 4.1 synonyms_set への同義語の追加 Bulk API (Pythonからのドキュメントの一括登録) で、同義語セット kakinosuke_synonyms_set の枠を作成していました(作成時点の中身は空っぽ)。 kakinosuke_202504 インデックスと kakinosuke_synonyms_set の関係を表す概略図 kakinosuke_synonyms_set に同義語を追加していきます。 下記のリクエストを Elastic Cloud の Console から発行します。 PUT _synonyms/kakinosuke_synonyms_set { "synonyms_set": [ { "synonyms": "おむすび,おにぎり" } ] } 5. 検索 同義語を追加したので、もう一度、”おにぎり” で検索してみます。(*脚注5 5 ) リクエスト GET /kakinosuke/_search { "_source": false, "fields": ["chunk_no", "content"], "query": { "match": { "content": "おにぎり" } } } レスポンス { ..., "hits": { "total": { "value": 9, "relation": "eq" }, "max_score": 2.0755403, "hits": [ { ..., "fields": { "chunk_no": [ 20 ], "content": [ "とたずねました。「悪霊島へ悪霊せいばつに行くのだ。」「お腰に下げたものは、何でございます。」「日本一のおむすびさ。」「一つ下さい、お供しましょう。」「よし、よし、やるから、ついて来い。」鷹もおむすびを一つもらって、柿之助のあとからついて行きました。" ] } }, ... ] } } “おにぎり” で検索を行いましたが、”おにぎり” の同義語である “おむすび” にマッチしたドキュメントがヒットしていることがわかります。 なお、ハイライト表示を行う指示をしていた場合、”おにぎり” の同義語である “おむすび” が強調表示されます。 リクエスト GET /kakinosuke/_search { "_source": false, "query": { "match": { "content": "おにぎり" } }, "highlight": { "fields": { "content": {} } } } レスポンス { ..., "hits": { ..., "hits": [ { ..., "highlight": { "content": [ "「日本一の<em>おむすび</em>さ。」「一つ下さい、お供しましょう。」「よし、よし、やるから、ついて来い。」鷹も<em>おむすび</em>を一つもらって、柿之助のあとからついて行きました。" ] } }, ... ] } } Elasticsearchでの検索結果のハイライト表示 の回で Streamlit を使ってハイライト表示していました。この検索アプリで “おにぎり” を検索して、検索結果を画面に表示してみます。 “おにぎり” を検索しましたが、 “おむすび” にマッチした部分がハイライト表示されていることがわかります。 ただし、4番目にヒットしている内容には、”おにぎり” も “おむすび” もヒットしていません。 推測になりますが、”おにぎり” の密ベクトルと、4番目のドキュメントの密ベクトルが近い値になっていたものと思われます。 “おにぎり” と関連がありそうな、 “お湯” や人物(”おじいいさん”, “おばあさん”, “子”)、”手” を含むドキュメントがヒットした可能性があります。 (上記の検索アプリは、キーワード検索と密ベクトルのハイブリッド検索を実行しています。) ※参考までに、下記のようなリクエストを発行すれば、密ベクトル検索のみを行うことができます。 GET /kakinosuke/_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": "おにぎり" } } } } 6. 発展形 このブログでは人間が同義語を考えて追加しましたが、LLM に同義語を列挙させる、といったテクニックもあります。 詳細については、(*脚注6 6 ) のブログを参照してください。 7. まとめ 同義語登録を行うことで、似たような意味を持つ単語もキーワード検索できるようになりました。 このようなチューニングを行うことで、RAG での検索結果を改善し、最終的な回答の精度向上につながり場合もあります。 ホワイトペーパー「Elasticsearchを使った簡易RAGアプリケーションの作成」は、下記からダウンロードできます (E-mailアドレスなどの入力が必要です)。 https://elastic.sios.jp/whitepaper/ ↩︎ 似たような意味の言葉を検索する方法として、同義語を登録する方法以外に、 – 疎ベクトル検索を行う。 – 密ベクトル検索を行う。 といった方法もあります。 ただし、5. 検索のところで説明したように、ヒットしてほしくないドキュメントにヒットしてしまう可能性もあります。 ↩︎ 下記の Elasticsearch 社のブログでも書かれていますが、ドキュメントの検索時に同義語を検索することが推奨されているようです。 ドキュメントの登録時に同義語を参照した方が検索は速くなるのですが、 同義語を追加・変更・削除するたびにドキュメントの再登録が必要となり、管理が煩雑になってしまいます。 検索時に同義語を参照するのであれば、同義語を追加・変更・削除してもドキュメントの再登録は必要ありません。 同義語について言及している Elasticsearch 社のブログ https://www.elastic.co/jp/blog/boosting-the-power-of-elasticsearch-with-synonyms ↩︎ 同義語は、下記の Synonyms API を利用して登録します。 https://www.elastic.co/guide/en/elasticsearch/reference/current/put-synonyms-set.html (Synonyms API は、Elasticsearch 8.10 から利用できるようになりました。) なお、同義語を登録後、個別の同義語を削除・変更したい場合には、”id” を明示する必要があります。 同義語を登録後に、削除・変更する可能性がある場合には、あらかじめ “id” を明示して登録しておくことをおすすめします。 ↩︎ Bulk API (Pythonからのドキュメントの一括登録) で、index の mappings に同義語参照時の設定をあらかじめ "updateable": true と指定していたので、同義語を追加しただけで検索結果が変わります。 ↩︎ LLM を使って同義語を自動登録するテクニックについて紹介している Elasticsearch 社のブログ https://www.elastic.co/search-labs/blog/elasticsearch-synonyms-automate ↩︎ The post Elasticsearchでの同義語を利用した検索 first appeared on Elastic Portal .
アバター
1. 前書き こんにちは。 前回に引き続き、ホワイトペーパー「Elasticsearchを使った簡易RAGアプリケーションの作成」(*脚注1 1 )に記載した技術的要素を紹介いたします。 今回は、ユーザー辞書登録についてです。 対象者 Elastic Cloud のアカウントを持っている人(トライアルライセンスを含む) Elasticsearch の初心者~中級者 できるようになること Elasticsearch のユーザー辞書への登録ができるようになる。 Elasticsearch でユーザー辞書に登録された単語を含む検索ができるようになる。 前提条件 Elastic Cloud (version: 8.17.3) Elasticsearch で日本語の形態素解析の設定を行っていること 日本語に適したインデックスの作成 の回で、日本語の形態素解析の設定方法を紹介しています。 (2025年04月02日時点の情報を元に記載しています。) 2. ユーザー辞書とは? Elasticsearch で日本語の検索を行う場合、一般的には kuromoji を使って形態素解析を行います。(*脚注2 2 ) しかし、kuromoji が知らない単語が含まれている場合、適切に形態素解析が行われないことがあります。 例) 社内の独自用語など。弊社の社名「サイオス」は、kuromoji が知らないため、「サイ」と「オス」に分割されてしまいます。 kuromoji が作られた時点では存在しなかった、あるいは一般的ではなかった単語。「クラウド」「タイパ」など。 このような単語を扱う場合、ユーザー辞書に登録することで、適切な形態素解析が行われ、その後の日本語での検索が正しく行われるようになります。 3. ユーザー辞書登録前の動作確認 前回の Elasticsearchでの検索結果のハイライト表示 では「柿之助」を検索したところ、「柿」もヒットしてしまいました。 これは、「柿之助」という単語を kuromoji が知らないため、「柿」と「助」に分解されていたことによるものです。 念のため、analyzer による動作を確認してみます。 下記のリクエストを Elastic Cloud の Console から実行してみます。 GET /kakinosuke/_analyze { "analyzer": "ja_kuromoji_search_analyzer", "text": "柿之助" } ※注 “ja_kuromoji_search_analyzer” は、 日本語に適したインデックスの作成 の回で登録した日本語用のアナライザーです。 レスポンス { "tokens": [ { "token": "柿", "start_offset": 0, "end_offset": 1, "type": "word", "position": 0 }, { "token": "助", "start_offset": 2, "end_offset": 3, "type": "word", "position": 2 } ] } 「柿之助」が「柿」と「助」に分解されてしまっていることがわかります。 (「之」は接続詞とみなされ除去されています。) 念のため、”柿之助” で検索した場合の検索結果を確認しておきます。 GET /kakinosuke/_search { "query": { "match": { "content": "柿之助" } }, "size": 30 } レスポンス ... "hits": { "total": { "value": 29, "relation": "eq" }, ... "_source": { "chunk_no": 4, ..., "content": "...さっきの柿を...この柿を。...みごとな柿を..." }, ... } 全体で29件ヒットして、そのうち、数件は「柿」のみにマッチしたドキュメントが返却されていることがわかります。 4. ユーザー辞書登録を行ったインデックスの作成 Bulk API (Pythonからのドキュメントの一括登録) の回で作成した kakinosuke_202503 インデックスではユーザー辞書登録を行っていませんでした。 今回は、kakinosuke_202503 インデックスとは別に、kakunosuke_202504 インデックスを作成し、ユーザー辞書登録を行います。(*脚注3 3 ) インデックス名 ユーザー辞書 kakinosuke_202503 登録なし kakinosuke_202504 “柿之助”を登録 kakinosuke_202504 インデックスの作成リクエスト PUT /kakinosuke_202504 { "settings": { "index": { "number_of_shards": 1, "number_of_replicas": 1, "refresh_interval": "3600s" }, "analysis": { "char_filter": { "ja_normalizer": { "type": "icu_normalizer", "name": "nfkc_cf", "mode": "compose" } }, "tokenizer": { "ja_kuromoji_tokenizer": { "mode": "search", "type": "kuromoji_tokenizer", "discard_compound_token": true, "user_dictionary_rules": [ "柿之助,柿之助,カキノスケ,カスタム名詞" ] } }, "filter": { "ja_search_synonym": { "type": "synonym_graph", "synonyms_set": "kakinosuke_synonyms_set", "updateable": true } }, "analyzer": { "ja_kuromoji_index_analyzer": { "type": "custom", "char_filter": [ "ja_normalizer", "kuromoji_iteration_mark" ], "tokenizer": "ja_kuromoji_tokenizer", "filter": [ "kuromoji_baseform", "kuromoji_part_of_speech", "cjk_width", "ja_stop", "kuromoji_number", "kuromoji_stemmer" ] }, "ja_kuromoji_search_analyzer": { "type": "custom", "char_filter": [ "ja_normalizer", "kuromoji_iteration_mark" ], "tokenizer": "ja_kuromoji_tokenizer", "filter": [ "kuromoji_baseform", "kuromoji_part_of_speech", "cjk_width", "ja_stop", "kuromoji_number", "kuromoji_stemmer", "ja_search_synonym" ] } } } }, "mappings": { "dynamic": false, "_source": { "excludes": [ "text_embedding.*" ] }, "properties": { "chunk_no": { "type": "integer" }, "content": { "type": "text", "analyzer": "ja_kuromoji_index_analyzer", "search_analyzer": "ja_kuromoji_search_analyzer" }, "text_embedding": { "properties": { "model_id": { "type": "keyword", "ignore_above": 256 }, "predicted_value": { "type": "dense_vector", "dims": 384 } } } } } } user_dictionary_rules がユーザー辞書登録の部分になります。 user_dictionary_rules に「柿之助」を登録します。 試しに、analyze を行ってみます。 下記のリクエストを Elastic Cloud の Console から実行してみます。 GET /kakinosuke_202504/_analyze { "analyzer": "ja_kuromoji_search_analyzer", "text": "柿之助" } レスポンス { "tokens": [ { "token": "柿之助", "start_offset": 0, "end_offset": 3, "type": "word", "position": 0 } ] } 「柿之助」が、「柿」と「助」に分解されずに、「柿之助」のままとなっています。 5. Reindex API の実行 Reindex API を使って、既存の kakinosuke_202503 インデックスから kakinosuke_202504 インデックスへドキュメントをコピーします。 POST _reindex?refresh=true { "source": { "index": "kakinosuke_202503", "_source": [ "chunk_no", "content" ] }, "dest": { "index": "kakinosuke_202504", "pipeline": "japanese-text-embeddings" } } Reindex を行う際に、以下の指定を行っています。 “refresh=true” Reindex 終了後に refresh を行います。 “source”: { “index”: “kakinosuke_202503”, … } コピー元のインデックスが “kakunosuke_202503” であることを指定します。 “_source” : [ “chunk_no”, “content” ] コピー対象のフィールド名を明記しておきます。すべてコピーする場合には省略可能ですが、今回はわかりやすいよう、あえて明記しています。 “dest”: { “index”: “kakinosuke_202504”, … } コピー先のインデックスが “kakunosuke_202504” であることを指定します。 “pipeline”: “japanese-text-embeddings” 文字列を再登録する際に、ベクトルを生成します。コピー元の kakinosuke_202501 インデックスでは、密ベクトルを _source に保持していないのでコピーすることができません。Reindex 実行時に、再度、pipeline を通すことで、密ベクトルを再作成します。 ※ “japanese-text-embeddings” パイプラインは、 ベクトル検索の準備 の回で登録した、密ベクトル生成用のパイプラインです。 ※ドキュメントを再度登録しないとユーザー辞書の内容が反映されないのが欠点です。 データ量が多いと、Reindex の完了には時間がかかります。 ですので、ユーザー辞書への登録タイミングは慎重に検討してください。 頻繁に行うのは、好ましくありません。 ユーザー辞書への登録が反映されているかどうか、確認してみます。 GET /kakinosuke_202504/_search { "query": { "match": { "content": "柿之助" } }, "size": 30 } レスポンス "hits": { "total": { "value": 23, "relation": "eq" }, ... "_source": { "chunk_no": ..., ..., "content": "...柿之助..." }, ... } さきほど29件ヒットしていたものが23件になっています。 また、「柿之助」にヒットしていますが、「柿」のみヒットしている箇所がないことがわかります。 6. エイリアスの再作成 エイリアス kakinosuke を再作成します。 変更前の参照先 変更後の参照先 kakinosuke_202503 kakinosuke_202504 下記のリクエストを Elastic Cloud の Console から実行します。 古いエイリアスを削除して、新しいエイリアスを作成します。 POST _aliases { "actions": [ { "remove": { "index": "kakinosuke_202503", "alias": "kakinosuke" } }, { "add": { "index": "kakinosuke_202504", "alias": "kakinosuke" } } ] } 7. 検索アプリの実行 前回 の検索アプリを実行してみます。 Streamlit で作成した検索アプリに ハイライト表示する:「はい」 検索クエリ:「柿之助 おばあさん」 を入力して、【検索】ボタンを押すと、次のような画面が表示されます。 「柿之助」と「おばあさん」のみが強調されていることがわかります。「柿」のみの箇所は強調されていません。 8. まとめ ユーザー辞書を登録することで、kuromoji が知らない単語を含む文章も適切に形態素解析が行われ、検索できるようになりました。この手法を用いて RAG での検索結果を改善し、最終的な回答の精度向上につながることもあります。 次回は、同義語について説明する予定です。 ホワイトペーパー「Elasticsearchを使った簡易RAGアプリケーションの作成」は、下記よりダウンロード可能です(E-mailアドレスなどの入力が必要です)。 https://elastic.sios.jp/whitepaper/ ↩︎ Elasticsearch が利用している kuromoji は、やや古い辞書を参照しています。このため、最近使われるようになった単語を知らない場合があります。 例)「クラウド」 → 「くら(蔵)」+「うど」に分解されます。 この問題に対応するため、kuromoji よりも新しい形態素解析エンジンを使う方法もあります。 例)sudachi ただし、”sudachi” が知らない単語(社内用語や専門用語など)を形態素解析したい場合は、やはりユーザー辞書登録するなどの対策が必要となります。 ↩︎ ※別のインデックスを作成せずに、元のインデックスのままユーザー辞書を登録する方法もあります。 その場合は、 – 1. index_name/_close – 2. ユーザー辞書登録 – 3. index_name/_open – 4. index_name/_update_by_query を行う必要があります。 ベクトルの再作成は必要ありません。その分、全体的な処理時間は短く済む可能性があります。 また、データ容量として1つ分のインデックスで済むので、(一時的であっても)2つのインデックスを保持する方法より少なくて済みます。 ただし、前述の手順の 1~4 の間、元のインデックスは利用できなくなります。(検索サービスを停止するなどの措置が必要です。) また、ユーザー辞書登録に問題が見つかってやり直したい場合、 – 事前に取得しておいた Snapshot から Restore する。 あるいは – ユーザー辞書を修正してから再度 _update_by_query を実行する(上記の 1~4 をやり直す)。 などの復旧策が必要となります。 いずれにしても、その間、元のインデックスは使えなくなります。サービスを一定時間停止してもいいような場合では、こちらの方法を採用するのもいいかもしれません。 なお、2つのインデックスを使う方法であれば、エイリアスを切り替えるだけで元に戻せます(ほんの一瞬です)。 ↩︎ The post Elasticsearchでのユーザー辞書登録を利用した検索 first appeared on Elastic Portal .
アバター
1. 前書き 前回に引き続き、ホワイトペーパー「Elasticsearchを使った簡易RAGアプリケーションの作成」(*脚注1 1 )に記載した技術的要素を紹介いたします。 今回は、検索結果のハイライト表示です。 なお、このブログ内に記載しているソースコードおよび Elasticsearch 用のリクエストは、下記のgithub リポジトリでも公開しています。 blogs/2025-04-highlight at main · sios-elastic-tech/blogs A repository for blog sources. Contribute to sios-elastic-tech/blogs development by creating an account on GitHub. github.com 対象者 Elastic Cloud のアカウントを持っている人(トライアルライセンスを含む) Elasticsearch の初心者~中級者 できるようになること Elasticsearch でキーワード検索(またはハイブリッド検索)した結果のハイライト表示を行えるようになる。 前提条件 Elastic Cloud (version: 8.17.3) Python 3.13 Streamlit 1.42.2 Elastic Cloud 上で、日本語検索をするための準備が完了していること。 Elastic Cloud 上で、検索対象となるインデックスを作成済であること。 Elastic Cloud 上で、検索対象となるインデックスにドキュメントを登録済であること。 ※前回の [Bulk API] で、検索対象となるインデックスへのドキュメント登録を行っています。 ※あくまでもサンプル用のソースコードなので、実運用で利用する場合には、エラー処理やセキュリティ対策を補足してください。 (2025年03月31日時点の情報を元に記載しています。) 2. ハイライト表示とは? 検索にマッチした部分を強調したり、色を変えたりしてハイライト表示させる機能です。 Elasticsearch では、キーワード検索でマッチしたキーワード部分をハイライト表示することができます。 キーワード検索+密ベクトル検索のハイブリッド検索でも、ハイライト表示することが可能です。 密ベクトル検索のみでは、ハイライト表示できません。 3. ハイライト表示を指示するAPI Highlighting | Elasticsearch Guide [8.17] | Elastic www.elastic.co 検索結果をハイライト表示するには、Search API で、”highlight” 句を指定します。 ハイライト表示を行うリクエストの例: GET /index_name/_search {   "query": {     "match": {       "content": "検索文字列"     }   },   "highlight": {     "pre_tags": ["<strong>"],     "post_tags": ["</strong>"],     "fields": {       "content": {}     }   } } このリクエストを実行すると、次のようなレスポンスが返ってきます。 {   ...   "hits": {     ...     "hits": [       {         "_index": "index_name",         "_id": "...",         ...,         "highlight": {           "content": [             " ... <strong>ヒットした文字列</strong> ..."           ]         }       },       {         ...       }     ]   } } “highlight” の部分を取得することで、画面にハイライト表示できるようになります。 (*脚注2 2 ) 4. ハイブリッド検索用テンプレートの修正 さきほどの内容を踏まえ、ハイライト表示できるよう、ハイブリッド検索用テンプレートを以下のようにします。 変更前のハイブリッド検索用テンプレート(ハイライトに非対応) PUT _scripts/rrf_search_template {   "script": {     "lang": "mustache",     "source": """{       "_source": false,       "fields": [ "chunk_no", "content" ],       "size": "{{size}}{{^size}}10{{/size}}",       "retriever": {         "rrf": {           "retrievers": [             {               "standard": {                 "query": {                   "match": {                     "content": "{{query_string}}"                   }                 }               }             },             {               "knn": {                 "field": "text_embedding.predicted_value",                 "k": "{{size}}{{^size}}10{{/size}}",                 "num_candidates": "{{num_candidates}}{{^num_candidates}}100{{/num_candidates}}",                 "query_vector_builder": {                   "text_embedding": {                     "model_id": ".multilingual-e5-small_linux-x86_64",                     "model_text": "{{query_for_vector}}"                   }                 }               }             }           ],           "rank_window_size": "{{size}}{{^size}}10{{/size}}",           "rank_constant": "{{rank_constant}}{{^rank_constant}}20{{/rank_constant}}"         }       }     }     """   } } 変更後のハイブリッド検索用テンプレート(ハイライトに対応) PUT _scripts/rrf_search_template {   "script": {     "lang": "mustache",     "source": """{       "_source": false,       "fields": [ "chunk_no", "content" ],       "size": "{{size}}{{^size}}10{{/size}}",       "retriever": {         "rrf": {           "retrievers": [             {               "standard": {                 "query": {                   "match": {                     "content": "{{query_string}}"                   }                 }               }             },             {               "knn": {                 "field": "text_embedding.predicted_value",                 "k": "{{size}}{{^size}}10{{/size}}",                 "num_candidates": "{{num_candidates}}{{^num_candidates}}100{{/num_candidates}}",                 "query_vector_builder": {                   "text_embedding": {                     "model_id": ".multilingual-e5-small_linux-x86_64",                     "model_text": "{{query_for_vector}}"                   }                 }               }             }           ],           "rank_window_size": "{{size}}{{^size}}10{{/size}}",           "rank_constant": "{{rank_constant}}{{^rank_constant}}20{{/rank_constant}}"         }       }       {{#highlight}}       ,       "highlight": {         "fields": {           "content": {},           "pre_tags": ["<strong>"],           "post_tags": ["</strong>"]         }       }       {{/highlight}}     }     """   } } 検索パラメーター: “highlight” に true を指定することでハイライト表示が行われるようになります。false を渡した場合は、ハイライト表示は行われません。 5. 検索用 API Key の作成 Python から Elastic Cloud にアクセスするには、Access Key が必要です。 前回の [Bulk API] の中で、読み書き用の Access Key を作成しましたが、今回は検索用として、読み取り専用の Access Key を作成します。 (読み書き用の Access Key を使って、読み取ることも可能ですが、予期せぬ事故を回避するため、読み取り専用の Access Key を使った方が安全です。) 読み取り専用の Access Key を作成するには、下記のリクエストを Elastic Cloud の Console から実行します。 (Elastic Cloud の Console の表示方法は、 [Elasticsearch へのインデックスの作成] の Dev Tools に関する説明を参照してください。) 読み取り専用 Access Key の作成リクエスト POST /_security/api_key { "name": "kakinosuke_read_api_key", "role_descriptors": { "kakinosuke_read_role": { "cluster": ["all"], "indices": [ { "names": ["kakinosuke*"], "privileges": ["read"] } ] } } } このリクエストを実行した際に返却される encode された値を、6.1. の .env ファイルに転記します。 6. ソースコードの説明 これらを踏まえて、ハイブリッド検索の結果をハイライト表示できるようにします。 ソースコード全体を見たい方は、下記のgithub リポジトリを参照してください。 blogs/2025-04-highlight at main · sios-elastic-tech/blogs A repository for blog sources. Contribute to sios-elastic-tech/blogs development by creating an account on GitHub. github.com 以下、ソースコードの抜粋です。 6.1. .env elasticsearch_endpoint='' read_api_key_encoded='' 接続に必要な key などを記載します。 elasticsearch_endpoint の取得方法は、2025年03月に公開したブログ [Bulk API] を参照してください。 read_api_key_encoded には、5. で作成した検索用の API Key の encode された値を転記します。 6.2. docker-compose.yml services: highlight_sample_202504: build: context: ./ dockerfile: Dockerfile container_name: 'highlight_sample_202504' volumes: - ./:/app ports: - 8501:8501 6.3. Dockerfile FROM python:3.13 WORKDIR /app COPY ./requirements.txt ./ RUN pip install --upgrade pip RUN pip install -r ./requirements.txt RUN groupadd -r appgroup && useradd -r -s /usr/sbin/nologin -g appgroup appuser COPY --chown=appuser:appgroup ./ /app USER appuser ENV PYTHONUNBUFFERED 1 ENTRYPOINT ["/bin/sh", "-c", "while :; do sleep 30; done"] 6.4. requirements.txt elasticsearch==8.17.2 python-dotenv==1.0.1 streamlit==1.42.2 今回のプログラムで必要となるライブラリです。 6.5. src/app.py ... def search(es_client: Elasticsearch, query: str, show_highlight: str = '') -> list: ... def search_onclick(): ... # 4. 検索結果の表示 for i, results in enumerate(search_results): st.write(f'-- {i+1} --') for sub_result in results: st.write(f'{sub_result}', unsafe_allow_html=True) def show_search_ui(): ... if __name__ == '__main__': ... 検索アプリの本体です。Streamlit から呼び出します。 <strong>~</strong> で囲まれた部分をハイライト表示できるように、unsafe_allow_html=True を指定しています。 6.6. src/elastic/es_consts.py ... # 検索対象のインデックス(のエイリアス) SEARCH_INDEX = 'kakinosuke' # 検索テンプレートのId SEARCH_TEMPLATE_ID = 'rrf_search_template' # ドキュメントの本文を格納しているフィールド名 CONTENT_FIELD_NAME = 'content' Elasticsearch 関連の定数を定義しています。 6.7. src/elastic/es_func.py ... def create_es_client(elasticsearch_endpoint: str, api_key_encoded: str) -> Optional[Elasticsearch]: ... def initialize_es(my_env: Dict[str, str]) -> Optional[Elasticsearch]: ... def create_search_params(query: str, highlight: bool = False) -> Mapping[str, any]: ... return { QUERY_STRING : query, QUERY_FOR_VECTOR : query, HIGHLIGHT : highlight } def extract_search_results(search_results, has_highlight: bool = False, field_name: str = CONTENT_FIELD_NAME, max_count: int = MAX_DOCS_COUNT) -> List[str]: ... def es_search_template(es_client: Elasticsearch, search_params: Mapping[str, any], search_index: str = SEARCH_INDEX, search_template_id: str = SEARCH_TEMPLATE_ID, field_name: str = CONTENT_FIELD_NAME, max_count: int = MAX_DOCS_COUNT) -> List[str]: ... Elasticsearch 関連の関数を集めたファイルです。ハイライト表示に対応させます。 7. 実行 Docker 上のコンテナにデプロイしていきます。 (ここでは Docker を利用していますが、Docker は必須ではありません。) 7.1. ビルド docker compose build 7.2. コンテナの起動 docker compose up -d 7.3. コンテナ内で bash を実行 docker exec -it highlight_sample_202504 /bin/bash “highlight_sample_202504” はコンテナ名です。 直接、streamlit を実行してもよいのですが、エラーが起きた場合などのハンドリングが小難しくなってしまうので、いったん、bash を経由してから実行します。 7.4. 検索プログラムの開始 さきほどコンテナ内で実行した bash から、下記のコマンドを実行します。 (Streamlitから検索プログラムを実行します) streamlit run src/app.py 7.5. Webブラウザからのアクセス Web ブラウザから http://localhost:8501 にアクセスします。 Web サーバーに正常に接続できたら、下記のような画面が表示されます。 ハイライト表示する:いいえ を選択し、検索したい内容に「柿之助 おばあさん」を入力して、【検索】ボタンを押してみます。すると、次のような画面が表示されます。 「柿之助」または「おばあさん」にマッチしたドキュメントが表示されますが、ハイライト表示はされていません。 次に、ハイライト表示する:はい を選択し、検索したい内容に「柿之助 おばあさん」を入力して、【検索】ボタンを押してみます。 「柿」、「助」、「おばあさん」にマッチした部分がハイライト表示されます。 一部は「柿」のみハイライト表示されている箇所があります。 これは、現時点の設定では、 「柿之助」 → 「柿」「助」 と分解されて、それぞれでマッチングが行われているためです。(「之」は助詞と判定され、マッチング対象になっていません。) これについては次回、「柿之助」をユーザー辞書登録することで改善されます。 8. まとめ Elasticsearch の Search API で highlight を指定することにより、検索結果をハイライト表示することができました。 次回も、ホワイトペーパー内に記載した技術的要素を取り上げて、紹介したいと思います。 (次回は、ユーザー辞書登録について紹介する予定です。) https://elastic.sios.jp/whitepaper/ からホワイトペーパーをダウンロードすることが可能です(E-mail アドレスなどの入力が必要です)。 ↩︎ 注意事項 下記のような markdown で表現された表が格納されている状態で、表の一部(あるいは表の直前や直後)がマッチしていた場合、highlight の項目には、表の一部のみが返却される場合があります。(表全体が返却されるとは限りません) → highlight で返却される内容は、表が崩れたものとなることがあります。 | xxx | xxx | xxx | |:–|:–|:–:| | xxx | xxx | xxx | ↩︎ The post Elasticsearch での検索結果のハイライト表示 first appeared on Elastic Portal .
アバター
1. 前書き 前回に引き続き、ホワイトペーパー「Elasticsearchを使った簡易RAGアプリケーションの作成」に記載した技術的要素を紹介いたします。 今回は、Bulk API を使って Python からドキュメントを一括登録する方法について紹介します。 なお、このブログ内に記載しているソースコードおよび Elasticsearch 用のリクエストは、github のリポジトリでも公開しています。(*脚注1 1 ) 1.1. 対象者 Elastic Cloud のアカウントを持っている人(トライアルライセンスを含む) Elasticsearch の初心者~中級者 1.2. できるようになること Python から Elasticsearch endpoint を利用して接続できるようになる。 Python から Elasticsearch の Bulk API を呼び出し、ドキュメントを一括登録できるようになる。 1.3. 前提条件 Elastic Cloud (version: 8.17.3) Python 3.13 Elastic Cloud 上で、日本語検索をするための準備が完了していること。 ※日本語検索するための準備(形態素解析の準備、ベクトル生成の準備)については、 ベクトル検索の準備 を参照してください。 (2025年03月07日時点の情報を元に記載しています。) 2. Bulk API とは? Bulk API は、Elasticsearch へドキュメントを登録する方法の一つです。 Elasticsearch では、データを登録する方法がいくつかあります。 ドキュメントを1つずつ、POST で登録する。 Bulk API を使って複数ドキュメントをまとめて登録する。 など。 今回は、後者の Bulk API を使って複数ドキュメントをまとめて登録する方法について紹介します。 Bulk API は、Elastic Cloud の Console から実行することも可能です。 Elasticsearch へのドキュメントの登録 の回では、Elastic Cloud の Console から Bulk API のリクエストを発行する方法について紹介しました。 今回は、Python から呼び出す方法について紹介します。 3. 同義語セットの作成 ドキュメントの登録先となるインデックスを作成する前に、同義語セット(の枠)を作成しておきます。 Synonym API (*脚注2 2 ) を使って、synonyms set を登録します。 下記のリクエストを Elastic Cloud の Console から発行します。 (Elastic Cloud の Console の表示方法は、 Elasticsearch へのインデックスの作成 の Dev Tools に関する説明を参照してください。) PUT _synonyms/kakinosuke_synonyms_set {   "synonyms_set": [] } (今回は実際の同義語は登録しません。後日登録する予定です。) 4. 書き込み先のインデックスの作成 今回は、書き込み対象のインデックス名を kakinosuke_202503 とします。 (「桃太郎」を改変した「柿之助」のお話を登録します。) ベクトル検索の準備 の回を参考にして、書き込み先となるインデックスを作成します。 下記のリクエストを Elastic Cloud の Console から実行します。 PUT /kakinosuke_202503 {   "settings": {     "index": {       "number_of_shards": 1,       "number_of_replicas": 1,       "refresh_interval": "3600s"     },     "analysis": {       "char_filter": {         "ja_normalizer": {           "type": "icu_normalizer",           "name": "nfkc_cf",           "mode": "compose"         }       },       "tokenizer": {         "ja_kuromoji_tokenizer": {           "mode": "search",           "type": "kuromoji_tokenizer",           "discard_compound_token": true,           "user_dictionary_rules": [           ]         }       },       "filter": {         "ja_search_synonym": {           "type": "synonym_graph",           "synonyms_set": "kakinosuke_synonyms_set",           "updateable": true         }       },       "analyzer": {         "ja_kuromoji_index_analyzer": {           "type": "custom",           "char_filter": [             "ja_normalizer",             "kuromoji_iteration_mark"           ],           "tokenizer": "ja_kuromoji_tokenizer",           "filter": [             "kuromoji_baseform",             "kuromoji_part_of_speech",             "cjk_width",             "ja_stop",             "kuromoji_number",             "kuromoji_stemmer"           ]         },         "ja_kuromoji_search_analyzer": {           "type": "custom",           "char_filter": [             "ja_normalizer",             "kuromoji_iteration_mark"           ],           "tokenizer": "ja_kuromoji_tokenizer",           "filter": [             "kuromoji_baseform",             "kuromoji_part_of_speech",             "cjk_width",             "ja_stop",             "kuromoji_number",             "kuromoji_stemmer",             "ja_search_synonym"           ]         }       }     }   } } リフレッシュは初回しか行わないので、”refresh_interval” : “3600s” としておきます。 “-1″ (リフレッシュしない)としてもいいのですが、リフレッシュをし忘れた場合の保険のために、”3600s” としています。 ホワイトペーパー内では “柿之助” という単語をユーザー辞書登録していましたが、まずは登録しないでおきます。(後日、登録します。) 検索用の同義語に関しては、先ほど作成した synonyms set を参照しておきます。(この時点では同義語の中身は空ですが、こちらも後日、登録します。) 5. 書き込み先のインデックスのフィールドの作成 続けて、フィールド作成用のリクエストも実行します。 PUT /kakinosuke_202503/_mappings {   "dynamic": false,   "_source": {     "excludes": [       "text_embedding.*"     ]   },   "properties": {     "chunk_no": {       "type": "integer"     },     "content": {       "type": "text",       "analyzer": "ja_kuromoji_index_analyzer",       "search_analyzer": "ja_kuromoji_search_analyzer"     },     "text_embedding": {       "properties": {         "model_id": {           "type": "keyword",           "ignore_above": 256         },         "predicted_value": {           "type": "dense_vector",           "dims": 384         }       }     }   } } text_embedding.* には密ベクトルの値が格納されますが、密ベクトル値は検索さえできればよいので、_source の対象外としています。 6. データ取り込み用のパイプラインの作成 ベクトル検索の準備 を参考に、パイプラインを作成します。 (すでに作成済の場合は、この処理は飛ばしてください。) PUT /_ingest/pipeline/japanese-text-embeddings {   "description" : "Text embedding pipeline",   "processors" : [     {       "inference": {         "model_id": ".multilingual-e5-small_linux-x86_64",         "target_field": "text_embedding",         "field_map": {           "content": "text_field"         }       }     }   ] } Bulk API 実行時にこのパイプラインを通すことで、  登録する文字列 → ベクトル生成 の処理が自動で行われるようになります(ベクトル化の処理の実装は必要ありません)。 7. エイリアスの作成 登録先のインデックス名の実体は、kakinosuke_202503 ですが、Python からは kakinosuke という名前でアクセスできるよう、エイリアスを作成しておきます。 すでに同名のエイリアス「kakinosuke」が存在する場合は削除するなり、今回作成するエイリアスを他の名前にするなどしてください。 同名のエイリアス「kakinosuke」が存在しない場合 POST _aliases {   "actions": [     {       "add": {         "index": "kakinosuke_202503",         "alias": "kakinosuke"       }     }   ] } 同名のエイリアス「kakinosuke」が既に存在する場合 POST _aliases {   "actions": [     {       "remove": {         "index": "変更前のインデックス名",         "alias": "kakinosuke"       }     },     {       "add": {         "index": "kakinosuke_202503",         "alias": "kakinosuke"       }     }   ] } ※既に kakinosuke エイリアスが既に存在しているかどうかは、下記で調べることが可能です。 GET _alias/kakinosuke 8. Elasticsearch endpoint 8.1. Elasticsearch への接続方法 Python から Bulk API を呼び出す前に、Python から Elastic Cloud へ接続する必要があります。 Python から Elastic Cloud へ接続する方法は、大きく分けて2種類あります。 Elasticsearch endpoint を使って接続する方法 Cloud ID を使って接続する方法 以前、 Python を使った Elasticsearch へのアクセス では、Cloud ID を使って接続する方法について説明しましたが、Elasticsearch v8.17.0 では、Elasticsearch endpoint を利用する方法が推奨されています。 取得した Elasticsearch endpoint を使って、Elasticsearch のクライアントを生成します。 8.2. Elasticsearch endpoint の取得方法 8.2.1. Elastic Cloud へログイン Elastic Cloud のログイン画面からユーザー名、パスワードを入力してデプロイメント一覧画面へ進みます。 8.2.2. Deployment 一覧画面 接続したい Deployment 名をクリックします。 クリックすると、Home 画面へ進みます。 8.2.3. Home 画面 Home 画面内の Elasticsearch をクリックします。 8.2.4. Elasticsearch 画面 Elasticsearch 画面内の中央上部に Elasticsearch endpoint の URL が表示されています。 コピーアイコンを押すと、クリップボードにコピーされます。 取得した Elasticsearch endpoint の URL を、後述の .env ファイルに転記します。 9. Access Key の作成 Python から Elastic Cloud にアクセスするには、Elasticsearch endpoint の他に、Access Key も必要です。 以前 Python を使った Elasticsearch へのアクセス では読み取り用の Access Key を作成しましたが、今回は Bulk API でデータを書き込む必要があるので、書き込み用の Access Key が必要となります。 書き込み用の Access Key を作成するには、下記のリクエストを Elastic Cloud の Console から実行します。 書き込み用 Access Key の作成リクエスト POST /_security/api_key {    "name": "kakinosuke_write_api_key",    "role_descriptors": {      "kakinosuke_write_role": {        "cluster": ["all"],        "indices": [          {            "names": ["kakinosuke*"],            "privileges": ["all"]          }        ]      }    } } このリクエストを実行すると、次のようなレスポンスが返ってきます。 {   "id": "......................",   "name": "kakinosuke_write_api_key",   "api_key": "......................",   "encoded": "***********************************************==" } ここで必要なのは encoded の値です。この値を後述の .env ファイルに転記します。 10. ソースの説明 これらを踏まえて、Bulk API を使ってドキュメントをインデックスへ登録します。 ソースの全体を見たい方は、下記の github リポジトリを参照してください。 blogs/2025-03-bulk at main · sios-elastic-tech/blogs A repository for blog sources. Contribute to sios-elastic-tech/blogs development by creating an account on GitHub. github.com 各ソースの抜粋を以下に記載します。 10.1. .env elasticsearch_endpoint='' write_api_key_encoded='' 接続に必要な endpoint と api_key を記載します。 elasticsearch_endpoint は、[8.2.4. Elasticsearch 画面] で取得した URL。 write_api_key_encoded は、[9. Access Key の作成] で取得した encoded の値。 10.2. docker-compose.yml services:   bulk_sample_202503:     build:       context: ./       dockerfile: Dockerfile     container_name: 'bulk_sample_202503'     volumes:       - ./:/app Docker 用の compose ファイルです。 10.3. Dockerfile FROM python:3.13 ...以下略 いわゆる Dockerfile です。 10.4. requirements.txt elasticsearch==8.17.1 python-dotenv==1.0.1 今回のプログラムで必要となるライブラリです。 10.5. data/kakinosuke.txt_chunked.txt むかし、むかし、あるところに、おじいさんとおばあさんがありました。まいにち、おじいさんは山へしば刈りに、おばあさんは川へ洗濯に行きました。ある日、おばあさんが、川のそばで、せっせと洗濯をし ... ゴリラはキャッ、キャッと笑いながら、白い歯をむき出しました。鷹はケン、ケンと鳴きながら、くるくると宙返りをしました。空は青々と晴れ上がって、お庭には桜の花が咲き乱れていました。 「桃太郎」を改変した「柿之助」のお話です。 ファイルの内容をすべて知りたい場合は、github 内のリポジトリを参照してください。 事前に LangChain を使ってチャンキング処理を行っています。 (チャンキング処理の詳細はここでは割愛します。詳しく知りたい方は、ホワイトペーパーをご参照ください。) 10.6. src/bulk_from_txt.py """ ... Usage: python bulk_from_txt.py chunked_textfilepath ... """ ... def bulk_from_file(es_client: Elasticsearch, chunked_textfilepath: str, index_name: str = SEARCH_INDEX, refresh: bool = False): ... if __name__ == "__main__": ... bulk_from_file(es_client=es_client, chunked_textfilepath=chunked_textfilepath, index_name=SEARCH_INDEX, refresh=True) Bulk API を呼び出す本体です。 ファイルから1行ずつ読み取って、Bulk API にドキュメントを1つ渡し、最後に refresh を呼び出しています。 ソース全体は、github 内のソースを参照してください。 10.7. src/common/setup_logger.py ... def setup_logger(logger_name: str, log_level: str = DEFAULT_LOG_LEVEL) -> logging.Logger: ... ロガー用の共通関数です。 10.8. src/elastic/es_consts.py # 検索対象のインデックス(のエイリアス) SEARCH_INDEX = 'kakinosuke' # ドキュメント登録時のパイプライン名 INGEST_PIPELINE = 'japanese-text-embeddings' Elasticsearch関連の定数を定義しています。 10.9. src/elastic/es_func.py def create_es_client(elasticsearch_endpoint: str, api_key_encoded: str) -> Elasticsearch: ... def streaming_bulk_wrapper(es_client: Elasticsearch, actions: Iterable) -> int: ... def refresh_index(es_client: Elasticsearch, index_name: str = SEARCH_INDEX): ... Elasticsearch 関連の関数を集めたファイルです。 create_es_client()関数から Python 用の Elasticsearch のクライアントを生成しています。(*脚注3 3 ) streaming_bulk_wrapper()関数から Python 用の Bulk API を呼び出しています。(*脚注4 4 ) 最後に refresh() を呼ぶことで、ドキュメント登録を確定させています。 11. 実行 Docker 上のコンテナにデプロイします。 (ここでは Docker を利用していますが、Docker は必須ではありません。) 11.1. ビルド docker compose build docker-compose.yml ファイルがあるディレクトリで、上記を実行します。 11.2. コンテナの起動 docker compose up -d エラーがなければ、コンテナ(コンテナ名 = “bulk_sample_202503”)が起動します。 11.3. コンテナ内で bash を実行 docker exec -it bulk_sample_202503 /bin/bash “bulk_sample_202503” はコンテナ名です。 直接、src/bulk_from_txt.py を実行してもよいのですが、エラーが起きた場合などのハンドリングが小難しくなってしまうので、いったん、bash を経由してから実行します。 11.4. Python スクリプトの実行 python src/bulk_from_txt.py data/kakinosuke.txt_chunked.txt コンテナ内部で実行中の bash から、上記のコマンドを実行します。 (data/kakinosuke.txt_chunked.txt が登録したいデータを1行ずつ格納しているテキストファイルの相対パスです。) テキストファイルから1行ずつ文字列を読み取り、インデックスへ登録します。 正常に終了したら、ロガーを通じて標準出力に次のようなログが出力されます。 [07:42:59] [INFO] main :88 bulk_from_file success_count=38 12. 確認 Elastic Cloud にログインし、Console から下記のリクエストを実行してみます。 GET /kakinosuke/_search {   "query": {     "match_all": {}   } } レスポンス { ... "hits": { "total": { "value": 38, "relation": "eq" }, ... } のように 38 個のドキュメントが見つかります。 13. まとめ Bulk API を利用することにより、Python からドキュメントをインデックスに一括登録できることを紹介しました。 次回も、ホワイトペーパー内に記載した技術的要素を取り上げて、紹介したいと思います。 (*脚注1) このブログ内に記載しているソースコード、および Elasticsearch 用のリクエストは、下記にて公開しています。 https://github.com/sios-elastic-tech/blogs/blob/main/2025-03-bulk ↩︎ (*脚注2) Synonyms API: https://www.elastic.co/guide/en/elasticsearch/reference/current/put-synonyms-set.html Elasticsearch 8.10.0 より使用できるようになった、同義語関連の API です。 ↩︎ (*脚注3) Elasticsearch のクライアント生成 API: https://elasticsearch-py.readthedocs.io/en/v8.17.1/api/elasticsearch.html#elasticsearch.client.Elasticsearch ↩︎ (*脚注4) Python から呼び出す Elasticsearch の bulk API: https://elasticsearch-py.readthedocs.io/en/v8.17.1/helpers.html#elasticsearch.helpers.streaming_bulk 今回は、helpers.streaming_bulk()を利用します。 ↩︎ The post Bulk API (Pythonからのドキュメントの一括登録) first appeared on Elastic Portal .
アバター
1. 前書き 先日、ホワイトペーパー「Elasticsearchを使った簡易RAGアプリケーションの作成」(*脚注1 1 ) を公開いたしました。 今回から、ホワイトペーパーの中で紹介した技術的要素のうち、まだこのブログで紹介していなかったものを紹介していきたいと思います。 今回は、エイリアスです。 対象者 – Elastic Cloud のアカウントを持っている人(トライアルライセンスを含む) – Elasticsearch の初心者~中級者 できるようになること – Elasticsearch のインデックスに対するエイリアスを作成できる。 – Elasticsearch でエイリアスを使ったアクセスができる。 前提条件 – Elastic Cloud (version: 8.17.2) (2025年02月27日時点の情報を元に記載しています。) 2. エイリアスとは? エイリアスの意味は、直訳すれば「別名」です。 他のサービスでもエイリアスを利用できたりしますが、Elasticsearch でも、インデックスに対する「別名」を利用できます。 3. エイリアスの用途 エイリアスの用途はいくつかあります。(*脚注2 2 ) – 1. インデックスの実体が変更になっても、プログラムの修正なしに動作し続けられるようにする。 – 2. 検索対象のインデックスが複数にまたがっており、それらを統合して指定できるようにする。 – 3. データストリーム(*脚注3 3 )において、読み込みは複数のインデックス、書き込みは1つのインデックスとする。 まずは、1番目の設定例を取り上げていきます。 4. エイリアスの設定 今回作成するエイリアスを、次のように設定します。 実体のインデックス名 エイリアス名 momotaro_v3 momotaro ベクトル検索の準備 の回で momotaro_v3 という名前のインデックスを作成しました。 そのインデックスに対するエイリアスを momotaro とします。 ※すでに momotaro という名前のインデックスがあった場合は、今回作成するエイリアスを “momotaro_alias” など他の名前にしてください。 5. エイリアスの作成 Elastic Cloud にログインし、Console から次のリクエストを発行します。 POST _aliases { "actions": [ { "add": { "index": "momotaro_v3", "alias": "momotaro" } } ] } 将来、momotaro_v4 インデックスを作成して、momotaro エイリアスの参照先を momotaro_v3 から momotaro_v4 に変更したい場合は、次のリクエストを発行します。 (まず、古いエイリアスを削除してから、新しいエイリアスを追加します。) POST _aliases { "actions": [ { "remove": { "index": "momotaro_v3", "alias": "momotaro" } }, { "add": { "index": "momotaro_v4", "alias": "momotaro" } } ] } 6. エイリアスを使った検索 エイリアスを定義した後は、エイリアスを使って検索などを行うことができます。 GET /momotaro/_search { "query": { "match_all": {} } } momotaroエイリアスに対して検索をリクエストすると、実際には、実体である momotaro_v3 インデックスに対して検索が行われます。 Python などからインデックスへアクセスする場合も、エイリアスを経由することでインデックスの実体が変更になってもプログラムを変更することなく動作し続けることが可能となります。 7. エイリアスの作成(その2) 複数インデックスを検索対象とし、書き込みインデックスを明示する場合のエイリアスの作成です。 こちらは、 3.エイリアスの用途 の3番目に相当します。 複数のインデックスを検索対象にし、かつ、書き込みインデックスを明示したい場合は、次のように記述します。 “logs-“で始まる(複数の)インデックスを検索対象とし、”logs-202501” インデックスを書き込み対象とする場合の例 (エイリアス = “logs”) POST _aliases { "actions": [ { "add": { "index": "logs-*", "alias": "logs", "is_write_index": false } }, { "add": { "index": "logs-202501", "alias": "logs", "is_write_index": true } } ] } このように logs エイリアスを作成することで、 GET /logs/_search { ... } のような検索リクエストでは logs- で始まる複数のインデックスに対し検索を行い、 POST /logs/_doc { ... } のようなドキュメント登録リクエストでは logs-202501 インデックスに対し、ドキュメントの登録を行います。 詳細は、Elasticsearch 公式ドキュメント内の Alias の説明(*脚注4 4 )を参照してください。 8. 作成済のエイリアスの確認 クラスター内の全エイリアスを確認するには、次のリクエストを発行します。 GET _alias 特定のインデックスに対するエイリアスを確認するには、次のリクエストを発行します。 GET 調べたいインデックスの名前/_alias 詳細は、Elasticsearch 公式ドキュメント内の Alias の説明(*脚注4)を参照してください。 9. 注意点 エイリアスを作成した場合でも、下記のような API Key を作成時には、インデックス名を指定する必要があります。 例: POST /_security/api_key { "name": "my-api-key", "role_descriptors": { "role-a": { "cluster": ["all"], "indices": [ { "names": ["index-a*"], "privileges": ["all"] } ] } } } 上記内の “index-a*” の箇所には、エイリアスではなく、インデックス名を記載する必要があります。 API Key の作成方法については、Elasticsearch の公式ドキュメント内の API Key についての説明(*脚注5 5 )を参照してください。 10. その他 条件付きのエイリアスなども作成可能です。 詳細は、Elasticsearch 公式ドキュメント内の Alias の説明(*脚注4)を参照してください。 11. まとめ エイリアスを利用することにより、インデックスの実体が変更になってもプログラムの修正を行うことなく、動作し続けられることを紹介しました。 次回も、ホワイトペーパー内に記載した技術的要素を取り上げて、紹介したいと思います。 ホワイトペーパー「Elasticsearchを使った簡易RAGアプリケーションの作成」は、2024年9月から2024年12月にかけて、当ブログ内で紹介した内容を再編したものです。  概要:Elasticsearchのインデックス作成 ~ RAGアプリケーションの作成 ホワイトペーパーは、以下のURLよりダウンロードできます。 (ダウンロードするには、email アドレスなどの入力が必要です。) https://elastic.sios.jp/whitepaper/ ↩︎ インデックスに対するエイリアス以外にも、フィールドに対するエイリアスもあります。これについては、今後、紹介したいと思います。 https://www.elastic.co/guide/en/elasticsearch/reference/current/field-alias.html ↩︎ データストリームについてはこれまで説明していませんが、ログなどのように日々増え続けていくデータを取り込むためのインデックスを指します。 (ドキュメントがどんどん追加されていきますが、参照が多く、更新はほとんどないようなケース) 詳細は、下記を参照願います。 https://www.elastic.co/guide/en/elasticsearch/reference/current/data-streams.html ↩︎ Elasticsearch 公式ドキュメント内の Alias の説明: https://www.elastic.co/guide/en/elasticsearch/reference/current/aliases.html ↩︎ Elasticsearch の公式ドキュメント内の API Key についての説明: https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-create-api-key.html ↩︎ The post Elasticsearchでのエイリアスの利用 first appeared on Elastic Portal .
アバター
1. 前書き こんにちは。サイオステクノロジーの田川です。 前回 は、Python と Streamlit を使って Elasticsearch へアクセスし、ハイブリッド検索(RRF)を行ってみました。 今回は、昨今、流行りの RAG アプリケーションを作成して、 質問した内容を Elasticsearch で検索し、 検索結果から LLM に回答を作ってもらうようにしてみます。検索する内容は前回までに登録した「桃太郎」を使い、LLM には今回は Cohere の Command R を使います。 (*脚注1) 1 対象者 Elastic Cloud のアカウントを持っている人(トライアルライセンスを含む) Elasticsearch の初心者~中級者 できるようになること Elasticsearch, Python, Streamlit, Cohere を使って、簡単な RAG のアプリケーションを動かせるようになる。 前提条件 Elastic Cloud 8.15.0 Docker 27.2.1 Python 3.12 pip 24.3.1 elasticsearch (Python用のClient, version: 8.15.0) streamlit 1.39.0 Cohere Command R 桃太郎の内容をテキスト型、および、密ベクトル型でインデックス済であること。 検索テンプレートを定義済であること。 (2024年12月2日時点の情報を元に記載しています。) 今回のブログでは、回答作成用の LLM として Cohere の Command R を使用するため、Cohere API の Key(Trial Key 可) が必要です。 (*脚注2) 2 2. RAG とは ? いろんなWebサイトで RAG の解説記事が掲載されているので、あらためて、ここに書くまでもないかと 思いますが、超簡単に書くと以下の図のような構造になっています。 LLM が知らない情報を事前に検索し、それを LLM に与えることで、LLMが知らない情報(企業内の独自の情報など)に関連する質問に答えることができるようになります。 今回は、 ユーザーとの対話部分に Streamlit を 検索部分に Elasticsearch を 回答作成用の LLM に Cohere Command R を それぞれ使います。 検索方法は、RRFによるハイブリッド検索です。 前回のRRFによるハイブリッド検索プログラムを元に、Cohere Command R に回答を依頼する処理を追加し、RAGのプログラムを作成します。 ※あくまでもサンプルですので、エラーチェックや、サニタイジング処理などは、ほとんど実装していません。また、LLM との対話履歴のクリアも必要な場合がありますが、実装していません。 3. 準備 3.1. ファイル構成 今回作成する RAG アプリケーションのファイル構成です(あくまでも例です)。 ルートフォルダ/   |- .env   |- docker-compose.yml   |- Dockerfile   |- requirement.txt   |- src/        |- app_rag.py        |- elastic/        |        |- es_consts.py        |        |- es_func.py        |-llm/            |- cohere_func.py            |- llm_consts.py 前回のファイル構成と比較して、LLM関連のフォルダ、ファイルを追加しています。 なお、このブログ内では、Docker を利用していますが、Docker の利用は必須ではありません。 (Dockerの設定については、ここでは割愛します。 また、Streamlit を使って画面制御を行っていますが、詳細については割愛します。) 3.2. .env cloud_id_encoded='........==' read_api_key_encoded='.......==' # cohere api key llm_api_key='..........' 前回登録した cloud_id_encoded, read_api_key_encoded に加えて、Cohere API の trial key を設定します。 3.3. docker-compose.yml services: sios_rag_sample_1: build: context: ./ dockerfile: Dockerfile container_name: 'sios_rag_sample_1' volumes: - ./:/app ports: - 8501:8501 コンテナ名を sios_rag_sample_1 としています。 3.4. Dockerfile FROM python:3.12 COPY ./requirements.txt ./ RUN pip install --upgrade pip RUN pip install -r ./requirements.txt WORKDIR /app COPY ./ /app ENV PYTHONUNBUFFERED 1 ENTRYPOINT ["/bin/sh", "-c", "while :; do sleep 30; done"] 前回と同じです。 3.5. requirements.txt elasticsearch==8.15.0 python-dotenv==1.0.1 streamlit==1.39.0 langchain-cohere==0.3.1 ※前回の requirements.txt に、langchain-cohere を追加します。 3.6. src/llm/llm_consts.py # 問い合わせ先のモデル LLM_MODEL_ID = 'command-r' LLM 用の固定文字列を定義します。 今回は、Cohere の Command R を使用するので、それを表す’command-r’を定義します。 3.7. src/llm/cohere_func.py from langchain_cohere import ChatCohere, CohereRagRetriever from langchain_core.documents import Document # Define the Cohere LLM def create_llm(cohere_api_key, model_id): return ChatCohere(cohere_api_key=cohere_api_key, model=model_id) # LLM に問い合わせる。 def request_to_llm(llm, question, search_results): # 検索結果を Cohere に渡すための詰め替え。 documents = [] for search_result in search_results: documents.append(Document(page_content=search_result)) rag = CohereRagRetriever(llm=llm, connectors=[]) response = rag.invoke(input=question, documents=documents) return response # Cohere の場合、response の最後の要素の page_content に回答が格納されている。 def retrieve_answer(response): return response[-1].page_content Cohere 用の関数を定義します。 初期化 LLM への問い合わせ レスポンスからの回答部分の抽出 を定義しています。 CohereRagRetriever を利用すると、 response = rag.invoke(input=question, documents=documents) という1文で、与えられた情報から回答を求める、といった動作をできるようになります。非常に簡単です。 (多少の準備作業は必要ですが、それほど大変ではありません。) 一般的に「プロンプトエンジニアリング」と呼ばれるようなテクニックも特に必要ありません。 3.8. src/elastic/es_consts.py # 検索対象のインデックス SEARCH_INDEX = 'momotaro_v3' # 検索テンプレートのId SEARCH_TEMPLATE_ID = 'rrf_search_template'  前回と同じです。 3.9. src/elastic/es_func.py from elasticsearch import Elasticsearch # Elasticsearch へアクセスするための client を生成する。 def create_es_client(cloud_id_encoded, api_key_encoded): es_client: Elasticsearch = None if cloud_id_encoded != '' and api_key_encoded != '': es_client = Elasticsearch(cloud_id=cloud_id_encoded, api_key=api_key_encoded) return es_client # 検索テンプレートに埋め込むパラメタを生成する。 def create_search_params(query): search_params = { 'query_string': query, 'query_for_vector': query } return search_params # 検索テンプレートを使って、検索を行う。 def es_search_template(es_client, search_index, search_template_id, search_params, field_name='content', max_count=5): results = [] search_results = es_client.search_template(index=search_index, id=search_template_id, params=search_params) for doc in search_results['hits']['hits'][:max_count]: # 配列になって返却されるので、0番目の要素を取り出す。 results.append(doc['fields'][field_name][0]) return results Elasticsearch への問い合わせ処理を実装しています。前回と同じです。 検索テンプレートを使って RRF ハイブリッド検索を行っていますが、Python側の実装は、非常に簡単になります。 (検索ロジックそのものは、Elasticsearch に登録した検索テンプレート(rrf_search_template)内に実装します。) 検索テンプレート(rrf_search_template)は、 ハイブリッド検索 の回で登録しています。 3.10. src/app_rag.py import os import streamlit as st from dotenv import load_dotenv from elastic.es_consts import SEARCH_INDEX, SEARCH_TEMPLATE_ID from elastic.es_func import create_es_client, create_search_params, es_search_template from llm.cohere_func import create_llm, request_to_llm, retrieve_answer from llm.llm_consts import LLM_MODEL_ID # 参考 # https://github.com/streamlit/example-app-langchain-rag/blob/main/streamlit_app.py # Elasticsearch用の client の初期化を行う。 def initialize_es(): cloud_id_encoded: str = '' read_api_key_encoded: str = '' if load_dotenv(verbose=True): print('load_dotenv success') if 'cloud_id_encoded' in os.environ: cloud_id_encoded = os.environ['cloud_id_encoded'] if 'read_api_key_encoded' in os.environ: read_api_key_encoded = os.environ['read_api_key_encoded'] if read_api_key_encoded == '': print('please set cloud_id_encoded and read_api_key_encoded in .env') return '' else: es_client = create_es_client(cloud_id_encoded, read_api_key_encoded) # debug print(f'{es_client.info()=}') return es_client # LLM の初期化を行う。 def initialize_llm(): llm_api_key: str = '' if load_dotenv(verbose=True): print('load_dotenv success') if 'llm_api_key' in os.environ: llm_api_key = os.environ['llm_api_key'] llm = create_llm(llm_api_key, LLM_MODEL_ID) return llm # 質問が入力された後に呼ばれる検索処理。 # (Elasticsearch へ検索を依頼する) def search(query): es_client = st.session_state['es_client'] # 検索用パラメタを生成する。 search_params = create_search_params(query) # 検索テンプレートを使って検索する。 search_results = es_search_template(es_client, SEARCH_INDEX, SEARCH_TEMPLATE_ID, search_params) # debug print (検索結果) print('----- search_results -----') i: int = 0 for result in search_results: print(f'{i}, {result}') i += 1 return search_results # 検索結果を情報源としてLLMへ問い合わせる。 def question_to_llm(query, search_results): # LLM に問い合わせる。 llm = st.session_state['llm'] response = request_to_llm(llm, query, search_results) # for debug (LLMの結果) print('----- llm response -----') print(response) answer = retrieve_answer(response) return answer # 画面の表示 def show_ui(prompt_to_user='質問を入力してください。'): if 'messages' not in st.session_state.keys(): st.session_state.messages = [{'role': 'assistant', 'content': prompt_to_user}] # Display chat messages for message in st.session_state.messages: with st.chat_message(message['role']): st.write(message['content']) # 1. ユーザーからの質問を受け付ける。 if query := st.chat_input(): with st.chat_message('user'): st.write(query) st.session_state.messages.append({'role': 'user', 'content': query}) # Generate a new response if last message is not from assistant if st.session_state.messages[-1]['role'] != 'assistant': with st.chat_message('assistant'): with st.spinner('Thinking...'): # 2. Elasticsearch へ問い合わせ / 3.検索結果の受け取り search_results = search(query) # 4. LLM へ問い合わせ / 5. 回答結果の受け取り answer = question_to_llm(query, search_results) # 6. 回答結果の表示 st.markdown(answer) message = {'role': 'assistant', 'content': answer} st.session_state.messages.append(message) # ----- main ----- if 'es_client' not in st.session_state: es_client = initialize_es() st.session_state['es_client'] = es_client if 'llm' not in st.session_state: llm = initialize_llm() st.session_state['llm'] = llm st.set_page_config(page_title='桃太郎 RAG') st.title('桃太郎 RAG') show_ui() RAG の本体部分の処理です。前述の概略図の「アプリケーション」の部分に該当します。 Streamlit 上で、質問の受付 ~ 回答の表示 までを行っています。 質問の受け付け Elasticsearch への検索依頼 検索結果の受け取り LLM への問い合わせ 回答結果の受け取り 回答結果の表示 4. Docker 上での Streamlit の実行 ビルド~アプリケーションの実行は、前回とほぼ同じです。 (コンテナ名 と 最後に動作させるファイル名 が異なっています。) docker-compose.yml があるホスト上のディレクトリで、 docker compose build を行います。 さらに docker compose up -d を行います。 これでコンテナが起動するはずなので、コンテナが起動したら、 docker exec -it sios_rag_sample_1 /bin/bash とします。 (sios_rag_sample_1 は、コンテナ名) bash を経由せずに、いきなり Streamlit を動かしてもいいのですが、 エラーが起きた場合にハンドリングしにくいので、 いったん、bash を経由して Streamlit を動かすようにしています。 bash の入力プロンプトから streamlit run src/app_rag.py を実行します。Streamlit の HTTP サーバーが開始されます。 Web ブラウザから http://localhost:8501 へアクセスします。 次のような画面が表示されます。 5. RAGの実行 それでは、実際に質問してみましょう。 5.1. 質問1 「おじいさんが山へ行った理由は?」という質問を入力してみます。 「しば」が「柴」に勝手に変換されていますが、これは、Cohere 側の問題(仕様)だと思われます(親切で変換してくれているのでしょう)。意味は合ってるので良しとしましょう。 5.2. その他の質問 他にもいくつか質問してみます。 おばあさんが川へ行った理由は? 船のへさきに立って見張りをしたのは誰? 桃太郎が鬼が島へ行くために乗った乗り物は? 桃太郎が鬼が島へ行った目的は? それぞれ、次のような回答が返ってきます。 おばあさんは川へ洗濯に行きました。 船のへさきに立って見張りをしたのはきじです。 桃太郎が鬼が島へ乗った乗り物は船です。 桃太郎が鬼が島へ行った目的は鬼せいばつをする事です。 6. まとめ 今回のプログラムを使って、簡単な RAG アプリケーションを動かすことができました。 RAG の基本的な動作自体は、あまり難しくないことがわかってもらえるとうれしく思います。 ただ、本格的な RAG システムを稼働させようとすると、様々なチューニングやセキュリティ対策を行う必要が出てきます。 これらについては、今後、紹介していければ、と思います。 (*脚注3) 3 (*脚注4) 4 最後に: これまで断片的に内容を記載していたため、ややわかりにくい点もあったかと思いますので、今後、インデックスの作成~RAGの動作 の内容を1つの PDF にまとめて掲載する予定です。 ※桃太郎の話は、LLMが既に学習済の可能性があります。 LLMが学習済であれば、わざわざ RAG を作る必要はありませんが、 あくまでも RAG の演習用として、桃太郎を RAG の題材にしています。 RAG でないと答えられないように手を加えるのであれば、わざと、  桃太郎 -> 柿之助  桃 -> 柿  猿 -> ゴリラ  犬 -> 猫  きじ -> 鷹  きびだんご -> おむすび などのように変換してからテキスト内容を登録する対策も考えられますが、今回はあくまでも RAG の演習用なので、そこまではやっていません。 ↩︎ Cohere API の Trial Key を発行するには、 https://cohere.com にアクセスして、右上の TRY NOW から Cohere のアカウントを作成後、 API Keys の Trial Key を発行してください。 発行した Trial key は、.env ファイルに記載します。 ※今回、Command R を利用するのは、 – 無料での試用が可能なこと – 日本語に対応していること – RAGとしての利用が簡易であること を踏まえてのものです。 日本語での RAG の動作を理解するためのお試しとしては、Command R は使いやすいと思います。ただし、本番運用での Command R の利用を推奨しているわけではありません。 また、Command R よりも Command R+ の方が優秀と思われますが、今回はあくまでも RAG の学習用なので、Command R を利用しています。 ↩︎ ※あくまでも RAG の基礎を理解するために構築したアプリケーションですので、答えられない質問もあります。 業務用の RAG システムであれば、 – 適切なチャンキングを行う。 – 適切なユーザー辞書登録を行う。 – 適切な同義語登録を行う。 などのチューニングを行う必要があります。 その他、 – 質問の内容にあわせて、検索結果のリランクを行う。 といった改善策もあります。 ↩︎ CohereRagRetrieverの場合、どの情報源を参照して回答を作成したのか?も返してくれます。 「桃太郎の家来が桃太郎からもらったものは?」の例だと、 question_to_llm 関数内の response で次のような内容が返却されます。 … ‘citations’: [ChatCitation(start=15, end=20, text=’きびだんご’, document_ids=[‘doc-0’, ‘doc-2’]), ChatCitation(start=24, end=26, text=’犬も’, document_ids=[‘doc-0’]), ChatCitation(start=26, end=29, text=’きじも’, document_ids=[‘doc-2’])], … page_content=’桃太郎の 家来がもらったものは、きびだんごでした。犬もきじも、きびだんごを一つもらって桃太郎についていきました。’ これは、 回答の根拠になった情報が – きびだんご (doc-0, doc-2 より) – 犬も (doc-0 より) – きじも (doc-2 より) であり、最終的な回答が 「桃太郎の 家来がもらったものは、きびだんごでした。犬もきじも、きびだんごを一つもらって桃太郎についていきました。」 であることを示しています。 doc-0, doc-2 は、Elasticsearch で RRF によるハイブリッド検索を行った際の 0番目(先頭)と2番目の検索結果を指します。(search関数内での search_results の 0番目 と 2番目) (なお、猿の情報は、登録した内容のチャンクサイズが適切でないために、検索で見つかっていません。) ↩︎ The post Elasticsearchを使ったRAGアプリケーションの作成 first appeared on Elastic Portal .
アバター