[アップデート] Amazon OpenSearch Serverless NextGen × Amazon Bedrock (Claude) で自然言語を検索 DSL に変換する Agentic Search を試してみた

[アップデート] Amazon OpenSearch Serverless NextGen × Amazon Bedrock (Claude) で自然言語を検索 DSL に変換する Agentic Search を試してみた

Amazon OpenSearch Serverless で自然言語検索(Agentic Search)が利用できるようになったので、東京リージョンで実際に構築して、Claude Sonnet 4.6を使った自然言語から検索DSLへの変換を試してみました。
2026.06.21

クラウド事業本部の石川です。Amazon OpenSearch Serverless で自然言語による検索(Agentic Search)が利用できるようになりましたので、実際に東京リージョンで構築して試してみました。

https://aws.amazon.com/jp/about-aws/whats-new/2026/06/opensearch-agentic-search/

ユーザーは「何を探しているか」を日本語や英語の文章で記述するだけで、システムが意図を解釈し、最適な検索戦略を計画して OpenSearch の DSL(ドメイン固有言語)クエリを生成し、推論内容の説明とともに結果を返します。

背後では、大規模言語モデル(LLM)を搭載した組み込みの QueryPlanningTool が自然言語を DSL クエリへ変換し、適切なツールをオーケストレーションして結果を取得します。設定は API または OpenSearch Dashboards から行えます。Agentic Search は、OpenSearch Serverless が提供されているすべての AWS 商用リージョンで利用できます。

なお、Agentic Search 自体は 2025 年 11 月にマネージド型の Amazon OpenSearch Service(バージョン 3.3 以降)向けに先行して導入されており、今回のアップデートでサーバーレスのコレクションでも利用できるようになりました。

今回は、次世代(NextGen)の OpenSearch Serverless コレクションと Amazon Bedrock の Claude Sonnet 4.6 を組み合わせ、商品データに対する自然言語検索を AWS CLI と OpenSearch REST API で構築してみます。

https://dev.classmethod.jp/articles/20260531-amazon-opensearch-service-nxgn-ga

Agentic Search とは

Agentic Search は、自律的なエージェントがユーザーの意図を理解し、適切なツールを選択して最適なクエリを生成・実行する仕組みです。中核となるのが QueryPlanningTool で、LLM を使って自然言語のリクエストを OpenSearch の DSL クエリに変換します。

エージェントには 2 種類あります。複数ターンの対話を扱う conversational 型と、単一の問い合わせを効率的に処理する flow 型です。今回は自然言語から DSL を生成して検索するというシンプルな用途のため、flow 型のエージェントに QueryPlanningTool を 1 つだけ持たせる構成にします。

処理の流れは次のとおりです。

QueryPlanningTool が LLM を呼び出すためには、Bedrock などの LLM への ML コネクタとモデルを事前に登録しておく必要があります。

https://docs.aws.amazon.com/opensearch-service/latest/developerguide/cfn-template-agentic-search.html

https://docs.aws.amazon.com/opensearch-service/latest/developerguide/agentic-search.html

やってみた

前提条件

  • 検証環境: 東京リージョン(ap-northeast-1)
  • Amazon OpenSearch Serverless NextGen(SEARCH Collection)
  • Amazon Bedrock の Claude Sonnet 4.6(推論プロファイル jp.anthropic.claude-sonnet-4-6
  • ローカルに Python 3、boto3requestsrequests-aws4auth をインストール済み

Amazon OpenSearch Serverless のデータプレーン(インデックス作成・データ投入・ML/エージェント API)の呼び出しには SigV4 署名が必要です。今回は AWS 公式の requests-aws4auth を使った Python スクリプトでリクエストに署名しました。署名サービス名はサーバーレスのデータプレーン用の aoss を指定します。

システム構成

agentic-search-environment.drawio

Bedrock 呼び出し用 IAM ロールの作成

Amazon OpenSearch Serverless が Amazon Bedrock を呼び出すための IAM ロールを作成します。サーバーレスの ML コネクタが引き受けるロールのため、信頼ポリシーのサービスプリンシパルには ml.opensearchservice.amazonaws.com を指定します。

% aws iam create-role --role-name OSAgenticBedrockRole \
  --assume-role-policy-document file://trust-policy.json
{
    "Role": {
        "Path": "/",
        "RoleName": "OSAgenticBedrockRole",
        "RoleId": "AROAXQ6Q7KC6T3LK553IF",
        "Arn": "arn:aws:iam::123456789012:role/OSAgenticBedrockRole",
        "CreateDate": "2026-06-21T07:52:26+00:00",
        "AssumeRolePolicyDocument": {
            "Version": "2012-10-17",
            "Statement": [
                {
                    "Effect": "Allow",
                    "Principal": {
                        "Service": [
                            "ml.opensearchservice.amazonaws.com",
                            "opensearchservice.amazonaws.com"
                        ]
                    },
                    "Action": "sts:AssumeRole"
                }
            ]
        }
    }
}

% aws iam put-role-policy --role-name OSAgenticBedrockRole \
  --policy-name bedrock-invoke --policy-document file://bedrock-policy.json
trust-policy.json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": { "Service": ["ml.opensearchservice.amazonaws.com", "opensearchservice.amazonaws.com"] },
      "Action": "sts:AssumeRole"
    }
  ]
}
bedrock-policy.json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": ["bedrock:InvokeModel", "bedrock:InvokeModelWithResponseStream", "bedrock:Converse", "bedrock:ConverseStream"],
      "Resource": "*"
    }
  ]
}

