使用Lucene的API遍历Lucene索引

一般使用Lucene的人都很少需要对索引进行遍历之类的操作,因为使用Lucene一般都不会对其索引文件产生太大兴趣,只注重将Lucene作为一个全文检索工具来使用而已,并不在意其内部实现和结构。但是很多学习Lucene的朋友都希望可以看见完整的Lucene索引内容,至少包含索引词、索引词出现的文档、索引词在文档中的位置(这里指的位置并不是词在原文中的位置,而是指其在Lucene对文档进行过滤后得到的新文档的位置)等信息。前几个月笔者就因为在实验室里的一个实验性的项目做了一些需要遍历Lucene索引的工作。

事实上,如果我们需要观察Lucene索引的内容,我们完全可以使用Luke,但是我们知道Luke所提供的信息并不是总能满足我们的需要,而且很多人都认为Luke的功能十分强大,但是实际上我们自己完全可以自己开发一个类似Luke的工具。只要你对Java界面编程比较熟悉的话(这通常是比较难的),那么仅仅需要知道一些本文即将阐述的几个Lucene API就可以了。

这里我们遍历索引的思路是,首先得到索引词,然后根据索引词得到关于这个索引词的相关信息(主要就是根据倒排文件的结构遍历)。第一步就是得到索引词的枚举器(enumeration),在Lucene里为我们提供了TermEnum类,该类位于org.apache.lucene.index包下,它的声明为

public abstract class TermEnum 
extends Object 
根据官方的API说明,该类是一个用于枚举索引词的抽象类。索引词枚举器总是按照Term.compareTo()进行排序。索引词枚举器中的任意一个词都比它之前的词要大。 
它只有一个无参构造方法。除了继承自Object类的方法外,它主要有以下几个方法:

abstract void
close()
          关闭枚举器,释放资源。

abstract int
docFreq()
          返回当前索引词的文档频率。

abstract boolean
next()
          枚举器向后移动一个位置。

boolean
skipTo(Term target)
          使枚举器向后移动,直到移动到某个大于等于(这里的比较概念是由Term.compareTo()定义的)target的词为止。

abstract Term
term()
          返回当前枚举器所枚举的词。

当我们看到TermEnum是一个抽象类的时候,我们也许会很无奈的想,我们必须要找到合适的并且已经继承了该类的非抽象类,然后还不得不对着它的文档再研读一番。你这么想是完全正确的,但是事实上我们完全没有必要这样做,因为Lucene的IndexReader类实际上为我们提供了一个很实用的方法

abstract TermEnum
terms()
          返回一个关于当前索引中所有索引词的一个枚举器。

您也许觉得我玩你,因为该方法也是一个抽象方法,因此IndexReader本身也是一个抽象方法!难道我们还需要找到一个继承该类的非抽象类么?当然不需要。我们有IndexSearcher类!而且令人振奋的是,该类终于不是抽象的啦!它含有一个我们神往的方法:

IndexReader
getIndexReader()
          返回该搜索对应的索引的索引阅读器。

但是您很可能又提出疑问了,IndexReader不是一个抽象类么,怎么能够返回一个抽象对象呢?是的,IndexReader的确是一个抽象方法,但是我们完全有理由相信该方法返回的实际上是一个继承自IndexReader的非抽象类。Lucene此处使用的是Java的多态,至于返回的到底是IndexReader的哪一个子类我们大可不必细究,交给JVM就好了。因此,我们就可以使用前面的所有的那些抽象方法(注意,当我们使用这些方法的时候,它们不再是抽象方法)了。

因此,得到一个索引的索引词就可以使用下面这段代码:

        IndexSearcher searcher = new IndexSearcher(IndexPath);
        IndexReader reader = searcher.getIndexReader();

        TermEnum enumeration = reader.terms();

        while(enumeration.next()){

//invoke the other methods in TermEnum

}

如果您是一个细心的读者,您可能会问到:enumeration.next()不是会枚举出下一个词么,那么上面那段代码不就会直接跳过第一个索引词么?是的,如果您这么想,那说明您考虑的很细致,但是我可以告诉您,上面的代码完全没有问题。因为一开始TermEnum枚举的并不是第一个索引词而是一个空对象,因此在我们使用TermEnum的其他方法之前应当首先调用next()方法。

现在我们能够得到所有的索引词了,那么怎么根据这些索引词得到其他信息(出现的文章、位置等)呢?事实上,原理完全和上面的方法差不多,只是使用的方法不同而已。

如果刚才我们仔细阅读Lucene关于IndexReader的API文档的话,那么我们可以发现一个方法:

