简介

ElasticStack在升级到5.0版本之后,带来了一个新的脚本语言,painless。这里说“新的“是相对与已经存在groove而言的。还记得Groove脚本的漏洞吧,Groove脚本开启之后,如果被人误用可能带来各种漏洞,为什么呢,主要是这些外部的脚本引擎太过于强大,什么都能做,用不好或者设置不当就会引起安全风险,基于安全和性能方面,所以elastic.co开发了一个新的脚本引擎,名字就叫Painless,顾名思义,简单安全,无痛使用,和Groove的沙盒机制不一样,Painless使用白名单来限制函数与字段的访问,针对es的场景来进行优化,只做es数据的操作,更加轻量级,速度要快好几倍,并且支持Java静态类型,语法保持Groove类似,还支持Java的lambda表达式。

  • 高性能, painless在es的运行速度是其他语言的数倍
  • 安全。使用白名单来限制函数与字段的访问,避免了可能的安全隐患
  • 可选类型。你可以在脚本当中使用强类型的编程方式或者动态类型的编程方式。
  • 语法。扩展了java的基本语法以兼容groove风格的脚本语言特性,使得plainless易读易写
  • 有针对的优化。这门语言是为elasticsearch专门定制的。

    初始化数据

    我们先输入一串曲棍球的数据到ES当中。 ```json 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”}
  1. <a name="PHHw9"></a>
  2. # painless获取doc的值
  3. 下面的例子中,我们通过function_score::script_score更新每个document的score。其中用到了for循环,和强类型定义int。<br />可以看到运行之后,_score的值,编程了goals值的sum。
  4. ```json
  5. GET hockey/_search
  6. {
  7. "query": {
  8. "function_score": {
  9. "script_score": {
  10. "script": {
  11. "lang": "painless",
  12. "source": """
  13. int total = 0;
  14. for (int i = 0; i < doc['goals'].length; ++i) {
  15. total += doc['goals'][i];
  16. }
  17. return total;
  18. """
  19. }
  20. }
  21. }
  22. }
  23. }

排序

GET hockey/_search
{
  "query": {
    "match_all": {}
  },
  "sort": {
    "_script": {
      "type": "string",
      "order": "asc",
      "script": {
        "lang": "painless",
        "inline": "doc['first.keyword'].value + ' ' + doc['last.keyword'].value"
      }
    }
  }
}

这里需要注意几点:

  • 这里都是_search操作,多个操作之间会形成管道,既query::match_all的输出会作为script_fields或者sort的输入。
  • _search操作中所有的返回值,都可以通过一个map类型变量doc获取。和所有其他脚本语言一样,用[]获取map中的值。这里要强调的是,doc只可以在_search中访问到。在下一节的例子中,你将看到,使用的是ctx。
  • _search操作是不会改变document的值的,即便是script_fields,你只能在当次查询是能看到script输出的值。
  • doc[‘first.keyword’]这样的写法是因为doc[]返回有可能是分词之后的value,所以你想要某个field的完整值时,请使用keyword

    通过painless更新对象值

    在读取值的时候,query的response虽然能够读到一个新值,但这个值并没有写入document当中。要更新值,需要通过_updateAPI。

    单条记录更新

    通过_updateAPI的script,我们可以增加一个新的field:nick: ```json POST hockey/_update/1 { “script”: { “lang”: “painless”, “source”: “””
    ctx._source.last = params.last;
    ctx._source.nick = params.nick
    
    “””, “params”: {
    "last": "gaudreau",
    "nick": "hockey"
    
    } } }

GET hockey/_doc/1

可以更改一个值:
```json
POST hockey/_update/1
{
  "script": {
    "lang": "painless",
    "source": "ctx._source.last = params.last",
    "params": {
      "last": "gaudreau"
    }
  }
}

GET hockey/_doc/1

这里需要注意的是:我们不再使用doc来访问对象,而是用ctx。

批量更新

在大多数应用场景下,我们是很少用到_updateAPI的。(当然,这里不包括你用python, java等语言用for循环多条更新)。在批量更新的场景,我推荐的是_update_by_queryAPI。

POST hockey/_update_by_query?conflicts=proceed
{
  "query": {
    "match_all": {}
  },
  "script": {
    "lang": "painless",
    "source": "ctx._source.last = params.last",
    "params": {
      "last": "lex"
    }
  }
}

POST hockey/_search
{
  "query": {
    "match_all": {}
  }
}

这里需要注意的是:

  • 虽然_update_by_queryAPI在批量更新时,和我们第一个例子很像,先query,再update,通过管道修改全部的值。但这里仍然只能用ctx,而不是doc。如果用doc,会抛出nullpointerException。
  • 另外,在例子中,我用的是match_all query,但实际上你可以用各种query,来划定精确的query范围,只修改你想修改的值。

    删除字段

    POST hockey/_update/1
    {
    "script": {
      "lang": "painless",
      "source": "ctx._source.remove(\"last\");"
    }
    }
    

    Dates

    Date类型的field会被解析成ReadableDateTime。所以它可以支持 getYear, getDayOfWeek等方法。 例如,要取milliseconds,就屌用getMillis方法。下面的例子,取每个球员是哪年出生的: ```json GET hockey/_search { “script_fields”: { “birth_year”: {
    "script": {
      "source": "doc.born.value.year"
    }
    
    } } }

输出 “fields” : { “birth_year” : [ 1996 ] }

<a name="JgIqU"></a>
# 正则表达式
默认情况下禁用正则表达式,因为它们可以避免针对长时间运行和内存耗尽的脚本的无痛保护。 更糟糕的是,即使看起来无害的正则表达式也可能具有惊人的性能和堆栈深度行为。 它们仍然是一个非常强大的工具,但是在默认情况下启用它太可怕了。 要自己启用它们,请在elasticsearch.yml中设置<br />script.painless.regex.enabled:true。<br />语法结构如下:

- / pattern /:模式文字创建模式。 这是在创建模式的唯一方法。
- =〜:find运算符返回一个布尔值,如果文本的子序列匹配,则返回true,否则返回false。
- ==〜:匹配运算符返回一个布尔值,如果文本匹配则返回true,否则返回false。

查找last中有b的情况:
```json
POST hockey/_update_by_query
{
  "script": {
    "lang": "painless",
    "source": """
      if (ctx._source.last =~ /b/) {
        ctx._source.last += "matched";
      } else {
        ctx.op = "noop";
      }
    """
  }
}

使用 Pattern.matcher 直接获取 Matcher 实例 并且删除所有的last names中的元音

POST hockey/_update_by_query
{
  "script": {
  "lang": "painless",
  "source": "ctx._source.last = /[aeiou]/.matcher(ctx._source.last).replaceAll('')"
 }
}

last names中的所有元音字母大写:

POST hockey/_update_by_query
{
  "script": {
    "lang": "painless",
    "source": """
      ctx._source.last = ctx._source.last.replaceAll(/[aeiou]/, m ->
        m.group().toUpperCase(Locale.ROOT))
    """
  }
}