信頼ポリシーと権限は次のようにしました。

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": { "Service": ["ml.opensearchservice.amazonaws.com"] },
      "Action": "sts:AssumeRole"
    }
  ]
}

権限側では bedrock:InvokeModel などを許可しています。

セキュリティポリシーとデータアクセスポリシーの作成

コレクションの作成前に、暗号化ポリシー(AWS Owned Key)とネットワークポリシー(パブリックアクセス)、データアクセスポリシーを作成します。

% aws opensearchserverless create-security-policy --name enc-agentic-search-blog --type encryption --policy file://enc.json
{
    "securityPolicyDetail": {
        "type": "encryption",
        "name": "enc-agentic-search-blog",
        "policyVersion": "MTc4MjAyOTk0Nzc0Ml8x",
        "policy": {
            "Rules": [
                {
                    "Resource": [
                        "collection/agentic-search-blog"
                    ],
                    "ResourceType": "collection"
                }
            ],
            "AWSOwnedKey": true
        },
        "createdDate": 1782029947742,
        "lastModifiedDate": 1782029947742
    }
}
% aws opensearchserverless create-security-policy --name net-agentic-search-blog --type network --policy file://net.json
{
    "securityPolicyDetail": {
        "type": "network",
        "name": "net-agentic-search-blog",
        "policyVersion": "MTc4MjAyOTk3NjE2Ml8x",
        "policy": [
            {
                "Rules": [
                    {
                        "Resource": [
                            "collection/agentic-search-blog"
                        ],
                        "ResourceType": "collection"
                    },
                    {
                        "Resource": [
                            "collection/agentic-search-blog"
                        ],
                        "ResourceType": "dashboard"
                    }
                ],
                "AllowFromPublic": true
            }
        ],
        "createdDate": 1782029976162,
        "lastModifiedDate": 1782029976162
    }
}
% aws opensearchserverless create-access-policy --name data-agentic-search-blog --type data --policy file://data.json
{
    "accessPolicyDetail": {
        "type": "data",
        "name": "data-agentic-search-blog",
        "policyVersion": "MTc4MjAyOTk5ODE4N18x",
        "policy": [
            {
                "Rules": [
                    {
                        "Resource": [
                            "collection/agentic-search-blog"
                        ],
                        "Permission": [
                            "aoss:*"
                        ],
                        "ResourceType": "collection"
                    },
                    {
                        "Resource": [
                            "index/agentic-search-blog/*"
                        ],
                        "Permission": [
                            "aoss:*"
                        ],
                        "ResourceType": "index"
                    },
                    {
                        "Resource": [
                            "model/agentic-search-blog/*"
                        ],
                        "Permission": [
                            "aoss:*"
                        ],
                        "ResourceType": "model"
                    },
                    {
                        "Resource": [
                            "agent/agentic-search-blog/*"
                        ],
                        "Permission": [
                            "aoss:*"
                        ],
                        "ResourceType": "agent"
                    }
                ],
                "Principal": [
                    "arn:aws:iam::123456789012:role/cm-ishikawa.satoru"
                ]
            }
        ],
        "createdDate": 1782029998187,
        "lastModifiedDate": 1782029998187
    }
}
enc.json
{
  "Rules": [
    { "ResourceType": "collection", "Resource": ["collection/agentic-search-blog"] }
  ],
  "AWSOwnedKey": true
}
net.json
[
 {
   "Rules": [
     { "ResourceType": "collection", "Resource": ["collection/agentic-search-blog"] },
     { "ResourceType": "dashboard", "Resource": ["collection/agentic-search-blog"] }
   ],
   "AllowFromPublic": true
 }
]

