📣 极限科技诚招搜索运维工程师(Elasticsearch/Easysearch)- 全职/北京 👉 : 立即申请加入
ES 踩坑记:Set Processor 字段更新引发的 _source 污染

问题背景 #

社区的一个伙伴想对一个 integer 的字段类型添加一个 keyword 类型的子字段,然后进行精确匹配的查询优化,提高查询的速度。

整个索引数据量不大,并不想进行 reindex 这样的复杂操作,就想到了使用 update_by_query 的存量数据更新。

所以我们测试了下面这套方案,在设置完字段的子字段后,利用 set processor 来对这个子字段进行 update_by_query

操作记录:

# 测试索引
PUT /test
{
  "mappings": {
    "properties": {
      "status": {
        "type": "integer"
      }
    }
  }
}
# 测试数据
POST /test/_bulk
{"index":{}}
{"status":404}
{"index":{}}
{"status":500}

GET test/_search

# 添加子字段
PUT test/_mapping
{
  "properties": {
    "status": {
      "type": "integer",
      "fields": {
        "keyword": {
          "type": "keyword"
        }
      }
    }
  }
}

GET test/_search

#创建管道pipeline.实现更新逻辑
PUT _ingest/pipeline/copy_status_to_keyword
{
  "description": "resets the value of status and subfields",
  "processors": [
    {
      "set": {
        "field": "status",
        "value": "{{{status}}}"
      }
    }
  ]
}

#update 执行
POST test/_update_by_query?pipeline=copy_status_to_keyword
{
  "query": {
    "bool": {
      "must_not": {
        "exists": {
          "field": "status.keyword"
        }
      },
      "must": {
        "exists": {
          "field": "status"
        }
      }
    }
  }
}


GET test/_search
{
  "query": {
    "exists": {
      "field": "status.keyword"
    }
  }
}

# 返回结果
   "hits": [
      {
        "_index": "test_set",
        "_type": "_doc",
        "_id": "G7zHNpUBLvnTvXTpVIC4",
        "_score": 1,
        "_source": {
          "status": "404"
        }
      },
      {
        "_index": "test_set",
        "_type": "_doc",
        "_id": "HLzHNpUBLvnTvXTpVIC4",
        "_score": 1,
        "_source": {
          "status": "500"
        }
      }
    ]

测试检查了一下,status.keyword 可以被 search,可以满足我们的预期要求。

但是,小伙伴到正式上线的时候却发生了问题。应用程序读取发现 _source 中 status 的类型变了,开始报错字段类型不符合。

# 写入的时候
    "hits": [
      {
        "_index": "test",
        "_type": "_doc",
        "_id": "2ry5NpUBLvnTvXTp1F5z",
        "_score": 1,
        "_source": {
          "status": 404 # 这里还是 integer 类型
        }
      },
      {
        "_index": "test",
        "_type": "_doc",
        "_id": "27y5NpUBLvnTvXTp1F5z",
        "_score": 1,
        "_source": {
          "status": 500
        }
      }
    ]

# update 完成后
   "hits": [
      {
        "_index": "test",
        "_type": "_doc",
        "_id": "2ry5NpUBLvnTvXTp1F5z",
        "_score": 1,
        "_source": {
          "status": "404" # 字段内容添加上了引号,成为了 string 类型
        }
      },
      {
        "_index": "test",
        "_type": "_doc",
        "_id": "27y5NpUBLvnTvXTp1F5z",
        "_score": 1,
        "_source": {
          "status": "500"
        }
      }
    ]

解决方案 #

还好小伙伴那边有数据主备库,赶紧做了切换。然后开始对已有的数据进行修复。

最终商定了下面两个方案进行 fix。

  1. 用 script 保持数据类型重写
POST test/_update_by_query
{
"script": {
  "source": """
    if (ctx._source.status instanceof String) {
      ctx._source.status = Integer.parseInt(ctx._source.status);
    }
  """,
  "lang": "painless"
  }
}
  1. 查询结果读取 docvalue 而不是 source。这个方案可以绕过这个问题,但是需要改动应用程序。
GET test/_search
{
  "_source": false,
  "docvalue_fields": [
    "status"
  ]
}

# 返回
    "hits": [
      {
        "_index": "test",
        "_type": "_doc",
        "_id": "wLy-NpUBLvnTvXTpRGvw",
        "_score": 1,
        "fields": {
          "status": [
            404
          ]
        }
      },
      {
        "_index": "test",
        "_type": "_doc",
        "_id": "wby-NpUBLvnTvXTpRGvw",
        "_score": 1,
        "fields": {
          "status": [
            500
          ]
        }
      }
    ]

问题分析 #

好了,现在我们回过头来分析一下之前方案出现的问题,用 set proceesor 为什么会导致 source 内的字段类型从 int 变成 string 呢?

