Elasticsearch中Painless编程

Painless使用类似于Groovy的Java样式语法。实际上,大多数Painless 脚本也是有效的Groovy,而简单的Groovy脚本通常也是有效的Painless 脚本。

Painless 本质上是Java的子集,具有一些其他脚本语言功能,使脚本更易于编写。但是,有一些重要的差异,尤其是在转化模型上。

使用ANTLR4ASM库来解析和编译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。例如,以下脚本计算学生的总体目标。本示例使用强类型intfor循环。

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因此它们支持getYeargetDayOfWeek和 方法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: trueelasticsearch.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.firstorg.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的_updateAPI:

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
}
goaccess: 最佳的可视Web日志分析器
Linux中的15个实用Grep命令示例