索引技术

来源:互联网 发布:python中文手册下载 编辑:程序博客网 时间:2024/06/06 09:21

看信息检索导论。把其中的关于索引的内容总结一下,主要是自己做个笔记,以后看着方便。

  倒排索引

1  从布尔模型到倒排索引

1.1  怎么出现的倒排索引

最基本的模型是布尔检索模型,但缺点矩阵过于稀疏结果没有排序

为了解决稀疏问题,提出了倒排索引(inverted index)。有了倒排索引,布尔检索就变成了求两个链表的交集(先操作规模最小的步骤)、并集、补集的操作了。至此解决了布尔矩阵的稀疏问题。

1.2  倒排索引的预处理

在建立索引之前需要对索引的对象进行预处理。

Q1.2-1 对什么建索引?

需要考虑语言、文档类型、索引粒度等问题。

语言最常见的是乱码问题,可以根据网页的charset判断编码方式,分别建立不同语言的索引(怎样自动检测语言(似乎用n-gram就可以,看n-gram出现在哪个语料库中,或者根据启发式规则),怎样对不同的用户显示不同的语言(国际化问题,检测IP),怎样解决中文乱码在各种情况下(页面、数据库等,主要还是编码的统一))。

文档类型,需要在后台有不同的解析器,解析zip、xml、doc、pdf等,对其中的文档建立索引。另外需要通篇建立索引么,网页上的非正文域,如广告,可以直接删除了吧。

索引的粒度,一般都是对所有内容建索引,但是如果是一本书的话,还是分章节建索引的好。粒度太大,找到无关内容,粒度太小,丢失重要内容。不过粒度过大可以通过邻近搜索缓解(我想是根据不同的类型和问题确定不同的粒度,设置一系列的规则)。

Q1.2-2 词条化

词条化确实有很多细节问题,但是其实只要查询和后台数据的词条化方式是一样的,一般都还是好的。语言方面,如果不是英文类的话,可以用分词技术或者k-gram技术。内容方面,对于特殊的词汇,还是需要统计建立规则,比如邮件地址、URL、IP等,这个应该是需要根据正则表达式识别,来确立相应的词条。粒度方面,根据哪些标点进行词条分割,我想这是需要对语料库进行观察统计的吧。

对于某些像中文需要分词的语言,可以利用k-gram技术词条化,这样省去了Q1.2-3和Q1.2-4的过程,而且效果不错。

Q1.2-3 去停用词

统计词频,去掉所有高频词,但是我想实际上加上了idf的计算,这些高频词的权重已经降低了。另外去停用词是为了减小索引开销,可以通过压缩和索引权重来解决。所以现在Web搜索引擎基本不用去停用词了。如何对常见词进行处理,需要考虑应用背景,在做重复缺陷报告检测的时候去停用词好,但做缺陷报告摘要的时候句子长度是一维特征,在计算这个特征的时候就不能去停用词了。

Q1.2-4 词项归一化

它应该是为了提高搜索引擎对于单词处理的鲁棒性。对于不同形的词,需要建立等价类,判断哪些词是相关的,这有点像在索引处进行的查询扩展了。对于同形的词,进行词干还原。(确实没必要进行词形还原,也比较麻烦,而且两者达到的目的都是把不同的词归一到一个词上)

通过上述几步,可以把一个文档的内容变成一系列的词项,下面就需要对这些词项建倒排索引了。

2  索引的数据结构

2.1 简单索引数据结构

在选择索引的数据结构方面,对于一条索引可以使用链表(linked list)或者可变长数组(variable length array),两者的选择取决于数据是否需要频繁的更新。极端方式,基本不更新,可以直接用定长数组的链表方式,这样数据存在磁盘连续的空间内,从硬件寻道次数的角度提高效率。感觉还是链式表常用一些吧,因为可以动态更新。

增加跳跃指针变成跳表,一般跳跃幅度为。甚至是多级跳跃指针提高检索速度。多级跳表是一种基于概率的多层链表,以一定概率p生成下一层链表,在多级跳表上的检索类似于树形结构的检索,时间复杂度是对数的。它实际就是一棵树,然后把同一层的节点连到一起成为一条链,类似于双链树(link-link)。是它的性能取决于概率p,还有IO和内存的性能。

2.2  倒排索引存储的内容

索引必定会包含的有“单词ID,文档频率,{文档ID}”。23,2,{1,5}

另外为了进行邻近搜索,需要增加单词在文档中的位置信息,有了位置信息实际上也在索引中增加了词项频率信息。因此存储的内容变为“单词ID,文档频率,{文档ID,词项频率,{位置}}”。23,2,{1,2{3,7},5,3{5,10,15}}。这样一来,一个索引中包含了计算TF和IDF的全部信息,在检索时只需要根据偏移量找到特定的文档读取内容即可。偏移量的查找需要顺序遍历,时间复杂度O(n)。

