Adding search templates
출처: 문서
본문
검색 템플릿 추가하기
QueryPlanningTool은 등록 시 검색 템플릿 목록을 받을 수 있어요. 검색을 수행할 때 QueryPlanningTool이 사용자 질문과 템플릿 설명을 바탕으로 적절한 검색 템플릿을 선택하고, 대형 언어 모델(LLM)이 선택된 검색 템플릿을 기반으로 쿼리를 생성해요.
이 접근 방식을 활용하면 LLM 혼자서는 다루기 어려운 복잡한 사용 사례도 해결할 수 있어요:
- 에이전틱 검색에서 쿼리 응답의 일관성을 높여요. 쿼리 도메인 특화 언어(DSL) 쿼리의 대부분은 검색 템플릿이 제공하고, LLM은 일부 조각이나 플레이스홀더만 제공해요.
- LLM이 올바른 쿼리를 생성하는 데 어려움을 겪는 복잡한 사용 사례를 처리해요.
- 예측 가능한 쿼리 구조와 네이밍 컨벤션을 보장해요.
모범 사례
에이전틱 검색용 검색 템플릿을 만들 때는 다음 가이드라인을 따르세요:
- LLM이 적절하게 선택할 수 있도록 각 템플릿에 상세한 설명을 작성하세요.
- 무엇을 채워야 하는지 명확히 드러나는 설명적인 플레이스홀더 이름을 사용하세요.
- 평소 자주 쓰는 다양한 쿼리 패턴에 맞춰 템플릿을 만드세요.
- 배포하기 전에 다양한 입력으로 템플릿이 올바르게 동작하는지 검증하세요.
1단계: 인덱스 생성하기
복잡한 집계 시나리오를 시연하기 위해 중첩된 인벤토리 데이터를 담은 stores 인덱스를 생성하세요:
PUT /stores
{
"mappings": {
"properties": {
"store_id": { "type": "keyword" },
"name": { "type": "text", "fields": { "keyword": { "type": "keyword", "ignore_above": 256 } } },
"address": {
"properties": {
"city": { "type": "keyword" },
"state": { "type": "keyword" }
}
},
"location": { "type": "geo_point" },
"inventory": {
"type": "nested",
"properties": {
"sku": { "type": "keyword" },
"qty": { "type": "integer" }
}
}
}
}
}
{% include copy-curl.html %}
2단계: 문서 수집하기
여러 도시와 제품의 인벤토리 데이터가 담긴 샘플 매장 문서를 추가하세요:
POST /_bulk
{ "index": { "_index": "stores", "_id": "S-SEA-001" } }
{ "store_id": "S-SEA-001", "name": "Downtown Seattle", "address": { "city": "Seattle", "state": "WA" }, "location": { "lat": 47.608, "lon": -122.335 }, "inventory": [ { "sku": "iphone_17_air", "qty": 12 }, { "sku": "iphone_17", "qty": 11 }, { "sku": "vision_pro", "qty": 3 } ] }
{ "index": { "_index": "stores", "_id": "S-SEA-002" } }
{ "store_id": "S-SEA-002", "name": "Capitol Hill", "address": { "city": "Seattle", "state": "WA" }, "location": { "lat": 47.623, "lon": -122.319 }, "inventory": [ { "sku": "iphone_17_air", "qty": 5 }, { "sku": "iphone_17", "qty": 25 }, { "sku": "vision_pro", "qty": 4 } ] }
{ "index": { "_index": "stores", "_id": "S-SEA-003" } }
{ "store_id": "S-SEA-003", "name": "South Lake Union", "address": { "city": "Seattle", "state": "WA" }, "location": { "lat": 47.626, "lon": -122.338 }, "inventory": [ { "sku": "iphone_17_air", "qty": 6 }, { "sku": "iphone_17", "qty": 9 }, { "sku": "vision_pro", "qty": 20 } ] }
{ "index": { "_index": "stores", "_id": "S-BEL-001" } }
{ "store_id": "S-BEL-001", "name": "Bellevue Square", "address": { "city": "Bellevue", "state": "WA" }, "location": { "lat": 47.616, "lon": -122.203 }, "inventory": [ { "sku": "iphone_17_air", "qty": 14 }, { "sku": "iphone_17", "qty": 4 }, { "sku": "vision_pro", "qty": 1 } ] }
{ "index": { "_index": "stores", "_id": "S-SEA-004" } }
{ "store_id": "S-SEA-004", "name": "Ballard", "address": { "city": "Seattle", "state": "WA" }, "location": { "lat": 47.668, "lon": -122.382 }, "inventory": [ { "sku": "iphone_17_air", "qty": 9 }, { "sku": "iphone_17", "qty": 7 }, { "sku": "vision_pro", "qty": 12 } ] }
{% include copy-curl.html %}
3단계: 검색 템플릿 등록하기
세 개 SKU에 걸친 합산 인벤토리가 최소 기준을 충족하는 도시의 매장을 반환하는 검색 템플릿을 등록하세요:
POST /_scripts/store_sum_skus
{
"script": {
"lang": "mustache",
"source": {
"size": 0,
"query": { "term": { "address.city": "{% raw %}{{city}}{% endraw %}" } },
"aggs": {
"by_store": {
"terms": {
"field": "store_id",
"size": "{% raw %}{{bucket_size}}{{^bucket_size}}200{{/bucket_size}}{% endraw %}",
"order": { "inv>skus>q": "desc" }
},
"aggs": {
"inv": {
"nested": { "path": "inventory" },
"aggs": {
"skus": {
"filter": { "terms": { "inventory.sku": ["{% raw %}{{sku1}}{% endraw %}","{% raw %}{{sku2}}{% endraw %}","{% raw %}{{sku3}}{% endraw %}"] } },
"aggs": { "q": { "sum": { "field": "inventory.qty" } } }
}
}
},
"keep": {
"bucket_selector": {
"buckets_path": { "t": "inv>skus>q" },
"script": { "source": "params.t >= {% raw %}{{min_total}}{{^min_total}}30{{/min_total}}{% endraw %}" }
}
},
"store": {
"top_hits": {
"size": 1,
"_source": { "includes": ["store_id","name","address.city"] }
}
}
}
}
}
}
}
}
{% include copy-curl.html %}
특정 SKU를 최소 수량 이상 보유한 도시 내 매장 수를 세는 검색 템플릿을 등록하세요:
POST /_scripts/stores_with_give_sku
{
"script": {
"lang": "mustache",
"source": {
"size": 0,
"query": { "term": { "address.city": "{% raw %}{{city}}{% endraw %}" } },
"aggs": {
"s": {
"terms": {
"field": "store_id",
"size": "{% raw %}{{bs}}{{^bs}}200{{/bs}}{% endraw %}"
},
"aggs": {
"i": {
"nested": { "path": "inventory" },
"aggs": {
"f": {
"filter": { "term": { "inventory.sku": "{% raw %}{{sku}}{% endraw %}" } },
"aggs": { "q": { "sum": { "field": "inventory.qty" } } }
}
}
},
"m": {
"bucket_script": {
"buckets_path": { "x": "i>f>q" },
"script": { "source": "{% raw %}params.x >= {{min}}{{^min}}10{{/min}} ? 1 : 0{% endraw %}" }
}
}
}
},
"cnt": { "sum_bucket": { "buckets_path": "s>m" } }
}
}
}
}
{% include copy-curl.html %}
4단계: QueryPlanningTool을 포함한 에이전트 등록하기
다음으로 QueryPlanningTool을 포함한 에이전트를 등록하고, 이 도구가 여러분의 검색 템플릿을 사용하도록 설정하세요.
4(a)단계: 에이전트와 QueryPlanningTool용 모델 생성하기
대화형 에이전트와 QueryPlanningTool 양쪽에서 사용할 모델을 등록하세요:
POST /_plugins/_ml/models/_register
{
"name": "My OpenAI model: gpt-5",
"function_name": "remote",
"description": "Model for agentic search with templates",
"connector": {
"name": "My openai connector: gpt-5",
"description": "The connector to openai chat model",
"version": 1,
"protocol": "http",
"parameters": {
"model": "gpt-5"
},
"credential": {
"openAI_key": "<OPEN AI KEY>"
},
"actions": [
{
"action_type": "predict",
"method": "POST",
"url": "https://api.openai.com/v1/chat/completions",
"headers": {
"Authorization": "Bearer ${credential.openAI_key}"
},
"request_body": "{ \"model\": \"${parameters.model}\", \"messages\": [{\"role\":\"developer\",\"content\":\"${parameters.system_prompt}\"},${parameters._chat_history:-}{\"role\":\"user\",\"content\":\"${parameters.user_prompt}\"}${parameters._interactions:-}], \"reasoning_effort\":\"low\"${parameters.tool_configs:-}}"
}
]
}
}
{% include copy-curl.html %}
4(b)단계: 검색 템플릿을 사용하는 에이전트 등록하기
여러분의 검색 템플릿을 사용하도록 설정된 QueryPlanningTool을 포함한 에이전트를 등록하세요:
POST /_plugins/_ml/agents/_register
{
"name": "Store Search Agent with Templates",
"type": "conversational",
"description": "Agent for store inventory searches using templates",
"llm": {
"model_id": "your-model-id-from-step-4a",
"parameters": {
"max_iteration": 15
}
},
"memory": {
"type": "conversation_index"
},
"parameters": {
"_llm_interface": "openai/v1/chat/completions"
},
"tools": [
{
"type": "QueryPlanningTool",
"parameters": {
"model_id": "your-model-id-from-step-4a",
"generation_type": "user_templates",
"search_templates": [
{
"template_id": "store_sum_skus",
"template_description": "Return stores in a given city where the combined quantity across a list of SKUs meets or exceeds a threshold."
},
{
"template_id": "stores_with_give_sku",
"template_description": "List stores in a given city that have at least min_qty units of a specific SKU."
}
]
}
}
],
"app_type": "os_chat"
}
{% include copy-curl.html %}
5단계: 검색 파이프라인 생성하기
검색 템플릿과 함께 여러분의 에이전트를 사용하는 검색 파이프라인을 생성하세요:
PUT _search/pipeline/agentic-pipeline
{
"request_processors": [
{
"agentic_query_translator": {
"agent_id": "your-agent-id-from-step-4b"
}
}
],
"response_processors": [
{
"agentic_context": {
"agent_steps_summary": true,
"dsl_query": true
}
}
]
}
{% include copy-curl.html %}
6단계: 복잡한 질문 테스트하기
고급 집계가 필요한 복잡한 쿼리를 전송해 보세요:
POST /stores/_search?search_pipeline=agentic-pipeline
{
"query": {
"agentic": {
"query_text": "List all stores in Seattle that have at least 30 combined units across these SKUs: iphone_17_air, iphone_17, and vision_pro."
}
}
}
{% include copy-curl.html %}
검색 템플릿이 없으면, 고급 집계와 스크립트가 얽힌 복잡한 쿼리는 LLM이 올바른 문법을 생성하지 못해 자주 실패해요. 예를 들어 4(b)단계에서 에이전트를 만들 때 검색 템플릿을 추가하지 않았다면, 앞선 요청은 다음과 비슷한 스크립트 실행 오류를 반환해요:
오류 응답
{: .text-delta}{
"error": {
"root_cause": [
{
"type": "script_exception",
"reason": "runtime error",
"script_stack": [
"for (item in params._source.inventory) { ",
" ^---- HERE"
],
"script": "int total = 0; for (item in params._source.inventory) { if (params.skus.contains(item.sku)) { if (item.qty instanceof Integer || item.qty instanceof Long) { total += (int)item.qty; } else if (item.qty instanceof String) { try { total += Integer.parseInt(it ...",
"lang": "painless",
"position": {
"offset": 42,
"start": 15,
"end": 56
}
}
],
"type": "search_phase_execution_exception",
"reason": "all shards failed",
"phase": "query",
"grouped": true,
"failed_shards": [
{
"shard": 0,
"index": "stores",
"node": "u3NEXA8PS8W8EJcT_9suGg",
"reason": {
"type": "script_exception",
"reason": "runtime error",
"script_stack": [
"for (item in params._source.inventory) { ",
" ^---- HERE"
],
"script": "int total = 0; for (item in params._source.inventory) { if (params.skus.contains(item.sku)) { if (item.qty instanceof Integer || item.qty instanceof Long) { total += (int)item.qty; } else if (item.qty instanceof String) { try { total += Integer.parseInt(it ...",
"lang": "painless",
"position": {
"offset": 42,
"start": 15,
"end": 56
},
"caused_by": {
"type": "null_pointer_exception",
"reason": "Cannot invoke \"Object.getClass()\" because \"callArgs[0]\" is null"
}
}
}
]
},
"status": 400
}
반면 검색 템플릿이 있으면, 에이전트는 적절한 템플릿을 선택하고 파라미터를 채우는 방식으로 복잡한 쿼리를 처리할 수 있어요. LLM은 store_sum_skus 템플릿을 정확히 식별해 사용하고, 템플릿 파라미터(예: city: "Seattle", sku1: "iphone_17_air")를 채운 뒤 중첩 집계와 버킷 셀렉터가 포함된 유효한 쿼리를 생성해요. 응답에는 합산 인벤토리가 ≥ 30개인 매장(S-SEA-002, S-SEA-003)이 포함되어 있어요:
{
"took": 21658,
"timed_out": false,
"terminated_early": true,
"_shards": {
"total": 1,
"successful": 1,
"skipped": 0,
"failed": 0
},
"hits": {
"total": {
"value": 4,
"relation": "eq"
},
"max_score": null,
"hits": []
},
"aggregations": {
"by_store": {
"doc_count_error_upper_bound": 0,
"sum_other_doc_count": 0,
"buckets": [
{
"key": "S-SEA-002",
"doc_count": 1,
"inv": {
"doc_count": 3,
"skus": {
"doc_count": 3,
"sum_qty": {
"value": 34.0
}
}
},
"store": {
"hits": {
"total": {
"value": 1,
"relation": "eq"
},
"max_score": 1.0,
"hits": [
{
"_index": "stores",
"_id": "S-SEA-002",
"_score": 1.0,
"_source": {
"store_id": "S-SEA-002",
"address": {
"city": "Seattle"
},
"name": "Capitol Hill"
}
}
]
}
}
},
{
"key": "S-SEA-003",
"doc_count": 1,
"inv": {
"doc_count": 3,
"skus": {
"doc_count": 3,
"sum_qty": {
"value": 35.0
}
}
},
"store": {
"hits": {
"total": {
"value": 1,
"relation": "eq"
},
"max_score": 1.0,
"hits": [
{
"_index": "stores",
"_id": "S-SEA-003",
"_score": 1.0,
"_source": {
"store_id": "S-SEA-003",
"address": {
"city": "Seattle"
},
"name": "South Lake Union"
}
}
]
}
}
}
]
}
},
"ext": {
"agent_steps_summary": "I have these tools available: [ListIndexTool, IndexMappingTool, query_planner_tool]\nFirst I used: query_planner_tool — qpt.question: \"List all stores in Seattle that have at least a combined total of 30 units across the following SKUs: \\\"iphone_17_air\\\", \\\"iphone_17\\\", and \\\"vision_pro\\\". The location must be Seattle. Sum the inventory counts for only these three SKUs per store and return stores where the sum is greater than or equal to 30.\"; index_name_provided: \"stores\"\nValidation: qpt output is valid JSON; adjusted numeric literals to integers and sizes to integers.",
"memory_id": "-BxpmJkB-5P992SCQ-qU",
"dsl_query":"{\"size\":0.0,\"query\":{\"term\":{\"address.city\":\"Seattle\"}},\"aggs\":{\"by_store\":{\"terms\":{\"field\":\"store_id\",\"size\":200.0},\"aggs\":{\"inv\":{\"nested\":{\"path\":\"inventory\"},\"aggs\":{\"skus\":{\"filter\":{\"terms\":{\"inventory.sku\":[\"iphone_17_air\",\"iphone_17\",\"vision_pro\"]}},\"aggs\":{\"sum_qty\":{\"sum\":{\"field\":\"inventory.qty\"}}}}}},\"keep\":{\"bucket_selector\":{\"buckets_path\":{\"total\":\"inv\>skus\>sum_qty\"},\"script\":{\"source\":\"params.total \>\= 30\"}}},\"store\":{\"top_hits\":{\"size\":1.0,\"_source\":{\"includes\":[\"store_id\",\"name\",\"address.city\"]}}}}}}}"
}
}