为搜索引擎构建索引,其目的是能更快速的提取与用户查询相关的文档信息,假设搜索引擎已经建立好了索引,如何根据倒排索引来相应用户的查询呢?
目前有两种查询机制:
假设用户输入查询“搜索引擎 技术”,分词后,“搜索引擎”这个单词对应的倒排列表中的文档ID依次为{1,3,4},“技术”这个单词对应的倒排列表中,文档ID列表为{1,2,4}。从这两个倒排列表可以看出,文档1和文档4同时包含了这两个查询词。
搜索引擎接收到用户的查询后,首先将两个单词的倒排列表从磁盘读入内存。所谓的一次一文档,就是以倒排列表中包含的文档为单位,每次将其中某个文档与查询的最终相似性得分,直到所有文档的得分都计算完毕为止。
下图是一次一文档的计算机制示意图,为了便于理解,图中对于两个单词的倒排列表中的公共文档进行了对齐。虚线标明了查询处理计算的行进方向。
以后的内容将说明如何计算得分,其实就是根据TF-IDF算法,以及余弦相似性来计算。
由于搜索的个数是限定的,比如指定输出10个结果,所以在实际实现一次一文档的时候,不必保留所有的得分,只需要维护一个优先级队列,得到K个文档即可。
一次一单词的计算过程与一次一文档不同,一次一文档是纵向的计算得分,然后再横向比较分数。一次一单词则是先横向将某个单词对应的倒排列表中的所有文档计算一遍,然后计算下一个单词,如果某个文档已经有了得分,则相加累计。最后得出输出最高的K个文档。
如果用户输入的查询包含了多个查询词,搜索引擎一般是默认“与&&”逻辑判断文档是否满足要求,即要求相关网页必须包含所有查询词,比如用户输入查询“搜索引擎 技术”,只有同时包含两个词汇的网页才会被认为是相关的,很明显这种场景采用一次一文档的查询处理方式是比较合适的。
很明显就是一个求并集的操作(但是我们这里求并集是通过压缩后的文档ID来求)。
跳跃指针怎么求并集呢?
跳跃指针的思想是将一个倒排列表化整为零,切分为若干个固定大小的数据块,一个数据块作为一组,对于每个数据块,增加元信息来记录关于这个块的一些信息,这样即使是面对压缩后(即索引压缩后)的倒排列表,在进行倒排列表合并的时候也能有两个好处:
如图,一开始是"Google"这个单词包含的文档编号,通过D-Gap压缩后出现差值,将差值分块,分块后插入跳跃指针,每个指针的管理信息例如<5,Pos1>
,其中5表示块中第一个文档ID的编号,Posl是跳跃指针指向第二块的起始位置。
所以,假设我们需要在单词"Google"压缩后的倒排列表里查找文档编号为7的文档。首先,对倒排列表前两个数值进行数据解压缩,读取第1块的跳跃指针数据,发现值为<5,pos1>
,然后根据pos1得到第2块的跳跃指针数据:<13,pos2>
。这样我们得到了两个未压缩的索引5和13,因为7在他们之间,如果存在一定在第一块中,如果第一块没有,说明不存在。
搜索引擎应该能够支持用户指定某个字段作为搜索范围,比如邮件搜索应用中应该允许用户在标题里搜索某个关键词,或者在正文里搜索,又或者在作者中搜索。
假设我们要处理的文档分为“标题”“摘要”“正文”三个字段。
当用户没有指定字段的时候,搜索引擎能够对所有字段查找并合并多个字段的相关性得分,对于多索引方式来说,就需要对多个索引进行读取,所以这种方式的效率会比较低。
为了能够支持对指定字段的搜索,也可以将字段信息存储在某个关键词对应的倒排列表在内,在倒排列表中每个文档索引项信息的末尾追加字段信息,这样在读出用户查询关键词的到排列表的同时,就可以根据字段信息,判断这个关键词是否在某个字段出现。
扩展列表是实际应用得比较多的支持多字段索引的方法,这个方法为每一个字段建立一个列表,这个列表记载了每个文档这个字段对应的出现位置信息。
短语是很常见的语言现象,但是短语之间的顺序会有很大的区别,比如“你懂的“ “懂你的”。
搜索引擎如何能够支持短语呢?如果单词的倒排列表只存储文档编号和单词词频信息,其保留的信息是不足以支持短语搜素的,因为单词之间的顺序关系没有保留。
较常见的支持短语查询的技术方法包括:
前面我们已经知道,单词的倒排列表,往往存储3种信息:文档ID、单词词频和单词的位置信息。一般情况下不存储单词位置信息,因为这种信息数量过大,一旦加入位置信息,单词的倒排列表长度会剧烈增长。这样一方面剧烈消耗了存储空间,另一方面影响磁盘的读取效率。
假设用户输入短语查询”爱情买卖“,图示很清楚在倒排列表里,”爱情“出现在ID为5的文档里,词频2次,出现位置分别是3和7。而”买卖“出现在ID为5的文档里,词频为1次,出现位置是4。然后判断位置信息,可以看出,5号文档里位置3和4分别对应”爱情买卖“,以此类推,这样就可以把正确的文档搜索返回。
因为短语至少包含了两个单词,也可能包含多个单词,统计表明两个单词的可能性最大,所以如果能够对双词短语提供快速查询,也能解决短语查询问题。
双词分别称”首词“和”下词“。下面是双词索引的结构:
假设输入”我的父亲“,首先被分词为”我的“和”的父亲“。然后发现包含”我的“的文档是ID5和ID7,而包含”的父亲“的是文档5,然后看看对应位置是否相邻即可。
很明显,双词索引中结构是二维的,所以倒排列表个数会发生爆炸性的增长。所以实现时不会对所有单词都建立双词索引,只会对计算代价高的短语建立双词索引。
直接在索引中加入短语索引,在词典中直接加入多词短语,并维护短语的倒排列表,这样也可以对短语进行支持。
短语索引有缺点:不可能事先对所有短语都建好索引,通用做法是挖掘出热门短语,为这些短语专门建立索引。
介绍这个查询,目的是查找相邻点(即给出点,查找距离这个点最近的一个点)。
第一种算法如下:
以杭州市黄龙体育场坐标(30.2660971991,120.1336939132)为例,把纬度区间[-90,90]分成[-90,0),[0,90],纬度30.2660971991属于[0,90],则标记为1,然后将区间[0,90]分为[0,45),[45,90],纬度30.2660971991属于[0,45),则标记为0。对纬度递归27次上述过程可以得到如下序列:
30.2660971991 => 101010110000101110001100101
同理,对经度递归28次上述过程可以得到如下序列:
120.1336939132 => 1101010101101101101010111110
然后把生成的序列按照偶数位放经度、奇数位放纬度的规则组合成如下序列:
1110011001100111001010001110011111001000110110101110110
(这样放置的原因是因为,交叉放置能够使得在搜索时,只会有末尾几个字符不一致。如果是前27位放纬度后28位放经度就会使得编码转换后不一致的位置会在中间和末尾出现。)
然后使用Base32对生成的序列编码得到11位编码为wtmkjty8vcq。
但是这样转码后,我们仍然不能直观的看出哪些位置的点是相邻的。
所以我们有了下面一种算法:
介绍Geohash算法的计算过程,一对经纬度坐标可以唯一确定地球上一个点,Geohash编码首先将经度和纬度编码到一个字符串中,编码的主要过程是用二分法递归计算经纬度逼近哪一个区间。Geohash其实就是将整个地图或者某个分割所得的区域进行一次划分,由于采用的是base32编码方式,这样我们可以将整个地图区域分为32个区域,通过00000 ~ 11111来标识这32个区域。第一次对地图划分后的情况如下图所示:
在下图中可以看到,按照层级关系建立树状结构保存Geohash编码(层级的树状结构来保存相邻位置,比如父节点M的子节点为KQSTWHJN,这样如果要找父节点M临近的点,可以直接在树上查出来。),因为在大多数情况下,相邻的位置有相同的前缀,这样的数据结构牺牲了索引速度,但是可以极大地加快查找速度:
Geohash的思想是将二维的经纬度转换成一维的字符串,Geohash有以下三个特点:
目前常用的分布式索引方案有两种:
所谓按文档划分,就是将整个文档集合切割成若干子集合,而每台机器负责对某个文档子集合建立索引,并响应查询请求。查询过程就是查询分发服务器接受到用户的查询请求后,将查询广播给所有索引服务器,每个索引服务器负责部分文档子集合的索引维护和查询响应。
对单词进行划分,每个索引服务器负责对部分单词的倒排列表的建立和维护。
两种方案比较: