大家好,我是藍胖子,在上一節我提到要想徹底搞懂elasticsearch 慢查詢的原因,必須搞懂lucene的查詢原理,所以在上一節我分析了lucene查詢的整體流程,除此以外,還必須要搞懂各種查詢型別內部是如何工作,比如比較複雜的查詢是將一個大查詢分解成了小查詢,然後通過對小查詢的結果進行合併得到最終結果。
今天就來看看幾種比較常見的查詢其內部的工作原理。
首先來看下布林查詢,拿下面這段程式碼舉例,我用lucene寫了一個布林查詢的例子,布林查詢由兩個term查詢組成,其中一個term是用must,一個term用的是should。
BooleanQuery.Builder query = new BooleanQuery.Builder();
query.add(new TermQuery(new Term(field1, "w3")), BooleanClause.Occur.MUST);
query.add(new TermQuery(new Term(field2, "xx")), BooleanClause.Occur.SHOULD);
int[] expDocNrs = {2, 3, 1, 0};
queriesTest(query.build(), expDocNrs);
布林查詢會將兩個term查詢的倒排鏈進行合併,得到最終結果。上一節有提到,計分邏輯是通過bulkScore.score方法實現的。在bulkScore.score方法內部 ,需要先遍歷篩選出符合條件的檔案,然後對該檔案進行計分,無論是篩選出符合條件的檔案,還是對檔案計分,都與weight物件建立的scorer物件有關,遍歷用到的是DocIdSetIterator,計分用到的是score() 方法,scorer涉及到的方法如下,
其中計分方法score是在scorer抽象類又繼承的一個Scorable 抽象類中,如下所示
public abstract class Scorer extends Scorable {
...
}
在遍歷倒排列表取出檔案id時,會呼叫DocIdSetIterator 的nextDoc 方法取出當前檔案id,並將便利指標移動到倒排列表的下一個檔案id處。
但是布林查詢往往是多個條件的組合查詢,它不可能是隻遍歷一個倒排連結串列,所以布林查詢的實現中,針對查詢條件生成了特殊的scorer物件,比如ConjunctionScorer 交集scorer,它會將查詢條件組合起來,並且利用子查詢的DocIdSetIterator 構造新的DocIdSetIterator 用於遍歷篩選出符合條件的檔案id。ConjunctionScorer 的nextDoc方法就相當於是在執行多個倒排連結串列合併的過程。
關於倒排連結串列的合併過程就不在這篇檔案繼續展開了。除此以外,布林查詢構建的scorer物件還有 並集DisjunctionSumScorer,差集ReqExclScorer,ReqOptSumScorer。它們的nextDoc方法也都是在做遍歷倒排連結串列取出檔案id的操作,不過遍歷合併倒排連結串列的邏輯各有不同。
所以,如果你的布林查詢命中結果比較多,並且需要計分的話, 會導致在進行倒排連結串列合併操作時花費比較長的時間。比如我之前碰到的一個慢查詢,經過profile的分析如下,布林查詢在next_doc操作上耗時比較長,next_doc對於布林查詢而言是在進行倒排連結串列的合併。
而對於布林查詢的子查詢term查詢你會發現耗時基本是花在了advance操作上。因為倒排列表合併過程中會有很多移動遍歷指標的操作也就是advance操作,所以在倒排列表比較長時,要想完整遍歷合併多個倒排列表則會有很多advance操作。
接著看另外一個常見的查詢型別MultiTermQuery,它的查詢重寫分好幾種型別,具體的重寫型別區別可以檢視官方文 https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-multi-term-rewrite.html
這裡我拿其中一種 CONSTANT_SCORE_BLENDED_REWRITE 舉例,這也是在複雜查詢例如
預設使用的重寫型別。
wildcardQuery這些模糊匹配,正則匹配差詢首先是構建自動狀態機,然後預設會將查詢重寫成為了CONSTANT_SCORE_BLENDED_REWRITE型別的MultiTermQuery查詢。
之後在建立weight的scorer物件時,會將詞典term dictionary中的term與自動狀態機做匹配,選出符合條件的term,根據term的個數判斷是將查詢重寫為布林查詢還是直接構建bitset用於後續計分時進行迭代遍歷。
符合條件的term 大於16個,則會進行bitset的構建,構建過程則是將符合條件的term對應的倒排列表取出來加到一個bitset中。這個過程是比較耗時的,特別是term對應的倒排列表過大或者term數量過多時,耗時會非常長。注意這個構建過程是發生在scoer物件建立的時候,即build_scorer階段。拿我之前遇到的一個慢查詢舉例,這是一個匹配到的term數量比較多的wildcardQuery,
下面是執行的DSL語句,
{"size":1000,"query":{"bool":{"filter":[{"term":{"owner_uid":{"value":712377485,"boost":1.0}}},{"term":{"pid":{"value":0,"boost":1.0}}},{"wildcard":{"name":{"wildcard":"*","boost":1.0}}},{"exists":{"field":"vgroup","boost":1.0}}],"adjust_pure_negative":true,"boost":1.0}},"_source":{"includes":["name"],"excludes":[]}}
經過profile分析可以看到wildcardQuery已經被重寫為了MultiTermQueryConstantScoreWrapper,耗時過長最大的階段則是在build_scorer階段,對每個階段不太熟悉的話可以翻看我前一篇文章 https://mp.weixin.qq.com/s/Drhs6lKPYy8vDHa2RouiyA
注意像wildcardQuery,字首匹配這些查詢都會構建自動狀態機,構建自動狀態機的過程在匹配規則文字比較長時,非常消耗cpu,生產上注意限制匹配規則文字長度,並且構建自動狀態機花費的時長不會體現在profile輸出結果中。