Indri中的动态文档索引技术

戴维 译

摘要: Indri 动态文档索引的实现技术,支持在更新索引的同时处理用户在线查询请求。 文本搜索引擎曾被设计为针对固定的文档集合进行查询,对不少应用来说,这种机制工作得很好,然而对于诸于新闻,财经和桌面搜索而言,需要的是高效、经常性的更新索引。 以往支持动态文档集合的研究主要围绕增量索引方法,增量系统通过往已有的索引中追加大的文档集合来优化索引性能,但是不允许在增量索引的同时处理用户查询。 与以往的增量系统不同,Indri搜索引擎的最新版本支持动态文档集合,不需要通过加大文档集合大小来获取索引性能,同时Indri支持索引和查询的并发,允许用户在增量索引的同时进行查询。

1.介绍

尽管全文索引技术已经出现了几十年之久,但是直到互联网的出现,它才真正得到普及。现在,几乎每个互联网使用者都是搜索引擎用户,全文搜索技术被广泛地用于各种应用领域,如Web搜索,新闻搜索,以及时下流行的桌面搜索等。

搜索桌面(或硬盘)文件和e-mails对大多数信息检索系统而言是一个新的挑战。用户期望他们的e-mails即到即索引,文件在保存到磁盘的顷刻便被索引好。永远不要期望桌面搜索用户能忍受由于索引更新所带来的存储消耗。不管是祈祷还是咒骂,用户看到了全文搜索的好处,更为普遍的事实是人们越来越发现自己离不开搜索。

然后,更新一个全文索引是一个耗时的过程,现在的搜索引擎通过建立大量文档的倒排索引表来创建索引,一个倒排索引项包含一篇文章的无重复词语列表,以及这些词语的附加信息(如词语在文档中的位置,词性等)。一篇短小的包含100个左右不重复词语的文章,索引的时候需要更新100个倒排索引项。如果这些倒排索引是存储在磁盘上的话,更新操作将需要100次的磁盘寻址,在现有的硬件配置下,这需要一秒或更长的时间。当然,长文章耗时也越长。另外,一个随之而来的问题是,在更新索引的时候,搜索引擎是该让用户等待更新完成还是继续处理用户的查询请求呢?

Indri搜索引擎新的版本突破了上述限制。作为Lemur[1]项目研发的一部分,Indri支持结构化查询语言,采用语言建模方法[2],同时为了满足问答系统的需要,Indri还支持对结构化文档不同域进行查询。Indri第一个版本没有加入对增量索引的支持,但在最新的版本中允许对单个文档进行真正的实时索引。

一个信息检索系统要处理动态的文档集合,需要解决三个关键问题。首先,要能快速地添加和删除集合中的文档,这里的快速取决于一个桌面搜索用户愿意花多长的时间来等待索引的完成,也就是说对于单个文档而言响应应该是瞬时的。其次,系统要允许查询在任何时候都能得到响应,即使是在新文档添加进文档集合的同时。另外,系统要实用,在索引和检索性能上比不支持动态文档集合的检索系统更具有竞争性。

在Indri最新版本中,我们通过引入如下设计原则来实现上述目标:

内存结构—为了避免读写磁盘,尽可能长地把数据调入内存。

加锁机制—系统在尽可能小的时间段内对数据进行加锁互斥。

只读结构—为了减少对互斥锁的依赖,系统引入只读数据结构。

后台I/O—系统采用后台线程来和低速设备进行交互,以提高索引操作性能。

多版本结构—如果一个耗时长的操作需要获取可能已经更新的数据,系统将维持此数据的多个版本来减少互斥锁的使用。

上述原则的具体运用将在文章后续部分进行详细介绍。

2.相关工作

数据库研究团体已经花了几十年时间来研究数据获取并发技术。Ramakrishnan和Gehrke提出了一个通用的数据库原则[3],Gray和Reuter则进一步深入探讨了事务处理系统[4]。

