变量和数据类型

Painless中变量可以声明为基本数据类型、引用类型、字符串、void(不返回值)、数组以及动态类型。其支持下面基本类型:
byte, short, char, int, long, float, double, boolean.声明变量与java类似:

  1. int i = 0; double a; boolean g = true;

引用类型也和java类似,除了不支持修饰符,但支持继承。变量通过new关键字初始化,例如声明a作为ArrayList,变量b类型为Map:

  1. ArrayList a = new ArrayList();
  2. Map b;
  3. Map g = [:];
  4. List q = [1, 2, 3];

List和Map与数组类似,除了初始化时不需要new关键字,但它们是引用类型不是数组。
字符串类型可以使用直接量赋值,或使用new关键字初始化。

  1. String a = "a";
  2. String foo = new String("bar");

数组类型支持一维和多维,初始值为null。与引用类型一样,使用new关键字,并为每个维度设置中括号。例如:

  1. int[] x = new int[2];
  2. x[0] = 3;
  3. x[1] = 4;

数组大小可以是显示的,如:int[] a = new int[2]。或者创建时直接赋值:

  1. int[] b = new int[] {1,2,3,4,5};

与Java和Groovy中的数组一样,数组数据类型在声明和初始化时必须有一个基本类型、字符串,甚至是与之关联的动态def类型。
def是Painless支持的动态类型,它所做的是模仿它在运行时分配任何类型的行为。所以,当定义一个变量时:

  1. def a = 1;
  2. def b = "foo";

elasticsearch会推断a是int类型,值为1; b是字符串类型,值为“foo”。
数组也可以通过def声明,举例:

  1. def[][] h = new def[2][2];
  2. def[] f = new def[] {4, "s", 5.7, 2.8C};

条件语句和运算符

如果你熟悉Java,Groovy,或其他高级语言,那么条件和操作符都类似。Painless包含完整的操作符列表,除了它们的优先级和结合性之外,这些操作符与其他高级语言几乎兼容。列表中的大多数操作符都与Java和Groovy语言兼容,操作符优先级可以用括号提升。例如: int t = 5+(5*5)

与其他语言一样,Painless支持if else,但不支持else if或switch。举例:

  1. if (doc['foo'].value = 5) {
  2. doc['foo'].value *= 10;
  3. }
  4. else {
  5. doc['foo'].value += 10;
  6. }

Painless也有Elvis操作符?:,其和Kotlin、Groovy。举例:

  1. x ?: y

如果x不null则评估返回左边表达式,如果x为null评估返回右边表达式。Elvis操作不能和基本类型一起工作,所以这里最好使用def类型。

方法

虽然Painless从Java语言中获得很多强大功能,但并不是Java标准库(Java运行时环境,JRE)中的每个类或方法都可用。Elasticsearch有Painless的类和方法参考列表。列表不仅包括JRE中有效的方法和类,还包括Elasticsearch 和 Painless中可用的方法。

Painless循环

Painless支持while, do…while, for循环,以及控制流语句,如break和continue,这些都是可用。下面示例中for循环与其他语言非常类似:

  1. def total = 0;
  2. for (def i = 0; i < doc['scores'].length; i++) {
  3. total += doc['scores'][i];
  4. }
  5. return total;

使用下面的形式也可以:

  1. def total = 0;
  2. for (def score : doc['scores']) {
  3. total += score;
  4. }
  5. return total;

脚本应用

使用脚本搜索

下面示例使用def演示Painless的动态类型。语句如下:

  1. GET sat/_search
  2. {
  3. "script_fields": {
  4. "some_scores": {
  5. "script": {
  6. "lang": "painless",
  7. "source": "def scores = 0; scores = doc['AvgScrRead'].value + doc['AvgScrWrit'].value; return scores;"
  8. }
  9. }
  10. }
  11. }

在script中可以定义语言类型lang的值,默认为Painless。另外可以定义script的source。

上面示例中使用_search API和script_fields命令。该命令可以创建新的字段存储脚本中定义的scores值,我们简单命名为some_scores,然后在source中定义脚本:

  1. def scores = 0;
  2. scores = doc['AvgScrRead'].value + doc['AvgScrWrit'].value;
  3. return scores;

你注意到上面示例中的脚本没有任何换行符,这是因为Elasticsearch中脚本必须是单行字符串。其实该示例可以不使用Painless实现,也可以使用lucene表达式实现,这里仅为说明脚本的作用。
看下部分返回结果:

  1. "hits" : [
  2. {
  3. "_index" : "sat",
  4. "_type" : "_doc",
  5. "_id" : "8-gOAnEBs8Ix-l1KQQ_6",
  6. "_score" : 1.0,
  7. "fields" : {
  8. "some_scores" : [
  9. 961
  10. ]
  11. }
  12. }
  13. ]

