Lucene學習總結之七:Lucene搜尋過程解析(7)轉
2.4、搜尋查詢物件
2.4.3.2、並集DisjunctionSumScorer(A OR B)
DisjunctionSumScorer中有成員變數List<Scorer> subScorers,是一個Scorer的連結串列,每一項代表一個倒排表,DisjunctionSumScorer就是對這些倒排表取並集,然後將並集中的文件號在nextDoc()函式中依次返回。
DisjunctionSumScorer還有一個成員變數minimumNrMatchers,表示最少需滿足的子條件的個數,也即subScorer中,必須有至少minimumNrMatchers個Scorer都包含某個文件號,此文件號才能夠返回。
為了描述清楚此過程,下面舉一個具體的例子來解釋倒排表合併的過程:
(1) 假設minimumNrMatchers = 4,倒排表最初如下:
(2) 在DisjunctionSumScorer的建構函式中,將倒排表放入一個優先順序佇列scorerDocQueue中(scorerDocQueue的實現是一個最小堆),佇列中的Scorer按照第一篇文件的大小排序。
private void initScorerDocQueue() throws IOException { scorerDocQueue = new ScorerDocQueue(nrScorers); for (Scorer se : subScorers) { if (se.nextDoc() != NO_MORE_DOCS) { //此處的nextDoc使得每個Scorer得到第一篇文件號。 scorerDocQueue.insert(se); } } } |
(3) 當BooleanScorer2.score(Collector)中第一次呼叫nextDoc()的時候,advanceAfterCurrent被呼叫。
public int nextDoc() throws IOException { if (scorerDocQueue.size() < minimumNrMatchers || !advanceAfterCurrent()) { currentDoc = NO_MORE_DOCS; } return currentDoc; } |
protected boolean advanceAfterCurrent() throws IOException { do { currentDoc = scorerDocQueue.topDoc(); //當前的文件號為最頂層 currentScore = scorerDocQueue.topScore(); //當前文件的打分 nrMatchers = 1; //當前文件滿足的子條件的個數,也即包含當前文件號的Scorer的個數 do { //所謂topNextAndAdjustElsePop是指,最頂層(top)的Scorer取下一篇文件(Next),如果能夠取到,則最小堆的堆頂可能不再是最小值了,需要調整(Adjust,其實是downHeap()),如果不能夠取到,則最頂層的Scorer已經為空,則彈出佇列(Pop)。 if (!scorerDocQueue.topNextAndAdjustElsePop()) { if (scorerDocQueue.size() == 0) { break; // nothing more to advance, check for last match. } } //當最頂層的Scorer取到下一篇文件,並且調整完畢後,再取出此時最上層的Scorer的第一篇文件,如果不是currentDoc,說明currentDoc此文件號已經統計完畢nrMatchers,則退出內層迴圈。 if (scorerDocQueue.topDoc() != currentDoc) { break; // All remaining subscorers are after currentDoc. } //否則nrMatchers加一,也即又多了一個Scorer也包含此文件號。 currentScore += scorerDocQueue.topScore(); nrMatchers++; } while (true); //如果統計出的nrMatchers大於最少需滿足的子條件的個數,則此currentDoc就是滿足條件的文件,則返回true,在收集文件的過程中,DisjunctionSumScorer.docID()會被呼叫,返回currentDoc。 if (nrMatchers >= minimumNrMatchers) { return true; } else if (scorerDocQueue.size() < minimumNrMatchers) { return false; } } while (true); } |
advanceAfterCurrent具體過程如下:
- 最初,currentDoc=2,文件2的nrMatchers=1
- 最頂層的Scorer 0取得下一篇文件,為文件3,重新調整最小堆後如下圖。此時currentDoc等於最頂層Scorer 1的第一篇文件號,都為2,文件2的nrMatchers為2。
- 最頂層的Scorer 1取得下一篇文件,為文件8,重新調整最小堆後如下圖。此時currentDoc等於最頂層Scorer 3的第一篇文件號,都為2,文件2的nrMatchers為3。
- 最頂層的Scorer 3取得下一篇文件,為文件7,重新調整最小堆後如下圖。此時currentDoc還為2,不等於最頂層Scorer 2的第一篇文件3,於是退出內迴圈。此時檢查,發現文件2的nrMatchers為3,小於minimumNrMatchers,不滿足條件。於是currentDoc設為最頂層Scorer 2的第一篇文件3,nrMatchers設為1,重新進入下一輪迴圈。
- 最頂層的Scorer 2取得下一篇文件,為文件5,重新調整最小堆後如下圖。此時currentDoc等於最頂層Scorer 4的第一篇文件號,都為3,文件3的nrMatchers為2。
- 最頂層的Scorer 4取得下一篇文件,為文件7,重新調整最小堆後如下圖。此時currentDoc等於最頂層Scorer 0的第一篇文件號,都為3,文件3的nrMatchers為3。
- 最頂層的Scorer 0取得下一篇文件,為文件5,重新調整最小堆後如下圖。此時currentDoc還為3,不等於最頂層Scorer 0的第一篇文件5,於是退出內迴圈。此時檢查,發現文件3的nrMatchers為3,小於minimumNrMatchers,不滿足條件。於是currentDoc設為最頂層Scorer 0的第一篇文件5,nrMatchers設為1,重新進入下一輪迴圈。
- 最頂層的Scorer 0取得下一篇文件,為文件7,重新調整最小堆後如下圖。此時currentDoc等於最頂層Scorer 2的第一篇文件號,都為5,文件5的nrMatchers為2。
- 最頂層的Scorer 2取得下一篇文件,為文件7,重新調整最小堆後如下圖。此時currentDoc還為5,不等於最頂層Scorer 2的第一篇文件7,於是退出內迴圈。此時檢查,發現文件5的nrMatchers為2,小於minimumNrMatchers,不滿足條件。於是currentDoc設為最頂層Scorer 2的第一篇文件7,nrMatchers設為1,重新進入下一輪迴圈。
- 最頂層的Scorer 2取得下一篇文件,為文件8,重新調整最小堆後如下圖。此時currentDoc等於最頂層Scorer 3的第一篇文件號,都為7,文件7的nrMatchers為2。
- 最頂層的Scorer 3取得下一篇文件,為文件9,重新調整最小堆後如下圖。此時currentDoc等於最頂層Scorer 4的第一篇文件號,都為7,文件7的nrMatchers為3。
- 最頂層的Scorer 4取得下一篇文件,結果為空,Scorer 4所有的文件遍歷完畢,彈出佇列,重新調整最小堆後如下圖。此時currentDoc等於最頂層Scorer 0的第一篇文件號,都為7,文件7的nrMatchers為4。
- 最頂層的Scorer 0取得下一篇文件,為文件9,重新調整最小堆後如下圖。此時currentDoc還為7,不等於最頂層Scorer 1的第一篇文件8,於是退出內迴圈。此時檢查,發現文件7的nrMatchers為4,大於等於minimumNrMatchers,滿足條件,返回true,退出外迴圈。
(4) currentDoc設為7,在收集文件的過程中,DisjunctionSumScorer.docID()會被呼叫,返回currentDoc,也即當前的文件號為7。
(5) 當再次呼叫nextDoc()的時候,文件8, 9, 11都不滿足要求,最後返回NO_MORE_DOCS,倒排表合併結束。
2.4.3.3、差集ReqExclScorer(+A -B)
ReqExclScorer有成員變數Scorer reqScorer表示必須滿足的部分(required),成員變數DocIdSetIterator exclDisi表示必須不能滿足的部分,ReqExclScorer就是返回reqScorer和exclDisi的倒排表的差集,也即在reqScorer的倒排表中排除exclDisi中的文件號。
當nextDoc()呼叫的時候,首先取得reqScorer的第一個文件號,然後toNonExcluded()函式則判斷此文件號是否被exclDisi排除掉,如果沒有,則返回此文件號,如果排除掉,則取下一個文件號,看是否被排除掉,依次類推,直到找到一個文件號,或者返回NO_MORE_DOCS。
public int nextDoc() throws IOException { if (reqScorer == null) { return doc; } doc = reqScorer.nextDoc(); if (doc == NO_MORE_DOCS) { reqScorer = null; return doc; } if (exclDisi == null) { return doc; } return doc = toNonExcluded(); } |
private int toNonExcluded() throws IOException { //取得被排除的文件號 int exclDoc = exclDisi.docID(); //取得當前required文件號 int reqDoc = reqScorer.docID(); do { //如果required文件號小於被排除的文件號,由於倒排表是按照從小到大的順序排列的,因而此required文件號不會被排除,返回。 if (reqDoc < exclDoc) { return reqDoc; } else if (reqDoc > exclDoc) { //如果required文件號大於被排除的文件號,則此required文件號有可能被排除。於是exclDisi移動到大於或者等於required文件號的文件。 exclDoc = exclDisi.advance(reqDoc); //如果被排除的倒排表遍歷結束,則required文件號不會被排除,返回。 if (exclDoc == NO_MORE_DOCS) { exclDisi = null; return reqDoc; } //如果exclDisi移動後,大於required文件號,則required文件號不會被排除,返回。 if (exclDoc > reqDoc) { return reqDoc; // not excluded } } //如果required文件號等於被排除的文件號,則被排除,取下一個required文件號。 } while ((reqDoc = reqScorer.nextDoc()) != NO_MORE_DOCS); reqScorer = null; return NO_MORE_DOCS; } |
2.4.3.4、ReqOptSumScorer(+A B)
ReqOptSumScorer包含兩個成員變數,Scorer reqScorer代表必須(required)滿足的文件倒排表,Scorer optScorer代表可以(optional)滿足的文件倒排表。
如程式碼顯示,在nextDoc()中,返回的就是required的文件倒排表,只不過在計算score的時候打分更高。
public int nextDoc() throws IOException { return reqScorer.nextDoc(); } |
2.4.3.5、有關BooleanScorer及scoresDocsOutOfOrder
在BooleanWeight.scorer生成Scorer樹的時候,除了生成上述的BooleanScorer2外, 還會生成BooleanScorer,是在以下的條件下:
- !scoreDocsInOrder:根據2.4.2節的步驟(c),scoreDocsInOrder = !collector.acceptsDocsOutOfOrder(),此值是在search中呼叫TopScoreDocCollector.create(nDocs, !weight.scoresDocsOutOfOrder())的時候設定的,scoreDocsInOrder = !weight.scoresDocsOutOfOrder(),其程式碼如下:
public boolean scoresDocsOutOfOrder() { int numProhibited = 0; for (BooleanClause c : clauses) { if (c.isRequired()) { return false; } else if (c.isProhibited()) { ++numProhibited; } } if (numProhibited > 32) { return false; } return true; } |
- topScorer:根據2.4.2節的步驟(c),此值為true。
- required.size() == 0,沒有必須滿足的子語句。
- prohibited.size() < 32,不需不能滿足的子語句小於32。
從上面可以看出,最後兩個條件和scoresDocsOutOfOrder函式中的邏輯是一致的。
下面我們看看BooleanScorer如何合併倒排表的:
public int nextDoc() throws IOException { boolean more; do { //bucketTable等於是存放合併後的倒排表的文件佇列 while (bucketTable.first != null) { //從佇列中取出第一篇文件,返回 current = bucketTable.first; bucketTable.first = current.next; if ((current.bits & prohibitedMask) == 0 && (current.bits & requiredMask) == requiredMask && current.coord >= minNrShouldMatch) { return doc = current.doc; } } //如果佇列為空,則填充佇列。 more = false; end += BucketTable.SIZE; //按照Scorer的順序,依次用Scorer中的倒排表填充佇列,填滿為止。 for (SubScorer sub = scorers; sub != null; sub = sub.next) { Scorer scorer = sub.scorer; sub.collector.setScorer(scorer); int doc = scorer.docID(); while (doc < end) { sub.collector.collect(doc); doc = scorer.nextDoc(); } more |= (doc != NO_MORE_DOCS); } } while (bucketTable.first != null || more); return doc = NO_MORE_DOCS; } |
public final void collect(final int doc) throws IOException { final BucketTable table = bucketTable; final int i = doc & BucketTable.MASK; Bucket bucket = table.buckets[i]; if (bucket == null) table.buckets[i] = bucket = new Bucket(); if (bucket.doc != doc) { bucket.doc = doc; bucket.score = scorer.score(); bucket.bits = mask; bucket.coord = 1; bucket.next = table.first; table.first = bucket; } else { bucket.score += scorer.score(); bucket.bits |= mask; bucket.coord++; } } |
從上面的實現我們可以看出,BooleanScorer合併倒排表的時候,並不是按照文件號從小到大的順序排列的。
從原理上我們可以理解,在AND的查詢條件下,倒排表的合併按照演算法需要按照文件號從小到大的順序排列。然而在沒有AND的查詢條件下,如果都是OR,則文件號是否按照順序返回就不重要了,因而scoreDocsInOrder就是false。
因而上面的DisjunctionSumScorer,其實"apple boy dog"是不能產生DisjunctionSumScorer的,而僅有在有AND的查詢條件下,才產生DisjunctionSumScorer。
我們做實驗如下:
對於查詢語句"apple boy dog",生成的Scorer如下:
scorer BooleanScorer (id=34) |
對於查詢語句"+hello (apple boy dog)",生成的Scorer物件如下:
scorer BooleanScorer2 (id=40) //weight(contents:apple) //weight(contents:boy) //weight(contents:cat) |