對
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