在链式存储中,索引中会存储许多指针,这是一个比较大的空间开销。

3  利用倒排索引进行临近搜索

方法一:直接建立二元词索引或者变长的短语索引(phrase index),或根据词性的分析,建立跨越多个词的索引,如NX*N,建立忽略两个名词间的索引,同时忽略多个词性为X的单词。此种方式空间开销过于大,其实可以对常见的短语进行此种形式的索引,提高检索效率。

方法二:利用索引中的位置信息,对两个链表进行临近词检索,不过算法复杂度高些。需要先定位相同文档(指针遍历),再在文档中找满足差值小于距离|k|的文档,返回<文档ID,词项在d1中的文职,词项在d2中的位置>结果集。

方法三:在位置索引后面增加一个后续词索引(next word index),形成“单词ID,文档频率,{文档ID,词项频率,{位置1,后序词ID,位置2,后序词ID}}”。23,2,{1,2{3,7},5,3{5,27,10,19, 15,33}}。这样一来,可以直接在第一词的索引上找下一个单词。不过这样只适用于两者连接的情况(除非后续词的定义不同)。此方法和方法一实现的功能差不多,不过比它节省空间。

索引的选择:需要建立二元词索引,方法二需要较高的计算量,所以可以建立混合索引解决临近搜索问题。Case1对于高频短语查询,或者利用方法二计算代价过高(两个链表很长)的情况,建立二元词并在其上进行搜索,Case2其它的用方法二。另外可以根据短语的不同在Case1中考虑方法三。

4  建立倒排索引

如何建索引取决于硬件,尽量使CPU和IO并行,选择性的选择内存内容,IO尽量连续,减少磁盘寻道时间,多硬件分布式等。

方法一,基于块的排序索引方法BSBI:将大文档分割成几个大小相等的部分,对各部分的词项ID-文档ID进行排序(n*logn)并形成部分倒排记录,保存中间结果,对多个块m的倒排记录利用堆的方式合并成为一个倒排记录表(n*logm)。时间开销在于第一个排序上。同时词项ID需要用词项-词项ID的数据结构维护,与后面介绍的词典压缩类似,维护该数据结构的空间开销很大。

方法二,内存式单边扫描索引算法SPIMI:在给定的内存上,直接利用哈希表为新来的词项-文档ID(直接用词项)建立倒排索引,不需要n*logn的排序,然后对词项进行排序(词项的数量和方法一中的词项ID-文档ID的数量不是一个量级的),写回硬盘,成为一个块。对多个块用方法一合并。

方法三,分布式索引构建(distributed indexing),MapReduce。Map阶段把数据按照块(16MB或64MB,正好是磁盘的连续块)分配给不同的机器,运行分析器(parser),分析器形成小的倒排索引。Reduce阶段把不同分析器的结果,根据一定规则分配给Reduce的机器运行倒排器(inverter),得到一个单词完整的倒排索引,然后合并所有Reduce的结果。Map和Reduce的任务分配有主控节点(master node)调节。许多海量数据的问题都可以用这种模式解决。

方法四,动态索引。(1)索引重构,由于新的文档加入,定期对索引重构。(2)辅助索引(auxiliary index),新文档存储在内存的辅助索引(增加的文档,删除文档做标记)中,同时检索主索引和辅助索引,当辅助索引变大时与主索引合并。对数合并(logarithmic merging),如果辅助索引是10,倒排数目是1000,那么可以把1000个内容分配到10,20,40,80,160,320,640等几个小的索引。在一次查询时对这些小索引和辅助索引同时查询。合并时,辅助索引先和10合并,如果达到上一层的规模,则和20合并,最后得到一个1280的索引。再合并时,就直接10,1280了,然后变成20,1280,然后变成10,20,1280。这样的好处是,不用每次合并都和最大的合并,因为那样时间复杂度是O(m+n),m太大了。但搜索需要在log(T/n)个索引上进行。同时对多个索引维护全局统计信息如单词对应的索引位置等,都开销变大。

用哪个方法,需要考虑查询的需求。

5  倒排索引压缩

首先倒排索引的一条并不存储词典ID,而是存储两个词典ID的差。

可变字节码(Variable byte,VB):对于214577,二进制为1101 0001100 0110001。如果可变字节定为8,首位表示为0表示继续,1表示结束,另外7位存储原数的二进制,变为00001101 00001100 10110001。这样不用为每个数分配同样的空间了。可变字节的数值可以根据情况改变。如果不是空间稀缺,VB编码就够了。

