Runtime Field

本文转载自https://blog.csdn.net/laoyang360/article/details/120574142

实战遇到的问题

在实战业务种,遇到数据导入后,但发现缺少部分必要字段,怎么解决?
例如: emotion 代表感情值,取值范围为: 0-1000, 其中 300-700 代表中性,0-300代表负面, 700-1000代表正面
但是实际业务种,我们需要: 中性: 0; 负面: -1; 正面: 1;

如何实现呢?

  • 方案一:重新创建索引时添加字段,清除已有数据再重新导入数据。
  • 方案二:重新创建索引时添加字段,原索引通过 reindex 写入到新索引。
  • 方案三:提前指定数据预处理,结合管道 ingest 重新导入或批量更新 update_by_query 实现。
  • 方案四:保留原索引不动,通过script 脚本实现。

方案一、二 比较好理解,我们实现下方案三、四

方案三 Ingest预处理实现

  1. DELETE news_00001
  2. PUT news_00001
  3. {
  4. "mappings": {
  5. "properties": {
  6. "emotion": {
  7. "type": "integer"
  8. }
  9. }
  10. }
  11. }
  12. POST news_00001/_bulk
  13. {"index":{"_id":1}}
  14. {"emotion":558}
  15. {"index":{"_id":2}}
  16. {"emotion":125}
  17. {"index":{"_id":3}}
  18. {"emotion":900}
  19. {"index":{"_id":4}}
  20. {"emotion":600}
  21. PUT _ingest/pipeline/my-pipeline-1
  22. {
  23. "description": "Set emotion flag param",
  24. "processors": [
  25. {
  26. "script": {
  27. "lang": "painless",
  28. "source": """
  29. if (ctx['emotion'] < 300 && ctx['emotion'] > 0)
  30. ctx['emotion_flag'] = -1;
  31. if (ctx['emotion'] >= 300 && ctx['emotion'] <= 700)
  32. ctx['emotion_flag'] = 0;
  33. if (ctx['emotion'] > 700 && ctx['emotion'] < 1000)
  34. ctx['emotion_flag'] = 1;
  35. """
  36. }
  37. }
  38. ]
  39. }
  1. POST news_00001/_update_by_query?pipeline=my-pipeline-1
  2. {
  3. "query": {
  4. "match_all": {}
  5. }
  6. }

方案三的核心:定义了预处理管道:my-pipeline-1,管道里做了逻辑判定,对于emotion 不同的取值区间,设置 emotion_flag 不同的结果值。
该方案必须提前创建管道,可以通过写入时指定缺省管道 default_pipeline或者结合批量更新实现。
实际是两种细分实现方式:

  • 方式一:udpate_by_query 批量更新。而更新索引尤其全量更新索引是有很大的成本开销的。
  • 方式二:写入阶段指定预处理管道,每写入一条数据预处理一次。

    方案四:script脚本实现

    1. POST news_00001/_search
    2. {
    3. "query": {
    4. "match_all": {}
    5. },
    6. "script_fields": {
    7. "emotion_flag": {
    8. "script": {
    9. "lang": "painless",
    10. "source": "if (doc['emotion'].value < 300 && doc['emotion'].value>0) return -1; if (doc['emotion'].value >= 300 && doc['emotion'].value<=700) return 0; if (doc['emotion'].value > 700 && doc['emotion'].value<=1000) return 1;"
    11. }
    12. }
    13. }
    14. }
    方案四的核心:通过 script_field 脚本实现。
    该方案仅是通过检索获取了结果值,该值不能用于别的用途,比如:聚合。
    还要注意的是:script_field 脚本处理字段会有性能问题。
    两种方案各有利弊,这时候我们会进一步思考:
    能不能不改 Mapping、不重新导入数据,就能得到我们想要的数据呢?
    早期版本不可以,7.11 版本之后的版本有了新的解决方案——Runtime fields 运行时字段

    Runtime fields产生背景

    Runtime fields 运行时字段是旧的脚本字段 script field 的 Plus 版本,引入了一个有趣的概念,称为“读取建模”(Schema on read)。
    有 Schema on read 自然会想到 Schema on write(写时建模),传统的非 runtime field 类型 都是写时建模的,而 Schema on read 则是另辟蹊径、读时建模。
    这样,运行时字段不仅可以在索引前定义映射,还可以在查询时动态定义映射,并且几乎具有常规字段的所有优点。
    Runtime fields在索引映射或查询中一旦定义,就可以立即用于搜索请求、聚合、筛选和排序。

    用Runtime fields解决问题

    ```json PUT news_00001/_mapping { “runtime”: { “emotion_flag_new”: {
    1. "type": "keyword",
    2. "script": {
    3. "source": "if (doc['emotion'].value > 0 && doc['emotion'].value < 300) emit('-1'); if (doc['emotion'].value >= 300 && doc['emotion'].value<=700) emit('0'); if (doc['emotion'].value > 700 && doc['emotion'].value<=1000) emit('1');"
    4. }
    } } }

GET news_00001/_search { “fields” : [“*”] }

  1. <a name="Xq5bq"></a>
  2. ### Runtime fields 实战
  3. 第一:PUT news_00001/_mapping 是在已有 Mapping 的基础上 更新 Mapping。<br />这是更新 Mapping 的方式。实际上,创建索引的同时,指定 runtime field 原理一致。实现如下:
  4. ```json
  5. PUT news_00002
  6. {
  7. "mappings": {
  8. "runtime": {
  9. "emotion_flag_new": {
  10. "type": "keyword",
  11. "script": {
  12. "source": "if (doc['emotion'].value > 0 && doc['emotion'].value < 300) emit('-1'); if (doc['emotion'].value >= 300 && doc['emotion'].value<=700) emit('0'); if (doc['emotion'].value > 700 && doc['emotion'].value<=1000) emit('1');"
  13. }
  14. }
  15. },
  16. "properties": {
  17. "emotion": {
  18. "type": "integer"
  19. }
  20. }
  21. }
  22. }

