Skip to main content
 首页 » 编程设计

Elasticsearch Painless Script入门教程

2022年07月19日153grandyang

Elasticsearch Painless Script入门教程

前面几篇文章已经陆续提到了Elasticsearch 脚本,但总感觉不够系统,本文带你系统地学习下Painless Script。

Painless 脚本介绍

自Elasticsearch 5.x 引入Painless,使得Elasticsearch拥有了安全、可靠、高性能脚本的解决方案。Painless是Elastic开发并做了专门的优化,相较之前的脚本更快、安全、易使用、可靠。

Painless脚本的目标是使编写脚本对用户来说无痛,特别是对于来自Java或Groovy环境的用户。可能你还不熟悉Elasticsearch脚本,让我们从基础开始。

下面我们简要介绍Painless,并示例展示如何使用脚本搜索、更新数据。

1.1 变量和数据类型

Painless中变量可以声明为基本数据类型、引用类型、字符串、void(不返回值)、数组以及动态类型。其支持下面基本类型:

byte, short, char, int, long, float, double, boolean.声明变量与java类似:

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

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

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

List和Map与数组类似,除了初始化时不需要new关键字,但它们是引用类型不是数组。

字符串类型可以使用直接量赋值,或使用new关键字初始化。

String a = "a";   
String foo = new String("bar");   

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

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

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

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

与Java和Groovy中的数组一样,数组数据类型在声明和初始化时必须有一个基本类型、字符串,甚至是与之关联的动态def类型。

def是Painless支持的动态类型,它所做的是模仿它在运行时分配任何类型的行为。所以,当定义一个变量时:

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

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

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

有了这些变量,让我们来看看条件语句和运算符。

1.2. 条件语句和运算符

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

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

if (doc['foo'].value = 5) {   
    doc['foo'].value *= 10; 
}  
else {   
    doc['foo'].value += 10; 
} 

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

x ?: y   

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

1.3. 方法

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

1.4 Painless循环

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

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

使用下面的形式也可以:

def total = 0;   
for (def score : doc['scores']) {   
    total += score; 
} 
return total;   

好了,我们已经看了Painless语言的基本规范,下面开始通过脚本实现一些查询。

2. 脚本应用

2.1. 准备数据

示例数据sat考试分数,可以在这里下载sat.json文件,在命令行执行下列命令:

curl -H 'Content-Type: application/json' -XPOST 'localhost:9200/sat/_doc/_bulk?pretty' --data-binary @sat.json 

批量导入json文件需要有两点注意:
1.每一行都是一个json对象;
2.第一行包括索引名称,第二行是插入数据。

如果需要登录,则需要在命令中加入-u username:password参数。

2.2. 使用脚本搜索

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

GET sat/_search 
{ 
  "script_fields": { 
    "some_scores": { 
      "script": { 
        "lang": "painless", 
        "source": "def scores = 0; scores = doc['AvgScrRead'].value + doc['AvgScrWrit'].value; return scores;" 
      } 
    } 
  } 
} 

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

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

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

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

看下部分返回结果:

"hits" : [ 
    { 
        "_index" : "sat", 
        "_type" : "_doc", 
        "_id" : "8-gOAnEBs8Ix-l1KQQ_6", 
        "_score" : 1.0, 
        "fields" : { 
            "some_scores" : [ 
                961 
            ] 
        } 
    } 
] 

上面脚本在索引中每个文档上执行,上面结果显示fields中通过script_fields命令创建了新的字段some_scores

下面实现另一个查询,搜索AvgScrRead成绩小于350,并且AvgScrMath成绩大于350,脚本如下:

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

查询语句:

GET sat/_search 
{ 
  "query": { 
    "script": { 
      "script": { 
        "source": "doc['AvgScrRead'].value < 350 && doc['AvgScrMath'].value > 350", 
        "lang": "painless" 
      } 
    } 
  } 
} 

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

def sat_scores = [];   
def score_names = ['AvgScrRead', 'AvgScrWrit', 'AvgScrMath'];   
for (int i = 0; i < score_names.length; i++) {   
    sat_scores.add(doc[score_names[i]].value) 
} 
def temp = 0;   
for (def score : sat_scores) {   
    temp += score; 
} 
sat_scores.add(temp);   
return sat_scores;   

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

完成查询示例代码:

GET sat/_search 
{ 
  "query": { 
    "script": { 
      "script": { 
        "source": "doc['AvgScrRead'].value < 350 && doc['AvgScrMath'].value > 350", 
        "lang": "painless" 
      } 
    } 
  }, 
  "script_fields": { 
    "scores": { 
      "script": { 
        "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;", 
        "lang": "painless" 
      } 
    } 
  } 
} 

返回结果类似这样:

"hits" : [ 
    { 
        "_index" : "sat", 
        "_type" : "_doc", 
        "_id" : "q-gOAnEBs8Ix-l1KQhAC", 
        "_score" : 1.0, 
        "fields" : { 
            "scores" : [ 
                349, 
                345, 
                352, 
                1046 
            ] 
        } 
    } 
] 

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

2.3. 使用脚本更新

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

POST sat/_update_by_query 
{ 
  "script": { 
    "source": "ctx._source.All_Scores = []", 
    "lang": "painless" 
  } 
} 

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

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);   

使用_update_update_by_queryAPI,不能使用doc变量。Elasticsearch提供了ctx变量和_source文档,可以访问文档中字段。故可以更新All_Scores数组,存储SAT成绩及总成绩。

完整脚本如下:

POST sat/_update_by_query 
{ 
  "script": { 
    "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);", 
    "lang": "painless" 
  } 
} 

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

POST sat/_update/UOgOAnEBs8Ix-l1KQhEK 
{ 
  "script": { 
    "source": "ctx._source.AvgScrMath += 10", 
    "lang": "painless" 
  } 
}   

3. 总结

本文我们介绍了Elasticsearch中Painless脚本语言的基本规范,通过一些示例说明如何使用。使用了Painless API方法,如HashMap和循环,同时也看了一些脚本查询、更新功能,希望这对你来说是最好的Painless入门。


本文参考链接:https://blog.csdn.net/neweastsun/article/details/105035538
阅读延展