倒排索引
搜索引擎基本工作流程
- 搜索引擎使用爬虫将大量网站收集起来
- 建立索引→倒排索引 / 正排索引
- 根据用户的搜索词进行查找
当接受到用户查询请求时,倒排索引中发生了什么?
一般地,当接受到用户查询请求时,进入到倒排索引进行检索时,在返回结果的过程中,主要有以下几个步骤:
Step1:在分词系统对用户请求等原始Query进行分析,产生对应的terms;
Step2:terms在倒排索引中的词项列表中查找对应的terms的结果列表;
Step3:对结果列表数据进行微运算,如:计算文档静态分,文档相关性等;
Step4:基于上述运算得分对文档进行综合排序,最后返回结果给用户。
上述该过程是较为简洁的一个检索过程。事实上,在生产环境中因为业务环境的繁杂,会使得索引的设计模式变得复杂且繁多。前文主要通过概念图来介绍倒排索引的架构体系,一个成熟的检索系统往往拥有一套较为稳定的算法体系,用于处理生产环境中的每一处细节技术需求。上述步骤中涉及了大量相关的数据储存技术、查找算法、排序算法、文本处理技术甚至I/O技术等等。
正排索引
正排索引是指以DocId为Key,构建一个表,表中记录网站中每个分词出现的次数和位置。
搜索引擎在查找时,需要对每个网页正排索引构建的表进行扫描,查看是否匹配。
正排索引是以DocID表示网页,假设现在有一个搜索词aaa,有100个网页,其中有10个网页中包含aaa这个分词。但由于正排索引的存储方式,需要将全网100个网页的正排表都进行check,将10个网站找出后,在根据分词aaa在10个网站中出现的次数,位置等特征求出10个网站的rank,sort。效率较低。
网页A=关键词1+关键词2+关键词3+关键词4+关键词5+关键词6+关键词7+.......
网页B=关键词2+关键词5+关键词+关键词12+关键词566+关键词26+关键词9+.....
网页C=关键词1+关键词3+关键词6+关键词9+关键词585+关键词25+关键词8+.....
优点:每当爬虫爬到一个新的网站时,只需要在DocId后添加一个新的项,便于维护。
缺点:搜索引擎搜索时时间过长。
倒排索引
倒排索引是先将网页中的内容采用NLP之类的技术将网页进行划分(正排索引中同样也需要记录分词)得到分词,这些分词的集合称为Lexicon(词典),Lexicon中包含了分词所拥有的特征,例如:出现次数,每次出现的位置等。然后倒排索引以index term(索引词)进行划分。维护所有网页中的每个term,用一张表来记录每个term出现的DocID,次数等信息。当出现一个搜索词aaa时,直接在表中查询aaa这个term,就可以知道aaa在那些网页中出现过,根据表中维护的信息,可以求rank。
关键词1=网页A+网页B+网页C+网页O+....
关键词2=网页B+网页P+网页Z+......
关键词3=网页D+网页T+网页Y+网页Z+.....
优点:查找迅速。
缺点:表维护时较为繁琐。
倒排索引的一些概念
文档(Document):一般搜索引擎的处理对象是互联网网页,而文档这个概念要更宽泛些,代表以文本形式存在的存储对象,相比网页来说,涵盖更多种形式,比如Word,PDF,html,XML等不同格式的文件都可以称之为文档。再比如一封邮件,一条短信,一条微博也可以称之为文档。在本书后续内容,很多情况下会使用文档来表征文本信息。
文档集合(Document Collection):由若干文档构成的集合称之为文档集合。比如海量的互联网网页或者说大量的电子邮件都是文档集合的具体例子。
文档编号(Document ID):在搜索引擎内部,会将文档集合内每个文档赋予一个唯一的内部编号,以此编号来作为这个文档的唯一标识,这样方便内部处理,每个文档的内部编号即称之为“文档编号”,后文有时会用DocID来便捷地代表文档编号。
单词编号(Word ID):与文档编号类似,搜索引擎内部以唯一的编号来表征某个单词,单词编号可以作为某个单词的唯一表征。
倒排索引(Inverted Index):倒排索引是实现“单词-文档矩阵”的一种具体存储形式,通过倒排索引,可以根据单词快速获取包含这个单词的文档列表。倒排索引主要由两个部分组成:“单词词典”和“倒排文件”。
单词词典(Lexicon):搜索引擎的通常索引单位是单词,单词词典是由文档集合中出现过的所有单词构成的字符串集合,单词词典内每条索引项记载单词本身的一些信息以及指向“倒排列表”的指针。
倒排列表(PostingList):倒排列表记载了出现过某个单词的所有文档的文档列表及单词在该文档中出现的位置信息,每条记录称为一个倒排项(Posting)。根据倒排列表,即可获知哪些文档包含某个单词。
倒排文件(Inverted File):所有单词的倒排列表往往顺序地存储在磁盘的某个文件里,这个文件即被称之为倒排文件,倒排文件是存储倒排索引的物理文件。
关于这些概念之间的关系,通过图3-2可以比较清晰的看出来。
图3-2 倒排索引基本概念示意图
倒排索引举例
倒排索引从逻辑结构和基本思路上来讲非常简单。下面我们通过具体实例来进行说明,使得读者能够对倒排索引有一个宏观而直接的感受。
假设文档集合包含五个文档,每个文档内容如图3-3所示,在图中最左端一栏是每个文档对应的文档编号。我们的任务就是对这个文档集合建立倒排索引。
图3-3 文档集合
中文和英文等语言不同,单词之间没有明确分隔符号,所以首先要用分词系统将文档自动切分成单词序列。这样每个文档就转换为由单词序列构成的数据流,为了系统后续处理方便,需要对每个不同的单词赋予唯一的单词编号,同时记录下哪些文档包含这个单词,在如此处理结束后,我们可以得到最简单的倒排索引(参考图3-4)。在图3-4中,“单词ID”一栏记录了每个单词的单词编号,第二栏是对应的单词,第三栏即每个单词对应的倒排列表。比如单词“谷歌”,其单词编号为1,倒排列表为{1,2,3,4,5},说明文档集合中每个文档都包含了这个单词。
图3-4 简单的倒排索引
之所以说图3-4所示倒排索引是最简单的,是因为这个索引系统只记载了哪些文档包含某个单词,而事实上,索引系统还可以记录除此之外的更多信息。图3-5是一个相对复杂些的倒排索引,与图3-4的基本索引系统比,在单词对应的倒排列表中不仅记录了文档编号,还记载了单词频率信息(TF),即这个单词在某个文档中的出现次数,之所以要记录这个信息,是因为词频信息在搜索结果排序时,计算查询和文档相似度是很重要的一个计算因子,所以将其记录在倒排列表中,以方便后续排序时进行分值计算。在图3-5的例子里,单词“创始人”的单词编号为7,对应的倒排列表内容为:(3:1),其中的3代表文档编号为3的文档包含这个单词,数字1代表词频信息,即这个单词在3号文档中只出现过1次,其它单词对应的倒排列表所代表含义与此相同。
图3-5 带有单词频率信息的倒排索引
实用的倒排索引还可以记载更多的信息,图3-6所示索引系统除了记录文档编号和单词频率信息外,额外记载了两类信息,即每个单词对应的“文档频率信息”(对应图3-6的第三栏)以及在倒排列表中记录单词在某个文档出现的位置信息。
图3-6 带有单词频率、文档频率和出现位置信息的倒排索引
“文档频率信息”代表了在文档集合中有多少个文档包含某个单词,之所以要记录这个信息,其原因与单词频率信息一样,这个信息在搜索结果排序计算中是非常重要的一个因子。而单词在某个文档中出现的位置信息并非索引系统一定要记录的,在实际的索引系统里可以包含,也可以选择不包含这个信息,之所以如此,因为这个信息对于搜索系统来说并非必需的,位置信息只有在支持“短语查询”的时候才能够派上用场。
以单词“拉斯”为例,其单词编号为8,文档频率为2,代表整个文档集合中有两个文档包含这个单词,对应的倒排列表为:{(3;1;<4>),(5;1;<4>)},其含义为在文档3和文档5出现过这个单词,单词频率都为1,单词“拉斯”在两个文档中的出现位置都是4,即文档中第四个单词是“拉斯”。
图3-6所示倒排索引已经是一个非常完备的索引系统,实际搜索系统的索引结构基本如此,区别无非是采取哪些具体的数据结构来实现上述逻辑结构。
有了这个索引系统,搜索引擎可以很方便地响应用户的查询,比如用户输入查询词“Facebook”,搜索系统查找倒排索引,从中可以读出包含这个单词的文档,这些文档就是提供给用户的搜索结果,而利用单词频率信息、文档频率信息即可以对这些候选搜索结果进行排序,计算文档和查询的相似性,按照相似性得分由高到低排序输出,此即为搜索系统的部分内部流程,具体实现方案本书第五章会做详细描述。
关于term的构造
在词项构造的过程中,利用分词系统对文本进行处理时往往涉及到很多方面的问题,而且对于不同语种,会有不同的处理机制。下面主要介绍在处理文本时涉及到的几个问题:
(1)文本词条化
一段文本信息,它本身是一个由语言组成的字符串系列,本项技术点的主要任务是将一段连续的文本序列信息拆分成多个子序列。它与语言本身相关,面对不同的语言,处理文本的方式往往会不一样。
(2)停用词过滤
停用词是指在文档列表中出现的频数较高且价值不大的词。以英文为例,在英文文档中出现次数较多的停用词如:”is”、”the”、”I”、“and”、”me”等等;这一类词语在往往出现在所有文档中,若以此类词语为term进行索引构建,则会产生多个全量文档索引列表。
3)词条归一化
基于上述两点,将文档内容转换成一个或多个term后,在查询时,最理想的情况是用户输入的关键字刚好与term完全匹配,实际上,很多时候用户输入的query与词条之间往往不会完全匹配,而用户们还是希望query能与词条进行匹配,比如用户在查询“color”时,用户肯定也希望能看到关于“colour”的返回结果。
词条归一化的任务就是将一些看起来不完全一致的词条划分为一个等价类,比如英式单词colour和美式单词color归为一类、Air-conditioner和airconditioner归为一类等等。这样,用户在查询时,只要对等价类中的任意单词进行搜索,都会返回包含等价类中的任意一个单词的文档。
4)词干提取、词形还原
这是词条规范化的两种重要方式,用于扩展检索范围。词干提取的主要思想是“缩减”,将词条转化为词干,如:将“beaches”处理成“beach”,将“bananas”处理成“banana”等;词形还原的主要思想是“转换”,如:将“doing”、“done”、“did”转化成原型“do”,将“given”、“gave”转化成原型“give”等;词干提取的实现方法一般是基于规则对词条后缀进行缩减,至于词形还原,其实现方法需要词典来进行词形变化的映射;基于在此结合词条归一化技术,对扩展检索范围会产生一定的正向作用。
关于倒排索引文件的构建
基本的构建方法如下:
S1:通过一系列的处理将文档集合转化为“词项ID—文档ID”对;
S2:对词项ID、文档ID进行排序,将具有相同词项对文档ID归并到该词项所对应的倒排记录表中,效果如图3所示;
S3:将上述步骤产生的倒排索引写入磁盘,生成中间文件;
S4:将上述所有的中间文件合并成最终的倒排索引。
从业务应用场景的角度出发,倒排记录表的构建方法主要有:单遍扫描和多遍扫描;从工程角度出发,倒排记录表的构建方法主要有:分布式构建和动态构建。
单遍扫描构建
顾名思义,单遍扫描指的是仅对文档集合进行一次遍历,即可完成倒排索引的构建。由于内存开销问题,会将全量文档集进行分割,转换成几个内存大小相同的文档集合,然后依次执行前文中提及到的构建方法。该方法能快速构建一个简单可行的倒排索引,帮助用户通过关键字匹配快速找到目标文档。
多遍扫描构建
多遍扫描主要用于构建索引时获取关于文档的更多相关信息,如一些词项TF-IDF指标、词频、文档内容关系等,以丰富倒排记录表的内容,为搜索引擎进行功能扩充;在工业流水线上,单遍扫描构建索引由于其查询类型的丰富度不够,显然已经不能满足广大用户的需求了。搜索用户的需求并不止于关键字查询,像短语查询、模糊查询、精确筛选、模糊筛选、排序、聚合统计等等需求。这意味着我们在构建倒排列表时要尽可能获取文档的更多信息,便于查询时的微运算、重排序、相关性分析等技术需求。
分布式构建
对于一些大型搜索引擎如Web搜索引擎,单台机器已无法支撑其索引构建,需要多台机器组成集群对其进行分布式处理,将构建成的倒排索引进行分割,分布在多台机器上,每台机器各自形成独立的索引结构。当用户发出请求时,会有多台机器响应,并且根据用户的搜索需求在各自的索引结构进行查询,返回相关结果,再将所有结果在内存中进行集中处理,最后把处理过的最优结果返回给用户。在具体的实现过程中,工程师们往往更钟情于一些通用的面向大规模机器计算的分布式架构如Hadoop中的MapReduce、Java中的Fork/join架构等,极大地提高了软件开发效率。
动态构建
该方法中的文档集合是变化的,这要求在对文档集进行索引构建时也要对文档的更新进行自适应。此问题常见于电商领域里,如商品的上下架、商品内容的更新等,都会引发索引的动态更新问题。于此,我们常采取一些策略型方法来解决该类型的问题,提高索引的实时性。
常见的策略如下两种:
1) 周期性对文档进行全量重建索引;
2) 基于主索引的前提下,构建辅助索引,用于储存新文档,维护于内存中,当辅助索引达到一定的内存占用时,写入磁盘与主索引进行合并。
策略1是最简单直接、且有效的索引更新策略,对于数量级较大的搜索引擎来说处理简单便捷,由于动态索引计算的复杂性,使用其它策略会使得索引难维护,甚至引发严重的性能问题。所以大型搜索引擎往往更倾向于周期性重建索引,不过这会涉及到索引热切换的问题,大量的文档经常会产生持续性的文档更新情况,这对于索引热切换时会造成一定的困难,处理不好会导致数据丢失,用户查不到新文档等问题。
策略2中在进行主辅索引合并时会遇到比较大的储存开销,由于文档量较大,这意味着在进行合并操作时会涉及到大量倒排文件的读写操作,要想将该过程高效化,目前能处理该问题的文件系统极其稀少,所以该策略在生产环境中往往可用性并不高。
关于词典
单词词典是倒排索引中非常重要的组成部分,它用来维护文档集合中出现过的所有单词的相关信息,同时用来记载某个单词对应的倒排列表在倒排文件中的位置信息。在支持搜索时,根据用户的查询词,去单词词典里查询,就能够获得相应的倒排列表,并以此作为后续排序的基础。
对于一个规模很大的文档集合来说,可能包含几十万甚至上百万的不同单词,能否快速定位某个单词,这直接影响搜索时的响应速度,所以需要高效的数据结构来对单词词典进行构建和查找,常用的数据结构包括哈希加链表结构和树形词典结构。
4.1 哈希加链表
图1-7是这种词典结构的示意图。这种词典结构主要由两个部分构成:
主体部分是哈希表,每个哈希表项保存一个指针,指针指向冲突链表,在冲突链表里,相同哈希值的单词形成链表结构。之所以会有冲突链表,是因为两个不同单词获得相同的哈希值,如果是这样,在哈希方法里被称做是一次冲突,可以将相同哈希值的单词存储在链表里,以供后续查找。
图1-7 哈希加链表词典结构
在建立索引的过程中,词典结构也会相应地被构建出来。比如在解析一个新文档的时候,对于某个在文档中出现的单词T,首先利用哈希函数获得其哈希值,之后根据哈希值对应的哈希表项读取其中保存的指针,就找到了对应的冲突链表。如果冲突链表里已经存在这个单词,说明单词在之前解析的文档里已经出现过。如果在冲突链表里没有发现这个单词,说明该单词是首次碰到,则将其加入冲突链表里。通过这种方式,当文档集合内所有文档解析完毕时,相应的词典结构也就建立起来了。
在响应用户查询请求时,其过程与建立词典类似,不同点在于即使词典里没出现过某个单词,也不会添加到词典内。以图1-7为例,假设用户输入的查询请求为单词3,对这个单词进行哈希,定位到哈希表内的2号槽,从其保留的指针可以获得冲突链表,依次将单词3和冲突链表内的单词比较,发现单词3在冲突链表内,于是找到这个单词,之后可以读出这个单词对应的倒排列表来进行后续的工作,如果没有找到这个单词,说明文档集合内没有任何文档包含单词,则搜索结果为空。
4.2 树形结构
B树(或者B+树)是另外一种高效查找结构,图1-8是一个 B树结构示意图。B树与哈希方式查找不同,需要字典项能够按照大小排序(数字或者字符序),而哈希方式则无须数据满足此项要求。
B树形成了层级查找结构,中间节点用于指出一定顺序范围的词典项目存储在哪个子树中,起到根据词典项比较大小进行导航的作用,最底层的叶子节点存储单词的地址信息,根据这个地址就可以提取出单词字符串。
图1-8 B树查找结构
关于搜索引擎的架构
- merger: 接收查询请求,分词后请求下游Indexer分别获取各个indexer的局部TopK文档,归拢后排序返回全局相似度最高的TopK文档。
- indexer:负责倒排拉取,并利用夹角余弦算法计算相似度,返回TopK结果。
- index:倒排索引,右侧图片
倒排索引的构建(前面是索引文件的构建)
对几亿甚至几十亿几百亿规模的文档集合建立倒排索引并不是一件很轻松的事情,它将面对着IO以及CPU计算的双重瓶颈,需要根据实际情况找到最优方法,接下来介绍两种不同的建立倒排的方式。
单遍内存型
内存中维护每个词对应的文档编号列表,当一个词对应的buffer满时,把内存中的数据flush到磁盘上,这样每个词对应一个文件。最后按照词编号由小到大的合并所有文件,得到最终的倒排索引。
-
优点:使用内存存放倒排索引,并分批flush到磁盘。这样减少了一些排序操作,速度快。
-
缺点:内存使用量稍大。可以减少内存buffer大小来降低内存使用量,但是这样做得缺点就是增加了磁盘随机写的次数。想想,每当一个buffer满时都会写对应的文件,并且buffer是随机的,所以增加了磁盘寻址的开销。
多路并归型
步骤如下:
-
首先,解析文档,把<词,文档编号,tf>写入到磁盘文件。
-
然后,对磁盘文件进行外部排序,排序规则:按照词的字典序从小到大排序,如果词相同,则按照文档编号从小到大排序,这样相同的词就排到了一起。
-
最后,顺序扫描排序后的文件,建立倒排索引。由于相同的词紧挨在一起,可以一个词一个词的建立倒排索引。
与内存型相比,这种方式适合在内存小,磁盘大的情况下进行倒排索引的建立,它的优缺点如下。
-
优点:内存使用量小,没有磁盘随机读写,基本全是顺序读写。对于超大规模文档集合来说,这种方式相对灵活一些(主要是排序灵活)。
-
缺点:多次归并排序,比较慢。但是,可以使用并发进行归并排序,这样能够提高一些速度。
索引切分
考虑到在海量文档下,倒排索引非常大,单台机器无法在内存中装下全部索引,所以有必要把索引进行切分,使得每一个索引服务只对文档中一部分的内容进行拉取、计算。常见的有两种可选择的方式。
按文档编号切
按照文档编号把文档分成几个小的集合,对每个小的文档集合单独建立索引。在这种方式建立索引上进行查询时,merge需要把查询请求下发到所有的后端indexer服务(因为每个index都有可能存在包含查询词的文档),indexer服务的计算量比较大。但是它也有一个优点:每个词的倒排拉链的长度可控。
按term切分
按照词进行索引划分,每个索引只保存若干词的所有文档编号。在这种方式建立的索引上进行查询时,merge可以根据查询词精确的把请求下发到对应的indexer上,减少了后端indexer的计算量。
但是这么做引入几个新问题:
-
1:某些高频词倒排拉链过长,导致这台indexer计算时间超出可忍受范围,出现拖后腿现象;
-
2:维护词表与indexer的对应关系,运维复杂;
-
3:对于热词所在的索引,对应的indexer请求量大,计算量大,负载高,需要考虑热词打散。
那么在实际中该如何进行索引切分呢?主要看是什么类型的查询、查询的量、以及文档集合的规模。
-
对于普通搜索引擎,用户输入的是数量较少的查询词,按照term切分可以有效减少查询时后端indexer的计算量,收益比较大。
-
对于现在流行的拍照搜题,拍摄的图片中文字一般较多,按照term切分的优势不明显而且引入了复杂的运维,建议按照文档编号切分。
-
对于高频词出现的文档,可以把这些文档选出来,然后按文档切分的方式建立索引,这样不会出现倒排拉链过长。总结下来就是:高频词按文档切分,低频词按照term切分,检索的时候,根据查询词是否是高频词执行不同的检索策略。
-
在不差钱并且文档规模不大,并且查询量没有达到必须优化索引的前提下,尽量使用运维更简单的索引:按照文档编号切分。
增量索引
很多搜索引擎都注重时效性搜索,比如对于时下刚刚发生的某件热门事件,需要搜索引擎能够第一时间搜索到该热门事件的页面,这该如何做到呢?由于建立一次全量文档倒排的时间基本都是按天计,如果不设计一些实时增量索引,那么根本满足不了时效性的检索。下面介绍增量索引,可以解决时效性搜索问题。
倒排索引双buffer设计方案
增量步骤:
-
1:indexer从索引2切换到索引1
-
2:更新索引2
-
3:indexer从索引1切回到索引2
-
4:更新索引1
这样可以保证实时的动态更新,但是它的缺点也很明显:必须使用2倍索引大小的内存,机器成本比较高,实际中更常用的是下面一种方案。
增量索引服务+双buffer方案
全量索引服务用来查询截止到某一个日期的全部文档,增量索引服务使用双buffer设计方案查询最近一段时间(可能是小时级或者分钟级)内实时更新的文档内容,然后定期(每天、每周、每月一次)把最近一段时间更新的文档追加在全量索引中。这样做的好处就在于只有少量近期更新文档的查询需要使用双倍内存,机器成本降低。需要注意的一点是,用这种方式建立增量索引时,必须更新全局word的df信息,对于发现的新词还需为其添加全局唯一id,这些信息统统要更新到线上正在运行的全量索引服务。
利用Hadoop并行建立倒排索引
对于超大规模的文档集合,可以使用Hadoop建立倒排索引。
-
map端读入每个文档并进行解析,生成一个tuple,key为词,value为tf+文档编号,发送该tuple给下游reduce,这样相同的词会分配给同一个reduce。
-
reduce保存每个tuple,并在结束时对每个词倒排按照文档编号由小到大排序,并保存到对应的文件中。
-
有多少个reduce就会有多少个索引文件,最后汇总这些文件得到最终的倒排索引。
实际工作中,为了增加map端读数据性能,并不是每个文档存放成单独一个文件,而是先把文档序列化成文件中的一行,这样每个文件可以存放多个文档内容,这就减少了小文件数,增加了map端的吞吐量。
参考链接:
【1】https://www.cnblogs.com/AndyStudy/p/9042032.html
【2】https://blog.csdn.net/tibowangt12/article/details/47337175
【3】https://blog.csdn.net/hguisu/article/details/7962350
【4】《这就是搜索引擎:核心技术详解》第三章
【5】https://www.jianshu.com/p/b3f987b0fbf1
【6】https://blog.csdn.net/uxiAD7442KMy1X86DtM3/article/details/79234981