第二:更新的什么呢?
加了字段,确切的说,加了:runtime 类型的字段,字段名称为:emotion_flag_new,字段类型为:keyword,字段数值是用脚本 script 实现的。
脚本实现的什么呢?

  • 当 emotion 介于 0 到 300 之间时,emotion_flag_new 设置为 -1 。
  • 当 emotion 介于 300 到 700 之间时,emotion_flag_new 设置为 0。
  • 当 emotion 介于 700 到 1000 之间时,emotion_flag_new 设置为 1。

第三:如何实现检索呢?
我们尝试一下传统的检索,看一下结果。
我们先看一下 Mapping:

  1. {
  2. "news_00001" : {
  3. "mappings" : {
  4. "runtime" : {
  5. "emotion_flag_new" : {
  6. "type" : "keyword",
  7. "script" : {
  8. "source" : "if (doc['emotion'].value > 0 && doc['emotion'].value < 300) emit('-1'); if (doc['emotion'].value >= 300 && doc['emotion'].value<=700) emit('0'); if (doc['emotion'].value > 700 && doc['emotion'].value<=1000) emit('1');",
  9. "lang" : "painless"
  10. }
  11. }
  12. },
  13. "properties" : {
  14. "emotion" : {
  15. "type" : "integer"
  16. }
  17. }
  18. }
  19. }
  20. }

多了一个 runtime 类型的字段:emotion_flag_new

  1. GET news_00001/_search

结果如下:
RunTimeFields.png
执行查询

  1. GET news_00001/_search
  2. {
  3. "query": {
  4. "match": {
  5. "emotion_flag_new": "-1"
  6. }
  7. }
  8. }

返回结果如下:
RunTimeFiled_search.png
执行:

  1. GET news_00001/_search
  2. {
  3. "fields" : ["*"],
  4. "query": {
  5. "match": {
  6. "emotion_flag_new": "-1"
  7. }
  8. }
  9. }

返回结果如下:
RuntimeFieldsSearch2.png

Runtime fields 核心语法解读

为什么加了:field:[] 才可以返回检索匹配结果呢?因为:Runtime fields 不会显示在:_source 中,但是:fields API 会对所有 fields 起作用。如果需要指定字段,就写上对应字段名称;否则,写 代表全部字段。

如果不想另起炉灶定义新字段,在原来字段上能实现吗?

其实上面的示例已经完美解决问题了,但是再吹毛求疵一下,在原有字段 emotion 上查询时实现更新值可以吗?

  1. POST news_00001/_search
  2. {
  3. "runtime_mappings": {
  4. "emotion": {
  5. "type": "keyword",
  6. "script": {
  7. "source": """
  8. if(params._source['emotion'] > 0 && params._source['emotion'] < 300) {emit('-1')}
  9. if(params._source['emotion'] >= 300 && params._source['emotion'] <= 700) {emit('0')}
  10. if(params._source['emotion'] > 700 && params._source['emotion'] <= 1000) {emit('1')}
  11. """
  12. }
  13. }
  14. },
  15. "fields": [
  16. "emotion"
  17. ]
  18. }

返回结果:
RuntimeFields2.png
解释一下:
第一:原来 Mapping 里面 emotion是 integer 类型的。
第二:我们定义的是检索时类型,mapping 没有任何变化,但是:检索时字段类型 emotion 在字段名称保持不变的前提下,被修改为:keyword 类型。
这是一个非常牛逼的功能!!!
早期 5.X、6.X 没有这个功能的时候,实际业务中我们的处理思路如下:

  • 步骤一:停掉实时写入;
  • 步骤二:创建新索引,指定新 Mapping,新增 emotion_flag 字段。
  • 步骤三:恢复写入,新数据会生效;老数据 reindex 到新索引,reindex 同时结合 ingest 脚本处理。

有了 Runtime field,这种相当繁琐的处理的“苦逼”日子一去不复回了!

Runtime fields 适用场景

比如:日志场景。运行时字段在处理日志数据时很有用,尤其是当不确定数据结构时。
使用了 runtime field,索引大小要小得多,可以更快地处理日志而无需对其进行索引。

Runtime fields 优缺点

优点 1:灵活性强

运行时字段非常灵活。主要体现在:

  • 需要时,可以将运行时字段添加到我们的映射中。
  • 不需要时,轻松删除它们。

删除操作实战如下:

  1. PUT news_00001/_mapping
  2. {
  3. "runtime": {
  4. "emotion_flag": null
  5. }
  6. }

也就是说将这个字段设置为:null,该字段便不再出现在 Mapping 中。

优点 2:打破传统先定义后使用方式

运行时字段可以在索引时或查询时定义。
由于运行时字段未编入索引,因此添加运行时字段不会增加索引大小,也就是说 Runtime fields 可以降低存储成本。

优点3:能阻止 Mapping 爆炸

Runtime field 不被索引(indexed)和存储(stored),能有效阻止 mapping “爆炸”。
原因在于 Runtime field 不计算在 index.mapping.total_fields 限制里面。

缺点1:对运行时字段查询会降低搜索速度

对运行时字段的查询有时会很耗费性能,也就是说,运行时字段会降低搜索速度。

Runtime fields 使用建议

  • 权衡利弊:可以通过使用运行时字段来减少索引时间以节省 CPU 使用率,但是这会导致查询时间变慢,因为数据的检索需要额外的处理。
  • 结合使用:建议将运行时字段与索引字段结合使用,以便在写入速度、灵活性和搜索性能之间找到适当的平衡。