データアクセスポリシーでは、リソースタイプとして collectionindex に加えて、ML リソース用の model、エージェント用の agent を指定し、自分の IAM ロールにアクセス権を付与します。modelagent のリソースタイプを忘れると、後段の ML コネクタやエージェント登録が 403 で失敗します。

data.json:

[
  {
    "Rules": [
      { "ResourceType": "collection", "Resource": ["collection/agentic-search-blog"], "Permission": ["aoss:*"] },
      { "ResourceType": "index", "Resource": ["index/agentic-search-blog/*"], "Permission": ["aoss:*"] },
      { "ResourceType": "model", "Resource": ["model/agentic-search-blog/*"], "Permission": ["aoss:*"] },
      { "ResourceType": "agent", "Resource": ["agent/agentic-search-blog/*"], "Permission": ["aoss:*"] }
    ],
    "Principal": ["arn:aws:iam::123456789012:role/cm-ishikawa.satoru"]
  }
]

NextGen コレクションの作成

次世代(NextGen)のコレクショングループを作成し、その上に SEARCH 型のコレクションを作成します。NextGen は最小 OCU が 0 まで下がる scale-to-zero に対応しているため、アイドル時のコストを抑えられます。コスト上限を抑えるため、OCU の最大値は 2 に設定しました。なお、NextGen コレクショングループでは standby レプリカを ENABLED にする必要があります。

% aws opensearchserverless create-collection-group \
  --name nextgen-agentic-search --standby-replicas ENABLED --generation NEXTGEN \
  --capacity-limits "maxIndexingCapacityInOCU=2,maxSearchCapacityInOCU=2"
{
    "createCollectionGroupDetail": {
        "id": "lw2twsdylfhs95wm8zw2",
        "arn": "arn:aws:aoss:ap-northeast-1:123456789012:collection-group/lw2twsdylfhs95wm8zw2",
        "name": "nextgen-agentic-search",
        "standbyReplicas": "ENABLED",
        "createdDate": 1782030106252,
        "capacityLimits": {
            "maxIndexingCapacityInOCU": 2.0,
            "maxSearchCapacityInOCU": 2.0,
            "minIndexingCapacityInOCU": 0.0,
            "minSearchCapacityInOCU": 0.0
        },
        "generation": "NEXTGEN"
    }
}

% aws opensearchserverless create-collection \
  --name agentic-search-blog --type SEARCH \
  --collection-group-name nextgen-agentic-search --standby-replicas ENABLED
{
    "createCollectionDetail": {
        "id": "mxrp0ih7g1itowz7om1m",
        "name": "agentic-search-blog",
        "status": "CREATING",
        "type": "SEARCH",
        "arn": "arn:aws:aoss:ap-northeast-1:123456789012:collection/mxrp0ih7g1itowz7om1m",
        "kmsKeyArn": "auto",
        "standbyReplicas": "ENABLED",
        "deletionProtection": "DISABLED",
        "createdDate": 1782030126378,
        "lastModifiedDate": 1782030126378,
        "collectionGroupName": "nextgen-agentic-search"
    }
}

作成されたコレクショングループの容量は、最小 OCU が 0、最大 OCU が 2 となっており、scale-to-zero が有効であることを確認できました。コレクションのステータスが ACTIVE になると、collectionEndpoint が払い出されます。NextGen ではこの起動が非常に速く、数秒で ACTIVE になりました。

Bedrock コネクタとモデルの登録

ここからはコレクションのエンドポイントに対する OpenSearch REST API の操作です。Bedrock の Converse API を呼び出す ML コネクタを定義し、モデルとして登録します。コネクタの credential には先ほど作成した IAM ロールの ARN を指定します。登録に成功すると model_id が返ります。

