Elasticsearch:painless script 語法基礎(chǔ)和實(shí)戰(zhàn)

摘要:ElasticsearchJava

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,docscript_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"}}
          ]
        }
    }
}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容