[Update] I Tried Agentic Search That Converts Natural Language to Search DSL Using Amazon OpenSearch Serverless NextGen × Amazon Bedrock (Claude)

[Update] I Tried Agentic Search That Converts Natural Language to Search DSL Using Amazon OpenSearch Serverless NextGen × Amazon Bedrock (Claude)

Natural language search (Agentic Search) became available in Amazon OpenSearch Serverless, so I actually built it in the Tokyo region and tried out natural language to search DSL conversion using Claude Sonnet 4.6.
2026.06.21

This page has been translated by machine translation. View original

I'm the Cloud Business Division's Ishikawa. Amazon OpenSearch Serverless now supports natural language search (Agentic Search), so I tried building and testing it in the Tokyo region.

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

Users simply describe "what they are looking for" in Japanese or English sentences, and the system interprets the intent, plans the optimal search strategy, generates OpenSearch DSL (Domain-Specific Language) queries, and returns results along with an explanation of the reasoning.

Behind the scenes, the built-in QueryPlanningTool powered by a large language model (LLM) converts natural language into DSL queries, orchestrates the appropriate tools, and retrieves results. Configuration can be done via API or OpenSearch Dashboards. Agentic Search is available in all AWS commercial regions where OpenSearch Serverless is offered.

Note that Agentic Search itself was first introduced in November 2025 for managed Amazon OpenSearch Service (version 3.3 and later), and this update makes it available for serverless collections as well.

This time, I'll combine a next-generation (NextGen) OpenSearch Serverless collection with Amazon Bedrock's Claude Sonnet 4.6 to build natural language search over product data using the AWS CLI and the OpenSearch REST API.

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

Agentic Search is a mechanism where an autonomous agent understands the user's intent, selects appropriate tools, and generates and executes optimal queries. The core component is the QueryPlanningTool, which uses an LLM to convert natural language requests into OpenSearch DSL queries.

There are two types of agents. The conversational type, which handles multi-turn interactions, and the flow type, which efficiently processes single queries. Since our use case is simply generating DSL from natural language and performing a search, we'll configure the flow-type agent to have just one QueryPlanningTool.

The processing flow is as follows.

For the QueryPlanningTool to call an LLM, an ML connector and model pointing to an LLM such as Bedrock must be registered in advance.

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

Trying It Out

Prerequisites

  • Test environment: Tokyo region (ap-northeast-1)
  • Amazon OpenSearch Serverless NextGen (SEARCH Collection)
  • Amazon Bedrock Claude Sonnet 4.6 (inference profile jp.anthropic.claude-sonnet-4-6)
  • Python 3, boto3, requests, and requests-aws4auth installed locally

SigV4 signing is required to call the Amazon OpenSearch Serverless data plane (index creation, data ingestion, ML/agent APIs). This time, requests were signed using a Python script with the official AWS requests-aws4auth. The signing service name is set to aoss, which is for the serverless data plane.

System Architecture

agentic-search-environment.drawio

Creating an IAM Role for Bedrock Invocation

Create an IAM role for Amazon OpenSearch Serverless to call Amazon Bedrock. Since this is the role assumed by the serverless ML connector, specify ml.opensearchservice.amazonaws.com as the service principal in the trust policy.

% 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": "*"
    }
  ]
}

The trust policy and permissions were configured as follows.

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

On the permissions side, bedrock:InvokeModel and others are allowed.

Creating Security Policies and Data Access Policies

Before creating the collection, create the encryption policy (AWS Owned Key), network policy (public access), and data access policy.

% 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
 }
]

In the data access policy, specify collection and index as resource types, as well as model for ML resources and agent for agents, granting access to your own IAM role. If you forget the model and agent resource types, subsequent ML connector and agent registration will fail with 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"]
  }
]

Creating the NextGen Collection

Create a next-generation (NextGen) collection group, then create a SEARCH-type collection on top of it. NextGen supports scale-to-zero, where the minimum OCU can drop to 0, allowing costs to be kept low during idle periods. To limit costs, the maximum OCU was set to 2. Note that NextGen collection groups require standby replicas to be set to 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"
    }
}

The capacity of the created collection group shows a minimum OCU of 0 and a maximum OCU of 2, confirming that scale-to-zero is enabled. Once the collection status becomes ACTIVE, a collectionEndpoint is issued. With NextGen, this startup is extremely fast, becoming ACTIVE within a few seconds.

Registering the Bedrock Connector and Model