% export ENDPOINT=$(aws opensearchserverless batch-get-collection \
  --names agentic-search-blog --region ap-northeast-1 \
  --query "collectionDetails[0].collectionEndpoint" --output text)

% python3 aoss_rest.py POST "/_plugins/_ml/models/_register?deploy=true" \
  --endpoint "$ENDPOINT" --data @model_register.json
HTTP 200
{"task_id":"63bf1410-e908-4463-a715-a77a09d32f1f","status":"CREATED","model_id":"0927fff6-c24b-4387-9dcf-e592fa441e20"}
aoss_rest.py
#!/usr/bin/env python3
"""OpenSearch Serverless データプレーンへの SigV4 署名付き REST クライアント。

署名は AWS 公式手法の requests-aws4auth(service 名 'aoss')を使用する。

使い方:
  python3 aoss_rest.py <METHOD> <PATH> --endpoint <collection-endpoint> [--data '@file' | --data '{...}']
"""
import sys
import argparse
import boto3
import requests
from requests_aws4auth import AWS4Auth

def main():
    p = argparse.ArgumentParser()
    p.add_argument("method")
    p.add_argument("path")
    p.add_argument("--endpoint", required=True)
    p.add_argument("--region", default="ap-northeast-1")
    p.add_argument("--service", default="aoss")
    p.add_argument("--data")
    p.add_argument("--content-type", default="application/json")
    args = p.parse_args()

    creds = boto3.Session().get_credentials().get_frozen_credentials()
    awsauth = AWS4Auth(
        creds.access_key, creds.secret_key, args.region, args.service,
        session_token=creds.token,
    )

    body = None
    if args.data:
        if args.data.startswith("@"):
            with open(args.data[1:], "rb") as f:
                body = f.read()
        else:
            body = args.data.encode("utf-8")

    url = args.endpoint.rstrip("/") + args.path
    resp = requests.request(
        args.method, url, auth=awsauth,
        headers={"Content-Type": args.content_type}, data=body,
    )
    print("HTTP", resp.status_code)
    print(resp.text)
    sys.exit(0 if resp.ok else 2)

if __name__ == "__main__":
    main()
model_register.json

QueryPlanningTool は、コネクタの request_body に system プロンプトと user プロンプトのパラメータが含まれていることを要求します。Bedrock Converse 用のリクエストボディは次のようにしました。

{
  "name": "Claude Sonnet 4.6 Query Planner",
  "function_name": "remote",
  "description": "Bedrock Claude Sonnet 4.6 for agentic query planning",
  "connector": {
    "name": "Bedrock Claude Sonnet 4.6 Connector",
    "description": "Bedrock Converse connector for Claude Sonnet 4.6",
    "version": 1,
    "protocol": "aws_sigv4",
    "parameters": {
      "region": "ap-northeast-1",
      "service_name": "bedrock",
      "model": "jp.anthropic.claude-sonnet-4-6"
    },
    "credential": {
      "roleArn": "arn:aws:iam::123456789012:role/OSAgenticBedrockRole"
    },
    "actions": [
      {
        "action_type": "predict",
        "method": "POST",
        "url": "https://bedrock-runtime.${parameters.region}.amazonaws.com/model/${parameters.model}/converse",
        "headers": { "content-type": "application/json" },
        "request_body": "{ \"system\": [{\"text\": \"${parameters.system_prompt}\"}], \"messages\": [${parameters._chat_history:-}{\"role\":\"user\",\"content\":[{\"text\":\"${parameters.user_prompt}\"}]}${parameters._interactions:-}]${parameters.tool_configs:-} }"
      }
    ]
  }
}

念のため、エージェント化の前にモデル単体で予測を実行し、Bedrock への接続(IAM ロールの委譲)が正しく機能するか確認しました。hello が返り、トークン使用量も取得できています。

% python3 aoss_rest.py POST "/_plugins/_ml/models/0927fff6-c24b-4387-9dcf-e592fa441e20/_predict" \
  --endpoint "$ENDPOINT" --region ap-northeast-1 \
  --data '{"parameters":{"system_prompt":"You are a helpful assistant. Answer concisely.","user_prompt":"Reply with exactly one word: hello"}}'

