动态索引的建立
真实环境中,搜索引擎需要处理的文档集合往往都是动态集合,即在建好初始的索引后,不断有新文档进入系统,同时原先的文档集合内有些文档可能被删除或更改。例如我们的本地文件系统,当新建立一个文件后,那么搜索系统需要将其将及时纳入,删除某个文件也需要在很短的时间内在搜索结果里提现出来。大多数搜索引擎面临的应用场景都是类似的动态环境。
索引系统如何做到实时反映这种变化呢?动态索引可以实现这种实时性要求,下图是动态索引的示意图,在这种动态索引中,有3个关键的索引结构:倒排索引、临时索引和删除文档列表。
倒排索引就是对初始文档集合建立的索引结构,一般单词词典都存储在内存,对应的倒排列表存储在磁盘文件中,临时索引是在内存中实时建立的倒排索引,其结构和前述的倒排索引是一样的,区别在于词典和倒排列表都在内存中存储。当有新文档进入系统时,实时解析文件并将其加入到临时索引结构中。已删除文档列表则用来存储已被删除的文档的相应的文档ID,形成一个文档ID列表。这里值得注意的是:当一篇文档内容被更改,可以认为是旧文档先被删除,然后系统在增加一篇新的文档,通过这种间接方式实现对内容更改的支持。
当系统发现有新文档进入时,立即将其加入到临时索引结构中。有文档被删除时,则将其加入删除文档队列。文档被更改时,则将原始文档放入已删除队列,解析更改后的文档内容,并将其加入临时索引中。通过这种方式可以满足实时性的要求。
如果用户输入查询请求,则搜索引擎同时从倒排索引和临时索引中读取用户查询单词的倒排列表,找到包含用户查询内容的文档集合,并对两个集合进行合并,然后再用删除文档列表进行过滤,将搜索结果中那些已经被删除的文档从结果中过滤,形成最终的搜过结果,并返回给用户查看。这样就能实现动态环境下的准实时搜索功能。
动态索引更新策略
动态索引通过在内存中维护临时索引,可以实现对动态文档和实时搜索的支持。但是服务器的内存总是有限的,随着加入的文档越来越多,临时索引消耗的内存也会随着不断增加。当最初分配的内存被使用完时,要考虑将临时索引的内容更新到磁盘索引中,以释放内存空间来容纳后续的新进文档,此时要考虑率合理有效的索引更新策略。
常用的索引更新策略主要有四种:完全重建策略、再合并策略、原地更新策略及混合策略。
01完全重建策略
完全重建策略是一个很直接的方法,当新增文档达到一定数量,将新增文档和原先的老文档进行合并,然后利用建立静态索引的方式,对所有文档重新建立索引。新索引建立完成后,老的索引被遗弃释放,之后对用户查询的相应完全由新的搜索来负责,下图是这种策略的说明示意图。
静态索引的建立可参考【全程干货】搜索引擎是如何建立索引的?
因为重建索引需要较长的时间,在进行索引重建过程中,内存中仍然需要维护老的索引来应对用户的查询请求。只有当新索引建立完成后,才能释放旧的索引,将用户查询请求切换到新索引上。
这种重新策略比较适合小文档集合,因为完全重建索引的代价较高,但是目前主流商业搜索引擎一般都是采用此方式来维护索引更新的,这与互联网本身的特性有关。
02再合并策略
有新增文档进入搜索系统时,搜索系统在内存维护临时倒排索引来记录信息,当新增文档达到一定数量,或者指定大小的内存被消耗完,则把临时索引和老文档的倒排索引进行合并,以生成新的索引。下图是这种策略的示意图。
在实际的搜索系统中,在合并策略按照以下步骤进行索引内容的更新。
1)当新增文档进入系统,解析文档,之后更新内存中的临时索引,文档中出现的每个单词,在其倒排列表末尾追加到排列表项,这个临时索引可称为增量索引。
2)一旦增量索引将制定的内存消耗光,此时需要进行一次索引合并,即将增量索引和老文档的倒排列表索引进行合并,下图是合并过程示意图。这里需要注意的是:倒排文档里的倒排列表存放顺序已经按照索引单词字典顺序由低到高进行了排序,增量索引在遍历词典的时候也按照字典顺序由低到高排列,这样对老的倒排文件只需进行一遍扫描,并可顺序读取,减少了文件操作中比较耗时的磁盘寻道时间,可以有效地提高合并效率。
在合并过程中,需要一次遍历增量索引和老索引单词词典中包含的单词及其对应的倒排列表,可以用两个指针分别指向两套索引中的目前需要合并的单词(参考下图箭头所指),按照如下方式进行倒排列表的合并。
考虑增量索引的单词指针指向的单词,如果这个单词在词典序中小于老索引的单词指针指向的单词,说明这个单词在老索引中未出现过,则直接将这个单词对应的倒排列表写入新索引的倒排文件中,同时增量索引单词指针后移指向下一个单词。
如果两个单词指针指向的单词相同,则说明这个单词同时在增量索引和老索引中同时出现,则将老索引中这个单词的倒排列表写入新索引的倒排列表,然后把增量索引中这个单词的倒排列表追加到其后,这样就完成了这个单词所有倒排列表的合并。上图中箭头所指单词是搜索,说明此时合并到了该单词,因为在老的索引系统和增量索引中都包含这个单词,所以首先将老索引对应的倒排列表追加到新索引倒排文件末尾,之后将增量索引这个单词对应的倒排列表追加在后面,这样就完成了这个单词索引项的合并。两个索引的单词指针都移动到下一个单词继续进行合并。
如果某个单词只在老索引中出现过,即发现老索引的单词指针指向的单词,其词典序小于增量索引单词指针指向的单词,则直接将老索引中对应的倒排列表写入新索引倒排文件中。新索引的单词指针后移指向下一个单词,继续进行合并。
当两个索引的所有单词都遍历完成后,新索引建成,此时可以遗弃释放老索引,使用新索引来响应用户查询请求。
同样地,在按照这个策略合并索引的过程中,为了响应用户查询,在合并索引期间,需要继续使用老索引来响应用户查询请求。
再合并策略是一种效率非常高的索引更新策略,主要原因在于:在对老的倒排索引进行遍历时,因为已经按照索引单词的词典序由低到高排好顺序,所以可以顺序读取文件内容,减少磁盘寻道时间,这是其高效的根本原因。但是这种方法也有缺点,因为要生成新的倒排索引文件,所以对于老索引中的很多单词来说,尽管其倒排列表并未发生任何变化,但是也需要将其从老索引中读取出来并写入新索引中,这种对磁盘输入输出的消耗是没有太大必要且非常耗时的。
03原地更新策略
原地更新策略的基本出发点,可以认为是试图改进再合并策略的缺点。也就是说,在索引更新过程中,如果老索引的倒排列表没有变化,可以不需要读取这些信息,而只对那些倒排列表变化的单词进行处理。甚至希望能更进一步:即使老索引的倒排列表发生变化,是否可以只在其末尾进行追加操作,而不需要读取原先的倒排列表并重写到磁盘另一个位置?如果能够达到这个目标,明显可以大量减少磁盘读/写操作,提升系统执行效率。
为了达到上述目标,原地更新策略在索引合并时,并不生成新的索引文件,而是直接在原先老的索引文件里进行追加操作(参考下图),将增量索引里单词的倒排列表项追加到老索引相应位置的末尾,这样就可以达到上述目标,即只更新增量索引里出现的单词相关信息,其他单词相关信息不做变动。
但是这里存在一个问题:对于倒排文件中的相邻单词,为了查询时加快读取速度,其倒排列表一般是顺序存储的,这就导致了没有空余位置来追加新信息。为了能够支持追加操作,原地更新策略在初始建立的索引中,会在每个单词的倒排列表末尾预留一定的磁盘空间,这样,在进行合并时,可以将增量索引追加到预留空间中。
下图是这种合并策略的一个说明,老索引中每个单词的倒排列表末尾都预留出空余磁盘空间,以作为信息追加的存储区域。在对新增索引进行合并时,按照词典序,依次遍历新增索引中包含的单词,并对新增倒排列表的大小和老索引中相应预留空间大小进行比较,如果预留空间足够大,则将新增列表追加到老索引后面即可,如果预留空间不足以容纳新增倒排列表,那么此时需要在磁盘中找到一块完整的连续存储区,这个存储足以容纳这个单词的倒排列表,之后将老索引中的倒排列表读出并写入新的磁盘位置,并将增量索引对应的倒排列表追加到其后,这样就完成了一次倒排列表的“迁移”工作。
在下图所示的例子中,可以看出,单词“技术”和“引擎”在老索引中的预留空间足够大,所以对增量索引只需做追加写入即可,但是对于单词“搜索”来说,其预留空间不足以容纳新增倒排列表,所以这个单词的倒排列表需要迁移到另外一个连续存储区域中。
原地更新策略的出发点很好,但是实验数据表明其索引更新效率比再合并策略低,主要是出于以下两个原因。
在这种方法中,对倒排列表进行“迁移”是比较常见的操作,为了能够进行快速迁移,需要找到足够大的磁盘连续存储区,所以这个策略需要对磁盘可用空间进行维护和管理,而这种维护和查找成本非常高,这成为该方法效率的一个瓶颈。
对于倒排文件中的相邻索引单词,其倒排列表顺序一般是按照相邻单词的词典序存储的,但是由于原地更新策略对单词的倒排列表做数据迁移,某些单词及其对应倒排列表会从老索引中移出,这样就破坏了这种单词连续性,导致在进行索引合并时不能进行顺序读取,必须维护一个单词到其倒排文件相应位置的映射表,而这样做,一方面降低了磁盘读取速度,另外一方面需要大量的内存来存储这种映射信息。
04混合策略
混合策略的出发点是能够结合不同索引更新策略的长处,将不同的索引更新策略混合,以形成更高效的方法。
混合策略一般会将单词根据不同性质进行分类 ,不同类别单词,对其索引采取不同的索引更新策略。常见的做法时:根据单词的倒排列表长度进行区分,因为有些单词经常在不同的文档中出现,所以其对应的倒排列表较长,而有些单词很少见,则其倒排列表就较短。根据这一性质将单词划分为长倒排列表单词和短倒排列表单词。长倒排列表单词采取原地更新策略,而短倒排列表单词则采取再合并策略。
之所以这样做,是由于原地更新策略更适合长倒排列表单词,因为这种策略能够节省磁盘读/写次数,而长倒排列表单词的读/写开销明显要比短倒排列表单词大很多,所以如果采用原地更新策略,效果提现得比较显著。而大量短倒排列表单词读/写开销相对而言不算太大,所以利用再合并策略来处理,则其顺序读/写优势也能被充分利用。