Elasticsearch:Painless scripting

Painless使用类似于Groovy的Java样式语法。实际上,大多数Painless脚本也是有效的Groovy,而简单的Groovy脚本通常是有效的Painless脚本。 (本规范假定您至少对Java和相关语言有一定的了解。)

Painless本质上是Java的子集,具有一些其他脚本语言功能,使脚本更易于编写。但是,有一些重要的差异,尤其是在铸造模型上。使用ANTLR4和ASM库来解析和编译Painless脚本。Painless脚本直接编译为Java字节码,并针对标准Java虚拟机执行。该规范使用ANTLR4语法符号来描述允许的语法。但是,实际的Painless语法比此处显示的更为紧凑。Painless是一种专门用于Elasticsearch的简单安全的脚本语言。它是Elasticsearch的默认脚本语言,可以安全地用于inline和stored脚本。

我们可以在Elasticsearch中可以使用脚本的任何地方使用Painless。 Painless功能包括:

  • 快速的性能:Painless脚本的运行速度比其他脚本快几倍。
  • 语法:扩展Java语法以提供Groovy风格的脚本语言功能,使脚本更易于编写。
  • 安全性:具有方法调用/字段粒度的细粒度白名单。 (有关可用类和方法的完整列表,请参阅《 Painless API参考》。)
  • 可选类型:变量和参数可以使用显式类型或动态def类型。
  • 优化:专为Elasticsearch脚本设计。

让我们通过将一些学术统计数据加载到Elasticsearch索引中来说明Painless的工作方式:

PUT academics/_bulk
{"index":{"_id":1}}
{"first":"Agatha","last":"Christie","base_score":[9,27,1],"target_score":[17,46,0],"grade_point_index":[26,82,1],"born":"1978/08/13"}
{"index":{"_id":2}}
{"first":"Alan","last":"Moore","base_score":[7,54,26],"target_score":[11,26,13],"grade_point_index":[26,82,82],"born":"1976/10/12"}
{"index":{"_id":3}}
{"first":"jiri","last":"Ibsen","base_score":[5,34,36],"target_score":[11,62,42],"grade_point_index":[24,80,79],"born":"1983/01/04"}
{"index":{"_id":4}}
{"first":"William","last":"Blake","base_score":[4,6,15],"target_score":[8,23,15],"grade_point_index":[26,82,82],"born":"1990/02/17"}
{"index":{"_id":5}}
{"first":"Shaun","last":"Tan","base_score":[5,0,0],"target_score":[8,1,0],"grade_point_index":[26,1,0],"born":"1993/06/20"}
{"index":{"_id":6}}
{"first":"Peter","last":"Hitchens","base_score":[0,26,15],"target_score":[11,30,24],"grade_point_index":[26,81,82],"born":"1969/03/20"}
{"index":{"_id":7}}
{"first":"Raymond","last":"Carver","base_score":[7,19,5],"target_score":[3,17,4],"grade_point_index":[26,45,34],"born":"1963/08/10"}
{"index":{"_id":8}}
{"first":"Lee","last":"Child","base_score":[2,14,7],"target_score":[8,42,30],"grade_point_index":[26,82,82],"born":"1992/06/07"}
{"index":{"_id":39}}
{"first":"Joseph","last":"Heller","base_score":[6,30,15],"target_score":[3,30,24],"grade_point_index":[26,60,63],"born":"1984/10/03"}
{"index":{"_id":10}}
{"first":"Harper","last":"Lee","base_score":[3,15,13],"target_score":[6,24,18],"grade_point_index":[26,82,82],"born":"1976/03/17"}
{"index":{"_id":11}}
{"first":"Ian","last":"Fleming","base_score":[3,18,13],"target_score":[6,20,24],"grade_point_index":[26,67,82],"born":"1972/01/30"}

在上面,我们已经创建好一个叫做academics的Elasticsearch索引。在下面,我们通过Painless的脚本来对这个索引进行操作。

通过Painless访问doc值

可以从名为doc的map访问文档值。 例如,以下脚本计算学生的总体目标。 本示例使用强类型的int和for循环。

GET academics/_search
{
  "query": {
    "function_score": {
      "script_score": {
        "script": {
          "lang": "painless",
          "source": """
             int total = 0; 
             for (int i = 0; i < doc['base_score'].length; ++i) { 
               total += doc['base_score'][i]; 
             } 
             return total;
          """
        }
      }
    }
  }
}