因为 script 脚本写法能够成功,而 set 会失败,我们从 set 的使用入手,去看看代码里是不是有什么线索?

set processor 问题的细节 #

让我们深入分析值类型转换的核心代码路径:

// SetProcessor.java
document.setFieldValue(field, value, ignoreEmptyValue);

这里的 value 参数类型为 ValueSource,

// SetProcessor.Factory.create()
Object value = ConfigurationUtils.readObject(TYPE, processorTag, config, "value");
ValueSource valueSource = ValueSource.wrap(value, scriptService);

其核心实现逻辑在接口 ValueSource.java 中:

// ValueSource.java 关键方法 59行
public static ValueSource wrap(Object value, ScriptService scriptService) {
    ......
    } else if (value instanceof String) {
            // This check is here because the DEFAULT_TEMPLATE_LANG(mustache) is not
            // installed for use by REST tests. `value` will not be
            // modified if templating is not available
            if (scriptService.isLangSupported(DEFAULT_TEMPLATE_LANG) && ((String) value).contains("{{")) {
                Script script = new Script(ScriptType.INLINE, DEFAULT_TEMPLATE_LANG, (String) value, Collections.emptyMap());
                return new TemplatedValue(scriptService.compile(script, TemplateScript.CONTEXT));
            } else {
                return new ObjectValue(value);
            }
        }
        ......
}

当配置中的 value 值为"{{{status}}}“字符串时,创建 TemplateValue 实例。

这里 “{{{status}}}” 的写法属于 Mustache 语法,一种轻量级的模板引擎语法。ES 在 search template 中会主要应用,在 set processor 也用了 Mustache 进行字段内容的引用。

// 在 ValueSource.java 内部
private static class TemplateValue extends ValueSource {
    private final TemplateScript.Factory template;

    @Override
        public Object copyAndResolve(Map<String, Object> model) {
            return template.newInstance(model).execute();
        }

}

继续看抽象类 TemplateScript.java#execute() ,这个方法在定义的时候已经明确声明返回的是 string

    /** Run a template and return the resulting string, encoded in utf8 bytes. */
    public abstract String execute();

而实现的子类则很明显是 MustacheExecutableScript.execute(),即 Mustache 语法引擎的实现。

private class MustacheExecutableScript extends TemplateScript {
  ......
  @Override
        public String execute() {
            final StringWriter writer = new StringWriter();
            try {
                // crazy reflection here
                SpecialPermission.check();
                AccessController.doPrivileged((PrivilegedAction<Void>) () -> {
                    template.execute(writer, params);
                    return null;
                });
            } catch (Exception e) {
                logger.error((Supplier<?>) () -> new ParameterizedMessage("Error running {}", template), e);
                throw new GeneralScriptException("Error running " + template, e);
            }
            return writer.toString();
        }
    ......

这里也可以印证了字段内容类型被强制转为字符串

类型转换过程 #

deepseek 帮我总结的类型转换过程如下:

sequenceDiagram
    participant SetProcessor
    participant ValueSource
    participant TemplateValue
    participant TemplateScript
    participant MustacheEngine

