简明 Painless 教程
编辑简明 Painless 教程
编辑为了说明 Painless 的工作原理,让我们将一些冰球统计数据加载到 Elasticsearch 索引中。
PUT hockey/_bulk?refresh {"index":{"_id":1}} {"first":"johnny","last":"gaudreau","goals":[9,27,1],"assists":[17,46,0],"gp":[26,82,1],"born":"1993/08/13"} {"index":{"_id":2}} {"first":"sean","last":"monohan","goals":[7,54,26],"assists":[11,26,13],"gp":[26,82,82],"born":"1994/10/12"} {"index":{"_id":3}} {"first":"jiri","last":"hudler","goals":[5,34,36],"assists":[11,62,42],"gp":[24,80,79],"born":"1984/01/04"} {"index":{"_id":4}} {"first":"micheal","last":"frolik","goals":[4,6,15],"assists":[8,23,15],"gp":[26,82,82],"born":"1988/02/17"} {"index":{"_id":5}} {"first":"sam","last":"bennett","goals":[5,0,0],"assists":[8,1,0],"gp":[26,1,0],"born":"1996/06/20"} {"index":{"_id":6}} {"first":"dennis","last":"wideman","goals":[0,26,15],"assists":[11,30,24],"gp":[26,81,82],"born":"1983/03/20"} {"index":{"_id":7}} {"first":"david","last":"jones","goals":[7,19,5],"assists":[3,17,4],"gp":[26,45,34],"born":"1984/08/10"} {"index":{"_id":8}} {"first":"tj","last":"brodie","goals":[2,14,7],"assists":[8,42,30],"gp":[26,82,82],"born":"1990/06/07"} {"index":{"_id":39}} {"first":"mark","last":"giordano","goals":[6,30,15],"assists":[3,30,24],"gp":[26,60,63],"born":"1983/10/03"} {"index":{"_id":10}} {"first":"mikael","last":"backlund","goals":[3,15,13],"assists":[6,24,18],"gp":[26,82,82],"born":"1989/03/17"} {"index":{"_id":11}} {"first":"joe","last":"colborne","goals":[3,18,13],"assists":[6,20,24],"gp":[26,67,82],"born":"1990/01/30"}
从 Painless 访问文档值
编辑文档值可以从名为 doc
的 Map
中访问。
例如,以下脚本计算球员的总进球数。此示例使用强类型 int
和 for
循环。
GET hockey/_search { "query": { "function_score": { "script_score": { "script": { "lang": "painless", "source": """ int total = 0; for (int i = 0; i < doc['goals'].length; ++i) { total += doc['goals'][i]; } return total; """ } } } } }
或者,您可以使用脚本字段而不是函数评分来完成相同的事情。
GET hockey/_search { "query": { "match_all": {} }, "script_fields": { "total_goals": { "script": { "lang": "painless", "source": """ int total = 0; for (int i = 0; i < doc['goals'].length; ++i) { total += doc['goals'][i]; } return total; """ } } } }
以下示例使用 Painless 脚本根据球员的姓名首字母和姓氏首字母组合对球员进行排序。姓名使用 doc['first'].value
和 doc['last'].value
访问。
GET hockey/_search { "query": { "match_all": {} }, "sort": { "_script": { "type": "string", "order": "asc", "script": { "lang": "painless", "source": "doc['first.keyword'].value + ' ' + doc['last.keyword'].value" } } } }
缺失的键
编辑doc['myfield'].value
如果文档中缺少该字段,则会抛出异常。
对于更动态的索引映射,您可以考虑编写一个捕获等式。
if (!doc.containsKey('myfield') || doc['myfield'].empty) { return "unavailable" } else { return doc['myfield'].value }
缺失的值
编辑要检查文档是否缺少值,您可以调用 doc['myfield'].size() == 0
。
使用 Painless 更新字段
编辑您也可以轻松更新字段。您可以访问字段的原始源作为 ctx._source.<field-name>
。
首先,让我们通过提交以下请求查看球员的源数据。
GET hockey/_search { "query": { "term": { "_id": 1 } } }
要将球员 1 的姓氏更改为 hockey
,只需将 ctx._source.last
设置为新值即可。
POST hockey/_update/1 { "script": { "lang": "painless", "source": "ctx._source.last = params.last", "params": { "last": "hockey" } } }
您还可以向文档添加字段。例如,此脚本添加一个包含球员昵称“hockey”的新字段。
POST hockey/_update/1 { "script": { "lang": "painless", "source": """ ctx._source.last = params.last; ctx._source.nick = params.nick """, "params": { "last": "gaudreau", "nick": "hockey" } } }
日期
编辑日期字段显示为 ZonedDateTime
,因此它们支持诸如 getYear
、getDayOfWeek
或例如使用 getMillis
获取自纪元以来的毫秒数之类的函数。要在脚本中使用这些函数,请省略 get
前缀,然后将方法名称其余部分小写。例如,以下代码返回每个冰球运动员的出生年份。
GET hockey/_search { "script_fields": { "birth_year": { "script": { "source": "doc.born.value.year" } } } }
正则表达式
编辑正则表达式默认启用,因为设置 script.painless.regex.enabled
有一个新的选项 limited
,这是默认值。这默认为使用正则表达式,但限制正则表达式的复杂性。看似简单的正则表达式可能具有惊人的性能和堆栈深度行为。但是,它们仍然是一个非常强大的工具。此外,除了 limited
之外,该设置还可以设置为之前的 true
,这将启用正则表达式而不会限制它们。要自己启用它们,请在 elasticsearch.yml
中设置 script.painless.regex.enabled: true
。
Painless 对正则表达式的原生支持具有语法结构。
-
/pattern/
:模式字面量创建模式。这是在 painless 中创建模式的唯一方法。/
中的模式只是 Java 正则表达式。有关详细信息,请参阅 模式标志。 -
=~
:查找运算符返回一个boolean
值,如果文本的子序列匹配,则为true
,否则为false
。 -
==~
:匹配运算符返回一个boolean
值,如果文本匹配,则为true
,如果不匹配,则为false
。
使用查找运算符 (=~
),您可以更新姓氏中包含“b”的所有冰球运动员。
POST hockey/_update_by_query { "script": { "lang": "painless", "source": """ if (ctx._source.last =~ /b/) { ctx._source.last += "matched"; } else { ctx.op = "noop"; } """ } }
使用匹配运算符 (==~
),您可以更新所有以辅音开头并以元音结尾的冰球运动员。
POST hockey/_update_by_query { "script": { "lang": "painless", "source": """ if (ctx._source.last ==~ /[^aeiou].*[aeiou]/) { ctx._source.last += "matched"; } else { ctx.op = "noop"; } """ } }
您可以直接使用 Pattern.matcher
获取 Matcher
实例并删除其所有姓氏中的所有元音。
POST hockey/_update_by_query { "script": { "lang": "painless", "source": "ctx._source.last = /[aeiou]/.matcher(ctx._source.last).replaceAll('')" } }
Matcher.replaceAll
只是对 Java 的 Matcher
的 replaceAll 方法的调用,因此它支持 $1
和 \1
用于替换。
POST hockey/_update_by_query { "script": { "lang": "painless", "source": "ctx._source.last = /n([aeiou])/.matcher(ctx._source.last).replaceAll('$1')" } }
如果您需要更多地控制替换,您可以使用 Function<Matcher, String>
在 CharSequence
上调用 replaceAll
,该函数构建替换。这不支持 $1
或 \1
来访问替换,因为您已经拥有对匹配器的引用,并且可以使用 m.group(1)
获取它们。
在构建替换的函数内调用 Matcher.find
是不合适的,并且可能会破坏替换过程。
这将使冰球运动员姓氏中的所有元音大写。
POST hockey/_update_by_query { "script": { "lang": "painless", "source": """ ctx._source.last = ctx._source.last.replaceAll(/[aeiou]/, m -> m.group().toUpperCase(Locale.ROOT)) """ } }
或者,您可以使用 CharSequence.replaceFirst
使其姓氏中的第一个元音大写。
POST hockey/_update_by_query { "script": { "lang": "painless", "source": """ ctx._source.last = ctx._source.last.replaceFirst(/[aeiou]/, m -> m.group().toUpperCase(Locale.ROOT)) """ } }
注意:上面所有 _update_by_query
示例实际上都需要一个 query
来限制它们拉回的数据。虽然您可以使用 脚本查询,但它不如使用任何其他查询高效,因为脚本查询无法使用倒排索引来限制它们必须检查的文档。