Lucene将索引文档的过程设计成两个阶段,写入内存阶段和写入硬盘阶段。在写入内存阶段,Lucene通过IndexChaindocument分解并把相关信息存储到内存中,等到满足flush条件(内存容量或者文档个数积累到临界值),就通过IndexChain把内存中的数据写入硬盘。IndexChainLucene索引文档很重要的一部分,那么IndexChain是什么呢?

       LuceneIndexChain

         Lucene形成索引的过程其实就是对document进行分解的过程。通过对document的分解,得到词典、倒排表等信息。IndexChain就是分解document的对象集合,或者说架构。索引链的结构如下图所示:

wKiom1PLaHHxt6o6AAGMjIvcCfs482.jpg

上图中IndexChain的起点是DocFieldProcessor,它会分别调用DocInverter(倒排信息处理)TowStoredFieldsConsumer(正向信息处理) 反向信息有四种:

信息种类

作用

处理组件

norm信息

用来消除长文本和短文本之间的差距

NormsConsumer

Freq信息

文档排序时的重要因子

FreqProxTermsWriter

Pos信息

位置信息,在PhraseQuery时会有用

FreqProxTermsWriter

TermVector

高亮处理需要记录的信息

TermVectorsConsumer

正向信息有两种:

信息种类

作用

处理组件

Fields

形成完整的一个doc

StoredFieldsProcessor

docValues

排序因子

DocValuesProcessor

对照两个表格,再回头看IndexChain,各个类的作用就很清晰了。

 

索引链被调用的过程如下图所示:

wKioL1PLaaaRuVl2AANpNw8esfE719.jpg

这种设计导致IndexChain只是一个骨架,实际上起分解Document作用的组件如下图所示:

wKioL1PLabSxjQrZAAF3whET2RE492.jpg

         跟上面的IndexChain相比,大多都是在类名后面加了后缀PerField,整个结构都是一样的。由于TwoStoredFieldsConsumers是存储Field的内容,并不对其进行分解,所以就不需要PerField.

LuceneIndexWriter是线程安全的,即它支持多线程索引。默认会生成8DocumentsWriterPerThread,每个DocumentsWriterPerThread都拥有一个IndexChain,每个IndexChain都有一个独立的索引内存空间。这使得IndexChain的这种模式在多线程索引时,各个IndexChain是互不干扰的,因而效率会很高。但是这并不意味着每一个用户线程都会对应一个IndexChain,生成一个独立的索引段。比如:

         public class LuceneDemo{

  

   static class IndexThread implements Runnable{

      IndexWriter iw ;

      String[] vals ;

      int start ;

      public IndexThread(IndexWriter iw,String[] vals,int start){

         this.iw = iw;

         this.vals = vals;

         this.start = start;

      }

      @Override

      public void run() {

            for(int i=start;i<vals.length;i+=2){

                Document doc =new Document();

                doc.add(new TextField("title",vals[i],Store.YES));

                try {

                   iw.addDocument(doc);

                }catch (Exception e) {}

            }

        

      }

   }

   public static voidmain(String[] args) throws IOException, InterruptedException{

      File file = new File("d:/tmp/index");

      Directory dir = FSDirectory.open(file);

      IndexWriterConfig conf = new IndexWriterConfig(Version.LUCENE_42,new WhitespaceAnalyzer(Version.LUCENE_42));

      IndexWriter iw = new IndexWriter(dir, conf);

      final String[] vals = {"common","term","new","term","term",

                   "term","common","term","common"};

 

      Thread it1 = new Thread(new IndexThread(iw, vals, 0));

      Thread it2 = new Thread(new IndexThread(iw, vals, 1));

     

      it1.start();it2.start();

      it1.join();it2.join();

     

      iw.commit();

      iw.close();

   }

}     

上面这段代码有两个用户索引线程。这段代码执行,最后生成了索引结构是不确定的.有时会有两个索引段,如下:

wKioL1PLacSAfS7KAAK0zqKJwm8783.jpg

但有时也会只有一个索引段,如下:

wKiom1PLaLTwjVrrAAFyZwqwBZE150.jpg

这是因为每个索引线程(dwpt)其实是从DocumentsWriterPerThreadPool里面获得空闲的DocumentsWriterPerThread对象。如果一个DocumentsWriterPerThread对象已经足够应付两个索引线程的差遣,就无需新的DocumentsWriterPerThread对象了。就像餐厅里客人用餐一样。如果一个服务员能够应付下来,为什么再去招募多的服务员增加成本呢?

Lucene在多线程索引时会充分利用DocumentsWriterPerThreadPool里面的DocumentsWriterPerThread对象.只要该对象对应的线程锁被释放,就会被其它的线程竞争.我们可以从ThreadAffinityDocumentsWriterThreadPool.getAndLock()方法了解其实现机制.由于多线程竞争的不确定性,导致了索引段个数的不确定性。这种设计方式也降低了多线程程序的复杂性,很值得深入学习。

         IndexChain属于Lucene索引过程的脉络和骨架,其核心点在于多线程的处理方式。但是由于索引中多线程并不常用,而且也不好调试,所以理解起来比较困难。另加上整个索引链组件众多,而且各个类的成员变量都以consumer命名,如果不画图而只是跟踪debug,很容易被consumer弄得晕头转向。

         了解了IndexChain,实际上只是了解了Lucene索引的框架。并没有了解到索引的细节,比如内存管理,数据存储方式。