乾貨 | Elasticsearch7.X Scripting腳本使用詳解

  • 2019 年 10 月 4 日
  • 筆記

0、題記

除了官方文檔,其他能找到的介紹Elasticsearch腳本(Scripting)的資料少之又少。

一方面:性能問題。 官方文檔性能優化中明確指出使用腳本會導致性能低;

另一方面:使用場景相對少。 非複雜業務場景下,基礎的增、刪、改、查基本上就能搞定。

但,不能否認,在解決複雜業務問題(如:自定義評分、自定義文本相關度、自定義過濾、自定義聚合分析)時,腳本依然是Elasticsearch強悍的利器之一。

本文在官方文檔基礎上,結合實際業務場景,在Elasticsearch7.3環境下進行腳本使用解讀。

1、官方scripting使用建議

Avoid scripts——In general, scripts should be avoided. If they are absolutely needed, you should prefer the painless and expressions engines.

ebay在性能優化實踐中也強調(本文做了擴展延伸):

避免使用腳本查詢(script query)計算動態欄位。

例如:我們有一個包含大量劇院資訊的索引,我們需要查詢以"Down"開頭的所有劇院。你可能運行一個如下腳本查詢:

 1POST seats/_search   2{   3 "query": {   4  "bool":{   5    "filter": {   6    "script":{   7      "script":{   8        "lang":"painless",   9        "source": "doc['theatre'].value.startsWith('Down')"  10      }  11    }  12    }  13  }  14 }  15}

這個查詢非常耗費資源,並且減慢整個系統。

解決方案:

  • 方案一:prefix前綴匹配;實測性能:prefix較scripting性能提升5倍。
  • 方案二:索引時考慮添加一個名為「theatre_prefix」的keyword類型欄位。然後我們可以查詢"theatre_prefix":"Down"。

2、ES Scripting歷史

版本

使用腳本

< Elasticsearch 1.4

MVEL 腳本

< Elasticsearch 5.0

Groovy 腳本

『>= Elasticsearch 5.0

painless 腳本

Groovy 的出現是解決MVEL的安全隱患問題; 但Groovy仍存在記憶體泄露+安全漏洞問題,

painless腳本的官宣時間:2016年9月21日。看似很新,截止目前,已經三年左右時間了。

正如其名字:無痛。painless的出現是為了用戶更方便、高效的使用腳本。

https://www.elastic.co/cn/blog/painless-a-new-scripting-language

3、Painless Scripting 簡介

Painless是一種簡單,安全的腳本語言,專為與Elasticsearch一起使用而設計。它是Elasticsearch的默認腳本語言,可以安全地用於內聯和存儲腳本。

Painless特點:

  • 性能牛逼:Painless腳本運行速度比備選方案(包括Groovy)快幾倍。
  • 安全性強:使用白名單來限制函數與欄位的訪問,避免了可能的安全隱患。
  • 可選輸入:變數和參數可以使用顯式類型或動態def類型。
  • 上手容易:擴展了java的基本語法,併兼容groove風格的腳本語言特性。
  • 特定優化:是ES官方專為Elasticsearch腳本編寫而設計。

4、Scripting 應用場景

認知前提: 增刪改查能解決業務場景80%的問題,Painless腳本操作一般應用於相對複雜的業務場景中。

常見場景舉例如下:

  • 自定義欄位
  • 自定義評分
  • 自定義更新
  • 自定義reindex
  • 聚合
  • 其他自定義操作

5、Scripting 使用模板

心中有模板,腳本認知就有了「套路」。

1"script": {  2    "lang":   "...",  3    "source" | "id": "...",  4    "params": { ... }  5  }
  1. lang:代表language腳本語言,默認指定為:painless。
  2. source:腳本的核心部分,id應用於:stored script。
  3. params:傳遞給腳本使用的變數參數。

6、Scripting 實戰

6.1 自定義欄位

舉例:返回原有Mapping未定義的欄位值。 如:以my_doubled_field返回my_field欄位的翻倍後的結果。

 1GET my_index/_search   2{   3  "script_fields": {   4    "my_doubled_field": {   5      "script": {   6        "lang":   "expression",   7        "source": "doc['my_field'] * multiplier",   8        "params": {   9          "multiplier": 2  10        }  11      }  12    }  13  }  14}

注意:這裡腳本語言選擇的expression,下一節講解。

