Elasticsearch:使用新的 field API 简化 Painless 语法和文档字段访问 - Elastic Stack 8.1

349 阅读5分钟

在 8.1 中,我们引入了从 Painless 脚本轻松访问文档中的字段的功能。 你可以使用新的 field API 访问文档以自动处理缺失值,而不是使用需要逻辑来检查字段及其值是否存在的 doc 变量方法。

字段 API 返回一个字段对象,该对象对具有多个值的字段进行迭代,通过 get(<default_value>) 方法以及类型转换和辅助方法提供对基础值的访问。 它还返回你指定的默认值,无论该字段是否存在或是否具有文档的任何值。 这意味着字段 API 可以处理缺失值,而无需额外的逻辑。

field(‘name’).get(<default_value>)

为了使脚本更具可读性,你还可以使用新的 快捷方式。确保包含快捷方式。 确保包含 符号、字段名称和在字段不存在时要获取的默认值:

$(‘field’, <default_value>)

借助这些增强的功能和简化的语法,你可以编写更短且更易于阅读的脚本。 例如,以下脚本使用过时的语法:

if (!doc.containsKey('myfield') || doc['myfield'].empty) { return "unavailable" } else { return doc['myfield'].value }

使用 field API,你现在可以更简洁地编写相同的脚本,而无需额外的逻辑来确定字段是否存在,然后再对它们进行操作:

$(‘myfield’, ‘unavailable’)

展示

在下面,我们来通过一些例子来进行展示。我们首先选择在之前的文章 “Elasticsearch:Painless scripting 编程实践” 文章中的例子来进行展示。



1.  PUT employee/_bulk?refresh
2.  {"index":{"_id": 1}}
3.  { "salary" : 5000, "bonus": 500, "@timestamp" : "2021-02-28", "weight": 60, "height": 175, "name" : "Peter", "occupation": "software engineer","hobbies": ["dancing", "badminton"]}
4.  {"index":{"_id": 2}}
5.  { "salary" : 6000, "bonus": 500, "@timestamp" : "2020-02-01", "weight": 50, "name" : "John", "occupation": "sales", "hobbies":["singing", "volleyball"]}
6.  {"index":{"_id": 3}}
7.  { "salary" : 7000, "bonus": 600, "@timestamp" : "2019-03-01", "weight": 55, "height": 172, "name" : "mary", "occupation": "manager", "hobbies":["dancing", "tennis"]}
8.  {"index":{"_id": 4}}
9.  { "salary" : 8000, "bonus": 700, "@timestamp" : "2018-02-28", "weight": 45, "height": 166, "name" : "jerry", "occupation": "sales", "hobbies":["biking", "swimming"]}
10.  {"index":{"_id": 5}}
11.  { "salary" : 9000, "bonus": 800, "@timestamp" : "2017-02-01", "weight": 60, "height": 170, "name" : "cathy", "occupation": "manager", "hobbies":["climbing", "jigging"]}
12.  {"index":{"_id": 6}}
13.  { "salary" : 7500, "bonus": 500, "@timestamp" : "2017-03-01", "weight": 40, "height": 158, "name" : "cherry", "occupation": "software engineer", "hobbies":["basketball", "yoga"]}


在上面,我需要特别指出的是针对 id 为 2 的文档:

{ "salary" : 6000, "bonus": 500, "@timestamp" : "2020-02-01", "weight": 50, "name" : "John", "occupation": "sales", "hobbies":["singing", "volleyball"]}

我特别有意地省去了它的 height 字段,也即其它的文档都含有这个 height 字段。

GET employee/_doc/2


1.  {
2.    "_index" : "employee",
3.    "_id" : "2",
4.    "_version" : 1,
5.    "_seq_no" : 1,
6.    "_primary_term" : 1,
7.    "found" : true,
8.    "_source" : {
9.      "salary" : 6000,
10.      "bonus" : 500,
11.      "@timestamp" : "2020-02-01",
12.      "weight" : 50,
13.      "name" : "John",
14.      "occupation" : "sales",
15.      "hobbies" : [
16.        "singing",
17.        "volleyball"
18.      ]
19.    }
20.  }


我们接下来想得到这些员工的 BMI 值。我们可以参考之前文章  “Elasticsearch:Painless scripting 编程实践” 中介绍的方法来创建一个 scripted field:



1.  GET employee/_search
2.  {
3.    "script_fields": {
4.      "BMI": {
5.        "script": {
6.          "source": """
7.            double height = (float)doc['height'].value/100.0;
8.            return doc['weight'].value / (height*height)
9.          """
10.        }
11.      }
12.    }
13.  }


如果我们运行上面的代码,我们会发现如下的错误信息:

很显然这个错误的信息是由于我们的一个文档缺少 height 这个字段而造成的。一种修改这种问题的办法是添加一些代码来完成,比如:



1.  GET employee/_search?filter_path=**.hits
2.  {
3.    "script_fields": {
4.      "BMI": {
5.        "script": {
6.          "source": """
7.            double height = 0;
8.            if(doc['height'].size() == 0) {
9.              height = 170/100.0;
10.            } else {
11.              height = (double)doc['height'].value/100.0;
12.            }
13.            return doc['weight'].value / (height*height);
14.          """
15.        }
16.      }
17.    }
18.  }


在上面,当 height 字段缺失的情况下,我们设置一个默认值为 170,这样避免了在计算中的缺失。上面的命令返回的结果为:



1.  {
2.    "hits" : {
3.      "hits" : [
4.        {
5.          "_index" : "employee",
6.          "_id" : "1",
7.          "_score" : 1.0,
8.          "fields" : {
9.            "BMI" : [
10.              19.591836734693878
11.            ]
12.          }
13.        },
14.        {
15.          "_index" : "employee",
16.          "_id" : "2",
17.          "_score" : 1.0,
18.          "fields" : {
19.            "BMI" : [
20.              17.301038062283737
21.            ]
22.          }
23.        },
24.        {
25.          "_index" : "employee",
26.          "_id" : "3",
27.          "_score" : 1.0,
28.          "fields" : {
29.            "BMI" : [
30.              18.591130340724717
31.            ]
32.          }
33.        },
34.        {
35.          "_index" : "employee",
36.          "_id" : "4",
37.          "_score" : 1.0,
38.          "fields" : {
39.            "BMI" : [
40.              16.330381768036002
41.            ]
42.          }
43.        },
44.        {
45.          "_index" : "employee",
46.          "_id" : "5",
47.          "_score" : 1.0,
48.          "fields" : {
49.            "BMI" : [
50.              20.761245674740486
51.            ]
52.          }
53.        },
54.        {
55.          "_index" : "employee",
56.          "_id" : "6",
57.          "_score" : 1.0,
58.          "fields" : {
59.            "BMI" : [
60.              16.023073225444637
61.            ]
62.          }
63.        }
64.      ]
65.    }
66.  }


针对 id 为 2 的情况,它计算出来的 BMI 为 17.301038062283737。

由于有了新的 field API,那么我们可以简化上面的计算步骤为:



1.  GET employee/_search?filter_path=**.hits
2.  {
3.    "script_fields": {
4.      "BMI": {
5.        "script": {
6.          "source": """
7.            double height = (double)$('height', 170)/100.0;
8.            return doc['weight'].value / (height*height)
9.          """
10.        }
11.      }
12.    }
13.  }


或者:



1.  GET employee/_search?filter_path=**.hits
2.  {
3.    "script_fields": {
4.      "BMI": {
5.        "script": {
6.          "source": """
7.            double height = (double)field('height').get(170)/100.0;
8.            return doc['weight'].value / (height*height)
9.          """
10.        }
11.      }
12.    }
13.  }


上面的代码非常之简单明了。当 height 字段不存在时,我们使用 170 来代替。上面运行的结果为:



1.  {
2.    "hits" : {
3.      "hits" : [
4.        {
5.          "_index" : "employee",
6.          "_id" : "1",
7.          "_score" : 1.0,
8.          "fields" : {
9.            "BMI" : [
10.              19.591836734693878
11.            ]
12.          }
13.        },
14.        {
15.          "_index" : "employee",
16.          "_id" : "2",
17.          "_score" : 1.0,
18.          "fields" : {
19.            "BMI" : [
20.              17.301038062283737
21.            ]
22.          }
23.        },
24.        {
25.          "_index" : "employee",
26.          "_id" : "3",
27.          "_score" : 1.0,
28.          "fields" : {
29.            "BMI" : [
30.              18.591130340724717
31.            ]
32.          }
33.        },
34.        {
35.          "_index" : "employee",
36.          "_id" : "4",
37.          "_score" : 1.0,
38.          "fields" : {
39.            "BMI" : [
40.              16.330381768036002
41.            ]
42.          }
43.        },
44.        {
45.          "_index" : "employee",
46.          "_id" : "5",
47.          "_score" : 1.0,
48.          "fields" : {
49.            "BMI" : [
50.              20.761245674740486
51.            ]
52.          }
53.        },
54.        {
55.          "_index" : "employee",
56.          "_id" : "6",
57.          "_score" : 1.0,
58.          "fields" : {
59.            "BMI" : [
60.              16.023073225444637
61.            ]
62.          }
63.        }
64.      ]
65.    }
66.  }


从上面的结果中,我们可以看出来它是一样的结果。

注意:field API 仍在开发中,应被视为 beta 功能。 API 可能会发生变化,并且此迭代可能不是最终状态。 对于功能状态,请参阅  #78920 。某些字段与 field API 不兼容,例如文本或地理字段。 继续使用 doc 访问字段 API 不支持的字段类型。