在开始之前,先对IndexWriter的所需要考虑的问题有个大致的了解。IndexWriter主要负责索引数据写入,所以,增删改是主要需要考虑的问题;其次,由于写入的过程涉及成功失败的问题,所以,事务以及二阶段事务也是应该考虑到的;再来就是需要写入过程有最大的吞吐量,所以,还存在缓存以及刷新的问题;另外,如何让多线程高效并发访问索引也是要思考的;还有,通常查找内容的数量都非常庞大,如何管理索引过程中的内存以及磁盘消耗也是值得考虑的事情。接下来的文章,会对这些问题分别分析。
索引的增删改
增加,下面的api主要是加入一个或多个文档,以及对这篇文档使用的analyzer。实际上,增加文档在背后是调用了更新这一操作。它所提供的接口无非是单文档和多文档,有分析器和无分析器的区别。
- public void addDocument(IndexDocument doc) throws IOException
- public void addDocument(IndexDocument doc, Analyzer analyzer) throws IOException
- public void addDocuments(Iterable<? extends IndexDocument> docs) throws IOException
- public void addDocuments(Iterable<? extends IndexDocument> docs, Analyzer analyzer) throws IOException
更新,需要明白的是,更新并非像数据库一样,而是一个先删除后增加的过程
- public void updateDocuments(Term delTerm, Iterable<? extends IndexDocument> docs) throws IOException
- public void updateDocuments(Term delTerm, Iterable<? extends IndexDocument> docs, Analyzer analyzer) throws IOException
- public void updateDocument(Term term, IndexDocument doc) throws IOException
- public void updateDocument(Term term, IndexDocument doc, Analyzer analyzer)
- throws IOException
删除,第一种方式是删除所有包含这个词的文档,第二种方式是删除所有包含这几个词的文档,第三种方式是删除所有符合该查询的文档,第四种方式是删除所有符合这组查询的文档,第五种方式是直接删除所有文档
- public void deleteDocuments(Term term) throws IOException
- public void deleteDocuments(Term... terms) throws IOException
- public void deleteDocuments(Query query) throws IOException
- public void deleteDocuments(Query... queries) throws IOException
- public void deleteAll() throws IOException
在更深入的讨论前先理下索引的链式结构,因为这个神奇的结构经常把人搞得云里雾里。默认的索引链在DocumentsWriterPerThread中有初始化。代码如下:
- static final IndexingChain defaultIndexingChain = new IndexingChain() {
- @Override
- DocConsumer getChain(DocumentsWriterPerThread documentsWriterPerThread) {
- /*
- This is the current indexing chain:
- DocConsumer / DocConsumerPerThread
- --> code: DocFieldProcessor
- --> DocFieldConsumer / DocFieldConsumerPerField
- --> code: DocFieldConsumers / DocFieldConsumersPerField
- --> code: DocInverter / DocInverterPerField
- --> InvertedDocConsumer / InvertedDocConsumerPerField
- --> code: TermsHash / TermsHashPerField
- --> TermsHashConsumer / TermsHashConsumerPerField
- --> code: FreqProxTermsWriter / FreqProxTermsWriterPerField
- --> code: TermVectorsTermsWriter / TermVectorsTermsWriterPerField
- --> InvertedDocEndConsumer / InvertedDocConsumerPerField
- --> code: NormsConsumer / NormsConsumerPerField
- --> StoredFieldsConsumer
- --> TwoStoredFieldConsumers
- -> code: StoredFieldsProcessor
- -> code: DocValuesProcessor
- */
- // Build up indexing chain:
- final TermsHashConsumer termVectorsWriter = new TermVectorsConsumer(documentsWriterPerThread);
- final TermsHashConsumer freqProxWriter = new FreqProxTermsWriter();
- final InvertedDocConsumer termsHash = new TermsHash(documentsWriterPerThread, freqProxWriter, true,
- new TermsHash(documentsWriterPerThread, termVectorsWriter, false, null));
- final NormsConsumer normsWriter = new NormsConsumer();
- final DocInverter docInverter = new DocInverter(documentsWriterPerThread.docState, termsHash, normsWriter);
- final StoredFieldsConsumer storedFields = new TwoStoredFieldsConsumers(
- new StoredFieldsProcessor(documentsWriterPerThread),
- new DocValuesProcessor(documentsWriterPerThread.bytesUsed));
- return new DocFieldProcessor(documentsWriterPerThread, docInverter, storedFields);
- }
- };
之所以说它是一个索引链,因为它把整个请求层层向下链状传递(注意这不是真正的责任链模式,虽然非常像,因为它们并不是一个统一的接口)。最上层的领导发话要做什么,都经过层层传递,每层分解任务做一小部分,然后把剩余的向下传递,直到最下层最终执行完任务。而且这个责任链比较整齐的一点是,从触发点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不仅表明该块结束,而且还能通过该数字反推出目前的块到底是第几层。
- public final static int[] NEXT_LEVEL_ARRAY = {1, 2, 3, 4, 5, 6, 7, 8, 9, 9};
- public final static int[] LEVEL_SIZE_ARRAY = {5, 14, 20, 30, 40, 40, 80, 80, 120, 200};
- public int allocSlice(final byte[] slice, final int upto) {
- final int level = slice[upto] & 15;
- final int newLevel = NEXT_LEVEL_ARRAY[level];
- final int newSize = LEVEL_SIZE_ARRAY[newLevel];
- // Maybe allocate another block
- if (byteUpto > BYTE_BLOCK_SIZE-newSize) {
- nextBuffer();
- }
- final int newUpto = byteUpto;
- final int offset = newUpto + byteOffset;
- byteUpto += newSize;
- // Copy forward the past 3 bytes (which we are about
- // to overwrite with the forwarding address):
- buffer[newUpto] = slice[upto-3];
- buffer[newUpto+1] = slice[upto-2];
- buffer[newUpto+2] = slice[upto-1];
- // Write forwarding address at end of last slice:
- slice[upto-3] = (byte) (offset >>> 24);
- slice[upto-2] = (byte) (offset >>> 16);
- slice[upto-1] = (byte) (offset >>> 8);
- slice[upto] = (byte) offset;
- // Write new level:
- buffer[byteUpto-1] = (byte) (16|newLevel);
- return newUpto+3;
- }
其次应当注意两个原则:差值原则和变长原则,请参见索引的文件格式一文。
接下来,基本上与上面一样,不同的是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
相关推荐
#### 四、Lucene索引过程分析 Lucene的索引过程是一个复杂而有序的操作流程,主要步骤如下: - **1. 创建IndexWriter对象**:初始化索引写入器。 - **2. 创建文档Document对象,并加入域(Field)**:定义文档结构和...
Lucene学习总结之四:Lucene索引过程分析(1) Lucene学习总结之四:Lucene索引过程分析(2) Lucene学习总结之四:Lucene索引过程分析(3) Lucene学习总结之四:Lucene索引过程分析(4) www.chinaandroid.com
2. **配置Analyzer**:在Lucene的索引创建和搜索过程中,需要定义Analyzer。对于IKAnalyzer,可以这样配置: ```java import org.apache.lucene.analysis.cn.ik.IKAnalyzer; Analyzer analyzer = new IKAnalyzer();...
索引过程则涉及将这些文档转换为倒排索引,这是一种优化查询速度的数据结构;最后,查询解析器接收用户输入的查询,将其转化为可以匹配索引的形式。 在集成Lucene到SSI的过程中,我们需要考虑以下关键步骤: 1. **...
索引过程涉及分词(Tokenization)、词干提取(Stemming)、同义词扩展(Synonym Expansion)等步骤,将文本转换为可搜索的结构。 4. 分词器(Analyzer):负责将输入文本分解成一系列独立的词语,这是构建索引的...
本篇文章将详细介绍如何使用Lucene3.0来创建索引,并通过一个具体的例子来演示整个过程。 #### 一、Lucene3.0简介 Lucene是一款高性能、全功能的全文搜索引擎库。它为开发者提供了构建搜索应用所需的所有基本工具...
在多线程索引过程中,还应关注以下几点优化策略: - **硬件资源**:充分利用CPU和磁盘I/O资源,根据服务器配置合理设置线程数量。 - **内存管理**:监控内存使用情况,避免因内存不足导致的性能下降或崩溃。 - **...
Lucene创建索引步骤: 1、创建Directory(索引位置) 2、创建IndexWrite(写入索引) 3、创建Document对象 4、为Document添加Field(相当于添加属性:类似于表与字段的关系) 5、通过IndexWriter添加文档到索引中
本篇文章将详细阐述如何使用Lucene来创建和查询索引,帮助你深入理解其核心概念和操作流程。 ### 1. Lucene基本概念 - **文档(Document)**:在Lucene中,一个文档代表你要索引的信息单元,它可以包含多个字段...
索引过程将文本转换为可搜索的数据结构,而搜索则通过这个索引来快速找到相关文档。在这个过程中,我们通常会涉及到以下关键组件: 1. **Analyzer**: 分析器负责将输入的文本拆分成可搜索的词项(tokens)。这包括...
本文将详细介绍如何利用Lucene对XML文档进行索引建立的过程,并通过示例代码具体阐述其实现方法。 #### 二、基础知识 1. **Lucene简介** - Lucene是一个开源的全文搜索引擎库,能够帮助开发者构建应用程序内的搜索...
赠送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...
10. **性能调优**:通过分析源码,开发者可以了解到如何调整各种参数,如缓存大小、合并策略等,来优化Lucene的性能。 总的来说,深入学习Lucene 3.5.0的源码,可以帮助开发者掌握全文检索的核心技术,了解其内部...
在这个过程中,Lucene会分析文档内容,建立倒排索引,以便快速查找包含特定关键词的文件。 描述中提到的博客链接可能提供了具体的实现步骤或示例代码,但具体内容未给出,所以我们将基于Lucene的一般用法进行讲解。...
《深入剖析Lucene.NET 2.9.1:源码解析与应用开发》 Lucene.NET 2.9.1是开源搜索引擎库Lucene的.NET版本,它为.NET开发者提供了强大的全文检索和索引功能。这个版本的源码提供了一个宝贵的资源,帮助我们理解其内部...
《Lucene 2.4.1 源码分析——Analyzer深入解析》 在Lucene这个强大的全文搜索引擎库中,Analyzer扮演着至关重要的角色。它负责将输入的文本进行分词,过滤,标准化等预处理操作,使得搜索引擎能够正确理解和索引...
- **分析器(Analyzer)**:分析器负责将用户输入的文本进行分词,形成可以被索引的术语。5.0版本提供了更多可定制的分析器,如语言敏感分析器,满足不同场景的需求。 - **查询解析器(Query Parser)**:将用户的...
2. **索引过程** - 索引创建(Indexing):这是Lucene的第一步,涉及文档的读取、分词、建立倒排索引等步骤。倒排索引是Lucene效率的关键,它将每个词项与包含它的文档位置关联起来,便于快速查找。 3. **分词与词...
Lucene.Net 的源码结构清晰,分为多个模块,如索引、查询解析、分词器等,每个模块都有明确的职责。源码主要由 C# 编写,遵循面向对象的设计原则,包括封装、继承和多态。通过阅读源码,开发者可以深入了解搜索引擎...
Lucene通过分析这些文本,将其拆分为术语,并在倒排索引中存储每个术语的位置信息,以便快速定位到包含特定术语的文档。 **2. 增量索引的概念** 增量索引的目的是避免重新构建整个索引,尤其是在大型数据集上,这...