尽管访问文档数据和访问数据库数据所遇到的问题类似,但它们之间仍然存在着显著的差别。在数据库系统中,用户特别关心的是数据的原子性,有一个经典得例子,一位银行顾客把d美元钱从一个储蓄户头转到一个支票账号,如果d美元先从储蓄户头减掉,那么这位顾客的总钱数就少了d美元;如果先往支票账号加入d美元,那么他的钱就凭空多了d美元。不管怎么样,在第一个账号改变的同时就会出现金额不一致的情况。于是,数据库中事件并发研究的一个主要任务就是确保数据库的一致性,即使是在系统操作失败的情况下也要如此。

在我们系统中可以确保文档的插入和删除是原子操作,没有用户会看到一个文档是部分被删除或者插入的。但我们不允许多个文档的插入和删除是原子的。既然文档之间很少像数据库中记录那样相互依赖,这就不会成为我们系统的主要限制。

虽然有上述的差异,我们仍然可以从数据库中得到借鉴。异步I/O和互斥技术被现代数据库系统广泛使用,我们也在索引系统中采用类似的多版本并发技术[5]。

信息检索研究团体没有忽略动态文档集合,他们把研究重点放在了增量系统上,这种系统通过一次性添加大批量文档到已有索引上来代替单个文档的高效添加。这种研究并没有考虑当更新索引时系统能否继续处理查询的问题。

Brown,Callan和Croft[6]研究了一种高效增量索引的方法。他们区别对待小于8k和大于8k的索引,当一个小的索引需要增大时,它将被拷贝到一个大的连续的倒排索引文件中。然而对于一个大的索引项则不需要移动,只需添加一个前向指针到新的存储段(segment)里面。这也使得倒排索引可以通过倒排索引文件串连起来。他们发现,当在7个簇中创建一个索引的时候,查询性能降低了6%。使用小的簇时代价偏高,在他们的模型中索引每簇大小为64M的文本所花的时间是索引每簇大小为1M的文本的8倍。

在最近的研究中,Lester,Zobel以及Williams[7]比较了三种索引策略:占位(in-place),合并(re-merge)以及重构(rebuild)。除了没有对连接链表进行优化外,in-place策略类似Brown所采用的方法。所有的倒排索引连续存放,如果没有足够的空间写入新数据的时候,已有数据必须被拷贝到别的地方。在re-merge策略中,新的文档簇被创建到单独的索引中,然后和已经存在的索引进行合并。rebuild策略则对已经构建的索引弃之不顾,在原始文档的基础上重新构建索引。他们研究发现,re-merge策略是最高效的更新索引的方法。但是,他们没有像Brow,Callan和Croft所作的那样,对预分配(pre-allocation)策略之间和处理大索引策略之间的差异进行比较。

Lester,Zobel和Williams提到,在使用最小的文档簇(10个文档)的情况下,表现最好的索引策略(in-place策略)在大约7秒的时间内更新了1G的索引。相对于别的策略而言,这已经是非常快了,但是,对于单个文档的索引更新来说,这并不是个理想的策略。

本文描述的方法类似Lucene搜索引擎,正常情况下,就像传统的批量索引一样,Lucene以分段(segment)的方式把数据写入磁盘。一旦数据被写入段中,他们就可以被查询到,并且不需要进行段的合并。这和Brown,Callan和Croft的连接链表方法有点类似,只是,把数据写入簇(batch)中需要更多的开销。为了获得更好的性能,许多文档必须被写入磁盘的一个簇(batch)中。

如果需要快速的响应,Lucene提供一个RAMDirectory类在内存中创建索引。添加一个文档到RAMDirectory很快,因为不需要进行磁盘I/O操作。一个文档一加入RAMDirectory便可以通过一个叫做IndexWriter的对象进行查询。这也解决了文档簇对于小文档集合的问题。然而,对于大小大于机器内存的文档集合,内存索引方式将不再可用,数据必须被写入磁盘,并根据用户对索引数据的定位方式来决定,哪些数据需要驻留内存,哪些数据应该写入磁盘。

