Elasticsearch Painless Script入門教程

Elasticsearch Painless Script入門教程

前面幾篇文章已經陸續提到了Elasticsearch 腳本,但總感覺不夠系統,本文帶你係統地學習下Painless Script。

Painless 腳本介紹

自Elasticsearch 5.x 引入Painless,使得Elasticsearch擁有了安全、可靠、高性能腳本的解決方案。Painless是Elastic開發並做了專門的優化,相較之前的腳本更快、安全、易使用、可靠。

Painless腳本的目標是使編寫腳本對用戶來說無痛,特別是對於來自Java或Groovy環境的用戶。可能你還不熟悉Elasticsearch腳本,讓我們從基礎開始。

下面我們簡要介紹Painless,並示例展示如何使用腳本搜索、更新數據。

1.1 變量和數據類型

Painless中變量可以聲明爲基本數據類型、引用類型、字符串、void(不返回值)、數組以及動態類型。其支持下面基本類型:

byte, short, char, int, long, float, double, boolean.聲明變量與java類似:

int i = 0; double a; boolean g = true;

引用類型也和java類似,除了不支持修飾符,但支持繼承。變量通過new關鍵字初始化,例如聲明a作爲ArrayList,變量b類型爲Map:

ArrayList a = new ArrayList();  
Map b;  
Map g = [:];  
List q = [1, 2, 3];  

List和Map與數組類似,除了初始化時不需要new關鍵字,但它們是引用類型不是數組。

字符串類型可以使用直接量賦值,或使用new關鍵字初始化。

String a = "a";  
String foo = new String("bar");  

數組類型支持一維和多維,初始值爲null。與引用類型一樣,使用new關鍵字,併爲每個維度設置中括號。例如:

int[] x = new int[2];  
x[0] = 3;  
x[1] = 4;  

數組大小可以是顯示的,如:int[] a = new int[2]。或者創建時直接賦值:

int[] b = new int[] {1,2,3,4,5};  

與Java和Groovy中的數組一樣,數組數據類型在聲明和初始化時必須有一個基本類型、字符串,甚至是與之關聯的動態def類型。

def是Painless支持的動態類型,它所做的是模仿它在運行時分配任何類型的行爲。所以,當定義一個變量時:

def a = 1;  
def b = "foo";  

elasticsearch會推斷a是int類型,值爲1; b是字符串類型,值爲“foo”。
數組也可以通過def聲明,舉例:

def[][] h = new def[2][2];  
def[] f = new def[] {4, "s", 5.7, 2.8C};  

有了這些變量,讓我們來看看條件語句和運算符。

1.2. 條件語句和運算符

如果你熟悉Java,Groovy,或其他高級語言,那麼條件和操作符都類似。Painless包含完整的操作符列表,除了它們的優先級和結合性之外,這些操作符與其他高級語言幾乎兼容。列表中的大多數操作符都與Java和Groovy語言兼容,操作符優先級可以用括號提升。例如: int t = 5+(5*5)

與其他語言一樣,Painless支持if else,但不支持else ifswitch。舉例:

if (doc['foo'].value = 5) {  
    doc['foo'].value *= 10;
} 
else {  
    doc['foo'].value += 10;
}

Painless也有Elvis操作符?:,其和Kotlin、Groovy。舉例:

x ?: y  

如果x不null則評估返回左邊表達式,如果x爲null評估返回右邊表達式。Elvis操作不能和基本類型一起工作,所以這裏最好使用def類型。

1.3. 方法

雖然Painless從Java語言中獲得很多強大功能,但並不是Java標準庫(Java運行時環境,JRE)中的每個類或方法都可用。Elasticsearch有Painless的類和方法參考列表。列表不僅包括JRE中有效的方法和類,還包括Elasticsearch 和 Painless中可用的方法。

1.4 Painless循環

Painless支持while, do...while, for循環,以及控制流語句,如break和continue,這些都是可用。下面示例中for循環與其他語言非常類似:

def total = 0;  
for (def i = 0; i < doc['scores'].length; i++) {  
    total += doc['scores'][i];
}
return total;  

使用下面的形式也可以:

def total = 0;  
for (def score : doc['scores']) {  
    total += score;
}
return total;  

好了,我們已經看了Painless語言的基本規範,下面開始通過腳本實現一些查詢。

2. 腳本應用

2.1. 準備數據

示例數據sat考試分數,可以在這裏下載sat.json文件,在命令行執行下列命令:

curl -H 'Content-Type: application/json' -XPOST 'localhost:9200/sat/_doc/_bulk?pretty' --data-binary @sat.json

批量導入json文件需要有兩點注意:
1.每一行都是一個json對象;
2.第一行包括索引名稱,第二行是插入數據。

如果需要登錄,則需要在命令中加入-u username:password參數。

2.2. 使用腳本搜索

下面示例使用def演示Painless的動態類型。語句如下:

GET sat/_search
{
  "script_fields": {
    "some_scores": {
      "script": {
        "lang": "painless",
        "source": "def scores = 0; scores = doc['AvgScrRead'].value + doc['AvgScrWrit'].value; return scores;"
      }
    }
  }
}

script中可以定義語言類型lang的值,默認爲Painless。另外可以定義script的source

上面示例中使用_search API和script_fields命令。該命令可以創建新的字段存儲腳本中定義的scores值,我們簡單命名爲some_scores,然後在source中定義腳本:

def scores = 0;  
scores = doc['AvgScrRead'].value + doc['AvgScrWrit'].value;  
return scores;  

