对
title这种text字段跑terms聚合,会返回 400illegal_argument_exception,reason 里写着 Text fields are not optimised for operations that require per-document field data。text字段只存分词后的词条,默认不建 doc values,聚合拿不到「每篇文档的字段原值」。正确做法是把分面字段映射成keyword,或用fields给text加一个.keyword子字段;fielddata=true能跑通,但会把字段值全加载进堆内存,只适合临时验证。适用 OpenSearch 2.x / 3.x,本文对 3.9.0 校验。
Issue & Env(案发现场与环境)
- 业务场景:跟着 从零跑通 OpenSearch 本地搜索服务 建好
movies索引(title: text、year: integer、genre: keyword),导入 5 条电影。想统计「每个类型有几部」,terms打在genre上一切正常;顺手把字段换成title,请求直接 400。 - 环境:OpenSearch 3.9.0,Docker 单节点,索引
movies(1 主分片、0 副本),security 插件开启(HTTPS + Basic Auth)。 - 关键区分:不是聚合语法写错,也不是字段名拼错。同一个
terms结构,字段是keyword能跑,是text就报错——问题在字段类型。
StackTrace(报错原文)
{
"error": {
"root_cause": [
{
"type": "illegal_argument_exception",
"reason": "Text fields are not optimised for operations that require per-document field data like aggregations and sorting, so these operations are disabled by default. Please use a keyword field instead. Alternatively, set fielddata=true on [title] in order to load field data by uninverting the inverted index. Note that this can use significant memory."
}
],
"type": "illegal_argument_exception",
"reason": "Text fields are not optimised for operations that require per-document field data like aggregations and sorting, so these operations are disabled by default. Please use a keyword field instead. Alternatively, set fielddata=true on [title] in order to load field data by uninverting the inverted index. Note that this can use significant memory."
},
"status": 400
}
报错里已经把三条路写出来了:换 keyword 字段、给字段加 fielddata=true、或者接受「这些操作默认关闭」。下面拆开看为什么。
Reproduce(复现代码)
前提:本地 OpenSearch 已启动,示例值沿用 quickstart(端口 9200、用户 admin、密码 Devfix@2026)。
建索引时 title 只映射成 text,没有子字段:
curl -sk -u admin:Devfix@2026 -X PUT "https://localhost:9200/movies?pretty" \
-H "Content-Type: application/json" \
-d '{
"settings": { "index.number_of_shards": 1, "index.number_of_replicas": 0 },
"mappings": {
"properties": {
"title": { "type": "text" },
"year": { "type": "integer" },
"genre": { "type": "keyword" }
}
}
}'
导入 5 条文档:
cat > movies.ndjson <<'EOF'
{ "index": { "_index": "movies", "_id": "1" } }
{ "title": "Let the Wind Rise", "year": 2013, "genre": "drama" }
{ "index": { "_index": "movies", "_id": "2" } }
{ "title": "Gone with the Wind", "year": 1939, "genre": "drama" }
{ "index": { "_index": "movies", "_id": "3" } }
{ "title": "The Rise of Machines", "year": 2021, "genre": "scifi" }
{ "index": { "_index": "movies", "_id": "4" } }
{ "title": "Deep Sea Chronicles", "year": 2018, "genre": "documentary" }
{ "index": { "_index": "movies", "_id": "5" } }
{ "title": "Windmill Valley", "year": 2015, "genre": "drama" }
EOF
curl -sk -u admin:Devfix@2026 -X POST "https://localhost:9200/_bulk?pretty" \
-H "Content-Type: application/x-ndjson" \
--data-binary @movies.ndjson
先打 keyword 字段,正常返回三个桶:
curl -sk -u admin:Devfix@2026 -X GET "https://localhost:9200/movies/_search?pretty" \
-H "Content-Type: application/json" \
-d '{ "size": 0, "aggs": { "by_genre": { "terms": { "field": "genre" } } } }'
{
"aggregations": {
"by_genre": {
"doc_count_error_upper_bound": 0,
"sum_other_doc_count": 0,
"buckets": [
{ "key": "drama", "doc_count": 3 },
{ "key": "scifi", "doc_count": 1 },
{ "key": "documentary", "doc_count": 1 }
]
}
}
}
把字段换成 title,同一个结构立刻 400:
curl -sk -u admin:Devfix@2026 -X GET "https://localhost:9200/movies/_search?pretty" \
-H "Content-Type: application/json" \
-d '{ "size": 0, "aggs": { "by_title": { "terms": { "field": "title" } } } }'
判定标准:genre 返回三个桶、title 返回 400 illegal_argument_exception,即复现成功。
Root Cause(根因分析)
text 与 keyword 存的东西不一样
text 字段在写入时就被分析器切成词条,倒排索引里存的是「词 → 文档」,不是「文档 → 原值」。keyword 字段不切词,整串当一个值存,并且默认建立 doc values(列式存储的字段值),正好是聚合和排序要读的结构。
官方文档对 text 的 fielddata 参数写得很明确:默认 false,含义是「是否允许访问该字段分析后的词条来做排序、聚合和脚本」。默认关闭,于是聚合请求被直接拒掉。
报错里的 [title] 是字段名,不是文档标题
set fielddata=true on [title] 这句里的 title 是字段名占位。如果被聚合的是 message,报错就会写 on [message]。论坛里搜到的 on [serviceName]、on [host.name] 都是同一个模板,只是字段不同。
为什么官方不建议开 fielddata
官方 faceted search 教程的原话是:可以在 text 上开 "fielddata": true,但生产环境应避免,因为它会把所有字段值加载进堆内存,显著抬高内存占用,可能引发性能问题和 OOM。分面字段的正确映射是 keyword。
The Fix(解决方案)
方案一:分面字段直接映射成 keyword(首选)
新建索引时就把要聚合 / 排序的字符串字段定为 keyword,像 movies 里的 genre 一样。写入时如果一个字段既要全文搜索又要分面,用 fields 同时索引两份——官方文档称这个参数就是为「一个版本用于搜索,另一个用于排序和聚合」设计的:
curl -sk -u admin:Devfix@2026 -X PUT "https://localhost:9200/movies_v2?pretty" \
-H "Content-Type: application/json" \
-d '{
"settings": { "index.number_of_shards": 1, "index.number_of_replicas": 0 },
"mappings": {
"properties": {
"title": {
"type": "text",
"fields": { "keyword": { "type": "keyword" } }
},
"year": { "type": "integer" },
"genre": { "type": "keyword" }
}
}
}'
把老索引的数据搬过去:
curl -sk -u admin:Devfix@2026 -X POST "https://localhost:9200/_reindex?pretty" \
-H "Content-Type: application/json" \
-d '{ "source": { "index": "movies" }, "dest": { "index": "movies_v2" } }'
之后聚合打在 .keyword 子字段上,title 本身照样能做 match 全文搜索:
curl -sk -u admin:Devfix@2026 -X GET "https://localhost:9200/movies_v2/_search?pretty" \
-H "Content-Type: application/json" \
-d '{ "size": 0, "aggs": { "by_title": { "terms": { "field": "title.keyword" } } } }'
方案二:临时开 fielddata(只用来验证)
已经建好的索引改不了字段类型,但可以补 fielddata 参数:
curl -sk -u admin:Devfix@2026 -X PUT "https://localhost:9200/movies/_mapping?pretty" \
-H "Content-Type: application/json" \
-d '{ "properties": { "title": { "type": "text", "fielddata": true } } }'
改完再跑聚合就能返回桶了。但这个开关是按字段生效的,开了之后该字段的全部词条常驻堆内存,数据量一大就会顶到内存上限。本地验证完记得关掉,生产别这么干。
注意事项
- 字段类型不能原地改:
text改成keyword必须新建索引 + 重建数据(_reindex或重新 bulk),不能PUT _mapping覆盖已有类型。 - 分面字段在写入前就要定好。后补
.keyword子字段同样要重建索引,成本一样。 keyword适合精确值分面;需要「按词分面」的场景(比如统计标题里出现过哪些词)才考虑 fielddata,且要先算内存账。- 聚合本身怎么写、
terms的默认桶数是多少,见 从零跑通 OpenSearch 聚合与分面。
References(参考来源)
- Text field type — OpenSearch 官方文档opensearch-docs
- Terms aggregation — OpenSearch 官方文档opensearch-docs
- Faceted search — OpenSearch 官方教程opensearch-docs
- Error Log: Illegal_argument_exception - Text fields are not optimised — OpenSearch 官方博客opensearch-blog
- Issue with message field data — OpenSearch 论坛opensearch-forum
