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创建连接),如何查询或操作就是另一部分的学习内容了,如果有问题 欢迎留言~

 



版权声明:本文为u012270682原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。