信息检索导论学习笔记(4)
索引构建
回顾倒排索引的构建,首先,我们扫描一遍文档集合得到所有的词项—文档 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算法的比较
- SPIMI是直接在倒排记录表中增加一项。和那种一开始就整理出所有的词项 ID—文档 ID 并对它们进行排序的做法 (这正好是 BSBI中的做法)不同,由于不需要排序操作,因此处理的速度更快;
- SPIMI 使用词项而不是其ID,它将每个块的词典写入磁盘,对于下一个块则重新采用新的词典。只要硬盘空间足够大,SPIMI就能够索引任何大小的文档集。 由于词项的 ID 不需要保存。这样,每次单独的处理的块大小可以非常大,整个倒排索引的构建过程也因此会非常高效。由于事先并不知道每个词项的倒排记录表大小,算法一开始会分配一个较小的倒排记录表空间,每次当该空间放满的时候,就会申请加倍的空间 。这意味着一些空间被浪费,也正好抵消了不用保存词项 ID所省下的空间。然而,总体来说,在 SPIMI 中对块建立索引所需的内存空间仍然比 BSBI 少。
新的问题:实际当中,文档集通常都很大,在单台计算机上很难高效地构建索引。
分布式索引
MapReduce
Wiki地址:http://zh.wikipedia.org/wiki/MapReduce
java开源框架实现hadoop:http://hadoop.apache.org/
MapReduce是Google提出的一个软件架构(一个鲁棒的分布式计算框架),用于大规模数据集(大于1TB)的并行运算。概念“Map(映射)”和“Reduce(化简)”,及他们的主要思想,都是从函数式编程语言借来的,还有从矢量编程语言借来的特性。
当前的软件实现是指定一个Map(映射)函数,用来把一组键值对映射成一组新的键值对,指定并发的Reduce(化简)函数,用来保证所有映射的键值对中的每一个共享相同的键组。
分布式索引构建核心思想:
- 维持一台主机(Master)来指挥索引构建任务‐这台主机被认为是安全的
- 把整个任务分成易分配的子任务块,集群中的主控节点(master node)负责处理任务在工作节点上的分配和重分配。
实现步骤说明:
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/
posted on 2012-08-15 16:30 God bless you 阅读(4145) 评论(0) 编辑 收藏 举报