在我们的工作中,当需要对文档进行快速存取时,Indri使用内存索引而不是批量索引。当需要同时处理查询请求的时候,Indri会立即决定什么时候该合并索引,而什么时候该把数据写入磁盘。

3.策略

3.1 内存结构

Indri采用两种类型的索引:内存索引和磁盘索引。内存索引驻留内存,而磁盘索引则存储在磁盘上。两种索引都能够处理查询,但只有内存索引能添加新的文档。Indri的磁盘索引结构是固定不变的,可以删除,但不能修改。

大多数的信息检索系统在磁盘上为所有文档创建一个单独的索引,而Indri在创建索引的同时还会生成不同用途的索引文件,这里我们使用“索引库repository”来指代一个文档集合对应的索引及其相关数据结构。

当在文本集合上创建索引库的时候,Indri把当前文档索引到活动的内存索引中。只要一个索引库处在打开写模式,就存在一个活动的内存索引准备接收文档。对于小的文档集合,索引数据直到索引库需要关闭的时候才写入磁盘。数据写入的同时,一个新的内存索引被创建,作为新的活动索引使用。

用上述方法构建一个索引相当于多个检索系统同时工作。文档通常一次性加入内存索引结构,只有当达到了内存限制时才被写入磁盘。在批量系统中,磁盘和内存结构不能独立工作,索引需要经过后续处理才能用于系统查询。

因为构建许多小的索引比构建一个单独的大索引更加高效,所以在磁盘上维持许多单独的小索引要更加有利。大的索引只有在需要合并小索引的时候才会出现,为了尽可能快地向系统中添加文档,简单地生成小的索引更有优势[7]。

然而,从一个单独的大索引中查询比在众多小索引中寻觅要快的多,主要的原因在于磁盘寻址时间,查询所需要的大量的磁盘寻址和索引数目之间具有线性的关系。因此,重查询负载的情况下,Indri将通过合并索引的方式来减少磁盘索引数目。

3.2 加锁机制

为了满足系统快速响应的需求,Indri必须快速地处理查询和加入文档。Indri以前的版本已经是一个有效的批量系统,可以很容易地对数据结构进行加锁,但是这也容易使查询或文档插入被长时间阻塞。为了保持快速的响应,必须确保系统中互斥锁的使用是在很短的时间内。

我们通过只允许活动内存索引可变来减少互斥锁的加锁时间。除了一些caches外,内存索引是系统加锁时需要处理的唯一结构。而内存索引的大小是受可用内存大小限制的,它的全部内容都驻留在内存中,即算对于复杂的查询,内存索引的响应也相当快。当然,加锁时间也可以通过减少内存限制来降低。

互斥锁也需要向内存索引中添加新的文档,为了减少加锁时间,我们确保每个文档在上锁前是被解析过的,只有当单个文档被索引的时候才加锁,然后开锁以处理查询。大多数的网页和新闻文档可以在小于1/100秒的时间内索引好,查询处理需要等待的时间相应也就很短。

当处理查询的时候有新文档到来,系统又正好处于加锁状态,此时,系统将禁止磁盘I/O操作。这样可以显著减少主线程在持有互斥锁的情形下任务调度混乱。

3.3 只读结构

为了减少系统在处理大卷数据时互斥锁加锁的时间,我们设法让大部分数据保持不变,对于磁盘数据来说这是基本准则。如果磁盘数据不是只读的,那么线程之间就需要在进行磁盘I/O读写时进行加锁,这可能导致加锁时数据的高度不一致。

既然磁盘数据在写入后便不允许更改,对于读取数据来说就不用加锁。加锁策略让Indri可以充分利用多处理器系统来提高性能。大部分的查询代码路径(query code path)仅仅需要只读锁就可以了,也就是说查询处理是高度并行的,尽管它看起来似乎受到磁盘子系统的并行限制。另外,由于文本解析和索引可以同时进行,索引过程也具有可并行性。

3.4 后台I/O

如果I/O操作在查询和索引时候不能执行,就需要引入异步I /O。Indri通过一直运行如下两个线程来实现异步I/O操作:

