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

這篇幫到你了嗎?

剛解決了一個棘手的報錯?花兩分鐘記錄下來,幫助下一個遇到同樣問題的開發者。

貢獻一條解法