对于Lucene的索引过程,除了将词(Term)写入倒排表并最终写入Lucene的索引文件外,还包括分词(Analyzer)和合并段(merge segments)的过程,本次不包括这两部分,将在以后的文章中进行分析。

Lucene的索引过程,很多的博客,文章都有介绍,推荐大家上网搜一篇文章:《Annotated Lucene》,好像中文名称叫《Lucene源码剖析》是很不错的。

想要真正了解Lucene索引文件过程,最好的办法是跟进代码调试,对着文章看代码,这样不但能够最详细准确的掌握索引过程(描述都是有偏差的,而代码是不会骗你的),而且还能够学习Lucene的一些优秀的实现,能够在以后的工作中为我所用,毕竟Lucene是比较优秀的开源项目之一。

由于Lucene已经升级到3.0.0了,本索引过程为Lucene 3.0.0的索引过程。

一、索引过程体系结构

Lucene 3.0的搜索要经历一个十分复杂的过程,各种信息分散在不同的对象中分析,处理,写入,为了支持多线程,每个线程都创建了一系列类似结构的对象集,为了提高效率,要复用一些对象集,这使得索引过程更加复杂。

其实索引过程,就是经历下图中所示的索引链的过程,索引链中的每个节点,负责索引文档的不同部分的信息 ,当经历完所有的索引链的时候,文档就处理完毕了。最初的索引链,我们称之基本索引链

为了支持多线程,使得多个线程能够并发处理文档,因而每个线程都要建立自己的索引链体系,使得每个线程能够独立工作,在基本索引链基础上建立起来的每个线程独立的索引链体系,我们称之线程索引链。线程索引链的每个节点是由基本索引链中的相应的节点调用函数addThreads创建的。

为了提高效率,考虑到对相同域的处理有相似的过程,应用的缓存也大致相当,因而不必每个线程在处理每一篇文档的时候都重新创建一系列对象,而是复用这些对象。所以对每个域也建立了自己的索引链体系,我们称之域索引链。域索引链的每个节点是由线程索引链中的相应的节点调用addFields创建的。

当完成对文档的处理后,各部分信息都要写到索引文件中,写入索引文件的过程是同步的,不是多线程的,也是沿着基本索引链将各部分信息依次写入索引文件的。

下面详细分析这一过程。

二、详细索引过程

1、创建IndexWriter对象

代码:

IndexWriter writer = new IndexWriter(FSDirectory.open(INDEX_DIR), new StandardAnalyzer(Version.LUCENE_CURRENT), true, IndexWriter.MaxFieldLength.LIMITED);

IndexWriter对象主要包含以下几方面的信息:

  • 用于索引文档

    • Directory directory;  指向索引文件夹
    • Analyzer analyzer;    分词器
    • Similarity similarity = Similarity.getDefault(); 影响打分的标准化因子(normalization factor)部分,对文档的打分分两个部分,一部分是索引阶段计算的,与查询语句无关,一部分是搜索阶段计算的,与查询语句相关。
    • SegmentInfos segmentInfos = new SegmentInfos(); 保存段信息,大家会发现,和segments_N中的信息几乎一一对应。
    • IndexFileDeleter deleter; 此对象不是用来删除文档的,而是用来管理索引文件的。
    • Lock writeLock; 每一个索引文件夹只能打开一个IndexWriter,所以需要锁。
    • Set<SegmentInfo> segmentsToOptimize = new HashSet<SegmentInfo>(); 保存正在最优化(optimize)的段信息。当调用optimize的时候,当前所有的段信息加入此Set,此后新生成的段并不参与此次最优化。
  • 用于合并段,在合并段的文章中将详细描述
    • SegmentInfos localRollbackSegmentInfos;
    • HashSet<SegmentInfo> mergingSegments = new HashSet<SegmentInfo>();
    • MergePolicy mergePolicy = new LogByteSizeMergePolicy(this);
    • MergeScheduler mergeScheduler = new ConcurrentMergeScheduler();
    • LinkedList<MergePolicy.OneMerge> pendingMerges = new LinkedList<MergePolicy.OneMerge>();
    • Set<MergePolicy.OneMerge> runningMerges = new HashSet<MergePolicy.OneMerge>();
    • List<MergePolicy.OneMerge> mergeExceptions = new ArrayList<MergePolicy.OneMerge>();
    • long mergeGen;
  • 为保持索引完整性,一致性和事务性
    • SegmentInfos rollbackSegmentInfos; 当IndexWriter对索引进行了添加,删除文档操作后,可以调用commit将修改提交到文件中去,也可以调用rollback取消从上次commit到此时的修改。
    • SegmentInfos localRollbackSegmentInfos; 此段信息主要用于将其他的索引文件夹合并到此索引文件夹的时候,为防止合并到一半出错可回滚所保存的原来的段信息。
  • 一些配置
    • long writeLockTimeout; 获得锁的时间超时。当超时的时候,说明此索引文件夹已经被另一个IndexWriter打开了。
    • int termIndexInterval; 同tii和tis文件中的indexInterval。