在上面请注意,我们使用了两个"""包含我们的Painless代码。这是为了能够让我们写出来比较好看格式的代码。从上面的代码我们可以看出来,代码和Java代码非常相似。通过这个循环,我们访问doc的map值,从而计算出base_score的总值。返回的结果为:

    "hits" : [
      {
        "_index" : "academics",
        "_type" : "_doc",
        "_id" : "2",
        "_score" : 87.0,
        "_source" : {
          "first" : "Alan",
          "last" : "Moore",
          "base_score" : [
            7,
            54,
            26
          ],
          "target_score" : [
            11,
            26,
            13
          ],
          "grade_point_index" : [
            26,
            82,
            82
          ],
          "born" : "1976/10/12"
        }
      },
     ...
  ]

在上面我们可以看出来我们的_score的分数为base_score的总和7 + 54 + 26 = 87。

另外,我们可以使用script_fields而不是function_score来做同样的事情:

GET academics/_search
{
  "query": {
    "match_all": {}
  },
  "script_fields": {
    "total_goals": {
      "script": {
        "lang": "painless",
        "source": """
           int total = 0; 
           for (int i = 0; i < doc['base_score'].length; ++i) { 
             total += doc['base_score'][i]; 
            } 
            return total;
        """
      }
    }
  }
}

返回结果:

    "hits" : [
      {
        "_index" : "academics",
        "_type" : "_doc",
        "_id" : "1",
        "_score" : 1.0,
        "fields" : {
          "total_goals" : [
            37
          ]
        }
      },
      {
        "_index" : "academics",
        "_type" : "_doc",
        "_id" : "2",
        "_score" : 1.0,
        "fields" : {
          "total_goals" : [
            87
          ]
        }
      },
      {
        "_index" : "academics",
        "_type" : "_doc",
        "_id" : "3",
        "_score" : 1.0,
        "fields" : {
          "total_goals" : [
            75
          ]
        }
     ...
 ]

在这里我们可以看到有一个叫做total_goals的field,它包含了所有文档的base_score的分数相加的结果。细心的读者可能已经看出来了,在返回的结果里,没有_source这个字段,为了能够得到_source这个字段,我们必须在请求中特别提出来:

GET academics/_search
{
  "_source" : [],
  "query": {
    "match_all": {}
  },
  "script_fields": {
    "total_goals": {
      "script": {
        "lang": "painless",
        "source": """
           int total = 0; 
           for (int i = 0; i < doc['base_score'].length; ++i) { 
             total += doc['base_score'][i]; 
            } 
            return total;
        """
      }
    }
  }
}

在上面,我们可以看出来,我特意添加了"_source": []在请求中,这样,我们可以得到如下的响应:

    "hits" : [
      {
        "_index" : "academics",
        "_type" : "_doc",
        "_id" : "5",
        "_score" : 1.0,
        "_source" : {
          "base_score" : [
            5,
            0,
            0
          ],
          "last" : "Tn",
          "born" : "1993/06/20",
          "target_score" : [
            8,
            1,
            0
          ],
          "first" : "Shaun",
          "grade_point_index" : [
            26,
            1,
            0
          ]
        },
        "fields" : {
          "total_goals" : [
            5
          ]
        }
      },
  ...
 ]

我们可以看到这次除了新增加的total_goals之外,我们所需要的_source也在返回的数据中。

脚本字段还可以使用params ['_ source']访问实际的_source文档,并提取要从中返回的特定元素。上面的请求也可以表述为:

GET academics/_search
{
  "query": {
    "match_all": {}
  },
  "script_fields": {
    "total_goals": {
      "script": {
        "lang": "painless",
        "source": """
           int total = 0; 
           for (int i = 0; i < params['_source']['base_score'].length; ++i) { 
             total += params['_source']['base_score'][i]; 
           } 
           return total
        """
      }
    }
  }
}

了解doc ['my_field'] .value和params ['_ source'] ['my_field']之间的区别很重要。 第一个使用doc关键字,将导致将该字段的术语加载到内存中(缓存),这将导致执行速度更快,但会占用更多内存。 另外,doc [...]符号仅允许使用简单值字段(您不能从中返回json对象),并且仅对未分析或基于单个术语的字段有意义。 但是,仍然建议使用doc(即使有可能)从文档访问值的方式,因为_source每次使用时都必须加载和解析。 使用_source非常慢。

我们也注意到所有文档的分数都是1.0,并且返回的结果不是按照我们想要的分数从高到底进行排序的。我们无法通过sort多total_goals进行排序,因为这个字段不是在source里的字段。但是我们可以进行如下的方法来进行排序:

GET academics/_search
{
  "query": {
    "match_all": {}
  },
  "script_fields": {
    "total_goals": {
      "script": {
        "lang": "painless",
        "source": """
           int total = 0; 
           for (int i = 0; i < doc['base_score'].length; ++i) { 
             total += doc['base_score'][i]; } 
             return total;
        """
      }
    }
  },
  "sort": {
    "_script": {
      "type": "string",
      "order": "asc",
      "script": {
        "lang": "painless",
        "source": """
          return doc['first.keyword'].value + ' ' + doc['last.keyword'].value
        """
      }
    }
  }
}

以上示例使用Painless脚本按学生的姓和名对学生进行排序。 使用doc ['first']。value和doc ['last']。value访问名称。在上面,我们通过doc['first.keyword'].value + ' ' + doc['last.keyword'].value来重新组合一个新的字段,并按照这个字段按照升序进行排序。这种排序的结果是分数变为null:

    "hits" : [
      {
        "_index" : "academics",
        "_type" : "_doc",
        "_id" : "1",
        "_score" : null,
        "fields" : {
          "total_goals" : [
            37
          ]
        },
        "sort" : [
          "Agatha Christie"
        ]
      },
      {
        "_index" : "academics",
        "_type" : "_doc",
        "_id" : "2",
        "_score" : null,
        "fields" : {
          "total_goals" : [
            87
          ]
        },
        "sort" : [
          "Alan Moore"
        ]
      },
    ...
  ]

 

使用Painless来更新字段

通过访问ctx._source。<field-name>字段的原始源,我们还可以轻松更新字段。

首先,我们通过提交以下请求来查看学生的源数据:

GET academics/_search
{
 "query": {
   "term": {
     "_id": 1
   }
 }
}

它显示的结果是:

    "hits" : [
      {
        "_index" : "academics",
        "_type" : "_doc",
        "_id" : "1",
        "_score" : 1.0,
        "_source" : {
          "first" : "Agatha",
          "last" : "Christie",
          "base_score" : [
            9,
            27,
            1
          ],
          "target_score" : [
            17,
            46,
            0
          ],
          "grade_point_index" : [
            26,
            82,
            1
          ],
          "born" : "1978/08/13"
        }
      }
    ]

要将id为1的学生的姓氏更改为“Frost”,只需将ctx._source.last设置为新值:

POST academics/_doc/1/_update
{
  "script": {
    "lang": "painless",
    "inline": "ctx._source.last = params.last",
    "params": {
      "last": "Frost"
    }
  }
}

在上面我们通过ctx._source.last = params.last把_source.last改为在params里定义的Frost。请注意,有的开发者可能认为如下的脚本会更加直接:

ctx._source.last = “Frost"

因为脚本在每次执行的时候都会被编译。一旦编译就会存于被缓存以便以后引用。编译一个脚本也会花时间的。如果一个脚本不发生变化就不会被重新编译,而引用之前编译好的脚本。如果我们使用上面的直接引用Frost的方式,那么如果接下来对其它的id来进行同样的操作,那么脚本将发生改变,那么新的脚本将被重新编译,从而浪费CPU的时间。这也就是我们为啥使用params来传输参数,而保持脚本不变。

我们还可以将字段添加到文档中。 例如,此脚本添加了一个新字段,其中包含学生的昵称“ JS”。

POST academics/_doc/1/_update
{
  "script": {
    "lang": "painless",
    "inline": "ctx._source.last = params.last; ctx._source.nick = params.nick",
    "params": {
      "last": "Smith",
      "nick": "JS"
    }
  }
}

在上面我们使用脚本cxt._source.nick = params.nick来为我们的文档添加了一个叫做nick的新字段。我们可以通过如下的方法来查询:

GET academics/_search
{
 "query": {
   "term": {
     "_id": 1
   }
 }
}

返回结果:

    "hits" : [
      {
        "_index" : "academics",
        "_type" : "_doc",
        "_id" : "1",
        "_score" : 1.0,
        "_source" : {
          "first" : "Agatha",
          "last" : "Smith",
          "base_score" : [
            9,
            27,
            1
          ],
          "target_score" : [
            17,
            46,
            0
          ],
          "grade_point_index" : [
            26,
            82,
            1
          ],
          "born" : "1978/08/13",
          "nick" : "JS"
        }
      }
    ]

我们可看到_id为1的文档多了一个nick的字段,同时它的last也修改为Smith。

Dates 

日期字段显示为ReadableDateTime,因此它们支持诸如getYear,getDayOfWeek和getMillis之类的方法。 例如,以下请求返回每个大学生的出生年份:

GET academics/_search
{
  "script_fields": {
    "birth_year": {
      "script": {
        "source": "doc.born.value.year"
      }
    }
  }
}

返回结果为:

    "hits" : [
      {
        "_index" : "academics",
        "_type" : "_doc",
        "_id" : "2",
        "_score" : 1.0,
        "fields" : {
          "birth_year" : [
            1976
          ]
        }
      },
      {
        "_index" : "academics",
        "_type" : "_doc",
        "_id" : "3",
        "_score" : 1.0,
        "fields" : {
          "birth_year" : [
            1983
          ]
        }
      },
   ...
 ]

在上面我可以看出来有一个birth_year含有该学生的出生年份。

Regular Expressions

默认情况下,正则表达式是禁用的,因为它们绕过了Painless对长时间运行且占用大量内存的脚本的保护。 更糟糕的是,即使看起来无害的正则表达式也可能具有惊人的性能和堆栈深度行为。 它们仍然是一个了不起的强大工具,但太可怕了,无法默认启用。 在elasticsearch.yml中将script.painless.regex.enabled设置为true以启用它们。

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

/pattern/:模式文字可创建模式。 这是Painless创建模式的唯一方法。 /.../内的模式只是Java正则表达式。

  • =〜:find运算符返回一个布尔值,如果文本的子序列匹配,则返回true,否则返回false。
  • ==〜:match运算符返回一个布尔值,如果文本匹配则返回true,否则返回false。

使用查找运算符(=〜),我们可以更新所有姓氏中带有“b”的学术学生:

POST academics/_update_by_query
{
  "script": {
    "lang": "painless",
    "source": """
       if (ctx._source.last =~ /b/) {
         ctx._source.last += "matched"
       } else {
         ctx.op = 'noop'
       }
    """
  }
}

我们查看一下我们的文档:

GET academics/_search
{
  "query": {
    "regexp": {
      "last": ".*matched"
    }
  }
}

返回结果:

    "hits" : [
      {
        "_index" : "academics",
        "_type" : "_doc",
        "_id" : "3",
        "_score" : 1.0,
        "_source" : {
          "base_score" : [
            5,
            34,
            36
          ],
          "last" : "Ibsenmatched",
          "born" : "1983/01/04",
          "target_score" : [
            11,
            62,
            42
          ],
          "first" : "jiri",
          "grade_point_index" : [
            24,
            80,
            79
          ]
        }
      }
    ]

在上面的结果中,我们可以看出来last为Ibsen的后面加上了一个matched。

使用匹配运算符(==〜),我们可以更新所有名称以辅音开头并以元音结尾的文档:

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

我们再次重新查询一下:

GET academics/_search
{
  "query": {
    "regexp": {
      "last": ".*matched"
    }
  }
}

我们可以看到:

      {
        "_index" : "academics",
        "_type" : "_doc",
        "_id" : "2",
        "_score" : 1.0,
        "_source" : {
          "base_score" : [
            7,
            54,
            26
          ],
          "last" : "Moorematched",
          "born" : "1976/10/12",
          "target_score" : [
            11,
            26,
            13
          ],
          "first" : "Alan",
          "grade_point_index" : [
            26,
            82,
            82
          ]
        }
      },
      {
        "_index" : "academics",
        "_type" : "_doc",
        "_id" : "4",
        "_score" : 1.0,
        "_source" : {
          "base_score" : [
            4,
            6,
            15
          ],
          "last" : "Blakematched",
          "born" : "1990/02/17",
          "target_score" : [
            8,
            23,
            15
          ],
          "first" : "William",
          "grade_point_index" : [
            26,
            82,
            82
          ]
        }
      },
    ...
  ]

在上面,我们看到Blake是复合条件的一个last。它是辅音b开头并以元音e结尾。它的last最后修改为Moorematched。

我们可以直接使用Pattern.matcher获取Matcher实例,并删除所有姓氏中的所有元音:

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

我们再次查询一下:

GET academics/_search
{
  "_source": ["last"]
}

显示结果:

    "hits" : [
      {
        "_index" : "academics",
        "_type" : "_doc",
        "_id" : "5",
        "_score" : 1.0,
        "_source" : {
          "last" : "Tn"
        }
      },
      {
        "_index" : "academics",
        "_type" : "_doc",
        "_id" : "6",
        "_score" : 1.0,
        "_source" : {
          "last" : "Htchns"
        }
      },
      {
        "_index" : "academics",
        "_type" : "_doc",
        "_id" : "7",
        "_score" : 1.0,
        "_source" : {
          "last" : "Crvr"
        }
      }
  ...
 ]

我们可以看到,在上的结果中,索引的last里都没有一个是属于[aeiou]的字母。

Matcher.replaceAll只是对Java Matcher的replaceAll方法的调用,因此它支持$1和\ 1进行替换:

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

在这之前,我也有另外一篇文章 “Elasticsearch: Painless script编程”。它和本文章有些地方是重复的地方。供大家从不同的角度来学习。

发布了489 篇原创文章 · 获赞 107 · 访问量 84万+

猜你喜欢

转载自blog.csdn.net/UbuntuTouch/article/details/104206569