Lucene建立Index的过程:
1. 抽取文本.
比如将PDF以及Word中的内容以纯文本的形式提取出来.Lucene所支持的类型主要为String,为了方便同时也支持Date 以及Reader.其实如果使用这两个类型lucene会自动进行类型转换.
2. 文本分析.
Lucene将针对所给的文本进行一些最基本的分析,并从中去除一些不必要的信息,比如一些常用字a ,an, the 等等,如果搜索的时候不在乎字母的大小写, 又可以去掉一些不必要的信息.总而言之你可以把这个过程想象成一个文本的过滤器,所有的文本内容通过分析, 将过滤掉一些内容,剩下最有用的信息.
3. 写入index.
和google等常用的索引技术一样lucene在写index的时候都是采用的倒排索引技术(inverted index.) 简而言之,就是通过某种方法(类似hash表?)将常见的”一篇文档中含有哪些词”这个问题转成”哪篇文档中有这些词”. 而各个搜索引擎的索引机制的不同主要在于如何为这张倒排表添加更准确的描述.比如google有名的PageRank因素.Lucene当然也有自己的技术,希望在以后的文章中能为大家加以介绍.
在上一篇文章中,使用了最基本的建立索引的方法.在这里将对某些问题加以详细的讨论.
1. 添加Document至索引
上次添加的每份文档的信息是一样的,都是文档的filename和contents.
doc.Add(Field.Keyword("filename", file.FullName));
doc.Add(Field.Text("contents", new StreamReader(file.FullName)));
在Lucene中对每个文档的描述是可以不同的,比如,两份文档都是描述一个人,其中一个添加的是name, age 另一个添加的是id, sex ,这种不规则的文档描述在Lucene中是允许的.
还有一点Lucene支持对Field进行Append , 如下:
string baseWord = "fast";
string synonyms[] = String {"quick", "rapid", "speedy"};
Document doc = new Document();
doc.Add(Field.Text("word", baseWord));
for (int i = 0; i < synonyms.length; i++)
doc.Add(Field.Text("word", synonyms[i]));
这点纯粹是为了方便用户的使用.在内部Lucene自动做了转化,效果和将它们拼接好再存是一样.
2. 删除索引中的文档
这一点Lucene所采取的方式比较怪,它使用IndexReader来对要删除的项进行标记,然后在Reader Close的时候一起删除.
这里简要介绍几个方法.
[TestFixture]
public class DocumentDeleteTest : BaseIndexingTestCase // BaseIndexingTestCase中的SetUp方法 //建立了索引其中加入了两个Document
{
[Test]
public void testDeleteBeforeIndexMerge()
{
IndexReader reader = IndexReader.Open(dir); //当前索引中有两个Document
Assert.AreEqual(2, reader.MaxDoc()); //文档从0开始计数,MaxDoc表示下一个文档的序号
Assert.AreEqual(2, reader.NumDocs()); //NumDocs表示当前索引中文档的个数
reader.Delete(1); //对标号为1的文档标记为待删除,逻辑删除
Assert.IsTrue(reader.IsDeleted(1)); //检测某个序号的文档是否被标记删除
Assert.IsTrue(reader.HasDeletions()); //检测索引中是否有Document被标记删除
Assert.AreEqual(2, reader.MaxDoc()); //当前下一个文档序号仍然为2
Assert.AreEqual(1, reader.NumDocs()); //当前索引中文档数变成1
reader.Close(); //此时真正从物理上删除之前被标记的文档
reader = IndexReader.Open(dir);
Assert.AreEqual(2, reader.MaxDoc());
Assert.AreEqual(1, reader.NumDocs());
reader.Close();
}
[Test]
public void DeleteAfterIndexMerge() //在索引重排之后
{
IndexReader reader = IndexReader.Open(dir);
Assert.AreEqual(2, reader.MaxDoc());
Assert.AreEqual(2, reader.NumDocs());
reader.Delete(1);
reader.Close();
IndexWriter writer = new IndexWriter(dir, GetAnalyzer(), false);
writer.Optimize(); //索引重排
writer.Close();
reader = IndexReader.Open(dir);
Assert.IsFalse(reader.IsDeleted(1));
Assert.IsFalse(reader.HasDeletions());
Assert.AreEqual(1, reader.MaxDoc()); //索引重排后,下一个文档序号变为1
Assert.AreEqual(1, reader.NumDocs());
reader.Close();
}
}
当然你也可以不通过文档序号进行删除工作.采用下面的方法,可以从索引中删除包含特定的内容文档.
IndexReader reader = IndexReader.Open(dir);
reader.Delete(new Term("city", "Amsterdam"));
reader.Close();
你还可以通过reader.UndeleteAll()这个方法取消前面所做的标记,即在read.Close()调用之前取消所有的删除工作
3. 更新索引中的文档
这个功能Lucene没有支持, 只有通过删除后在添加来实现. 看看代码,很好理解的.
[TestFixture]
public class DocumentUpdateTest : BaseIndexingTestCase
{
[Test]
public void Update()
{
Assert.AreEqual(1, GetHitCount("city", "Amsterdam"));
IndexReader reader = IndexReader.Open(dir);
reader.Delete(new Term("city", "Amsterdam"));
reader.Close();
Assert.AreEqual(0, GetHitCount("city", "Amsterdam"));
IndexWriter writer = new IndexWriter(dir, GetAnalyzer(),false);
Document doc = new Document();
doc.Add(Field.Keyword("id", "1"));
doc.Add(Field.UnStored("contents","Amsterdam has lots of bridges"));
doc.Add(Field.Text("city", "Haag"));
writer.AddDocument(doc);
writer.Optimize();
writer.Close();
Assert.AreEqual(1, GetHitCount("city", "Haag"));
}
protected override Analyzer GetAnalyzer()
{
return new WhitespaceAnalyzer(); //注意此处如果用SimpleAnalyzer搜索会失败,因为建立索引的时候使用的SimpleAnalyse它会将所有字母变成小写.
}
private int GetHitCount(String fieldName, String searchString)
{
IndexSearcher searcher = new IndexSearcher(dir);
Term t = new Term(fieldName, searchString);
Query query = new TermQuery(t);
Hits hits = searcher.Search(query);
int hitCount = hits.Length();
searcher.Close();
return hitCount;
}
}
需要注意的是以上所有有关索引的操作,为了避免频繁的打开和关闭Writer和Reader.又由于添加和删除是不同的连接(Writer, Reader)做的.所以应该尽可能的将添加文档的操作放在一起批量执行,然后将删除文档的操作也放在一起批量执行.避免添加删除交替进行.
索引的权重
根据文档的重要性的不同,显然对于某些文档你希望提高权重以便将来搜索的时候,更符合你想要的结果. 下面的代码演示了如何提高符合某些条件的文档的权重.
比如对公司内很多的邮件做了索引,你当然希望主要查看和公司有关的邮件,而不是员工的个人邮件.这点根据邮件的地址就可以做出判断比如包含@alphatom.com的就是公司邮件,而@gmail.com等等就是私人邮件.如何提高相应邮件的权重? 代码如下:
public static String COMPANY_DOMAIN = "alphatom.com";
Document doc = new Document();
String senderEmail = GetSenderEmail();
String senderName = getSenderName();
String subject = GetSubject();
String body = GetBody();
doc.Add(Field.Keyword("senderEmail”, senderEmail));
doc.Add(Field.Text("senderName", senderName));
doc.Add(Field.Text("subject", subject));
doc.Add(Field.UnStored("body", body));
if (GetSenderDomain().EndsWith(COMPANY_DOMAIN))
//如果是公司邮件,提高权重,默认权重是1.0
doc.SetBoost(1.5);
else //如果是私人邮件,降低权重.
doc.SetBoost(0.1);
writer.AddDocument(doc);
不仅如此你还可以对Field也设置权重.比如你对邮件的主题更感兴趣.就可以提高它的权重.
Field senderNameField = Field.Text("senderName", senderName);
Field subjectField = Field.Text("subject", subject);
subjectField.SetBoost(1.2);
lucene搜索的时候会对符合条件的文档按匹配的程度打分,这点就和google的PageRank有点类似, 而SetBoost中的Boost就是其中的一个因素,当然还有其他的因素.这要放到搜索里再说.
利用IndexWriter 变量对建立索引进行高级管理
在建立索引的时候对性能影响最大的地方就是在将索引写入文件的时候, 所以在具体应用的时候就需要对此加以控制.
在建立索引的时候对性能影响最大的地方就是在将索引写入文件的时候所以在具体应用的时候就需要对此加以控制
IndexWriter属性 | 默认值 | 描述 |
MergeFactory | 10 | 控制segment合并的频率和大小 |
MaxMergeDocs | Int32.MaxValue | 限制每个segment中包含的文档数 |
MinMergeDocs | 10 | 当内存中的文档达到多少的时候再写入segment |
Lucene默认情况是每加入10份文档就从内存往index文件写入并生成一个segement,然后每10个segment就合并成一个segment.通过MergeFactory这个变量就可以对此进行控制.
MaxMergeDocs用于控制一个segment文件中最多包含的Document数.比如限制为100的话,即使当前有10个segment也不会合并,因为合并后的segmnet将包含1000个文档,超过了限制.
MinMergeDocs用于确定一个当内存中文档达到多少的时候才写入文件,该项对segment的数量和大小不会有什么影响,它仅仅影响内存的使用,进一步影响写索引的效率.
为了生动的体现这些变量对性能的影响,用一个小程序对此做了说明.
这里有点不可思议.Lucene in Action书上的结果比我用dotLucene做的结果快了近千倍.这里给出书中用Lucene的数据,希望大家比较一下看看是不是我的问题.
Lucene in Action书中的数据:
% java lia.indexing.IndexTuningDemo 100000 10 9999999 10
Merge factor: 10
Max merge docs: 9999999
Min merge docs: 10
Time: 74136 ms
% java lia.indexing.IndexTuningDemo 100000 100 9999999 10
Merge factor: 100
Max merge docs: 9999999
Min merge docs: 10
Time: 68307 ms
我的数据: 336684128 ms
可以看出MinMergeDocs(主要用于控制内存)和MergeFactory(控制合并的次数和合并后的大小) 对建立索引有显著的影响.但是并不是MergeFactory越大越好,因为如果一个segment的文档数很多的话,在搜索的时候必然也会影响效率,所以这里MergeFactory的取值是一个需要平衡的问题.而MinMergeDocs主要受限于内存.
利用RAMDirectory充分发挥内存的优势
从上面来看充分利用内存的空间,减少读写文件(写入index)的次数是优化建立索引的重要方法.其实在Lucene中提供了更强大的方法来利用内存建立索引.使用RAMDirectory来替代FSDirectory. 这时所有的索引都将建立在内存当中,这种方法对于数据量小的搜索业务很有帮助,同时可以使用它来进行一些小的测试,避免在测试时频繁建立删除索引文件.
在实际应用中RAMDirectory和FSDirectory协作可以更好的利用内存来优化建立索引的时间.
具体方法如下:
1.建立一个使用FSDirectory的IndexWriter
2 .建立一个使用RAMDirectory的IndexWriter
3 把Document添加到RAMDirectory中
4 当达到某种条件将RAMDirectory 中的Document写入FSDirectory.
5 重复第三步
示意代码:
private FSDirectory fsDir = FSDirectory.GetDirectory("index",true);
private RAMDirectory ramDir = new RAMDirectory();
private IndexWriter fsWriter = IndexWriter(fsDir,new SimpleAnalyzer(), true);
private IndexWriter ramWriter = new IndexWriter(ramDir,new SimpleAnalyzer(), true);
while (there are documents to index)
{
ramWriter.addDocument(doc);
if (condition for flushing memory to disk has been met)
{
fsWriter.AddIndexes(Directory[]{ramDir}) ;
ramWriter.Close(); //why not support flush?
ramWriter =new IndexWriter(ramDir,new SimpleAnalyzer(),true);
}
}
这里的条件完全由用户控制,而不是FSDirectory采用对Document计数的方式控制何时写入文件.相比之下有更大的自由性,更能提升性能.
利用RAMDirectory并行建立索引
RAMDirectory还提供了使用多线程来建立索引的可能性.
甚至你可以在一个高速的网络里使用多台计算机来同时建立索引
虽然有关并行同步的问题需要你自己进行处理,不过通过这种方式可以大大提高对大量数据建立索引的能力.
控制索引内容的长度.
在我的一篇速递介绍过Google Desktop Search只能搜索到文本中第5000个字的.也就是google在建立索引的时候只考虑前5000个字,在Lucene中同样也有这个配置功能.
Lucene对一份文本建立索引时默认的索引长度是10,000. 你可以通过IndexWriter 的MaxFieldLength属性对此加以修改.还是用一个例子说明问题.
[Test]
public void FieldSize()
// AddDocuments 和 GetHitCount都是自定义的方法,详见源代码
{
AddDocuments(dir, 10);
//第一个参数是目录,第二个配置是索引的长度
Assert.AreEqual(1, GetHitCount("contents", "bridges"))
//原文档的contents为”Amsterdam has lots of bridges”
//当索引长度为10个字时能找到bridge
AddDocuments(dir, 1);
Assert.AreEqual(0, GetHitCount("contents", "bridges"));
//当索引长度限制为1个字时就无法发现第5个字bridges
}
对索引内容限长往往是处于效率和空间大小的考虑.能够对此进行配置是建立索引必备的一个功能.
Optimize 优化的是什么?
在以前的例子里,你可能已经多次见过writer.Optimize()这段代码.Optimize到底做了什么?
让你吃惊的是这里的优化对于建立索引不仅没有起到加速的作用,反而是延长了建立索引的时间.为什么?
因为这里的优化不是为建立索引做的,而是为搜索做的.之前我们提到Lucene默认每遇到10个Segment就合并一次,尽管如此在索引完成后仍然会留下几个segmnets,比如6,7.
而Optimize的过程就是要减少剩下的Segment的数量,尽量让它们处于一个文件中.
它的过程很简单,就是新建一个空的Segmnet,然后把原来的几个segmnet全合并到这一个segmnet中,在此过程中,你的硬盘空间会变大,因为同时存在两份一样大小的索引.不过在优化完成后,Lucene会自动将原来的多份Segments删除,只保留最后生成的一份包含原来所有索引的segment.
尽量减少segments的个数主要是为了增加查询的效率.假设你有一个Server,同时有很多的Client建立了各自不同的索引,如果此时搜索,那么必然要同时打开很多的索引文件,这样显然会受到很大的限制,对性能产生影响.
当然也不是随时做Optimize就好,如前所述做优化时要花费更多的时间和空间,而且在做优化的时候是不能进行查询的.所以索引建立的后期,并且索引的内容不会再发生太多的变化的时候做优化是一个比较好的时段.