Lucene将索引文档的过程设计成两个阶段,写入内存阶段和写入硬盘阶段。在写入内存阶段,Lucene通过IndexChain把document分解并把相关信息存储到内存中,等到满足flush条件(内存容量或者文档个数积累到临界值),就通过IndexChain把内存中的数据写入硬盘。IndexChain是Lucene索引文档很重要的一部分,那么IndexChain是什么呢?
Lucene的IndexChain Lucene形成索引的过程其实就是对document进行分解的过程。通过对document的分解,得到词典、倒排表等信息。IndexChain就是分解document的对象集合,或者说架构。索引链的结构如下图所示:
上图中IndexChain的起点是DocFieldProcessor,它会分别调用DocInverter(倒排信息处理)和TowStoredFieldsConsumer(正向信息处理)。 反向信息有四种:
信息种类 | 作用 | 处理组件 |
norm信息 | 用来消除长文本和短文本之间的差距 | NormsConsumer |
Freq信息 | 文档排序时的重要因子 | FreqProxTermsWriter |
Pos信息 | 位置信息,在PhraseQuery时会有用 | FreqProxTermsWriter |
TermVector | 高亮处理需要记录的信息 | TermVectorsConsumer |
正向信息有两种:
信息种类 | 作用 | 处理组件 |
Fields | 形成完整的一个doc | StoredFieldsProcessor |
docValues | 排序因子 | DocValuesProcessor |
对照两个表格,再回头看IndexChain,各个类的作用就很清晰了。
索引链被调用的过程如下图所示:
这种设计导致IndexChain只是一个骨架,实际上起分解Document作用的组件如下图所示:
跟上面的IndexChain相比,大多都是在类名后面加了后缀PerField,整个结构都是一样的。由于TwoStoredFieldsConsumers是存储Field的内容,并不对其进行分解,所以就不需要PerField了.
Lucene的IndexWriter是线程安全的,即它支持多线程索引。默认会生成8个DocumentsWriterPerThread,每个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();
}
}
上面这段代码有两个用户索引线程。这段代码执行,最后生成了索引结构是不确定的.有时会有两个索引段,如下:
但有时也会只有一个索引段,如下:
这是因为每个索引线程(dwpt)其实是从DocumentsWriterPerThreadPool里面获得空闲的DocumentsWriterPerThread对象。如果一个DocumentsWriterPerThread对象已经足够应付两个索引线程的差遣,就无需新的DocumentsWriterPerThread对象了。就像餐厅里客人用餐一样。如果一个服务员能够应付下来,为什么再去招募多的服务员增加成本呢?
Lucene在多线程索引时会充分利用DocumentsWriterPerThreadPool里面的DocumentsWriterPerThread对象.只要该对象对应的线程锁被释放,就会被其它的线程竞争.我们可以从ThreadAffinityDocumentsWriterThreadPool.getAndLock()方法了解其实现机制.由于多线程竞争的不确定性,导致了索引段个数的不确定性。这种设计方式也降低了多线程程序的复杂性,很值得深入学习。
IndexChain属于Lucene索引过程的脉络和骨架,其核心点在于多线程的处理方式。但是由于索引中多线程并不常用,而且也不好调试,所以理解起来比较困难。另加上整个索引链组件众多,而且各个类的成员变量都以consumer命名,如果不画图而只是跟踪debug,很容易被consumer弄得晕头转向。
了解了IndexChain,实际上只是了解了Lucene索引的框架。并没有了解到索引的细节,比如内存管理,数据存储方式。
本文出自 “每天进步一点点” 博客,请务必保留此出处http://sbp810050504.blog.51cto.com/2799422/1440510
Lucene的索引链结构_IndexChain