上面脚本在索引中每个文档上执行,上面结果显示fields中通过script_fields命令创建了新的字段some_scores。
下面实现另一个查询,搜索AvgScrRead成绩小于350,并且AvgScrMath成绩大于350,脚本如下:

  1. doc['AvgScrRead'].value < 350 && doc['AvgScrMath'].value > 350

查询语句:

  1. GET sat/_search
  2. {
  3. "query": {
  4. "script": {
  5. "script": {
  6. "source": "doc['AvgScrRead'].value < 350 && doc['AvgScrMath'].value > 350",
  7. "lang": "painless"
  8. }
  9. }
  10. }
  11. }

下面我们查询四个成绩即总分和其他三科成绩,我们使用脚本定义数组保存三科成绩及总成绩:

  1. def sat_scores = [];
  2. def score_names = ['AvgScrRead', 'AvgScrWrit', 'AvgScrMath'];
  3. for (int i = 0; i < score_names.length; i++) {
  4. sat_scores.add(doc[score_names[i]].value)
  5. }
  6. def temp = 0;
  7. for (def score : sat_scores) {
  8. temp += score;
  9. }
  10. sat_scores.add(temp);
  11. return sat_scores;

我们定义 sat_scores 数组存储SAT分数 (AvgScrRead, AvgScrWrit, AvgScrMath) 及计算的总成绩。创建另一个数组scores_names存储SAT中包括成绩的三个字段名称。如果未来索引中字段名称变了,则需要修改数组的值。使用for循环遍历scores_names数组,在sat_scores数组加入相应的值,接着循环sat_scores通过temp遍历累计成绩,最后在sat_scores中增加总成绩。当然这个示例也可以通过一个循环实现。

完成查询示例代码:

  1. GET sat/_search
  2. {
  3. "query": {
  4. "script": {
  5. "script": {
  6. "source": "doc['AvgScrRead'].value < 350 && doc['AvgScrMath'].value > 350",
  7. "lang": "painless"
  8. }
  9. }
  10. },
  11. "script_fields": {
  12. "scores": {
  13. "script": {
  14. "source": "def sat_scores = []; def scores = ['AvgScrRead', 'AvgScrWrit', 'AvgScrMath']; for (int i = 0; i < scores.length; i++) {sat_scores.add(doc[scores[i]].value)} def temp = 0; for (def score : sat_scores) {temp += score;} sat_scores.add(temp); return sat_scores;",
  15. "lang": "painless"
  16. }
  17. }
  18. }
  19. }
  20. 返回结果类似这样:
  21. "hits" : [
  22. {
  23. "_index" : "sat",
  24. "_type" : "_doc",
  25. "_id" : "q-gOAnEBs8Ix-l1KQhAC",
  26. "_score" : 1.0,
  27. "fields" : {
  28. "scores" : [
  29. 349,
  30. 345,
  31. 352,
  32. 1046
  33. ]
  34. }
  35. }
  36. ]

上面查询并没有存储结果,如果存储需要使用_update 或 _update_by_query API更新每个文档。下面我们看看如何更新查询结果。

使用脚本更新

实现之前,我们先创建另一个字段存储SAT分数数组。可以通过Elasticsearch的_update_by_queryAPI增加新的字段All_Scores,一开始使用空数组进行初始化

  1. POST sat/_update_by_query
  2. {
  3. "script": {
  4. "source": "ctx._source.All_Scores = []",
  5. "lang": "painless"
  6. }
  7. }

到此我们给所有文档增加了新的字段,接着需要更新该字段,我们使用脚本更新在字段All_Scores:

  1. def scores = ['AvgScrRead', 'AvgScrWrit', 'AvgScrMath'];
  2. for (int i = 0; i < scores.length; i++) {
  3. ctx._source.All_Scores.add(ctx._source[scores[i]]);
  4. }
  5. def temp = 0;
  6. for (def score : ctx._source.All_Scores) {
  7. temp += score;
  8. }
  9. ctx._source.All_Scores.add(temp);

使用_update或_update_by_queryAPI,不能使用doc变量。Elasticsearch提供了ctx变量和_source文档,可以访问文档中字段。故可以更新All_Scores数组,存储SAT成绩及总成绩。
完整脚本如下:

  1. POST sat/_update_by_query
  2. {
  3. "script": {
  4. "source": "def scores = ['AvgScrRead', 'AvgScrWrit', 'AvgScrMath']; for (int i = 0; i < scores.length; i++) { ctx._source.All_Scores.add(ctx._source[scores[i]])} def temp = 0; for (def score : ctx._source.All_Scores) {temp += score;}ctx._source.All_Scores.add(temp);",
  5. "lang": "painless"
  6. }
  7. }

如果仅更新单个文档,也可以使用类似脚本实现。但需要指明文档的id,下面示例给 UOgOAnEBs8Ix-l1KQhEK文档的AvgScrMath字段值加上10:

  1. POST sat/_update/UOgOAnEBs8Ix-l1KQhEK
  2. {
  3. "script": {
  4. "source": "ctx._source.AvgScrMath += 10",
  5. "lang": "painless"
  6. }
  7. }