1. 程式人生 > >Lucene學習總結之七:Lucene搜尋過程解析(5)

Lucene學習總結之七:Lucene搜尋過程解析(5)

2.4、搜尋查詢物件

2.4.3、進行倒排表合併

在得到了Scorer物件樹以及SumScorer物件樹後,便是倒排表的合併以及打分計算的過程。

合併倒排表在此節中進行分析,而Scorer物件樹來進行打分的計算則在下一節分析。

BooleanScorer2.score(Collector) 程式碼如下:

public void score(Collector collector) throws IOException {

  collector.setScorer(this);

  while ((doc = countingSumScorer.nextDoc()) != NO_MORE_DOCS) {

    collector.collect(doc);

  }

}

從程式碼我們可以看出,此過程就是不斷的取下一篇文件號,然後加入文件結果集。

取下一篇文件的過程,就是合併倒排表的過程,也就是對多個查詢條件進行綜合考慮後的下一篇文件的編號。

由於SumScorer是一棵樹,因而合併倒排表也是按照樹的結構進行的,先合併子樹,然後子樹與子樹再進行合併,直到根。

按照上一節的分析,倒排表的合併主要用了以下幾個SumScorer:

  • 交集ConjunctionScorer
  • 並集DisjunctionSumScorer
  • 差集ReqExclScorer
  • ReqOptSumScorer

下面我們一一分析:

2.4.3.1、交集ConjunctionScorer(+A +B)

ConjunctionScorer中有成員變數Scorer[] scorers,是一個Scorer的陣列,每一項代表一個倒排表,ConjunctionScorer就是對這些倒排表取交集,然後將交集中的文件號在nextDoc()函式中依次返回。

為了描述清楚此過程,下面舉一個具體的例子來解釋倒排表合併的過程:

(1) 倒排表最初如下:

image

(2) 在ConjunctionScorer的建構函式中,首先呼叫每個Scorer的nextDoc()函式,使得每個Scorer得到自己的第一篇文件號。

for (int i = 0; i < scorers.length; i++) {

  if (scorers[i].nextDoc() == NO_MORE_DOCS) {

    //由於是取交集,因而任何一個倒排表沒有文件,交集就為空。

    lastDoc = NO_MORE_DOCS;

    return;

  }

}

(3) 在ConjunctionScorer的建構函式中,將Scorer按照第一篇的文件號從小到大進行排列。

Arrays.sort(scorers, new Comparator() {

  public int compare(Scorer o1, Scorer o2) {

    return o1.docID() - o2.docID();

  }

});

倒排表如下:

image

(4) 在ConjunctionScorer的建構函式中,第一次呼叫doNext()函式。

if (doNext() == NO_MORE_DOCS) {

  lastDoc = NO_MORE_DOCS;

  return;

}

private int doNext() throws IOException {

  int first = 0;

  int doc = scorers[scorers.length - 1].docID();

  Scorer firstScorer;

  while ((firstScorer = scorers[first]).docID() < doc) {

    doc = firstScorer.advance(doc);

    first = first == scorers.length - 1 ? 0 : first + 1;

  }

  return doc;

}

姑且我們稱擁有最小文件號的倒排表稱為first,其實從doNext()函式中的first = first == scorers.length - 1 ? 0 : first + 1;我們可以看出,在處理過程中,Scorer陣列被看成一個迴圈陣列(Ring)。

而此時scorer[scorers.length - 1]擁有最大的文件號,doNext()中的迴圈,將所有的小於當前陣列中最大文件號的文件全部用firstScorer.advance(doc)(其跳到大於或等於doc的文件)函式跳過,因為既然它們小於最大的文件號,而ConjunctionScorer又是取交集,它們當然不會在交集中。

此過程如下:

  • doc = 8,first指向第0項,advance到大於8的第一篇文件,也即文件10,然後設doc = 10,first指向第1項。

image

  • doc = 10,first指向第1項,advance到文件11,然後設doc = 11,first指向第2項。

image

  • doc = 11,first指向第2項,advance到文件11,然後設doc = 11,first指向第3項。

image

  • doc = 11,first指向第3項,advance到文件11,然後設doc = 11,first指向第4項。

image

  • doc = 11,first指向第4項,advance到文件11,然後設doc = 11,first指向第5項。

image

  • doc = 11,first指向第5項,advance到文件11,然後設doc = 11,first指向第6項。