2、创建文档Document对象,并加入域(Field)

代码:

Document doc = new Document();

doc.add(new Field("path", f.getPath(), Field.Store.YES, Field.Index.NOT_ANALYZED));

doc.add(new Field("modified",DateTools.timeToString(f.lastModified(), DateTools.Resolution.MINUTE), Field.Store.YES, Field.Index.NOT_ANALYZED));

doc.add(new Field("contents", new FileReader(f)));

Document对象主要包括以下部分:

  • 此文档的boost,默认为1,大于一说明比一般的文档更加重要,小于一说明更不重要。
  • 一个ArrayList保存此文档所有的域
  • 每一个域包括域名,域值,和一些标志位,和fnm,fdx,fdt中的描述相对应。

3、将文档加入IndexWriter

代码:

writer.addDocument(doc); 
-->IndexWriter.addDocument(Document doc, Analyzer analyzer) 
     -->doFlush = docWriter.addDocument(doc, analyzer); 
          --> DocumentsWriter.updateDocument(Document, Analyzer, Term) 
注:--> 代表一级函数调用

IndexWriter继而调用DocumentsWriter.addDocument,其又调用DocumentsWriter.updateDocument。

4、将文档加入DocumentsWriter

代码:

DocumentsWriter.updateDocument(Document doc, Analyzer analyzer, Term delTerm) 
-->(1) DocumentsWriterThreadState state = getThreadState(doc, delTerm); 
-->(2) DocWriter perDoc = state.consumer.processDocument(); 
-->(3) finishDocument(state, perDoc);

DocumentsWriter对象主要包含以下几部分:

  • 用于写索引文件

    • IndexWriter writer;
    • Directory directory;
    • Similarity similarity:分词器
    • String segment:当前的段名,每当flush的时候,将索引写入以此为名称的段。
IndexWriter.doFlushInternal() 
--> String segment = docWriter.getSegment();//return segment 
--> newSegment = new SegmentInfo(segment,……); 
--> docWriter.createCompoundFile(segment);//根据segment创建cfs文件。
    • String docStoreSegment:存储域所要写入的目标段。(在索引文件格式一文中已经详细描述)
    • int docStoreOffset:存储域在目标段中的偏移量。
    • int nextDocID:下一篇添加到此索引的文档ID号,对于同一个索引文件夹,此变量唯一,且同步访问。
    • DocConsumer consumer; 这是整个索引过程的核心,是IndexChain整个索引链的源头。

5、DocumentsWriter对CharBlockPool,ByteBlockPool,IntBlockPool的缓存管理

  • 在索引的过程中,DocumentsWriter将词信息(term)存储在CharBlockPool中,将文档号(doc ID),词频(freq)和位置(prox)信息存储在ByteBlockPool中。
  • 在ByteBlockPool中,缓存是分块(slice)分配的,块(slice)是分层次的,层次越高,此层的块越大,每一层的块大小事相同的。
    • nextLevelArray表示的是当前层的下一层是第几层,可见第9层的下一层还是第9层,也就是说最高有9层。
    • levelSizeArray表示每一层的块大小,第一层是5个byte,第二层是14个byte以此类推。

ByteBlockPool类中有以下静态变量:

final static int[] nextLevelArray = {1, 2, 3, 4, 5, 6, 7, 8, 9, 9}; 
final static int[] levelSizeArray = {5, 14, 20, 30, 40, 40, 80, 80, 120, 200};

6、关闭IndexWriter对象

代码:

writer.close();

--> IndexWriter.closeInternal(boolean)

--> (1) 将索引信息由内存写入磁盘: flush(waitForMerges, true, true); 
      --> (2) 进行段合并: mergeScheduler.merge(this);

对段的合并将在后面的章节进行讨论,此处仅仅讨论将索引信息由写入磁盘的过程。

代码:

