"properties" : {
        "app_id" : {
          "type" : "text",
          "fields" : {
            "keyword" : {
              "type" : "keyword",
              "ignore_above" : 256
            }
          }
        },
        "archive_id" : {
          "type" : "text",
          "fields" : {
            "keyword" : {
              "type" : "keyword",
              "ignore_above" : 256
            }
          }
        },
如上是mapping中的部分字段,然后put如下

PUT /business_order_detail_test/_mapping
{
  "runtime": {
    "app_archive_key": {
      "type": "keyword",
      "script": {
        "source": "if (doc['app_id.keyword'].size() != 0 && doc['archive_id.keyword'].size() != 0) { emit(doc['app_id.keyword'].value + '|' + doc['archive_id.keyword'].value); }"
      }
    }
  }
}

在现有索引上添加一个“运行时字段”(Runtime Field),用于在查询时动态生成一个组合键,而不修改底层索引数据或重新索引。

为了让你更清楚,我把它拆解成几个层面来解释:

  1. 它做了什么(技术动作)

你通过 PUT /business_order_detail_test/_mapping 更新了索引的映射。

新增了一个名为 app_archive_key 的运行时字段。

这个字段不会被物理存储在磁盘上,只在查询(Search)或聚合(Aggregation)时实时计算。

  1. 脚本逻辑(计算规则)

脚本的逻辑是拼接字符串:

doc[‘app_id.keyword’].size() != 0:检查文档中是否存在 app_id 且不为空值。

doc[‘archive_id.keyword’].size() != 0:检查文档中是否存在 archive_id 且不为空值。

如果两个字段都存在,则执行 emit(…),将 app_id 和 archive_id 用竖线 | 连接起来。

  1. 为什么要用 .keyword 而不是直接用 app_id?

你给的映射中,app_id 和 archive_id 都是 text 类型(用于全文分词搜索),但同时拥有 keyword 子字段(用于精确匹配、聚合和排序)。

脚本里用了 doc[‘app_id.keyword’].value,是因为 text 类型默认会分词(比如 “abc123” 可能被拆成 “abc” 和 “123”),直接取值会报错或取不到完整值。

使用 .keyword 可以确保取到该字段的原始完整值(如 “order_001”),保证拼接出的组合键是精确的。

  1. 实际业务场景(为什么要这么做)

假设你有两条数据:

文档 A:app_id=“shop”, archive_id=“2024”

文档 B:app_id=“shop”, archive_id=“2025”

查询时,app_archive_key 会动态计算出:

文档 A:“shop|2024”

文档 B:“shop|2025”

这样做的好处是:

无需重索引:如果直接写死一个拼接字段,需要修改数据源并重建索引。使用运行时字段,可以零停机成本地新增这个逻辑。

灵活聚合:你可以直接对这个 app_archive_key 进行 terms 聚合,来统计每种 (app_id, archive_id) 组合下的文档数量,或者用它来做精确过滤(term 查询)。

⚠️ 需要注意的性能代价:
因为这是查询时计算,每次搜索都会执行这个脚本。如果数据量极大或并发很高,会消耗较多的 CPU 资源。如果该组合键频繁用于聚合或过滤,建议还是考虑在写入数据时(通过 Ingest Pipeline)直接写入这个字段,或者在创建索引时就定义为普通字段并重新索引数据。

更多推荐