编码:用1的数量表示二进制的长度,后面跟二进制的表示,比如13,二进制位1101。不考虑首位的1,则长度为3(101),那么用三个1加一个零表示长度,编码为1110 101。压缩率为最优压缩(纯二进制排在一起)的常数倍,压缩率最高,但解码较麻烦。

似乎把单词分为高频和低频,前者压缩,后者不用压缩(因为文档跨度很大),这样更好点。

从另一个角度,可以考虑,如何设置文档ID,使得其均匀分布,让压缩率更低(这个角度很好)。这里没有考虑频率和位置信息的压缩。另外这些都是在布尔查询前提下的,对排序式检索,文档ID不是递增的,而是按照单词频率排序的(直接略过后面频率低的文档),那么上面的文档ID压缩方式就不好用了(似乎可以局部的递增压缩)。

 

  词典索引

1  简单词典索引

实际上除了全文内容需要建立倒排索引,对于每个单词都有一个单词与ID的词典,为了提高检索效率,需要对词典建立索引。通常两种方法,哈希表和搜索树。其实对于大量和字符串统计相关的工作,所需要的数据结构都是这两个,如统计海量字符串的频率,查找唯一一个不同的字符串等。

需要考虑的因素:键的数目、键的变化情况(插入、删除频率)、键的访问频率。

对于简单的应用,使用哈希表比较方便,但是哈希表所需要的空间较大(一般,字典大小=哈希表大小*装载因子)、存在哈希冲突(一般用开放地址法解决),关键是无法进行前缀查询(通配符查询)。

哈希表的另一个变种是Trie树,即字典树。又称单词查找树,Trie树,是一种树形结构,是一种哈希树的变种。典型应用是用于统计,排序和保存大量的字符串(但不仅限于字符串),所以经常被搜索引擎系统用于文本词频统计。它的优点是:利用字符串的公共前缀来减少查询时间,最大限度地减少无谓的字符串比较,查询效率比哈希表高。Trie的检索复杂度取决于单词的平均长度。

另外就是搜索树,如二叉树,每次二分的查找字典,二叉树的压缩版就是B树,B树是关键字可以在非叶结点出现(所以存储空间比B+树、AVL树低,但插入删除费事。不过词典一般插入删除比较少,所以B树较好),每个结点可以有[M/2,M]个子结点的查找关键字的数据结构。每次把词典根据字母分成[M/2,M]份,依次查找,如果在键中找到,则返回倒排索引的位置。其实感觉在这个问题上它没有Trie树好。对于Trie和搜索树,都需要对单词有一个预定义的排序顺序。

2  词典索引上的查询

2.1 模糊查询

用户在输入查询时,查询词最先访问的就是词典索引。使用搜索树,可以应对普通查询和前缀(a*)查询。但后缀(*a)查询怎么办,可以建立一个反向B树(reverse B-tree)存储所有到此的倒序形式,解决后缀查询(*abc,相当于在反向B树中查找前缀cba*)。两棵树,也解决了中间含有通配符的情形(ab*cd),相当于(前缀ab*和后缀dc*的结果交集)。

如果有多个通配符怎么办,利用两棵树虽然也能查找,但是中间结果太大。

另一个解决通配符的方式是轮排索引(permuterm index),它的好处是不像两棵树必须要前后缀,因为可以把ab*cd*ef变为ef$ab*的形式,这样一次性完成了两棵树两次的遍历。时间快了,但生成的中间结果与两棵树一样多,同时单词轮排的空间开销比较大。

第三种是k-gram索引,就是建立单词片的索引,比如为包含$re三个字母的单词建立索引,这个方式比单词轮排的开销小一些,一般建立2-3-4 gram也就够了。不过也是会有许多中间结果,需要后过滤(postfiltering)。

其实在用搜索引擎时,在普通搜索的情况下,它们似乎更多的是把通配符当做一个普通的字母处理了(或者进行一些高频的替换),貌似并没有真正的如此遍历一个上述的索引。

2.2 拼写校正

另一个问题是拼写校正,其实这和索引的结构无关,做简单的处理。两个步骤,一选出最相似的正确单词集合,二在正确单词集合中选出校正单词。

