# Painless 脚本语言

原文链接 : [https://www.elastic.co/guide/en/elasticsearch/reference/5.3/modules-scripting-painless.html](https://www.elastic.co/guide/en/elasticsearch/reference/5.3/modules-scripting-painless.html)

译文链接 : [http://www.apache.wiki/pages/viewpage.action?pageId=10027215](http://www.apache.wiki/pages/viewpage.action?pageId=10027215)

贡献者 : [@琴剑蓝天](http://www.apache.wiki/users/viewuserprofile.action?username=xujie)，[ApacheCN](/display/~apachecn)，[Apache中文网](/display/~apachechina)

_Painless_ 脚本语言是新的，仍然被标记为实验性语言。 如果需要，语法或API可能会以非向后兼容的方式在将来更改。

_Painless_ 是默认情况下 _Elasticsearch_ 中提供的一种简单，安全的脚本语言。 它专门设计用于 _Elasticsearch_，可以安全地使用内联和存储的脚本，默认情况下启用。 

_Painless_ 语法类似于 [_Groovy_](http://groovy-lang.org/index.html)。

您可以在任何地方使用 _Painless_ 来在 _Elasticsearch_ 中使用脚本。 如果您不设置lang参数是默认值，但如果要显式，则可以将lang参数设置为 _Painless_ 。

## _Painless_ 功能

*   性能快：比替代品快几倍。
*   安全：具有方法调用/字段粒度的细粒度白名单。 有关可用类和方法的完整列表，请参见[附录A“_Painless_ API参考”](https://www.elastic.co/guide/en/elasticsearch/reference/5.3/painless-api-reference.html)。
*   可选输入：变量和参数可以使用显式类型或动态def类型。
*   语法：使用Groovy的子集扩展Java的语法，以方便使用。 请参阅[语法概述](https://www.elastic.co/guide/en/elasticsearch/reference/5.3/modules-scripting-painless-syntax.html)。
*   优化：专为 _Elasticsearch_ 脚本而设计。

## _Painless_ 示例

为了说明 _Painless_ 如何工作，我们将一些曲棍球统计数据加载到一个Elasticsearch索引中：

```
PUT hockey/player/_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 获取文档值
_

可以以Map命名的文档访问文档值。 

例如，以下脚本计算玩家的总目标。 此示例使用强类型的int和for循环。

```
GET hockey/_search
{
  "query": {
    "function_score": {
      "script_score": {
        "script": {
          "lang": "painless",
          "inline": "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",
        "inline": "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",
        "inline": "doc['first.keyword'].value + ' ' + doc['last.keyword'].value"
      }
    }
  }
}
```

### 用 _Painless 更新字段_

_您也可以轻松更新字段。 您访问一个字段的原始源为 `ctx._source.&lt;field-name&gt;`.
_

_首先，我们通过提交以下请求来查看播放器的源数据：_

```
GET hockey/_search
{
  "stored_fields": [
    "_id",
    "_source"
  ],
  "query": {
    "term": {
      "_id": 1
    }
  }
}
```

__要将玩家1的姓氏更改为曲棍球，只需将 `ctx._source.last` 设置为新值即可：__

```
POST hockey/player/1/_update
{
  "script": {
    "lang": "painless",
    "inline": "ctx._source.last = params.last",
    "params": {
      "last": "hockey"
    }
  }
}
```

__您还可以向文档添加字段。 例如，此脚本添加了一个包含播放器昵称，曲棍球的新字段__

```
POST hockey/player/1/_update
{
  "script": {
    "lang": "painless",
    "inline": "ctx._source.last = params.last; ctx._source.nick = params.nick",
    "params": {
      "last": "gaudreau",
      "nick": "hockey"
    }
  }
}
```

### 正则表达式

日期与常规值有所不同。 这是一个返回每个玩家诞生年份的例子：

```
GET hockey/_search
{
  "script_fields": {
    "birth_year": {
      "script": {
        "inline": "doc.born.date.year"
      }
    }
  }
}
```

这里的关键是不能直接编入 _`doc.born`_，就像您正常的字段，你必须调用 _doc.born.date_ 来获取一个 _ReadableDateTime_ 。 从那里可以调用 _getYear_ 和 _getDayOfWeek_ 等方法。 在上面的例子中，_getYear()_ 的一个快捷方式。 

如果日期字段是列表，那么日期将始终返回第一个日期。 要访问所有日期，请使用 _dates_ 而不是 _date_。

### 正则表达式

默认情况下，正则表达式被禁用，因为它们规避了 _Painless_ 对长时间运行和内存饥饿脚本的保护。 更糟糕的是，即使 _Painless_ 的正则表达式也可以具有令人吃惊的性能和堆栈深度行为。 它们仍然是一个惊人的强大工具，但是在默认情况下太可怕了。 要使他们自己设置_script.painless.regex.enabled：true_ 在elasticsearch.yml中。 我们非常希望有一个安全的替代实现，默认情况下可以启用，所以检查这个空间以备以后的开发！

Painless对正则表达式的本机支持具有语法结构：

*   / pattern /：模式文字创建模式。 这是创造无痛模式的唯一途径。 `/`中的模式只是[Java正则表达式](http://docs.oracle.com/javase/8/docs/api/java/util/regex/Pattern.html)。 有关更多信息，请参阅“[模式标志](https://www.elastic.co/guide/en/elasticsearch/reference/5.3/modules-scripting-painless-syntax.html#modules-scripting-painless-regex-flags)”一节。
*   =〜：find运算符返回一个布尔值，如果文本的子序列匹配则为true，否则为false。
*   ==〜：匹配运算符返回一个布尔值，如果文本匹配则返回true，否则返回false。

 使用 _find_ 操作符（=〜），您可以用“b”更新所有曲棍球选手：

```
POST hockey/player/_update_by_query
{
  "script": {
    "lang": "painless",
    "inline": "if (ctx._source.last =~ /b/) {ctx._source.last += \"matched\"} else {ctx.op = 'noop'}"
  }
}
```

使用匹配运算符（==〜），您可以更新名称以辅音开头的所有曲棍球运动员，并以元音结尾：

```
POST hockey/player/_update_by_query
{
  "script": {
    "lang": "painless",
    "inline": "if (ctx._source.last ==~ /[^aeiou].*[aeiou]/) {ctx._source.last += \"matched\"} else {ctx.op = 'noop'}"
  }
}
```

您可以直接使用 _Pattern.matcher_ 获取 _Matcher_ 实例，并删除其所有姓氏中的所有元音：

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

_Matcher.replaceAll_ 只是调用 _Java Matcher replaceAll_ 方法，所以它支持 _$1_ 和 _\1_ 替换：

```
POST hockey/player/_update_by_query
{
  "script": {
    "lang": "painless",
    "inline": "ctx._source.last = /n([aeiou])/.matcher(ctx._source.last).replaceAll('$1')"
  }
}
```

如果需要更多的替代控件，您可以使用构建替换的 _`Function&lt;Matcher, String&gt;`_ 调用 _CharAequence_ 上的 _replaceAll_ 。 这不支持_$1_ 和 _\1_ 访问替换，因为您已经有了匹配器的引用，可以使用 _m.group(1) 获取它们。_

在构建替换的函数内调用 Matcher.find 是粗鲁的，并且可能会破坏替换过程。

_这将使曲棍球运动员姓氏中的所有元音大写:_

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

_或者您可以使用 CharSequence.replaceFirst 将其第一个元音以大写字母表示：_

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

_注意：上面所有的 _update_by_query 示例可能真的可以用查询来限制他们拉回的数据。 虽然您可以使用脚本查询，但它不会像使用任何其他查询一样有效，因为脚本查询不能使用反向索引来限制他们必须检查的文档。_

###  _painless_ 调度功能

_Painless_ 使用接收器，名称和方法进行方法调度。例如，通过首先获取 _s_ 的类，然后用两个参数查找方法 _foo_ 来解决 _s.foo(a,b)_ 。这与使用参数的[运行时类型](https://en.wikipedia.org/wiki/Multiple_dispatch)的Groovy和使用编译时类型的参数的Java不同。 

这样做的结果是，_Painless_ 不支持像Java这样的重载方法，当从Java标准库中将类列入白名单时，会导致一些麻烦。例如，在Java和Groovy中，Matcher有两种方法：_group(int)_ 和 _group(String)_。_Painless_ 无法将这两种方法列入白名单，因为它们具有相同的名称和相同数量的参数。因此，它具有 _group__(int)_ 和 _namedGroup__(String)_。 

我们对这种不同的调度方法有几个理由：  

1.  它使 _def_ 类型的操作更简单，可能更快。使用接收器，名称和数量意味着当 _Painless_ 看到对def对象的调用时，它可以调度适当的方法，而不必对参数类型进行昂贵的比较。对于使用def类型参数的调用也是如此。
2.  它保持一致。如果涉及到def类型的参数和Java，否则 _Painless_ 的行为就像Groovy一样真的很奇怪。它总是像Groovy一样慢一点。
3.  它保持 _Painless_ 可维护。添加Java或Groovy的方法调度感觉就像添加了一大堆复杂性，使维护和其他改进更加困难。