`
wbj0110
  • 浏览: 1610670 次
  • 性别: Icon_minigender_1
  • 来自: 上海
文章分类
社区版块
存档分类
最新评论

lucene4.5源码分析系列:索引的创建过程

阅读更多

在开始之前,先对IndexWriter的所需要考虑的问题有个大致的了解。IndexWriter主要负责索引数据写入,所以,增删改是主要需要考虑的问题;其次,由于写入的过程涉及成功失败的问题,所以,事务以及二阶段事务也是应该考虑到的;再来就是需要写入过程有最大的吞吐量,所以,还存在缓存以及刷新的问题;另外,如何让多线程高效并发访问索引也是要思考的;还有,通常查找内容的数量都非常庞大,如何管理索引过程中的内存以及磁盘消耗也是值得考虑的事情。接下来的文章,会对这些问题分别分析。

 

  索引的增删改

  增加,下面的api主要是加入一个或多个文档,以及对这篇文档使用的analyzer。实际上,增加文档在背后是调用了更新这一操作。它所提供的接口无非是单文档和多文档,有分析器和无分析器的区别。

 

[java] view plaincopy
 
  1. public void addDocument(IndexDocument doc) throws IOException  
  2. public void addDocument(IndexDocument doc, Analyzer analyzer) throws IOException  
  3. public void addDocuments(Iterable<? extends IndexDocument> docs) throws IOException  
  4. public void addDocuments(Iterable<? extends IndexDocument> docs, Analyzer analyzer) throws IOException  

  更新,需要明白的是,更新并非像数据库一样,而是一个先删除后增加的过程

 

[java] view plaincopy
 
  1. public void updateDocuments(Term delTerm, Iterable<? extends IndexDocument> docs) throws IOException  
  2. public void updateDocuments(Term delTerm, Iterable<? extends IndexDocument> docs, Analyzer analyzer) throws IOException  
  3. public void updateDocument(Term term, IndexDocument doc) throws IOException  
  4. public void updateDocument(Term term, IndexDocument doc, Analyzer analyzer)  
  5.       throws IOException  

  删除,第一种方式是删除所有包含这个词的文档,第二种方式是删除所有包含这几个词的文档,第三种方式是删除所有符合该查询的文档,第四种方式是删除所有符合这组查询的文档,第五种方式是直接删除所有文档

[java] view plaincopy
 
  1. public void deleteDocuments(Term term) throws IOException  
  2. public void deleteDocuments(Term... terms) throws IOException  
  3. public void deleteDocuments(Query query) throws IOException  
  4. public void deleteDocuments(Query... queries) throws IOException  
  5. public void deleteAll() throws IOException  

  在更深入的讨论前先理下索引的链式结构,因为这个神奇的结构经常把人搞得云里雾里。默认的索引链在DocumentsWriterPerThread中有初始化。代码如下:

 

[java] view plaincopy
 
  1. static final IndexingChain defaultIndexingChain = new IndexingChain() {  
  2.   
  3.     @Override  
  4.     DocConsumer getChain(DocumentsWriterPerThread documentsWriterPerThread) {  
  5.       /* 
  6.       This is the current indexing chain: 
  7.  
  8.       DocConsumer / DocConsumerPerThread 
  9.         --> code: DocFieldProcessor 
  10.           --> DocFieldConsumer / DocFieldConsumerPerField 
  11.             --> code: DocFieldConsumers / DocFieldConsumersPerField 
  12.               --> code: DocInverter / DocInverterPerField 
  13.                 --> InvertedDocConsumer / InvertedDocConsumerPerField 
  14.                   --> code: TermsHash / TermsHashPerField 
  15.                     --> TermsHashConsumer / TermsHashConsumerPerField 
  16.                       --> code: FreqProxTermsWriter / FreqProxTermsWriterPerField 
  17.                       --> code: TermVectorsTermsWriter / TermVectorsTermsWriterPerField 
  18.                 --> InvertedDocEndConsumer / InvertedDocConsumerPerField 
  19.                   --> code: NormsConsumer / NormsConsumerPerField 
  20.           --> StoredFieldsConsumer 
  21.             --> TwoStoredFieldConsumers 
  22.               -> code: StoredFieldsProcessor 
  23.               -> code: DocValuesProcessor 
  24.     */  
  25.   
  26.     // Build up indexing chain:  
  27.   
  28.       final TermsHashConsumer termVectorsWriter = new TermVectorsConsumer(documentsWriterPerThread);  
  29.       final TermsHashConsumer freqProxWriter = new FreqProxTermsWriter();  
  30.   
  31.       final InvertedDocConsumer termsHash = new TermsHash(documentsWriterPerThread, freqProxWriter, true,  
  32.                                                           new TermsHash(documentsWriterPerThread, termVectorsWriter, falsenull));  
  33.       final NormsConsumer normsWriter = new NormsConsumer();  
  34.       final DocInverter docInverter = new DocInverter(documentsWriterPerThread.docState, termsHash, normsWriter);  
  35.       final StoredFieldsConsumer storedFields = new TwoStoredFieldsConsumers(  
  36.                                                       new StoredFieldsProcessor(documentsWriterPerThread),  
  37.                                                       new DocValuesProcessor(documentsWriterPerThread.bytesUsed));  
  38.       return new DocFieldProcessor(documentsWriterPerThread, docInverter, storedFields);  
  39.     }  
  40.   };  

 

  之所以说它是一个索引链,因为它把整个请求层层向下链状传递(注意这不是真正的责任链模式,虽然非常像,因为它们并不是一个统一的接口)。最上层的领导发话要做什么,都经过层层传递,每层分解任务做一小部分,然后把剩余的向下传递,直到最下层最终执行完任务。而且这个责任链比较整齐的一点是,从触发点DocConsumer开始,都hold住两个对象(他们都是consumer),并且先后被调用。最下层的TeamVectorsWriter实际上已经是解码器的范畴了。lucene4比起lucene3做了一些改进,取缔了*PerThread的一堆类和结构,我猜测lucene3是一个更细粒度的线程同步,对下面的每个小组件都做了同步的选择(即PerThread),但是实际上发现,最后一般都使用粗粒度的同步,就是DocumentsWriterPerThread,从这里向下的方法都是同步的结果。

 

  下图是PerField的索引链,add和update实际上就是通过这条链向下传递的。

  

  这里用我的文章作为例子来讲解lucene是如何将这样的文档变为倒排索引结构的,假设有如下1篇文章,简化考虑,他们只有3个field:title和content和author

  

  主要的执行过程在DocFieldProcessor的processDocument中,有这么几个主要步骤(只是为了说明主体,实际上还有很多细节):

  

  可以看到这几个主要的组件的组织非常有逻辑,基本上保持着朴素的分治思想,他们的主要作用如下:

  1. DocFieldProcessor: 主要是处理遇到的单个文档,也就是说每碰到一个文档就会有一个DocFieldProcessor来进行相应的处理。

  2. DocFieldProcessorPerField: 主要是将文档按field来归类分好,以便将任务分解,经过DocFieldProcessorPerField,可以发现整个文档已经被归类为了许多fields,接下来可以分别对每个field的进行处理,如下图:

  3. DocInverterPerField: 主要是对每个field中的fieldsData做语汇单元的分析处理,经过这步以后,每次处理的就只是一个term

  4. TermHashPerField: 记录将上一步的term映射到缓冲区中后的各种信息,它会生成部分的postingsArray以及填充term的文本缓冲区。TermHashPerField中最重要的结构就是postingsArray,可以把它理解为二维数组,如图:termID其实就是获取每条记录的key,每条记录其实就是记录每个词的一些元信息。其中最重要的是textStarts,byteStarts,intStarts以及termFreqs,termFreqs很好理解,其实就是记录了词频,比如“的”字出现了3次,因此termFreqs为3.

  那么textStarts,byteStarts,intStarts分别是什么呢?

  在lucene3中,有三个重要的结构作为索引在内存的缓冲,它们分别是IntBlockPool, ByteBlockPool和TextBlockPool,其中IntBlockPool存在的目的是指示docid(文档号),freq(词频)和prox(位置)在ByteBlockPool中的位置,而byteBlockPool专门存放docId,freq,prox,因为存放的字节可能不等,所以需要IntBlockPool的位置指示;而TextBlockPool则存放真正的term文本。而到了lucene4,这个结构有了稍微的改变,TextBlockPool合并到了ByteBlockPool。下面,我们对这两个重要的缓冲区和postingArray的关系展开说明,事实上,只需要了解这两个缓冲区的数据结构,便能清楚的知道这个整体的结构。

  从之前的postingsArray来看,我们有textStarts,它主要作用是指示term文本内容的位置,这样,每个term都能根据textStarts迅速定位到term文本;从图中可以看出来,对应“的”的textStarts指示的位置就能够定位到ByteBlockPool中第116个字节,这里首先指示了接下来三个字节是我们的文本,于是,从117到119就可以确定是文本了。接下来byteStarts指示的是docid,freq和prox开始的位置,即ByteBlockPool的第120个位置;IntBlockPool紧邻的两个数值分别指示docid,freq与prox,intStarts指示13,于是位于IntBlockPool的第13个字节120就是docid,freq在ByteBlockPool中的位置,位于128的就是prox的下一个待写入位置。如此一来,我们就完成了从postingsArray到Pool的映射。

  当然,这里远远没有了解所有问题。比如docid,freq和prox为什么都以16结尾,他们每块为什么都是5个字节?如果同一个term重复了多次,docid和prox会有多份,应当如何记录?为什么在这个例子中ByteBlockPool第一个term只有docid,freq而没有prox?

  实际上,这部分问题是比较复杂的。这里尝试用一个简单的例子讲解。这次,我们向同一个文档中加了6个同样的词,如下

  整个结构会变为

  首先解释为什么每块5个字节,结束位置是16. 请看ByteBlockPool代码,每次空间不够时,会按照固定好的数组来分配空间。每次分配时按照层级,一共有9层,NEXT_LEVEL_ARRAY规定了这点,每层的byte数量在LEVEL_SIZE_ARRAY中规定,比如刚开始是第一层,会分配好5个bytes,(byte) (16|newLevel)便定义了第1层的结束标志为16,第2层(实际上newLevel为1)为17,以此类推……而这个结束标志16不仅表明该块结束,而且还能通过该数字反推出目前的块到底是第几层。

 

[java] view plaincopy
 
  1. public final static int[] NEXT_LEVEL_ARRAY = {1234567899};  
  2. public final static int[] LEVEL_SIZE_ARRAY = {514203040408080120200};  
  3. public int allocSlice(final byte[] slice, final int upto) {  
  4.   
  5.     final int level = slice[upto] & 15;  
  6.     final int newLevel = NEXT_LEVEL_ARRAY[level];  
  7.     final int newSize = LEVEL_SIZE_ARRAY[newLevel];  
  8.   
  9.     // Maybe allocate another block  
  10.     if (byteUpto > BYTE_BLOCK_SIZE-newSize) {  
  11.       nextBuffer();  
  12.     }  
  13.   
  14.     final int newUpto = byteUpto;  
  15.     final int offset = newUpto + byteOffset;  
  16.     byteUpto += newSize;  
  17.   
  18.     // Copy forward the past 3 bytes (which we are about  
  19.     // to overwrite with the forwarding address):  
  20.     buffer[newUpto] = slice[upto-3];  
  21.     buffer[newUpto+1] = slice[upto-2];  
  22.     buffer[newUpto+2] = slice[upto-1];  
  23.   
  24.     // Write forwarding address at end of last slice:  
  25.     slice[upto-3] = (byte) (offset >>> 24);  
  26.     slice[upto-2] = (byte) (offset >>> 16);  
  27.     slice[upto-1] = (byte) (offset >>> 8);  
  28.     slice[upto] = (byte) offset;  
  29.           
  30.     // Write new level:  
  31.     buffer[byteUpto-1] = (byte) (16|newLevel);  
  32.   
  33.     return newUpto+3;  
  34.   }  

  其次应当注意两个原则:差值原则和变长原则,请参见索引的文件格式一文。

 

  接下来,基本上与上面一样,不同的是prox需要记录很多词的位置,是如何做呢,第一个term“索引”到来会在12处记录位置为0,第二个“索引”会在13处记录位置为4(增量),第三个会在14处记录4,第四个会在15处记录4,而第5个会发现已经没有空间了(16不能被占用),于是分配第二层块,插入到ByteBlockPool后面。查LEVEL_SIZE_ARRAY得知,块大小为14,块的结束符为17(例子中17-30就是第二层块),于是将16前面的3个字节向后移动到第二层块的起始位置,连同16共4个字节用于指向下一层的指针,指示下一个块的地址,在本例中将13-15中的4移动到17-19,将16指向了17,再将第5个4放到20,第6个4放到21。这个动态的过程如下,左边序号代表到来的term“索引”:

  为什么需要4个字节的指针?本例中不是一个字节就搞定了吗?当然这是为了支持更大的Pool。那又为什么需要指针呢,从上面来看16就指向了17,完全没有必要,但是这只是一个特例,lucene采用的方法是非常高效的,为了避免在待扩展块后面直接插入扩展块(即在有序数组的中间来扩展),lucene会将扩展块放到ByteBlockPool的最后,这样一来,省去了每次扩展时对数组的一个大规模移动,只需要用指针来换取这种移动。

  关于doc和freq也是类似的,这里就不再赘述。

  最后,为什么在我们最初的例子中只有docid而没有prox,事实上,这与选择的Field有关系,对于author这个field,我选择了StringField,它的IndexOptions为DOCS_ONLY,意味着我们将只会记录docid,忽略了freq和prox

  5. FreqProxTermsWriterPerField: 将最终分析的prox, offset以及lastDocIDs, lastDocCodes等信息记录下来。其实这部所做的事情已经在上面分析过了。

  接下来的问题是,当添加或者更新完毕之后,这些数据会立即写入磁盘吗?答案是不会。lucene会在flush的时候将缓冲区的数据写入磁盘。FreqProxTermWriterPerField中的flush方法就负责这件事。关于缓存及刷新,请参考后续文章

 

  参考文档:Lucene索引过程分析(2)  http://www.cnblogs.com/forfuture1978/archive/2010/02/02/1661440.html

                      Lucene索引过程分析(3)  http://www.cnblogs.com/forfuture1978/archive/2010/02/02/1661441.html

                      Annotated lucene

http://blog.csdn.net/liweisnake/article/details/11364597

 
分享到:
评论

相关推荐

    lucene3源码分析

    #### 四、Lucene索引过程分析 Lucene的索引过程是一个复杂而有序的操作流程,主要步骤如下: - **1. 创建IndexWriter对象**:初始化索引写入器。 - **2. 创建文档Document对象,并加入域(Field)**:定义文档结构和...

    Lucene 3.0 原理与代码分析PDF

    Lucene学习总结之四:Lucene索引过程分析(1) Lucene学习总结之四:Lucene索引过程分析(2) Lucene学习总结之四:Lucene索引过程分析(3) Lucene学习总结之四:Lucene索引过程分析(4) www.chinaandroid.com

    IK分词器集成lucene4.5使用方法

    2. **配置Analyzer**:在Lucene的索引创建和搜索过程中,需要定义Analyzer。对于IKAnalyzer,可以这样配置: ```java import org.apache.lucene.analysis.cn.ik.IKAnalyzer; Analyzer analyzer = new IKAnalyzer();...

    SSI集成lucene4.5使用案例

    索引过程则涉及将这些文档转换为倒排索引,这是一种优化查询速度的数据结构;最后,查询解析器接收用户输入的查询,将其转化为可以匹配索引的形式。 在集成Lucene到SSI的过程中,我们需要考虑以下关键步骤: 1. **...

    Lucene学习源码.rar

    索引过程涉及分词(Tokenization)、词干提取(Stemming)、同义词扩展(Synonym Expansion)等步骤,将文本转换为可搜索的结构。 4. 分词器(Analyzer):负责将输入文本分解成一系列独立的词语,这是构建索引的...

    Lucene3.0创建索引

    本篇文章将详细介绍如何使用Lucene3.0来创建索引,并通过一个具体的例子来演示整个过程。 #### 一、Lucene3.0简介 Lucene是一款高性能、全功能的全文搜索引擎库。它为开发者提供了构建搜索应用所需的所有基本工具...

    lucene索引优化多线程多目录创建索引

    在多线程索引过程中,还应关注以下几点优化策略: - **硬件资源**:充分利用CPU和磁盘I/O资源,根据服务器配置合理设置线程数量。 - **内存管理**:监控内存使用情况,避免因内存不足导致的性能下降或崩溃。 - **...

    Lucene创建索引步骤

    Lucene创建索引步骤: 1、创建Directory(索引位置) 2、创建IndexWrite(写入索引) 3、创建Document对象 4、为Document添加Field(相当于添加属性:类似于表与字段的关系) 5、通过IndexWriter添加文档到索引中

    Lucene 索引的简单使用

    本篇文章将详细阐述如何使用Lucene来创建和查询索引,帮助你深入理解其核心概念和操作流程。 ### 1. Lucene基本概念 - **文档(Document)**:在Lucene中,一个文档代表你要索引的信息单元,它可以包含多个字段...

    Lucene5学习之创建索引入门示例

    索引过程将文本转换为可搜索的数据结构,而搜索则通过这个索引来快速找到相关文档。在这个过程中,我们通常会涉及到以下关键组件: 1. **Analyzer**: 分析器负责将输入的文本拆分成可搜索的词项(tokens)。这包括...

    lucene 对 xml建立索引

    本文将详细介绍如何利用Lucene对XML文档进行索引建立的过程,并通过示例代码具体阐述其实现方法。 #### 二、基础知识 1. **Lucene简介** - Lucene是一个开源的全文搜索引擎库,能够帮助开发者构建应用程序内的搜索...

    lucene-core-7.7.0-API文档-中文版.zip

    赠送jar包:lucene-core-7.7.0.jar; 赠送原API文档:lucene-core-7.7.0-javadoc.jar; 赠送源代码:lucene-core-7.7.0-sources.jar; 赠送Maven依赖信息文件:lucene-core-7.7.0.pom; 包含翻译后的API文档:lucene...

    Lucene3.5源码jar包

    10. **性能调优**:通过分析源码,开发者可以了解到如何调整各种参数,如缓存大小、合并策略等,来优化Lucene的性能。 总的来说,深入学习Lucene 3.5.0的源码,可以帮助开发者掌握全文检索的核心技术,了解其内部...

    Lucene对本地文件多目录创建索引

    在这个过程中,Lucene会分析文档内容,建立倒排索引,以便快速查找包含特定关键词的文件。 描述中提到的博客链接可能提供了具体的实现步骤或示例代码,但具体内容未给出,所以我们将基于Lucene的一般用法进行讲解。...

    lucene.net 2.9.1 源码

    《深入剖析Lucene.NET 2.9.1:源码解析与应用开发》 Lucene.NET 2.9.1是开源搜索引擎库Lucene的.NET版本,它为.NET开发者提供了强大的全文检索和索引功能。这个版本的源码提供了一个宝贵的资源,帮助我们理解其内部...

    Lucene 2.4.1源码分析

    《Lucene 2.4.1 源码分析——Analyzer深入解析》 在Lucene这个强大的全文搜索引擎库中,Analyzer扮演着至关重要的角色。它负责将输入的文本进行分词,过滤,标准化等预处理操作,使得搜索引擎能够正确理解和索引...

    lucene5.0源码包

    - **分析器(Analyzer)**:分析器负责将用户输入的文本进行分词,形成可以被索引的术语。5.0版本提供了更多可定制的分析器,如语言敏感分析器,满足不同场景的需求。 - **查询解析器(Query Parser)**:将用户的...

    lucene源码分析1111

    2. **索引过程** - 索引创建(Indexing):这是Lucene的第一步,涉及文档的读取、分词、建立倒排索引等步骤。倒排索引是Lucene效率的关键,它将每个词项与包含它的文档位置关联起来,便于快速查找。 3. **分词与词...

    Lucene.Net源码与说明文档

    Lucene.Net 的源码结构清晰,分为多个模块,如索引、查询解析、分词器等,每个模块都有明确的职责。源码主要由 C# 编写,遵循面向对象的设计原则,包括封装、继承和多态。通过阅读源码,开发者可以深入了解搜索引擎...

    基于lucene技术的增量索引

    Lucene通过分析这些文本,将其拆分为术语,并在倒排索引中存储每个术语的位置信息,以便快速定位到包含特定术语的文档。 **2. 增量索引的概念** 增量索引的目的是避免重新构建整个索引,尤其是在大型数据集上,这...

Global site tag (gtag.js) - Google Analytics