热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

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

在开始之前,先对IndexWriter的所需要考虑的问题有个大致的了解。IndexWriter主要负责索引数据写入,所以,增删改是主要需要考虑的问题;其次,由于写入的过程涉及成功失败的问题,所以,

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

  索引的增删改

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

public void addDocument(IndexDocument doc) throws IOException
public void addDocument(IndexDocument doc, Analyzer analyzer) throws IOException
public void addDocuments(Iterable docs) throws IOException
public void addDocuments(Iterable docs, Analyzer analyzer) throws IOException
   更新,需要明白的是,更新并非像数据库一样,而是一个先删除后增加的过程

public void updateDocuments(Term delTerm, Iterable docs) throws IOException
public void updateDocuments(Term delTerm, Iterable 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


推荐阅读
  • Spring框架中枚举参数的正确使用方法与技巧
    本文详细阐述了在Spring Boot框架中正确使用枚举参数的方法与技巧,旨在帮助开发者更高效地掌握和应用枚举类型的数据传递,适合对Spring Boot感兴趣的读者深入学习。 ... [详细]
  • 深入解析 Lifecycle 的实现原理
    本文将详细介绍 Android Jetpack 中 Lifecycle 组件的实现原理,帮助开发者更好地理解和使用 Lifecycle,避免常见的内存泄漏问题。 ... [详细]
  • 本文介绍了在 Java 编程中遇到的一个常见错误:对象无法转换为 long 类型,并提供了详细的解决方案。 ... [详细]
  • 在C#编程中,设计流畅的用户界面是一项重要的任务。本文分享了实现Fluent界面设计的技巧与方法,特别是通过编写领域特定语言(DSL)来简化字符串操作。我们探讨了如何在不使用`+`符号的情况下,通过方法链式调用来组合字符串,从而提高代码的可读性和维护性。文章还介绍了如何利用静态方法和扩展方法来实现这一目标,并提供了一些实用的示例代码。 ... [详细]
  • 浅析python实现布隆过滤器及Redis中的缓存穿透原理_python
    本文带你了解了位图的实现,布隆过滤器的原理及Python中的使用,以及布隆过滤器如何应对Redis中的缓存穿透,相信你对布隆过滤 ... [详细]
  • DAO(Data Access Object)模式是一种用于抽象和封装所有对数据库或其他持久化机制访问的方法,它通过提供一个统一的接口来隐藏底层数据访问的复杂性。 ... [详细]
  • 在多线程并发环境中,普通变量的操作往往是线程不安全的。本文通过一个简单的例子,展示了如何使用 AtomicInteger 类及其核心的 CAS 无锁算法来保证线程安全。 ... [详细]
  • 解决Bootstrap DataTable Ajax请求重复问题
    在最近的一个项目中,我们使用了JQuery DataTable进行数据展示,虽然使用起来非常方便,但在测试过程中发现了一个问题:当查询条件改变时,有时查询结果的数据不正确。通过FireBug调试发现,点击搜索按钮时,会发送两次Ajax请求,一次是原条件的请求,一次是新条件的请求。 ... [详细]
  • 如何在Java中使用DButils类
    这期内容当中小编将会给大家带来有关如何在Java中使用DButils类,文章内容丰富且以专业的角度为大家分析和叙述,阅读完这篇文章希望大家可以有所收获。D ... [详细]
  • 本文详细介绍了 PHP 中对象的生命周期、内存管理和魔术方法的使用,包括对象的自动销毁、析构函数的作用以及各种魔术方法的具体应用场景。 ... [详细]
  • 基于Linux开源VOIP系统LinPhone[四]
    ****************************************************************************************** ... [详细]
  • 在软件开发过程中,经常需要将多个项目或模块进行集成和调试,尤其是当项目依赖于第三方开源库(如Cordova、CocoaPods)时。本文介绍了如何在Xcode中高效地进行多项目联合调试,分享了一些实用的技巧和最佳实践,帮助开发者解决常见的调试难题,提高开发效率。 ... [详细]
  • 基于Net Core 3.0与Web API的前后端分离开发:Vue.js在前端的应用
    本文介绍了如何使用Net Core 3.0和Web API进行前后端分离开发,并重点探讨了Vue.js在前端的应用。后端采用MySQL数据库和EF Core框架进行数据操作,开发环境为Windows 10和Visual Studio 2019,MySQL服务器版本为8.0.16。文章详细描述了API项目的创建过程、启动步骤以及必要的插件安装,为开发者提供了一套完整的开发指南。 ... [详细]
  • 如何使用 `org.eclipse.rdf4j.query.impl.MapBindingSet.getValue()` 方法及其代码示例详解 ... [详细]
  • Unity3D 中 AsyncOperation 实现异步场景加载及进度显示优化技巧
    在Unity3D中,通过使用`AsyncOperation`可以实现高效的异步场景加载,并结合进度条显示来提升用户体验。本文详细介绍了如何利用`AsyncOperation`进行异步加载,并提供了优化技巧,包括进度条的动态更新和加载过程中的性能优化方法。此外,还探讨了如何处理加载过程中可能出现的异常情况,确保加载过程的稳定性和可靠性。 ... [详细]
author-avatar
飞逝舞_975
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有