IndexWriter.flush(boolean triggerMerge, boolean flushDocStores, boolean flushDeletes)

--> IndexWriter.doFlush(boolean flushDocStores, boolean flushDeletes)

--> IndexWriter.doFlushInternal(boolean flushDocStores, boolean flushDeletes)

将索引写入磁盘包括以下几个过程:

  • 得到要写入的段名:String segment = docWriter.getSegment();
  • DocumentsWriter将缓存的信息写入段:docWriter.flush(flushDocStores);
  • 生成新的段信息对象:newSegment = new SegmentInfo(segment, flushedDocCount, directory, false, true, docStoreOffset, docStoreSegment, docStoreIsCompoundFile, docWriter.hasProx());
  • 准备删除文档:docWriter.pushDeletes();
  • 生成cfs段:docWriter.createCompoundFile(segment);
  • 删除文档:applyDeletes();

6.1、得到要写入的段名

代码:

SegmentInfo newSegment = null;

final int numDocs = docWriter.getNumDocsInRAM();//文档总数

String docStoreSegment = docWriter.getDocStoreSegment();//存储域和词向量所要要写入的段名,"_0"

int docStoreOffset = docWriter.getDocStoreOffset();//存储域和词向量要写入的段中的偏移量

String segment = docWriter.getSegment();//段名,"_0"

在Lucene的索引文件结构一章做过详细介绍,存储域和词向量可以和索引域存储在不同的段中。

6.2、将缓存的内容写入段

代码:

flushedDocCount = docWriter.flush(flushDocStores);

此过程又包含以下两个阶段;

  • 按照基本索引链关闭存储域和词向量信息
  • 按照基本索引链的结构将索引结果写入段

6.3、生成新的段信息对象

代码:

newSegment = new SegmentInfo(segment, flushedDocCount, directory, false, true, docStoreOffset, docStoreSegment, docStoreIsCompoundFile, docWriter.hasProx());

segmentInfos.add(newSegment);

6.4、准备删除文档

代码:

docWriter.pushDeletes();

--> deletesFlushed.update(deletesInRAM);

此处将deletesInRAM全部加到deletesFlushed中,并把deletesInRAM清空。原因上面已经阐明。

6.5、生成cfs段

代码:

docWriter.createCompoundFile(segment);

newSegment.setUseCompoundFile(true);

代码为:

DocumentsWriter.createCompoundFile(String segment) {

CompoundFileWriter cfsWriter = new CompoundFileWriter(directory, segment + "." + IndexFileNames.COMPOUND_FILE_EXTENSION);

//将上述中记录的文档名全部加入cfs段的写对象。

for (final String flushedFile : flushState.flushedFiles)

cfsWriter.addFile(flushedFile);

cfsWriter.close();

}

6.6、删除文档

代码:

applyDeletes();

代码为:

boolean applyDeletes(SegmentInfos infos) {

if (!hasDeletes())

return false;

final int infosEnd = infos.size();

int docStart = 0;

boolean any = false;

for (int i = 0; i < infosEnd; i++) {

assert infos.info(i).dir == directory;

SegmentReader reader = writer.readerPool.get(infos.info(i), false);

try {

any |= applyDeletes(reader, docStart);

docStart += reader.maxDoc();

} finally {

writer.readerPool.release(reader);

}

}

deletesFlushed.clear();

return any;

}

  • Lucene删除文档可以用reader,也可以用writer,但是归根结底还是用reader来删除的。
  • reader的删除有以下三种方式:
    • 按照词删除,删除所有包含此词的文档。
    • 按照文档号删除。
    • 按照查询对象删除,删除所有满足此查询的文档。
  • 但是这三种方式归根结底还是按照文档号删除,也就是写.del文件的过程。

