自己动手写分布式搜索引擎
上QQ阅读APP看书,第一时间看更新

3.3.12 遍历索引库

IndexReader提供了遍历索引库的接口。在遍历索引库之前可以先看一下索引库中文档的数量。

        Directory directory = getDir();  //得到路径对象
        IndexReader indexReader = DirectoryReader.open(directory); //取得IndexReader对象
        System.out.println(indexReader.maxDoc()); // 打印文档数量

可以根据文档编号遍历索引库中的每个文档。

        IndexReader reader = DirectoryReader.open(directory);
        int totalDocs = reader.numDocs(); //取得所有文档的数量
        for(int m=0; m<totalDocs; m++){
            Document thisDoc = reader.document(m); //取得索引库中的每个文档
        }

检查文档是否已经被删除了,可以调用MultiFields.getLiveDocs(reader)方法。

        Bits liveDocs = MultiFields.getLiveDocs(reader); //返回值可能为空
        for (int i=0; i<reader.maxDoc(); i++) {
            if (liveDocs ! = null && ! liveDocs.get(i))
              continue;
            Document doc = reader.document(i);
        }

文档编号是一个非负整数。在重新做索引后,文档的文档编号可能会改变。所以在重新打开IndexReader以后,原来的文档编号就失效了。

IndexSearcher也是通过IndexReader取得文档对象。

        public class IndexSearcher {
          final IndexReader reader;


          public Document doc(int docID) throws IOException {
            return reader.document(docID); //通过IndexReader取得文档对象
          }
        }

IndexSearcher.doc(int)方法只是为了方便从搜索结果中得到文档对象。

下面介绍与遍历索引库相关的几个类。TermEnum用来枚举一个给定的域中的所有项,而不管这个项在哪个文档中。例如:

        String indexDir = "D:/test/chatindex";
        FSDirectory directory = FSDirectory.open(new File(indexDir));
        DirectoryReader reader = DirectoryReader.open(directory);


        // 读取索引文件里所有的Term
        Terms terms = SlowCompositeReaderWrapper.wrap(reader).terms("field");


        TermsEnum  termsEnum = terms.iterator(null);


        BytesRef term;
        while ((term = termsEnum.next()) ! = null) {
            String s = new String(term.bytes, term.offset, term.length);
            System.out.println("词: "+s);
        }

TermDocs和TermEnum不同,TermDocs用来识别哪个文档包含指定的项,并且它也会给出该项在文档中的词频。

TermFreqVector(即Term Frequency Vector或者简称Term Vector)是一个数据结构,包含一个指定文档的项和词频信息,并且当在索引期间存储项向量的时候,才能通过IndexReader检索出TermFreqVector。

所谓Term Vector,就是对于文档的某一列,如title、body这种文本类型的列,建立词频的多维向量空间。一个词就是一维,该维的值就是这个词在这个列中的频率。

getTerms()和getTermFrequencies()是并列的数组。也就是说,getTerms()[i]有一个文档频率getTermFrequencies()[i]。

例如,源文本见表3-4。

表3-4 源文本

词排序后的位置见表3-5。

表3-5 排序后位置

用下面的代码发现the出现的位置:

        int index = termPositionVector.indexOf("the"); // 7
        int positions = termPositionVector.getTermPositions(index); // {0, 6}

这里使用TermEnum按词遍历索引库,代码如下:

        public static void getTerms(IndexReader reader) throws IOException {
            TermEnum terms = reader.terms(); // 读取索引文件里所有的Term


            while (terms.next()) {// 取出一个Term对象
                String field = terms.term().field(); //列名
                String text = terms.term().text(); //词
                System.out.println(text+":"+field);
            }
        }

经常需要统计索引库中哪些词出现的频率最高。例如,需要统计旅游活动索引库中的热门目的地。可以先对目的地列做索引,索引列不分词,然后取得该列中最常出现的几个词,也就是热门目的地。实现方法是:可以用TermEnum遍历索引库中所有的词,取出每个词的文档频率,然后使用优先队列找出频率最高的几个词。

        public class TermInfo {
          public Term term;   //索引库中的词
          public int docFreq; //文档频率,也就是这个词在多少个文档中出现过


          public TermInfo(Term t, int df) {
            this.term = t;
            this.docFreq = df;
          }
        }


        //找出频率最高的numTerms个词
        public static TermInfo[] getHighFreqTerms(IndexReader reader,
                                          int numTerms, String field){
              //实例化一个TermInfo的队列
              TermInfoQueue tiq = new TermInfoQueue(numTerms);
              TermEnum terms = reader.terms(); //读取索引文件里所有的term
              int minFreq = 0; //队列最后一个term的频率即当前最小频率值
              while (terms.next()) {//取出一个term对象
                  String field = terms.term().field();
                  if (fields ! = null && fields.length > 0) {
                      boolean skip = true; //跳过标识
                      for (int i = 0; i < fields.length; i++) {
                          //当前Field属于fields数组中的某一个则处理对应的term
                          if (field.equals(fields[i])) {
                            skip = false;
                            break;
                          }
                      }
                      if (skip) continue;
                  }
                  //当前term的内容是过滤词,则直接跳过
                  if (junkWords ! = null && junkWords.get(terms.term().text()) ! =
        null) continue;


                  /*获取最高频率的term。基本方法是:
                  队列底层是最大频率term,顶层是最小频率term,
                  当插入一个元素后超出初始化队列大小则取出最上面的那个元素,
                  重新设置最小频率值minFreq*/
                  if (terms.docFreq() > minFreq) {//当前Term的频率大于最小频率则插入队列
                      tiq.insertWithOverflow(new TermInfo(terms.term(), terms.docFreq()));
                      if (tiq.size() >= numTerms) { //当队列中的元素个数大于numTerms
                          tiq.pop();  // 取出最小频率的元素,即最上面的一个元素
                          minFreq = ((TermInfo)tiq.top()).docFreq; //重新设置最小频率
                      }
                  }
              }
              //取出队列元素,最终存放在数组中元素的词频率按从大到小排列
              TermInfo[] res = new TermInfo[tiq.size()];
              for (int i = 0; i < res.length; i++) {
                  res[res.length - i -1] = (TermInfo)tiq.pop();
              }
              return res;
        }

四维枚举API指通过列、文档、词、位置四个维度来遍历索引库。