HTTP 200
{"inference_results":[{"output":[{"name":"response","dataAsMap":{"metrics":{"latencyMs":696},"output":{"message":{"content":[{"text":"hello"}],"role":"assistant"}},"stopReason":"end_turn","usage":{"cacheReadInputTokenCount":0,"cacheReadInputTokens":0,"cacheWriteInputTokenCount":0,"cacheWriteInputTokens":0,"inputTokens":26,"outputTokens":4,"serverToolUsage":{},"totalTokens":30}}}],"status_code":200}]}

インデックスの作成と日本語サンプルデータの投入

検索対象として、日本語の商品データ(bulk.ndjson)のインデックス sample-products (形態素解析エンジンにkuromoji)を作成します。価格は float、在庫は boolean、カテゴリやブランドは keyword といった型をマッピングで指定しておくと、QueryPlanningTool がより適切な DSL を生成できます。

% python3 aoss_rest.py PUT "/sample-products" --endpoint "$ENDPOINT" --region ap-northeast-1 --data @index_mapping.json
HTTP 200
{"acknowledged":true,"shards_acknowledged":false,"index":"sample-products"}

% python3 aoss_rest.py POST "/_bulk" --endpoint "$ENDPOINT" --region ap-northeast-1 \
  --data @bulk.ndjson --content-type "application/x-ndjson"
HTTP 200
{"took":15820,"errors":false,"items":[{"index":{"_index":"sample-products","_id":"F4sg6p4BYcptwQFi9Kob","_version":1,"result":"created","_shards":{"total":0,"successful":0,"failed":0},"_seq_no":0,"_primary_term":0,"status":201}},{"index":{"_index":"sample-products","_id":"GIsg6p4BYcptwQFi9Kob","_version":1,"result":"created","_shards":{"total":0,"successful":0,"failed":0},"_seq_no":0,"_primary_term":0,"status":201}},{"index":{"_index":"sample-products","_id":"GYsg6p4BYcptwQFi9Kob","_version":1,"result":"created","_shards":{"total":0,"successful":0,"failed":0},"_seq_no":0,"_primary_term":0,"status":201}},{"index":{"_index":"sample-products","_id":"Gosg6p4BYcptwQFi9Kob","_version":1,"result":"created","_shards":{"total":0,"successful":0,"failed":0},"_seq_no":0,"_primary_term":0,"status":201}},{"index":{"_index":"sample-products","_id":"G4sg6p4BYcptwQFi9Kob","_version":1,"result":"created","_shards":{"total":0,"successful":0,"failed":0},"_seq_no":0,"_primary_term":0,"status":201}},{"index":{"_index":"sample-products","_id":"HIsg6p4BYcptwQFi9Kob","_version":1,"result":"created","_shards":{"total":0,"successful":0,"failed":0},"_seq_no":0,"_primary_term":0,"status":201}},{"index":{"_index":"sample-products","_id":"HYsg6p4BYcptwQFi9Kob","_version":1,"result":"created","_shards":{"total":0,"successful":0,"failed":0},"_seq_no":0,"_primary_term":0,"status":201}},{"index":{"_index":"sample-products","_id":"Hosg6p4BYcptwQFi9Kob","_version":1,"result":"created","_shards":{"total":0,"successful":0,"failed":0},"_seq_no":0,"_primary_term":0,"status":201}},{"index":{"_index":"sample-products","_id":"H4sg6p4BYcptwQFi9Kob","_version":1,"result":"created","_shards":{"total":0,"successful":0,"failed":0},"_seq_no":0,"_primary_term":0,"status":201}},{"index":{"_index":"sample-products","_id":"IIsg6p4BYcptwQFi9Kob","_version":1,"result":"created","_shards":{"total":0,"successful":0,"failed":0},"_seq_no":0,"_primary_term":0,"status":201}}]}
bulk.ndjson
{"index":{"_index":"sample-products"}}
{"product_name":"UltraBook Pro 14","category":"laptops","brand":"Acme","price":1299,"rating":4.7,"in_stock":true,"description":"軽量14インチのハイエンドノートパソコン"}
{"index":{"_index":"sample-products"}}
{"product_name":"Budget Laptop 15","category":"laptops","brand":"Nimbus","price":699,"rating":4.1,"in_stock":true,"description":"コストパフォーマンス重視の15インチノートパソコン"}
{"index":{"_index":"sample-products"}}
{"product_name":"Gaming Laptop X","category":"laptops","brand":"Vortex","price":1899,"rating":4.8,"in_stock":false,"description":"高性能GPU搭載のゲーミングノートパソコン"}
{"index":{"_index":"sample-products"}}
{"product_name":"Wireless Earbuds","category":"electronics","brand":"Acme","price":149,"rating":4.3,"in_stock":true,"description":"ノイズ低減対応のワイヤレスイヤホン"}
{"index":{"_index":"sample-products"}}
{"product_name":"4K Monitor 27","category":"electronics","brand":"Nimbus","price":459,"rating":4.6,"in_stock":true,"description":"27インチ4K解像度の液晶モニター"}
{"index":{"_index":"sample-products"}}
{"product_name":"Mechanical Keyboard","category":"electronics","brand":"Vortex","price":119,"rating":4.5,"in_stock":true,"description":"打鍵感の良いメカニカルキーボード"}
{"index":{"_index":"sample-products"}}
{"product_name":"Office Chair Ergo","category":"furniture","brand":"Comfy","price":329,"rating":4.2,"in_stock":true,"description":"長時間でも疲れにくいエルゴノミクスチェア"}
{"index":{"_index":"sample-products"}}
{"product_name":"Standing Desk","category":"furniture","brand":"Comfy","price":549,"rating":4.4,"in_stock":false,"description":"電動昇降式のスタンディングデスク"}
{"index":{"_index":"sample-products"}}
{"product_name":"Noise Cancel Headphones","category":"electronics","brand":"Acme","price":279,"rating":4.9,"in_stock":true,"description":"高い遮音性を持つノイズキャンセリングヘッドホン"}
{"index":{"_index":"sample-products"}}
{"product_name":"Tablet 11","category":"electronics","brand":"Nimbus","price":599,"rating":4.0,"in_stock":true,"description":"11インチの軽量タブレット"}
index_mapping.json
{
  "mappings": {
    "properties": {
      "product_name": { "type": "text", "analyzer": "kuromoji" },
      "description": { "type": "text", "analyzer": "kuromoji" },
      "category": { "type": "keyword" },
      "price": { "type": "float" },
      "rating": { "type": "float" },
      "in_stock": { "type": "boolean" }
    }
  }
}