RepositoryMaintenanceThread

RepositoryLoadThread

这两个线程和特定的Repository相关联,如果多于一个Repository被打开,每个repository均需要对应的这样一对线程相关联。

RepositoryLoadThread执行两项任务。一是为查询和新加入的文档载入统计数据,这种数据载入和Unix进程载入有些类似,线程标明在过去的1,5以及15分钟内处理的查询和添加的文档数目,以帮助系统决定何时该把内存数据写入磁盘。

RepositoryLoadThread的另一项任务是检查系统的内存使用情况。如果系统使用的内存超过用户限制的25%,RepositoryLoadThread将挂起所有文档索引线程直到内存占用降下来为止,这可以防止系统在大批量文档加入的时候崩溃。对于多数可能的实时程序,如新闻播报,系统的运行决不能超出内存限制。

RepositoryMaintenanceThread把索引写入磁盘,它是唯一能把索引数据写入磁盘,并可以从磁盘删除数据的线程,在这个线程中不需要复杂的加锁机制。该线程每分钟激活5次以检查系统当前内存占用量,如果系统使用了过多的内存,它就开始把内存数据写入磁盘。

如上所述,创建新的磁盘索引并不是总有好处,因为许多小的磁盘索引结构对于大的磁盘索引结构来说需要更多的查询时间。为此,索引库维护线程RepositoryMaintenaceThread在写磁盘之前检查查询和文档的载入情况,如果查询相对于文档载入量更大的话,维护线程将进行索引合并而不是往磁盘中写入一个新的索引。

3.5 多版本结构

Indri Repository可能包含多个索引,Indri维护一个称之为index_state的结构,这个结构持有指向当前索引库中所有索引的指针。索引数据在两种情况下被写入磁盘:

内存索引MemoryIndex已经达到了它的内存限制;

存在过多的内存索引MemoryIndex,他们需要进行合并。

在上述两种情况下,Indri把数据写入磁盘,这些数据可能已经以别的形式存在于系统中,因此index_state需要进行修改以反映数据是否被删除。

一个解决办法是对index_state结构的读写进行互斥,然而,这种方法也可能导致在重负载情况下系统性能低下。

考虑Indri是运行在一个并行系统中,并且用户正在进行复杂的查询,这些查询每个都需要10分钟的处理时间。假如用户在两个独立的线程中提交查询,系统中就总是有两份同样的查询在运行。如果这些查询以5分钟为时间片轮流执行,A线程分别在一个小时的0,10,20,30,40和50分钟时开始运行,而B线程则分别在5,15,25,35,45和55分钟时开始执行。在一个小时开始的时候,我们对index_state的写进行加锁,当在B线程处理完一个查询前,A线程不允许处理它的下一个的查询,于是一个处理器将有5分钟的空闲时间,这是我们不希望看到的。

为了避免上述情况的出现,Indri在同一时间维持多个index_state结构,所有新的任务(如新的查询,文档的加入)使用新的index_state结构,而旧的任务继续使用旧的index_state结构,当没有用户需要使用index_state的时候,它将被删除。

在上面的例子中,这意味着线程B在使用它旧的index_state结构完成它的查询处理的同时,线程A使用新的index_state结构开始处理它的下一个查询。当线程B处理完毕当前查询,旧的index_state将不在有用户使用,从而被删除掉。

4.删除文档

Indri支持删除标记。删除标记是一种弱删除方式,只是简单地隐藏文档对于用户的可见性,而不是真正的删除。文档对应的索引数据并不会真正从倒排链表,有向链表或者压缩集合中删除掉,也就是说文档中词语的计数仍然保留在语料统计数据库中。

假设我们有一个文档集合A,以及它的一个子集B,首先创建A的索引I,然后从I中删除B对应的索引。我们只是通过把文档集合A-B添加到I’来创建一个相似的索引I’。由于包含了文档集B的数据,索引I比索引I’需要占用更多的磁盘空间。进一步,因为I和I’对应的语料统计库稍有差别,当在这两个索引上进行查询时,查询结果也会有所不同。基于如上原因,当使用Indri进行搜索的时候要谨慎地使用文档删除特性。