你注意到上面示例中的腳本沒有任何換行符,這是因爲Elasticsearch中腳本必須是單行字符串。其實該示例可以不使用Painless實現,也可以使用lucene表達式實現,這裏僅爲說明腳本的作用。

看下部分返回結果:

"hits" : [
    {
        "_index" : "sat",
        "_type" : "_doc",
        "_id" : "8-gOAnEBs8Ix-l1KQQ_6",
        "_score" : 1.0,
        "fields" : {
            "some_scores" : [
                961
            ]
        }
    }
]

上面腳本在索引中每個文檔上執行,上面結果顯示fields中通過script_fields命令創建了新的字段some_scores

下面實現另一個查詢,搜索AvgScrRead成績小於350,並且AvgScrMath成績大於350,腳本如下:

doc['AvgScrRead'].value < 350 && doc['AvgScrMath'].value > 350  

查詢語句:

GET sat/_search
{
  "query": {
    "script": {
      "script": {
        "source": "doc['AvgScrRead'].value < 350 && doc['AvgScrMath'].value > 350",
        "lang": "painless"
      }
    }
  }
}

下面我們查詢四個成績即總分和其他三科成績,我們使用腳本定義數組保存三科成績及總成績:

def sat_scores = [];  
def score_names = ['AvgScrRead', 'AvgScrWrit', 'AvgScrMath'];  
for (int i = 0; i < score_names.length; i++) {  
    sat_scores.add(doc[score_names[i]].value)
}
def temp = 0;  
for (def score : sat_scores) {  
    temp += score;
}
sat_scores.add(temp);  
return sat_scores;  

我們定義 sat_scores 數組存儲SAT分數 (AvgScrRead, AvgScrWrit, AvgScrMath) 及計算的總成績。創建另一個數組scores_names存儲SAT中包括成績的三個字段名稱。如果未來索引中字段名稱變了,則需要修改數組的值。使用for循環遍歷scores_names數組,在sat_scores數組加入相應的值,接着循環sat_scores通過temp遍歷累計成績,最後在sat_scores中增加總成績。當然這個示例也可以通過一個循環實現。

完成查詢示例代碼:

GET sat/_search
{
  "query": {
    "script": {
      "script": {
        "source": "doc['AvgScrRead'].value < 350 && doc['AvgScrMath'].value > 350",
        "lang": "painless"
      }
    }
  },
  "script_fields": {
    "scores": {
      "script": {
        "source": "def sat_scores = []; def scores = ['AvgScrRead', 'AvgScrWrit', 'AvgScrMath']; for (int i = 0; i < scores.length; i++) {sat_scores.add(doc[scores[i]].value)} def temp = 0; for (def score : sat_scores) {temp += score;} sat_scores.add(temp); return sat_scores;",
        "lang": "painless"
      }
    }
  }
}

返回結果類似這樣:

"hits" : [
    {
        "_index" : "sat",
        "_type" : "_doc",
        "_id" : "q-gOAnEBs8Ix-l1KQhAC",
        "_score" : 1.0,
        "fields" : {
            "scores" : [
                349,
                345,
                352,
                1046
            ]
        }
    }
]

上面查詢並沒有存儲結果,如果存儲需要使用_update_update_by_query API更新每個文檔。下面我們看看如何更新查詢結果。

2.3. 使用腳本更新

實現之前,我們先創建另一個字段存儲SAT分數數組。可以通過Elasticsearch的_update_by_queryAPI增加新的字段All_Scores,一開始使用空數組進行初始化:

POST sat/_update_by_query
{
  "script": {
    "source": "ctx._source.All_Scores = []",
    "lang": "painless"
  }
}

到此我們給所有文檔增加了新的字段,接着需要更新該字段,我們使用腳本更新在字段All_Scores

def scores = ['AvgScrRead', 'AvgScrWrit', 'AvgScrMath'];  
for (int i = 0; i < scores.length; i++) {  
    ctx._source.All_Scores.add(ctx._source[scores[i]]);
} 
def temp = 0;  
for (def score : ctx._source.All_Scores) {  
    temp += score;
}
ctx._source.All_Scores.add(temp);  

使用_update_update_by_queryAPI,不能使用doc變量。Elasticsearch提供了ctx變量和_source文檔,可以訪問文檔中字段。故可以更新All_Scores數組,存儲SAT成績及總成績。

完整腳本如下:

POST sat/_update_by_query
{
  "script": {
    "source": "def scores = ['AvgScrRead', 'AvgScrWrit', 'AvgScrMath']; for (int i = 0; i < scores.length; i++) { ctx._source.All_Scores.add(ctx._source[scores[i]])} def temp = 0; for (def score : ctx._source.All_Scores) {temp += score;}ctx._source.All_Scores.add(temp);",
    "lang": "painless"
  }
}

如果僅更新單個文檔,也可以使用類似腳本實現。但需要指明文檔的id,下面示例給 UOgOAnEBs8Ix-l1KQhEK文檔的AvgScrMath字段值加上10:

POST sat/_update/UOgOAnEBs8Ix-l1KQhEK
{
  "script": {
    "source": "ctx._source.AvgScrMath += 10",
    "lang": "painless"
  }
}  

3. 總結

本文我們介紹了Elasticsearch中Painless腳本語言的基本規範,通過一些示例說明如何使用。使用了Painless API方法,如HashMap和循環,同時也看了一些腳本查詢、更新功能,希望這對你來說是最好的Painless入門。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章