ノートパソコンや電子機器、家具など 10 件の商品を投入しました。errorsfalse で 10 件すべて登録できています。

QueryPlanningTool エージェントの登録

QueryPlanningTool を 1 つ持つ flow 型エージェントを登録します。model_id に先ほどのモデルを指定し、response_filter には Bedrock Converse(Claude)のレスポンスから生成クエリを取り出す JSONPath $.output.message.content[0].text を指定します。登録に成功すると agent_id が返ります。

% python3 aoss_rest.py POST "/_plugins/_ml/agents/_register" --endpoint "$ENDPOINT" --data @agent.json
HTTP 200
{"agent_id":"ce5eaec8-f779-4d57-a314-9f59595f8e76"}
agent.json
{
  "name": "Agentic Search Product Agent",
  "type": "flow",
  "description": "NL to DSL query planning on sample-products",
  "tools": [
    {
      "type": "QueryPlanningTool",
      "parameters": {
        "model_id": "0927fff6-c24b-4387-9dcf-e592fa441e20",
        "response_filter": "$.output.message.content[0].text"
      }
    }
  ]
}

自然言語クエリの実行(NL から DSL 変換)

いよいよ本題です。エージェントの _execute API に自然言語の質問を投げると、生成された DSL クエリが返ってきます。

「800ドル未満のノートパソコンを表示して」

まずは「800ドル未満のノートパソコンを表示して」と尋ねてみます。

% python3 aoss_rest.py POST "/_plugins/_ml/agents/ce5eaec8-f779-4d57-a314-9f59595f8e76/_execute" \
  --endpoint "$ENDPOINT" \
  --data '{"parameters":{"question":"800ドル未満のノートパソコンを表示して","index_name":"sample-products"}}'

HTTP 200
{"inference_results":[{"output":[{"name":"response","result":"{\"size\":10,\"query\":{\"bool\":{\"must\":[{\"match\":{\"product_name\":{\"query\":\"ノートパソコン\",\"analyzer\":\"kuromoji\"}}}],\"filter\":[{\"range\":{\"price\":{\"lt\":800}}}]}},\"sort\":[{\"_score\":{\"order\":\"desc\"}}],\"track_total_hits\":false}"}]}]}