第一步就是计算错误单词和所有单词的相似度,(1)编辑距离:缺点遍历的空间较大,可以假设单词有l个拼写正确,m个拼写错误,则利用轮排的思想查找l开头的前缀模糊匹配,然后在这个范围内计算编辑距离。(2)k-gram,把每个单词分成k片,然后返回所有包含这k片中某些片的单词列表,作为可能的拼写校正。对于每个校正单词可以计算Jaccard系数,寻找更为可能的。(3)基于发音的校正,根据语言专家的总结,把相似发音的字母组合归结为同一个,寻找发音最相近的。(4)其他启发式规则:如根据单词与当前词的键盘距离寻找可能的错误。另外对于所有拼写都正确,也可以进行拼写校正(可能单词的组合出错,我想一般是拼写看似正确但得到的结果很少的时候),同时看校正后的单词组合的可能性。(5)概率模型:错误查询是正确查询满足某一条件的概率变形的结果,当然条件可以加入语音相似度、键盘距离等多种上述度量方式,最后综合得到结果,不过它应该需要训练集了。第一步的目的是找出所有可能的拼写错误情况,但是结果空间要尽可能小。

第二步根据用户历史搜索记录,找出高频的候选结果作为拼写校正结果。

3  词典索引压缩

需要压缩的原因是由Heaps定于导致的,它说随着语料库的增加,词典的大小会持续增加。而压缩能够把更多的内容放入内存,并且加快从硬盘读取的速度。但是压缩的程度需要在CPU和IO时间上进行权衡。

对词典的压缩就是对已排序的字符串进行压缩。(1)把单词合并成为一个长的字符串,每个单词开头增加一个指针进行定位。(2)在单词中保留一个长度信息,从而可以k个单词保留一个定位指针。(3)提取公共前缀(这里肯定需要设置当共同前缀为长时才提取),类似于Trie的结构了。(4)如果词典还是无法保存在内存中,可以把单词分块压缩存储在硬盘中,在内存中维护一个B树的索引。这时估计是把常用的词典快加载到内存了(类似于操作系统的换页操作)。

 

  B树索引

对于大的词典文件上面提到如果内存放不下,在压缩后还是需要放在硬盘中的,因此需要对这些内容建立索引,考虑使用B树。

B树(B-Tree,也有翻译为B-树)是除根节点外每个节点有[m/2,m]的子树,相应的数量减一个关键词,叶子节点为空(代表查找失败),每个关键词指向词典某个块的位置。根据词典的大小设置m的值,使得最坏在logmT次IO操作查找到包含指定词项和索引位置的词典文件。比二叉树的好处是m的设置使得树的高度降低。

B+树同样是[m/2,m]个子树,但是关键词都在叶子节点上,即只有到了叶子节点才能知道具体的磁盘块的位置。并且叶子节点从小到大排序,用链表连接起来。

比较B树和B+树[1]

B+树比B树浪费的空间要多,但是在数据库上却用这种方式,因为B+树最大的好处,方便扫库,B树必须用中序遍历的方法按序扫库,而B+树直接从叶子结点挨个扫一遍就完了,B+树支持range-query非常方便,而B树不支持。这是数据库选用B+树的最主要原因。比如要查 5-10之间的,B+树一把到5这个标记,再一把到10,然后串起来就行了,B树就非常麻烦。B树的好处,就是成功查询特别有利,因为树的高度总体要比B+树矮。不成功的情况下,B树也比B+树稍稍占一点点便宜。B树比如你的例子中查,17的话,一把就得到结果了,有很多基于频率的搜索是选用B树,越频繁query的结点越往根上走,前提是需要对query做统计,而且要对key做一些变化(比如最频繁的查询是12,45,78,那么可以把它们设置成为高层的键,即45是根,12,78是左右孩子,其他的在底层)。另外B树也好B+树也好,根或者上面几层因为被反复query,所以这几块基本都在内存中,不会出现读磁盘IO,一般已启动的时候,就会主动换入内存。”

] mysql 底层存储是用B+树实现的,内存中B+树是没有优势的,但是一到磁盘,B+树的威力就出来了。

还有一个数据结构是B*树,它是在B+树的基础上把[m/2,m]变成[2m/3,m]提高空间利用率,同时减少平衡操作。

适用情况

对于这几种数据结构,如果数据量小,用二叉树(如AVL树),查找最快,因为B树类在每个结点需要线性遍历关键词。如果内存放不下,需要0/1查找(成功或失败)时,用B树,如词典。需要range查找时,用B+、B*树,如数据库。B+和B*选取取决于硬件。

 

  总结一下:

这里介绍的都是最适用于布尔查询的。倒排索引可以建立跳跃指针,需要进行压缩,合并。在倒排索引上需要词典索引,涉及到压缩,拼写校正,模糊查询等。词典索引上面是B树索引。所以搜索引擎似乎把常见的索引都用上了呀。



[1] http://geekannie.diandian.com/post/2013-09-17/40053632892

0 0
原创粉丝点击