• Elasticsearch:painless script 语法基础和实战


    摘要:ElasticsearchJava

    script的作用

    script是Elasticsearch的拓展功能,通过定制的表达式实现已经预设好的API无法完成的个性化需求,比如完成以下操作

    • 字段再加工/统计输出
    • 字段之间逻辑运算
    • 定义查询得分的计算公式
    • 定义特殊过滤条件完成搜索
    • 类似于pandas的个性化增删改操作
    内容概述
    • (1)script格式说明,inline和stored脚本的调用方法
    • (2)在无新增文档的情况下,对现有文档的字段个性化字段更新(update_update_by_queryctx._source,Math,数组add/remove)
    • (3)在不修改文档的情况下,在搜索返回中添加个性化统计字段(_searchdocscript_fieldsreturn
    • (4)在无新增文档的情况下,对现有文档的字段进行新增和删除(ctx._sourcectx._source.remove,条件判断)
    • (5)在无新增文档的情况下,基于现有的多个字段生成新字段(加权求和,大小比较)
    • (6)搜索文档时使用script脚本
    • (7)其他painless语法(循环,null判断)

    script格式

    语法都遵循相同的模式

    1. "script": {
    2. "lang": "...",
    3. "source" | "id": "...",
    4. "params": { ... }
    5. }

    其中三要素功能如下

    • lang:指定编程语言,默认是painless,还有其他编程语言选项如expression
    • source | id: source,id二者选其一,source后面接inline脚本(就是将脚本逻辑直接放在DSL里面),id对应一个stored脚本(就是预先设置类似UDF,使用的时候根据UDF的id进行调用和传参
    • params:在脚本中任何有名字的参数,用params传参

    inline和stored脚本快速开始

    使用script脚本修改某文档的某个字段,先插入一条文档

    1. POST /hotel/_doc/100
    2. {
    3. "name": "苏州木棉花酒店",
    4. "city": "苏州",
    5. "price": 399,
    6. "start_date": "2023-01-01"
    7. }
    (1)使用inline的方式将脚本写在DSL里面

    1. POST /hotel/_doc/100/_update
    2. {
    3. "script": {
    4. "source": "ctx._source.price=333"
    5. }
    6. }

    注意在kibiban客户端带上_update,否则相当于覆盖整个文档,新建了一个含有script字段的文档。本例中将price字段修改为333,如果是带有单引号的'333'则修改为字符串数据,字符串还可以使用\转义

    1. POST /hotel/_doc/100/_update
    2. {
    3. "script": {
    4. "source": "ctx._source.price=\"333\""
    5. }
    6. }

    获取字段的方式除了使用ctx._source.字段之外,还可以ctx._source['字段']

    1. POST /hotel/_doc/100/_update
    2. {
    3. "script": {
    4. "source": "ctx._source['price']=333"
    5. }
    6. }

    只要inline脚本中的内容出现些许不一样就需要重新编译,因此推荐的方法是把inline中固定的部分编译一次,变量命名放在params中传参使用,这样只需要编译一次,下次使用调用缓存

    1. POST /hotel/_doc/100/_update
    2. {
    3. "script": {
    4. "source": "ctx._source.price=params.price",
    5. "params": {
    6. "price": 334
    7. }
    8. }
    9. }
    (2)使用stored预先设置脚本的方式

    这种类似于先注册UDF函数,使用PUT_scripts传入脚本

    1. PUT /_scripts/my_script_1
    2. {
    3. "script": {
    4. "lang": "painless",
    5. "source": "ctx._source.price=params.price"
    6. }
    7. }

    在插入之后使用GET可以查看到对应的脚本内容

    1. GET /_scripts/my_script_1
    2. {
    3. "_id" : "my_script_1",
    4. "found" : true,
    5. "script" : {
    6. "lang" : "painless",
    7. "source" : "ctx._source.price=params.price"
    8. }
    9. }

    脚本中并没有指定params,params在调用的是有进行设置,调用的时候使用id指定my_script_1这个id即可,不再使用source

    1. POST /hotel/_doc/100/_update
    2. {
    3. "script": {
    4. "id": "my_script_1",
    5. "params": {
    6. "price": 335
    7. }
    8. }
    9. }

    script脚本更新字段

    所有update/update_by_query 脚本使用 ctx._source

    (1)普通字段更新

    除了上面快速开始的直接使用=赋值修改的情况,还可以对字段做数值运算,比如加减乘除开方等等

    1. POST /hotel/_doc/100/_update
    2. {
    3. "script": {
    4. "source": "ctx._source.price += 100"
    5. }
    6. }

    使用Math.pow对数值进行开方

    1. POST /hotel/_doc/100/_update
    2. {
    3. "script": {
    4. "source": "ctx._source.price=Math.pow(ctx._source.price, 2)"
    5. }
    6. }

    Math下的方法还有sqrtlog

    (2)集合字段更新

    主要说明下数组类型字段的更新,使用ctx._source.字段.add/remove,先新建一个带有数组字段的文档

    1. POST /hotel/_doc/101
    2. {
    3. "name": "苏州大酒店",
    4. "city": "苏州",
    5. "tag": ["贵"]
    6. }

    使用script将tag数组字段增加元素,使用add

    1. POST /hotel/_doc/101/_update
    2. {
    3. "script": {
    4. "source": "ctx._source.tag.add('偏')"
    5. }
    6. }

    插入新元素后看下数据,已经成功

    1. {
    2. "_index" : "hotel",
    3. "_type" : "_doc",
    4. "_id" : "101",
    5. "_score" : 1.0,
    6. "_source" : {
    7. "name" : "苏州大酒店",
    8. "city" : "苏州",
    9. "tag" : [
    10. "贵",
    11. "偏"
    12. ]
    13. }

    删除数组元素使用remove指定对应的索引位置即可

    1. POST /hotel/_doc/101/_update
    2. {
    3. "script": {
    4. "source": "ctx._source.tag.remove(0)"
    5. }
    6. }

    如果位数不足会报错类似数组越界


    script脚本对字段再加工返回

    此功能使用search脚本,配合script中的doc实现,整体效果类似于map操作,对所选定的文档操作返回

    (1)提取日期类型的元素并返回一个自定义字段

    先设置一个字段schema

    1. POST /hotel/_doc/_mapping
    2. {
    3. "properties": {
    4. "dt": {
    5. "type": "date",
    6. "format": "yyyy-MM-dd HH:mm:ss"
    7. }
    8. }
    9. }

    插入一条日期数据

    1. POST /hotel/_doc/301
    2. {
    3. "dt": "2021-01-01 13:13:13"
    4. }

    插入效果如下

    1. {
    2. "_index" : "hotel",
    3. "_type" : "_doc",
    4. "_id" : "301",
    5. "_score" : 1.0,
    6. "_source" : {
    7. "dt" : "2021-01-01 13:13:13"
    8. }

    下面检索所有文档,提取日期的年份,使用GET+_search请求,DSL中指定script_fields的自定义字段year,给year设置script脚本

    1. GET /hotel/_doc/_search
    2. {
    3. "script_fields": {
    4. "year": {
    5. "script": {"source": "if (doc.dt.length != 0) {doc.dt.value.year}"}
    6. }
    7. }
    8. }

    doc的取值方式
    假设有一个字段:"a": 1,那么:

    • doc['a']返回的是[1],是一个数组,如果文档没有该字段,返回空数组及doc['a'].length=0
    • doc['a'].value返回的是1,也就是取第一个元素。
    • doc['a'].values与doc['a']表现一致,返回整个数组[1]

    script_fields脚本字段
    每个_search 请求的匹配(hit)可以使用 script_fields定制一些属性,一个 _search 请求能定义多于一个以上的 script field这些定制的属性通常是:

    • 针对原有值的修改(比如,价钱的转换,不同的排序方法等)
    • 一个崭新的及算出来的属性(比如,总和,加权,指数运算,距离测量等)

    script_fields在结果中的返回是{fileds: 字段名:[]}的json格式和_source同一级

    doc.dt.value获取第一个数组元素,存储数据类型为amic getter [org.elasticsearch.script.JodaComp,该类型通过year属性获得年份。查看以下返回结果,由于没有筛选条件所有文档都被返回,存在dt字段的提取年份,不存在dt字段的也会有返回值为null,由此可见_search + doc操作实际上是完成了原始文档的一个映射转换操作,并产生了一个自定义的临时字段,不会对原始索引做任何更改操作

    1. {
    2. "_index" : "hotel",
    3. "_type" : "_doc",
    4. "_id" : "301",
    5. "_score" : 1.0,
    6. "fields" : {
    7. "year" : [
    8. 2021
    9. ]
    10. }
    11. },
    12. {
    13. "_index" : "hotel",
    14. "_type" : "_doc",
    15. "_id" : "002",
    16. "_score" : 1.0,
    17. "fields" : {
    18. "year" : [
    19. null
    20. ]
    21. }
    22. },
    23. ...

    如果只返回存在dt字段的,需要在DSL中增加query逻辑

    1. GET /hotel/_doc/_search
    2. {
    3. "query": {
    4. "exists": {
    5. "field": "dt"
    6. }
    7. },
    8. "script_fields": {
    9. "year": {
    10. "script": {"source": "doc.dt.value.year"}
    11. }
    12. }
    13. }
    (2)统计一个数组字段数组的和并且返回

    插入一个数值数组字段,搜索统计返回数组的和

    1. POST /hotel/_doc/_mapping
    2. {
    3. "properties": {
    4. "goals" : {"type": "keyword"}
    5. }
    6. }

    插入数据

    1. POST /_bulk
    2. {"index": {"_index": "hotel", "_type": "_doc", "_id": "123"}}
    3. {"name": "a酒店","city": "扬州", "goals": [1, 5, 3] }
    4. {"index": {"_index": "hotel", "_type": "_doc", "_id": "124"}}
    5. {"name": "b酒店","city": "杭州", "goals": [9, 5, 1] }
    6. {"index": {"_index": "hotel", "_type": "_doc", "_id": "125"}}
    7. {"name": "c酒店","city": "云州", "goals": [2, 7, 9] }

    下面计算有goals字段的求goals的和到一个临时字段

    1. GET /hotel/_doc/_search
    2. {
    3. "query": {
    4. "exists": {
    5. "field": "goals"
    6. }
    7. },
    8. "script_fields": {
    9. "goals_sum": {
    10. "script": {"source": """
    11. int total =0;
    12. for (int i=0; i < doc.goals.length; i++) {
    13. total += Integer.parseInt(doc.goals[i])
    14. }
    15. return total
    16. """
    17. }
    18. }
    19. }
    20. }

    在script中每一行结束要加分号;,使用Java语法的循环求得数组的和,每个数组元素需要使用Java语法中的Integer.parseInt解析,否则报错String类型无法转Num,查看返回

    1. "hits" : [
    2. {
    3. "_index" : "hotel",
    4. "_type" : "_doc",
    5. "_id" : "123",
    6. "_score" : 1.0,
    7. "fields" : {
    8. "goals_sum" : [
    9. 9
    10. ]
    11. }
    12. },
    13. {
    14. "_index" : "hotel",
    15. "_type" : "_doc",
    16. "_id" : "124",
    17. "_score" : 1.0,
    18. "fields" : {
    19. "goals_sum" : [
    20. 15
    21. ]
    22. }
    23. },
    24. {
    25. "_index" : "hotel",
    26. "_type" : "_doc",
    27. "_id" : "125",
    28. "_score" : 1.0,
    29. "fields" : {
    30. "goals_sum" : [
    31. 18
    32. ]
    33. }
    34. }

    script脚本新建/删除字段

    新建字段和删除字段都是update操作,使用ctx._source

    (1)新建字段

    对于存在dt字段的文档,新增一个字段dt_year,值为dt的年份

    1. POST /hotel/_doc/_update_by_query
    2. {
    3. "query": {
    4. "exists": {
    5. "field": "dt"
    6. }
    7. },
    8. "script": {
    9. "source": "ctx._source.dt_year = ctx._source.dt.year"
    10. }
    11. }

    以上直接在source中使用ctx._source.dt_year引入一个新列,可惜直接报错

    1. "reason": "dynamic getter [java.lang.String, year] not found

    此处并没有向doc一样数据为日期类型而是字符串,因此需要引入Java解析

    1. POST /hotel/_doc/_update_by_query
    2. {
    3. "query": {
    4. "exists": {
    5. "field": "dt"
    6. }
    7. },
    8. "script": {
    9. "source": """
    10. LocalDateTime time2Parse = LocalDateTime.parse(ctx._source.dt, DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
    11. ctx._source.dt_year = time2Parse.getYear()
    12. """
    13. }
    14. }

    查看结果

    1. {
    2. "_index" : "hotel",
    3. "_type" : "_doc",
    4. "_id" : "301",
    5. "_score" : 1.0,
    6. "_source" : {
    7. "dt" : "2021-01-01 13:13:13",
    8. "dt_year" : 2021
    9. }
    10. }

    也可以做其他操作比如获得LocalDateTime类型之后再做格式化输出

    1. POST /hotel/_doc/_update_by_query
    2. {
    3. "query": {
    4. "exists": {
    5. "field": "dt"
    6. }
    7. },
    8. "script": {
    9. "source": """
    10. LocalDateTime time2Parse = LocalDateTime.parse(ctx._source.dt, DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
    11. ctx._source.dt_year = time2Parse.format(DateTimeFormatter.ofPattern("yyyy-MM-dd"))
    12. """
    13. }
    14. }
    (2)删除字段

    删除字段直接使用ctx._source.remove(\"字段名\"),可以删除单个文档,也可以update_by_query批量删除

    1. POST /hotel/_doc/123
    2. {
    3. "script": {
    4. "source": "ctx._source.remove(\"goals\")"
    5. }
    6. }

    1. POST /hotel/_doc/_update_by_query
    2. {
    3. "query": {
    4. "exists": {
    5. "field": "goals"
    6. }
    7. },
    8. "script": {
    9. "source": "ctx._source.remove(\"goals\")"
    10. }
    11. }

    script脚本条件判断

    支持if,else if,else,比如根据某值进行二值判断生成新字段

    1. POST /hotel/_doc/_update_by_query
    2. {
    3. "query": {
    4. "exists": {
    5. "field": "price"
    6. }
    7. },
    8. "script": {
    9. "source": """
    10. double price = ctx._source.price;
    11. if (price >= 10) {
    12. ctx._source.expensive = 1
    13. } else {
    14. ctx._source.expensive = 0
    15. }
    16. """
    17. }
    18. }

    1. POST /hotel/_doc/_update_by_query
    2. {
    3. "query": {
    4. "exists": {
    5. "field": "price"
    6. }
    7. },
    8. "script": {
    9. "source": """
    10. double price = ctx._source.price;
    11. if (price >= 10) {
    12. ctx._source.expensive = 1
    13. } else if (price == 0) {
    14. ctx._source.expensive = -1
    15. } else {
    16. ctx._source.expensive = 0
    17. }
    18. """
    19. }
    20. }

    注意:经过多轮测试如果source中有多轮if判断语法会报错,貌似只能支持一个if,解决方案是使用Java的三元表达式?;三元表达式写多少个判断都行


    script使用return

    return用在_search操作中,配合script_fields使用,例如在搜索结果中新增一个字段area为china,此字段不更新到索引只是在搜索时返回

    1. GET /hotel/_doc/_search
    2. {
    3. "_source": true,
    4. "script_fields": {
    5. "area": {
    6. "script": {
    7. "source": "return \"china\""
    8. }
    9. }
    10. }
    11. }

    以上指定"_source": true防止被script_fields覆盖,一条输出结果如下

    1. {
    2. "_index" : "hotel",
    3. "_type" : "_doc",
    4. "_id" : "123",
    5. "_score" : 1.0,
    6. "_source" : {
    7. "city" : "扬州",
    8. "name" : "a酒店"
    9. },
    10. "fields" : {
    11. "area" : [
    12. "china"
    13. ]
    14. }

    script多个字段组合/逻辑判断
    (1)多个字段加权求和

    先插入3个子模型分,在生成一个总分,权重是0.6,0.2,0.2

    1. POST /_bulk
    2. {"index": {"_index": "hotel", "_type": "_doc", "_id": "333"}}
    3. {"name": "K酒店","city": "扬州", "model_1": 0.79, "model_2": 0.39, "model_3": 0.72}
    4. {"index": {"_index": "hotel", "_type": "_doc", "_id": "334"}}
    5. {"name": "L酒店","city": "江州", "model_1": 0.62, "model_2": 0.55, "model_3": 0.89}
    6. {"index": {"_index": "hotel", "_type": "_doc", "_id": "335"}}
    7. {"name": "S酒店","city": "兖州", "model_1": 0.83, "model_2": 0.45, "model_3": 0.58}

    现在计算总分给到score字段

    1. POST /hotel/_doc/_update_by_query
    2. {
    3. "query": {
    4. "bool": {
    5. "must": [
    6. {"exists": {
    7. "field": "model_1"
    8. }},
    9. {"exists": {
    10. "field": "model_2"
    11. }},
    12. {"exists": {
    13. "field": "model_3"
    14. }}
    15. ]
    16. }
    17. },
    18. "script": {
    19. "source": "ctx._source.score = 0.6 * ctx._source.model_1 + 0.2 * ctx._source.model_2 + 0.2 * ctx._source.model_3"
    20. }
    21. }

    看一下运行结果

    1. GET /hotel/_doc/_search
    2. {
    3. "query": {
    4. "exists": {
    5. "field": "score"
    6. }
    7. }
    8. }

    1. "hits" : [
    2. {
    3. "_index" : "hotel",
    4. "_type" : "_doc",
    5. "_id" : "335",
    6. "_score" : 1.0,
    7. "_source" : {
    8. "score" : 0.704,
    9. "city" : "兖州",
    10. "name" : "S酒店",
    11. "model_1" : 0.83,
    12. "model_3" : 0.58,
    13. "model_2" : 0.45
    14. }
    15. },
    16. {
    17. "_index" : "hotel",
    18. "_type" : "_doc",
    19. "_id" : "333",
    20. "_score" : 1.0,
    21. "_source" : {
    22. "score" : 0.6960000000000001,
    23. "city" : "扬州",
    24. "name" : "K酒店",
    25. "model_1" : 0.79,
    26. "model_3" : 0.72,
    27. "model_2" : 0.39
    28. }
    29. },
    30. ...
    (2)两个字段大小比较

    直接取ctx._source对应字段进行比较,使用Java三元表达式?:赋值给新字段

    1. POST /hotel/_doc/_update_by_query
    2. {
    3. "query": {
    4. "bool": {
    5. "must": [
    6. {"exists": {
    7. "field": "model_1"
    8. }},
    9. {"exists": {
    10. "field": "model_2"
    11. }}
    12. ]
    13. }
    14. },
    15. "script": {
    16. "source": "ctx._source.max_score = ctx._source.model_1 > ctx._source.model_2 ? ctx._source.model_1 : ctx._source.model_2"
    17. }
    18. }

    script脚本null判断

    有两种情况字段为null和params为null

    (1)字段为null

    如果某字段为空,文档不存在该字段,则填充为0

    1. POST /hotel/_doc/_update_by_query
    2. {
    3. "script": {
    4. "source": "if (ctx._source.score == null) ctx._source.score = 0.0"
    5. }
    6. }
    (2)params传参为null

    如果传入params不存在某个key,则删除该字段

    1. POST /hotel/_doc/_update_by_query
    2. {
    3. "script": {
    4. "source": """
    5. String[] cols = new String[3];
    6. cols[0] = "name";
    7. cols[1] = "city";
    8. cols[2] = "price";
    9. for (String c : cols) {
    10. if (params[c] == null) {
    11. ctx._source.remove(c)
    12. } else {
    13. ctx._source[c] = params[c]
    14. }
    15. }
    16. """,
    17. "params": {
    18. "name": "test",
    19. "city": "test_loc"
    20. }
    21. }
    22. }

    注意:在循环中拿到局部变量c传递给params,params[c]不能用点.或者带有双引号params["c"],否则是判断params中是否有c这个名字的字段

    在本例中使用String[] cols = new String[3];创建了一个静态变量,对于这种集合类的变量painless的语法和Java略有不同,写几个例子如下

    1. ArrayList l = new ArrayList(); // Declare an ArrayList variable l and set it to a newly allocated ArrayList
    2. Map m = new HashMap(); // Declare a Map variable m and set it to a newly allocated HashMap
    3. List l = new ArrayList(); // Declare List variable l and set it a newly allocated ArrayList
    4. List m; // Declare List variable m and set it the default value null
    5. int[] ia1; //Declare int[] ia1; store default null to ia1
    6. 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
    7. ia2[0] = 1; //Load from ia11-d int array reference; store int 1 to index [0] of 1-d int array reference
    8. 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
    9. ic2[1][3] = 2; //Load from ic22-d int array reference; store int 2 to index [1, 3] of 2-d int array reference
    10. ic2[0] = ia1; //Load from ia11-d int array reference; load from ic22-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进来


    script作为查询过滤条件

    查看某列的值大于某列,在query下可以使用script,注意格式script下还套着一个script,search请求使用doc获取值

    1. GET /hotel/_doc/_search
    2. {
    3. "query": {
    4. "script" : {
    5. "script" : {
    6. "source": "doc.score.value < doc.model_3.value"
    7. }
    8. }
    9. }
    10. }

    以上语句会报warn,doc选取字段如果字段为空会填充默认值,因此再限制一下字段不为空

    1. GET /hotel/_doc/_search
    2. {
    3. "query": {
    4. "bool" : {
    5. "must" : [{
    6. "script" : {
    7. "script" : {
    8. "source": "doc.score.value < doc.model_3.value"
    9. }
    10. }
    11. },
    12. {"exists": {"field": "score"}},
    13. {"exists": {"field": "model_3"}}
    14. ]
    15. }
    16. }
    17. }



    作者:xiaogp
    链接:https://www.jianshu.com/p/66a72d7ba3da
    来源:简书
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

  • 相关阅读:
    SPA项目实现登录注册
    pytorch中nn.Parameter()使用方法
    单节点k8s—自签名证书—四层负载均衡—helm安装rancher
    【分享】GIS领域论坛社区
    爱尔兰药品局药品信息数据查询
    Java学习笔记:SQLite数据库
    Git企业开发级讲解(一)
    查看进程:让查看进程变得才貌双绝 - procs
    swift-基础
    减轻关键基础设施网络安全风险的 3 种方法
  • 原文地址:https://blog.csdn.net/qq_32907195/article/details/133931759