terms聚合的size預設是 10,字段有 12 個取值也只返回 10 個桶,被丟掉的計數彙總在sum_other_doc_count裡。調大size即可拿全;多分片下每個分片只上報size * 1.5 + 10個候選,計數可能不準,誤差上界看doc_count_error_upper_bound。桶特別多時改用composite聚合分頁。適用 OpenSearch 2.x / 3.x,本文對 3.9.0 校驗。
Issue & Env(案發現場與環境)
- 業務場景:給內容站做類型篩選面板,
genre是個keyword字段,一共 12 個取值。前端只渲染出 10 個篩選項,剩下兩個類型在頁面上完全消失,接口也沒報錯。 - 環境:OpenSearch 3.9.0,Docker 單節點,1 主分片、0 副本,
genre映射為keyword。 - 關鍵區分:不是數據沒進去,也不是 mapping 寫錯。
_count是 12,terms只回 10 個桶,而且響應裡sum_other_doc_count正好等於 2。
StackTrace(響應原文)
{
"aggregations": {
"by_genre": {
"doc_count_error_upper_bound": 0,
"sum_other_doc_count": 2,
"buckets": [
{ "key": "action", "doc_count": 1 },
{ "key": "comedy", "doc_count": 1 },
{ "key": "drama", "doc_count": 1 },
{ "key": "horror", "doc_count": 1 },
{ "key": "scifi", "doc_count": 1 },
{ "key": "documentary", "doc_count": 1 },
{ "key": "animation", "doc_count": 1 },
{ "key": "thriller", "doc_count": 1 },
{ "key": "romance", "doc_count": 1 },
{ "key": "mystery", "doc_count": 1 }
]
}
}
}
12 個類型,10 個桶,sum_other_doc_count 是 2。計數全是 1 時,具體返回哪 10 個並沒有保證——同樣計數的桶之間順序取決於分片內部順序,別拿這份列表當穩定契約。
Reproduce(復現程式碼)
前提:本地 OpenSearch 已啟動,示例值沿用 quickstart(端口 9200、用戶 admin、密碼 Devfix@2026)。
建一個只有 genre 的索引,導入 12 條各不相同的類型:
curl -sk -u admin:Devfix@2026 -X PUT "https://localhost:9200/shows?pretty" \
-H "Content-Type: application/json" \
-d '{
"settings": { "index.number_of_shards": 1, "index.number_of_replicas": 0 },
"mappings": { "properties": { "genre": { "type": "keyword" } } }
}'
cat > shows.ndjson <<'EOF'
{ "index": { "_index": "shows", "_id": "1" } }
{ "genre": "action" }
{ "index": { "_index": "shows", "_id": "2" } }
{ "genre": "comedy" }
{ "index": { "_index": "shows", "_id": "3" } }
{ "genre": "drama" }
{ "index": { "_index": "shows", "_id": "4" } }
{ "genre": "horror" }
{ "index": { "_index": "shows", "_id": "5" } }
{ "genre": "scifi" }
{ "index": { "_index": "shows", "_id": "6" } }
{ "genre": "documentary" }
{ "index": { "_index": "shows", "_id": "7" } }
{ "genre": "animation" }
{ "index": { "_index": "shows", "_id": "8" } }
{ "genre": "thriller" }
{ "index": { "_index": "shows", "_id": "9" } }
{ "genre": "romance" }
{ "index": { "_index": "shows", "_id": "10" } }
{ "genre": "mystery" }
{ "index": { "_index": "shows", "_id": "11" } }
{ "genre": "fantasy" }
{ "index": { "_index": "shows", "_id": "12" } }
{ "genre": "western" }
EOF
curl -sk -u admin:Devfix@2026 -X POST "https://localhost:9200/_bulk?pretty" \
-H "Content-Type: application/x-ndjson" \
--data-binary @shows.ndjson
不寫 size,直接聚合:
curl -sk -u admin:Devfix@2026 -X GET "https://localhost:9200/shows/_search?pretty" \
-H "Content-Type: application/json" \
-d '{ "size": 0, "aggs": { "by_genre": { "terms": { "field": "genre" } } } }'
判定標準:buckets 陣列長度是 10、sum_other_doc_count 是 2,即復現成功。
Root Cause(根因分析)
size 預設 10,多餘的桶被摺疊
官方 terms 文檔的參數表寫得很直接:size 是「要返回的桶數,預設 10」。被摺疊的桶不是消失了,它們的文檔數加總進 sum_other_doc_count。所以這不屬於數據丟失,只是返回上限。
分佈式下還有一層近似
terms 不是把全量桶排好序再截斷。每個分片先在本地取候選,再彙總排序。文檔裡 shard_size 的作用是「每個分片收集的候選詞條數」,預設 size * 1.5 + 10;doc_count_error_upper_bound 表示「某個桶的計數可能被低估的最大幅度」。
單分片時這兩個值通常都是 0,看起來一切精確;一旦索引有多個分片、字段取值又很分散,doc_count_error_upper_bound 就可能大於 0。這時不是程式碼寫錯,而是 terms 本身的近似性。
桶的順序也不是「按字母」
order 預設 {"_count": "desc"},按文檔數降序。計數相同的桶之間沒有穩定順序,想要固定順序得顯式指定 {"_key": "asc"}。
The Fix(解決方案)
方案一:顯式設置 size
知道取值上限就直接給足:
curl -sk -u admin:Devfix@2026 -X GET "https://localhost:9200/shows/_search?pretty" \
-H "Content-Type: application/json" \
-d '{
"size": 0,
"aggs": {
"by_genre": {
"terms": { "field": "genre", "size": 20, "order": { "_key": "asc" } }
}
}
}'
這次 12 個桶全在,sum_other_doc_count 變成 0,順序也固定成字母序。
方案二:桶多到一頁放不下時用 composite
取值可能有幾百上千個時,terms 的 size 開再大也只是「一次拿一頁」,還費記憶體。composite 聚合支持 after 游標翻頁,適合「把所有類型逐個拉完」的離線任務:
curl -sk -u admin:Devfix@2026 -X GET "https://localhost:9200/shows/_search?pretty" \
-H "Content-Type: application/json" \
-d '{
"size": 0,
"aggs": {
"genres": {
"composite": {
"size": 5,
"sources": [ { "genre": { "terms": { "field": "genre" } } } ]
}
}
}
}'
響應裡會帶 after_key,把它塞回請求的 composite.after 就是下一頁。
方案三:需要更準的計數時調 shard_size
多分片場景下,把 shard_size 調大能減少誤差,代價是每個分片多做一輪候選收集:
curl -sk -u admin:Devfix@2026 -X GET "https://localhost:9200/shows/_search?pretty" \
-H "Content-Type: application/json" \
-d '{
"size": 0,
"aggs": {
"by_genre": {
"terms": { "field": "genre", "size": 20, "shard_size": 100 }
}
}
}'
注意事項
- 判斷是否被截斷,看
sum_other_doc_count,別看桶的數量猜。 size只控制返回桶數,不影響hits;兩者是獨立的兩個參數。- 分面面板通常給
size留一點餘量(比如展示 10 個就設 12),再用bucket_sort做最終截斷。 - 聚合怎麼寫、
bucket_sort怎麼用,見 從零跑通 OpenSearch 聚合與分面。
References(參考來源)
- Terms aggregation — OpenSearch 官方文檔opensearch-docs
- Composite aggregation — OpenSearch 官方文檔opensearch-docs
- Aggregations — OpenSearch 官方文檔opensearch-docs