image

  • doc = 11,first指向第6項,advance到文件11,然後設doc = 11,first指向第7項。

image

  • doc = 11,first指向第7項,advance到文件11,然後設doc = 11,first指向第0項。

image

  • doc = 11,first指向第0項,advance到文件11,然後設doc = 11,first指向第1項。

image

  • doc = 11,first指向第1項。因為11 < 11為false,因而結束迴圈,返回doc = 11。這時候我們會發現,在迴圈退出的時候,所有的倒排表的第一篇文件都是11。

image

(5) 當BooleanScorer2.score(Collector)中第一次呼叫ConjunctionScorer.nextDoc()的時候,lastDoc為-1,根據nextDoc函式的實現,返回lastDoc = scorers[scorers.length - 1].docID()也即返回11,lastDoc也設為11。

public int nextDoc() throws IOException {

  if (lastDoc == NO_MORE_DOCS) {

    return lastDoc;

  } else if (lastDoc == -1) {

    return lastDoc = scorers[scorers.length - 1].docID();

  }

  scorers[(scorers.length - 1)].nextDoc();

  return lastDoc = doNext();

}

(6) 在BooleanScorer2.score(Collector)中,呼叫nextDoc()後,collector.collect(doc)來收集文件號(收集過程下節分析),在收集文件的過程中,ConjunctionScorer.docID()會被呼叫,返回lastDoc,也即當前的文件號為11。

(7) 當BooleanScorer2.score(Collector)第二次呼叫ConjunctionScorer.nextDoc()時:

  • 根據nextDoc函式的實現,首先呼叫scorers[(scorers.length - 1)].nextDoc(),取最後一項的下一篇文件13。

image

  • 然後呼叫lastDoc = doNext(),設doc = 13,first = 0,進入迴圈。
  • doc = 13,first指向第0項,advance到文件13,然後設doc = 13,first指向第1項。

image

  • doc = 13,first指向第1項,advance到文件13,然後設doc = 13,first指向第2項。

image

  • doc = 13,first指向第2項,advance到文件13,然後設doc = 13,first指向第3項。

image

  • doc = 13,first指向第3項,advance到文件13,然後設doc = 13,first指向第4項。

image

  • doc = 13,first指向第4項,advance到文件13,然後設doc = 13,first指向第5項。

image

  • doc = 13,first指向第5項,advance到文件13,然後設doc = 13,first指向第6項。

image

  • doc = 13,first指向第6項,advance到文件13,然後設doc = 13,first指向第7項。

image

  • doc = 13,first指向第7項,advance到文件13,然後設doc = 13,first指向第0項。

image

  • doc = 13,first指向第0項。因為13 < 13為false,因而結束迴圈,返回doc = 13。在迴圈退出的時候,所有的倒排表的第一篇文件都是13。

image

(8) lastDoc設為13,在收集文件的過程中,ConjunctionScorer.docID()會被呼叫,返回lastDoc,也即當前的文件號為13。

(9) 當再次呼叫nextDoc()的時候,返回NO_MORE_DOCS,倒排表合併結束。

2.4.3.2、並集DisjunctionSumScorer(A OR B)

DisjunctionSumScorer中有成員變數List subScorers,是一個Scorer的連結串列,每一項代表一個倒排表,DisjunctionSumScorer就是對這些倒排表取並集,然後將並集中的文件號在nextDoc()函式中依次返回。

DisjunctionSumScorer還有一個成員變數minimumNrMatchers,表示最少需滿足的子條件的個數,也即subScorer中,必須有至少minimumNrMatchers個Scorer都包含某個文件號,此文件號才能夠返回。

為了描述清楚此過程,下面舉一個具體的例子來解釋倒排表合併的過程:

(1) 假設minimumNrMatchers = 4,倒排表最初如下:

image

(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);

    }

  }

}

image