生成された DSL は次のとおりです。「800ドル未満」が pricerange フィルタ(lt: 800)に、「ノートパソコン」が product_namedescription に対する multi_match クエリに、正しく変換されました。

「在庫切れの商品をすべて見せて」

続いて「在庫切れの商品をすべて見せて」と尋ねると、in_stockfalseterm フィルタが生成されました。

% python3 aoss_rest.py POST "/_plugins/_ml/agents/ce5eaec8-f779-4d57-a314-9f59595f8e76/_execute" \
  --endpoint "$ENDPOINT" \
  --data '{"parameters":{"question":"在庫切れの商品をすべて見せて","index_name":"sample-products"}}'

HTTP 200
{"inference_results":[{"output":[{"name":"response","result":"{\"size\":10,\"query\":{\"bool\":{\"filter\":[{\"term\":{\"in_stock\":false}}]}},\"sort\":[{\"_score\":{\"order\":\"desc\"}}],\"track_total_hits\":false}"}]}]}

「評価が4.5以上の電子機器を評価が高い順に表示」

さらに「評価が4.5以上の電子機器を評価が高い順に表示」では、カテゴリの term フィルタ、評価の range フィルタ、評価による降順ソートが組み合わさった複合クエリが生成されました。

% python3 aoss_rest.py POST "/_plugins/_ml/agents/ce5eaec8-f779-4d57-a314-9f59595f8e76/_execute" \
  --endpoint "$ENDPOINT" \
  --data '{"parameters":{"question":"評価が4.5以上の電子機器を評価が高い順に表示","index_name":"sample-products"}}'

HTTP 200
{"inference_results":[{"output":[{"name":"response","result":"{\"size\":10,\"query\":{\"bool\":{\"filter\":[{\"term\":{\"category\":\"electronics\"}},{\"range\":{\"rating\":{\"gte\":4.5}}}]}},\"sort\":[{\"rating\":{\"order\":\"desc\"}}],\"track_total_hits\":false}"}]}]}

数値の範囲・真偽値・カテゴリ・ソート順といった条件を、自然言語の文章から的確に DSL へ落とし込めています。

検索パイプラインでエンドツーエンドに検索する

_execute は DSL を生成するだけですが、検索パイプラインを使うと、自然言語クエリから生成した DSL でそのまま検索を実行し、結果を返せます。agentic_query_translator リクエストプロセッサーにエージェントを紐付けたパイプラインを作成します。

% python3 aoss_rest.py PUT "/_search/pipeline/agentic_search_pipeline" --endpoint "$ENDPOINT" \
  --data '{"request_processors":[{"agentic_query_translator":{"agent_id":"ce5eaec8-f779-4d57-a314-9f59595f8e76"}}],"response_processors":[{"agentic_context":{"dsl_query":true}}]}'
HTTP 200
{"acknowledged":true}

このパイプラインを指定し、agentic クエリで自然言語を渡します。

% python3 aoss_rest.py POST "/sample-products/_search?search_pipeline=agentic_search_pipeline" \
  --endpoint "$ENDPOINT" \
  --data '{"query":{"agentic":{"query_text":"800ドル未満のノートパソコンを表示して"}}}'
HTTP 200
{"took":2855,"timed_out":false,"_shards":{"total":0,"successful":0,"skipped":0,"failed":0},"hits":{"max_score":null,"hits":[]},"ext":{"dsl_query":"{\"size\":10,\"query\":{\"bool\":{\"must\":[{\"match\":{\"product_name\":{\"query\":\"ノートパソコン\",\"analyzer\":\"kuromoji\"}}}],\"filter\":[{\"range\":{\"price\":{\"lt\":800}}}]}},\"sort\":[{\"_score\":{\"order\":\"desc\"}}],\"track_total_hits\":false}"}}

実際の検索結果が返ってきました。3 台のノートパソコン(1299・699・1899 ドル)のうち、800 ドル未満は「Budget Laptop 15」(699 ドル)だけです。期待どおり 1 件だけがヒットし、ext.dsl_query には生成された DSL も含まれています。

自然言語のリクエストから DSL の生成、検索の実行、結果の返却までが一気通貫で動作することを確認できました。

考察

