索引结构如果建立好了,可以提高搜素的速度,那么给定一个文档集合,索引是如何建立起来的?建立索引的方式有很多种,本文讲解比较实用的3种建立索引的方法。
两边文档遍历法
顾名思义,此方法需要对文档集合进行两边扫描,下图是这种方式的示意图。值得注意的一点是,此方法完全是在内存里完成索引的创建过程的,而另外两种方法则是通过内存和磁盘相互配合来完成索引建立任务的。
第一遍文档遍历
在第一遍扫描文档集合时,该方法并没有立即开始建立索引,而是收集一些全局的统计信息。比如文档集合包含的文档个数为N,文档集合内所包含的不同单词个数M,每个单词在多少个文档中出现过的信息DF。将所有单词对应的DF值全部相加,就可以知道建立最终索引所需的内存大小是多少,因为一个单词对应的DF值如果是10,说明有10个文档包含这个单词,那么这个单词对应的倒排列表应该包含10项内容,每一项记载某个文档的文档ID和单词在该文档对应的出现次数TF。
在获得了上述3类信息后,就可以知道最终索引的大小,于是在内存中分配足够大的空间,用来存储倒排索引内容。如下图所示,在内存中可以开辟连续存储区域,因为第一遍扫描已经获得了每个单词的DF信息,所以将连续存储区域划分成不同大小的片段,词典内某个单词根据自己对应的DF信息,可以通过指针,指向属于自己的内存片段的起始位置和终止位置,将来在第二遍扫描时,这个单词对应的倒排列表信息会被填充进这个片段中。
综上所述,第一遍扫描的目的是获得一些统计信息,并根据统计信息分配内存等资源,同时建立好单词对应的到排列表在内存中的位置信息,即主要做些资源准备工作。
第二遍文档遍历
在第二遍扫描的时候,开始真正建立每个单词的倒排列表信息,即对某个单词来说,获得包含这个单词的每个文档的文档ID,以及这个单词在文档中的出现次数TF,这样就可以不断填充第一遍扫描所分配的内存空间。当第二遍扫描结束的时候,分配的内存空间正好被填充满,而每个单词用指针所指向的内存区域“片段”,其起始位置和终止位置之间的数据就是这个单词对应的倒排列表。
经过两边扫描完成索引建立后,即可将内存的到排列表和词典信息写入磁盘,这样就完成了建立索引的过程。从上述流程可以看出,索引的建立完全是在内存中完成的,这就要求内存一定要足够大,否则如果文档集合太大,内存未必能满足需求。
从另外一个角度看,在建立索引的过程中,从磁盘读取文档并解析文档基本是最消耗时间的一个步骤,而两遍扫描法因为要对文档集合进行两边遍历,所以从速度上不占优势,在实施中采用这种方法的系统并不常见。而下面介绍的两种方法都是对文档集合进行一遍扫描,所以在速度方面明显占优。
排序法
两边遍历法在建立索引的过程中,对内存的消耗要求较高,不同的文档集合包含文档数量大小不同,其所需内存是不确定的。当文档集合非常大时,可能内存不够,导致无法建立索引,排序法对此做出了改进,该方法在建立索引的过程中,始终在内存中分配固定大小的内存空间,用来存放词典信息和索引的中间结果,当分配的空间被消耗光时,把中间结果写入磁盘,清空内存里中间结果所占空间,以便做下一轮存放索引中间结果的存储区。这种方法由于只需固定大小的内存,所以可以对任意大小的文档集合建立索引。(参考下图)
中间结果内存排序
上图是排序算法在内存中建立索引中间结果的示意图。读入文档后,对文档进行编号,赋予唯一的文档ID,并对文档内容解析。对于文档中出现的单词,通过查词典将单词转换为对应的单词ID,如果词典中没有这个单词,说明是第一次碰到,则赋予单词以唯一的单词ID并插入词典中。在完成了由单词映射为单词ID的过程之后,可以对该文档内每个单词建立一个(单词ID、文档ID、单词频率)三元组,这个三元组就是单词对应文档的倒排列表项,将这个三元组追加进中间结果存储区末尾。如果文档内的所有单词都经过如此处理,形成三元组序列的形式,则该文档被处理完成,开始依次处理下一文档,过程与此类似。
随着新的文档不断被处理完成,存储三元组集合的中间结果所占用的内存会越来越大,词典里所包含的新单词也越来越多,当分配的内存定额被占满时,该方法对三元组中间结果进行排序。排序的原则是:主键是单词ID,即首先要按照单词ID由小到大排序;次键是文档ID,即在相同单词ID的情况下,按照文档ID由小到大排序。通过以上方式,三元组变为有序形式。为了腾出内存空间,将排序好的三元组写入磁盘临时文件中,这样就空出内存来进行后续文档的处理。这里需要注意的是:在建立索引的过程中,词典是一直存储在内存中的,每次清空内存只是将中间结果写入磁盘。随着处理文档的增加,词典占用的内存会逐渐增加,由于分配内存是固定大小,而词典占用内存越来越大,也就是说,越往后,可用来存储三元组的空间是越来越少的。
之所以要对中间结果进行排序,主要是为了方便后续的处理,因为每一轮处理都会在磁盘产生一个对应的中间结果文件,当所有文档处理完成后,在磁盘中会有多少个中间结果文件,为了产生最终的索引,需要将这些中间结果文件合并。下图是对中间结果文件合并的示意图。
合并中间结果
如上图所示,在合并中间结果的过程中,系统为每个中间结果文件在内存中开辟一个数据缓冲区,用来存放文件的部分数据。因为在形成中间结果文件前,已经按照单词ID和文档ID进行了排序,所以进入缓冲区的数据已经是有序的。在合并过程中,将不同缓冲区中包含的同一个单词ID的三元组进行合并,如果某个单词ID的所有三元组全部合并完成,说明这个单词的倒排列表已经构建完成,则将其写入最终索引中,同时将各个缓冲区中对应这个单词ID的三元组内容清空,这样缓冲区就可以继续从中间结果文件中读入后续的三元组来进行下一个单词的三元组合并。当所有中间结果文件都依次被读入缓冲区,在合并完成后,就形成了最终的索引文件。
归并法
排序法分配固定大小内存来建立索引,所以无论要建立索引的文档集合有多大,都可以通过这种方法完成。但是如上所述,在分配的内存定额被消耗光时,排序法只是将中间结果写入磁盘,而词典信息一直在内存中进行维护,随着处理的文档越来越多,词典里包含的词典项越来越多,所以占用内存越来越大,导致后期中间结果可用内存越来越少。归并法对此做出了改进,即每次将内存中数据写入磁盘时,包括词典在内的所有中间结果信息都被写入磁盘。这样内存所有内容都可以被清空,后续建立索引可以使用全部的定额内存。
下图是归并法的示意图。其整体流程和排序法大致相同,也是分为两个大的阶段,首先在内存里维护中间结果,当内存占满后,将内存数据写入磁盘临时文件,第二阶段对临时文件进行归并形成最终索引。
尽管从整体流程看,和排序法大致相同,但是在具体实现方式上有较大差异。
首先,排序法在内存中存放的是词典信息和三元组数据,在建立索引的过程中,词典和三元组数据并没有直接的联系,词典只是为了将单词映射为单词ID。而归并法则是在内存中建立一个完整的内存索引结构,相当于对目前处理的文档子集单独在内存中建立起了一整套倒排索引,和最终索引相比,其结构和形式是相同的,区别只是这个索引只是部分文档的索引而非全部文档的索引。
其次,在将中间结果写入磁盘临时文件时,归并法会将整个内存的倒排索引写入临时文件,对于某个单词的倒排列表在写入磁盘文件时,将词典项放在列表最前端,之后跟随相应的倒排列表,这样依次将单词和对应的倒排列表写入磁盘文件,随后彻底清空所占内存。而排序法如上节所述,只是将三元组数据排序后写入磁盘临时文件,词典作为一个映射表一直存储在内存中。
最后的临时文件合并为最终索引的过程中,两者也有差异。排序法因为保存的是有序三元组信息,所以在合并时,是对同一单词的三元组依次进行合并;而归并法的临时文件则是每个单词对应的部分倒排列表,所以在合并时针对每个单词的到排列表进行合并,形成这个单词的最终倒排列表。另外,归并法在最后的合并过程中形成最终的词典信息。