如:返回日期欄位中的「年」或「月」或「日」等。

 1GET hockey/_search   2{   3  "script_fields": {   4    "birth_year": {   5      "script": {   6        "source": "doc.born.value.year"   7      }   8    }   9  }  10}

6.2 自定義評分

1GET my_index/_search   2{   3  "query": {   4    "function_score": {   5      "query": {   6        "match": {   7          "text": "quick brown fox"   8        }   9      },  10      "script_score": {  11        "script": {  12          "lang": "expression",  13          "source": "_score * doc['popularity']"  14        }  15      }  16    }  17  }  18}

6.3 自定義更新

Update:將已有欄位值賦值給其他欄位。

 1POST hockey/_update/1   2{   3  "script": {   4    "lang": "painless",   5    "source": """   6      ctx._source.last = params.last;   7      ctx._source.nick = params.nick   8    """,   9    "params": {  10      "last": "gaudreau",  11      "nick": "hockey"  12    }  13  }  14}

Update_by_query:滿足b開頭(注意正則)的欄位,末尾添加matched。

 1POST hockey/_update_by_query   2{   3  "script": {   4    "lang": "painless",   5    "source": """   6      if (ctx._source.last =~ /b/) {   7        ctx._source.last += "matched";   8      } else {   9        ctx.op = "noop";  10      }  11    """  12  }  13}

6.4 自定義reindex

Elasticsearch認證考試題:

有index_a包含一些文檔, 要求創建索引index_b,通過reindex api將index_a的文檔索引到index_b。

要求: 1)增加一個整形欄位,value是index_a的field_x的字元長度;

2)再增加一個數組類型的欄位,value是field_y的詞集合。

(field_y是空格分割的一組詞,比方"foo bar",索引到index_b後,要求變成["foo", "bar"])

 1POST _reindex   2{   3  "conflicts": "proceed",   4  "source": {   5    "index": "index_a"   6  },   7  "dest": {   8    "index": "index_b"   9  },  10  "script": {  11    "source": "ctx._source.parts = / /.split(ctx._source.address); ctx._source.tag = ctx._source.city.length();"  12  }  13}

語法參考:

https://www.elastic.co/guide/en/elasticsearch/painless/7.3/painless-regexes.html

6.5 聚合

 1GET /_search   2{   3    "aggs" : {   4        "genres" : {   5            "terms" : {   6                "script" : {   7                    "source": "doc['genre'].value",   8                    "lang": "painless"   9                }  10            }  11        }  12    }  13  14}

6.6 其他自定義操作

需要結合業務去實踐。

7、常見坑及問題

7.1 腳本只有Painless嗎?

顯然不是,第6節用到的expression 是Lucene』s expressions 腳本語言。

還可以基於腳本引擎自己開發插件實現,

https://www.elastic.co/guide/en/elasticsearch/reference/current/modules-scripting-engine.html

7.2 怎麼界定是expressions 還是Painless?

"lang": "painless", "lang": "expressions ", 是唯一區分。

7.3 使用painless就百分之百「無痛」,無漏洞後顧之憂了嗎?

凡事不能絕對。 核心注意點: 第一:不要root賬戶下運行Elasticsearch。 第二:不要公開ES路徑給其他用戶。 第三:不要公開ES路徑到互聯網。

實戰推薦:

  • 1、用戶在搜索框中鍵入文本,文本將直接發送到後台的match、match_phrase、Simple query string或 Suggesters.
  • 2、作為應用程式開發過程的一部分(而非全部)開放上述查詢的腳本。
  • 3、使用用戶提供的參數運行腳本。
  • 4、文檔固定的Mapping結構。

不推薦:

  • 1、用戶可以編寫任意scripts, queries(檢索), _search requests(search請求)。
  • 2、文檔結構可以用戶自定義。

8、小結

本文講解了腳本的發展歷史、使用場景、應用實戰,但相比於實際業務的複雜需求仍然是九牛一毛。

實戰中,肯定還會遇到這樣、那樣的問題。

一方面:歡迎留言交流。 另一方面:多研讀官方文檔,很多細節值得深究。

參考: https://www.elastic.co/guide/en/elasticsearch/reference/current/tune-for-search-speed.html https://www.infoq.cn/article/elasticsearch-performance-tuning-practice-at-ebay https://github.com/laoyang360/deep_elasticsearch/blob/master/es_dsl_study/6.scripting.md https://github.com/elastic/elasticsearch/issues/19396 https://www.youtube.com/watch?v=3FLEJJ8PsM4 https://blog.csdn.net/u013613428/article/details/78134170


Tags: