在今天的文章中,我将列举一些例子来讲述使用 Elasticsearch ingest pipeline (摄取管道)的一些技巧。这些技巧虽然简单,但是在很多的应用场景中还是非常实用的。更多关于 ingest pipeline 的文章,请详细阅读我们之前的文章 “Elastic:开发者上手指南” 里的 “Ingest pipeline” 章节。
为文档添加 last_update_time
摄取管道以两种方式帮助处理数据:
- 在最终摄取到 Elasticsearch 数据节点之前对文档进行预处理(又名数据处理过程)
- 根据增强的业务需求对现有文档进行数据修复(也称为数据修补)
无论是预处理阶段还是数据修复阶段,通常都有一个共同的目标 => 添加一个 last_update_time 时间戳字段来标识何时施加了更改。
添加回当前时间戳也有几种方法。
是在文档的预处理阶段重新使用摄取时间戳
我们使用如下的命令来模拟一个 ingest pipeline:
1. POST _ingest/pipeline/_simulate
2. {
3. "pipeline": {
4. "description": "PRE-PROCESSING: add back last_update_time by using the ingest object",
5. "processors": [
6. {
7. "set": {
8. "field": "last_update_time",
9. "value": "{{_ingest.timestamp}}"
10. }
11. }
12. ]
13. },
14. "docs": [
15. {
16. "_source": {
17. "action": "an action",
18. "user": "balabala",
19. "procurement_id": "12_yuy190"
20. }
21. }
22. ]
23. }
上面命令运行的结果是:
1. {
2. "docs": [
3. {
4. "doc": {
5. "_index": "_index",
6. "_id": "_id",
7. "_version": "-3",
8. "_source": {
9. "action": "an action",
10. "last_update_time": "2023-03-09T00:02:35.886218305Z",
11. "procurement_id": "12_yuy190",
12. "user": "balabala"
13. },
14. "_ingest": {
15. "timestamp": "2023-03-09T00:02:35.886218305Z"
16. }
17. }
18. }
19. ]
20. }
在上面,我们通过设置 last_update_time 为 ingest pipeline 运行的时间来达到记录处理事件的时间。
通过添加脚本处理器并以脚本方式创建当前时间戳
1. POST _ingest/pipeline/_simulate
2. {
3. "pipeline": {
4. "description": "any stage: add back last_update_time by script",
5. "processors": [
6. {
7. "script": {
8. "lang": "painless",
9. "source": """
10. def ts = new Date();
11. ctx.last_update_time = ts;
12. """
13. }
14. }
15. ]
16. },
17. "docs": [
18. {
19. "_source": {
20. "action": "an action",
21. "user": "balabala",
22. "procurement_id": "12_yuy190"
23. }
24. }
25. ]
26. }
模拟的结果是:
1. {
2. "docs": [
3. {
4. "doc": {
5. "_index": "_index",
6. "_id": "_id",
7. "_version": "-3",
8. "_source": {
9. "action": "an action",
10. "last_update_time": "2023-03-09T00:06:00.095Z",
11. "procurement_id": "12_yuy190",
12. "user": "balabala"
13. },
14. "_ingest": {
15. "timestamp": "2023-03-09T00:06:00.095106511Z"
16. }
17. }
18. }
19. ]
20. }
我们通过脚本活动当前机器的时间,并把这个时间设置为 last_update_time。有趣的是,你现在可以看到 last_update_time 与 _ingest.timestamp 略有不同,因为脚本处理器在摄取之前运行,因此 _ingest.timestamp 应该稍晚一些。
管道设计方法
现在我们对添加当前时间戳的方法进行了测试; 但是随后许多数据消息处理过程需要在他们的工作流程中添加回这个时间戳信息,这是否意味着你每次创建新管道时都需要复制并粘贴上述代码/处理器???
从 Elasticsearch 6.5.x 开始,有一个新的 “pipeline” 处理器,我们可以在其中调用当前管道中的另一个管道。 现在这真的很酷,所有管道突然都像函数/API;你可以在必要时调用其中任何一个,因此可以重用代码。
基于这个新特性,你可能需要重构现有的 pipeline 代码,将常见的业务逻辑提取出来,形成一个 function pipeline,供其他 main-stream-pipelines 调用。 在我们的场景中,添加 last_update_time 是一种功能管道
创建一个新的功能管道如下:
1. PUT _ingest/pipeline/func_add_last_update_time
2. {
3. "version": 1,
4. "description": "any stage: add back last update timestamp using script approach",
5. "processors": [
6. {
7. "script": {
8. "lang": "painless",
9. "source": """
10. ctx.last_update_time = new Date();
11. """
12. }
13. }
14. ]
15. }
太好了,我们添加了 func_add_last_update_time。 现在假设我们有另一个用于某些业务逻辑的管道,每当对文档应用更改时,我们希望在其中添加一个 last_update_time。
1. POST _ingest/pipeline/_simulate
2. {
3. "pipeline": {
4. "processors": [
5. {
6. "dissect": {
7. "field": "message",
8. "pattern": "%{user_id}:%{action}:%{action_id}:%{action_ts}:%{description}"
9. }
10. },
11. {
12. "date": {
13. "field": "action_ts",
14. "formats": ["yyyy/MM/dd HH-mm-ss"],
15. "target_field": "action_ts"
16. }
17. },
18. {
19. "remove": {
20. "field": [
21. "message"
22. ]
23. }
24. }
25. ]
26. },
27. "docs": [
28. {
29. "_source": {
30. "message": "jaime_10234:filling_procurement_form:12_yuy190:2019/04/18 13-12-09:procurement for ABC company on spare parts"
31. }
32. }
33. ]
34. }
上面运行的结果为:
1. {
2. "docs": [
3. {
4. "doc": {
5. "_index": "_index",
6. "_id": "_id",
7. "_version": "-3",
8. "_source": {
9. "action": "filling_procurement_form",
10. "action_ts": "2019-04-18T13:12:09.000Z",
11. "description": "procurement for ABC company on spare parts",
12. "user_id": "jaime_10234",
13. "action_id": "12_yuy190"
14. },
15. "_ingest": {
16. "timestamp": "2023-03-09T00:11:30.535138719Z"
17. }
18. }
19. }
20. ]
21. }
上面的主流管道非常简单,它试图将给定的 message 解析为各个字段; 还有一个时间戳字段转换加上在使用后删除原始 message。 如果我们想添加一个last_update_time,只需在工作流程的后面添加一个 pipeline 处理器:
1. POST _ingest/pipeline/_simulate
2. {
3. "pipeline": {
4. "processors": [
5. {
6. "dissect": {
7. "field": "message",
8. "pattern": "%{user_id}:%{action}:%{action_id}:%{action_ts}:%{description}"
9. }
10. },
11. {
12. "date": {
13. "field": "action_ts",
14. "formats": ["yyyy/MM/dd HH-mm-ss"],
15. "target_field": "action_ts"
16. }
17. },
18. {
19. "pipeline": {
20. "name": "func_add_last_update_time"
21. }
22. },
23. {
24. "remove": {
25. "field": [
26. "message"
27. ]
28. }
29. }
30. ]
31. },
32. "docs": [
33. {
34. "_source": {
35. "message": "jaime_10234:filling_procurement_form:12_yuy190:2019/04/18 13-12-09:procurement for ABC company on spare parts"
36. }
37. }
38. ]
39. }
上面命令运行的结果是:
1. {
2. "docs": [
3. {
4. "doc": {
5. "_index": "_index",
6. "_id": "_id",
7. "_version": "-3",
8. "_source": {
9. "last_update_time": "2023-03-09T00:21:37.511Z",
10. "user_id": "jaime_10234",
11. "action_id": "12_yuy190",
12. "action": "filling_procurement_form",
13. "action_ts": "2019-04-18T13:12:09.000Z",
14. "description": "procurement for ABC company on spare parts"
15. },
16. "_ingest": {
17. "timestamp": "2023-03-09T00:21:37.51032725Z"
18. }
19. }
20. }
21. ]
22. }
现在我们毫无困难地得到了 last_update_time! 看看重用代码是多么容易!
调用另一个管道时的异常处理
因为我们现在可以像函数调用一样调用另一个管道; 有一个新问题浮出水面 —— 调用管道上的异常处理……
让我们创建一个新的功能管道和另一个主流管道来说明这个场景:
1. PUT _ingest/pipeline/func_convert_age
2. {
3. "processors": [
4. {
5. "convert": {
6. "field": "age",
7. "type": "integer"
8. }
9. }
10. ]
11. }
模拟如下的 ingest pipeline:
1. POST _ingest/pipeline/_simulate
2. {
3. "pipeline": {
4. "processors": [
5. {
6. "dissect": {
7. "field": "message",
8. "pattern": "%{name}:%{age}"
9. }
10. },
11. {
12. "pipeline": {
13. "name": "func_convert_age"
14. }
15. }
16. ]
17. },
18. "docs": [
19. {
20. "_source": {
21. "message": "helen wong:45"
22. }
23. },
24. {
25. "_source": {
26. "message": "josh blake:a46"
27. }
28. }
29. ]
30. }
结果:
1. {
2. "docs": [
3. {
4. "doc": {
5. "_index": "_index",
6. "_id": "_id",
7. "_version": "-3",
8. "_source": {
9. "message": "helen wong:45",
10. "name": "helen wong",
11. "age": 45
12. },
13. "_ingest": {
14. "timestamp": "2023-03-09T00:25:48.38476263Z"
15. }
16. }
17. },
18. {
19. "error": {
20. "root_cause": [
21. {
22. "type": "illegal_argument_exception",
23. "reason": "unable to convert [a46] to integer"
24. }
25. ],
26. "type": "illegal_argument_exception",
27. "reason": "unable to convert [a46] to integer",
28. "caused_by": {
29. "type": "number_format_exception",
30. "reason": "For input string: \"a46\""
31. }
32. }
33. }
34. ]
35. }
用于模拟的第一个文件,它应该可以工作,但第二个文档不会,原因很简单 -> “无法将 [a46] 转换为整数”。 如果我们想捕获这个异常并做一些事情,只需添加“on_failure” 子句:
1. POST _ingest/pipeline/_simulate
2. {
3. "pipeline": {
4. "processors": [
5. {
6. "dissect": {
7. "field": "message",
8. "pattern": "%{name}:%{age}"
9. }
10. },
11. {
12. "pipeline": {
13. "name": "func_convert_age",
14. "on_failure": [
15. {
16. "set": {
17. "field": "error",
18. "value": "{{ _ingest.on_failure_processor_type }} - {{ _ingest.on_failure_message }}"
19. }
20. },
21. {
22. "remove": {
23. "field": [ "name", "age" ]
24. }
25. }
26. ]
27. }
28. }
29. ]
30. },
31. "docs": [
32. {
33. "_source": {
34. "message": "helen wong:45"
35. }
36. },
37. {
38. "_source": {
39. "message": "josh blake:a46"
40. }
41. }
42. ]
43. }
上述命令的返回结果是:
1. {
2. "docs": [
3. {
4. "doc": {
5. "_index": "_index",
6. "_id": "_id",
7. "_version": "-3",
8. "_source": {
9. "message": "helen wong:45",
10. "name": "helen wong",
11. "age": 45
12. },
13. "_ingest": {
14. "timestamp": "2023-03-09T00:27:50.726106798Z"
15. }
16. }
17. },
18. {
19. "doc": {
20. "_index": "_index",
21. "_id": "_id",
22. "_version": "-3",
23. "_source": {
24. "message": "josh blake:a46",
25. "error": "convert - For input string: \\\"a46\\\""
26. },
27. "_ingest": {
28. "timestamp": "2023-03-09T00:27:50.726149381Z"
29. }
30. }
31. }
32. ]
33. }
现在有问题的文档; 它将包含未触及的 message 字段和一个说明异常的新 error 字段。 稍后,如果你只想查询有异常的文档,exists 查询就可以解决问题。
1. // replace {{target_index}} with the resulting index
2. GET {{target_index}}/_search
3. {
4. "query": {
5. "exists": {
6. "field": "error"
7. }
8. }
9. }
更多关于 ingest pipeline 的文章请详细阅读:
将管道应用于现有文档(数据修复阶段)
将创建的管道应用于现有文档; 你可以简单地使用 _update_by_query:
1. // replace the {{pipeline_name}} to any valid pipeline
2. POST blog_pipeline_tips1/_update_by_query?pipeline={{pipeline_name}}
重要的一点是添加了一个 pipeline 参数并精确定位到相应的管道 ID。
为索引设置默认管道
从 Elasticsearch 6.5.x 开始,引入了一个名为 index.default_pipeline 的新索引设置。 这仅仅意味着所有摄取的文档都将由默认管道进行预处理; 例如,添加last_update_time 用例应该在索引的每个新传入文档上运行。 语法相当简单明了:
1. PUT app_log_1
2. {
3. "settings": {
4. "default_pipeline": "add_last_update_time"
5. }
6. }
条件切换逻辑
根据源字段设置值
举个例子,我们有一个名为 categoryValue 的字段。 如果此值等于 plant,则 categoryCode 字段的值将设置为 A。 下面是逻辑矩阵:
- categoryValue = “plant”, categoryCode = “A”
- categoryValue = "animal", categoryCode = "B"
对应的 pipeline 可以这样写:
1. POST _ingest/pipeline/_simulate
2. {
3. "pipeline": {
4. "processors": [
5. {
6. "set": {
7. "field": "categoryCode",
8. "value": "A",
9. "if": "ctx.categoryValue == 'plant'"
10. }
11. },
12. {
13. "set": {
14. "field": "categoryCode",
15. "value": "B",
16. "if": "ctx.categoryValue == 'animal'"
17. }
18. }
19. ]
20. },
21. "docs": [
22. {
23. "_source": {
24. "categoryValue": "plant"
25. }
26. },
27. {
28. "_source": {
29. "categoryValue": "animal"
30. }
31. },
32. {
33. "_source": {
34. "categoryValue": "unknown"
35. }
36. }
37. ]
38. }
响应是:
1. {
2. "docs": [
3. {
4. "doc": {
5. "_index": "_index",
6. "_id": "_id",
7. "_version": "-3",
8. "_source": {
9. "categoryValue": "plant",
10. "categoryCode": "A"
11. },
12. "_ingest": {
13. "timestamp": "2023-03-09T00:45:25.659339217Z"
14. }
15. }
16. },
17. {
18. "doc": {
19. "_index": "_index",
20. "_id": "_id",
21. "_version": "-3",
22. "_source": {
23. "categoryValue": "animal",
24. "categoryCode": "B"
25. },
26. "_ingest": {
27. "timestamp": "2023-03-09T00:45:25.659388883Z"
28. }
29. }
30. },
31. {
32. "doc": {
33. "_index": "_index",
34. "_id": "_id",
35. "_version": "-3",
36. "_source": {
37. "categoryValue": "unknown"
38. },
39. "_ingest": {
40. "timestamp": "2023-03-09T00:45:25.659396717Z"
41. }
42. }
43. }
44. ]
45. }
切换逻辑基于 set 处理器的 if子句,变量 ctx 是提供对该文档实例中字段的访问的文档上下文。
很简单,不是吗?
向管道提供参数 - 1
如果你阅读 pipeline 处理器的官方文档,你将找不到一句话提到如何为 pipeline 提供参数。 但实际上,有一个解决方法(虽然有点难看)。
1. PUT _ingest/pipeline/pipMultiply2
2. {
3. "processors": [
4. {
5. "script": {
6. "source": """
7. if (ctx.paramValue != null) {
8. ctx.finalValue = ctx.paramValue * 2;
10. // remove the original "paramValue" field
11. ctx.remove("paramValue");
12. }
13. """
14. }
15. }
16. ]
17. }
我们首先创建一个名为 pipMultiply2 的管道 —— 简单地运行字段 paramValue 提供的值的乘法。 请注意,字段存在性检查是通过以下方式完成的:
if (ctx.paramValue != null) …
乘法结果被设置为字段 finalValue。 之后我们还删除了参数字段:
ctx.remove(“paramValue”)
现在是模拟管道 + 提供测试参数的时候了:
1. POST _ingest/pipeline/_simulate
2. {
3. "pipeline": {
4. "processors": [
5. {
6. "set": {
7. "field": "paramValue",
8. "value": 100
9. }
10. },
11. {
12. "pipeline": {
13. "name": "pipMultiply2"
14. }
15. }
16. ]
17. },
18. "docs": [
19. {
20. "_source": {}
21. }
22. ]
23. }
结果值将恰好为 200。 如前所述,这种方法可行但有点难看,因为我们需要在运行乘以 2 管道之前为目标文档设置一个字段(例如 paramValue)。 之后还需要删除 paramValue(如有必要)。
向管道提供参数 - 2
我们已经知道如何使用 “丑陋” 的方法将参数传递给管道,但是如果你不喜欢这种方法,还有另一种解决方法,如下所示:
1. PUT _scripts/test_parameter_pip
2. {
3. "script": {
4. "lang": "painless",
5. "source": """
6. if (params['paramValue'] != null) {
7. ctx.finalValue = params['paramValue'] * 2;
8. }
9. """
10. }
11. }
我们在 elasticsearch 集群中创建了一个存储脚本。 这个脚本的逻辑很简单 —— 将参数的值乘以 2。请注意检查存在性逻辑是通过以下方式应用的:
if (params[‘paramValue’] != null) …
现在在管道中测试我们的脚本:
1. POST _ingest/pipeline/_simulate
2. {
3. "pipeline": {
4. "processors": [
5. {
6. "script": {
7. "id": "test_parameter_pip",
8. "params": {
9. "paramValue": 12
10. }
11. }
12. }
13. ]
14. },
15. "docs": [
16. {
17. "_source": {
18. "message": "hello"
19. }
20. }
21. ]
22. }
1. {
2. "docs": [
3. {
4. "doc": {
5. "_index": "_index",
6. "_id": "_id",
7. "_version": "-3",
8. "_source": {
9. "message": "hello",
10. "finalValue": 24
11. },
12. "_ingest": {
13. "timestamp": "2023-03-09T00:55:19.670380755Z"
14. }
15. }
16. }
17. ]
18. }
最终结果将涉及一个值为 24 的字段 finalValue。 这种方法在技术上使用了 pipeline 处理器,但它的工作原理很吸引人,并且仍然能够保持可重用性(尽管我们通过脚本而不是 pipeline 处理器来抽象可重用性特性......我知道这听起来令人困惑:))))