(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

image

  • 最頂層的Scorer 0取得下一篇文件,為文件3,重新調整最小堆後如下圖。此時currentDoc等於最頂層Scorer 1的第一篇文件號,都為2,文件2的nrMatchers為2。

image

  • 最頂層的Scorer 1取得下一篇文件,為文件8,重新調整最小堆後如下圖。此時currentDoc等於最頂層Scorer 3的第一篇文件號,都為2,文件2的nrMatchers為3。

image

  • 最頂層的Scorer 3取得下一篇文件,為文件7,重新調整最小堆後如下圖。此時currentDoc還為2,不等於最頂層Scorer 2的第一篇文件3,於是退出內迴圈。此時檢查,發現文件2的nrMatchers為3,小於minimumNrMatchers,不滿足條件。於是currentDoc設為最頂層Scorer 2的第一篇文件3,nrMatchers設為1,重新進入下一輪迴圈。

image

  • 最頂層的Scorer 2取得下一篇文件,為文件5,重新調整最小堆後如下圖。此時currentDoc等於最頂層Scorer 4的第一篇文件號,都為3,文件3的nrMatchers為2。

image

  • 最頂層的Scorer 4取得下一篇文件,為文件7,重新調整最小堆後如下圖。此時currentDoc等於最頂層Scorer 0的第一篇文件號,都為3,文件3的nrMatchers為3。

image

  • 最頂層的Scorer 0取得下一篇文件,為文件5,重新調整最小堆後如下圖。此時currentDoc還為3,不等於最頂層Scorer 0的第一篇文件5,於是退出內迴圈。此時檢查,發現文件3的nrMatchers為3,小於minimumNrMatchers,不滿足條件。於是currentDoc設為最頂層Scorer 0的第一篇文件5,nrMatchers設為1,重新進入下一輪迴圈。

image

  • 最頂層的Scorer 0取得下一篇文件,為文件7,重新調整最小堆後如下圖。此時currentDoc等於最頂層Scorer 2的第一篇文件號,都為5,文件5的nrMatchers為2。

 image

  • 最頂層的Scorer 2取得下一篇文件,為文件7,重新調整最小堆後如下圖。此時currentDoc還為5,不等於最頂層Scorer 2的第一篇文件7,於是退出內迴圈。此時檢查,發現文件5的nrMatchers為2,小於minimumNrMatchers,不滿足條件。於是currentDoc設為最頂層Scorer 2的第一篇文件7,nrMatchers設為1,重新進入下一輪迴圈。

image

  • 最頂層的Scorer 2取得下一篇文件,為文件8,重新調整最小堆後如下圖。此時currentDoc等於最頂層Scorer 3的第一篇文件號,都為7,文件7的nrMatchers為2。

image

  • 最頂層的Scorer 3取得下一篇文件,為文件9,重新調整最小堆後如下圖。此時currentDoc等於最頂層Scorer 4的第一篇文件號,都為7,文件7的nrMatchers為3。

image

  • 最頂層的Scorer 4取得下一篇文件,結果為空,Scorer 4所有的文件遍歷完畢,彈出佇列,重新調整最小堆後如下圖。此時currentDoc等於最頂層Scorer 0的第一篇文件號,都為7,文件7的nrMatchers為4。

image

  • 最頂層的Scorer 0取得下一篇文件,為文件9,重新調整最小堆後如下圖。此時currentDoc還為7,不等於最頂層Scorer 1的第一篇文件8,於是退出內迴圈。此時檢查,發現文件7的nrMatchers為4,大於等於minimumNrMatchers,滿足條件,返回true,退出外迴圈。

image

(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)   
    bucketTable    BooleanScorer$BucketTable  (id=39)   
    coordFactors    float[4]  (id=41)   
    current    null   
    doc    -1   
    doc    -1   
    end    0   
    maxCoord    4   
    minNrShouldMatch    0   
    nextMask    1   
    prohibitedMask    0   
    requiredMask    0   
    scorers    BooleanScorer$SubScorer  (id=43)   
        collector    BooleanScorer$BooleanScorerCollector  (id=49)   
        next    BooleanScorer$SubScorer  (id=51)   
            collector    BooleanScorer$BooleanScorerCollector  (id=68)   
            next    BooleanScorer$SubScorer  (id=69)   
                collector    BooleanScorer$BooleanScorerCollector  (id=76)   
                next    null   
                prohibited    false   
                required    false   
                scorer    TermScorer  (id=77)   
                    doc    -1   
                    doc    0   
                    docs    int[32]  (id=79)   
                    freqs    int[32]  (id=80)   
                    norms    byte[4]  (id=58)   
                    pointer    0   
                    pointerMax    2   
                    scoreCache    float[32]  (id=81)   
                    similarity    DefaultSimilarity  (id=45)   
                    termDocs    SegmentTermDocs  (id=82)   
                    weight    TermQuery$TermWeight (id=84)  //weight(contents:apple) 
                    weightValue    0.828608   
            prohibited    false   
            required    false   
            scorer    TermScorer  (id=70)   
                doc    -1   
                doc    1   
                docs    int[32]  (id=72)   
                freqs    int[32]  (id=73)   
                norms    byte[4]  (id=58)   
                pointer    0   
                pointerMax    1   
                scoreCache    float[32]  (id=74)   
                similarity    DefaultSimilarity  (id=45)   
                termDocs    SegmentTermDocs  (id=86)   
                weight    TermQuery$TermWeight  (id=87) //weight(contents:boy)  
                weightValue    1.5407716   
        prohibited    false   
        required    false   
        scorer    TermScorer  (id=52)   
            doc    -1   
            doc    0   
            docs    int[32]  (id=54)   
            freqs    int[32]  (id=56)   
            norms    byte[4]  (id=58)   
            pointer    0   
            pointerMax    3   
            scoreCache    float[32]  (id=61)   
            similarity    DefaultSimilarity  (id=45)   
            termDocs    SegmentTermDocs  (id=62)   
            weight    TermQuery$TermWeight  (id=66)  //weight(contents:cat)  
            weightValue    0.48904076   
    similarity    DefaultSimilarity  (id=45)   

對於查詢語句"+hello (apple boy dog)",生成的Scorer物件如下:

scorer    BooleanScorer2  (id=40)   
    coordinator    BooleanScorer2$Coordinator  (id=42)   
    countingSumScorer    ReqOptSumScorer  (id=43)    
    minNrShouldMatch    0   
    optionalScorers    ArrayList  (id=44)   
        elementData    Object[10]  (id=62)   
            [0]    BooleanScorer2  (id=84)   
                coordinator    BooleanScorer2$Coordinator  (id=87)   
                countingSumScorer    BooleanScorer2$1  (id=88)    
                minNrShouldMatch    0   
                optionalScorers    ArrayList  (id=89)   
                    elementData    Object[10]  (id=95)   
                        [0]    TermScorer  (id=97)    
                            docs    int[32]  (id=101)   
                            freqs    int[32]  (id=102)   
                            norms    byte[4]  (id=71)   
                            pointer    0   
                            pointerMax    2   
                            scoreCache    float[32]  (id=103)   
                            similarity    DefaultSimilarity  (id=48)   
                            termDocs    SegmentTermDocs  (id=104)   

                            //weight(contents:apple)
                            weight    TermQuery$TermWeight  (id=105)   
                            weightValue    0.525491   
                        [1]    TermScorer  (id=98)    
                            docs    int[32]  (id=107)   
                            freqs    int[32]  (id=108)   
                            norms    byte[4]  (id=71)   
                            pointer    0   
                            pointerMax    1   
                            scoreCache    float[32]  (id=110)   
                            similarity    DefaultSimilarity  (id=48)   
                            termDocs    SegmentTermDocs  (id=111)   

                            //weight(contents:boy)
                            weight    TermQuery$TermWeight  (id=112)   
                            weightValue    0.9771348   
                        [2]    TermScorer  (id=99)    
                            docs    int[32]  (id=114)   
                            freqs    int[32]  (id=118)   
                            norms    byte[4]  (id=71)   
                            pointer    0   
                            pointerMax    3   
                            scoreCache    float[32]  (id=119)   
                            similarity    DefaultSimilarity  (id=48)   
                            termDocs    SegmentTermDocs  (id=120)   

                            //weight(contents:cat)
                           weight    TermQuery$TermWeight  (id=121)   
                            weightValue    0.3101425    
                    size    3   
                prohibitedScorers    ArrayList  (id=90)   
                requiredScorers    ArrayList  (id=91)   
                similarity    DefaultSimilarity  (id=48)    
        size    1   
    prohibitedScorers    ArrayList  (id=46)   
    requiredScorers    ArrayList  (id=47)   
        elementData    Object[10]  (id=59)   
            [0]    TermScorer  (id=66)    
                docs    int[32]  (id=68)   
                freqs    int[32]  (id=70)   
                norms    byte[4]  (id=71)   
                pointer    0   
                pointerMax    0   
                scoreCache    float[32]  (id=73)   
                similarity    DefaultSimilarity  (id=48)   
                termDocs    SegmentTermDocs  (id=76)   
                weight    TermQuery$TermWeight  (id=78)   //weight(contents:hello)
                weightValue    2.6944637    
        size    1   
    similarity    DefaultSimilarity  (id=48)