变量和数据类型
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};
条件语句和运算符
如果你熟悉Java,Groovy,或其他高级语言,那么条件和操作符都类似。Painless包含完整的操作符列表,除了它们的优先级和结合性之外,这些操作符与其他高级语言几乎兼容。列表中的大多数操作符都与Java和Groovy语言兼容,操作符优先级可以用括号提升。例如: int t = 5+(5*5)
与其他语言一样,Painless支持if else,但不支持else if或switch。举例:
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类型。
方法
虽然Painless从Java语言中获得很多强大功能,但并不是Java标准库(Java运行时环境,JRE)中的每个类或方法都可用。Elasticsearch有Painless的类和方法参考列表。列表不仅包括JRE中有效的方法和类,还包括Elasticsearch 和 Painless中可用的方法。
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;
脚本应用
使用脚本搜索
下面示例使用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更新每个文档。下面我们看看如何更新查询结果。
使用脚本更新
实现之前,我们先创建另一个字段存储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"
}
}