尽管实际应用中,删除是个很有用的特性,但单纯的删除用处不大。删除往往被用于进行文档更新(删除旧的版本,插入新的版本)。对于桌面搜索或新闻搜索而言,经常需要更新已有文档的错误(或者过时)版本,这就显得尤为重要。

我们采用一个简单的位图来标记文档的删除,当一个文档需要删除,就为其设置对应的比特位,任何不在位图中的比特位均假设没有被设置。因此,如果没有文档被删除,位图文件将是一个空文件。这个文件会一直扩充直到最后一个比特位被设置为非0。

查询时,每个文档均要在打分前对照位图进行检查,只有没有被标记为删除的文档才能进行查询计分。

5.总结

Indri现在可以在小于1秒的短时间片内完成文档索引并立即用于查询,这使得高速、并发访问新索引的文档所付出的代价足够小,以至Indri不需要采用特殊的批量和增量模式。Indri可以每小时索引约15G的Web数据,包括压缩和存储每个原始文档。

在这种性能下,我们已经实现了适合新闻过滤以及桌面搜索应用的检索系统,我们相信这是第一个具有如此高性能的开源系统。

Copyright@戴维 2005.8 于北京

参考文献:

[1] Trevor Strohman, Donald Metzler, Howard Turtle, and W. Bruce Croft, Indri: A language model-based serach engine for complex queries, IA 2005: Proceedings of the 2nd International Conference on Intelligence Analysis (to appear), 2005.

[2] Donald Metzler, Victor Lavrenko, and W. Bruce Croft, Formal multiple-bernoulli models for language modeling, Proceedings of ACM SIGIR 2004, 2004, pp. 540–541.

[3] Raghu Ramakrishnan and Johannes Gehrke, Database management systems, McGraw-Hill Higher Education, 2000.

[4] Jim Gray and Andreas Reuter, Transaction processing: Concepts and techniques, Morgan Kaufmann, 1993.

[5] Philip A. Bernstein and Nathan Goodman, Multiversion concurrency control˙theory and algorithms, ACM Trans. Database Syst. 8 (1983), no. 4, 465–483.

[6] Eric W. Brown, Fast evaluation of structured queries for information retrieval, SIGIR’95: Proceedings of the 18th annual international ACM SIGIR conference on Research and development in information retrieval_r(New York, NY, USA), ACM Press, 1995, pp. 30–38.

[7] Nicholas Lester, Justin Zobel, and Hugh E. Williams, In-place versus re-build versus re-merge: index maintenance strategies for text retrieval systems, Proceedings of the 27th conference on Australasian computer science, Australian Computer Society, Inc., 2004, pp. 15–23.

来源:Trevor Strohman Dynamic Collections in Indri

相关链接: http://newhaven.lti.cs.cmu.edu/indri/

