lucene索引的核心流程
核心流程:新增,修改,删除,查询。
删除操作:没有什么特别的,内存中使用一个list对象记住将被删除的数据,调用commit数据最终才会被删除。
修改操作:先执行删除后新增的过程。
索引的基本流程
- 提取文本和创建文档:构建document实例和Field实例的过程
- 分析文档:将文本数据分割成词汇单串(看用的是什么解析器,不用的解析器会对英文的大小写,停词,以及中文的分词方式会有不同的处理)
- 向索引中新增文档:分析文档后,将分析结构以倒排索引的数据结构进行存储
新增索引
代码用例
@Test
public void addDoc() throws Exception {
dir = FSDirectory.open(Paths.get("/Users/siminglang/Desktop/Demo5"));
IndexWriter indexWriter = getIndexWriter();
for (int i = 0; i < ids.length; i++) {
Document doc = new Document();
doc.add(new StringField("id", ids[i], Field.Store.YES));
doc.add(new StringField("city", citys[i], Field.Store.YES));
doc.add(new TextField("desc", descs[i], Field.Store.NO));
indexWriter.addDocument(doc);
}
indexWriter.close();
}
基本流程:创建一个indexWriter类, 构建document实例,调用addDocument方法新增文档,调用indexWriter.close(方法内部会调用commit方法)提交文档
lucene新增索引的过程有何特点
- 写入索引过程中会触发的操作:flush,commit,merge
- lucene索引文档前,会判断是否需要进行段合并操作。 为什么要这样做,已经如何去实现它。
lucene的flush操作,实现了什么能力。又是怎么实现的
实现的能力:将缓存中的数据以segment的形式写入到磁盘当中。写入磁盘后,缓存会被释放。
如何实现:构建segmeng对象,写入到磁盘中。
lucene的commit又实现了什么能力呢?
- commit提供的能力:让所有的更改,包括缓存中的更改或者已经刷新的能改在索引中保持可见。
- 由lucene源码可知,commit的时候会执行flushAllThreads方法,对某个indexWriter所有的线程执行flush操作。
lucene数据写入过程流程图
lucene的merge操作
什么是merge操作:
段合并,如果索引中包含太多的段,IndexWriter会选择其中一些段并将它们合并成一个唯一的更大的段
好处:
加快搜索速度,因为被搜索的段的数量更少了;减少来索引的尺寸,例如如果被合并的段中包含删除操作的话,那么合并过程会释放文档删除标识所占用的数据位。即使没有删除操作,一般来说合并后的段也会占用更少的存储空间。
段合并时机:
变更操作进行刷新时;或者上一个段合并操作已经完成时
合并策略
段合并策略LogByteSizeMergePolicy,IndexWriter默认的情况下使用。该策略会测量段包含的所有文件的总字节数。
段合并策略LogDocMergePolicy,它测量的是段中的文档数量。
会按照段的尺寸对段进行分组,处于同一分组的段才可能被合并。每次合并时,只会取一个分组中的某些段进行合并,不会合并一个分组中的所有段,会控制参与合并的段的数量。maxMergeMB或者maxMergeDocs可以控制超过改数值的段不参与段合并,它们是大段。
控制段合并的类
MergePolicy来控制段合并
实施合并操作的类:MergerScheduler。它有两个子类,一个是ConcurrentMergeScheduler,后台线程完成段合并; 一个是SerialMergeScheduler,调用它的线程完成段合并。
删除索引
代码用例
@Test
public void deleteDoc() throws Exception {
dir = FSDirectory.open(Paths.get("/Users/siminglang/Desktop/Demo5"));
IndexWriter indexWriter = getIndexWriter();
indexWriter.deleteDocuments(new Term("id", "1"));
indexWriter.commit();
System.out.println("indexWriter.hasDeletions() :" + JSON.toJSONString(indexWriter.hasDeletions() ));
System.out.println("indexWriter.getInfoStream() :" + JSON.toJSONString(indexWriter.getInfoStream()));
System.out.println("indexWriter.getDocStats() :" + JSON.toJSONString(indexWriter.getDocStats()));
}
基本流程:创建一个indexwriter对象;构建一个Term删除条件,匹配条件对的文档将会被删除;调用commit提交删除操作;
特性
- 调用commit或者是close可以向索引中提交删除操作
- 及时删除操作完成,存储文档的磁盘也不会立即被释放,lucene只是将文档标记为删除。真正的删除操作是在文档那对应的段进行合并时才执行
- lucene中实现文档删除能力的类有两个:IndexReader和IndexWriter。IndexReader立即决定删除哪个文档。IndexWriter仅仅是将被删除的Term进行缓存,后续再进行实际的删除操作。
- lucene使用一个简单的方式记录被删除的文档,使用bit数组的形式记录,这个操作速度非常快,但是被删除的文档数据依然占用文档空间。
更新文档
lucene更新文档的过程,其实是将文档删除后新增的过程。特性和流程和上述的类似。
lucene新增文档过程
疑问:
- Field.Store字段的含义是?Store.YES 保存 可以查询 可以打印内容 ; Store.NO 不保存 可以查询 不可打印内容 由于不保存内容所以节省空间
- 完成写入过程的类?DefaultIndexingChain.StoredFieldsConsumer完成字段的写入过程。
- lucene是如何控制写入速度的?通过flushController进行控制,执行插入动作前,需要保证排队中的flush个数和停顿的线程数为0, 否则将执行flush操作。
- 整理下DocumentsWriterFlushControl的能力
什么是lucene中的segment
- lucene的索引是由一个或者多个segment组成的
- 每个segment都是一个独立的索引,它是整个文档索引的一个子集
- 每当write刷新缓冲区新增的文档,以及挂起目录删除操作时,索引文件都会建立一个新的段
- 在搜索索引时,每个段都是单独访问的,但搜索结果是合并后返回的
- 为了防止segment过多,影响搜索效率,lucene有segment合并策略,会周期性的将一些满足条件的segment合并成较大的segment。
何时回收被删除文档使用的磁盘空间
- 发生段合并的时候
- 显式调用optimize的时候,操作所有的段
- expungeDeletes 只针对进行了删除操作的段进行合并
缓存和刷新
- 为什么要用缓冲技术:减少磁盘IO
- 当新增一个文档到lucene索引时,或者挂起一个删除操作时,这些操作首先会被刷到内存当中,而不是立即刷到磁盘中
- 什么时候会触发将缓存中的数据刷新的磁盘当中呢?
- 当缓存所占用的空间超过预设的RAM比例时进行刷新,预设方法为setRAMBufferSizeMB
- 指定文档号对应的文档添加进索引后,通过调用setMaxBufferedDocs完成刷新
- 在删除项和查询语句等操作占用的缓存总量超过预设值,可以通过调用setMaxBufferedDeleteTerm触发刷新操作。
- 默认情况下,indexWriter只有在RAM用量为16M时启动刷新操作
索引提交
- 索引提交后的可视性问题?
- indexWriter的提交步骤:
- 两阶段提交
- 索引删除策略
- IndexDeletionPolicy负责通知IndexWriter核实能够安全的删除旧的提交
- 默认的KeepOnlyLastCommitDeletionPolicy, 该策略在每次创建完新的提交后删除先前的提交
如何提升索引文档的吞吐量
- 常用方式:使用固态磁盘SSD;升级到最新的lucene版本;升级到最新的java版本;;indexWriter,indexReader,indexSearch尽量使用共享实例;使用多线程;尽量给lucene配置更多的内存
- 尽可能的减少段的合并操作,因为该操作会消耗大量的CPU和IO资源
- 调整indexWriter刷新事件的阈值,调用方法setRAMBufferSizeMB。通常来说缓冲值是越大越好,但是你需要通过测试来得到一个合适的实践值。
- 关闭复杂文件格式选项,IndexWriter.setUseCompoundFile(false)
- 对Document实例和Filed实例进行重用。
- 针对不同的mergeFactor值进行测试,更高的值来带更低的索引开销,但同时也意味着更低的索引速度。但是一定要在真实的环境中进行测试。
并发,线程安全及锁机制
要点梳理
- 任意数量的indexReader都可以同时打开同一个索引。但是需要记住,利用资源和发挥效率最好的方法是利用多线程共享一个indexReader实例。
- 对于一个索引来说,一次只能打开一个Writer。lucene采用文件锁的方式来提供保障。一旦建立起来indexWriter对象,系统会立即分配一个锁给它。该锁只有当IndexWriter对像关闭时才释放。
- 注意如果使用indexReader来改变索引的话,这时indexReader对象会作为writer使用,它必须在执行修改动作之前获取到锁,并在关闭时会释放掉锁。
- indexReader 可以在indexWriter正在修改索引的时候打开。
- indexReader对象只有在indexWriter对象提交修改或者自己重新打开后才能获知索引的修改情况??一个更好的选择,打开indexReader时采用的参数create=true。这样的话,indexReader会持续检查索引的情况。
- idnexWriter和indexReader都是线程安全且是线程友好的( 标记为synchronized的代码并不多,仅为最小值)
- lucene采用的文件锁:如果锁文件(默认是write.lock)存在你的索引目录内,说明此时有一个writer打开了索引,如果针对同一索引创建一个新的writer的话,将产生一个lockObtainFailedException异常。这是一个重要保护机制,因为针对同一索引打开两个writer,会导致索引锁坏。
- 参看有关刷新和段合并的有关信息:IndexWriterConfig conf = new IndexWriterConfig(analyzer);
conf.setInfoStream(System.out);