简介 IndexTank是一个托管的搜索基础服务。他主要有如下几个特色(从官网介绍翻译过来的):java
索引更新实时生效 地理位置搜索 支持多种客户端语言 Ruby, Rails, Python, Java, PHP, .NET & more! 支持灵活的排序与评分控制 支持自动完成 支持面搜索(facet search) 支持匹配高亮 支持海量数据扩展(Scalable from a personal blog to hundreds of millions of documents! ) 支持动态数据数据库
IndexTank在2011年10月被Linkedin收购,其原有提供的搜索托管服务将在2012年4月终止。目前Linkedin已经把IndexTank的索引引擎代码以及部分上层服务的代码开源。 2. 设计分析 因为IndexTank是一个开源不久的项目,官方公布的设计相关的资料以及网上对其分析测试能够说是几乎没有,对其内部实现分析都是经过分析代码得出,因为目前只是大体看了一下代码,部分细节分析可能有误。 2.1. 索引数据结构 IndexTank在底层索引倒排表是基于lucene实现的,可是其索引数据的总体设计与lucene有较大的区别。其基本思想为把索引数据分红三大类:倒排表数据、动态数据、原始文档数据。 2.1.1. 倒排表数据 这 部分数据主要经过lucene封装实现,底层文件数据结构与lucene彻底相同,可是与lucene对外暴露docId不一样,在倒排表获取的docId 会被转换为全局惟一不变的id,这点相似于ZOIE,可是实现上有所区别,具体看后文实现分析。倒排表数据为了支持实时搜索分红文件倒排和内存倒排两部分 数据 2.1.2. 动态数据 这部分数据是保存文档中会常常动态修改的数据,主要包括用于计算评分的数据、facet search字段等。该部分数据经过全局惟一的主键进行访问,数据彻底加载在内存中,定时dump到文件系统。 2.1.3. 原始文档数据 这部分数据是保存文档中不会动态修改的原始数据,主要用于在检索时获取原始文档内容生成摘要等。该部分数据经过全局惟一的主键进行访问。目前开源部分的 IndexTank-engine代码只定义了一个DocumentStorage的接口,并无提供实现,用户能够本身实现所需的存储控制器(如基于数 据库等)。 2.2. 代码分析 索引引擎的代码结构以下图: IndexTank全文检索引擎设计分析 - 网易杭研后台技术中心 - 网易杭研后台技术中心的博客缓存
上图为IndexTank中索引引擎IndexEngine中的主要类结构,各主要类的设计以下:安全
LargeScaleIndex
大 规模数据倒排索引封装类,用于存储持久化到文件系统中的索引数据,底层经过lucene实现。该类对外提供建索引文档的插入、删除以及查找制定查询对象匹 配的记录的接口,这些接口都与lucene无关,是IndexTank本身封装的接口,主要能够抽象为如下几个接口(其中findMatches为该类的 内部类中提供,为解析方便抽象出来一块儿介绍):数据结构
void add(String docId, Document document);并发
void del(String docId);分布式
// 注意下面的TopMatches与Query并不是lucene中的,而是IndexTank自行封装的函数
TopMatches findMatches(Query query, int limit, int scoringFunctionIndex);测试
能够看出,LargeScaleIndex与lucene的IndexWriter与IndexReader不一样,提供的接口都是以全局惟一且不变的docId为依据进行操做的,而不是使用lucene内部生成且会变化的docId。编码
在 建索引的时候会多创建一个特殊的字段,字段名和值都固定,并在payload中保存该文档对应的全局惟一不变的docId,从而造成一个按lucene内 部docId连续排序的倒排表,其中每项payload中保存对应的全局惟一不变docId,经过这种方式保存lucene docId与全局docId的映射关系,实现方式与zoie相同。另外,全部字段在经过lucene建索引时参数都是分词+不保存原值+不保存 TermVector+文本类型的字段。
在 检索的时候,首先经过lucene IndexReader的底层接口获取TermPositions封装生成匹配结果的迭代器,用于获取全部匹配Query对象的docId、位置偏移等信 息。而后遍历该迭代器,并在迭代过程当中经过payload字段获取匹配记录对应的全局docId,而后经过 DynamicDataFacetingManager统计分类信息,经过BoostsScorer计算最终文档评分,最终转换生成TopMatches 返回。
RealTimeIndex
实 时索引数据管理类,用于保存最近加入的文档的索引,数据保存在内存中,用于在数据刷写到文件系统前经过内存索引提供实时检索支持。内部保存两份倒排表数 据,分别为markedIndex和index,全部新数据进入index,而后在LargeScaleIndex开始刷写新数据的时候对 RealTimeIndex进行标记(mark),此时index数据放到markedIndex中,index新开一份空间用于保存新数据,在刷写完毕 的时候丢弃markedIndex,刷写过程当中检索数据同时读取两份倒排数据进行合并。每份倒排数据相似于经过一个Map>的结构保存全部倒排数据,以及DocId[]保存原始id与全局DocId的映射关系。该类提供了跟 LargeScaleIndex同样的接口。检索时重用了LargeScaleIndex的大部分代码。
DynamicDataManager
用 于保存文档动态数据的管理类,保存如推荐数等容易发生变化的数据,另外facet search中使用的分类信息(可选值数量比较小的字符串)也经过该类保存,在访问该类时,经过全局惟一不变docId定位文档。相关数据所有保存在内存 中并定时dump到文件系统,在保存分类信息的时候,会对分类字符串值进行相似哈弗曼编码以数值进行保存,避免重复保存相同的字符串占用内存。
DynamicDataFacetingManager
处理面搜索相关逻辑的管理器,在检索过程当中被Blender调用统计匹配结果中不一样类别的记录数,底层经过DynamicDataManager获取记录的分类信息。
BoostsScorer
记录评分计算器,内部保存各类用户评分计算接口实例,在计算记录评分的时候,经过计算接口对象从DynamicDataManager获取动态数据计算最终评分。
UserFunctionsManager
评 分接口管理类,IndexTank定义了一套评分函数,UserFunctionsManager负责对用户设置的评分函数字符串进行解析,并动态生成继 承评分计算接口(ScoreFunction)java.class对象进行加载并保存到BoostsScorer中。
IndexEngineParser
保存分词器并用于解析用户查询条件字符串。
Dealer
IndexEngine 中用于处理建索引请求的对象,在建索引的时候,会把同一个请求经过LargeScaleIndex和RealTimeIndex进行处理。其中 LargeScaleIndex只有在刷写新加数据到文件系统中才可以被检索出来,新加记录经过RealTimeIndex进行检索。
Blender
IndexEngine 中用于处理检索请求的对象,在检索的时候,会经过LargeScaleIndex和RealTimeIndex进行处理,其中 LargeScaleIndex获取老数据,RealTimeIndex获取新加记录,由Blender负责把两部分进行合并。
DocumentStorage
负责保存文档不变字段的原始内容,在访问该类时,经过全局惟一不变docId定位文档。IndexTank并无开源DocumentStorage的实现,只给出了其接口定义,由使用者自行定义实现。
Suggester
实现自动完成的相关功能,内部在内存经过相似Tri Tree的数据结构保存相关自动数据,并定时dump到文件系统。 2.3. 重要流程分析 2.3.1. 实时索引 IndexTank的实时索引的实现方式与ZOIE和LuceneNRT都不一样,其原理以下:
首先经过RealTimeIndex创建内存中的反向索引,该部分数据直接经过java的map、list等内存对象结构存储,没有经过lucene的RamDirectory实现,没有数据压缩。 而后把相同的文档经过LargeScaleIndex内部的lucene IndexWriter再次建一份反向索引,可是IndexWriter不commit,至关与只是在内存中进行了反向表的相关编码等,尚未写到文件系统。 在 调用Dealer对象的dump接口的时候,会经过LargeScaleIndex内IndexWriter的commit方法把内存中的数据刷到文件系 统,并根据合并策略进行merge操做。这个过程有可能会比较长,为了让实时索引保持正常,会对RealTimeIndex进行mark操做,原来的内存 Map对象的反向数据会保存到markIndex引用中,并新开一个Map对象保存新记录的反向索引,而LargeScaleIndex的新记录会缓存到 一个队列中等合并完毕在进行处理。这段时间内全部检索都要同时经过LargeScaleIndex中的老IndexReader和 RealTimeIndex中经过两个Map保存的方向索引数据进行检索并合并。等merge操做完成后,会同步reopen LargeScaleIndex中的IndexReader并丢弃RealTimeIndex中的markIndex。设计思想与ZOIE相似。
2.3.2. 创建索引
在创建索引使用者须要首先把索引文档字段划分为三类:须要建反向索引的静态字段、须要存储的静态字段、不须要反向索引的动态字段。 经过IndexEngine获取DocumentStorage对象,经过其接口保存须要存储的静态字段 经过IndexEngine获取Dealer对象,经过updateBoosts和updateCategories保存动态的评分字段与分类信息字段 经过Dealer对象add接口对给须要建反向索引的静态字段创建反向索引 定时调用Dealer对象的dump接口把内存中的实时反向索引数据、动态字段数据、自动完成数据持久化到文件系统
2.3.3. 检索记录
从IndexEngine中获取Blender对象,经过search接口传入query对象进行检索直接获取匹配结果的DocId与动态字段数据,具体处理步骤以下: 经过LargeScaleIndex和RealTimeIndex获取与query对象匹配的文档记录的迭代器 在 上一步生成的遍历迭代器基础上封装TopMatches迭代器,该迭代器在迭代的时候,经过DynamicDataManager获取须要的动态数据,由 DynamicDataFacetManager统计facet search的结果,由BoostsScorer计算文档评分 遍历上一步生成的迭代器获取全部匹配文档记录,在遍历的同时,根据制定获取的动态字段列表,从DynamicDataManager获取该文档的动态字段值。最终返回匹配结果。 从IndexEngine中获取DocumentStorage对象,根据须要获取文档的原始记录
2.3.4. 数据恢复
IndexTank在创建文档索引的时候会记录操做日志 IndexTank会定时把内存中的数据(包括实时反向索引、自动完成数据、动态字段数据等)dump到文件系统 若是系统崩溃,IndexTank会首先加载最后一次dump到文件系统的数据,而后在根据操做日志从dump的时间点开始重建索引数据。恢复的原理跟数据库相似。