Lucene下进行高级全文索引

Lucene下进行高级全文索引

689个读者 原作者: Otis Gospodnetic  原文 译者: sinopower  11/22/2007 收藏本文 引用

lucene是一个用java开发的开源文本索引和检索API。为了进一步阅读本文中即将阐述的索引技术,你需要对lucene的索引结构有个大致的了解。正如我在本系列的以前的文章中提到的,一个典型的lucene索引存储在硬盘的文件系统的某个目录(directory)中。
lucene中一个索引的核心元素包括片段(segment),文档(document),字段(field),条目(term)等。每个索引(index)包含一到多个片段,每个片段包含一到多个文档,每个文档包含一到多个字段,每个字段包含一到多个条目,每个条目是对字段进行描述的键值对。一个片段由一系列的文件(file)组成,构成片段的文件的确切数字因不同的索引(index)而不同,取决于索引中字段的数目。同一片段的所有文件共享通用的前缀,通过不同的后缀区分他们。你可以把片段假想为一个子索引,虽然片段不是一个完全独立的索引。
-rw-rw-r--    1 otis     otis            4   Nov 22 22:43 deletable
-rw-rw-r-- 1 otis otis 1000000 Nov 22 22:43 _lfyc.f1
-rw-rw-r-- 1 otis otis 1000000 Nov 22 22:43 _lfyc.f2
-rw-rw-r-- 1 otis otis 31030502 Nov 22 22:28 _lfyc.fdt
-rw-rw-r-- 1 otis otis 8000000 Nov 22 22:28 _lfyc.fdx
-rw-rw-r-- 1 otis otis 16 Nov 22 22:28 _lfyc.fnm
-rw-rw-r-- 1 otis otis 1253701335 Nov 22 22:43 _lfyc.frq
-rw-rw-r-- 1 otis otis 1871279328 Nov 22 22:43 _lfyc.prx
-rw-rw-r-- 1 otis otis 14122 Nov 22 22:43 _lfyc.tii
-rw-rw-r-- 1 otis otis 1082950 Nov 22 22:43 _lfyc.tis
-rw-rw-r-- 1 otis otis 18 Nov 22 22:43 segments

示例1:只包含了一个片段的索引
注意所有属于该片段的文件(file)拥有相同的前缀:_lfyc。因为这个索引包含两个字段,你会注意到有两个
文件具有满足fN模式的后缀(例中:f1和f2),如果这个索引有3个字段,那么肯定有一个文件的名称是_lfyc.f3。
一旦索引完全建好了,片段的数目也就固定了,但是
该数目会在索引创建过程中变化。当新的文档(document)
加入到索引的时候,lucene会增加片段,并且片段的合并也频繁发生。在下一部分中会介绍如何控制片段的添加
和合并以便提高索引速度。
更多关于文件(files)创建索引(index)的信息可以查阅在lucene的站点中的文件格式的文档。你也可以在这篇文
章的
参考部分找到。
索引加速的要素
以前的文章中介绍了如何使用
LuceneIndexExample类索引文本。因为那个例子太基础了,没有必要考虑索引速度
问题。而如果你要在重要的应用中使用lucene,可能就想确定最佳索引性能。典型的文本检索的瓶颈发生在向硬盘
中写入索引文件的过程中。因此,在索引文档的过程中,我们需要指导lucene能智能的增加和合并索引片段。
当新的文档被加入到一个lucene索引中时,它们先被存储在内存中而不是立即写入到硬盘中,这是出于性能的考虑。
提高lucene索引性能的最简单的方法是调整
IndexWriter类的mergeFactor成员变量的值,这个配置值告诉lucene在
写入硬盘之前在内存中存储多少文档,还能控制多个片段
合并到一起的频率。默认值为10的情况下,在写入到硬盘
中的一个片段中之前,lucene会存储10个文档在内存中,同样在
mergeFactor默认值等于10的情况下,硬盘上片段的
数量达到10个的时候,lucene会合并这些片段到一个片段中(这时可能会抛出一个异常,我会在后面解释)。
例如,如果我们设置
mergeFactor=10,每加入10个文档(document)到索引(index)中就会在硬盘中添加一个片段
(segment),当第10个大小为10的片段(包含10个文档的片段)被创建时,这10个片段中包含的100个文档将会被合并为
一个片段,以此类推,当10个大小为100的片段被创建(确切说是被合并)时,这10个片段就会被合并成1个大小为1000
的片段,因此在任何时间,一个索引中都不会超过9个片段。
前面提到的异常需要另一个成员变量来处理:
maxMergeDocs。当合并片段的时候,lucene会确信创建的片段没有超过
阈值maxMergeDocs。例如:如果我们设置maxMergeDocs等于1000,当我们增加10000个文档的时候,lucene会创建第10
个大小为1000的片段并且每添加1000个文档保持片段的大小为1000而不是把10个大小为1000的片段为一个大小为10000
的片段。
maxMergeDocs的默认值是常量#MAX_VALUE,以我的经验,该值几乎不需要调整。
现在我解释一下
mergeFactor和maxMergeDocs如何工作,你会看到mergeFactor设置的越大,将会占用越多的内存资源,
当然会减少向磁盘中写入的次数,进而加快索引的速度。相反,
mergeFactor值设置的越小,内存占用就越少,会造成
索引被频繁的更新,使得索引保持比较新的状态,当然就会减慢索引进程。相似的,
maxMergeDocs越大,对批量索引
就越适合,相反,
maxMergeDocs值越小,越适合互动的索引。
为了更好的感觉
mergeFactor和maxMergeDocs对索引速度的不同影响,让我们来看看下面的IndexTuningDemo类,这个类
在命令行下有三个参数:要加入到索引中的文档数目,
mergeFactor配置项的值,maxMergeDocs配置项的值,所有的参数
必须要指定,必须为数字,并且必须按照这个顺序,为了使代码简短、清晰,没有对不正常调用的检查。
import org.apache.lucene.index.IndexWriter;
import org.apache.lucene.analysis.Analyzer;
import org.apache.lucene.analysis.StopAnalyzer;
import org.apache.lucene.document.Document;
import org.apache.lucene.document.Field;
/**
* 在临时文件夹中创建一个叫'index'的索引.
* 加入索引中的文档数, mergeFactor 和
* maxMergeDocs 必须 在命令行中按顺序指定
* - 这个类期望被以正确的方式调用.
*
* 注意: 在第一次运行的时候, 手动创建
* 'index'文件夹到临时目录中.
*/

