背景
全文检索是信息检索技术的一种,主要是把用户的查询请求和全文中的每一个词进行比较,不考虑查询请求与文本语义上的匹配。在信息检索工具中,全文检索是最具通用性和实用性的。Lucene是apache软件基金会4 jakarta项目组的一个子项目,是一个开源的全文检索引擎工具包,基于倒排文件索引结构来实现检索功能。
什么是倒排索引?
什么是倒排索引?与正排索引有什么区别呢?
正排索引(正向索引)
正排表是以文档的ID为关键字,表中记录文档中每个字的位置信息,查找时扫描表中每个文档中字的信息直到找出所有包含查询关键字的文档。
正排表结构如图所示,这种组织方法在建立索引的时候结构比较简单,建立比较方便且易于维护;因为索引是基于文档建立的,若是有新的文档加入,直接为该文档建立一个新的索引块,挂接在原来索引文件的后面。若是有文档删除,则直接找到该文档号文档对应的索引信息,将其直接删除。但是在查询的时候需对所有的文档进行扫描以确保没有遗漏,这样就使得检索时间大大延长,检索效率低下。
倒排索引(反向索引)
倒排表以字或词为关键字进行索引,表中关键字所对应的记录表项记录了出现这个字或词的所有文档,一个表项就是一个字表段,它记录该文档的ID和字符在该文档中出现的位置情况。
由于每个字或词对应的文档数量在动态变化,所以倒排表的建立和维护都较为复杂,但是在查询的时候由于可以一次得到查询关键字所对应的所有文档,所以效率高于正排表。在全文检索中,检索的快速响应是一个最为关键的性能,而索引建立由于在后台进行,尽管效率相对低一些,但不会影响整个搜索引擎的效率。
倒排表的结构图如图所示:
索引原理
Lucene经多年演进优化,现在的一个索引文件结构如图所示,基本可以分为三个部分:词典、倒排表、正向文件、列式存储DocValues。
词典是如何构建的?
倒排索引中的词典位于内存,其结构尤为重要,有很多种词典结构,各有各的优缺点,最简单如排序数组,通过二分查找来检索数据,更快的有哈希表,磁盘查找有B树、B+树,但一个能支持TB级数据的倒排索引结构需要在时间和空间上有个平衡,下图列了一些常见词典的优缺点:
数据结构 | 优缺点 |
---|---|
跳跃表 | 占用内存小,且可调,但是对模糊查询支持不好 |
排序列表Array/List | 使用二分法查找,不平衡 |
字典树 | 查询效率跟字符串长度有关,但只适合英文词典 |
哈希表 | 性能高,内存消耗大,几乎是原始数据的三倍 |
双数组字典树 | 适合做中文词典,内存占用小,很多分词工具均采用此种算法 |
Finite State Transducers (FST) | 一种有限状态转移机,Lucene 4有开源实现,并大量使用 |
B树 | 磁盘索引,更新方便,但检索速度慢,多用于数据库 |
Lucene3.0之前使用的也是跳跃表结构,后换成了FST,但跳跃表在Lucene其他地方还有应用如倒排表合并和文档号索引。
FST原理简析
Lucene现在采用的数据结构为FST,它的特点就是:
1、词查找复杂度为O(len(str))
2、共享前缀、节省空间
3、内存存放前缀索引、磁盘存放后缀词块
这跟我们前面说到的词典结构三要素是一致的:1. 查询速度。2. 内存占用。3. 内存+磁盘结合。
已知FST要求输入有序,所以Lucene会将解析出来的文档单词预先排序,然后构建FST,我们假设输入为abd,abd,acf,acg,那么整个构建过程如下:
工具演示: http://examples.mikemccandless.com/fst.py?terms=&cmd=Build+it%21
举例:
输入到FST中的数据为:
String inputValues[] = {"mop","moth","pop","star","stop","top"};
long outputValues[] = {0,1,2,3,4,5};
数据结构 | 构建时间(ms) | 查询所有key(ms) |
---|---|---|
FST | 1512 | 890 |
HashMap | 185 | 106 |
TreeMap | 500 | 218 |
生成的FST:
100万数据性能测试
数据结构 | 构建时间(ms) | 查询所有key(ms) |
---|---|---|
FST | 1512 | 890 |
HashMap | 185 | 106 |
TreeMap | 500 | 218 |
可以看出,FST性能基本跟HaspMap差距不大,但FST有个不可比拟的优势就是占用内存小,只有HashMap10分之一左右,这对大数据规模检索是至关重要的,毕竟速度再快放不进内存也是没用的。
倒排表结构
倒排表就是文档号集合,但怎么存,怎么取也有很多讲究,Lucene现使用的倒排表结构叫Frame of reference,它主要有两个特点:
1. 数据压缩,可以看下图怎么将6个数字从原先的24bytes压缩到7bytes。
- 跳跃表加速合并,因为布尔查询时,and 和or 操作都需要合并倒排表,这时就需要快速定位相同文档号,所以利用跳跃表来进行相同文档号查找。
Luence中倒排索引使用举例
假设现在有两篇文章:
文章1的内容为:Tom lives in Guangzhou,I live in Guangzhou too
文章2的内容为:He once lived in Shanghai.
倒排索引建立过程如下:
(1)取得关键词
由于lucene是基于关键词索引和查询的,首先我们要取得这两篇文章的关键词,通常我们需要如下处理措施:
a.我们现在有的是文章内容,即一个字符串,我们先要找出字符串中的所有单词,即分词。英文单词由于用空格分隔,比较好处理。中文单词间是连在一起的需要特殊的分词处理(注:中文分词比英文分词更加复杂,如:“帽子和服装”、“中华人民共和国”等,有兴趣可以单独研究)。
b.文章中的”in”, “once” “too”等词没有什么实际意义,中文中的“的”“是”等字通常也无具体含义,这些不代表概念的词可以过滤掉
c.用户通常希望查“He”时能把含“he”,“HE”的文章也找出来,所以所有单词需要统一大小写。
d.用户通常希望查“live”时能把含“lives”,“lived”的文章也找出来,所以需要把“lives”,“lived”还原成“live”
e.文章中的标点符号通常不表示某种概念,也可以过滤掉
在lucene中以上措施由Analyzer类完成。 经过上面处理后,
文章1的所有关键词为:[tom] [live] [guangzhou] [i] [live] [guangzhou]
文章2的所有关键词为:[he] [live] [shanghai]
(2)建立倒排索引
有了关键词后,我们就可以建立倒排索引了。上面的对应关系是:“文章号”对“文章中所有关键词”。倒排索引把这个关系倒过来,变成: “关键词”对“拥有该关键词的所有文章号”。
文章1,2经过倒排后变成
关键词 | 文章号 |
---|---|
guangzhou | 1 |
he | 2 |
i | 1 |
live | 1,2 |
shanghai | 2 |
tom | 1 |
通常仅知道关键词在哪些文章中出现还不够,我们还需要知道关键词在文章中出现次数和出现的位置,通常有两种位置:
a.字符位置,即记录该词是文章中第几个字符(优点是关键词亮显时定位快);
b.关键词位置,即记录该词是文章中第几个关键词(优点是节约索引空间、词组(phase)查询快),lucene中记录的就是这种位置。
加上“出现频率”和“出现位置”信息后,我们的索引结构变为:
关键词 | 文章号[出现频率] | 出现位置 |
---|---|---|
guangzhou | 1[2] | 3,6 |
he | 2[1] | 1 |
i | 1[1] | 4 |
live | 1[2],2[1] | 2,5,2 |
shanghai | 2[1] | 3 |
tom | 1[1] | 1 |
(3)检索过程
lucene的检索过程分为以下几个步骤:
1. 内存加载tip文件,通过FST匹配前缀找到后缀词块位置。
2. 根据词块位置,读取磁盘中tim文件中后缀块并找到后缀和相应的倒排表位置信息。
3. 根据倒排表位置去doc文件中加载倒排表。
- 根据倒排表查到相关的文档,并计算相关度。
(4)检索结果
如果此时检索“tom live”,那么只需要检索包含[tom]和[live]这两个词的文档
关键词 | 文章号[出现频率] |
---|---|
tom | 1[1] |
live | 1[2],2[1] |
可以看到,两个词都在文档1中出现,且词频更高. 而文档2只被live命中一次。因此,检索结果中文档1的相关度更高。lucene默认使用TF/IDF算法来计算文档的相关度。