Lucene学习笔记: 四,Lucene索引过程分析的更多相关文章

  1. MongoDB学习笔记四:索引

    索引就是用来加速查询的.创建数据库索引就像确定如何组织书的索引一样.但是你的优势是知道今后做何种查询,以及哪些内容需要快速查找.比如:所有的查询都包括"date"键,那么很可能(至 ...

  2. Lucene学习笔记(更新)

    1.Lucene学习笔记 http://www.cnblogs.com/hanganglin/articles/3453415.html    

  3. C#可扩展编程之MEF学习笔记(四):见证奇迹的时刻

    前面三篇讲了MEF的基础和基本到导入导出方法,下面就是见证MEF真正魅力所在的时刻.如果没有看过前面的文章,请到我的博客首页查看. 前面我们都是在一个项目中写了一个类来测试的,但实际开发中,我们往往要 ...

  4. IOS学习笔记(四)之UITextField和UITextView控件学习

    IOS学习笔记(四)之UITextField和UITextView控件学习(博客地址:http://blog.csdn.net/developer_jiangqq) Author:hmjiangqq ...

  5. java之jvm学习笔记四(安全管理器)

    java之jvm学习笔记四(安全管理器) 前面已经简述了java的安全模型的两个组成部分(类装载器,class文件校验器),接下来学习的是java安全模型的另外一个重要组成部分安全管理器. 安全管理器 ...

  6. Mysql数据库学习笔记之数据库索引(index)

    什么是索引: SQL索引有两种,聚集索引和非聚集索引,索引主要目的是提高了SQL Server系统的性能,加快数据的查询速度与减少系统的响应时间. 聚集索引:该索引中键值的逻辑顺序决定了表中相应行的物 ...

  7. Learning ROS for Robotics Programming Second Edition学习笔记(四) indigo devices

    中文译著已经出版,详情请参考:http://blog.csdn.net/ZhangRelay/article/category/6506865 Learning ROS for Robotics Pr ...

  8. SQL反模式学习笔记13 使用索引

    目标:优化性能 改善性能最好的技术就是在数据库中合理地使用索引.  索引也是数据结构,它能使数据库将指定列中的某个值快速定位在相应的行. 反模式:无规划的使用索引 1.不使用索引或索引不足 2.使用了 ...

  9. Typescript 学习笔记四:回忆ES5 中的类

    中文网:https://www.tslang.cn/ 官网:http://www.typescriptlang.org/ 目录: Typescript 学习笔记一:介绍.安装.编译 Typescrip ...

  10. ES6学习笔记<四> default、rest、Multi-line Strings

    default 参数默认值 在实际开发 有时需要给一些参数默认值. 在ES6之前一般都这么处理参数默认值 function add(val_1,val_2){ val_1 = val_1 || 10; ...

随机推荐

  1. NSArray 数组排序

    //方法1,使用自带的比较器 //compare是数组自带的比较方法 NSArray *array=[NSArray arrayWithObjects:@"3",@"1& ...

  2. C#版二维码生成器

    前言 本文所使用的二维码生成代码是谷歌开源的条形码图像处理库完成的,c#版的代码可去 这里   --  下载压缩包. 截止目前为止最新版本为2.2,提供以下编码格式的支持: UPC-A and UPC ...

  3. iOS方法封装

    (void) setSubView:(UIView *)masterView subCCGRect:(CGRect)subCCGRect imageName:(NSString *)imageName ...

  4. Hibernate的一个注释 @Transient

    @Transient表示该属性并非一个到数据库表的字段的映射,ORM框架将忽略该属性. 如果一个属性并非数据库表的字段映射,就务必将其标示为@Transient,否则,ORM框架默认其注解为@Basi ...

  5. tuxedo入门

    为文件增加用户执行权限: 官网下载tuxedo111120_64_Linux_01_x86.bin su //进入root操作,防止权限不够 创建文件夹,用来做tuxedo文件的安装路径 cd /op ...

  6. mac下git中文乱码

    今天从window切mac,git使用时各种问题.典型的就是,git commit 可以使用中文注释,但是使用 git log 查看的时候发现都是乱码,乱码效果如下: <B1><E0 ...

  7. csv,txt,excel文件之间的转换,perl脚本

    最近接触一些需要csv,txt,excel文件之间的转换,根据一些网上搜索加上自己的改动,实现自己想要的结果为主要目的,代码的出处已经找不到了,还请见谅,以下主要是针对csv&excel 和t ...

  8. Codeforces Round #254 (Div. 2) B. DZY Loves Chemistry (并查集)

    题目链接 昨天晚上没有做出来,刚看题目的时候还把题意理解错了,当时想着以什么样的顺序倒,想着就饶进去了, 也被题目下面的示例分析给误导了. 题意: 有1-n种化学药剂  总共有m对试剂能反应,按不同的 ...

  9. websocket webworker

    对我来说最快的学习途径是实践,所以找两个东西来练手.一个是websocket一个是webwoker,今天先说第一个. 要理解socket就要先理解http和tcp的区别,简单说就是一个是短链,一个是长 ...

  10. BZOJ 2194 快速傅里叶之二

    fft. #include<iostream> #include<cstdio> #include<cstring> #include<cstdlib> ...