public class IndexTuningDemo
{
public static void main(String[] args) throws Exception
{
int docsInIndex = Integer.parseInt(args[0]);

// create an index called 'index' in a temporary directory
String indexDir =
System.getProperty("java.io.tmpdir", "tmp") +
System.getProperty("file.separator") + "index";

Analyzer analyzer = new StopAnalyzer();
IndexWriter writer = new IndexWriter(indexDir, analyzer, true);

// set variables that affect speed of indexing
writer.mergeFactor = Integer.parseInt(args[1]);
writer.maxMergeDocs = Integer.parseInt(args[2]);

long startTime = System.currentTimeMillis();
for (int i = 0; i < docsInIndex; i++)
{
Document doc = new Document();
doc.add(Field.Text("fieldname", "Bibamus, moriendum est"));
writer.addDocument(doc);
}
writer.close();
long stopTime = System.currentTimeMillis();
System.out.println("Total time: " + (stopTime - startTime) + " ms");
}
}

下面是输出结果:

prompt> time java IndexTuningDemo 100000 10 1000000

Total time: 410092 ms

real 6m51.801s
user 5m30.000s
sys 0m45.280s

prompt> time java IndexTuningDemo 100000 1000 100000

Total time: 249791 ms

real 4m11.470s
user 3m46.330s
sys 0m3.660s

正如你看到的,两个调用都创建了一个包含100000文档的索引,可第一个花费的时间要长,这是因为第一个调用中
mergeFactor设定的是默认值10,使得lucene写入磁盘的频率比第二个(mergeFactor=1000)要高。
注意,这两个值在提升lucene索引性能的同时,同样会影响lucene所用的文件描述符的数量,能够导致抛出

"Too many open files"异常,如果发生这个错误,你首先需要考虑如果你能优化索引,那么这将在稍后进行阐述。
优化对多于一个片段的索引有帮助,如果优化索引没有解决这个问题,你应该尝试增加你的计算机中允许打开的最
大文件数的值,这个通常在操作系统级进行调整,并且调节方法因操作系统而异。如果你在UNIX系统下使用lucene,
你可以通过命令行获知允许打开的最大文件数。
在bash下你可以通过内建的ulimit命令:
prompt> ulimit -n

tcsh下,该命令对应为:
prompt> limit descriptors

bash下改变此值, 如下:

prompt> ulimit -n <允许打开的最大文件数>

tcsh下, 调整如下:

prompt> limit descriptors <允许打开的最大文件数>
为了为索引进程估计一个允许打开的最大文件数的设置值,记住lucene在索引过程中将要打开的文件的最大数可以
通过(1 + mergeFactor) * FilesPerSegment公式计算出来。
例如:在mergeFactor默认值等于10,并且索引1000000文档的前提下,在一个未经优化的索引中,lucene会打开110
个文件,在调用了IndexWriter的optimize()方法的时候,所有的片段将被合并为1个片段,将会减少lucene需要打
开文件的数目。

内存中索引
前面我提到了,新的文档加入索引首先先保存在内存中,而不是立即写入到硬盘上,你也看到了如何通过改变

IndexWriter成员变量控制保存到内存中的文件比率,lucene的分发包(源码)中包含了一个RAMDirctory类,
可以提供索引中更多控制,跟FSDirectory类一样,这个类也实现了Directory接口,只不过FSDirctory存储索引
文档到硬盘上,而RAMDirectory存储到内存中。
由于RAMDirectory不是把所有东西都写到硬盘上,因此速度比FSDirectory更快。但是,通常计算机中的内存容量
要比硬盘小,因此RAMDirectory对大规模的索引不太适合。
下面的
MemoryVsDisk 类将对如何使用RAMDirectory作为内存级的缓存以提升索引速度进行示范。

import org.apache.lucene.index.IndexWriter;
import org.apache.lucene.analysis.Analyzer;
import org.apache.lucene.analysis.StopAnalyzer;
import org.apache.lucene.document.Document;
import org.apache.lucene.document.Field;
import org.apache.lucene.store.Directory;
import org.apache.lucene.store.RAMDirectory;
import java.io.IOException;

/**
* 在临时文件夹中创建一个叫'index'的索引.
* 加入索引中的文档数, mergeFactor 和
* maxMergeDocs 必须 在命令行中按顺序指定
* - 这个类期望被以正确的方式调用.
*
* 注意: 在第一次运行的时候, 手动创建'index'文件夹到临时目录中.

* 另外, 如果第四个命令行参数是 '-r' 此类
* 将在最后写入磁盘之前先将所有文档索引在RAMDirectory中,
* 如果要使用常规的FSDirectory,请把第四个命令行参数设为'-f'
*
* 注意: 第一次运行时需要在临时文件夹中手动创建 'index'目录.
*/
public class MemoryVsDisk
{
public static void main(String[] args) throws Exception
{
int docsInIndex = Integer.parseInt(args[0]);

// 在临时文件夹中创建一个'index'索引
String indexDir =
System.getProperty("java.io.tmpdir", "tmp") +
System.getProperty("file.separator") + "index";

Analyzer analyzer = new StopAnalyzer();
long startTime = System.currentTimeMillis();

if ("-r".equalsIgnoreCase(args[3]))
{
// 如果指定 -r 参数, 使用 RAMDirectory
RAMDirectory ramDir = new RAMDirectory();
IndexWriter ramWriter = new IndexWriter(ramDir, analyzer, true);
addDocs(ramWriter, docsInIndex);
IndexWriter fsWriter = new IndexWriter(indexDir, analyzer, true);
fsWriter.addIndexes(new Directory[] { ramDir });
ramWriter.close();
fsWriter.close();
}
else
{
// 使用FSDirectory创建索引
IndexWriter fsWriter = new IndexWriter(indexDir, analyzer, true);
fsWriter.mergeFactor = Integer.parseInt(args[1]);
fsWriter.maxMergeDocs = Integer.parseInt(args[2]);
addDocs(fsWriter, docsInIndex);
fsWriter.close();
}

long stopTime = System.currentTimeMillis();
System.out.println("Total time: " + (stopTime - startTime) + " ms");
}

private static void addDocs(IndexWriter writer, int docsInIndex)
throws IOException
{
for (int i = 0; i < docsInIndex; i++)
{
Document doc = new Document();
doc.add(Field.Text("fieldname", "Bibamus, moriendum est"));
writer.addDocument(doc);
}
}
}


使用 FSDirectory创建一个包含 10,000 文档的索引 , 如下:

prompt> time java MemoryVsDisk 10000 10 100000 -f

Total time: 41380 ms
real 0m42.739s
user 0m36.750s
sys 0m4.180s

使用 RAMDirectory,更快的创建一个同样大小的索引,调用 MemoryVsDisk 如下:

prompt> time java MemoryVsDisk 10000 10 100000 -r
Total time: 27325 ms
real 0m28.695s
user 0m27.920s
sys 0m0.610s

注意:通过选择一个更合适的mergeFactor值,你可以达到相同的甚至更快的索引速度

