Zoie是linkedin公司基于Lucene实现的实时搜索引擎系统,按照其官方wiki的描述为:
http://snaprojects.jira.com/wiki/display/ZOIE/Overview
Zoie is a realtime indexing and search system, and as such needs to have relatively close coupling between the logically distinct Indexing and Searching subsystems: as soon as a document made available to be indexed, it must be immediately searchable.
The ZoieSystem is the primary component of Zoie, that incorporates both Indexing (via implementing DataConsumer ) and Search (via implementing IndexReaderFactory > ).
Zoie是一个实时的搜索引擎系统,其需要逻辑上独立的索引和搜索子系统相对紧密的结合在一起,从而使得一篇文档一经索引,就能够立刻被搜索的到。
ZoieSystem是Zoie的重要组成部分,其一方面通过实现DataConsumer接口而完成了索引功能,一方面通过实现IndexReaderFactory >而完成了搜索功能,并将二者紧密的结合在一起。
下面就是ZoieSystem的总体架构图:
ZoieSystem是可以使用spring进行配置的,一个典型的配置如下:
|
看完了ZoieSystem的配置以后,我们首先来看看ZoieSystem的构造函数是如何使用这些参数进行初始化的:
(1) 其根据制定的索引文件夹${index.directory}生成一个DefaultDirectoryManager _dirMgr,用于管理索引文件夹及索引的版本号IndexSignature。
(2) 生成一个SearchIndexManager _searchIdxMgr,它是实现实时搜索的关键类,包含如下的成员变量:
(3) 将参数赋值成员变量ZoieIndexableInterpreter _interpreter,Analyzer _analyzer,Similarity _similarity
(4) 创建DiskLuceneIndexDataLoader _diskLoader对象,用于索引到硬盘索引
(5) 如果实时索引_realtimeIndexing设置为true,则创建RealtimeIndexDataLoader _rtdc,第四步中的_diskLoader作为其成员变量。将其设置为ZoieSystem的父类AsyncDataConsumer的成员变量setDataConsumer(_rtdc)
(1) 当系统启动的时候,索引处在Sleeping状态,这时Mem结构中,只有索引A,索引B为null,索引A为_currentWritable,_currentReadOnly为null,_diskIndexReader为硬盘索引的IndexReader。由于内存中索引的IndexReader是每添加完文档后立刻更新的,而且速度很快,而硬盘上的索引一旦打开,在下次合并之前,一直使用,可以保证新添加的文档能够马上被搜索到。
(2) 当A中的文档数量达到一定的数量的时候,需要同硬盘上的索引进行合并,因此要进入Working状态。合并是一个相对比较长的过程,这时候会创建内存索引B,在合并过程中新添加的文档全部索引到B中。此时的Mem结构中,有内存索引A,内存索引B,索引A为currentReadOnly,索引B为currentWritable,diskIndexReader为硬盘索引的IndexReader。此时要获得ZoieSystem的IndexReader,则三个IndexReader全都返回,由于索引B的IndexReader是添加文档后立刻更新的,因而能够保证新添加的文档能够马上被搜索到,这个时候虽然索引A已经在同硬盘索引进行合并,然而由于硬盘索引的IndexReader还没有重新打开,因而索引A中的数据不会被重复搜到。
(3) 当索引A中的数据已经完全合并到硬盘上之后,则要重新打开硬盘索引的IndexReader,打开完毕后,创建一个新的Mem结构,原来的索引B作为索引A,为currentWritable,原来的索引A被抛弃,设为null,currentReadOnly也设为null,diskIndexReader为新打开的硬盘索引的IndexReader。然后通过无缝切换用新的Mem结构替代旧的Mem结构,然后索引进入Sleeping状态。
上面一节中,我们可以看到,对于新添加的文档的实时搜索问题相对简单,然而当遇到文档更新的时候,就相对复杂了。
如何实时的删除已经索引在硬盘上的文档是一个很大的问题,为此Zoie实现了ZoieSegmentReader:
有了ZoieSegmentReader,下面我们来看文档更新情况下的实时搜索机制。
(1) 最初系统启动的时候,是在Sleeping状态下的,这个时候,内存索引为空,硬盘索引上有文档A,B,C。
(2) 在Sleeping状态下,更新文档B,则新的文档B进入内存索引,而硬盘索引中B被标记删除。
(3) 当内存中索引足够大的时候,索引会进入Working状态,进入合并过程。合并过程会首先将硬盘索引中被标记删除的文档先真实的删除,然后再将内存索引向硬盘索引进行合并。此时如果有新的更新进入,比如更新文档A,则将在另外一个内存索引和硬盘索引中都标记删除,然后将新文档添加到内存索引中。
(4) 当合并完毕后,硬盘索引会标记删除原来在内存索引中标记删除的文档,被合并的索引以及其标记删除的文档全部丢弃,索引进入Working状态。
(1) Zoie的索引过程由DataProvider中调用ZoieSystem的consume函数开始,其实是调用AsyncDataConsumer的consume(Collection > data)函数,其仅仅将DataEvent放在LinkedList > _batch中。
(2) AsyncDataConsumer有一个背后的线程ConsumerThread _consumerThread,其会调用_consumer.consume(currentBatch),由ZoieSystem的构造函数中第(5)步我们知道,此处的_consumer为RealtimeIndexDataLoader _rtdc。
(3) RealtimeIndexDataLoader.consume函数分一下几个步骤:
(4) RAMLuceneIndexDataLoader的consume函数会调用LuceneIndexDataLoader的consume函数,其包含以下步骤:
RealtimeIndexDataLoader的父类是BatchedIndexDataLoader,其有一个背后的线程LoaderThread,其会调用processBatch函数。
RealtimeIndexDataLoader的processBatch函数过程如下:
(1) 当内存索引中的文档数量超过配置的batch size或者时间超过设置的_delay的时候,就进行内存索引到硬盘索引的合并。
(2) 设置索引的状态从Sleeping到Working,_idxMgr.setDiskIndexerStatus(SearchIndexManager.Status.Working)
(3) 得到需要合并的内存索引readOnlyMemIndex = _idxMgr.getCurrentReadOnlyMemoryIndex()
(4) 将内存索引合并到硬盘索引:_luceneDataLoader.loadFromIndex(readOnlyMemIndex),DiskLuceneIndexDataLoader的loadFromIndex函数做以下事情
(5) 设置索引的状态从Working到Sleeping,_idxMgr.setDiskIndexerStatus(Status.Sleep)
在使用Zoie进行搜索的时候,要调用ZoieSystem的getIndexReaders()函数,其调用了_searchIdxMgr.getIndexReaders()。
SearchIndexManager的getIndexReaders函数,分别得到RAMSearchIndex memIndexA的IndexReader,RAMSearchIndex memIndexB的IndexReader,以及硬盘索引的IndexReader。在Sleeping状态下得到两个IndexReader,在Working状态下得到三个IndexReader。