“為什麼搜出來的結果總數永遠是 10,000?是不是數據丟了?”
這是每一個 Elasticsearch 新手在 7.x 版本之後都會遇到的“靈魂發問”。
而在得知“這是 ES 的默認性能優化”後,絕大多數人的第一反應往往是抗拒:“我不想要優化,我要精準的數字!老闆要看,前端分頁也要用。”
於是,在許多項目的代碼庫裏,我們都能看到這樣一行“補丁”:
"track_total_hits": true
數字終於準了,變成了 854,321,大家都很滿意。
但沒人注意到,就在這行代碼生效的瞬間,集羣的 CPU 佔用率可能直接翻倍,查詢延遲從 20ms 劣化到了 500ms。
為什麼一個簡單的“計數”,會成為拖垮集羣性能的隱形殺手?
今天,我們不談玄學,從 Lucene 底層原理 層面,揭秘這背後的代價。
現象:消失的“尾部數據”
在 ES 7.0 之前,查詢默認會算出精確總數。但在 7.0 之後,默認的響應體變成了這樣:
"hits": {
"total": {
"value": 10000,
"relation": "gte" // 注意:Greater Than or Equal to (大於等於)
},
...
}
這不僅是一個數字的變化,更是搜索引擎檢索邏輯的根本性範式轉移:
從“找出所有匹配文檔”進化為“只找出最有價值的 Top N”。
深度解析:Block-Max WAND 算法的智慧
要理解為什麼“數不準”能帶來性能飛躍,必須深入 Lucene 的底層,瞭解 Block-Max WAND (BMW) 算法。
1. 倒排索引的物理結構:Block
在 Lucene 的倒排索引(Inverted Index)中,一個 Term(詞項)對應的 Postings List(文檔 ID 列表)並不是一整條長鏈,而是被切分成了多個 Block(塊),通常包含 128 個文檔 ID。
關鍵點在於:
每個 Block 都有一個 Block Header,裏面記錄了這個塊的元數據,其中最重要的一個是:
Max Score(塊內最大分數):即這個 Block 裏所有文檔中,能產生的最高相關性得分是多少。
2. 競速機制:Min Competitive Score
當 ES 執行查詢(比如查詢“手機”)並索要 Top 10 結果時,它會維護一個 最小競爭分數(Min Competitive Score)。
-
剛開始,Top 10 沒滿,最小競爭分數是 0。
-
隨着掃描進行,找到了 10 個文檔,第 10 名的分數是 5.0。
-
此時,最小競爭分數變成了 5.0。意味着:任何分數低於 5.0 的文檔,連進決賽圈的資格都沒有。
3. “隔山打牛”的跳躍優化
接下來,神奇的事情發生了。
當遍歷指針來到下一個 Block(假設包含 ID 1000-1127)時,算法不需要解壓這個 Block,也不需要計算具體的文檔分數,而是直接看 Block Header:
算法拷問: “這個 Block 的 Max Score 是多少?”Block 回答: “最高只能得 4.5 分。”算法判斷: “現在的門檻(Min Competitive Score)已經是 5.0 了。你這個 Block 裏的文檔全是‘廢柴’,整個 Block 直接跳過!”
結果: ES 可能直接跳過了數百萬個低分文檔。
代價: 因為跳過了,ES 根本不知道剛才那個 Block 裏有幾個文檔匹配(雖然分低,但確實匹配)。所以,它無法給出精確的總數。
隱形陷阱:為什麼關了 track_total_hits 依然慢?
你以為把 track_total_hits 設為 false 就萬事大吉了?
別天真了。 在某些特定場景下,即使你顯式關閉了計數,ES 依然無法使用 Block-Max WAND 進行剪枝,查詢性能依然會很差。
這就好比你告訴司機“不用數路邊有幾棵樹(不計數)”,但你同時又下令“把路邊每棵樹的葉子都摘一片下來(聚合)”。司機雖然不用數數,但他依然得停在每一棵樹下。
1. 聚合(Aggregations):WAND 的天敵
WAND 算法的核心奧義是 “Skipping(跳過)”。
而聚合(Aggregations)的核心訴求是 “Traversing(遍歷)”。
如果你在查詢中包含任何聚合(例如 terms、avg、date_histogram),ES 必須訪問所有匹配的文檔來計算統計值。
-
衝突點:你不能跳過一個 Block,因為那個被跳過的 Block 裏可能包含聚合所需的數據。
-
結果:WAND 優化被迫關閉,全量掃描不可避免。
2. 排序與分數的糾結(Sort + track_scores)
Block-Max WAND 依賴於 Max Score 來進行跳躍。
-
場景:你按時間排序 (
sort: [{"timestamp": "desc"}])。 -
陷阱:如果你手滑加了
track_scores: true(或者某些 Client 默認開啓)。 -
後果:雖然是按時間排序,但因為你強行索要
_score,ES 必須對每個文檔計算分數,導致無法利用 BKD Tree 的索引順序進行快速跳躍,也難以通過 WAND 進行分數剪枝(因為排序不是按分數排的)。
代價:track_total_hits 如何摧毀性能
口説無憑,數據為證。我們在 Serverless 8.17 環境下,創建一個6分片1副本的索引,寫入約 2 億條 脱敏文檔,總計約30G數據,查詢qps控制在30 。用實測結果向你展示——track_total_hits 取不同值時的cu消耗及響應時長:
CPU 資源消耗對比(true,false,10000)
當你加上 "track_total_hits": true 時,你實際上是在對底層引擎下達一道“死命令”:
“禁用 Block-Max WAND 優化,禁止跳過任何一個 Block。”這帶來的性能崩塌是顯著的。
1. CPU 的燃燒(計算密集)
-
優化模式:只解壓和計算高分 Block。
-
強制計數:必須解壓所有匹配的 Block(使用 Frame Of Reference 編碼),並對每一個文檔 ID 進行解碼和比對。
-
量級差異:如果查詢匹配 1 億條數據,原本只需計算 Top 1000 的分數,現在必須計算 1 億次。CPU 消耗可能增加幾個數量級。
2. I/O 的雪崩(訪存密集)
-
優化模式:低分 Block(通常是歷史冷數據)直接跳過,不需要從磁盤讀取。
-
強制計數:強制讀取所有 Block。
-
後果:這會導致大量的隨機 I/O。更致命的是,這些本該沉睡的冷數據被加載到內存中,會污染 Page Cache,把真正熱點的數據(比如最近 5 分鐘的日誌)擠出去。
-
現象:你會發現,為了查一個總數,整個集羣的寫入性能和熱查詢性能都下降了。
決策矩陣:到底什麼時候該用?
我們需要建立一個清晰的決策標準,而不是盲目地 true。
Serverless 下的治理之道
在自建集羣中,很難限制開發者的代碼行為。但在 阿里雲 ES Serverless 中,我們建議通過配置進行防禦性治理。
1. 軟限制:"Track Total Hits上限"
你可以通過 ES Serverless 控制枱—開啓功能設置,給查詢加上一道“保護鎖”:
-
效果:一旦設置,即使客户端請求中帶了
"track_total_hits": true,服務端也會強制忽略,只計算到 10,000。 -
價值:從根源上防止了某個實習生的一行代碼搞掛整個生產集羣。
2. 擁抱“模糊的正確”
在雲原生時代,算力是按量計費的(CU)。
“無意義的精確計數” = “直接燒錢”。
利用 Serverless 的彈性與治理能力,將算力集中在真正產生業務價值的 Top N 搜索上,才是降本增效的關鍵。
附錄:代碼與實操
1. 正確的查詢姿勢 (DSL)
場景 A:只要 Top 10 (性能最快)
GET /logs/_search
{
"query": { "match": { "message": "error" } },
"size": 10,
"track_total_hits": false // 顯式關閉,連 10000 都不數,最快
}
場景 B:需要精確計數 (Java Client)
SearchRequest searchRequest = new SearchRequest("logs");
SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
sourceBuilder.query(QueryBuilders.matchQuery("message", "error"));
// ⚠️ 警告:僅在必要時開啓,注意性能損耗
sourceBuilder.trackTotalHits(true);
// 或者設置一個具體的上限,例如 5萬// sourceBuilder.trackTotalHitsUpTo(50000);
searchRequest.source(sourceBuilder);
2. 高效替代方案:Cardinality 聚合
如果你只是想知道“大概有多少條日誌”,不要用 track_total_hits,請用 HyperLogLog 算法:
GET /logs/_search
{
"size": 0,
"aggs": {
"total_count_approx": {
"cardinality": {
"field": "_id", // 或者其他高基數主鍵
"precision_threshold": 10000
}
}
}
}
-
優點:性能極快,內存佔用極低。
-
缺點:有約 5% 以內的誤差。