Lucene 全文索引心得
2.7 控制索引过程
在对中小型文档集合进行索引的情况下,默认配置的Lucene能够很好地工作。但是,如果应用程序要处理很大的索引,你可能想在Lucene的索引过程中添加一些控制,以保证应用程序获得最佳的索引性能。例如,你可能正在索引几百万个文档,并想加速这一过程,使花费的时间从几个小时缩短到几分钟。而你的计算机有空闲的内存(RAM),所以你有必要知道如何使Lucene能更好地利用空闲的内存空间。Lucene中有几个参数允许你在索引期间控制Lucene的性能和资源的使用。
2.7.1 调整索引性能
在一个典型的索引应用中,程序性能的瓶颈存在于将索引文件写入磁盘的过程中。如果你曾经分析过索引应用程序,应该会发现运行该程序大部分的时间都耗费在操作索引文件的程序段上。因此,我们有必要使Lucene索引新对象和修改索引文件时变得更智能。
如图2.2所示,当新的Document对象添加到Lucene的索引里时,它们最初将被缓存在内存中,而不是立刻写入磁盘里。
图2.2 基于内存Document缓存有助于改善Lucene索引性能
这个缓存操作的目的是提高性能;而且值得庆幸的是,IndexWriter提供了几个变量,用于调节缓存的大小和磁盘写入的频率。表2.1对这些变量进行了总结。
表2.1 调整索引性能的参数
IndexWriter参数 |
系统属性 |
默认值 |
描述 |
mergeFactor |
org.apache.lucene.mergeFactor |
10 |
控制段的合并频率和大小 |
maxMergeDocs |
org.apache.lucene.maxMergeDocs |
Integer.MAX_VALUE |
限制每个段的文档数量 |
minMergeDocs |
org.apache.lucene.minMergeDocs |
10 |
控制索引时RAM使用的总量 |
IndexWriter 的mergeFactor参数在将Document对象写入磁盘之前,让你能控制在内存中存储Document对象的数量以及合并多个索引段的频率(索引段在附录B中介绍)。在将它们作为单个段(segments)写入磁盘之前,Lucene在内存中默认存储10个Document对象。 mergeFactor的值为10也意味着磁盘上的段数达到10的乘方时,Lucene会将这些段合并为一个段。
例如,如果你将mergeFactor设置为10,每当有10个Document对象添加到索引中时,Lucene就会在磁盘上创建一个新的段。当添加第10个大小为10个文档对象的段时,所有的这10个段会合并为大小为100的一个段。又当添加了10个这样大小为100的段之后,它们将被合并为包含1000个 Document对象的一个段,依次类推。因此,任意时刻索引中的段数都不会出现大于9的情况,并且每个合并后的段的大小均为10的乘方。
这一规则有一个小小的例外,与IndexWriter另外一个实例参数maxMergeDocs参数有关:当合并多个段时,Lucene要确保各个段中所包含的 Document对象的个数不超过maxMergeDocs的大小。例如,假定maxMergeDocs的值设置为1000。当添加第10000个 Document对象时,Lucene不会将多个段合并成大小为10000的一段,而是会创建大小为1000的第10个段,并且在添加每1000个 Document对象之后,仍将新段的大小保持为1000。
现在你已经明白了参数mergeFactor和maxMergeDocs是怎样工作的,也能推断出使用较大的mergeFactor参数会让Lucene使用更多的 RAM;而另一方面,这样也使磁盘写入数据的频率降低,因此加速了索引过程。较小的mergeFactor参数使内存的使用减少,并使索引更新的频率升高。这样做令数据的实时性更强,但是也降低了索引过程的速度。类似地,较大的maxMergeDocs参数更适用于批量索引的情况,较小的 maxMergeDocs参数更适用于交互性较强的索引。需要小心的是,因为较大的mergeFactor参数意味着较低频率的合并,会导致索引中的索引文件数增多。虽然这并不影响索引的性能,但是它会降低搜索速度,因为Lucene需要打开、读取和处理更多的索引文件。
minMergeDocs 是IndexWriter另一个影响索引性能的变量。在Document对象被合并为一段之前,minMergeDocs的值控制着缓存的 Document对象个数。minMergeDocs参数让你能够使用更多的内存空间来换取更快的索引。与mergeFactor不同的是,该参数并不影响磁盘上的索引段大小。
范例:IndexTuningDemo
为了对不同的mergeFactor 、maxMergeDocs和minMergeDocs参数值对索引速度的影响有更好的理解,请参看程序2.4中的IndexTuningDemo类。该类有四个命令行参数:添加到索引中的Document对象总数、mergeFactor的值、maxMergeDocs的值,以及 minMergeDocs的值。所有的四个参数必须被指定好,必须为整型,必须按照以上顺序输入。为了使代码更简洁,这里并没有对非法使用进行检查。
程序2.4 说明mergeFactor 、maxMergeDocs和minMergeDocs参数的使用
第一个参数表示添加到索引中的Documents对象个数;第二个参数表示mergeFactor的设定值,接着是maxMergeDocs的值;最后一个参数是minMergeDocs的值:
两次调用都创建了包含100000个Documents对象的索引,但是第一次调用花费了更长的时间(第一次74136毫秒,第二次68307毫秒)。因为第一次调用使用了mergeFactor参数的默认值10,和第二个调用(mergeFactor为100)相比,这样会导致Lucene更频繁地向磁盘写入 Document对象。下面我们使用不同的参数值对运行结果进行进一步的观察:
从以上运行情况可以看到,当操作系统为运行JVM(Java虚拟机)提供更多内存时,增大mergeFactor 和 minMergeDocs的值可以提高索引过程的速度。请注意,若将minMergeDocs设为10000会导致程序抛出 OutOfMemoryError的异常;如果为mergeFactor赋一个太大的值,也可能会导致这种异常。
注:增大mergeFactor 和 minMergeDocs的值只会在一定程度上提高索引的速度。值越大,占用的内存也越多,如果设定的值过大有可能会导致索引进程耗尽所有内存。记住 IndexTuningDemo(就像这个词本意已经表示出来的那样)仅仅是mergeFactor,maxMergeDocs和 minMergeDocs的一个使用范例。在这个类中,只是向索引添加由单个词构成的域的Documents对象,因而我们才可以为 mergeFactor设置一个非常大的值。
在实际应用中,使用Lucene的应用程序处理的索引往往是这样的:索引的文档含有多个域,并且每个域包含大量的文本。这些应用程序不能像这个例子一样,将 mergeFactor和minMergeDocs的值设得过大,除非机器拥有很大的内存空间——因为在索引时,内存容量的大小正是限制 mergeFactor和minMergeDocs值大小的主要因素。如果你要运行IndexTuningDemo,一定要谨记操作系统和文件系统的高速缓存对程序运行性能的影响。确保缓存已经可以使用并且每个配置都已经运行了几遍,特别是在其他空闲的机器上运行这样的应用程序时更应如此。此外,还要创建一个足够大的索引以使得这些缓存的影响达到最小。最后,需要再次强调的是:使用一个较大的mergeFactor值将会影响搜索性能,增大它的值的时候一定要谨慎。
注:一定要记住,给JVM提供一个较大的内存堆可以提高索引性能。进行这个操作往往是通过Java解释器中的-Xms和-Xmx参数共同来完成的。给JVM提供一个较大的堆,同样可以使mergeFactor和minMergeDocs参数接受更大的值。确保HotSpot,JIT或者其他类似的JVM选项已经被激活,这会对程序运行产生积极的影响。
修改UNIX对打开文件最大数的限制
我们注意到:虽然这三个变量可以改善索引的性能,但是它们也会影响Lucene使用的文件描述符(file descriptors)的数量,并且会在使用多文件索引时产生“打开文件过多”的异常(多文件索引和复合索引会在附录B中详细介绍)。如果出现了上述错误,首先应该检查一下索引目录的内容。若其中包含多个段,就可以使用IndexWriter类的optimize()方法来对这个索引进行优化,本书将在 2.8节详细地介绍如何优化索引。Lucene可以通过把多个段合并成单个段来优化索引。如果使用优化的方法还不能解决这个问题,或者是索引已经只有一个段了,则可以尝试通过增加机器所允许打开文件的最大数来解决。而这种增加打开文件上限的方法往往是在操作系统级别上进行操作,且操作方法因操作系统的不同而异。如果你在运行UNIX操作系统的机器上使用Lucene,就可以通过命令行来查看所能打开文件的最大数。在bash下,可以通过内置的ulimit 命令来查看当前设置。
在tcsh下,等价的命令是:
在bash下要改变数值,使用如下命令:
在tcsh下,使用如下命令:
要估算索引时需要打开文件的最大数目时,请记住Lucene将在索引过程中的任意时刻打开的文件的最大数目是:
例如,mergeFactor的默认值为10,当创建一个包含一百万个Document对象的索引,而Lucene在未优化的且只包含单个索引域的多文件索引上进行操作时,最多需要打开88个文件(open files)。这个数值可以由以下公式得到:
如果上述方法仍不能消除同时打开文件过多的问题,并且你正在使用多文件索引结构,此时应该考虑把原来的索引转换成复合结构的索引。就像附录B中所描述的那样,此方法能够进一步减少Lucene存取索引时所需要打开的文件数目。
注:如果可用的文件描述符已经全部用完,而你并没有对索引进行优化,此时可以考虑对索引做优化处理。
2.7.2 内存中的索引:RAMDirectory
在前面的章节中,我们提到Lucene把新加的文档先保存在内存中后才把它们存到磁盘上,并通过这种方法来进行内部缓冲。如果你正在使用 FSDirectory(Directory类的一个基于文件的具体实现),以上的缓存操作将是自动、透明地完成的。但是,你可能会希望对索引过程、内存使用情况和从内存缓冲区到磁盘传送文件的频率某方面进行更多的控制。此时可以把RAMDirectory作为一个内存缓冲器使用。
RAMDirectory和FSDirectory的对比
RAMDirectory 在内存中所进行的操作比FSDirectory在磁盘上所完成的工作要快得多。程序2.5中的代码创建了两个索引:一个基于FSDirectory,另一个则基于RAMDirectory。除此之外,其他部分都是相同的——每个索引都包含了1000个具有相同内容文档的对象。
程序2.5 RAMDirecotory的性能总是比FSDirectory优越
虽然,存在更好的方法构建测试标准(参看第6.5节中的例子,此例说明了如何使用JunitPerfto来测试搜索操作的性能),不过以上这个程序已经能够说明与 FSDirectory相比,RAMDirectory具有性能优势了。如果运行程序2.5中的测试程序,当不断地增大mergeFactor或者 minMergeDocs的值时,你就可以发现基于FSDirectory的索引速度会渐渐逼近基于RAMDirectory的索引速度。不过,你还可以发现,无论如何组合参数,基于FSDirectory的索引性能都不可能超越基于RAMDirectory的性能。
即使可以使用索引参数来使Lucene减少在磁盘上合并段(segments)的频率,基于FSDirectory的索引终究还是要把它们写入磁盘;而 RAMDirectory则完全不用向磁盘写入任何东西,这就是这两个Directory之间存在性能差别的根源。当然,这也就意味着一旦关闭了索引程序,基于RAMDirectory的索引就会随之消失。
通过把RAMDirectory作为一个缓冲器实现对索引的批处理
假设你在通过修改IndexWriter类的mergeFactor、maxMergeDocs和minMergeDocs参数已不足以提高性能的情况下,仍然希望进一步改进Lucene的索引操作性能,就可以把RAMDirector作为缓冲器,先将索引文件缓存在缓冲器中,再把数据写入基于 FSDirectory的索引中,以达到改善性能的目的。下面是一个简单的操作方法:
1.创建一个基于FSDirectory的索引;
2.创建一个基于RAMDirectory的索引;
3.向基于RAMDirecotry的索引中添加文档;
4.不定期把缓存在RAMDirectory中的所有数据写入FSDirectory;
5.转到第三步(谁说GOTO无用武之地?)。
我们可以把这个步骤清单转换成下列使用了Lucene API的伪代码:
这种方法可以在让你在任何需要的时候,随心所欲地将内存中的Document对象存储到磁盘上。比如你可以使用一个计数器,每添加N个 Document对象到基于RAMDirectory的索引时,计数器就会触发将缓存写入磁盘的操作。同样地,也可以使用一个计时器周期性地把 Document对象从内存写入磁盘,而不管需要转存的Document对象的数量。另一个更为复杂的方法是跟踪RAMDirectory的内存使用情况,进而判断是否要将内存中的Document对象写入磁盘,这样做是为了防止RAMDirectory索引体积变得过大。
无论使用哪种方法,最后你都要利用IndexWriter的addIndexes(Directory[])方法把基于RAMDirectory的索引和磁盘上的索引合并起来。这种方法把一组任何类型的Directory对象组合成为一个Directory对象,并将这个新生成的Directory索引存放在 IndexWriter构造函数中所指定的位置里。
并行索引多个索引文件
把 RAMDirectory作为一个缓冲器的思想还可以有更深入的应用,如图2.3所示。你可以创建一个多线程的索引程序,这个程序并行地使用多个基于 RAMDirectory的索引,每个线程中都有一个这样的索引,并利用IndexWriter的addIndexs (Directory[])方法把这些索引合并成一个单一的索引文件存储到磁盘上。
图2.3 一个使用多个基于RAMDirectory索引的多线程应用程序
此外,选择在何时以何种方式对进程进行同步、把它们的RAMDirectory对象合并成存放在磁盘上的单一索引都取决于你自己。当然,如果使用的是多磁盘系统,你还可以对基于磁盘的索引进行并行化处理,因为我们可以独立地对两个磁盘进行操作。
如果要将多个计算机连接到一个快速的网络里,会使用什么传输媒介呢,光纤如何?我们还可以通过使用由一组计算机组成的索引集群来达到并行索引的目的。一个复杂的索引程序应该能够并行地在多个计算机上创建存储于内存中的或基于文件系统的索引,并且能够周期性地向主服务器发送它们的索引,在主机中它们将被合并为一个更大型的索引。
图2.4中的体系结构中存在两个明显的缺陷:集中式的索引(主服务器上的索引)就是其中一个缺陷,此外,当索引节点增加时这个索引一定会成为制约软件性能的瓶颈。不管怎样,这应该还是能给你一些启示。当你学习如何使用Lucene对多个索引进行并行搜索,甚至是远程地使用索引(参看5.6节)时,你可以考虑通过 Lucene创建大型的分布式索引和对集群索引进行搜索。
图2.4 索引器节点集群把单个索引传送到大型的集中式索引服务器上
到目前为止,读者应该可以清楚地看到一些使用模式。内存的访问速度比硬盘要快:如果想在Lucene下获得更高的效率,那么你就应该在高速RAM中使用 RAMDirectory完成大部分的索引操作,尽量减少对索引的合并。如果你有充足的硬件资源,比如多个CPU、多个磁盘、或者多个机器,那么可以考虑并行地进行索引,并使用addIndexs (Directory[])方法把它们最终写入到一个索引中,即最终所要创建并搜索的索引。为了最大程度地利用这个解决方案,一定要确保在磁盘进行索引的线程或计算机都没有处于空闲状态,因为空闲就意味着需要消耗更多的时间。
在3.2.3小节中,我们将讨论完全相反的索引转换方向:在那里,将向你介绍如何把一个文件系统中的索引移入内存,这个问题将留到搜索的章节里再讨论,因为搜索操作才是把文件系统的索引移入内存的最佳动机。
2.7.3 限制域的大小:maxFieldLength
有些应用程序会索引事先不知道大小的文档。为了控制应用程序对内存和虚拟内存的使用,需要限制输入索引的文档的数量。而另一些应用程序则对已知大小的文档进行索引,不过它们可能只需要索引每个文档中的一部分内容。例如,你可能只需要对每个文件的前200个词进行索引。Lucene的IndexWriter类中包含有一个maxFieldLength变量,这个变量可以让用户有计划地对大文档域进行截取。当maxFieldLength取默认值10000 时,Lucene就只索引每个域的前10000个项。也就是说只有前10000个项可以被用于搜索,除此之外的所有部分都不会被Lucene索引。
为了把域的大小限制在1000个项以内,某个应用程序也许会把maxFieldLength设置为1000,但为了从根本上消除这个固定大小的限制,应用程序在处理 maxFieldLength时,应该把它设置成Integer.MAX_VALUE。MaxFieldLength的值可在索引过程中的任意时刻被修改,并对其后添加的所有文档起作用。改变该值的设置对已经索引过的域是无效的,因此所有已经按照较小的maxFieldLength值进行截取的域,仍将按照原先设定的大小进行截取。程序2.6展示了一个具体的实例。
程序2.6 利用maxFieldLength控制域大小
通过这个程序,你可以看到我们是如何实现对需要索引的项的数目进行控制的:
??首先我们用IndexWriter来索引前10个项;
?在添加完第一个Document对象之后,我们可以为bridges项找出一个与之相匹配的文档,因为它是此文档的文本“Amsterdam has lots of bridges”中的第五个单词;
?下面我们重新索引这个文档,指定IndexWriter对象只索引Document对象中各个域的第一个项;
?这样我们就找不到包含单词bridges的那个Document对象了,因为Lucene只索引第一个词,Amsterdam。因此,剩下的单词,包括bridges,都被忽略掉了。
爱瑞曼aeromind ZC0B4/超低价推出全棉工字背心长背心T恤