prompt> time java MemoryVsDisk 10000 1000 100000 -f
Total time: 24724 ms
real 0m26.108s
user 0m25.280s
sys 0m0.620s

在你调节
mergeFactor值的时候要注意,如果超过了JVM运行的最大内存的阈值,将会抛出java.lang.OutOfMemoryError异常
最后,不要忘了,通过为JVM分配足够多的内存,可以极大的提升所有java程序的运行效率。
prompt> time java -Xmx300MB -Xms200MB MemoryVsDisk 10000 10 100000 -r

Total time: 15166 ms
real 0m17.311s
user 0m15.400s
sys 0m1.590s

合并索引
当你通过调节
IndexWriter类的mergeFactormaxMergeDocs成员变量提升lucene的索引速度已经满足不了需要的时候,
你可以使用
RAMDirectory创建内存级的索引,你也可以创建多线程的索引程序,使用多个基于RAMDirectory的索引进行
并行处理,每一个在一个线程中,通过使用
IndexWriter的addIndexes(Directory[])方法在硬盘上把他们合并为一个索引。
再往更深的层次考虑,一个复杂的索引应用可以在多个并行的计算机上创建基于内存的索引。

在多线程的环境下构建索引
多线程或者进程可以同时对同一索引进行检索,而同一时间只能有一个线程或进程对同一索引进行修改。如果你的多线程的
应用使用多个线程向同一索引中加入文档,你必须将他们对
IndexWriter.addDocument(Document)方法的调用序列化,放任
非序列化的调用会导致修改索引时发生冲突,使lucene抛出意想不到的异常。另外,为了防止滥用,lucene使用文件锁禁止
多线程或者进程在同一时间对同一索引目录创建
IndexWriter类。
例如,下面的代码

import org.apache.lucene.index.IndexWriter;
import org.apache.lucene.analysis.Analyzer;
import org.apache.lucene.analysis.StopAnalyzer;

/**
* 示范Lucene通过加锁避免多进程在同一时间向同一索引执行写操作
* 注意: 在第一次运行之前,需要在临时文件夹中手动创建'index'目录.
*/
public class DoubleTrouble
{
public static void main(String[] args) throws Exception
{
// create an index called 'index' in a temporary directory
String indexDir =
System.getProperty("java.io.tmpdir", "tmp") +
System.getProperty("file.separator") + "index";

Analyzer analyzer = new StopAnalyzer();
IndexWriter firstWriter = new IndexWriter(indexDir, analyzer, true);

// the following line will cause an exception
IndexWriter secondWriter = new IndexWriter(indexDir, analyzer, false);

// the following two lines will never even be reached
firstWriter.close();
secondWriter.close();
}
}

会导致下面的异常:
Exception in thread "main" java.io.IOException:
Index locked for write: Lock@/tmp/index/write.lock
at org.apache.lucene.index.IndexWriter.<init>(IndexWriter.java:145)
at org.apache.lucene.index.IndexWriter.<init>(IndexWriter.java:122)
at DoubleTrouble.main(DoubleTrouble.java:23)

索引优化
在这篇文章中我已经提到优化索引很多次了,可我还没有详细的对此进行解释。若要优化索引,你需要调用IndexWriter实例的
optimize()方法,调用发生时,所有内存中的文档将被写入到硬盘上,所有的片段将被合并为一个,减少索引中的文件数目,
不过,索引优化不会提高索引进程的性能,相反,还会使得索引的速度减慢,即便如此,为了使打开文件的数目得到控制,优化
有时是很有必要的。例如,在索引进程工作的时候优化索引可能需要考虑索引进程和检索进程并行工作的情况,虽然这两个进程都
单独维护自己打开的文件集。一个好的规则是,如果不久会有更多的文档被加入到索引中,就要避免调用optimize()方法,另一方面,
如果你知道该索引会在很长一段时间内不被修改,该索引只是被检索,你需要优化它,这样会减少片段的数量
(在硬盘上的文件数),
于是就会提升检索性能,在检索时,lucene打开的文件越少,检索速度越快。
下面使用
IndexOptimizeDemo类讲解optimize起到的效果:

import org.apache.lucene.index.IndexWriter;
import org.apache.lucene.analysis.Analyzer;
import org.apache.lucene.analysis.StopAnalyzer;
import org.apache.lucene.document.Document;
import org.apache.lucene.document.Field;

