回顾倒排索引的构建,首先,我们扫描一遍文档集合得到所有的词项—文档 ID 对。然后,我们以词项为主键、文档 ID 为次键进行排序。最后,将每个词项的文档 ID组织成倒排记录表, 并计算诸如词项频率或者文档频率的统计量。对于小规模文档集来说,上述过程均可在内存中完成。对于大规模文档集,由于内存不足,我们必须使用基于磁盘的外部排序算法(external sorting algorithm) 。
BSBI(blocked sort-based indexing algorithm,基于块的排序索引算法)
第 1 步,将文档集分割成几个大小相等的部分,如倒排记录累积到10,000,000条;
第 2 步,将每个部分的词项 ID—文档 ID 对在内存中排序;
第 3 步,将中间产生的临时排序结果存放到磁盘中;
第 4 步,将所有的中间文件合并成最终的索引。
注:该算法中有一个关键决策就是确定块的大小
算法实现步骤说明:为使索引构建过程效率更高,我们将词项用其 ID 来代替,每个词项的 ID 是唯一的序列编号。我们可以在处理文档集之余将词项映射成其ID。即该算法将文档解析成词项 ID—文档 ID对,并在内存中一直进行解析处理,直到累积至放满一个固定大小的块空间(如倒排记录累积到10,000,000条)为止。我们选择合适的块大小,使之其能方便加载到内存并允许在内存中快速排序。排序后的块转换成倒排索引格式后写入磁盘。算法实现的最后一步是:将若干个块索引同时合并成一个索引文件。合并时,同时打开所有块对应的文件,内存中维护了为若干个块准备的读缓冲区和一个为最终合并索引准备的写缓冲区。每次迭代中,利用优先级队列(即堆结构)或者类似的数据结构选择最小的未处理词项 ID 进行处理。读入该词项的倒排记录表并合并,合并结果写回磁盘中。
BSBI的问题
前提假设是词典可以在内存放下,并需要一种将词项映射成其 ID 的数据结构。对于大规模的文档集来说,该数据结构会很大以致在内存中难以存放。实际上,倒排记录表可以直接采用term,docID 方式而不是termID,docID方式,但是此时中间文件将会变得很大。
SPIMI(single-pass in-memory indexing,内存式单遍扫描索引算法)
1.算法逐一处理每个词项—文档 ID 对
2.如果词项是第一次出现,那么将之加入词典(最好通过哈希表来实现) ,同时建立一个新的倒排记录表
3.如果该词项不是第一次出现则直接在倒排记录表中增加一项
4.由于倒排记录表是动态增长的,算法事先并不知道每个词项的倒排记录表大小,故一开始会分配一个较小的倒排记录表空间,每次当该空间放满的时候,就会申请加倍的空间
5.基于上述步骤可以对每个块生成一个完整的倒排索引,为加快最后的合并过程,要对词项进行排序操作,然后写入磁盘形成单块索引
6.这些独立的索引最后合并一个大索引
与BSBI算法的比较
新的问题:实际当中,文档集通常都很大,在单台计算机上很难高效地构建索引。
分布式索引
MapReduce
Wiki地址:http://zh.wikipedia.org/wiki/MapReduce
java开源框架实现hadoop:http://hadoop.apache.org/
MapReduce是Google提出的一个软件架构(一个鲁棒的分布式计算框架),用于大规模数据集(大于1TB)的并行运算。概念“Map(映射)”和“Reduce(化简)”,及他们的主要思想,都是从函数式编程语言借来的,还有从矢量编程语言借来的特性。
当前的软件实现是指定一个Map(映射)函数,用来把一组键值对映射成一组新的键值对,指定并发的Reduce(化简)函数,用来保证所有映射的键值对中的每一个共享相同的键组。
分布式索引构建核心思想:
实现步骤说明:
Map阶段:
主控节点将一个数据片分配给一台空闲的分析器(Parser),分析器一次读一篇文档然后输出(term,docID)对,然后分析器将这些(term,docID)对又分成j个词项分区,每个分区按照词项首字母进行划分。E.g., a‐f, g‐p, q‐z (这里j = 3)
Reduce阶段:
主控节点将一个词项分区分配给一台空闲的倒排器(Inverter),倒排器收集对应某一词项分区(e.g., a-f分区)所有的(term,docID) 对(即倒排记录表),排序并写进倒排记录表
基于MapReduce的索引构建示图(Casesar简写成 C,conquered 简写成 c’ed)
注:分布式索引是个极其复杂的问题,主控节点任务分配的控制,分布式集群网络通讯的数据传递问题等等
新问题:以上我们都假设文档集是静态的,这对于很少甚至永远不会改变的文档集(如《圣经》或莎士比亚的著作)来说没有任何问题。然而,大部分文档集会随文档的增加、删除或更新而不断改变。这也意味着需要将新的词项加入词典,并对已有词项的倒排记录表进行更新。
动态索引构建
简单办法:
最简单的索引更新办法就是周期性地对文档集从头开始进行索引重构。如果随时间的推移文档更新的次数不是很多,并且能够接受对新文档检索的一定延迟,再加上如果有足够的资源能够支持在建立新索引的同时让旧索引继续工作, 那么周期性索引重构不失为一种较好的选择。
引入辅助索引的解决办法:
同时保持两个索引:一个是大的主索引,另一个是小的用于存储新文档信息的辅助索引(auxiliary index) ,后者保存在内存中。检索时可以同时遍历两个索引并将结果合并。每当辅助索引变得很大时,就将它合并到主索引中。文档的删除操作记录在一个无效位向量(invalidation bit vector)中,在返回结果之前可以利用它过滤掉已删除文档。某篇文档的更新通过先删除后重新插入来实现。
主辅索引合并分析:
对数索引合并(lucene最早应用)
同以往一样,内存中的辅助索引(我们称之为Z0)最多能容纳n个倒排记录。当达到上限n时, Z0中的n个倒排记录会移到一个建立在磁盘上的新索引I0中。 当Z0下一次放满时,它会和I0合并,并建立一个大小为 2×n的索引Z1。Z1可以以I1的方式存储(如果I1不存在)或者和I1合并成Z2(如果I1已存在)。上述过程可以持续下去。对搜索请求进行处理时,我们会利用内存的索引Z0和现有的磁盘上的有效索引Ii进行处理,并将结果合并。看到这里,对二项式堆结构比较熟悉的读者会发现它与刚才提到的对数合并下的倒排索引结构之间具有很高的相似性。
LinkedIn的java开源实时索引框架Zoie:http://javasoze.github.com/zoie/