実際に試してみて得られた知見と注意点を整理します。

  • DSL 生成の精度は実用的: 数値の範囲(〜未満、〜以上)、真偽値(在庫切れ)、カテゴリ、ソート順といった条件を、日本語の文章から的確に DSL へ変換できました。検索 DSL に不慣れな利用者でも、自然言語で OpenSearch を検索できる体験は強力です。
  • 日本語のフリーテキスト検索には kuromoji が前提: QueryPlanningTool の日本語理解は良好で、category などの構造化フィールドへの term/range に落ちる限りは日本語でも問題なく動作します。一方、description のような日本語テキストフィールドに対する match/multi_match の全文検索は、デフォルトの analyzer だとカタカナの連続が 1 つのトークンに結合されてしまい(例: 「ハイエンドノートパソコン」が 1 トークン)、「ノートパソコン」で検索しても 0 件になりました。説明文などの日本語フリーテキスト検索を狙う場合は、テキストフィールドに kuromoji(日本語形態素解析)の analyzer を設定してインデックスを作成するのが前提になります。kuromoji を適用すると「ハイエンド」「ノート」「パソコン」のように単語単位で分割され、match でヒットするようになります。なお analyzer は索引時に適用されるため、設定変更にはインデックスの作り直しとデータの再投入が必要です。
  • scale-to-zero でコストを抑えやすい: NextGen コレクションは最小 OCU を 0 まで下げられ、アイドル時の課金を回避できます。今回のように短時間の検証では、コストを大きく抑えられます。
  • 生成 DSL が透過的に確認できる: 検索パイプラインの agentic_context レスポンスプロセッサーで dsl_query を有効にすると、生成された DSL がレスポンスの ext に含まれます。エージェントが「どう解釈したか」を確認でき、デバッグや検証に役立ちます。
  • ベクトル埋め込みは必須ではない: Agentic Search の QueryPlanningTool は自然言語から DSL を生成する機能のため、ベクトル検索を併用しない限り埋め込みモデルは不要です。今回は SEARCH 型コレクションで埋め込みなしの構成で検証できました。
  • データアクセスポリシーの設計に注意: ML コネクタ・モデルやエージェントの API を使うには、データアクセスポリシーに modelagent のリソースタイプを追加する必要があります。許可がないと 403 が返ります。
  • コネクタの response_filter はモデルに合わせる: Bedrock の Claude(Converse API)では $.output.message.content[0].text、OpenAI 系では $.choices[0].message.content というように、利用する LLM に応じて生成クエリの抽出先を指定します。
  • データプレーンの署名は aoss: REST API の SigV4 署名サービス名はサーバーレスの aoss です。署名に不備があると認可前の 403 になり原因の切り分けに手間取るため、requests-aws4auth など実績のある実装を使うのが無難でした。

今回は flow 型エージェントで単発の検索を試しましたが、conversational 型を使えば対話的な検索や、会話メモリを活かしたマルチターンのやり取りも構成できます。また、user_templates を使って事前定義した検索テンプレートに沿って DSL を生成させることもできるため、生成されるクエリの形を制御したい場合に有効です。

最後に

Amazon OpenSearch Serverless NextGen の Agentic Search を、東京リージョンで実際に構築して試してみました。Amazon Bedrock の Claude Sonnet 4.6 を LLM に据え、自然言語の質問から OpenSearch DSL を生成し、検索結果まで一気通貫で取得できることを確認できました。NextGen コレクションの scale-to-zero と組み合わせれば、アイドル時のコストを抑えつつ、検索 DSL を意識せずに使える検索体験を提供できます。検索 UI の自然言語化や、社内データの検索アシスタントなどに活用できそうです。

なお、説明文などの日本語フリーテキストに対する全文検索を狙う場合は、テキストフィールドへの kuromoji(日本語形態素解析)の設定が前提になる点だけ注意してください。詳細は考察で触れたとおりです。

OpenSearch Serverless で自然言語検索を検討されている方は、検証してみてはいかがでしょうか。


Claudeならクラスメソッドにお任せください

クラスメソッドは、Anthropic社とリセラー契約を締結しています。各種製品ガイドから、業種別の活用法、フェーズごとのお悩み解決などサービス支援ページにまとめております。まずはご覧いただき、お気軽にご相談ください。

サービス詳細を見る

この記事をシェアする

AWSのお困り事はクラスメソッドへ

関連記事