painless脚本应用及与elasticsearch,java的结合使用

    写在前面    
    painless是一个较新的脚本语言,毕竟不是一加一等于二那么简单,开始不懂是很正常的,如果看不懂 请看第二遍第三遍乃至N次  相信我 一定能看得懂的,书读百遍,其义自见

 es5以上版本推出了简单安全快捷的painless脚本来替代原有的一些脚本语言,最近正好需要过滤查询一些逻辑相对复杂的数据并对原有的groovy脚本进行升级,所以对painless进行了学习,发现网上对这个脚本的说明非常少, 官网有英文版的说明,所以特将学习结果分享出来。

     painless安全且高效,书写方法和java类似,安全是因为painless提供了一个方法的白名单(链接:painless支持的类),即链接地址里的所有类和方法,除了这些方法以外的所有方法都不允许使用,而不是像groovy一样 提供一个较浅的沙盒很容易被利用(groovy漏洞分析),从而保证了安全性;高效是因为painless是由es团队自己开发的脚本,且不支持重载方法,从而保证了高效性(不支持重载方法,当一个def动态参数被定义立即就能获得这个参数对应的静态类,而不需要一个一个去遍历所有的重载方法,这正是比groovy高效的地方),且无需安装其他插件,即写即用,容易上手。

      以下为es+painless脚本筛选数据举例及分析:

    举个例子

首先设想一个场景:

一个球队,评选最有潜力球员,简单按进球数多少排序很好实现。但助攻和普通进攻也可以作为一个评选指标,最后需求为进球数权重5,助攻权重3,普通进攻权重2的方式为球员排名这怎么排序呢?单单靠dsl写起来很复杂。可用脚本定义好逻辑,让es去调用返回结果即可

准备一个球队的数据,配置好elasticsearch,并启动

工具:postman(这是一个可以模拟请求的url工具,且可返回结果)

在postman中按下图输入地址:127.0.0.1:8200/football/player/_bulk?refresh

选中Body-->raw-->JSON……

并在Body中键入球队的队员信息内容(球队信息内容来自网络,侵删):

{"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"}

查询进球总数大于50个的球员信息:

核心代码为:    

"script":{
        "inline":"int total = 0; for (int i = 0; i < doc['goals'].length; ++i) { total += doc['goals'][i]; }  if(total>50) return total;",
        "lang":"painless"
        }

其中inline表示脚本写在查询语句内,"lang":"painless"表示脚本语言为painless

全部查询语句见下图


多条件联合查询:查询普通进攻总数(gp)大于50且进球数大于50,且firstname中包含‘sean’的球员信息

    {
      
      "query": {
         "bool":{
            "must":{
                "script":{
                    "script":{
                        "inline":"int total = 0; for (int i = 0; i < doc['gp'].length; ++i) { total += doc['gp'][i]; }  if(total>30) return total;",
                        "lang":"painless"
                    },
                    "script":{
                        "inline":"int total = 0; for (int i = 0; i < doc['goals'].length; ++i) { total += doc['goals'][i]; }  if(total>50) return total;",
                        "lang":"painless"
                    },
                    "script":{
                        "inline":"if('sean'.equals(doc['first.keyword'].value))   return true;",
                        "lang":"painless"
                    }
                }
            }
        }
      }

其实简单来说就是在inline内输入过滤语句 lang 后面加入脚本名称即可

但是如果查询条件相当复杂,需要多种判断,循环才能组成过滤条件,用inline的方式编写脚本显然不适合,那就需要将脚本代码逻辑放在file文件内(文件后缀为.painless),将file放在es安装目录下的\config\scripts文件夹下(如下图),然后调用,方式如下

     "script": {
            "lang": "painless",
            "file": "football_score"//这里将inline改为file   后面跟脚本名称football_score
          }


football_score的逻辑为:

进球数权重5,助攻权重3,普通进攻权重2返回总得分


    脚本化field

在上面的查询中,只能查询出相应结果,但是如果想要对某些字段进行重新处理,比如对进球数汇总,将first\last合并为全名这时就不光只用到查询了,还需要对field进行脚本化

就像下面这样

执行后的结果


全部过程如下


脚本化field完成,还有一个问题,这些都知识针对固定的字段得出的结果,如果要传参怎么办,我想传一个外部的信息,然后和es中的数据做匹配,上面的内容又支持不了了,所以接着看下面吧

    painless传参    

核心脚本代码


totalgoalbs.painless文件中写法:params.param1为参数值,return 0或false时过滤,  1或true匹配


当然也可以传递多个参数

甚至传递list,只要后台写好解析就行了,像这样

在painless中写好自己的解析逻辑即可

    java+es+painless   先看一下java调用painless的api是怎么写的

inline方式的脚本,在java中直接用上面的new Script即可

但是java调用 一般都是要传递好多参数的,所以一般还是把painless写进file里


api中的写法如上,当然 还是有人不懂是怎么用的

我来举个例子,比如我要对一些数据匹配ip,筛选出相应的ip

java部分

    Map<String, Object> params = new HashMap<String, Object>();//存放参数的map
    params.put("resourceMapList", resourceMapList);//其他一些匹配信息List<Map<isnot;ip>>,isnot及ip的含义见下面脚本代码
    params.put("fieldName", fieldName);//ip对应的字段名称
    Script script =new Script(ScriptType.FILE, "painless", "function_ip_resources",params);//脚本文件名称,脚本类型
    ScriptQueryBuilder filterBuilder = QueryBuilders.scriptQuery(script);//创建scriptquery
    QueryBuilder q=QueryBuilders.boolQuery().filter(filterBuilder); //将scriptQueryc存入过滤条件


painless脚本:名称为function_ip_resources.painless

def ipStand = /^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?).(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?).(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?).(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/;
def flag = false;//是否匹配标记
def resourceMapList=params.resourceMapList;//其他匹配信息
String filedName=params.fieldName;//ip对应的字段名称,这个字段类型用的是keyword
String filedIp=doc[filedName+'.keyword'].value;//取值
if(!(ipStand.matcher(filedIp).matches())){//正则匹配ipv4
    return false;//不符合ip格式 直接返回false
}else{
    for(ipResMap in resourceMapList){ //遍历其他匹配信息
        def isNot = ipResMap.get('isnot');//取反参数,isnot为true:取与下面一行ip不匹配的所有ip  ;isnot为false 取与下面一行ip相同的所有ip
        String leftVal = ipResMap.get('ip'); //匹配值
        if(filedIp.equals(leftVal)){
            //如果不是取反,filedIp与IP参数相同表明匹配成功
            if("false".equals(isNot)){
                flag=true;
            }
            }else{
                //取反,ip不包含在ip参数内时匹配成功
                 if(!("false".equals(isNot))){
                  flag=true;
                 }
        }
    }
}
 
return flag;

执行后可正常筛选出符合条件的ip,上面那部分只是应用script部分的代码,至于如何连接es(elastisearch之java api Transportclient创建连接),如何查询或操作就是另一部分的学习内容了,如果有问题 欢迎留言~
 

猜你喜欢

转载自blog.csdn.net/aa1215018028/article/details/85078256