摘要:Elasticsearch
嗅虏,Java
script的作用
script是Elasticsearch的拓展功能卧秘,通過定制的表達(dá)式實(shí)現(xiàn)已經(jīng)預(yù)設(shè)好的API無法完成的個(gè)性化需求杆勇,比如完成以下操作
- 字段再加工/統(tǒng)計(jì)輸出
- 字段之間邏輯運(yùn)算
- 定義查詢得分的計(jì)算公式
- 定義特殊過濾條件完成搜索
- 類似于pandas的個(gè)性化增刪改操作
內(nèi)容概述
- (1)script格式說明,inline和stored腳本的調(diào)用方法
- (2)在無新增文檔的情況下,對(duì)現(xiàn)有文檔的字段個(gè)性化字段更新(
update
物咳,_update_by_query
诫钓,ctx._source
旬昭,Math,數(shù)組add/remove) - (3)在不修改文檔的情況下菌湃,在搜索返回中添加個(gè)性化統(tǒng)計(jì)字段(
_search
问拘,doc
,script_fields
,return
) - (4)在無新增文檔的情況下骤坐,對(duì)現(xiàn)有文檔的字段進(jìn)行新增和刪除(
ctx._source
绪杏,ctx._source.remove
,條件判斷) - (5)在無新增文檔的情況下或油,基于現(xiàn)有的多個(gè)字段生成新字段(加權(quán)求和寞忿,大小比較)
- (6)搜索文檔時(shí)使用script腳本
- (7)其他painless語法(循環(huán),null判斷)
script格式
語法都遵循相同的模式
"script": {
"lang": "...",
"source" | "id": "...",
"params": { ... }
}
其中三要素功能如下
-
lang
:指定編程語言顶岸,默認(rèn)是painless
腔彰,還有其他編程語言選項(xiàng)如expression
等 -
source | id
: source,id二者選其一辖佣,source后面接inline腳本(就是將腳本邏輯直接放在DSL里面)霹抛,id對(duì)應(yīng)一個(gè)stored腳本(就是預(yù)先設(shè)置類似UDF,使用的時(shí)候根據(jù)UDF的id進(jìn)行調(diào)用和傳參) -
params
:在腳本中任何有名字的參數(shù)卷谈,用params傳參
inline和stored腳本快速開始
使用script腳本修改某文檔的某個(gè)字段杯拐,先插入一條文檔
POST /hotel/_doc/100
{
"name": "蘇州木棉花酒店",
"city": "蘇州",
"price": 399,
"start_date": "2023-01-01"
}
(1)使用inline的方式將腳本寫在DSL里面
POST /hotel/_doc/100/_update
{
"script": {
"source": "ctx._source.price=333"
}
}
注意在kibiban客戶端帶上_update
,否則相當(dāng)于覆蓋整個(gè)文檔世蔗,新建了一個(gè)含有script字段的文檔端逼。本例中將price字段修改為333,如果是帶有單引號(hào)的'333'則修改為字符串?dāng)?shù)據(jù)污淋,字符串還可以使用\轉(zhuǎn)義
POST /hotel/_doc/100/_update
{
"script": {
"source": "ctx._source.price=\"333\""
}
}
獲取字段的方式除了使用ctx._source.字段
之外顶滩,還可以ctx._source['字段']
POST /hotel/_doc/100/_update
{
"script": {
"source": "ctx._source['price']=333"
}
}
只要inline腳本中的內(nèi)容出現(xiàn)些許不一樣就需要重新編譯,因此推薦的方法是把inline中固定的部分編譯一次寸爆,變量命名放在params中傳參使用礁鲁,這樣只需要編譯一次,下次使用調(diào)用緩存
POST /hotel/_doc/100/_update
{
"script": {
"source": "ctx._source.price=params.price",
"params": {
"price": 334
}
}
}
(2)使用stored預(yù)先設(shè)置腳本的方式
這種類似于先注冊(cè)UDF函數(shù)赁豆,使用PUT
對(duì)_scripts
傳入腳本
PUT /_scripts/my_script_1
{
"script": {
"lang": "painless",
"source": "ctx._source.price=params.price"
}
}
在插入之后使用GET
可以查看到對(duì)應(yīng)的腳本內(nèi)容
GET /_scripts/my_script_1
{
"_id" : "my_script_1",
"found" : true,
"script" : {
"lang" : "painless",
"source" : "ctx._source.price=params.price"
}
}
腳本中并沒有指定params仅醇,params在調(diào)用的是有進(jìn)行設(shè)置,調(diào)用的時(shí)候使用id
指定my_script_1這個(gè)id即可魔种,不再使用source
POST /hotel/_doc/100/_update
{
"script": {
"id": "my_script_1",
"params": {
"price": 335
}
}
}
script腳本更新字段
所有update/update_by_query 腳本使用 ctx._source
(1)普通字段更新
除了上面快速開始的直接使用=賦值修改的情況析二,還可以對(duì)字段做數(shù)值運(yùn)算,比如加減乘除開方等等
POST /hotel/_doc/100/_update
{
"script": {
"source": "ctx._source.price += 100"
}
}
使用Math.pow
對(duì)數(shù)值進(jìn)行開方
POST /hotel/_doc/100/_update
{
"script": {
"source": "ctx._source.price=Math.pow(ctx._source.price, 2)"
}
}
Math下的方法還有sqrt
节预,log
等
(2)集合字段更新
主要說明下數(shù)組類型字段的更新甲抖,使用ctx._source.字段.add/remove
,先新建一個(gè)帶有數(shù)組字段的文檔
POST /hotel/_doc/101
{
"name": "蘇州大酒店",
"city": "蘇州",
"tag": ["貴"]
}
使用script將tag數(shù)組字段增加元素心铃,使用add
POST /hotel/_doc/101/_update
{
"script": {
"source": "ctx._source.tag.add('偏')"
}
}
插入新元素后看下數(shù)據(jù)准谚,已經(jīng)成功
{
"_index" : "hotel",
"_type" : "_doc",
"_id" : "101",
"_score" : 1.0,
"_source" : {
"name" : "蘇州大酒店",
"city" : "蘇州",
"tag" : [
"貴",
"偏"
]
}
刪除數(shù)組元素使用remove指定對(duì)應(yīng)的索引位置即可
POST /hotel/_doc/101/_update
{
"script": {
"source": "ctx._source.tag.remove(0)"
}
}
如果位數(shù)不足會(huì)報(bào)錯(cuò)類似數(shù)組越界
script腳本對(duì)字段再加工返回
此功能使用search腳本,配合script中的doc
實(shí)現(xiàn)去扣,整體效果類似于map操作柱衔,對(duì)所選定的文檔操作返回
(1)提取日期類型的元素并返回一個(gè)自定義字段
先設(shè)置一個(gè)字段schema
POST /hotel/_doc/_mapping
{
"properties": {
"dt": {
"type": "date",
"format": "yyyy-MM-dd HH:mm:ss"
}
}
}
插入一條日期數(shù)據(jù)
POST /hotel/_doc/301
{
"dt": "2021-01-01 13:13:13"
}
插入效果如下
{
"_index" : "hotel",
"_type" : "_doc",
"_id" : "301",
"_score" : 1.0,
"_source" : {
"dt" : "2021-01-01 13:13:13"
}
下面檢索所有文檔樊破,提取日期的年份,使用GET+_search請(qǐng)求唆铐,DSL中指定script_fields的自定義字段year哲戚,給year設(shè)置script腳本
GET /hotel/_doc/_search
{
"script_fields": {
"year": {
"script": {"source": "if (doc.dt.length != 0) {doc.dt.value.year}"}
}
}
}
doc的取值方式
假設(shè)有一個(gè)字段:"a": 1,那么:
- doc['a']返回的是[1]艾岂,是一個(gè)數(shù)組顺少,如果文檔沒有該字段,返回空數(shù)組及doc['a'].length=0
- doc['a'].value返回的是1王浴,也就是取第一個(gè)元素脆炎。
- doc['a'].values與doc['a']表現(xiàn)一致,返回整個(gè)數(shù)組[1]
script_fields腳本字段
每個(gè)_search 請(qǐng)求的匹配(hit)可以使用 script_fields定制一些屬性氓辣,一個(gè) _search 請(qǐng)求能定義多于一個(gè)以上的 script field這些定制的屬性通常是:
- 針對(duì)原有值的修改(比如秒裕,價(jià)錢的轉(zhuǎn)換,不同的排序方法等)
- 一個(gè)嶄新的及算出來的屬性(比如钞啸,總和几蜻,加權(quán),指數(shù)運(yùn)算体斩,距離測(cè)量等)
script_fields在結(jié)果中的返回是{fileds: 字段名:[]}的json格式和_source同一級(jí)
doc.dt.value獲取第一個(gè)數(shù)組元素梭稚,存儲(chǔ)數(shù)據(jù)類型為amic getter [org.elasticsearch.script.JodaComp
,該類型通過year屬性獲得年份絮吵。查看以下返回結(jié)果弧烤,由于沒有篩選條件所有文檔都被返回,存在dt字段的提取年份源武,不存在dt字段的也會(huì)有返回值為null,由此可見_search + doc操作實(shí)際上是完成了原始文檔的一個(gè)映射轉(zhuǎn)換操作想幻,并產(chǎn)生了一個(gè)自定義的臨時(shí)字段粱栖,不會(huì)對(duì)原始索引做任何更改操作
{
"_index" : "hotel",
"_type" : "_doc",
"_id" : "301",
"_score" : 1.0,
"fields" : {
"year" : [
2021
]
}
},
{
"_index" : "hotel",
"_type" : "_doc",
"_id" : "002",
"_score" : 1.0,
"fields" : {
"year" : [
null
]
}
},
...
如果只返回存在dt字段的,需要在DSL中增加query
邏輯
GET /hotel/_doc/_search
{
"query": {
"exists": {
"field": "dt"
}
},
"script_fields": {
"year": {
"script": {"source": "doc.dt.value.year"}
}
}
}
(2)統(tǒng)計(jì)一個(gè)數(shù)組字段數(shù)組的和并且返回
插入一個(gè)數(shù)值數(shù)組字段脏毯,搜索統(tǒng)計(jì)返回?cái)?shù)組的和
POST /hotel/_doc/_mapping
{
"properties": {
"goals" : {"type": "keyword"}
}
}
插入數(shù)據(jù)
POST /_bulk
{"index": {"_index": "hotel", "_type": "_doc", "_id": "123"}}
{"name": "a酒店","city": "揚(yáng)州", "goals": [1, 5, 3] }
{"index": {"_index": "hotel", "_type": "_doc", "_id": "124"}}
{"name": "b酒店","city": "杭州", "goals": [9, 5, 1] }
{"index": {"_index": "hotel", "_type": "_doc", "_id": "125"}}
{"name": "c酒店","city": "云州", "goals": [2, 7, 9] }
下面計(jì)算有g(shù)oals字段的求goals的和到一個(gè)臨時(shí)字段
GET /hotel/_doc/_search
{
"query": {
"exists": {
"field": "goals"
}
},
"script_fields": {
"goals_sum": {
"script": {"source": """
int total =0;
for (int i=0; i < doc.goals.length; i++) {
total += Integer.parseInt(doc.goals[i])
}
return total
"""
}
}
}
}
在script中每一行結(jié)束要加分號(hào);
闹究,使用Java語法的循環(huán)求得數(shù)組的和,每個(gè)數(shù)組元素需要使用Java語法中的Integer.parseInt解析食店,否則報(bào)錯(cuò)String類型無法轉(zhuǎn)Num渣淤,查看返回
"hits" : [
{
"_index" : "hotel",
"_type" : "_doc",
"_id" : "123",
"_score" : 1.0,
"fields" : {
"goals_sum" : [
9
]
}
},
{
"_index" : "hotel",
"_type" : "_doc",
"_id" : "124",
"_score" : 1.0,
"fields" : {
"goals_sum" : [
15
]
}
},
{
"_index" : "hotel",
"_type" : "_doc",
"_id" : "125",
"_score" : 1.0,
"fields" : {
"goals_sum" : [
18
]
}
}
script腳本新建/刪除字段
新建字段和刪除字段都是update操作,使用ctx._source
(1)新建字段
對(duì)于存在dt字段的文檔吉嫩,新增一個(gè)字段dt_year价认,值為dt的年份
POST /hotel/_doc/_update_by_query
{
"query": {
"exists": {
"field": "dt"
}
},
"script": {
"source": "ctx._source.dt_year = ctx._source.dt.year"
}
}
以上直接在source中使用ctx._source.dt_year引入一個(gè)新列,可惜直接報(bào)錯(cuò)
"reason": "dynamic getter [java.lang.String, year] not found
此處并沒有向doc一樣數(shù)據(jù)為日期類型而是字符串自娩,因此需要引入Java解析
POST /hotel/_doc/_update_by_query
{
"query": {
"exists": {
"field": "dt"
}
},
"script": {
"source": """
LocalDateTime time2Parse = LocalDateTime.parse(ctx._source.dt, DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
ctx._source.dt_year = time2Parse.getYear()
"""
}
}
查看結(jié)果
{
"_index" : "hotel",
"_type" : "_doc",
"_id" : "301",
"_score" : 1.0,
"_source" : {
"dt" : "2021-01-01 13:13:13",
"dt_year" : 2021
}
}
也可以做其他操作比如獲得LocalDateTime類型之后再做格式化輸出
POST /hotel/_doc/_update_by_query
{
"query": {
"exists": {
"field": "dt"
}
},
"script": {
"source": """
LocalDateTime time2Parse = LocalDateTime.parse(ctx._source.dt, DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
ctx._source.dt_year = time2Parse.format(DateTimeFormatter.ofPattern("yyyy-MM-dd"))
"""
}
}
(2)刪除字段
刪除字段直接使用ctx._source.remove(\"字段名\")
用踩,可以刪除單個(gè)文檔,也可以u(píng)pdate_by_query批量刪除
POST /hotel/_doc/123
{
"script": {
"source": "ctx._source.remove(\"goals\")"
}
}
POST /hotel/_doc/_update_by_query
{
"query": {
"exists": {
"field": "goals"
}
},
"script": {
"source": "ctx._source.remove(\"goals\")"
}
}
script腳本條件判斷
支持if,else if脐彩,else碎乃,比如根據(jù)某值進(jìn)行二值判斷生成新字段
POST /hotel/_doc/_update_by_query
{
"query": {
"exists": {
"field": "price"
}
},
"script": {
"source": """
double price = ctx._source.price;
if (price >= 10) {
ctx._source.expensive = 1
} else {
ctx._source.expensive = 0
}
"""
}
}
POST /hotel/_doc/_update_by_query
{
"query": {
"exists": {
"field": "price"
}
},
"script": {
"source": """
double price = ctx._source.price;
if (price >= 10) {
ctx._source.expensive = 1
} else if (price == 0) {
ctx._source.expensive = -1
} else {
ctx._source.expensive = 0
}
"""
}
}
注意:經(jīng)過多輪測(cè)試如果source中有多輪if判斷語法會(huì)報(bào)錯(cuò),貌似只能支持一個(gè)if惠奸,解決方案是使用Java的三元表達(dá)式
?;
梅誓,三元表達(dá)式寫多少個(gè)判斷都行
script使用return
return用在_search操作中,配合script_fields使用佛南,例如在搜索結(jié)果中新增一個(gè)字段area為china梗掰,此字段不更新到索引只是在搜索時(shí)返回
GET /hotel/_doc/_search
{
"_source": true,
"script_fields": {
"area": {
"script": {
"source": "return \"china\""
}
}
}
}
以上指定"_source": true防止被script_fields覆蓋,一條輸出結(jié)果如下
{
"_index" : "hotel",
"_type" : "_doc",
"_id" : "123",
"_score" : 1.0,
"_source" : {
"city" : "揚(yáng)州",
"name" : "a酒店"
},
"fields" : {
"area" : [
"china"
]
}
script多個(gè)字段組合/邏輯判斷
(1)多個(gè)字段加權(quán)求和
先插入3個(gè)子模型分共虑,在生成一個(gè)總分愧怜,權(quán)重是0.6,0.2,0.2
POST /_bulk
{"index": {"_index": "hotel", "_type": "_doc", "_id": "333"}}
{"name": "K酒店","city": "揚(yáng)州", "model_1": 0.79, "model_2": 0.39, "model_3": 0.72}
{"index": {"_index": "hotel", "_type": "_doc", "_id": "334"}}
{"name": "L酒店","city": "江州", "model_1": 0.62, "model_2": 0.55, "model_3": 0.89}
{"index": {"_index": "hotel", "_type": "_doc", "_id": "335"}}
{"name": "S酒店","city": "兗州", "model_1": 0.83, "model_2": 0.45, "model_3": 0.58}
現(xiàn)在計(jì)算總分給到score字段
POST /hotel/_doc/_update_by_query
{
"query": {
"bool": {
"must": [
{"exists": {
"field": "model_1"
}},
{"exists": {
"field": "model_2"
}},
{"exists": {
"field": "model_3"
}}
]
}
},
"script": {
"source": "ctx._source.score = 0.6 * ctx._source.model_1 + 0.2 * ctx._source.model_2 + 0.2 * ctx._source.model_3"
}
}
看一下運(yùn)行結(jié)果
GET /hotel/_doc/_search
{
"query": {
"exists": {
"field": "score"
}
}
}
"hits" : [
{
"_index" : "hotel",
"_type" : "_doc",
"_id" : "335",
"_score" : 1.0,
"_source" : {
"score" : 0.704,
"city" : "兗州",
"name" : "S酒店",
"model_1" : 0.83,
"model_3" : 0.58,
"model_2" : 0.45
}
},
{
"_index" : "hotel",
"_type" : "_doc",
"_id" : "333",
"_score" : 1.0,
"_source" : {
"score" : 0.6960000000000001,
"city" : "揚(yáng)州",
"name" : "K酒店",
"model_1" : 0.79,
"model_3" : 0.72,
"model_2" : 0.39
}
},
...
(2)兩個(gè)字段大小比較
直接取ctx._source對(duì)應(yīng)字段進(jìn)行比較,使用Java三元表達(dá)式?:
賦值給新字段
POST /hotel/_doc/_update_by_query
{
"query": {
"bool": {
"must": [
{"exists": {
"field": "model_1"
}},
{"exists": {
"field": "model_2"
}}
]
}
},
"script": {
"source": "ctx._source.max_score = ctx._source.model_1 > ctx._source.model_2 ? ctx._source.model_1 : ctx._source.model_2"
}
}
script腳本null判斷
有兩種情況字段為null和params為null
(1)字段為null
如果某字段為空妈拌,文檔不存在該字段拥坛,則填充為0
POST /hotel/_doc/_update_by_query
{
"script": {
"source": "if (ctx._source.score == null) ctx._source.score = 0.0"
}
}
(2)params傳參為null
如果傳入params不存在某個(gè)key,則刪除該字段
POST /hotel/_doc/_update_by_query
{
"script": {
"source": """
String[] cols = new String[3];
cols[0] = "name";
cols[1] = "city";
cols[2] = "price";
for (String c : cols) {
if (params[c] == null) {
ctx._source.remove(c)
} else {
ctx._source[c] = params[c]
}
}
""",
"params": {
"name": "test",
"city": "test_loc"
}
}
}
注意:在循環(huán)中拿到局部變量c傳遞給params尘分,
params[c]
不能用點(diǎn).
或者帶有雙引號(hào)params["c"]
猜惋,否則是判斷params中是否有c這個(gè)名字的字段
在本例中使用String[] cols = new String[3];
創(chuàng)建了一個(gè)靜態(tài)變量,對(duì)于這種集合類的變量painless的語法和Java略有不同培愁,寫幾個(gè)例子如下
ArrayList l = new ArrayList(); // Declare an ArrayList variable l and set it to a newly allocated ArrayList
Map m = new HashMap(); // Declare a Map variable m and set it to a newly allocated HashMap
List l = new ArrayList(); // Declare List variable l and set it a newly allocated ArrayList
List m; // Declare List variable m and set it the default value null
int[] ia1; //Declare int[] ia1; store default null to ia1
int[] ia2 = new int[2]; //Allocate 1-d int array instance with length [2] → 1-d int array reference; store 1-d int array reference to ia1
ia2[0] = 1; //Load from ia1 → 1-d int array reference; store int 1 to index [0] of 1-d int array reference
int[][] ic2 = new int[2][5]; //Declare int[][] ic2; allocate 2-d int array instance with length [2, 5] → 2-d int array reference; store 2-d int array reference to ic2
ic2[1][3] = 2; //Load from ic2 → 2-d int array reference; store int 2 to index [1, 3] of 2-d int array reference
ic2[0] = ia1; //Load from ia1 → 1-d int array reference; load from ic2 → 2-d int array reference; store 1-d int array reference to index [0] of 2-d int array reference; (note ia1, ib1, and index [0] of ia2 refer to the same instance)
List著摔,Map這些集合都沒有泛型,并且集合的值貌似不能直接初始化定续,需要add谍咆,put進(jìn)來
script作為查詢過濾條件
查看某列的值大于某列,在query下可以使用script私股,注意格式script下還套著一個(gè)script摹察,search請(qǐng)求使用doc獲取值
GET /hotel/_doc/_search
{
"query": {
"script" : {
"script" : {
"source": "doc.score.value < doc.model_3.value"
}
}
}
}
以上語句會(huì)報(bào)warn,doc選取字段如果字段為空會(huì)填充默認(rèn)值倡鲸,因此再限制一下字段不為空
GET /hotel/_doc/_search
{
"query": {
"bool" : {
"must" : [{
"script" : {
"script" : {
"source": "doc.score.value < doc.model_3.value"
}
}
},
{"exists": {"field": "score"}},
{"exists": {"field": "model_3"}}
]
}
}
}