From here, the operations are OpenSearch REST API calls against the collection endpoint. Define an ML connector that calls the Bedrock Converse API, and register it as a model. Specify the ARN of the IAM role created earlier in the connector's credential. A successful registration returns a 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
"""SigV4-signed REST client for the OpenSearch Serverless data plane.

Signing uses the official AWS method requests-aws4auth (service name 'aoss').

Usage:
  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

The QueryPlanningTool requires that the connector's request_body contains parameters for a system prompt and a user prompt. The request body for Bedrock Converse was configured as follows.

{
  "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:-} }"
      }
    ]
  }
}

As a precaution, before setting up the agent, I ran a prediction on the model alone to verify that the Bedrock connection (IAM role delegation) was working correctly. hello was returned, and token usage was also retrieved.

% 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}]}

Creating an Index and Loading Japanese Sample Data

As search targets, we will create an index sample-products for Japanese product data (bulk.ndjson) with the morphological analysis engine set to kuromoji. By specifying types in the mapping such as float for price, boolean for stock, and keyword for category and brand, the QueryPlanningTool can generate more appropriate 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":"Lightweight 14-inch high-end laptop"}
{"index":{"_index":"sample-products"}}
{"product_name":"Budget Laptop 15","category":"laptops","brand":"Nimbus","price":699,"rating":4.1,"in_stock":true,"description":"15-inch laptop focused on cost performance"}
{"index":{"_index":"sample-products"}}
{"product_name":"Gaming Laptop X","category":"laptops","brand":"Vortex","price":1899,"rating":4.8,"in_stock":false,"description":"Gaming laptop equipped with a high-performance GPU"}
{"index":{"_index":"sample-products"}}
{"product_name":"Wireless Earbuds","category":"electronics","brand":"Acme","price":149,"rating":4.3,"in_stock":true,"description":"Wireless earphones with noise reduction support"}
{"index":{"_index":"sample-products"}}
{"product_name":"4K Monitor 27","category":"electronics","brand":"Nimbus","price":459,"rating":4.6,"in_stock":true,"description":"27-inch 4K resolution LCD monitor"}
{"index":{"_index":"sample-products"}}
{"product_name":"Mechanical Keyboard","category":"electronics","brand":"Vortex","price":119,"rating":4.5,"in_stock":true,"description":"Mechanical keyboard with satisfying key feel"}
{"index":{"_index":"sample-products"}}
{"product_name":"Office Chair Ergo","category":"furniture","brand":"Comfy","price":329,"rating":4.2,"in_stock":true,"description":"Ergonomic chair that reduces fatigue even during long hours"}
{"index":{"_index":"sample-products"}}
{"product_name":"Standing Desk","category":"furniture","brand":"Comfy","price":549,"rating":4.4,"in_stock":false,"description":"Electrically height-adjustable standing desk"}
{"index":{"_index":"sample-products"}}
{"product_name":"Noise Cancel Headphones","category":"electronics","brand":"Acme","price":279,"rating":4.9,"in_stock":true,"description":"Noise-canceling headphones with high sound insulation"}
{"index":{"_index":"sample-products"}}
{"product_name":"Tablet 11","category":"electronics","brand":"Nimbus","price":599,"rating":4.0,"in_stock":true,"description":"Lightweight 11-inch tablet"}
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" }
    }
  }
}

We loaded 10 products including laptops, electronics, and furniture. With errors being false, all 10 items were successfully registered.

Registering the QueryPlanningTool Agent

We register a flow-type agent with one QueryPlanningTool. We specify the earlier model in model_id, and for response_filter we specify the JSONPath $.output.message.content[0].text to extract the generated query from the Bedrock Converse (Claude) response. Upon successful registration, an agent_id is returned.

% 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"
      }
    }
  ]
}

Executing Natural Language Queries (NL to DSL Conversion)

Now for the main topic. When you send a natural language question to the agent's _execute API, the generated DSL query is returned.

"Show me laptops under $800"

First, let's ask "Show me laptops under $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}"}]}]}

The generated DSL is as follows. "Under $800" was correctly converted to a range filter (lt: 800) on price, and "laptop" was correctly converted to a multi_match query against product_name and description.

"Show me all out-of-stock items"

Next, when asking "Show me all out-of-stock items," a term filter with in_stock set to false was generated.

% 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}"}]}]}

"Show electronics with a rating of 4.5 or higher, sorted by highest rating"

Furthermore, for "Show electronics with a rating of 4.5 or higher, sorted by highest rating," a compound query was generated combining a term filter for category, a range filter for rating, and a descending sort by rating.

% 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}"}]}]}

Conditions such as numeric ranges, boolean values, categories, and sort order were accurately translated from natural language sentences into DSL.

End-to-End Search Using a Search Pipeline

While _execute only generates DSL, using a search pipeline allows you to execute a search directly with the DSL generated from a natural language query and return results. We create a pipeline that binds an agent to the agentic_query_translator request processor.

% 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}

Specifying this pipeline, we pass natural language using an agentic query.

% 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}"}}

Actual search results were returned. Of the three laptops ($1299, $699, $1899), only "Budget Laptop 15" ($699) is under $800. As expected, only 1 result was hit, and ext.dsl_query also contains the generated DSL.

We confirmed that the entire flow from natural language request to DSL generation, search execution, and result retrieval works end-to-end.

Discussion

Here is a summary of the insights and notes gained from actually trying this out.

  • DSL generation accuracy is practical: Conditions such as numeric ranges (less than ~, greater than or equal to ~), boolean values (out of stock), categories, and sort order were accurately converted from Japanese sentences into DSL. The experience of being able to search OpenSearch in natural language, even for users unfamiliar with search DSL, is powerful.
  • kuromoji is a prerequisite for Japanese free-text search: The QueryPlanningTool's Japanese comprehension is good, and as long as queries fall into term/range for structured fields like category, they work fine in Japanese. On the other hand, for full-text search using match/multi_match against Japanese text fields like description, consecutive katakana characters are merged into a single token with the default analyzer (e.g., "ハイエンドノートパソコン" becomes 1 token), resulting in 0 results when searching for "ノートパソコン." If you aim for Japanese free-text search on description fields, setting up the kuromoji (Japanese morphological analysis) analyzer on the text field when creating the index is a prerequisite. When kuromoji is applied, words are split into units like "ハイエンド," "ノート," and "パソコン," making them searchable with match. Note that since the analyzer is applied at index time, changing the setting requires recreating the index and re-loading the data.
  • Easy to keep costs down with scale-to-zero: NextGen collections can reduce the minimum OCU to 0, avoiding charges during idle time. For short-term verification like this, costs can be significantly reduced.
  • Generated DSL can be transparently verified: By enabling dsl_query in the agentic_context response processor of the search pipeline, the generated DSL is included in the ext of the response. This allows you to check "how the agent interpreted" the query, which is useful for debugging and verification.
  • Vector embeddings are not required: Since the QueryPlanningTool in Agentic Search is a feature for generating DSL from natural language, an embedding model is not needed unless vector search is also used. In this case, we were able to verify the configuration without embeddings using a SEARCH type collection.
  • Pay attention to data access policy design: To use ML connector, model, and agent APIs, you need to add the resource types model and agent to the data access policy. Without permission, a 403 will be returned.
  • Connector response_filter must match the model: As in $.output.message.content[0].text for Bedrock's Claude (Converse API) and $.choices[0].message.content for OpenAI-type models, specify the extraction location of the generated query according to the LLM being used.
  • Data plane signature is aoss: The SigV4 signature service name for the REST API is aoss for serverless. If the signature is incorrect, a 403 is returned before authorization, which takes effort to troubleshoot, so it was safer to use a proven implementation like requests-aws4auth.

This time we tried a single search with a flow-type agent, but using a conversational type allows you to configure interactive search or multi-turn exchanges leveraging conversation memory. Also, you can use user_templates to generate DSL following predefined search templates, which is effective when you want to control the form of the generated queries.

Closing

We actually built and tried Amazon OpenSearch Serverless NextGen's Agentic Search in the Tokyo region. Using Claude Sonnet 4.6 from Amazon Bedrock as the LLM, we confirmed that natural language questions can be used to generate OpenSearch DSL and retrieve search results end-to-end. Combined with NextGen collection's scale-to-zero, it is possible to provide a search experience where users don't need to be aware of search DSL while keeping idle costs low. This seems applicable to natural language search UIs or internal data search assistants.

Note that if you aim for full-text search on Japanese free text such as descriptions, setting up kuromoji (Japanese morphological analysis) on the text field is a prerequisite. Details are covered in the discussion section.

If you are considering natural language search with OpenSearch Serverless, why not try it out?


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

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

サービス詳細を見る

Share this article

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