Painless execute API 允许执行任意脚本并返回结果。请注意的是:这个 API 是新的,请求方式及响应在未来可能会有所改变。
请求
POST /_scripts/painless/_execute
描述
使用此 API 构建和测试脚本,例如在为 runtime field 定义脚本时。 此 API 需要很少的依赖项,如果你没有在集群上写入文档的权限,则特别有用。
API 使用多个上下文(context),它们控制脚本的执行方式、运行时可用的变量以及返回类型。
每个上下文都需要一个脚本,但其他参数取决于您用于该脚本的上下文。
Context
Context 也即是上下文的意思。Context 控制了脚本如何执行,在运行时可以使用哪些变量,返回类型是什么。
Plainless test context
这是默认的 context。painless_test 上下文按原样执行脚本,并且不添加任何特殊参数。 唯一可用的变量是 params,可用于访问用户定义的值。 脚本的结果总是转换为字符串。 如果未指定上下文,则默认使用此上下文。
例子
我们发送如下的请求:
POST /_scripts/painless/_execute
{
"script": {
"source": "params.count / params.total",
"params": {
"count": 100.0,
"total": 1000.0
}
}
}
上面的响应结果为:
{
"result": "0.1"
}
在上面,我们通过 params 把参数传入到 source 里进行执行,并返回结果。
Filter context
Filter 上下文执行脚本的方式就像在脚本查询中执行脚本一样。 为了进行测试,必须提供一个文档,以便可以在内存中对其进行临时索引并可以从脚本中对其进行访问。 更准确地说,此类文档的 _source,存储的字段和 doc 值可用于正在测试的脚本。
可以在 context_setup 中为过滤器上下文指定以下参数:
- document:包含将在内存中临时建立索引并可以从脚本访问的文档。
- index:索引名称,其中包含与要编制索引的文档兼容的映射。
例子
PUT /my-index-000001
{
"mappings": {
"properties": {
"field": {
"type": "keyword"
}
}
}
}
POST /_scripts/painless/_execute
{
"script": {
"source": "doc['field'].value.length() <= params.max_length",
"params": {
"max_length": 4
}
},
"context": "filter",
"context_setup": {
"index": "my-index-000001",
"document": {
"field": "four"
}
}
}
响应:
{
"result" : true
}
在上面,我们首先定义了一个叫做 my-index-000001 的索引。由于我们使用了 filter 上下文,我们通过 context_setup 把相应的 index 及 document 进行定义。
Score context
Ccore 上下文将执行脚本,就像在 function_score 查询中的 script_score 函数中执行脚本一样。
可以在 context_setup 中为得分上下文指定以下参数:
- document:包含将在内存中临时建立索引并可以从脚本访问的文档。
- index:索引名称,其中包含与要编制索引的文档兼容的映射。
- query:如果在脚本中使用了 _score,则 query 可以指定它将用于计算分数。
例子
PUT /my-index-000002
{
"mappings": {
"properties": {
"field": {
"type": "keyword"
},
"rank": {
"type": "long"
}
}
}
}
POST /_scripts/painless/_execute
{
"script": {
"source": "doc['rank'].value / params.max_rank",
"params": {
"max_rank": 5.0
}
},
"context": "score",
"context_setup": {
"index": "my-index-000002",
"document": {
"rank": 4
}
}
}
响应:
{
"result" : 0.8
}
Field contexts
字段上下文将脚本视为在搜索查询的 runtime_mappings 部分中运行。 你可以使用字段上下文来测试不同字段类型的脚本,然后将这些脚本包含在它们受支持的任何位置,例如运行时字段(runtime field)。
根据要返回的数据类型选择字段上下文。
boolean_field
当你想从脚本评估中返回真值或假值时,请使用 boolean_field 字段上下文。 Boolean fields 接受 true 和 false,但也可以接受被解释为真或假的字符串。
假设你拥有有史以来排名前 100 的科幻小说的数据。 你想要编写返回布尔响应的脚本,例如书籍是否超过特定页数,或者书籍是否在特定年份之后出版。
考虑你的数据结构如下:
PUT /my-index-000001
{
"mappings": {
"properties": {
"name": {
"type": "keyword"
},
"author": {
"type": "keyword"
},
"release_date": {
"type": "date"
},
"page_count": {
"type": "double"
}
}
}
}
然后,你可以在 boolean_field 上下文中编写一个脚本,指示一本书是否在 1972 年之前出版:
POST /_scripts/painless/_execute
{
"script": {
"source": """
emit(doc['release_date'].value.year < 1972);
"""
},
"context": "boolean_field",
"context_setup": {
"index": "my-index-000001",
"document": {
"name": "Dune",
"author": "Frank Herbert",
"release_date": "1965-06-01",
"page_count": 604
}
}
}
因为 Dune 是在 1965 年发布的,所以结果返回 true:
{
"result" : [
true
]
}
同样,你可以编写一个脚本来确定作者的名字是否超过一定数量的字符。 以下脚本对作者字段进行操作,以确定作者的名字是否包含至少一个字符,但少于五个字符:
POST /_scripts/painless/_execute
{
"script": {
"source": """
int space = doc['author'].value.indexOf(' ');
emit(space > 0 && space < 5);
"""
},
"context": "boolean_field",
"context_setup": {
"index": "my-index-000001",
"document": {
"name": "Dune",
"author": "Frank Herbert",
"release_date": "1965-06-01",
"page_count": 604
}
}
}
因为 Frank 是五个字符,所以脚本评估的响应返回 false:
{
"result" : [
false
]
}
date_time
有几个选项可用于在 Painless 中使用日期时间。 在此示例中,你将根据某位作者的发行日期和该作者的写作速度来估计某位作者何时开始写作。 该示例做了一些假设,但展示了编写一个在日期上运行的脚本,同时包含其他信息。
将以下字段添加到你的索引映射以开始:
PUT /my-index-000001
{
"mappings": {
"properties": {
"name": {
"type": "keyword"
},
"author": {
"type": "keyword"
},
"release_date": {
"type": "date"
},
"page_count": {
"type": "long"
}
}
}
}
下面的脚本做出了令人难以置信的假设,即在写一本书时,作者只写每一页而不做研究或修改。 此外,该脚本假定写一页所需的平均时间为八小时。
该脚本检索 author 并做出另一个奇妙的假设,即根据作者感知的写作速度除以或乘以 pageTime 值(又一个疯狂的假设)。
该脚本从 pageTime 乘以 page_count 的计算中减去发布日期值(以毫秒为单位),以确定作者开始写作本书的大致时间(基于许多假设)。
POST /_scripts/painless/_execute
{
"script": {
"source": """
String author = doc['author'].value;
long pageTime = 28800000;
if (author == 'Robert A. Heinlein') {
pageTime /= 2;
} else if (author == 'Alastair Reynolds') {
pageTime *= 2;
}
emit(doc['release_date'].value.toInstant().toEpochMilli() - pageTime * doc['page_count'].value);
"""
},
"context": "date_field",
"context_setup": {
"index": "my-index-000001",
"document": {
"name": "Revelation Space",
"author": "Alastair Reynolds",
"release_date": "2000-03-15",
"page_count": 585
}
}
}
上面的 pageTime 为 28800000 毫秒,相当于 8 个小时。Robert A. Heinlein 比 Alastair Reynolds 要快四倍。
在这种情况下,作者是 Alastair Reynolds。 根据 2000 年 3 月 15 日的发布日期,该脚本计算出作者于 1999 年 2 月 19 日开始撰写《启示录空间》。仅用一年多的时间就完成了一本 585 页的书,令人印象深刻!
{
"result" : [
"1999-02-19T00:00:00.000Z"
]
}
double_field
对 double 类型的数值数据使用 double_field 上下文。 例如,假设你有传感器数据,其中包含一个值为 5.6 的 voltage 字段。 在索引数百万个文档后,你发现型号为 QVKC92Q 的传感器报告的电压低于 1.7 倍。 你可以使用运行时字段(runtime field)来修复它,而不需要重新索引你的数据。
你需要将此值相乘,但仅限于与特定型号匹配的传感器。
将以下字段添加到你的索引映射。 voltage 是 measures 对象的一个子字段。
PUT my-index-000001
{
"mappings": {
"properties": {
"@timestamp": {
"type": "date"
},
"model_number": {
"type": "keyword"
},
"measures": {
"properties": {
"voltage": {
"type": "double"
}
}
}
}
}
}
以下脚本匹配 model_number 等于 QVKC92Q 的任何文档,然后将电压值乘以 1.7。 当你要选择特定文档并仅对与指定条件匹配的值进行操作时,此脚本很有用。
POST /_scripts/painless/_execute
{
"script": {
"source": """
if (doc['model_number'].value.equals('QVKC92Q'))
{emit(1.7 * params._source['measures']['voltage']);}
else{emit(params._source['measures']['voltage']);}
"""
},
"context": "double_field",
"context_setup": {
"index": "my-index-000001",
"document": {
"@timestamp": 1516470094000,
"model_number": "QVKC92Q",
"measures": {
"voltage": 5.6
}
}
}
}
结果包括计算电压,该电压是通过将原始值 5.6 乘以 1.7 确定的:
{
"result" : [
9.52
]
}
geo_point_field
Geo-point 字段接受纬度-经度值。 你可以通过多种方式定义 geo-point 字段,并在脚本的文档中包含纬度和经度值。
如果你已经有一个已知的 geo-point,那么在索引映射中清楚地说明 lat 和 lon 的位置会更简单。
PUT /my-index-000001/
{
"mappings": {
"properties": {
"lat": {
"type": "double"
},
"lon": {
"type": "double"
}
}
}
}
然后,你可以使用 geo_point_field 运行时字段(runtime field)上下文来运用 lat 及 lon 编写一个脚本。
POST /_scripts/painless/_execute
{
"script": {
"source": """
emit(doc['lat'].value, doc['lon'].value);
"""
},
"context": "geo_point_field",
"context_setup": {
"index": "my-index-000001",
"document": {
"lat": 41.12,
"lon": -71.34
}
}
}
因为你使用的是 geo-point 类型,所以响应包括格式为坐标的结果:
{
"result" : [
{
"coordinates" : [
-71.34000004269183,
41.1199999647215
],
"type" : "Point"
}
]
}
注意:Geo-point 字段的 emit 函数接受两个参数,lat 在前,log 在后,但输出 GeoJSON 格式将坐标排序为 [ lon, lat ]。
ip_field
ip_field 上下文对于包含 ip 类型的 IP 地址的数据很有用。 例如,假设你有一个来自 Apache 日志的消息字段。 此字段包含 IP 地址,还包含你不需要的其他数据。
你可以将 message 字段作为通配符(wildcard)添加到索引映射中,以接受你想要放入该字段的几乎任何数据。
PUT /my-index-000001/
{
"mappings": {
"properties": {
"message": {
"type": "wildcard"
}
}
}
}
然后,你可以使用从消息字段中提取结构化字段的 grok 模式定义运行时脚本。
该脚本匹配 %{COMMONAPACHELOG} 日志模式,该模式了解 Apache 日志的结构。 如果模式匹配,脚本会发出与 IP 地址匹配的值。 如果模式不匹配(clientip != null),脚本只会返回字段值而不会崩溃。
POST /_scripts/painless/_execute
{
"script": {
"source": """
String clientip=grok('%{COMMONAPACHELOG}').extract(doc["message"].value)?.clientip;
if (clientip != null) emit(clientip);
"""
},
"context": "ip_field",
"context_setup": {
"index": "my-index-000001",
"document": {
"message": "40.135.0.0 - - [30/Apr/2020:14:30:17 -0500] \"GET /images/hm_bg.jpg HTTP/1.0\" 200 24736"
}
}
}
响应仅包括 IP 地址,忽略 message 字段中的所有其他数据。
{
"result": [
"40.135.0.0"
]
}
keyword_field
keyword 字段通常用于排序、聚合和术语级别的查询。
假设你有一个时间戳。 你想根据该值计算星期几并返回它,例如 Thursday。 以下请求将日期类型的 @timestamp 字段添加到索引映射:
PUT /my-index-000001
{
"mappings": {
"properties": {
"@timestamp": {
"type": "date"
}
}
}
}
要根据你的时间戳返回等效的星期几,您可以在 keyword_field 运行时字段上下文中创建一个脚本:
POST /_scripts/painless/_execute
{
"script": {
"source": """
emit(doc['@timestamp'].value.dayOfWeekEnum.getDisplayName(TextStyle.FULL, Locale.ROOT));
"""
},
"context": "keyword_field",
"context_setup": {
"index": "my-index-000001",
"document": {
"@timestamp": "2020-04-30T14:31:43-05:00"
}
}
}
该脚本对为 @timestamp 字段提供的值进行操作,以计算并返回星期几:
{
"result" : [
"Thursday"
]
}
long_field
假设你有 measures 对象的传感器数据。 此对象包含一个 start 和 end 字段,你想要计算这些值之间的差异。
以下请求将 measures 对象添加到具有两个字段的映射中,这两个字段都是 long 类型:
PUT /my-index-000001/
{
"mappings": {
"properties": {
"measures": {
"properties": {
"start": {
"type": "long"
},
"end": {
"type": "long"
}
}
}
}
}
}
然后,你可以定义一个脚本,为 start 和 end 字段分配值并对其进行操作。 以下脚本从 measures 对象中提取 end 字段的值并从 start 字段中减去它:
POST /_scripts/painless/_execute
{
"script": {
"source": """
emit(doc['measures.end'].value - doc['measures.start'].value);
"""
},
"context": "long_field",
"context_setup": {
"index": "my-index-000001",
"document": {
"measures": {
"voltage": "4.0",
"start": "400",
"end": "8625309"
}
}
}
}
响应包括脚本评估的计算值:
{
"result" : [
8624909
]
}
composite_field
假设你有一个原始 message 字段的日志记录数据,你希望将其拆分为多个可以单独访问的子字段。
以下请求将 message 字段添加到 keyword 类型的映射中:
PUT /my-index-000001/
{
"mappings": {
"properties": {
"message": {
"type" : "keyword"
}
}
}
}
然后,你可以定义一个脚本,使用 grok 函数将此类 message 字段拆分为子字段:
POST /_scripts/painless/_execute
{
"script": {
"source": "emit(grok(\"%{COMMONAPACHELOG}\").extract(doc[\"message\"].value));"
},
"context": "composite_field",
"context_setup": {
"index": "my-index-000001",
"document": {
"timestamp":"2020-04-30T14:31:27-05:00",
"message":"252.0.0.0 - - [30/Apr/2020:14:31:27 -0500] \"GET /images/hm_bg.jpg HTTP/1.0\" 200 24736"
}
}
}
响应包括脚本发出的值:
{
"result" : {
"composite_field.timestamp" : [
"30/Apr/2020:14:31:27 -0500"
],
"composite_field.auth" : [
"-"
],
"composite_field.response" : [
"200"
],
"composite_field.ident" : [
"-"
],
"composite_field.httpversion" : [
"1.0"
],
"composite_field.verb" : [
"GET"
],
"composite_field.bytes" : [
"24736"
],
"composite_field.clientip" : [
"252.0.0.0"
],
"composite_field.request" : [
"/images/hm_bg.jpg"
]
}
}
参考:
【1】Painless execute API | Painless Scripting Language [8.4] | Elastic