Indri中的动态文档索引技术的更多相关文章

  1. 在DHTML中把整个文档的各个元素作为对象处理的技术是:()

    在DHTML中把整个文档的各个元素作为对象处理的技术是:() A.HTML B.CSS C.DOM D.Script(脚本语言) 解答:C DOM:文档对象模型

  2. WPF中使用流文档

    转载自:http://www.cnblogs.com/zlgcool/archive/2008/11/17/1335456.html WPF面向的是UI展现,而文本显示无疑是UI层中的重要功能之一.W ...

  3. DOM和SAX是应用中操纵XML文档的差别

    查看原文:http://www.ibloger.net/article/205.html DOM和SAX是应用中操纵XML文档的两种主要API.它们分别解释例如以下:          DOM.即Do ...

  4. MVC自定定义扩展点之ActionNameSelectorAttribute+ActionFilterAttribute 在浏览器中打开pdf文档

    仅仅演示 了ASP.MVC 5 下为了在在浏览器中打开pdf文档的实现方式之一,借此理解下自定义ActionNameSelectorAttribute+ActionFilterAttribute 类的 ...

  5. 【ElasticSearch学习】之一图读懂文档索引全过程

    ES索引过程详解: 1.客户端发送索引请求. 客户端向ES节点发送索引请求,以RestClient客户端发起请求为例: ES提供了Java High Level REST Client,用户可以通过R ...

  6. 如何在程序中给word文档加上标和下标

    如何在程序中给word文档加上标和下标 上标或下标是一个小于普通行格式的数字,图形,标志或者指示通常它的设置与行相比偏上或偏下.下标通常显示于或者低于基准线,而上标则高于.上标和下标通常被用于表达公式 ...

  7. C# 中使用Word文档对图像进行操作

    C# 中使用Word文档对图像进行操作 Download Files: ImageOperationsInWord.zip 简介 在这篇文章中我们可以学到在C#程序中使用一个Word文档对图像的各种操 ...

  8. ABBYY PDF Transformer+从文件选项中创建PDF文档的教程

    可使用OCR文字识别软件ABBYY PDF Transformer+从Microsoft Word.Microsoft Excel.Microsoft PowerPoint.HTML.RTF.Micr ...

  9. openoffice转换过程中遇到繁体字文档转换失败的问题

    今天发现上线的文档转换功能中存在一个文档转换不成功,查看后台日志标志文档无法加载成功,提示日志如下: INFO: connected Jul 08, 2015 2:50:33 PM com.artof ...

随机推荐

  1. 基于ArcEngine与C#的鹰眼地图实现

    鹰眼图是对全局地图的一种概略表达,具有与全局地图的空间参考和空间范围.为了更好起到空间提示和导航作用,有些还具备全局地图中重要地理要素,如主要河流.道路等的概略表达.通过两个axMapControl控 ...

  2. HTML5自带的原生定位

    使用谷歌的,与百度有偏差,一般不推荐使用   一.window.navigator.geolocation 验证浏览器是否支持 if (window.navigator.geolocation) { ...

  3. 每天一道LeetCode--119.Pascal's Triangle II(杨辉三角)

    Given an index k, return the kth row of the Pascal's triangle. For example, given k = 3,Return [1,3, ...

  4. 三道题(关于虚表指针位置/合成64位ID/利用栈实现四则运算)

    第一题 C++标准中,虚表指针在类的内存结构位置没有规定,不同编译器的实现可能是不一样的.请实现一段代码,判断当前编译器把虚表指针放在类的内存结构的最前面还是最后面.  第二题 在游戏中所有物品的实例 ...

  5. XML解析的例子

    ////  main.m//  homewoek////  Created by hehe on 15/9/9.//  Copyright (c) 2015年 wang.hehe. All right ...

  6. C++转到C#历程零基础知识(持续增加)

    1.命名空间.类和源文件的关系:几个源文件用同一个命名空间时候,那么这几个源文件中的各个类之间的调用时可行的.他不会根据你的源文件分离而分开,因为最终编译后生成的是dll,这里来看你的几个源文件是没有 ...

  7. 深入理解JavaScript中的this关键字

    1. 一般用处 2. this.x 与 apply().call() 3. 无意义(诡异)的this用处 4. 事件监听函数中的this 5. 总结 在JavaScript中this变量是一个令人难以 ...

  8. 委托和事件[delegate and event]_C#

    委托和事件: 1. 委托:一个能够表示方法的数据类型:它将方法作为对象封装起来,允许在运行时间接地绑定一个方法调用. 2. 声明委托数据类型: public delegate  bool Greate ...

  9. ASP.NET MVC3 使用kindeditor编辑器获取不到值

    做开发真的是会遇到各种问题,如果不亲自尝试,不动手,很难发现问题. 下面我们说下在MVC中的用法 1,首先引入js文件 <script type="text/javascript&qu ...

  10. javascrip笔记——图片加载

    var t_img; // 定时器 var isLoad = true; // 控制变量 // 判断图片加载状况,加载完成后回调 isImgLoad(function(){ // 加载完成 }); / ...