/**
* Creates an index called 'index' in a temporary directory.
* If you want the index to optimize the index at the end use '-o'
* command line argument. If you do not want to optimize the index
* at the end use any other value for the command line argument.
* This class expects to be called correctly.
*
* Note: before running this for the first time, manually create the
* directory called 'index' in your temporary directory.
*/
public class IndexOptimizeDemo
{
public static void main(String[] args) throws Exception
{
// create an index called 'index' in a temporary directory
String indexDir =
System.getProperty("java.io.tmpdir", "tmp") +
System.getProperty("file.separator") + "index";

Analyzer analyzer = new StopAnalyzer();
IndexWriter writer = new IndexWriter(indexDir, analyzer, true);

for (int i = 0; i < 15; i++)
{
Document doc = new Document();
doc.add(Field.Text("fieldname", "Bibamus, moriendum est"));
writer.addDocument(doc);
}
if ("-o".equalsIgnoreCase(args[0]))
{
System.out.println("Optimizing the index...");
writer.optimize();
}
writer.close();
}
}

从类的代码中你可以看到,只有-o参数被指定时创建的索引才被优化,如果要创建一个没有被优化的索引,如下:

prompt> java IndexOptimizeDemo -n

-rw-rw-r-- 1 otis otis 10 Feb 18 23:50 _a.f1
-rw-rw-r-- 1 otis otis 260 Feb 18 23:50 _a.fdt
-rw-rw-r-- 1 otis otis 80 Feb 18 23:50 _a.fdx
-rw-rw-r-- 1 otis otis 14 Feb 18 23:50 _a.fnm
-rw-rw-r-- 1 otis otis 30 Feb 18 23:50 _a.frq
-rw-rw-r-- 1 otis otis 30 Feb 18 23:50 _a.prx
-rw-rw-r-- 1 otis otis 11 Feb 18 23:50 _a.tii
-rw-rw-r-- 1 otis otis 41 Feb 18 23:50 _a.tis
-rw-rw-r-- 1 otis otis 4 Feb 18 23:50 deletable
-rw-rw-r-- 1 otis otis 5 Feb 18 23:50 _g.f1
-rw-rw-r-- 1 otis otis 130 Feb 18 23:50 _g.fdt
-rw-rw-r-- 1 otis otis 40 Feb 18 23:50 _g.fdx
-rw-rw-r-- 1 otis otis 14 Feb 18 23:50 _g.fnm
-rw-rw-r-- 1 otis otis 15 Feb 18 23:50 _g.frq
-rw-rw-r-- 1 otis otis 15 Feb 18 23:50 _g.prx
-rw-rw-r-- 1 otis otis 11 Feb 18 23:50 _g.tii
-rw-rw-r-- 1 otis otis 41 Feb 18 23:50 _g.tis
-rw-rw-r-- 1 otis otis 22 Feb 18 23:50 segments


实例2: 一个没有被优化的索引通常包含比第一个更多的文件,这个索引包含了两个片段

如果要对索引进行完全优化,如下:
prompt> java IndexOptimizeDemo -o
-rw-rw-r-- 1 otis otis 4 Feb 18 23:50 deletable
-rw-rw-r-- 1 otis otis 15 Feb 18 23:50 _h.f1
-rw-rw-r-- 1 otis otis 390 Feb 18 23:50 _h.fdt
-rw-rw-r-- 1 otis otis 120 Feb 18 23:50 _h.fdx
-rw-rw-r-- 1 otis otis 14 Feb 18 23:50 _h.fnm
-rw-rw-r-- 1 otis otis 45 Feb 18 23:50 _h.frq
-rw-rw-r-- 1 otis otis 45 Feb 18 23:50 _h.prx
-rw-rw-r-- 1 otis otis 11 Feb 18 23:50 _h.tii
-rw-rw-r-- 1 otis otis 41 Feb 18 23:50 _h.tis
-rw-rw-r-- 1 otis otis 15 Feb 18 23:50 segments


示例 3: 一个完全优化的索引只包含了一个片段.

结论

该文章讨论了lucene索引的基本结构并且示范了提高索引性能的一些技术,你还可以认识到在多线程环境下进行索引的一些潜在的问题,如何优化索引,对索引进程有什么影响,这些知识可以让你更好的控制lucene的索引进程以便提高它的性能,下篇文章中将介绍lucene的文本检索能力.

参考

Otis Gospodnetic 是 Apache Jakarta 项目组的活跃会员, Apache Jakarta 项目管理委员会的成员, Lucene 的开发者, jGuru's Lucene FAQ的维护者.

原文地址:https://www.cnblogs.com/imxh/p/1153863.html