TermPositions
termPositions(Term term)
          返回一个包含term的所有文档的枚举器。

现在我们就来看看TermPositions。我们可以发现,TermPositions并不是一个类,而是一个接口,而且该接口是继承自TermDocs接口的。现在我们暂且不看TermDocs,先来了解一下TermPositions接口,该接口的API说明文档是这样阐述的:

public interface TermPositions 
extends TermDocs
TermPositions 提供枚举一个词的<document, frequency, <position>* >三元组的接口 。

其中,document 和 frequency 的含义与 TermDocs中的相同。 而position部分则顺序列出了一个词在一个文档中的每一个出现位置。

该接口含有一个方法:

int
nextPosition()
          返回在当前文档中的下一个出现位置。

使用该方法我们就可以自如地遍历上面三元组的position部分了,也就是说我们可以得到一个词在一个文档中的所有出现位置了!

但是您可能觉得这点信息实在是少得可怜。别着急,前面说过TermPositions接口是继承自TermDocs接口的(真是惊讶于Lucene的体系架构,你完全可以把Lucene的设计作为一个设计模式的范例去学习),那么TermDocs接口应该为我们设计了更多的实用方法。事实确实如此!

我们完全没有必要去全面的了解TermDocs接口,我们现在所需要知道的就是TermPositions接口究竟从TermDocs接口继承了哪些方法。从TermPositions的API文档处就可以轻易地发现它继承了如下方法:close, doc, freq, next, read, seek, seek, skipTo。这些方法几乎都是自解释的,这里就不再赘述每一种方法了,感兴趣的读者可以自行参阅Lucene的API说明文档。有了这些方法,我们就可以完成我们对Lucene索引文件的遍历了。这里我需要强调一下,虽然我们没有实现任何实现了上面接口的类,但是我们在调用reader.termPositions(Term term)方法时实际上Lucene给我们返回了一个实现了TermPositions接口的类的实例(如果您对这点仍然不甚了然的话,请您再去翻翻您的Java教程)。

利用下面这段代码,我们可以对于一个给定的Lucene索引打印出<term, document, frequency, <position>* >四元组。

        IndexSearcher searcher = new IndexSearcher(IndexPath);//根据指定的路径构造一个搜索器
        IndexReader reader = searcher.getIndexReader();//得到搜索器的索引阅读器
        
        TermEnum enumeration = reader.terms();//得到索引的索引词表

        while(enumeration.next())//遍历索引此表
        {
            if(enumeration.term().field().equals("content"))//我们仅处理所在域域名为content的索引词
            {
                //out是一个输出流,它输出到一个文本,这里没有给出out的定义,读者可以自己定义它
                out.write(enumeration.term().text() + "\n");

                TermPositions posEnum = reader.termPositions(new Term("content",enumeration.term().text()));
                StringBuffer sb = new StringBuffer(65536);
                while(posEnum.next())
                {
                     sb.append(reader.document(posEnum.doc()).getField("DOCNO").stringValue());//DOCNO是笔者所使用语料的文档的标号,对应一般使用者的"filename"域
                     sb.append(":");
                     sb.append(posEnum.freq());
                     sb.append(" ");
                     for( int i = 0; i < posEnum.freq(); i++)
                         sb.append("["+posEnum.nextPosition()+"]");
                     sb.append(";");
                 }
                 out.write(sb.toString()+"\n");

         }

out.close();

searcher.close();

这样,我们就完成了一个简单的索引遍历的操作。打印出的结果的一个局部视图如下:

modifyits
AP890915-0286 :1 [317]; AP890918-0217 :1 [368]; AP891215-0011 :1 [245];
modifyrecipes
AP890830-0142 :1 [332];
modifyself
AP890914-0048 :2 [83] [126];
modifythe
AP890814-0212 :1 [133]; AP890923-0115 :1 [58];

以"modifyself"来说,它出现在文档编号为AP890914-0048的文档中,在该文档中出现2次,位置分别是83和126。

当然,你可以使用更多的方法来打印出更多的信息。

好了,至此我们已经把基本的遍历Lucene索引的API及其使用介绍完了,你是不是觉得Luke实际上也没有很神秘呢?你完全有能力自己写一个Lucene索引查看器。

P.S. 本文完全是笔者自己从在使用经验中总结出来的,由于笔者自己也是刚接触Lucene,因此理解难免有偏颇之处,希望大家指正。同时笔者所使用的Lucene版本为2.0.0版,使用的API文档也是针对本版本的英文帮助(文中关于API的官方说明系笔者根据英文版翻译而来,若有错漏之处尽请指正)。