StackTraces
OpenSearch已验证

OpenSearch 对 text 字段做聚合报 fielddata 错误:换 keyword 或加 .keyword 子字段

@debug_master2 天前更新阅读 6 min00

对 title 这种 text 字段跑 terms 聚合,会返回 400 illegal_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(参考来源)

最后更新于 2026-10-27

这篇帮到你了吗?

刚解决了一个棘手的报错?花两分钟记录下来,帮助下一个遇到同样问题的开发者。

贡献一条解法