    SetProcessor->>ValueSource: wrap("{{status}}")
    ValueSource->>TemplateValue: 创建实例
    SetProcessor->>TemplateValue: copyAndResolve(doc)
    TemplateValue->>TemplateScript: newInstance(doc)
    TemplateScript->>MustacheEngine: compile("{{status}}")
    MustacheEngine-->>TemplateScript: 返回Mustache编译后模板实现
    TemplateValue->>TemplateScript: execute()
    MustacheEngine-->>TemplateScript: 在这里将结果渲染为String
    TemplateValue-->>SetProcessor: 返回"200"(String)

小结 #

所以,这里 source 内字段类型被转变的原因,是 ES 对 set processor 使用 Mustache 语法产生的结果值进行了特殊处理,将内容都处理成了 string。

假设这次使用 set 去处理的值都是一个默认值 404 ,则不会出现这个问题

PUT _ingest/pipeline/copy_status_to_keyword_1
{
  "description": "resets the value of status and subfields",
  "processors": [
    {
      "set": {
        "field": "status",
        "value": 404
      }
    }
  ]
}

#update 执行方式
POST test/_update_by_query?pipeline=copy_status_to_keyword_1
{
  "query": {
    "bool": {
      "must_not": {
        "exists": {
          "field": "status.keyword"
        }
      },
      "must": {
        "exists": {
          "field": "status"
        }
      }
    }
  }
}

GET test/_search
# 返回内容
      {
        "_index": "test",
        "_type": "_doc",
        "_id": "tN0QRZUBLvnTvXTpJMTI",
        "_score": 1,
        "_source": {
          "status": 404
        }
      },
      {
        "_index": "test",
        "_type": "_doc",
        "_id": "td0QRZUBLvnTvXTpJMTI",
        "_score": 1,
        "_source": {
          "status": 404
        }
      }

那 ES 在 set 这段 Mustache 语法的处理里,使用 string 作为返回值,大家觉得合理么?如果需要保留原来的数据内容类型,不修改 TemplateScript.java#execute()这个方法可以实现么?

标签
Easysearch x
产品更新 x
performance x
2026 x
开源 x
赞助 x
开源生态 x
社区 x
Coco AI x
二等奖 x
兴智杯 x
人工智能 x
赛事 x
低空经济 x
商业化 x
数据分析 x
金猿奖 x
国产化 x
搜索引擎 x
技术卓越奖 x
创新产品奖 x
IT168 x
APM x
Skywalking x
Easy-Es x
Coco x
AI x
GitLab x
代码审核 x
石油石化 x
Gitee x
投票 x
Meilisearch x
Rust x
轻量级 x
搜索百科 x
Docker x
Docker Compose x
Easyserach x
Console x
DevOps x
Elasticsearch x
国产替代 x
backup x
snapshot x
CCR x
Gateway x
esdump x
source_reuse x
ignore_above x
OpenSearch x
AWS x
Lucene x
Solr x
Easyearch x
发明专利 x
数据分区 x
国际专利 x
一等奖 x
人工智能应用创新大赛 x
bulk x
embedding x
OpenAI x
IK x
TDBC x
2025 x
信通院 x
可信数据库大会 x
搜索型数据库 x
中国数据库产业图谱 x
上海开源创新菁英荟 x
开源创新新星企业 x
Workshop x
AI 搜索 x
智能助手 x
Automation x
Logstash x
MongoDB x
开源中国 x
直播 x
merge x
Elasticsearch 9 x
GitCode x
AI搜索 x
Cloud x
rollup x
Kubernetes x
Operator x
Arm64 x
Snapshot x
S3 x
Grafana x
Opensearch x
Nginx x
直播活动 x
搜索客社区 x
Meetup x
ES x
企业搜索 x
DeepSeek x
RAG x
certificate x
windows x
Rollup x
TopN x
Filebeat x
Ubuntu x
请求限速 x
INFINI Console x
指标 x
Kibana x
多集群 x
client x
Spring Boot x
ECE x
ES Bulk x
vector database x
Postgres x
可搜索快照 x
SDK x
官网 x
Web 开发 x
Next.js x
React x
Three.js x
Metrics x
Helm x
filter x
querycache x
practice x
Agent x
localStorage x
响应式 x
时间组件 x
时区组件 x
极限科技 x
三周年 x
周年庆 x
国家高新技术企业 x
校园招聘 x
湖北工业大学 x
Tauri x
Web 开发人员 x
桌面应用开发 x
桌面端 x
Electron x
Pizza x
认证培训 x
报名 x
Scrapy x
爬虫 x
Rust开发者大会 x
docsearch x
文档搜索 x
Easyseach x
有奖征文 x
黑神话悟空 x
EKS x
征文系列 x
跨集群搜索 x
科技中小企业 x
白皮书 x
Python SDK x
数据库产业图谱 x
超大规模 x
分布式集群 x
写入限流 x
2024可信数据库发展大会 x
创新型中小企业 x
搜索数据库 x
正排索引 x
免费许可证 x
K8S x
DTC2024 x
实时搜索 x
ES国产化 x
Redis x
OOM x
测试 x
内存 x
趋势 x
AI绘画 x
Stable Diffusion x
Diffusion x
Model x
GAN x
语义搜索 x
知识图 x
向量数据库 x
中国信通院 x
星河(Galaxy) x
标杆案例 x
鲲鹏 x
鲲鹏技术认证 x
客户端 x
日志平台 x
LDAP x
Loadgen x
中国一汽 x
国内数据库 x
墨天轮 x
监控系统 x
集成测试 x
ZSTD x
Helm Charts x
国产适配 x
兆芯 x
Linux x
LoongArch x
信创适配 x
二维拆分算法 x
中国移动云 x
Vault x
加密 x
安全工具 x
kNN x
向量检索 x
图片搜索 x
Alerting x
SQL x
搜索 x
Embedding x
可信数据库 x
统信 x
海光 x
龙芯 x
restore x
Arm x
大数据企业证书 x
移动云大会 x
信通院产品评测 x
国内首家 x
数据可视化 x
北京软协 x
第十届理事会会员单位 x
Apache Arrow x
宣传片 x
大会分享 x
多集群管理 x
无缝数据迁移 x
Loadrun x
INFINI Gateway x
log4j x