ml-commons

Activity, over time

Daily observations from the public event archive.

Attention

Daily stars & forks

Loading attention chart

Star accumulation

Cumulative stars

Loading star accumulation chart

Pull requests

Daily opens & merges

Loading pull requests chart

Conversations

Daily issues & comments

Loading conversations chart

Community

Daily accounts & reviews

Loading community chart

Releases

Daily releases

Loading releases chart

Mentions

1post · 30d
Nov 40 posts
  1. Zenn · · @waddledee

    OpenSearchのLLM-as-a-Judgeを日本語の商品検索で動かして、人手ラベルと比べてみた はじめに Search Relevance Workbench(SRW)は、OpenSearch 3.1で導入された検索品質を評価するための機能です。クエリと検索設定、正解データを組み合わせてNDCGなどの指標を計算できます。正解データは自分で用意するのが一般的ですが、SRWにはLLMに検索結果の関連度を採点させて正解データを作る、LLM-as-a-Judgeという機能も用意されています。 英語の紹介記事はありましたが、日本語のデータで試した例は見当たらなかったので、今回はAmazonの商品検索データセット(ESCI)の日本語データを使って実際に動かしてみました。ESCIには人が付けた関連度ラベルがあるので、いくつかの例でLLMの採点と見比べてみます。 使ったものは以下のとおりです。 OpenSearch / OpenSearch Dashboards 3.9.0(Docker) analysis-kuromoji 3.9.0 Gemini 3.5 Flash-Lite(Gemini API) Shopping Queries Dataset(ESCI)の日本語データ Python 3.12(uv) SRWとESCIについて SRWは、以下の4つの部品を組み合わせて検索品質を評価します。 Query set:評価に使うクエリ Search configuration:検索の設定(どのインデックスを、どんなクエリで検索するか) Judgment list:正解データ(クエリ・文書・評価値の組) Experiment:検索設定の結果を正解データで採点して、NDCGなどを計算する 正解データは、外部で作った評価値を取り込む(IMPORT_JUDGMENT)こともできますが、今回はLLMに検索結果を採点させるLLM_JUDGMENTを試します。採点は0〜1の段階評価(SCORE0_1)か、関連あり/なしの二択(RELEVANT_IRRELEVANT)を選べます。 ESCIはAmazonが公開している商品検索のデータセットで、クエリと候補商品の組に、人が以下の4段階のラベルを付けています。 E(Exact):条件をすべて満たす S(Substitute):一部違うが代わりに使える C(Complement):一緒に使うもの I(Irrelevant):無関係 環境構築 Dockerを使ってOpenSearchとDashboardsを起動します。Dockerfileでは、OpenSearch公式のDockerイメージをベースに、日本語の形態素解析を行うkuromojiプラグインをインストールします。 docker/opensearch/Dockerfile ARG OPENSEARCH_VERSION=3.9.0 FROM opensearchproject/opensearch:${OPENSEARCH_VERSION} RUN /usr/share/opensearch/bin/opensearch-plugin install --batch analysis-kuromoji 3.9.0のSRWプラグインにはslf4j-apiが入っておらず、LLM_JUDGMENTでLLMの応答を受け取ると NoClassDefFoundError: org/slf4j/LoggerFactoryでOpenSearchが落ちる。ML Commonsのものをコピーして補う RUN cp /usr/share/opensearch/plugins/opensearch-ml/slf4j-api-1.7.36.jar \ /usr/share/opensearch/plugins/opensearch-search-relevance/ 1ノード構成でコンテナを定義します。今回は動作確認が目的なので、セキュリティ設定を無効化しています。 docker/docker-compose.yml name: srw-esci-ja services: opensearch: build: context: ./opensearch args: OPENSEARCH_VERSION: 3.9.0 image: srw-esci-ja/opensearch-kuromoji:3.9.0 container_name: srw-opensearch cpus: 4 # LLM_JUDGMENTが同時にLLMを呼ぶ数はCPUの数で決まるので、APIの回数制限に当たらないよう絞る environment: - discovery.type=single-node - bootstrap.memory_lock=true - OPENSEARCH_JAVA_OPTS=-Xms2g -Xmx2g - DISABLE_INSTALL_DEMO_CONFIG=true # 動作確認用なのでdisableにする - DISABLE_SECURITY_PLUGIN=true # 動作確認用なのでdisableにする ulimits: memlock: soft: -1 hard: -1 nofile: soft: 65536 hard: 65536 volumes: - opensearch-data:/usr/share/opensearch/data ports: - 9200:9200 dashboards: image: opensearchproject/opensearch-dashboards:3.9.0 container_name: srw-dashboards environment: - OPENSEARCH_HOSTS=["opensearch"] - DISABLE_SECURITY_DASHBOARDS_PLUGIN=true # 動作確認用なのでdisableにする ports: - 5601:5601 depends_on: - opensearch volumes: opensearch-data: コンテナを起動します。 $ docker compose -f docker/docker-compose.yml up -d --build 商品データを入れる ESCIのデータは、amazon-science/esci-dataのリポジトリからダウンロードします。parquetファイルはGit LFSで管理されているので、github.com/.../raw/...のURLを指定します。 $ curl -L -O github.com/amazon-science/esci-data/raw/refs/head… $ curl -L -O github.com/amazon-science/esci-data/raw/refs/head… ESCIのラベルは、クエリごとの候補商品にしか付いていません。今回は日本語データから一部のクエリを選び、その候補商品だけをインデックスに入れます。 ここからのOpenSearchへのリクエストは、DashboardsのDev Tools(localhost)から実行します。 まず、以下のマッピングでproducts_kuromojiインデックスを作成します。テキストのフィールドにはkuromojiを使い、どのクエリの候補かを表すcandidate_queriesは、完全一致で検索するためにkeyword型にしています。 PUT products_kuromoji { "mappings": { "properties": { "product_id": { "type": "keyword" }, "product_title": { "type": "text", "analyzer": "kuromoji" }, "product_brand": { "type": "text", "analyzer": "kuromoji" }, "product_bullet_point": { "type": "text", "analyzer": "kuromoji" }, "product_description": { "type": "text", "analyzer": "kuromoji" }, "candidate_queries": { "type": "keyword" } // どのクエリの候補か } } } 作成したインデックスに、候補商品をPythonで取り出して投入しました。 LLMをつなぐ SRWは、ML Commonsのコネクタを経由して外部のLLMを呼び出します。ml-commonsのリポジトリにSRW用のGeminiコネクタの例があるので、それを参考にしました。今回はLLMにGemini 3.5 Flash-Liteを使います。APIキーはGoogle AI Studioで発行したものを使います。 まず、コネクタの接続先を許可します。 PUT _cluster/settings { "persistent": { "plugins.ml_commons.trusted_connector_endpoints_regex": [ "^generativelanguage\.googleapis\.com/.*$" ], "plugins.ml_commons.only_run_on_ml_node": false } } 次に、コネクタを登録します。例から変えたのは、タイムアウトと再試行の設定(client_config)と、LLMの応答を読み取る処理(post_process_function)の2か所です。デフォルトのタイムアウト(30秒)と再試行(0回)では足りなかったので延ばしています。また、Gemini 3.5 Flash-Liteでは、採点結果のJSONがエスケープされた形({\"ratings\": ...)で返ってくることがあり、そのままだとSRWが読めずに未採点になってしまうので、replaceで戻してから渡しています。 POST _plugins/_ml/connectors/_create { "name": "Gemini gemini-3.5-flash-lite", "version": "1", "protocol": "http", "parameters": { "model": "gemini-3.5-flash-lite" }, "credential": { "gemini_key": "<API キー>" }, "client_config": { "read_timeout": 180, "max_retry_times": 5, "retry_backoff_policy": "exponential_full_jitter", "retry_backoff_millis": 5000, "retry_timeout_seconds": 300, "connection_timeout": 30 }, "actions": [ { "action_type": "predict", "method": "POST", "url": "generativelanguage.googleapis.com", "headers": { "Content-Type": "application/json", "x-goog-api-key": "${credential.gemini_key}" }, "request_body": "{\"systemInstruction\":{\"parts\":[{\"text\":\"${parameters.system_prompt}\"}]},\"contents\":[{\"role\":\"user\",\"parts\":[{\"text\":\"${parameters.user_prompt}\"}]}]}", "post_process_function": "def text = params.candidates[0].content.parts[0].text.replace('\\\"', '\"'); return '{\"name\":\"response\",\"dataAsMap\":{\"response\":\"' + escape(text) + '\"}}'" } ] } 最後に、モデルとして登録してデプロイします。 POST _plugins/_ml/model_groups/_register { "name": "srw_llm_judgment" } POST _plugins/_ml/models/_register?deploy=true { "name": "Gemini gemini-3.5-flash-lite", "function_name": "remote", "model_group_id": "<model_group_id>", "connector_id": "<connector_id>" } LLMに採点させてみる 商品データを入れたproducts_kuromojiインデックスで、「電源なしスピーカー」の検索結果の上位5件をLLMに採点させてみます。 まず、クエリセットと検索設定を登録します。検索設定の%SearchText%には、クエリセットのクエリが入ります。 PUT _plugins/_search_relevance/query_sets { "name": "handson", "sampling": "manual", "querySetQueries": [{ "queryText": "電源なしスピーカー" }] } PUT _plugins/_search_relevance/search_configurations { "name": "bm25-kuromoji", "index": "products_kuromoji", "query": "{\"query\":{\"multi_match\":{\"query\":\"%SearchText%\",\"fields\":[\"product_title^2\",\"product_brand\",\"product_bullet_point\",\"product_description\"]}}}" } LLM_JUDGMENTで採点させます。 PUT _plugins/_search_relevance/judgments { "name": "handson-score", "type": "LLM_JUDGMENT", "modelId": "<model_id>", "querySetId": "<query_set_id>", "searchConfigurationList": ["<search_configuration_id>"], "size": 5, // 検索結果の上位何件を採点するか "contextFields": ["product_title", "product_brand", "product_bullet_point"], // LLMに渡すフィールド "llmJudgmentRatingType": "SCORE0_1" // 0〜1の段階評価 } 採点は非同期で進むので、少し待ってから結果を取得します。 GET _plugins/_search_relevance/judgments/<judgment_id> statusがCOMPLETEDになっていて、商品ごとに点数が付いていれば成功です。一部の商品が採点できなかった場合もCOMPLETEDになるので、failuresに商品が入っていないかも確認します。入っていた場合は、POST _plugins/_search_relevance/judgments/<judgment_id>/_retryで再実行できます。 結果を商品名と並べると、以下のようになりました。 順位 商品(タイトルの要約) LLM 1 木製無電源スピーカー 1.0 2 PC用スピーカーUSB電源 0.2 3 ポータブルスピーカー(電源不要) 0.8 4 ミニスピーカーUSB電源 0.3 5 木製スピーカー(電源不要) 1.0 電源を使わないスピーカーは0.8〜1.0と高く、USB給電のスピーカーは0.2〜0.3と低くなっていて、「電源なし」を文字どおりに解釈して採点しているようです。 人手ラベルと比べてみる ESCIには人手で付けた関連度ラベルがあるので、LLMの採点と比べてみます。いくつか試してみた中で、うまくいった例と、人手ラベルとずれた例を見てみます。 LLM_JUDGMENTは、検索結果の上位の商品だけを採点します。今回は人手ラベルと同じ商品で比べたいので、検索の代わりに、そのクエリの候補商品(人手ラベルが付いている商品)をすべて返す検索設定を、先ほどのマッピングで用意したcandidate_queriesを使って作りました。 PUT _plugins/_search_relevance/search_configurations { "name": "esci-candidates", "index": "products_kuromoji", "query": "{\"query\":{\"term\":{\"candidate_queries\":\"%SearchText%\"}}}" } この検索設定を使って、LLM_JUDGMENTを実行します。 PUT _plugins/_search_relevance/judgments { "name": "full-score01", "type": "LLM_JUDGMENT", "modelId": "<model_id>", "querySetId": "<query_set_id>", "searchConfigurationList": ["<esci-candidates の ID>"], "size": 40, // 候補の最大件数 "tokenLimit": 50000, // 1リクエストのトークン上限で、分割を減らすため大きめに設定 "contextFields": ["product_title", "product_brand", "product_bullet_point"], "llmJudgmentRatingType": "SCORE0_1" } 取得した点数を、ESCIの人手ラベルと並べてみます。 人手ラベルと傾向が近かった例:液体ミルク 人手ラベルがEの液体ミルクは、LLMもすべて1.0にしていました。Sの粉ミルクやキューブタイプのミルクは0.3〜0.4で、人手ラベルと整合的な傾向が見られました。 商品(タイトルの要約) LLM 人手ラベル 明治 ほほえみ らくらくミルク240ml(液体ミルク) 1.0 E アイクレオ 赤ちゃんミルク125ml(液体ミルク) 1.0 E 明治 ほほえみ らくらくキューブ 0.4 S 和光堂 レーベンスミルク はいはい(粉ミルク) 0.3 S 人手ラベルとずれた例:電源なしスピーカー 先ほど試したクエリです。人手ラベルは多くの商品がEですが、LLMは電源をまったく使わないスピーカーだけを高く採点していました。 商品(タイトルの要約) LLM 人手ラベル 木製無電源スピーカー 1.0 E ミニスピーカーUSB電源 0.3 E Bluetoothスピーカー(充電式) 0.2 E スピーカーケーブル 0.1 E 人手ラベルでは、USB給電や充電式のスピーカーもEになっています。たとえば「ミニスピーカーUSB電源」は人手ラベルがEですが、LLMは0.3でした。USBから給電する点を、LLMは「電源あり」と判断したと考えられます。同じように「Bluetoothスピーカー(充電式)」もEですが、LLMは0.2で、充電が必要なことから低く採点したようです。「電源なし=コンセントが不要」と読めば、これらはEとも言えるので、ここはLLMと人手の解釈の違いのようです。一方で、「スピーカーケーブル」は人手ラベルがE、LLMは0.1でした。スピーカー本体ではないため、LLMの採点のほうが自然に見えます。ケーブルがEになっている点は解釈の違いでは説明しにくく、人手ラベル自体にも揺れがあるようです。 終わりに 今回は、OpenSearchのSRWのLLM-as-a-Judgeを日本語の商品データで試してみました。Gemini 3.5 Flash-Liteでの採点は、人手ラベルと傾向が近いクエリもあれば、大きくずれるクエリもあり、結果はモデルによっても変わりそうです。今後としては、上位のモデルやプロンプトを変えて、より多くのクエリで比較してみたいです。 参考 amazon-science/esci-data(ライセンス:Apache-2.0) Reddy et al., Shopping Queries Dataset: A Large-Scale ESCI Benchmark for Improving Product Search (2022) OpenSearch Docs: Search Relevance Workbench OpenSearch Docs: Judgments ml-commons: Google Gemini connector blueprint for LLM judgment

Latest stargazers

The last 10 distinct accounts we observed this year.

Most active participants

PR, issue, comment & review events in the past year.

Packages

2
PackageVersionDownloads · 30dTrend · 30d
———
———

README badges

Embed a badge in your README or website.

Live mentions · last 30 days
opensearch-project/ml-commons mentions in the last 30 days