1. 程式人生 > >painless指令碼應用及與elasticsearch,java的結合使用

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建立連線),如何查詢或操作就是另一部分的學習內容了,如果有問題 歡迎留言~