写在前面
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/p /_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个的球员信息:
核心代码为:
\" \":{
\"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\":{
\" \":{
\" \":{
\"inline\":\"int total = 0; for (int i = 0; i < doc[\'gp\'].length; ++i) { total += doc[\'gp\'][i]; } if(total>30) return total;\",
\"lang\":\"painless\"
},
\" \":{
\"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\":\"if(\'sean\'.equals(doc[\'first.keyword\'].value)) return true;\",
\"lang\":\"painless\"
}
}
}
}
}
其实简单来说就是在inline内输入过滤语句 lang 后面加入脚本名称即可
但是如果查询条件相当复杂,需要多种判断,循环才能组成过滤条件,用inline的方式编写脚本显然不适合,那就需要将脚本代码逻辑放在file文件内(文件后缀为.painless),将file放在es安装目录下的\\config\\ s文件夹下(如下图),然后调用,方式如下
\" \": {
\"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 即可
但是java调用 一般都是要传递好多参数的,所以一般还是把painless写进file里
api中的写法如上,当然 还是有人不懂是怎么用的
我来举个例子,比如我要对一些数据匹配ip,筛选出相应的ip
java部分
Map<String, > params = new HashMap<String, >();//存放参数的map
params.put(\"resourceMapList\", resourceMapList);//其他一些匹配信息List<Map<isnot;ip>>,isnot及ip的含义见下面脚本代码
params.put(\"fieldName\", fieldName);//ip对应的字段名称
=new ( Type.FILE, \"painless\", \"function_ip_resources\",params);//脚本文件名称,脚本类型
QueryBuilder filterBuilder = QueryBuilders. Query( );//创建 query
QueryBuilder q=QueryBuilders.boolQuery().filter(filterBuilder); //将 Queryc存入过滤条件
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,上面那部分只是应用 部分的代码,至于如何连接es(elastisearch之java api Transportclient创建连接),如何查询或操作就是另一部分的学习内容了,如果有问题 欢迎留言~
继续阅读与本文标签相同的文章
剑指offer21-25
-
数十万共享雨伞不翼而飞,创始人却高兴的要命!网友:赚翻了
2026-05-18栏目: 教程
-
滴滴 这是一见钟情的感脚
2026-05-18栏目: 教程
-
以实践的方式讨论:N-Gram原理与其应用
2026-05-18栏目: 教程
-
Hi拼团,第六代云服务器拼团购买更便宜,低至148元/年
2026-05-18栏目: 教程
-
汇编(五)栈、CPU提供的栈机制、push、pop指令
2026-05-18栏目: 教程
