TECH PLAY

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

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

57

この記事では、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 .
アバター
1. 前書き こんにちは。 サイオステクノロジーの田川です。 これまで Elastic Cloud 上の GUI から Elasticsearch での検索などを行ってきましたが、実際の開発・運用時にはプログラム言語を使って Elasticsearch へアクセスすることも珍しくありません。 Elasticsearch へ、様々なプログラム言語からアクセスすることが可能ですが、今回はその中でも比較的メジャーな Python を取り上げます。 今回は、Python を使って Elasticsearch へ検索を要求し、受け取った結果を画面に表示できるようにしてみたいと思います。 (画面の制御には、 streamlit を使います。) 対象者 Elastic Cloud のアカウントを持っている人(トライアルライセンスを含む) Elasticsearch の初心者~中級者 できるようになること Python を使って Elastic Cloud 上の Elasticsearch にアクセスし、検索テンプレートを使った検索をできるようになる。 Streamlit を使って 検索用クエリを受け取り、検索結果を画面に表示できるようになる。 前提条件 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 桃太郎の内容をテキスト、および、密ベクトルでインデックス済であること。 検索テンプレートを定義済であること。 (2024年11月18日時点の情報を元に記載しています。) 2. 準備 2.1. ファイル構成 次のような構成で動かしました(あくまでも例です)。 ルートフォルダ/ |- .env |- docker-compose.yml |- Dockerfile |- requirement.txt |- src/ | |- app.py | |- elastic/ |- es_consts.py |- es_func.py なお、このブログ内では、Docker を利用していますが、Docker の利用は必須ではありません。 (Dockerの設定については、ここでは割愛します。 また、streamlit を使って画面制御を行っていますが、詳細については割愛します。) (*脚注1) 1 2.2. .env (Elasticsearch への接続情報) の内容 cloud_id_encoded='' read_api_key_encoded='' (後述の処理で取得した値をここに転記します。) 2.3. docker-compose.yml の記載例 services: sios_blog_sample_1: build: context: ./ dockerfile: Dockerfile container_name: 'sios_blog_sample_1' volumes: - ./:/app ports: - 8501:8501 ※8501は、streamlit を動かす際のディフォルトのポート番号です。 2.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"] 2.5. requirements.txt の例 elasticsearch==8.15.0 python-dotenv==1.0.1 streamlit==1.39.0 2.6. src/app.py (アプリ本体) の内容 from elastic.es_func import create_es_client, create_search_params, es_search_template from elastic.es_consts import SEARCH_INDEX, SEARCH_TEMPLATE_ID import streamlit as st from dotenv import load_dotenv import os # 初期化を行う。 def initialize(): 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 # Elasticsearchで検索を行う。 def search(es_client, query): # 検索用パラメタを生成する。 search_params = create_search_params(query) # 検索テンプレートを使って検索する。 search_result = es_search_template(es_client, SEARCH_INDEX, SEARCH_TEMPLATE_ID, search_params) return search_result # 検索ボタンが押されたときに呼ばれる処理。 def search_onclick(): es_client = st.session_state['es_client'] query = st.session_state['query'] # 検索を行う。 search_results = search(es_client, query) # 結果を表示する。 st.write(f'クエリ: {query}') st.write('検索結果:') for result in search_results: st.write(result) # 画面の作成 def createScreen(): st.text_input(label= 'クエリ', key= 'query') st.button(label= '検索', on_click= search_onclick) # ----- main ----- if 'es_client' not in st.session_state: es_client = initialize() st.session_state['es_client'] = es_client createScreen() 2.7. src/elastic/es_consts.py (定数定義部分) の内容 # 検索対象のインデックス SEARCH_INDEX = 'momotaro_v3' # 検索テンプレートのId SEARCH_TEMPLATE_ID = 'rrf_search_template' 2.8. src/elastic/es_func.py (Elasticsearch関連の関数部分) の内容 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 create_es_client 関数 create_es_client 関数で、Elasticsearch へアクセスするための client を生成しています。 client を生成する方法はいくつかありますが、ここでは、cloud_id と api_key を渡す方法を採用しています。 詳細は、下記を参照してください。 https://elasticsearch-py.readthedocs.io/en/v8.15.1/quickstart.html#connecting https://elasticsearch-py.readthedocs.io/en/v8.15.1/api/elasticsearch.html#elasticsearch.client.Elasticsearch es_search_template 関数 es_search_template 関数から search_template API を使って Elasticsearch へ検索を行っていますが、 渡している情報は、 インデックス名 検索テンプレートID 検索パラメタ のみです。検索クエリの詳細な内容は、Python側には、一切、書いていません。 (Elasticsearchの検索テンプレート内に記載しています) search_tempate API の詳細は、下記を参照してください。 https://elasticsearch-py.readthedocs.io/en/v8.15.1/api/elasticsearch.html#elasticsearch.client.Elasticsearch.search_template (*脚注2) 2 3. 接続用情報の取得 3.1. Elasticsearch に接続するために必要な情報 Python から Elasticsearch に接続するために必要な情報は、下記の2つです。 Cloud ID API Key (*脚注3) 3 3.2. Cloud ID の取得 Elastic Cloud にログインし、Home 画面を表示します。 Home 画面の デプロイメント(ここでは “SIOS-BLOG-SAMPLE-1”)の Manage をクリックします。 選択されたデプロイメントの画面が表示されますが、この右側に Cloud ID が表示されています。 これをコピーして、ソースの .env ファイルの cloud_id_encoded=’…’ に貼り付けてください。 .env cloud_id_encoded='SIOS-BLOG-SAMPLE-1:.............=' 3.3. API Key の取得 Python のプログラムから Elastic Cloud に接続する方法は、大きく分けて2つあります。 Username/Password を利用する方法 Access Key を利用する方法 ここでは、後者の Access Key を利用します。 Access Key を使うメリットとしては、 権限の設定を行うことが可能。 失効の操作が簡単。 などが挙げられます。 Elastic Cloud の Console から Access Key を作成します。 左側のリクエスト欄に下記を入力し、 ▷ をクリックします。 (今回は読み取り専用で十分なので、privileges: [“read”] としておきます。) POST /_security/api_key { "name": "momotaro_read_api_key", "role_descriptors": { "momotaro_api_key": { "cluster": ["all"], "indices": [ { "names": ["momotaro*"], "privileges": ["read"] } ] } } } API Key の作成に成功すると、右側のレスポンス欄に、生成された API Key が表示されます。 { "id": "***********************", "name": "momotaro_read_api_key", "api_key": "***********************", "encoded": "***********************************************==" } ここで重要な値は、encoded に記載されている値です。 この値をコピーして、.env ファイルの read_api_key_encoded=’…’ に貼り付けます。 .env read_api_key_encoded='.............==' ※注 API Key の api_key および encoded の値は、API Key を生成したときしか表示されません。 もし忘れてしまってわからなくなった場合は、再作成してください。 ※API Key についての詳細は、下記を参照してください。 https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-create-api-key.html 4. Docker 上での streamlit の実行 docker-compose.yml があるホスト上のディレクトリで、 docker compose build を行います。 さらに docker compose up -d を行います。 これでコンテナが起動するはずなので、コンテナが起動したら、 docker exec -it sios_blog_sample_1 /bin/bash とします。 (sios_blog_sample_1 は、コンテナ名) bash を経由せずに、いきなり streamlit を動かしてもいいのですが、 エラーが起きた場合にハンドリングしにくいので、 いったん、bash を経由して streamlit を動かすようにしています。 接続先の bash の入力プロンプトから streamlit run src/app.py を実行します。 Streamlit が開始されるので、Web ブラウザから http://localhost:8501 へアクセスします。 次のような画面が表示されます。 5. 検索の実行 それでは、検索してみましょう。 5.1. 検索1 クエリの入力欄に「おじいさんが山へ行ったのはなぜ」と入力し、[検索]ボタンを押します。 すると、次のような画面になります。 検索結果の上位5件が表示されます。 5.2. 検索2 次に、クエリの入力欄に「桃太郎の家来になった動物」と入力し、[検索]ボタンを押します。 すると、次のような画面になります。 6. まとめ Python と streamlit を使って、Elasticsearch に検索を行い、結果を画面に表示することができました。 検索テンプレートを使って Python から検索を行うことで、役割の分担ができます。 Python 側の実装者は、Elasticsearch でのDSLの文法や、細かい検索の仕様について熟知する必要はありません。 逆に、Elasticsearch側で検索テンプレートを組み立てている担当者は、その検索テンプレートが どのようなコードから実行されているか? など、細かい実装処理について熟知する必要はありません。 検索ロジックを変更したい場合も、Elasticsearch 側で検索テンプレートを変更し、 Python 側からは検索テンプレートのidとパラメタを渡すことで変更されたロジックで検索できるようになります。 次回は、生成AI と組み合わせた RAG を動かしてみたいと思います。 あくまでもサンプルなので、エラー処理やサニタイジング処理は省略しています。 ↩︎ なお、このサンプルプログラムでは、データの投入は行っていません。前回までのブログでデータは投入済であることが前提です。 Python から bulk API を呼び出してデータを投入することも可能です。 詳細は、下記を参照してください。 https://elasticsearch-py.readthedocs.io/en/v8.15.1/api/elasticsearch.html#elasticsearch.client.Elasticsearch.bulk その他の参考URL – https://www.elastic.co/guide/en/elasticsearch/client/python-api/current/index.html – https://elasticsearch-py.readthedocs.io/en/v8.15.1 ↩︎ Elastic Cloud 上で、(AWS/Azure/GCP用に) Private Link を設定した場合、Cloud IDでは接続できなくなります。 その場合は、Cluster ID と ホスト名を指定して接続するようにしてください。 (詳細は、公式ドキュメントを参照してください。) ↩︎ The post Python を使った Elasticsearch へのアクセス first appeared on Elastic Portal .
アバター
1. 前書き こんにちは。 サイオステクノロジーの田川です。 今回は、キーワード検索とベクトル検索を組み合わせたハイブリッド検索を行ってみます。 対象者 Elastic Cloud のアカウントを持っている人(トライアルライセンスを含む) Elasticsearch の初心者~中級者 できるようになること Elasticsearch で日本語のハイブリッド検索(キーワード検索とベクトル検索のRRF検索)を行えるようになる。 Elasticsearch で検索用テンプレートを使った検索を行えるようになる。 前提条件 Elastic Cloud (version: 8.15.0) Elastic Cloud 上に日本語でのベクトルデータを投入済みである。 前々回の記事 に日本語でのベクトルデータの投入作業を記載しています。 (2024年11月 5日時点の情報を元に記載しています。) 2. ハイブリッド検索 以前の記事で、 日本語でのキーワード検索 、 ベクトル検索 を行いましたが、 キーワード検索では上位にランキングされるのにベクトル検索では下位にランキングされるクエリ あるいは逆に ベクトル検索では上位にランキングされるのにキーワード検索では下位にランキングされるクエリ があったりします。 これらを平準化するための一つの方法がハイブリッド検索です。 キーワード検索とベクトル検索の両方を行い、検索漏れを少なくしようというものです。 Elasticsearch では、ハイブリッド検索の方法として2種類の方法を利用可能です。 スコアベース <キーワード検索のスコア> と <ベクトル検索のスコア> から <総合的なスコア> を算出する。 順位ベース <キーワード検索の順位> と <ベクトル検索の順位> から <総合的な順位> を算出する。(RRF) 今回は、後者の方法(RRFを使った順位ベースのハイブリッド検索)を使います。 (*脚注1) 1 3. RRF Reciprocal Rank Fusion (RRF) は、複数の検索結果の順位から、最終的な順位を決定するアルゴリズムです。 score = 1 / (検索方法1での順位 + rank_constant) + ... + 1 / (検索方法nでの順位 + rank_constant) により score を計算して、score が高いものの順に検索結果を並び替えます。 (*脚注2) 2 RRFによるハイブリッド検索を行うリクエストの例: (キーワード検索:1回、ベクトル検索:1回) GET /momotaro_v3/_search { "_source": false, "fields": [ "chunk_no", "content" ], "size": 10, "retriever": { "rrf": { "retrievers": [ { "standard": { "query": { "match": { "content": "キーワード検索用クエリ" } } } }, { "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": "ベクトル検索用クエリ" } } } } ], "rank_window_size": "10", "rank_constant": "60" } } } RRF検索を行いたい場合、”retriever” の “rrf” を利用します。 “rrf” 内の “retrievers” 配列に、リランク元の順位を与える検索内容を記述します。 上記の例では、キーワード検索:1回、ベクトル検索:1回としていますが、 キーワード検索:2回、ベクトル検索:3回 や、キーワード検索:2回、ベクトル検索:0回 のようなことも可能です。 (ただし検索回数を多くすると、その分、遅くなったりリソースを消費したりします。) 上記のリクエスト内の “k”, “num_candidates”, “rank_window_size”, “rank_constant” の値は、 リランク処理に影響を与えるパラメータです。 おおまかに説明すると下表のようになります。 項目名 説明 上記での設定値 k ベクトル検索時に上位何件の検索結果を返すか? 10 num_candidates 各シャードで何件の候補を出すか? 100 rank_window_size 各検索時に、何件返すか? 10 rank_constant 前述の RRF のスコア計算時の定数(rank_constant) 60 (*脚注3) 3 実運用でRRFによるリランク処理を行いたい場合は、これらのパラメータの値を調整する必要があります。 今回は、サンプルですので、細かい調整は行わずに、上記の値を使用します。 4. 実際にハイブリッド検索してみる。 実際に、ハイブリッド検索を行ってみます。 検索例: キーワード検索用クエリ: “桃太郎の家来が桃太郎からもらったもの” ベクトル検索用クエリ: “桃太郎の家来が桃太郎からもらったもの” ※キーワード検索用のクエリ と ベクトル検索用のクエリは異なっていてもかまいませんが、 サンプルとして同じ内容を指定しています。 (参考)キーワード検索の結果: 第2位 "chunk_no": [ 82 ] "content": [ " 犬はきびだんごを一つもらって、桃太郎のあとから、ついて行きました。" ] ... 第3位 "chunk_no": [ 102 ] "content": [ " きじもきびだんごを一つもらって、桃太郎のあとからついて行きました。" ] (参考)ベクトル検索の結果: 第4位 "chunk_no": [ 82 ] "content": [ " 犬はきびだんごを一つもらって、桃太郎のあとから、ついて行きました。" ] ... 第6位 "chunk_no": [ 102 ] "content": [ " きじもきびだんごを一つもらって、桃太郎のあとからついて行きました。" ] ハイブリッド検索の結果: 第1位 "chunk_no": [ 82 ] "content": [ " 犬はきびだんごを一つもらって、桃太郎のあとから、ついて行きました。" ] ... 第3位 "chunk_no": [ 102 ] "content": [ " きじもきびだんごを一つもらって、桃太郎のあとからついて行きました。" ] 1位と3位にヒットしています。 他のクエリでも試したいところですが、毎回、前述のような長いリクエストを発行するのが 面倒だと思われた方もいらっしゃるかもしれません。 そういった問題を解消するために、Elasticsearchでは、検索用テンプレートの機能が用意されています。 検索用テンプレートを使用するメリットとして、下記などが挙げられます。 Python などの Client から検索する場合に、Python側のコードが簡潔になる。 検索用クエリを修正する場合、Python側のコードの修正は不要。 Elasticsearch側のクエリを書く人と、Python側の呼び出し用コードを書く人の作業分担がしやすい。 (*脚注4) 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}}60{{/rank_constant}}" } } } """ } } 上記の検索テンプレート内では、下記のパラメータを指定可能としています。 パラメータ名 説明 ディフォルト値 size ハイブリッド検索結果の返却件数 10 query_string キーワード検索クエリ なし num_candidates ベクトル検索時のシャードごとの候補の件数 100 query_for_vector ベクトル検索クエリ なし rank_constant RRF検索時の rank_constant 60 (k=size, rank_window_size=size としています。) いったん上記で検索用テンプレートを作ってしまえば、 これ以降は検索用テンプレートを使って検索することができるようになります。 検索用テンプレートを使って検索リクエストを書いてみます。 キーワード検索用クエリ: “桃太郎の家来になった動物” ベクトル検索用クエリ: “桃太郎の家来になった動物” GET /momotaro_v3/_search/template { "id": "rrf_search_template", "params": { "query_string": "桃太郎の家来になった動物", "query_for_vector": "桃太郎の家来になった動物" } } (“id” に指定している “rrf_search_template” は、先ほど作成した検索用テンプレートの id です。) 随分と簡単に書けるようになりました。 これを実行した結果を見てみます。 (参考)キーワード検索の結果: 第2位 "chunk_no": [ 103 ] "content": [ " 犬と、猿と、きじと、これで三にんまで、いい家来ができたので..." ] (参考)ベクトル検索の結果: 第1位 "chunk_no": [ 103 ] "content": [ " 犬と、猿と、きじと、これで三にんまで、いい家来ができたので..." ] ハイブリッド検索の結果: 第1位 "chunk_no": [ 103 ] "content": [ " 犬と、猿と、きじと、これで三にんまで、いい家来ができたので..." ] キーワード検索では2位でしたが、ハイブリッド検索では1位になりました。 他の例も見てみます。 キーワード検索用クエリ: “おじいさんが山へ行ったのはなぜ” ベクトル検索用クエリ: “おじいさんが山へ行ったのはなぜ” GET /momotaro_v3/_search/template { "id": "rrf_search_template", "params": { "query_string": "おじいさんが山へ行ったのはなぜ", "query_for_vector": "おじいさんが山へ行ったのはなぜ" } } (参考)キーワード検索の結果: 第1位 "chunk_no": [ 1 ] "content": [ "...おじいさんは山へしば刈りに...行きました。" ] (参考)ベクトル検索の結果: 第2位 "chunk_no": [ 1 ] "content": [ "...おじいさんは山へしば刈りに...行きました。" ] ハイブリッド検索の結果 第1位 "chunk_no": [ 1 ] "content": [ "...おじいさんは山へしば刈りに...行きました。" ] ベクトル検索では2位でしたが、ハイブリッド検索では1位になりました。 良好な結果が得られています。 5. まとめ このように Elasticsearch では、独自に難しいロジックを組むことなく、 ハイブリッド検索を行うことができます。 今回はサンプルでデータ件数が少ないこともありますが、検索も高速です。 (*脚注5) 5 また、検索用テンプレートを活用することで呼び出し側のコードを簡潔にすることができました。 次回は、Python を使った Elasticsearch の操作について紹介したいと思います。 キーワード検索のスコア と ベクトル検索のスコア から総合的なスコアを算出したい場合は、 下記を参照してください。 https://www.elastic.co/guide/en/elasticsearch/reference/current/knn-search.html#_combine_approximate_knn_with_other_features 単純に  キーワード検索のスコア + ベクトル検索のスコア とするだけでなく、  キーワード検索のスコア + ベクトル検索のスコア*10 のようなスコア計算を行うことも可能です。 ↩︎ RRF の詳細については、下記を参照してください。 https://www.elastic.co/guide/en/elasticsearch/reference/current/rrf.html https://www.elastic.co/guide/en/elasticsearch/reference/current/retriever.html#rrf-retriever-example-hybrid ↩︎ RRFによるハイブリッド検索時のパラメタの詳細は、下記を参照してください。 https://www.elastic.co/guide/en/elasticsearch/reference/current/rrf.html#rrf-api https://www.elastic.co/guide/en/elasticsearch/reference/current/knn-search-api.html#knn-search-api-request-body ↩︎ 検索テンプレートの詳細については、下記を参照してください。 https://www.elastic.co/guide/en/elasticsearch/reference/current/search-template.html ( _render を使って、作成した検索テンプレートをテストすることもできます。) ↩︎ ハイブリッド検索にすれば、それだけで完璧というわけではありません。 格納するデータのチャンクサイズの調整を行ったり、検索対象のデータのメタ情報(タイトル、作成者、最終更新日時など)を考慮した検索を行った方がいい場合もあります。 その他、Elasticsearch v8.14 からは、 キーワード検索 + (Cohereなどを使った)セマンティックリランク といった手法も行えるようになりました。こちらは、今後、紹介する予定です。 ↩︎ The post Elasticsearchでのハイブリッド検索 first appeared on Elastic Portal .
アバター
1. 前書き こんにちは。 サイオステクノロジーの田川です。 今回は、Elasticsearch で実際に日本語でのベクトル検索を行ってみたいと思います。 対象者 Elastic Cloud のアカウントを持っている人(トライアルライセンスを含む) Elasticsearch の初心者~中級者 できるようになること Elasticsearch で日本語のベクトル検索(ANN)を行えるようになる。 前提条件 Elastic Cloud (version: 8.15.0) Elastic Cloud 上に日本語でのベクトル検索の準備が完了している。 前回の記事 に日本語用のベクトル検索の準備作業を記載しています。 (2024年10月28日時点の情報を元に記載しています。) 2. ベクトル検索の種類 前回、ベクトル検索を行うための準備を行いました。 今回は、実際にベクトル検索を行ってみたいと思いますが、 その前に Elasticsearch における密ベクトルに対する検索の種類について説明したいと思います。 Elasticsearch では密ベクトルに対し2種類の検索方法が用意されています。 厳密に、クエリに最も近いベクトルを検索する方法(Exact kNN) 厳密ではないが、クエリにある程度近いものを高速に検索する方法(Approximate kNN(ANN)) 今回は、ある程度近いものを取得できればよいので、後者の方法(ANN)を利用します。 (*脚注1) 1 3. ベクトル検索の実行 いくつか検索してみます。 3.1 ベクトル検索(その1) クエリ1: “桃太郎が鬼が島へ向かった際の乗り物は” まず、キーワード検索をしてみます。 ( 前回のブログ記事 の中で、momotaro_v3 インデックスに桃太郎の本文をベクトル値を含めて登録済です。) GET /momotaro_v3/_search { "_source": false, "fields": ["chunk_no", "content"], "query": { "match": { "content": "桃太郎が鬼が島へ向かった際の乗り物は" } } } 上記のキーワード検索を行っても、top 10 以内に「船」という情報は見つかりません。 次にANNベクトル検索を行ってみます。 Elasticsearch で ANNベクトル検索を行いたい場合、”knn” を利用します。 (*脚注2) 2 GET /momotaro_v3/_search { "_source": false, "fields": ["chunk_no", "content"], "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": "桃太郎が鬼が島へ向かった際の乗り物は" } } } } (*脚注3) 3 結果 ... 第3位 "chunk_no" : [ 133 ], "content": " 桃太郎は...また船に乗りました。..." 第4位 "chunk_no" : [ 113 ], "content": " 桃太郎も...船の進むにしたがって..." ... 第9位 "chunk_no": [ 105 ], "content": " 桃太郎と、...この船に乗り込みました。" いくつか「船」を示唆する情報がヒットしています。 ここで注目なのは、検索クエリである “桃太郎が鬼が島へ向かった際の乗り物は” から算出されるベクトル値を明示していないことです。 ベクトル検索では、検索クエリのベクトル値に近いベクトル値を持つドキュメントを検索するので、 検索クエリのベクトル値が必要なはずです。 にもかかわらずベクトル検索を行うことができます。 これは、 "query_vector_builder": { "text_embedding": { "model_id": "モデルid", "model_text": "検索したいクエリ" } } の部分に入力された情報を元に Elasticsearch が “検索したいクエリ” → ベクトルの生成 を内部的に行ってくれているためです。 このように、Elasticsearch では、ドキュメントの登録時だけでなく、 検索時もベクトル化の処理を実装することなく、ベクトル検索を行うことができます。 3.2 ベクトル検索(その2) 別の質問をしてみます。 クエリ2: “桃から産まれたのは” まず、キーワード検索をしてみます。 GET /momotaro_v3/_search { "_source": false, "fields": ["chunk_no", "content"], "query": { "match": { "content": "桃から産まれたのは?" } } } 第5位 "chunk_no": [ 40 ], "content": " そして桃の中から生まれた子だというので、この子に桃太郎という名をつけました。" ヒットはしましたが、5位に表示されました。 ベクトル検索をしてみます。 GET /momotaro_v3/_search { "_source": false, "fields": ["chunk_no", "content"], "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": "桃から産まれたのは?" } } } } 第1位 "chunk_no": [ 40 ], "content": " そして桃の中から生まれた子だというので、この子に桃太郎という名をつけました。" 1位に表示されました。 キーワード検索では、”産まれる” と “生まれる” は全く別の単語という扱いのため、上位にヒットしませんでしたが、 ベクトル検索では、”産まれる” と “生まれる” が近いベクトルを持っているため、 上位にヒットしたものと考えられます。 3.3 ベクトル検索(その3) ベクトル検索のちょっと変わった使い方としては、別の言語による検索も(ある程度)可能です。 クエリ3: “Who was born from a peach ?” キーワード検索では、全くヒットしません。 元の文章が日本語で書かれているのに英語で検索を行っているためです。 一方、ベクトル検索では、第3位に表示されました。 第3位 "chunk_no": [ 40 ], "content": " そして桃の中から生まれた子だというので、この子に桃太郎という名をつけました。" “peach” と “桃”、”born” と “生まれる” が近いものとして捉えられているためだと思われます。 4. まとめ 今回、ANNによるベクトル検索を行い、ある程度期待した結果を得ることができました。 しかも、独自にベクトル化の処理を実装する必要はありませんでした。 ただし、ベクトル検索は万能ではありません。キーワード検索の方が適している場合もあります。 次回は、キーワード検索とベクトル検索の両方を行うハイブリッド検索を行ってみたいと思います。 Elasticsearch は、超大規模なデータであっても動作することを念頭に、 時間やメモリを消費して厳密な値を求めることよりも、 近似値であっても、より速く、より省メモリーで算出する傾向にあるように感じます。 ANN だけでなく、統計情報の中央値などの算出に関しても、 (厳密な値ではないかもしれませんが)近似値をより早く算出するような動きになっています。 (厳密な値を求めようとして、莫大な時間がかかってタイムアウトになったり、 莫大なメモリーを必要として Out of memory になってエラーになることは避け, 近似値であっても、限られたリソース(時間、メモリ)内で答えを出そうとします。) どうしても最近傍探索を行いたいという方は、下記を参考にしてみてください。 cosineSimilarity 関数を利用しますが、ANN検索よりもかなり手間がかかります。 https://www.elastic.co/guide/en/elasticsearch/reference/current/knn-search.html#exact-knn https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-script-score-query.html https://www.elastic.co/jp/blog/text-similarity-search-with-vectors-in-elasticsearch ↩︎ Elasticsearch で、ANN検索を行う場合、”knn” パラメータを指定して検索を行います。 (ANNなのに”knn”を指定するのでややこしいのですが、そういう風になっている、と理解してください。) ↩︎ “.multilingual-e5-small_linux-x86_64” では、検索クエリのプレフィックスとして “query: ” を付けた方がいいという仕様のようですが、 今回は、サンプルなので、プレフィックスは付けていません。 https://huggingface.co/intfloat/multilingual-e5-small#faq ↩︎ The post Elasticsearchでのベクトル検索 first appeared on Elastic Portal .
アバター
こんにちは。 サイオステクノロジーの田川です。 今回は、ベクトル検索のための準備を行います。 対象者 Elastic Cloud のアカウントを持っている人(トライアルライセンスを含む) Elasticsearch の初心者~中級者 できるようになること Elastic Cloud でベクトル検索を行うための準備が完了する。 (今回は、まだ、ベクトル検索は行いません。) 前提条件 Elastic Cloud (version: 8.15.0) Elastic Cloud 上のインデックスにデプロイメントを作成済 (2024年10月21日時点の情報を元に記載しています。) 1. ベクトル検索とは 前回、形態素解析を適用したことで、随分と日本語の検索がしやすくなりましたが、 “桃太郎の家来になった鳥は?” のような検索を行っても望んでいるような検索結果にはなりません。 (検索しても関係が薄そうなドキュメントが多数ヒットしてしまいます。) なぜなら、人間は ”きじ”が”鳥”の一種であること を知っていますが、 単なるキーワード検索では “きじ” != “鳥” となるため、 きじを含むドキュメントがヒットしないのです。 このような問題の解決策の一つがベクトル検索になります。 例) “りんご”は、甘い果物。 “いちご”も甘い果物。 “犬”は(果物ではなく)動物 → よって、”りんご” と “いちご” は近い関係にあり、”りんご” と “犬” は、それよりも遠い関係にある。 ベクトル検索を使うと、このように意味の近いものを検索しやすくなります。 また、 “桃から産まれたのは?” をキーワード検索しても、関連が低そうなドキュメントが上位にヒットしてしまいます。 これは、”桃から産まれる” という文章の意味を Elasticsearch が理解していないため、 キーワード検索では、似たような意味のドキュメントを見つけることができないのです。 これもベクトル検索を行うことで、”桃から産まれる” に意味が近い(似ている)ドキュメントを 検索できるようになります。 (*脚注1) 1 2. ベクトルの種類 ベクトル化を行う前に、ベクトルの種類について説明しておきます。 ベクトルの種類は大きく分けて次の2つになります。 疎ベクトル ほとんどの要素が0で、少数の0でない値を持つベクトル。 データ量が少なく、高速 単純な単語の出現を示すことなど簡素な表現に向いている。 密ベクトル ほとんどの要素が0以外の値を持つベクトル。 データ量が多く、表現力が豊か。 自然言語や画像などの表現に向いている。 Elasticsearch では、疎ベクトル(sparse_vector)も、密ベクトル(dense_vector)も、 どちらも利用可能です。 詳細な説明はここでは省略しますが、小説の文章を検索したい場合は 疎ベクトルよりも密ベクトルの方が適しているため、今回は密ベクトルを利用します。 (*脚注2) 2 3. Elasticsearch でのベクトル検索の準備 Elasticsearch で日本語のベクトル検索を行いたい場合、準備作業として以下が必要となります。 3.1. 日本語の形態素解析用のプラグインのインストール 3.2. トレーニングされたモデルのインストール 3.3. パイプラインの作成 3.4. ベクトル値を格納するフィールドの作成 3.1 日本語の形態素解析用プラグインのインストール これは 前回 analysis-icu と analysis-kuromoji を追加インストール済なので、 ここでは説明は省略します。 3.2 トレーニングされたモデルのインストール Elasticsearch では、日本語用にトレーニングされたモデルとして、あらかじめ、以下の2つが用意されています。 model 次元数 .multilingual-e5-small_linux-x86_64 384 cl-tohoku__bert-base-japanese-v3 768 今回は、サンプルデータですので、軽量な .multilingual-e5-small_linux-x86_64 をインストールします。 (*脚注3) 3 手順: Elastic Cloud へログインします。 デプロイメント一覧からデプロイメント(このブログでは “SIOS-BLOG-SAMPLE-1” )の Manage をクリックします。 Edit 左のメニューから Edit をクリックします。 Edit内の各種設定を行います。 – Enable Autoscaling for: “Machine Learning only” となっていることを確認します。 なっていない場合は、”Machine Learning only” に変更します。 – Hot data and Content tier 厳密なサイジングに関する説明は、ここでは割愛しますが、 Machine Learning を利用する場合、Hot data and Content tier に割り当てる zone は、 最低でも 2 にしておいた方がいいです。 ここでは、2 zones としておきます。(あくまでもサンプル用の設定です。本番運用時には、きちんとしたサイジング設計が必要です) – Machine Learning instances Machine Learning instances の Minimum Size per zone は、4GB または、それ以上に変更します(モデルの展開に4GB程度必要になります)。 変更後、一番下の [Save]をクリックします。 反映にしばらくかかるので、待ちます。 左のメニューから Kibana をクリックします。 Kibana を Open し、Home 画面へ戻ります。 Home画面の左のメニューから Analytics の Machine Learning をクリックします。 Machine Learning の画面が表示されます。 左のメニューの中から、 Model Management の Trained Models をクリックします。 Elasticsearch であらかじめ用意されているモデルの一覧が表示されます。 この右上の [Add trained model] をクリックします。 次に、下図のようなダイアログが表示されるので、 Intel and Linux Optimized .multinlingual-e5-small_linux-x86_64 を選択してください。(※ディフォルトでは、上部にある .elser_model_2_linux-x86_64 が選択されています。) .multinlingual-e5-small_linux-x86_64 を選択後、下の [Download] をクリックします。 .multinlingual-e5-small_linux-x86_64 のダウンロードが開始されるので、しばらく待ちます。 .multinlingual-e5-small_linux-x86_64 のダウンロードが終了すると、 元のモデル一覧中の .multilingual-e5-small_linux-x86_64 のステータスが Ready to deploy に変わるので、右の3点メニューから ▷ Deploy をクリックします。 下図のようなダイアログが表示されるので、ディフォルトのまま [Start] をクリックします。 上図の設定値を転記すると、以下のようになります。 項目 設定値 Deployment ID .multilingual-e5-small_linux-x86_64 Priority normal Number of allocations 1 Threads per allocation 1 .multilingual-e5-small_linux-x86_64 モデルのデプロイが始まり、 モデル一覧中の .multilingual-e5-small_linux-x86_64 モデルのステータスが Starting deployment… に変わります。デプロイが終了するまで、しばらく待ちます。 しばらく待っても変わらない場合は、右上の [Refresh] を押してみてください。 (それでも変わらない場合は、再びしばらく待ってから[Refresh]を押してみてください。) デプロイが完了すると、 モデル一覧中の .multilingual-e5-small_linux-x86_64 モデルのステータスが Deployed に変わります。 なお、機械学習モデルがインストールされることで、Elasticsearchの1時間あたりの利用料金が若干上昇します。 3.3 パイプラインの作成 前述でインストールしたモデルを使ってベクトル化を行えるよう、パイプラインを作成します。 Elasticsearch で言うところのパイプラインというのは、ざっくり説明すると、ドキュメント投入時に行われる処理になります。 パイプラインで、何らかの処理が行われます。 (今回の記事の中では、文字列 → ベクトル化の処理) 参考URL https://www.elastic.co/guide/en/elasticsearch/reference/current/ingest.html 下記のリクエストを Elastic Cloud の Console に入力し、実行します。 パイプライン作成リクエスト: 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" } } } ] } 上記で作成した japanese-text-embeddings パイプラインの意味は、次のとおりです。 content フィールドに格納された文字列を .multilingual-e5-small_linux-x86_64 を使ってベクトル化し、 算出されたベクトル値を text_embedding フィールドに格納します。 作成したパイプラインをテストすることも可能です。 パイプラインのテスト用のリクエストを Console から発行してみます。 GET _ingest/pipeline/japanese-text-embeddings/_simulate { "docs": [ { "_source": { "content": "むかし、むかし、あるところに" } } ] } レスポンス: { "docs": [ { "doc": { ... "_source": { "text_embedding": { "predicted_value": [ ... 384次元のベクトル値 ... ], "model_id": ".multilingual-e5-small_linux-x86_64" }, "content": "むかし、むかし、あるところに" }, ... } } ] } うまく動いていそうです。 3.4 ベクトル値を格納するフィールドの作成 前述のパイプラインを利用してベクトル化された値を格納できるよう、新たなインデックスを作成します。 用途 項目 値 インデックス インデックス名 momotaro_v3 チャンク番号用 フィールド名 chunk_no フィールドタイプ integer 本文用 フィールド名 content フィールドタイプ text 登録用アナライザー ja_kuromoji_index_analyzer 検索用アナライザー ja_kuromoji_search_analyzer ベクトル値(の親フィールド)用 フィールド名 text_embedding フィールドタイプ (マルチフィールド)(*1) (*1) text_embedding フィールドは、内部に別フィールド(model_id, predicted_value)を持つマルチフィールドとします。 マルチフィールドの詳細については、下記を参照してください。 (一般的なリレーショナルデータベースのカラムにはない考え方です) https://www.elastic.co/guide/en/elasticsearch/reference/current/mapping-types.html#types-multi-fields https://www.elastic.co/guide/en/elasticsearch/reference/current/multi-fields.html text_embedding 内の子フィールド 用途 項目 値 ベクトルのモデルid用 フィールド名 model_id (*2) フィールドタイプ keyword 最大長 256 ベクトル値用 フィールド名 predicted_value フィールドタイプ dense_vector 次元数 384 (*2) 今回の設定では、model_id に格納される値は、”.multilingual-e5-small_linux-x86_64″固定となります。 これらを踏まえて、インデックス “momotaro_v3” を作成します。 下記のリクエストを Elastic Cloud の Console に入力し、実行します。 momotaro_v3 インデックスの作成リクエスト: PUT /momotaro_v3 { "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": [ "きびだんご,きびだんご,キビダンゴ,カスタム名詞", "鬼が島,鬼が島,オニガシマ,カスタム名詞" ] } }, "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" ] } } } } } momotaro_v3 インデックスのマッピング作成のリクエスト: PUT /momotaro_v3/_mappings { "dynamic": false, "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 } } } } } 4. ドキュメントのコピー(Reindex) Reindex API を使って、momotaro_v2 から momotaro_v3 へドキュメントをコピーします。 その際、pipelineに “japanese-text-embeddings” を指定します。 (これにより、momotaro_v3 の “content” フィールドへ値を格納する際にベクトル化の処理が行われ、算出されたベクトル値が “text_embedding” フィールドに格納されます。) 下記のリクエストを Elastic Cloud の Console に入力し、実行します。 Reindex リクエスト: POST /_reindex?refresh=true { "source": { "index": "momotaro_v2" }, "dest": { "index": "momotaro_v3", "pipeline": "japanese-text-embeddings" } } これで、momotaro_v3 インデックスに桃太郎の本文と、本文から算出されたベクトル値が格納されます。 5. 登録されたドキュメントの確認 下記の検索リクエストを Console から発行してみます。 GET /momotaro_v3/_search レスポンス: { ... "hits": { ... "hits": [ { "_index": "momotaro_v3", ... "_source": { "chunk_no": 1, "text_embedding": { "predicted_value": [ 0.09554096311330795, ... (384次元のベクトル) ... ], "model_id": ".multilingual-e5-small_linux-x86_64" }, "content": " むかし、むかし、あるところに、..." }, ... } ] }, ... } text_embedding.predicted_value フィールドにベクトル値が格納されていることがわかります。 お気づきかもしれませんが、Elasticsearch では、ベクトル化の処理を独自に実装しなくても ベクトル化の処理が行われ、算出されたベクトル値が登録されます。 次回は、実際にベクトル検索を行ってみたいと思います。 同義語を定義することでキーワード検索でも似たような意味を持つドキュメントを検索することは可能ですが、 この記事では、同義語に関しては考慮していません。 ↩︎ Elasticsearch で疎ベクトルを扱うことは可能ですが、v8.15.0 の時点では、 ディフォルトで用意されている疎ベクトル用のモデル(ELSER)は英語の場合に推奨されるようです。日本語を利用する場合には、v8.15.0の時点では密ベクトルを利用するのがいいように思われます。 https://www.elastic.co/guide/en/machine-learning/current/ml-nlp-elser.html ↩︎ 今回は、.multilingual-e5-small_linux-x86_64 のモデルをインストールしましたが、 eland を使って、他のモデルをインストールすることも可能です。 https://www.elastic.co/guide/en/machine-learning/current/ml-nlp-deploy-models.html ↩︎ The post Elasticsearchでのベクトル検索の準備 first appeared on Elastic Portal .
アバター
こんにちは。 サイオステクノロジーの田川です。 前回、登録した「桃太郎」の内容を検索しようとしましたが、 うまく検索できないことがありました。 主な原因は、作成したインデックスが日本語用に設定されていなかったことにあります。 今回は、日本語に適したインデックスを作成してみます。 ※今回は、過去4回よりは、多少、難しい内容になっています。 対象者 Elastic Cloud のアカウントを持っている人(トライアルライセンスを含む) Elasticsearch の初心者~中級者 できるようになること Elastic Cloud のデプロイメントに日本語用の形態素解析に必要なプラグインを追加インストールする。 Elastic Cloud のデプロイメント内に日本語に適したインデックスを作成する。 reindex を用いて、旧インデックスから新インデックスへのドキュメントのコピーを行う。 日本語を含むキーワード検索を行う。 前提条件 Elastic Cloud (version: 8.15.0) Elastic Cloud 上にデプロイメントを作成済 (2024年10月15日時点の情報を元に記載しています。) 1. プラグインの追加インストール 日本語に適した検索を行えるようにするため、日本語の形態素解析を行えるようにします。 日本語の形態素解析の例です。 おばあさんは川へ洗濯に行きました ↓ おばあさん / は / 川 / へ / 洗濯 / に / 行きました このように形態素解析することで、日本語の検索を行いやすくなります。 日本語の形態素解析を行えるようにするため、 Elastic Cloud のデプロイメントに対し、必要なプラグインの追加インストールを行います。 Elastic Cloud へログインします。 Elastic Cloud のデプロイメント(このブログ記事では”SIOS-BLOG-SAMPLE-1″)の Manage をクリックします。 左のメニューから Edit をクリック Manage user settings and extensions をクリック Elasticsearch user settings and extensions のダイアログが表示されます。 ここで、Extensions のタブをクリックします。 追加可能なプラグインの一覧が表示されます。 ここから、 analysis-icu analysis-kuromoji の2つにチェックを入れて、ダイアログ下部の [Back] をクリックします。 (この状態では、2つのプラグインは、まだ追加されていません。) ダイアログが閉じられた後、画面下部の [Save] をクリックします。 次のような確認ダイアログが表示されるので、[Confirm] をクリックします。 [Confirm] をクリックすると、2つのプラグインの追加インストールが開始されます。 しばらく待つと、プラグインのインストールが完了し、次のような画面になります。 これで、日本語の形態素解析を行える準備ができました。 2. 日本語用のインデックスの作成準備 日本語の文章を検索するためのインデックスの作成準備を行います。 設定する項目の値を下表に記載しておきます。 項目 設定値 備考 例 インデックス名 momotaro_v2 number_of_shards 1 あくまでもサンプルなので 1 としておく。 number_of_replicas 1 あくまでもサンプルなので 1 としておく。 refresh_interval 3600s analyzer ja_kuromoji_index_analyzer 登録用アナライザー(*1) analyzer ja_kuromoji_search_analyzer 検索用アナライザー(*1) char_filter ja_normalizer (*2) char_filter kuromoji_iteration_mark 「々」、「ヽ」、「ゝ」 などの踊り字の正規化する。 “各々”→”各各” tokenizer ja_kuromoji_tokenizer (*3) user_dictionary_rules 定義する “きびだんご”と”鬼が島”を登録しておく。(*4) filter kuromoji_baseform 原形化 “来ました”→”来る” filter kuromoji_part_of_speech 不要な品詞を除去する。 “東京の天気は晴れ”→”東京”,”天気”,”晴れ” filter cjk_width 全角英数記号→半角英数記号、半角カナ→全角カナ “#A1カナ”→”#A1カナ” filter ja_stop ストップワードの除去 “この街角”→”街角” filter kuromoji_number 漢数字の半角数字化 “一二三四”→”1234” filter kuromoji_stemmer 長音の除去 “サーバー”→”サーバ” kuromoji_tokenizer などの詳細については、Elastic社の公式ドキュメントを参照してください。 https://www.elastic.co/guide/en/elasticsearch/plugins/current/analysis-icu.html https://www.elastic.co/guide/en/elasticsearch/plugins/current/analysis-kuromoji.html など (*1) 今回の設定内容では、登録用のアナライザーと検索用のアナライザーは同じなので、兼用することも可能ですが、将来、検索用のアナライザーにのみ同義語を登録する場合もあるため、登録用と検索用のアナライザーは別にしておくことをお薦めします。 (*2) ja_normalizer の詳細設定は下記になります。 項目 値 説明 例 type icu_normalizer Elasticsearchで提供されている char_filter . name nfkc_cf 正規分解して合成する。ただし合成文字は分解する。また、小文字への統一も行う。 “㌢”→”セ”,”ン”,”チ”, “APPLE”→”apple” mode compose 分解された文字を合成する。 “か” + “゛” → “が” (*3) ja_kuromoji_tokenizer の詳細設定は下記になります。 項目 値 説明 例 type kuromoji_tokenizer トークン化を行う。 “東京国際空港”→”東京”,”東京国際空港”,”国際”,”空港” mode search 検索を対象としたセグメンテーションで単語分割する。 “ファンクションコール”→”ファンクション”,”コール” discard_compound_token true mode=searchのときに複合語が出力されなくなる。 “東京国際空港”→”東京”,”国際”,”空港” (*4) 本来は、何をユーザー辞書に登録すべきか?を調査すべきですが、今回は、その手順を省略しています。後からユーザー辞書に単語を追加登録することは可能ですが、その場合ドキュメントの再登録が必要となります。(後からユーザー辞書に単語を追加登録しても、既にインデックスされたドキュメントに対しては影響を与えないため) ※上記以外に同義語登録もありますが、ひとまず、考慮しないでおきます。 (なお、検索用の同義語であれば後から追加しても、ドキュメントの再登録は不要です。) ※その他、N-gramを考慮して検索を行うこともありますが、今回は触れないでおきます。 3. 日本語用のインデックスの作成 前項で掲載した値を踏まえて、日本語用のインデックスを新規に作成します(インデックス名= “/momotaro_v2” )。 PUT /momotaro_v2 { "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": [ "きびだんご,きびだんご,キビダンゴ,カスタム名詞", "鬼が島,鬼が島,オニガシマ,カスタム名詞" ] } }, "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" ] } } } } } content フィールドの登録時に “ja_kuromoji_index_analyzer” を適用し、 content フィールドに対する検索時に “ja_kuromoji_search_analyzer” を適用するように設定します。 PUT /momotaro_v2/_mappings { "dynamic": false, "properties": { "chunk_no": { "type": "integer" }, "content": { "type": "text", "analyzer": "ja_kuromoji_index_analyzer", "search_analyzer": "ja_kuromoji_search_analyzer" } } } これで、ドキュメントを登録する準備はできました。 4. ドキュメントのコピー(Reindex) Reindex API を使って、 事前に momotaro_v1 インデックスに登録しておいた内容を、 新たに momotaro_v2 インデックスへコピーします。 momotaro_v2 インデックスへドキュメントが登録される際に、上記で設定しておいた analyzer (ja_kuromoji_index_analyzer) が適用され、適切な形態素解析が行われます。 reindex 終了時に refresh を行い、検索可能な状態にするよう refresh=true を付けておきます。 (今回のインデックスでは、リフレッシュ間隔を1時間としているため) POST /_reindex?refresh=true { "source": { "index": "momotaro_v1" }, "dest": { "index": "momotaro_v2" } } Reindex API の詳細については、下記を参照してください。 https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-reindex.html なお、momotaro_v1 インデックスにデータを登録していない場合は、 momotaro_v2 インデックスへ bulk API を使って、データを登録してください。 これで momotaro_v2 インデックスにドキュメントが登録され、日本語での検索を行えるようになりました。 5. 検索してみる。 前回うまく検索できなかった”桃太郎”を検索してみます。 検索リクエスト: GET /momotaro_v2/_search { "query": { "match": { "content": "桃太郎" } } } レスポンス: { ... "hits": { "total": { "value": 31, "relation": "eq" }, "max_score": 2.3206909, "hits": [ { "_index": "momotaro_v2", "_id": "*******************", "_score": 2.3206909, "_source": { "chunk_no": 75, "content": "「桃太郎さん、桃太郎さん、どちらへおいでになります。」" } }, ... ] } } ちゃんと31件ヒットします。 (日本語用に設定されていない momotaro_v1 インデックスでは、40件でした。) “桃太郎” または “きびだんご” を含むドキュメントを検索してみます。 検索リクエスト: GET /momotaro_v2/_search { "query": { "match": { "content": "桃太郎 きびだんご" } } } レスポンス: { ... "hits": { "total": { "value": 36, "relation": "eq" }, "max_score": 4.487536, "hits": [ { "_index": "momotaro_v2", "_id": "****************", "_score": 4.487536, "_source": { "chunk_no": 62, "content": " きびだんごがうまそうにでき上がると、桃太郎のしたくもすっかりでき上がりました。" } }, ... ] } } 36件がヒットします。 (日本語用に設定されていない momotaro_v1 インデックスでは、108件でした。) “桃太郎” と “きびだんご” の両方を含むドキュメントを検索してみます。 検索リクエスト: GET /momotaro_v2/_search { "query": { "match": { "content" : { "query" : "桃太郎 きびだんご", "operator": "and" } } } } レスポンス: { ... "hits": { "total": { "value": 5, "relation": "eq" }, "max_score": 4.487536, "hits": [ { "_index": "momotaro_v2", "_id": "*******************", "_score": 4.487536, "_source": { "chunk_no": 62, "content": " きびだんごがうまそうにでき上がると、桃太郎のしたくもすっかりでき上がりました。" } }, ... ] } } 5件がヒットしました。 “桃太郎”は含むが”きびだんご”を含まないドキュメントを検索してみます。 検索リクエスト: GET /momotaro_v2/_search { "query": { "bool": { "must": [ { "match": { "content" : "桃太郎" } } ], "must_not": [ { "match": { "content": "きびだんご" } } ] } } } レスポンス: { ... "hits": { "total": { "value": 26, "relation": "eq" }, "max_score": 2.3206909, "hits": [ { "_index": "momotaro_v2", "_id": "Qv6NLJIBnwZx8PFpcU2K", "_score": 2.3206909, "_source": { "chunk_no": 75, "content": "「桃太郎さん、桃太郎さん、どちらへおいでになります。」" } }, ... ] } } 26件がヒットしました。 形態素解析の効果も確かめてみます。 検索リクエスト: GET /momotaro_v2/_search { "query": { "match": { "content" : "三にんの家来が桃太郎からもらったもの" } } } レスポンス: ... "content": " 桃太郎と、三にんの家来は、さっそく、この船に乗り込みました。" ... "content": " 犬と、猿と、きじと、これで三にんまで、いい家来ができたので、桃太郎は..." ... "content": " 犬はきびだんごを一つもらって、桃太郎のあとから、ついて行きました。" ... どのような形態素解析が行われているのか、確かめてみます。 リクエスト: GET /momotaro_v2/_analyze { "analyzer": "ja_kuromoji_search_analyzer", "text": "三にんの家来が桃太郎からもらったもの" } レスポンス: { "tokens": [ { "token": "3", "start_offset": 0, "end_offset": 1, "type": "word", "position": 3 }, { "token": "家来", "start_offset": 4, "end_offset": 6, "type": "word", "position": 7 }, { "token": "桃太郎", "start_offset": 7, "end_offset": 10, "type": "word", "position": 9 }, { "token": "もらう", "start_offset": 12, "end_offset": 15, "type": "word", "position": 11 } ] } つまり、”三にんの家来が桃太郎からもらったもの” の検索は、 “3”, “家来”, “桃太郎”, “もらう” のいずれかを含むドキュメントの検索に置き換わっています。 “三” は、”3″ に変換されています。 (検索対象のドキュメントには検索用キーワードとして”三”ではなく、”3″が登録されています。 検索用クエリ内の”三”も”3″に変換してから検索を行います。) “の”, “が”, “から” が不要な品詞と判断され除去されています。 “にん” も除去されていますが、これは、”に” と “ん” が不要な品詞と判断されているためだと思われます。 “もの” はストップワードとして除去されています(あるいは、”も” と “の” が不要な品詞と判断され削除されています)。 “もらった” が “もらう” に変換されています。。 このように、特別な形態素解析の処理を独自に実装しなくても、あらかじめ設定を行っておくことで Elasticsearch 内で自動的に形態素解析を行ってくれます。 今回は、日本語に適したインデックスを作成し、日本語でのキーワード検索ができるようになりました。 次回は、ベクトル検索について触れたいと思います。 ※参考URL https://www.elastic.co/jp/blog/how-to-implement-japanese-full-text-search-in-elasticsearch (2024-12-19) 古いインデックス名を momotaro → momotaro_v1 に訂正しました。 The post Elasticsearchでの日本語に適したインデックスの作成 first appeared on Elastic Portal .
アバター
こんにちは。 サイオステクノロジーの田川です。 今回は、インデックスに登録したドキュメントを検索してみたいと思います。 対象者 Elastic Cloud のアカウントを持っている人(トライアルライセンスを含む) Elasticsearch の初心者 できるようになること Elastic Cloud の Console から、ドキュメントに対して簡単な検索を行える。 前提条件 Elastic Cloud (version: 8.15.0) Elastic Cloud 上のインデックスにドキュメントを登録済 (2024年10月11日時点の情報を元に記載しています。) 1. 全件を検索 前回登録した「桃太郎」の内容を Elasticsearch の Search API を使って検索してみます。 Search API の詳細な仕様については、下記の公式ドキュメントを参照してください。 https://www.elastic.co/guide/en/elasticsearch/reference/current/search-search.html まずは、全ての内容を検索してみます。 初回に作成した “SIOS-BLOG-SAMPLE-1” のデプロイメント内の Console を表示します。 (Console 画面の表示方法については、 Elastissearch へのインデックスの作成 のブログ記事を参照してください。) Console のリクエスト欄に下記を入力して、 ▷ をクリックします。 GET /momotaro_v1/_search 全149ドキュメントがヒットしますが、右側のレスポンス欄には10件のみが表示されます。 これは、レスポンスとして返却される件数(“size”パラメータ)のディフォルトが 10 となっているためです。 { ... "hits": { "total": { "value": 149, "relation": "eq" }, "max_score": 1, "hits": [ ... (全10件分のドキュメント) ... ] } ※注 ディフォルトでは、score の大きい順にソートされますが、 この例では、検索条件やソート条件を全て省略しているので、並び順にさほど意味はありません。 149件全部を表示したい場合は、”size” に 149以上の値を設定する必要があります。 (なお、”size” の最大値は、10000となっています。) “size” の詳細については、下記を参照してください。 https://www.elastic.co/guide/en/elasticsearch/reference/current/paginate-search-results.html 下記のリクエストを Console から発行してみます。 GET /momotaro_v1/_search { "size": 150 } すると、右側のレスポンス欄には、全149件が表示されます。 ※注 通常は、必要以上にレスポンスを取得しないことをお勧めいたします。 上記で、”size” に 150 を指定しているのは、あくまでも、”size” の働きを確認するためです。 2. 検索条件付きの検索 2.1 キーワードを1つだけ指定した検索 今度は、本文に「桃太郎」を含むドキュメントのみ検索してみます。 “match” を使って検索してみます。 “match” クエリの詳細は、下記を参照してください。 https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-match-query.html 下記のリクエストを Console から発行してみます。 GET /momotaro_v1/_search { "query": { "match": { "content": "桃太郎" } } } すると、40件のドキュメントがヒットしました。 レスポンスの例: { "took": 1, ... "hits": { "total": { "value": 40, "relation": "eq" }, "max_score": 6.645262, "hits": [ ... 10件分のヒット内容 ... ] } } score が大きい順(検索条件に、よりマッチした順)での上位10件が表示されます。 “size” に 40 を指定して、”桃太郎”にマッチした全件を見てみます。 GET /momotaro_v1/_search { "query": { "match": { "content": "桃太郎" } }, "size": 40 } すると、下位の方では、”桃太郎”ではなく、”桃”にヒットしていることがわかります。 レスポンスの例: { ... "content": "...さっきの桃を重そうにかかえて来て、" ... } これは、検索用に指定した文字列の “桃太郎” が “桃”, “太”, “郎” に分解されてしまい、 “桃”, “太”, “郎” それぞれによる検索が行われているためです。 今回のサンプルでは、形態素解析を行う analyzer を指定していません。 ディフォルトの analyzer として standard analyzer が適用されますが、 standard analyzer が、そのような仕様(“桃太郎”が”桃”,”太”,”郎”に分解される)になっているためです。 standard analyzer を利用した場合に、”桃太郎”がどのように解析されるのか?を Analyze APIを使って確認してみます。 リクエスト: GET _analyze { "analyzer": "standard", "text": "桃太郎" } レスポンス: { "tokens": [ { "token": "桃", "start_offset": 0, "end_offset": 1, "type": "<IDEOGRAPHIC>", "position": 0 }, { "token": "太", "start_offset": 1, "end_offset": 2, "type": "<IDEOGRAPHIC>", "position": 1 }, { "token": "郎", "start_offset": 2, "end_offset": 3, "type": "<IDEOGRAPHIC>", "position": 2 } ] } このように “桃太郎” が、”桃”, “太”, “郎” に分解されているのがわかります。 Analyze API の詳細な説明については、下記を参照してください。 https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-analyze.html 正直なところ、何の対策もせずに standard analyzer を使って日本語を検索するのは、あまり好ましくありません。 念のため検索文字列に、”桃 太 郎” のように “桃” と “太” と “郎” の間に半角空白を入れて検索した場合にも 40件がヒットすることがわかります。 (*脚注1) 1 どうしても、”桃太郎”に合致するドキュメントのみを取得したい場合には、 下記のように、”match” ではなく “match_phrase” を指定してみてください。 “match_phrase” だと、”桃” と “太” と “郎” の並びが”桃太郎”になっているドキュメントのみがマッチします。 GET /momotaro_v1/_search { "query": { "match_phrase": { "content": "桃太郎" } }, "size": 40 } すると、ヒットする件数が31件となり、”桃太郎”を含むドキュメントのみが取得されます。 2.2 キーワードを2つ指定した検索(OR検索) 次に、「桃太郎」または「きびだんご」を含むドキュメントを検索してみます。 Elasticsearch の場合、”a” または “b” を含むドキュメントを検索したい場合は、 検索対象文字列に “a b” のように半角空白で連結して記載します。 GET /momotaro_v1/_search { "query": { "match": { "content": "桃太郎 きびだんご" } } } 108件に増えました。 これは、今回の環境では “match” による検索を行うと、 “桃”,”太”,”郎”,”き”,”び”,”だ”,”ん”,”ご” に分解されて、どれか 1文字でもマッチしたドキュメントが検索されるためです。 この検索結果の改善は、次回、行っていきたいと思います。 2.3 キーワードを2つ指定した検索(AND検索) では、「桃太郎」と「きびだんご」の両方を含むドキュメントを検索してみます。 Elasticsearch では、”a” を含み、かつ、”b” を含むドキュメントを検索したい場合、 いくつかの書き方があります。 (1) operator に “and” を指定する。 https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-match-query.html#query-dsl-match-query-boolean (2) bool を指定する。 (2.1) bool と filter を指定する。 (2.2) bool と must を指定する。 (2.3) bool と should を指定する。 https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-bool-query.html (2.2) の bool と must を指定する方法の例です。 GET /momotaro_v1/_search { "query": { "bool": { "must": [ { "match_phrase": { "content": "桃太郎" } }, { "match_phrase": { "content": "きびだんご" } } ] } } } 5件になりました。 2.4 キーワードを2つ指定した検索(片方を含み、もう片方を含まない検索) では、「桃太郎」を含むが、「きびだんご」を含まないドキュメントを検索してみます。 “must_not”を使います。 GET /momotaro_v1/_search { "query": { "bool": { "must": [ { "match_phrase": { "content": "桃太郎" } } ], "must_not": [ { "match_phrase": { "content": "きびだんご" } } ] } } } 26件がヒットしました。 “桃太郎”を含むドキュメントが31件、 “桃太郎” と “きびだんご” の両方を含むドキュメントが5件なので、 31 – 5 = 26 と、合っています。 (*脚注2) 2 3. ソート ディフォルトでは、score が大きい順(どれだけ検索条件にマッチしているか?の順)に並び替えされますが、 業務上の要件で、指定した順序で並び替えを行いたい場合もあるかと思います (例: ドキュメントの最終更新日時が新しい順にソートしたい、など)。 そのような場合には、”sort” を指定します。 ここでは、試しに、chunk_no の昇順にソートしてみます。 GET /momotaro_v1/_search { "query": { "match_phrase": { "content": "桃太郎" } }, "sort": [ "chunk_no" ] } これで、chunk_no の昇順に結果を得ることができます。 次の10件(11件目-20件目)を取得するには、”from” に 10 を指定します。 GET /momotaro_v1/_search { "query": { "match_phrase": { "content": "桃太郎" } }, "sort": [ "chunk_no" ], "from": 10 } 上記のリクエストを発行すると、11件目から20件目が表示されます。 ※レスポンスだけを見ると、この結果が11件目から20件目であることはわかりません。 リクエスト時点で何件目から何件目までを取得しようとしているのかをきちんと把握しておく必要があります。 ※”from”を指定することで、ヒットするドキュメントが大量にある場合に、少しずつ結果を取得することが可能となります。 “sort”, “from” の詳細については、それぞれ、下記を参照してください。 https://www.elastic.co/guide/en/elasticsearch/reference/current/sort-search-results.html https://www.elastic.co/guide/en/elasticsearch/reference/current/paginate-search-results.html (*脚注3) 3 4. 取得するフィールドの指定 ディフォルトだと、全てのフィールドを取得します。 仮に、取得する必要がないフィールドがあった場合は、下記のように、 “_source”: false を指定し、かつ、”fields” に取得したいフィールド名を配列で指定します。 (下記の例では、”fields” に1つのフィールドのみ指定していますが、複数フィールドを指定可能です。) 取得するフィールドを限定することで、クエリ実行時に消費するリソースを抑えることができます。 GET /momotaro_v1/_search { "_source": false, "fields": [ "content" ], "query": { "match_phrase" : { "content" : "桃太郎" } } } レスポンスの例: { ... "hits": { ... "hits": [ { "_index": "momotaro_v1", "_id": "********************", "_score": 6.645262, "fields": { "content": [ "「桃太郎さん、桃太郎さん、どちらへおいでになります。」" ] } }, ... ] } } “fileds” の詳細は、下記を参照してください。 https://www.elastic.co/guide/en/elasticsearch/reference/current/search-fields.html 今回、紹介したクエリ以外にもいろんな種類のクエリを利用可能です。 興味がある方は、ぜひ、お試しください。 今回は、検索はできるようになりましたが、日本語の検索でうまくいかない部分がありました。 次回は、日本語用の形態素解析を行った上で、検索を行ってみたいと思います。 (2024-12-19) インデックス名を momotaro → momotaro_v1 に訂正しました。 (*脚注) anlayzerに standard ではなく、日本語用の analyzer を明示することで、 “match”:{“content”:”桃太郎”} でも”桃太郎”にマッチするドキュメントのみを取得することは可能です。 ただし、記事の内容が多くなってしまうので、今回は触れずに、次回のブログ記事で書きたいと思います。 ↩︎ must_not とはやや目的が異なりますが、score を減点する方法もあります。 – 「桃太郎」を含む場合 score を加点する。 – 「きびだんご」を含む場合 score を減点する。 scrore を減点したい場合、”negative” や “negative_boost” を指定します。 そういった細かい調整が、Elasticsearchでは可能です。 https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-boosting-query.html ↩︎ “from” を指定する以外に、”search_after”を指定する方法もあります。 https://www.elastic.co/guide/en/elasticsearch/reference/current/paginate-search-results.html#search-after ↩︎ The post Elasticsearch で検索してみよう first appeared on Elastic Portal .
アバター
こんにちは。 サイオステクノロジーの田川です。 今回はインデックスにドキュメントを登録してみたいと思います。 対象者 Elastic Cloud のアカウントを持っている人(トライアルライセンスを含む) Elasticsearch の初心者 できるようになること Elastic Cloud の Console から、インデックスにドキュメントを登録する。 前提条件 Elastic Cloud (version: 8.15.0) Elastic Cloud 上にインデックスを作成済 (2024年09月25日時点の情報を元に記載しています。) 1. 登録するドキュメントの準備 (Elasticsrearch の “ドキュメント” は、リレーショナルデータベースで言うところの “レコード” のようなものとなります。) 1.1 ドキュメントの登録方法 一般的なドキュメントの登録方法は、大きく分けると2つに分かれます。 1. 1件ずつドキュメントを登録する。 2. Bulk API を利用してドキュメントを複数件まとめて登録する。 (これら以外に、クロールしたWebページ情報を登録する方法などもあります。) ここでは、2番目の Bulk API を用いて複数件のドキュメントをまとめて登録していきます。 ※参考 Bulk API の詳細は下記を参照してください。 Bulk API | Elasticsearch Guide [8.15] | Elastic www.elastic.co なお、Bulk API を呼び出す方法として、 Elastic Cloud の Console から実行する方法 Pythonなどのプログラミングコードから呼び出す方法 がありますが、ここでは、Elastic Cloud の Console から実行する方法について記載しています。 1.2 登録するドキュメント このブログ記事内で登録するドキュメントは、「桃太郎」の本文としています。 桃太郎 (楠山 正雄) 一 むかし、むかし、あるところに、おじいさんとおばあさんがありました。まいにち、おじいさんは山へしば刈(か)りに、おばあさんは川へ洗濯(せんたく)に行きました。 ある日、おばあさんが、川のそばで、せっ… www.aozora.gr.jp 上記の青空文庫の URL から、zip ファイルをダウンロード後、ルビを除去したものを利用しています。 ルビを除去した本文を読み込んで、Bulk API用の入力データに変換しています。 (ファイルの加工方法については、Elasticsearch の話から脱線してしまうので、ここでは詳細は割愛しますが、 下記のような簡単な Python スクリプトで変換しています。) text2bulk_input.py ----------------------------------------------------- textfile: str = "./momotaro_no_ruby_utf8.txt" chunk_no: int = 0 with open(textfile, 'r', encoding="utf-8") as file: while True: one_line :str = file.readline() if one_line == '': break chunk_no += 1 # remove last crlf content = one_line.replace("\n", "") print('{"index": {}}') print(f'{{"chunk_no": {chunk_no}, "content": "{content}"}}') (*脚注1) 1 前述の Python スクリプトを実行すると、次のような Bulk API用の入力データが出力されます。 {"index": {}} {"chunk_no": 1, "content": " むかし、むかし、あるところに、おじいさんとおばあさんがありました。まいにち、おじいさんは..."} {"index": {}} {"chunk_no": 2, "content": " ある日、おばあさんが、川のそばで、せっせと洗濯をしていますと、川上から、大きな桃が一つ、"} ... {"index": {}} {"chunk_no": 148, "content": " きじはケン、ケンと鳴きながら、くるくると宙返りをしました。"} {"index": {}} {"chunk_no": 149, "content": " 空は青々と晴れ上がって、お庭には桜の花が咲き乱れていました。"} (*脚注2) 2 2. ドキュメントの登録(1ドキュメントのみ) 前述で生成された Bulk API用の入力データを Elastic Cloud の Console に貼り付けて実行してみます。 (Elastic Cloud の Console への画面遷移については、前回のブログ記事を参照してください。) Elasticsearch へのインデックスの作成 今回は Elasticsearch にインデックスを作成してみたいと思います。 対象者:Elastic Cloud のアカウントを持っている人(トライアルライセンスを含む)・Elasticsearch の初心者。 できるようになること:Elastic Cloud のデプロイメント内にインデックスを作成する。 できるようになること:Elastic Cloud のデプロイメント内にインデックスを作成する。 elastic.sios.jp 2024.09.19 ドキュメントの登録先は、前回作成した momotaro_v1 インデックスです。 いきなり全部を貼り付けることはせずに、まずは、最初の2行(1ドキュメント分)のみ貼り付けて実行してみます。 POST /momotaro_v1/_bulk?refresh=true {"index": {}} {"chunk_no": 1, "content": " むかし、むかし、あるところに、おじいさんとおばあさんがありました。まいにち、おじいさんは..."} (*脚注3) 3 ドキュメントの登録に成功したら、次のような結果がレスポンスに表示されます。 { "errors": false, "took": 26734000762, "items": [ { "index": { "_index": "momotaro_v1", "_id": "*****************", "_version": 1, "result": "created", "_shards": { "total": 2, "successful": 1, "failed": 0 }, "_seq_no": 0, "_primary_term": 1, "status": 201 } } ] } 上記の “_id” は、リレーショナルデータベースで言うところの主キーに該当します。 さきほどの POST メソッドにより、Elasticsearch 側で自動採番されます。 3. 登録されたドキュメントの確認 Elastic Cloud の Console から次のようなリクエストを発行してみます。 GET /momotaro_v1/_search すると、次のような結果が返ってきます。 { "took": 0, "timed_out": false, "_shards": { "total": 1, "successful": 1, "skipped": 0, "failed": 0 }, "hits": { "total": { "value": 1, "relation": "eq" }, "max_score": 1, "hits": [ { "_index": "momotaro_v1", "_id": ""*****************", "_score": 1, "_source": { "chunk_no": 1, "content": " むかし、むかし、あるところに、おじいさんとおばあさんが...(以降省略)..." } } ] } } ※ _search の response の詳細については、下記を参照してください。 Search API | Elasticsearch Guide [8.15] | Elastic www.elastic.co 1件のドキュメントが登録され、検索できるようになっていることが確認できました。 インデックスの Overview をクリックしてみます。 (画面上は momotaro となっていますが、momotaro_v1 が正しいです。) Document count が 1 になっていることがわかります。 インデックスの Documents をクリックしてみます。 さきほど登録したドキュメントが表示されています。 4. 残りのドキュメントの登録 chunk_no : 2 ~ 149 の本文を Console から Bulk API を使って登録します。 POST /momotaro_v1/_bulk?refresh=true {"index": {}} {"chunk_no": 2, "content": " ある日、おばあさんが、川のそばで、せっせと洗濯をしていますと、川上から、大きな桃が一つ、"} {"index": {}} {"chunk_no": 3, "content": "「ドンブラコッコ、スッコッコ。"} ... {"index": {}} {"chunk_no": 148, "content": " きじはケン、ケンと鳴きながら、くるくると宙返りをしました。"} {"index": {}} {"chunk_no": 149, "content": " 空は青々と晴れ上がって、お庭には桜の花が咲き乱れていました。"} momotaro_v1 インデックスのOverviewを確認すると、 Document count = 149 となっていることがわかります。 今回は149件という比較的少ないデータ量だったので、Console への貼り付けでドキュメントの登録を行いましたが、もっと大量にある場合や、外部のファイルなどからプログラムでテキストを読み込んで登録したい場合などは、Pythonなどのプログラムからドキュメントを登録する方法が向いています。 (Pythonからの呼び出しについては、後日、説明予定です。) 次回は、登録したドキュメントを検索してみたいと思います。 (2024-12-19) インデックス名を momotaro → momotaro_v1 に訂正しました。 (*脚注) Elasticsearchの1つのドキュメントに、どのような単位で登録するか?というのは、検索精度における課題の一つとなります。 検索システムで扱う本番用データであれば、どのように分割するか?は、慎重に検討すべき問題です。 例) ・一定の文字数で区切って登録する。 ・一文ずつ登録する。 ・”。”で区切って登録する。 ・意味的な塊ごとに登録する。 また、前後のドキュメント(いわゆるチャンク)との重複をどの程度にするか?も調整が難しいポイントになります。 今回は、検索できればいい、というシンプルなサンプルデータですので、元のデータで「一文」に区切られている部分を、そのまま「1つのドキュメント」として登録しています。 ↩︎ ここでの Bulk API用の入力データでは、”_id”を指定していません。 Elasticsearch への登録時に自動的に”_id”が割り振られます。 Bulk API を利用してドキュメントを登録したり、あるいは、1件ずつドキュメントを登録する場合に、 “_id” を指定して登録することも可能と言えば可能ですが、推奨いたしません。 “_id” を指定して新規登録する場合、Elasticsearch 側で “_id” が重複していないか?といったチェックが 働くことになり、ドキュメントの登録にかかる時間が長くなります。 どうしても “_id” を指定しなければならない特別な場合を除いては、”_id” は指定しないことをお勧めします。 ↩︎ インデックスのリフレッシュ間隔を短い時間にしている場合は、refresh=true を指定しなくても検索できるようになりますが、前回のブログ記事内で refresh_interval = 3600s としているため、refresh を明示的に行う必要があります。 ↩︎ The post Elasticsearch へのドキュメントの登録 first appeared on Elastic Portal .
アバター
こんにちは。 サイオステクノロジーの田川です。 今回は Elasticsearch にインデックスを作成してみたいと思います。 対象者 Elastic Cloud のアカウントを持っている人(トライアルライセンスを含む) Elasticsearch の初心者 できるようになること Elastic Cloud のデプロイメント内にインデックスを作成する。 (インデックスは作成されますが、ドキュメントは、まだ登録しません。) 前提条件 Elastic Cloud (version: 8.15.0) Elastic Cloud 上にデプロイメントを作成済 (2024年09月17日時点の情報を元に記載しています。) 1. Elasticsearch のデータ構造 Elasticsearch のデータ構造をかなり大雑把に書くと、次のような構造となります。 クラスター → インデックス → シャード → ドキュメント → フィールド ※参考 Elasticsearch での用語とリレーショナルデータベースでの用語の対応表は、おおよそ下記のようになります。 Elasticsearch での用語 リレーショナルデータベースでの用語 デプロイメント データベース クラスター (なし) インデックス テーブル シャード (なし) ドキュメント レコード フィールド カラム ※注 Elasticsearhの仕組みとリレーショナルデータベースの仕組みは異なるため、    完全に上の表に一致するわけではありません。あくまでも、「似たようなもの」です。 前回のデプロイメントの作成 で、クラスターまでは設定出来ています。 今回は、インデックス ~ フィールド を作ります。 2. インデックスの作成準備 インデックスを作成する際、シャード数やレプリケーション数を決定しておく必要があります。 レプリケーション数は後から変更することも可能ですが、シャード数はインデックス作成時に決定したものとなります。 (シャード数を変更する方法は皆無ではありませんが、レプリカほど簡単には変更できません。) ついでに、リフレッシュ間隔も設定しておきます。 この記事ではサンプルデータとして、「桃太郎」を使用します。 今回は、更新がほとんどない少量のサンプルデータなので、下表のようにします。 項目 設定値 ディフォルト値 number_of_shards 1 1 number_of_replicas 1 1 refresh_interval 3600s 1s ※端的には、大量のドキュメントがある場合には、shard の数を増やします。  また、耐障害性や高速読み取り性能を向上させたい場合には、replica の数を増やします。 ※参考 下記の Elastic 社の公式ページによると、1シャードあたりのサイズを10~50GB程度になるようシャード数を設定した方がいいようです。詳細は、下記のページを参照してください。 Size your shards | Elasticsearch Guide [8.15] | Elastic www.elastic.co 3. Dev Tools 先ほどの設定値を踏まえ、実際に、インデックスを作成します。 そのために、Dev Tools 画面を開きます。 Elastic Cloud へログイン後、前回作成したデプロイメントの Open をクリックします。 そうすると、「Welcome home」と表示された画面へ遷移します。 「Welcome home」画面の右下にある Dev Tools をクリックします。 クリックすると、次の画面(Dev Tools 画面)が表示されます。 (Dev Tools画面への遷移方法は他にもあります。) 初回は、右側の説明画面が表示されますが、[Dismiss] をクリックすることで、 この部分を閉じることができます。 左側のリクエスト欄に GET, POST, PUT, DELETE の Web リクエストを記述することができます。 Web リクエストを記述した後 ▷ をクリックすると、そのリクエストが実行され、結果が右側のレスポンス欄に表示されます。 試しに、ライセンスを確認してみます。 左側の入力欄に GET _license と入力し、その行にカーソルを置いた状態で、 ▷ をクリックします。 右側のレスポンス欄に、次のような結果が返却されます。(uidは、伏字にしています。) { "license": { "status": "active", "uid": "***********************************", "type": "enterprise", "issue_date": "2023-03-02T00:00:00.000Z", "issue_date_in_millis": 1677715200000, "expiry_date": "2028-02-29T23:59:59.999Z", "expiry_date_in_millis": 1835481599999, "max_nodes": null, "max_resource_units": 100000, "issued_to": "Elastic Cloud", "issuer": "API", "start_date_in_millis": 1677628800000 } } なお、左側のリクエスト欄に書いたリクエストは、消さない限り、これまでに発行したリクエストが残っています。 頻繁に呼びたいリクエストは、ここに書いておけば、 ▷ をクリックするとすぐにリクエストを再発行できます。 4. インデックスの作成 Dev Tools を使って、インデックスの作成リクエストを発行します。 Dev Tools の左側にリクエスト欄に、次のように入力し、 ▷ をクリックします。 (この後、「桃太郎」の内容を登録する予定なので、作成するインデックス名は、”momotaro_v1″ とします。) PUT /momotaro_v1 { "settings": { "index": { "number_of_shards": 1, "number_of_replicas": 1, "refresh_interval": "3600s" } } } 成功すれば右側のレスポンス欄に、次のような結果が表示されます。 { "acknowledged": true, "shards_acknowledged": true, "index": "momotaro_v1" } インデックスが作成されたか、確認してみます。 Dev Tools から下記のリクエストを発行してみます。 GET /momotaro_v1/_settings 成功すると、右側のレスポンス欄に、以下のように出力されます。 { "momotaro_v1": { "settings": { "index": { "routing": { "allocation": { "include": { "_tier_preference": "data_content" } } }, "refresh_interval": "3600s", "number_of_shards": "1", "provided_name": "momotaro_v1", "creation_date": "1726546814052", "number_of_replicas": "1", "uuid": "*****************", "version": { "created": "8512000" } } } } } Elastic Cloud の画面からも確認してみます。 左上の3点メニューをクリックし、プルダウンメニューを表示します。 そこから、Search の Content を選択します。 すると、インデックス一覧画面へ遷移します。 (画面上は momotaro となっていますが、momotaro_v1 が正しいです。) 先ほど作成した momotaro_v1 が表示されています。 ただし、データはまだ登録していないので、Docs count = 0 となっています。 5. フィールドマッピングの設定 インデックスにデータを格納する前に、フィールドマッピングを設定しておきます。 今回は単純なサンプルデータなので、フィールドは下表のようにします。 フィールド名 フィールドタイプ 説明 chunk_no integer チャンク番号 content text 本文 フィールドタイプは、リレーショナルデータベースでのカラムタイプに相当します。 フィールドタイプの詳細な説明については、下記を参照してください。 Field data types | Elasticsearch Guide [8.15] | Elastic www.elastic.co 補足1 文字列型の代表的なフィールドタイプには、text と keyword がありますが、用途が異なります。 今回は、小説内の文章を全文検索したいので、text を利用します。 フィールドタイプ 説明 keyword 各種コードなど、厳密に一致する文字列を検索するのに使用する。 ソートや集計も可能。 text 文章など、あいまい検索に利用する。 補足2 ちょっとだけデータを登録して、軽く確認したい、という程度であれば、 フィールドマッピングを省略してデータを登録しても、Elasticsearch がある程度、自動で設定してくれます。また、Elastic Cloud の管理画面上のGUIを使っても、ある程度、設定可能です。 ただし、細かい設定を行いたい場合は、あらかじめフィールドマッピングを API を使って作成しておいた方がいいと思います。 補足3 Elasticsearch では、フィールドの追加は、比較的容易にできます。 しかし、フィールドの削除は、容易にはできません。 フィールドタイプの変更も、制約があります。 上記を踏まえて、フィールドマッピング用のリクエストを Dev Tools に入力します。 インデックスの一覧画面から Dev Tools の画面を開くには、画面下部の Console をクリックします。 左側のリクエスト欄に次のリクエストを入力し、 ▷ をクリックします。 PUT /momotaro_v1/_mapping { "dynamic": false, "properties": { "chunk_no": { "type": "integer" }, "content": { "type": "text" } } } 成功したら、右側のレスポンス欄に、下記が表示されます。 { "acknowledged": true } 6.フィールドマッピングの確認 実際にフィールドが作成されたか確認してみます。 まずは、Dev Tools から、下記のリクエストを送信してみます。 GET /momotaro_v1/_mapping 成功すると、右側のレスポンス欄に、以下のように出力されます。 { "momotaro_v1": { "mappings": { "dynamic": "false", "properties": { "chunk_no": { "type": "integer" }, "content": { "type": "text" } } } } } Elastic Cloud の画面からも確認してみます。 インデックス一覧画面から “momotaro_v1” をクリックします。 すると、momotaro_v1 インデックスの画面が表示されます。 この画面の “Index mappings” をクリックします。 momotaro_v1 インデックスのマッピング情報が表示されます。 chunk_no が Integer 型で、content が Text 型で作成されていることがわかります。 なお、この画面にある [+ Add field] ボタンから、新たにフィールドを追加することも可能です。 ただし、細かい設定を行いたい場合には、API を使って追加することをお薦めします。 次回は、作成したmomotaro_v1インデックスへのドキュメントを登録します。 (2024-12-19) 作成するインデックス名を momotaro → momotaro_v1 に訂正しました。 The post Elasticsearch へのインデックスの作成 first appeared on Elastic Portal .
アバター
サイオステクノロジーの田川と申します。 ここでは、Elastic に関する技術的な内容を記載していきたいと思います。 まずは、Elastic Cloud 上でのデプロイメントの作成方法について説明していきます。 Elastic Cloud 上にデータを格納したい場合、その準備としてデプロイメントを作成しておく必要があります。 対象者 Elastic Cloud のアカウントを持っている人(トライアルライセンスを含む) Elastic Cloud の初心者 本記事で、できるようになること Elastic Cloud 上にデプロイメントが作成されます。 (デプロイメントが作成されますが、ドキュメント格納用のインデックスは、本記事では作成していません。) 前提条件 Elastic Cloud version: 8.15.0 (この記事内では、Enterprise ライセンスでの画面を記載しています。  他のライセンスでは、動作しない箇所があるかもしれません。) (2024年09月10日時点の情報を元に記載しています。) デプロイメントの作成手順 1. ログイン画面 URL = https://cloud.elastic.co/login へアクセスすると、ログイン画面が表示されます。 Email 欄に ログインするユーザーの E-mail アドレスを、 Password 欄に、そのユーザーのパスワードを入力して、 [Log in] をクリックします。 2. ホーム画面 ログインに成功すると、ホーム画面へ遷移します。 URL = https://cloud.elastic.co/home ※こちらの画面では、既にいくつかのデプロイメントが作成済ですが、初期状態では空です。 3. デプロイメント作成画面 さきほどのホーム画面の上部中央にある [Create Deployment] ボタンをクリックします。 すると、デプロイメントの新規作成用画面である、 Create a deployment 画面へ進みます。 Name欄に、デプロイメント名を入力し、右下の [Create deployment] ボタンをクリックすると、デプロイメントが作成されますが、Cloud Provider や Region などを変更すると左下に表示されている料金が変化します。 さらに、Version の下にある > Advanced settings をクリックすると、いろんな設定を変更することが可能です。 これらの設定を変更すると、料金も変化します。 必要に応じて、設定を変えます。 この記事では、後で機械学習まで行う予定ですので、機械学習を行える設定にしておきます。 また、ここで扱っているデータは重要なデータではなく、あくまでもサンプルデータであり、特別、高速なレスポンスも必要としていないため、耐障害性やパフォーマンスについては最低限の値とします。 実際には、本番のサービスで必要とされている機能、耐障害性、パフォーマンス要件に合わせて、設定してください。 (これらの設定値は、デプロイメント作成後に、後述のデプロイメントの編集画面から変更することも可能です。) (この記事では、Deployment name は “SIOS-BLOG-SAMPLE-1” としておきます。 このDeployment nameは、半角英数字とハイフンのようなシンプルな名前にしておくのが賢明だと思われます。) 4. デプロイメント作成中~作成完了画面 さきほどのCreate deployment画面で設定を終了したら、右下の [Create deployment] ボタンをクリックします。 すると、次の画面が表示されます。 5分ほど待つと、次の画面になります。 (“Your deployment is ready!” が表示されます。) この画面に表示されている Username, Password は、アプリ接続時などで必要となるので忘れないようにしてください。 万が一、忘れてしまった場合は、パスワードのリセットを行ってください。 [Download] ボタンをクリックすると、CSV形式で、Username, Password がダウンロードされます。 5. デプロイメントの初期画面 さきほどの画面の右上の [Continue] ボタンをクリックすると、作成したデプロイメントの初期画面に遷移します。 URL = https://***/app/home#/getting_started (*** には、「実際にデプロイされた地域のホスト名のFQDN:ポート番号」がはいります。) 6. 作成されたデプロイメントの確認 先ほど作成したデプロイメントが正しく作成されたかどうかを確認してみます。 一度、ログアウトして、再ログインしてみます。 すると home 画面に、先ほど作成したデプロイメントが表示されていることを確認できます。 この画面の右下の Manage をクリックすると、選択したデプロイメントの管理画面へ遷移します。 7. デプロイメントの管理画面 (デプロイメント名を変更することは滅多にないと思われますが、画面中央の下部にある Deployment name の Edit から Deployment name を変更することも可能です。) デプロイメントの管理画面の左のメニューから Edit をクリックします。 8. デプロイメントの編集画面 デプロイメントの現在の設定値を知ることができます。 設定値を変更して、下にある [Save] ボタンをクリックすると変更することもできます。 [Save] ボタンは、かなり下に表示されるので、下スクロールする必要があります。 また、[Save] をクリックして設定値を変更する場合、変更内容を反映させるために時間がかかるので、注意が必要です。 参考にしたWebページ Elastic Cloud について 〜実際にデプロイメントを作ってみよう〜 - Qiita (情報は投稿時点(2022年3月頃)の話です)Elastic Cloud について 〜実際にデプロイメントを作ってみよう〜Elastic Cloud は一言でいうと、Elastic が提供する… qiita.com The post Elastic Cloud 上でのデプロイメントの作成 first appeared on Elastic Portal .
アバター