Elasticsearch中Painless编程
Painless使用类似于Groovy的Java样式语法。实际上,大多数Painless 脚本也是有效的Groovy,而简单的Groovy脚本通常也是有效的Painless 脚本。
Painless 本质上是Java的子集,具有一些其他脚本语言功能,使脚本更易于编写。但是,有一些重要的差异,尤其是在转化模型上。
使用ANTLR4和ASM库来解析和编译Painless 脚本。Painless 脚本直接编译为Java字节码,并针对标准Java虚拟机执行。本规范使用ANTLR4语法符号描述允许的语法。但是,实际的Painless 语法比此处显示的更为紧凑。Painless 是一种专门用于Elasticsearch的简单安全的脚本语言。它是Elasticsearch的默认脚本语言,可以安全地用于内联和存储脚本。
我们可以在Elasticsearch中可以使用脚本的任何地方使用Painless。Painless 功能包括:
- 快速的性能:Painless 脚本的运行速度比其他脚本快几倍。
- 语法:扩展Java语法以提供Groovy风格的脚本语言功能,使脚本更易于编写。
- 安全性:具有方法调用/字段粒度的细粒度白名单。(有关可用类和方法的完整列表,请参阅《 Painless API参考》)
- 可选类型:变量和参数可以使用显式类型或动态def类型。
- 优化:专为Elasticsearch脚本设计。
让我们通过将一些学术统计数据加载到Elasticsearch索引中来说明Painless的工作原理:
curl -XPUT 'ES_HOST:ES_PORT/academics/student/_bulk?refresh&pretty' -H 'Content-Type: application/json' -d'
{"index":{"_id":1}}
{"first":"Agatha","last":"Christie","base_score":[9,27,1],"target_score":[17,46,0],"grade_point_index":[26,82,1],"born":"1978/08/13"}
{"index":{"_id":2}}
{"first":"Alan","last":"Moore","base_score":[7,54,26],"target_score":[11,26,13],"grade_point_index":[26,82,82],"born":"1976/10/12"}
{"index":{"_id":3}}
{"first":"jiri","Henrik":"Ibsen","base_score":[5,34,36],"target_score":[11,62,42],"grade_point_index":[24,80,79],"born":"1983/01/04"}
{"index":{"_id":4}}
{"first":"William","last":"Blake","base_score":[4,6,15],"target_score":[8,23,15],"grade_point_index":[26,82,82],"born":"1990/02/17"}
{"index":{"_id":5}}
{"first":"Shaun","last":"Tan","base_score":[5,0,0],"target_score":[8,1,0],"grade_point_index":[26,1,0],"born":"1993/06/20"}
{"index":{"_id":6}}
{"first":"Peter","last":"Hitchens","base_score":[0,26,15],"target_score":[11,30,24],"grade_point_index":[26,81,82],"born":"1969/03/20"}
{"index":{"_id":7}}
{"first":"Raymond","last":"Carver","base_score":[7,19,5],"target_score":[3,17,4],"grade_point_index":[26,45,34],"born":"1963/08/10"}
{"index":{"_id":8}}
{"first":"Lee","last":"Child","base_score":[2,14,7],"target_score":[8,42,30],"grade_point_index":[26,82,82],"born":"1992/06/07"}
{"index":{"_id":39}}
{"first":"Joseph","last":"Heller","base_score":[6,30,15],"target_score":[3,30,24],"grade_point_index":[26,60,63],"born":"1984/10/03"}
{"index":{"_id":10}}
{"first":"Harper","last":"Lee","base_score":[3,15,13],"target_score":[6,24,18],"grade_point_index":[26,82,82],"born":"1976/03/17"}
{"index":{"_id":11}}
{"first":"Ian","last":"Fleming","base_score":[3,18,13],"target_score":[6,20,24],"grade_point_index":[26,67,82],"born":"1972/01/30"}'
从Painless访问文档值
可以从名为的地图访问文档值doc
。例如,以下脚本计算学生的总体目标。本示例使用强类型int
和for
循环。
curl -XGET 'ES_HOST:ES_PORT/academics/_search?pretty' -H 'Content-Type: application/json' -d '{
"query": {
"function_score": {
"script_score": {
"script": {
"lang": "painless",
"inline": "int total = 0; for (int i = 0; i < doc[‘base_score’].length; ++i) { total += doc[‘base_score’][i]; } return total;"
}
}
}
}
}'
另外,我们可以使用脚本字段而不是功能分数来做同样的事情:
curl -XGET 'ES_HOST:ES_PORT/academics/_search?pretty' -H 'Content-Type: application/json' -d '{
"query": {
"match_all": {}
},
"script_fields": {
"total_goals": {
"script": {
"lang": "painless",
"inline": "int total = 0; for (int i = 0; i < doc[‘base_score’].length; ++i) { total += doc[‘base_score’][i]; } return total;"
}
}
}
}'
以下示例使用Painless 脚本按学生的姓和名对学生进行排序。使用doc['first'].value
和访问名称doc['last'].value
。
curl -XGET 'ES_HOST:ES_PORT/academics/_search?pretty' -H 'Content-Type: application/json' -d '{
"query": {
"match_all": {}
},
"sort": {
"_script": {
"type": "string",
"order": "asc",
"script": {
"lang": "painless",
"inline": "doc['first.keyword'].value + ' ' + doc['last.keyword'].value"
}
}
}
}'
Painless 更新字段
我们还可以通过访问字段的原始源来轻松更新字段ctx._source.<field-name>
。
首先,我们通过提交以下请求来查看学生的源数据:
curl -XGET 'ES_HOST:ES_PORT/academics/_search?pretty' -H 'Content-Type: application/json' -d '{
"stored_fields": [
"_id",
"_source"
],
"query": {
"term": {
"_id": 1
}
}
}'
为了将学生1的姓氏更改为“ 弗罗斯特 ”,只需将其设置ctx._source.last
为新值即可:
curl -XPOST 'ES_HOST:ES_PORT/academics/student/1/_update?pretty' -H 'Content-Type: application/json' -d '{
"script": {
"lang": "painless",
"inline": "ctx._source.last = params.last",
"params": {
"last": "Frost"
}
}
}'
我们还可以将字段添加到文档中。例如,此脚本添加了一个新字段,其中包含学生的昵称-“ JS ”。
curl -XPOST 'ES_HOST:ES_PORT/academics/student/1/_update?pretty' -H 'Content-Type: application/json' -d '{
"script": {
"lang": "painless",
"inline": "ctx._source.last = params.last; ctx._source.nick = params.nick",
"params": {
"last": "Smith",
"nick": "JS"
}
}
}'
日期
日期字段公开为,ReadableDateTime
因此它们支持getYear
和getDayOfWeek
和 方法getMillis
。例如,以下请求返回每个大学生的出生年份:
curl -XGET 'ES_HOST:ES_PORT/academics/_search?pretty' -H 'Content-Type: application/json' -d '{
"script_fields": {
"birth_year": {
"script": {
"inline": "doc.born.value.year"
}
}
}
}'
常用表达
默认情况下,正则表达式是禁用的,因为它们绕过了Painless对长时间运行且占用大量内存的脚本的保护。更糟糕的是,即使看起来无害的正则表达式也可能具有惊人的性能和堆栈深度行为。它们仍然是一个了不起的强大工具,但太可怕了,无法默认启用。设置script.painless.regex.enabled: true
在elasticsearch.yml
启用它们。
Painless对正则表达式的本机支持具有语法构造:
/pattern/
:模式文字会创建模式。这是Painless 创建图案的唯一方法。/内的模式只是Java正则表达式。
=~
:find操作符返回一个布尔值,如果文本的子序列匹配,则返回true,否则返回false。==~
:match运算符返回一个布尔值,如果文本匹配,则返回true,否则返回false。
使用find运算符(=~)
,我们可以使用其姓氏中的所有“ b ” 来更新所有学术学生:
curl -XPOST 'ES_HOST:ES_PORT/academics/student/_update_by_query?pretty' -H 'Content-Type: application/json' -d '{
"script": {
"lang": "painless",
"inline": "if (ctx._source.last =~ /b/) {ctx._source.last += \"matched\"} else {ctx.op = 'noop'}"
}
}'
使用match运算符(==~)
,我们可以更新所有以辅音开头并以元音结尾的学术学生:
curl -XPOST 'ES_HOST:ES_PORT/academics/student/_update_by_query?pretty' -H 'Content-Type: application/json' -d '{
"script": {
"lang": "painless",
"inline": "if (ctx._source.last ==~ /[^aeiou].*[aeiou]/) {ctx._source.last += \"matched\"} else {ctx.op = 'noop'}"
}
}'
我们可以Pattern.matcher
直接使用来获取Matcher实例,并删除其所有姓氏中的所有元音:
curl -XPOST 'ES_HOST:ES_PORT/academics/student/_update_by_query?pretty' -H 'Content-Type: application/json' -d '{
"script": {
"lang": "painless",
"inline": "ctx._source.last = /[aeiou]/.matcher(ctx._source.last).replaceAll('')"
}
}'
Matcher.replaceAll
只是对Java Matcher的replaceAll方法的调用,因此它支持$ 1和\ 1进行替换:
curl -XPOST 'ES_HOST:ES_PORT/academics/student/_update_by_query?pretty' -H 'Content-Type: application/json' -d '{
"script": {
"lang": "painless",
"inline": "ctx._source.last = /n([aeiou])/.matcher(ctx._source.last).replaceAll('$1')"
}
}'
如果您需要对替换项的更多控制,则可以replaceAll
使用Function<Matcher, String>
构建替换项的CharSequence进行调用。这不支持$ 1或\ 1来访问替换项,因为您已经具有对匹配项的引用,并且可以使用进行匹配m.group(1)
。
注意:Matcher.find
在构建替换函数的内部调用是不礼貌的,并且很可能会破坏替换过程。
以下请求将使大学生的姓氏中的所有元音都大写:
curl -XPOST 'ES_HOST:ES_PORT/academics/student/_update_by_query?pretty' -H 'Content-Type: application/json' -d '{
"script": {
"lang": "painless",
"inline": "ctx._source.last = ctx._source.last.replaceAll(/[aeiou]/, m -> m.group().toUpperCase(Locale.ROOT))"
}
}'
或者我们可以使用CharSequence.replaceFirst
来将第一个元音的姓氏改为大写:
curl -XPOST 'ES_HOST:ES_PORT/academics/student/_update_by_query?pretty' -H 'Content-Type: application/json' -d '{
"script": {
"lang": "painless",
"inline": "ctx._source.last = ctx._source.last.replaceFirst(/[aeiou]/, m -> m.group().toUpperCase(Locale.ROOT))"
}
}'
Painless 调试
Painless没有REPL,虽然很高兴有一天,但它不会告诉我们调试嵌入Elasticsearch的Painless 脚本的全部过程,因为脚本可以访问或“上下文”访问数据非常重要 调试嵌入式脚本的最佳方法是在选择的位置抛出异常。尽管我们可以抛出自己的异常,但Painless的沙箱阻止了我们访问有用的信息,例如对象的类型。因此,Painless具有实用程序方法,Debug.explain
该方法为我们抛出了异常。例如,我们可以_explain
用来探索脚本查询可用的上下文。
curl -XPUT 'ES_HOST:ES_PORT/academics/student/1?refresh&pretty' -H 'Content-Type: application/json' -d '
{"first":"Robert","last":"Williamson","base_score":[3,5,12],"target_Score":[12,15,18],"grade_point_index":[9,14,16]}
'
curl -XPOST 'ES_HOST:ES_PORT/academics/student/1/_explain?pretty' -H 'Content-Type: application/json' -d '{
"query": {
"script": {
"script": "Debug.explain(doc.base_score)"
}
}
}'
这表明的类doc.first
是org.elasticsearch.index.fielddata.ScriptDocValues.Longs
通过响应:
{
"error": {
"type": "script_exception",
"to_string": "[3,5,12]",
"painless_class": "org.elasticsearch.index.fielddata.ScriptDocValues.Longs",
"java_class": "org.elasticsearch.index.fielddata.ScriptDocValues$Longs",
...
},
"status": 500
}
我们可以用同样的伎俩一看就知道_source
是一个LinkedHashMap的中_update
API:
curl -XPOST 'ES_HOST:ES_PORT/academics/student/1/_update?pretty' -H 'Content-Type: application/json' -d '{
"script": "Debug.explain(ctx._source)"
}'
{
"error" : {
"root_cause": ...,
"type": "illegal_argument_exception",
"reason": "failed to execute script",
"caused_by": {
"type": "script_exception",
"to_string": "{grade_point_index:[9,14,16], last=Williamson, target_Score:[12,15,18], first=Robert, base_Score=[3, 5, 12]}",
"painless_class": "LinkedHashMap",
"java_class": "java.util.LinkedHashMap",
...
}
},
"status": 400
}