Lucene.Net 系列一
本文介绍了什么是Lucene,Lucene能做什么.
如何从一个文件夹下的所有txt文件中查找特定的词?
本文将围绕该个实例介绍了lucene.net的索引的建立以及如何针对索引进行搜索.最后还将给出源代码供大家学习.
源代码下载
What’s Lucene Lucene是一个信息检索的函数库(Library),利用它你可以为你的应用加上索引和搜索的功能.
Lucene的使用者不需要深入了解有关全文检索的知识,仅仅学会使用库中的一个类,你就为你的应用实现全文检索的功能.
不过千万别以为Lucene是一个象google那样的搜索引擎,Lucene甚至不是一个应用程序,它仅仅是一个工具,一个Library.你也可以把它理解为一个将索引,搜索功能封装的很好的一套简单易用的API.利用这套API你可以做很多有关搜索的事情,而且很方便.
What Can Lucene Do
Lucene可以对任何的数据做索引和搜索. Lucene不管数据源是什么格式,只要它能被转化为文字的形式,就可以被Lucene所分析利用.也就是说不管是MS word, Html ,pdf还是其他什么形式的文件只要你可以从中抽取出文字形式的内容就可以被Lucene所用.你就可以用Lucene对它们进行索引以及搜索.
How To Use Lucene --- A Simple Example 示例介绍:
为作为输入参数的文件夹下的所有txt类型的文件做索引,做好的索引文件放入index文件夹.
然后在索引的基础上对文件进行全文搜索.
1. 建立索引
IndexWriter writer = new IndexWriter("index", new StandardAnalyzer(), true);
IndexDocs(writer, new System.IO.FileInfo(args[0]));
writer.Optimize();
writer.Close();
IndexWriter是对索引进行写操作的一个类,利用它可以创建一个索引对象然后往其中添加文件.需要注意它并不是唯一可以修改索引的类.在索引建好后利用其他类还可以对其进行修改.
构造函数第一个参数是建立的索引所要放的文件夹的名字.第二个参数是一个分析对象,主要用于从文本中抽取那些需要建立索引的内容,把不需要参与建索引的文本内容去掉.比如去掉一些a the之类的常用词,还有决定是否大小写敏感.不同的选项通过指定不同的分析对象控制.第三个参数用于确定是否覆盖原有索引的.
第二步就是利用这个writer往索引中添加文件.具体后面再说.
第三步进行优化.
第四步关闭writer.
下面具体看看第二步:
public static void IndexDirectory(IndexWriter writer, FileInfo file)
{
if (Directory.Exists(file.FullName))
{
String[] files = Directory.GetFileSystemEntries(file.FullName);
// an IO error could occur
if (files != null)
{
for (int i = 0; i < files.Length; i )
{
IndexDirectory(writer, new FileInfo(files[i])); //这里是一个递归
}
}
}
else if (file.Extension == ".txt")
{
IndexFile(file, writer);
}
}
private static void IndexFile(FileInfo file, IndexWriter writer) { Console.Out.WriteLine("adding " file); try { Document doc = new Document(); doc.Add(Field.Keyword("filename", file.FullName));
doc.Add(Field.Text("contents", new StreamReader(file.FullName)));
writer.AddDocument(doc); } catch (FileNotFoundException fnfe) { } }
主要就是两个函数一个用于处理文件夹(不是为文件夹建立索引),一个用于真正为文件建立索引.
因此主要集中看一下IndexFile这个方法.首先建立Document对象,然后为Document对象添加一些属性Field.你可以把Document对象看成是虚拟文件,将来将从此获取信息.而Field则看成是描述此虚拟文件的元数据(metadata).
其中Field包括四个类型:
Keywork | 该类型的数据将不被分析,而会被索引并保存保存在索引中. |
---|---|
UnIndexed | 该类型的数据不会被分析也不会被索引,但是会保存在索引. |
UnStored | 和UnIndexed刚好相反,被分析被索引,但是不被保存. |
Text | 和UnStrored类似.如果值的类型为string还会被保存.如果值的类型Reader就不会被保存和UnStored一样. |
最后将每一个Document添加到索引当中.
需要注意的是索引不仅可以建立在文件系统上,也可以建立在内存中.
例如
IndexWriter writer = new IndexWriter("index", new StandardAnalyzer(), true);
在第一个参数不是指定文件夹的名字而是使用Directory对象,并使用它的子类RAMDirectory,就可以将索引建立在内存当中.
2. 对索引进行搜索
IndexSearcher indexSearcher= new IndexSearcher(indexDir);
Query query = QueryParser.Parse(queryString, "contents",new StandardAnalyzer());
Hits hits = indexSearcher.Search(query);
第一步利用IndexSearcher打开索引文件用于后面搜索,其中的参数是索引文件的路径.
第二步使用QueryParser将可读性较好的查询语句(比如查询的词lucene ,以及一些高级方式lucene AND .net)转化为Lucene内部使用的查询对象.
第三步执行搜索.并将结果返回到hits集合.需要注意的是Lucene并不是一次将所有的结果放入hits中而是采取一次放一部分的方式.出于空间考虑.
Lucene.net 系列二 --- index 上
本文继续系列一详细介绍了有关Lucene.net索引添加删除更新的详细内容.并给出了所有的TestCase供学习参考.
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.UnIndexed("country", "Netherlands"));
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)做的.所以应该尽可能的将添加文档的操作放在一起批量执行,然后将删除文档的操作也放在一起批量执行.避免添加删除交替进行.
Lucene.net 系列三 --- index 中 |
---|
本文将进一步讨论有关Lucene.net建立索引的问题:
主要包含以下主题: 1.索引的权重 2.利用IndexWriter 属性对建立索引进行高级管理 3.利用RAMDirectory充分发挥内存的优势 4.利用RAMDirectory并行建立索引 5.控制索引内容的长度 6.Optimize 优化的是什么?
源代码下载
本文将进一步讨论有关Lucene.net建立索引的问题:
索引的权重 根据文档的重要性的不同,显然对于某些文档你希望提高权重以便将来搜索的时候,更符合你想要的结果. 下面的代码演示了如何提高符合某些条件的文档的权重.
比如对公司内很多的邮件做了索引,你当然希望主要查看和公司有关的邮件,而不是员工的个人邮件.这点根据邮件的地址就可以做出判断比如包含@alphatom.com的就是公司邮件,而@gmail.com等等就是私人邮件.如何提高相应邮件的权重? 代码如下:
publicstatic 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就好,如前所述做优化时要花费更多的时间和空间,而且在做优化的时候是不能进行查询的.所以索引建立的后期,并且索引的内容不会再发生太多的变化的时候做优化是一个比较好的时段.
Lucene.net 系列四 --- index 下 |
---|
本文将介绍有关索引并发控制的问题,以结束对Lucene.net建立索引问题的讨论.
1. 允许任意多的读操作并发.即可以有任意多的用户在同一时间对同一份索引做查询工作.
2. 允许任意多的读操作在索引被正在被修改的时候进行.即哪怕索引正在被优化,添加删除文档,这时也是允许用户对索引进行查询工作. (it’s so cool.)
3. 同一时间只允许一个对索引修改的操作.即同一时间只允许IndexWriter或IndexReader打开同一份索引.不能允许两个同时打开一份索引.
Lucene提供了几种对索引进行读写的操作.添加文档到索引,从索引中删除文档,优化索引,合并Segments.这些都是对索引进行写操作的方法. 查询的时候就会读取索引的内容.
有关索引并发的问题是一个比较重要的问题,而且是Lucene的初学者容易忽略的问题,当索引被破坏,或者程序突然出现异常的时候初学者往往不知道是自己的误操作造成的.
下面让我们看看Lucene是如何处理索引文件的并发控制的.
首先记住一下三点准则:
1. 允许任意多的读操作并发.即可以有任意多的用户在同一时间对同一份索引做查询工作.
2. 允许任意多的读操作在索引被正在被修改的时候进行.即哪怕索引正在被优化,添加删除文档,这时也是允许用户对索引进行查询工作. (it’s so cool.)
3. 同一时间只允许一个对索引修改的操作.即同一时间只允许IndexWriter或IndexReader打开同一份索引.不能允许两个同时打开一份索引.
第一个准则很容易理解,第二个准则说明Lucene对并发的操作支持还是不错的.第三个准则也很正常,不过需要注意的是第三个准则只是表明IndexWriter和IndexReader不能并存,而没有反对在多线程中利用同一个IndexWriter对索引进行修改.这个功能可是经常用到的,所以不要以为它是不允许的.不过这个时候的并发就需要你自己加以控制,以免出现冲突.
(注: 在前面的系列中已说过IndexReader不是对Index进行读操作,而是从索引中删除docuemnt时使用的对象)
表中列出了有关索引的主要读写操作.其中空白处表示X轴的操作和Y轴的操作允许并发.
而X处表明X轴的操作和Y轴的操作不允许同时进行.
比如Add document到索引的时候不允许同时从索引中删除document.
其实以上这张表就是前面三个准则的体现.Add Optimize Merge操作都是由IndexWriter来做的.而Delete则是通过IndexReader完成.所以表中空白处正是第一条和第二条准则的体现,而X(冲突)处正是第三个原则的具体表现.
为了在不了解并发控制的情况下对Lucene API的乱用. Lucene提供了基于文件的锁机制以确保索引文件不会被破坏.
当你对index 进行修改的时候, 比如添加删除文档的时候就会产生 ***write.lock文件,而当你从segment进行读取信息或者合并segments的时候就会产生***commit.lock文件.在默认情况下,这些文件是放在系统临时文件夹下的. 简而言之, write.lock文件存在的时间比较长,也就是对index进行修改的锁时间比较长,而commit.lock存在的时间往往很短.具体情况见下表.
如果索引存在于server, 很多clients想访问的时候,自然希望能看到其他用户的锁文件,这时把锁文件放到系统临时文件夹就不好了.此时可以通过配置文件来改变锁文件存放的位置.
比如在一个asp.net的应用下,你就可以象下面这样利用web.config文件来实现你的目的.
<configuration> <appSettings> <add key="Lucene.Net.lockdir" value="c:yourdir" /> </appSettings> </configuration>
不仅如此,在某些情况下比如你的索引文件存放在一个CD-ROM中,这时根本就无法对索引进行修改,也就不存在所谓的并发冲突,这种情况下你甚至可以讲锁文件的机制取消掉.同样通过配置文件.
<configuration> <appSettings> <add key="disableLuceneLocks" value="true" /> </appSettings> </configuration>
不过请注意不要乱用此功能,不然你的索引文件将不再受到安全的保护.
下面用一个例子说明锁机制的体现.
using System; using System.IO; using Lucene.Net.Analysis; using Lucene.Net.Index; using Lucene.Net.Store; using NUnit.Framework; using Directory = Lucene.Net.Store.Directory;
[TestFixture] public class LockTest { private Directory dir;
[SetUp] public void Init() { String indexDir = "index"; dir = FSDirectory.GetDirectory(indexDir, true); }
[Test] [ExpectedException(typeof(IOException))] public void WriteLock() { IndexWriter writer1 = null; IndexWriter writer2 = null; try { writer1 = new IndexWriter(dir, new SimpleAnalyzer(), true); writer2 = new IndexWriter(dir, new SimpleAnalyzer(), true); } catch (IOException e) { Console.Out.WriteLine(e.StackTrace); } finally { writer1.Close(); Assert.IsNull(writer2); } }
[Test] public void CommitLock() { IndexReader reader1 = null; IndexReader reader2 = null; try { IndexWriter writer = new IndexWriter(dir, new SimpleAnalyzer(), true); writer.Close(); reader1 = IndexReader.Open(dir); reader2 = IndexReader.Open(dir); } finally { reader1.Close(); reader2.Close(); } } }
不过很令人失望的是在Lucene(Java)中应该收到的异常在dotLucene(1.4.3)我却没有捕获到.随后我在dotLucene的论坛上问了一下,至今尚未有解答.这也是开源项目的无奈了吧.
Lucene.net 系列五 --- search 上 |
---|
在前面的系列我们一直在介绍有关索引建立的问题,现在是该利用这些索引来进行搜索的时候了,Lucene良好的架构使得我们只需要很少的几行代码就可以为我们的应用加上搜索的功能,首先让我们来认识一下搜索时最常用的几个类.
查询特定的某个概念
当我们搜索完成的时候会返回一个按Sorce排序的结果集Hits. 这里的Score就是接近度的意思,象Google那样每个页面都会有一个分值,搜索结果按分值排列. 如同你使用Google一样,你不可能查看所有的结果, 你可能只查看第一个结果所以Hits返回的不是所有的匹配文档本身, 而仅仅是实际文档的引用. 通过这个引用你可以获得实际的文档.原因很好理解, 如果直接返回匹配文档,数据量太大,而很多的结果你甚至不会去看, 想想你会去看Google 搜索结果10页以后的内容吗?
下面用一个例子来简要介绍一下Search
先建立索引
namespacedotLucene.inAction.BasicSearch
{
[TestFixture]
public class BaseIndexingTestCase
{
protected String[] keywords = {"1930110994", "1930110995"};
protected String[] unindexed = {"Java Development with Ant", "JUnit in Action"};
protected String[] unstored = { "we have ant and junit", "junit use a mock,ant is also", };
protected String[] text1 = { "ant junit", "junit mock" };
protected String[] text2 = {
"200206",
"200309"
};
protected String[] text3 = { "/Computers/Ant", "/Computers/JUnit" };
protected Directory dir;
[SetUp] protected void Init() { string indexDir = "index"; dir = FSDirectory.GetDirectory(indexDir, true); AddDocuments(dir); }
protected void AddDocuments(Directory dir)
{
IndexWriter writer=new IndexWriter(dir, GetAnalyzer(), true);
for (int i = 0; i < keywords.Length; i ) {
Document doc = new Document(); doc.Add(Field.Keyword("isbn", keywords[i])); doc.Add(Field.UnIndexed("title", unindexed[i])); doc.Add(Field.UnStored("contents", unstored[i])); doc.Add(Field.Text("subject", text1[i])); doc.Add(Field.Text("pubmonth", text2[i])); doc.Add(Field.Text("category", text3[i])); writer.AddDocument(doc);
} writer.Optimize(); writer.Close();
}
protected virtual Analyzer GetAnalyzer() { PerFieldAnalyzerWrapper analyzer = new PerFieldAnalyzerWrapper( new SimpleAnalyzer()); analyzer.AddAnalyzer("pubmonth", new WhitespaceAnalyzer()); analyzer.AddAnalyzer("category", new WhitespaceAnalyzer()); return analyzer; } } } 这里用到了一些有关Analyzer的知识,将放在以后的系列中介绍.
查询特定的某个概念
然后利用利用TermQery来搜索一个Term(你可以把它理解为一个Word)
[Test] public void Term() { IndexSearcher searcher = new IndexSearcher(directory); Term t = new Term("subject", "ant"); Query query = new TermQuery(t); Hits hits = searcher.Search(query); Assert.AreEqual(1, hits.Length(), "JDwA");
t = new Term("subject", "junit"); hits = searcher.Search(new TermQuery(t)); Assert.AreEqual(2, hits.Length());
searcher.Close(); }
利用QueryParse简化查询语句
显然对于各种各样的查询(与或关系,等等各种复杂的查询,在下面将介绍),你不希望一一对应的为它们写出相应的XXXQuery. Lucene已经为你考虑到了这点, 通过使用QueryParse这个类, 你只需要写出我们常见的搜索语句, Lucene会在内部自动做一个转换.
这个过程有点类似于数据库搜索, 我们已经习惯于使用SQL查询语句,其实在数据库的内部是要做一个转换的, 因为数据库不认得SQL语句,它只认得查询语法树.
让我们来看一个例子.
[Test] public void TestQueryParser() { IndexSearcher searcher = new IndexSearcher(directory);
Query query = QueryParser.Parse(" JUNIT ANT -MOCK", "contents", new SimpleAnalyzer()); Hits hits = searcher.Search(query); Assert.AreEqual(1, hits.Length()); Document d = hits.Doc(0); Assert.AreEqual("Java Development with Ant", d.Get("title"));
query = QueryParser.Parse("mock OR junit", "contents", new SimpleAnalyzer()); hits = searcher.Search(query); Assert.AreEqual(2, hits.Length(), "JDwA and JIA"); }
由以上的代码可以看出我们不需要为每种特定查询而去设定XXXQuery 通过QueryParse类的静态方法Parse就可以很方便的将可读性好的查询口语转换成Lucene内部所使用的各种复杂的查询语句. 有一点需要注意:在Parse方法中我们使用了SimpleAnalyzer, 这时候会将查询语句做一些变换,比如这里将JUNIT 等等大写字母变成了小写字母,所以才能搜索到(因为我们在建立索引的时候使用的是小写),如果你将StanderAnalyzer变成WhitespaceAnalyzer就会搜索不到.具体原理以后再说.
A B表示A和B要同时存在,-C表示C不存在,A OR B表示A或B二者有一个存在就可以.
其中title等等的field表示你在建立索引时所采用的属性名.
Lucene.net系列六 -- search 下 |
---|
本文主要结合测试案例介绍了Lucene下的各种查询语句以及它们的简化方法.
通过本文你将了解Lucene的基本查询语句,并可以学习所有的测试代码已加强了解.
源代码下载
具体的查询语句
在了解了SQL后, 你是否想了解一下查询语法树?在这里简要介绍一些能被Lucene直接使用的查询语句.
1. TermQuery 查询某个特定的词,在文章开始的例子中已有介绍.常用于查询关键字.
[Test]
public void Keyword()
{
IndexSearcher searcher = new IndexSearcher(directory);
Term t = new Term("isbn", "1930110995");
Query query = new TermQuery(t);
Hits hits = searcher.Search(query);
Assert.AreEqual(1, hits.Length(), "JUnit in Action");
}
注意Lucene中的关键字,是需要用户去保证唯一性的.
TermQuery和QueryParse
只要在QueryParse的Parse方法中只有一个word,就会自动转换成TermQuery.
2. RangeQuery 用于查询范围,通常用于时间,还是来看例子:
namespace dotLucene.inAction.BasicSearch
{
public class RangeQueryTest : LiaTestCase
{
private Term begin, end;
[SetUp] protected override void Init() { begin = new Term("pubmonth", "200004");
end = new Term("pubmonth", "200206"); base.Init(); }
[Test] public void Inclusive() { RangeQuery query = new RangeQuery(begin, end, true); IndexSearcher searcher = new IndexSearcher(directory);
Hits hits = searcher.Search(query);
Assert.AreEqual(1, hits.Length());
}
[Test] public void Exclusive() { RangeQuery query = new RangeQuery(begin, end, false); IndexSearcher searcher = new IndexSearcher(directory);
Hits hits = searcher.Search(query); Assert.AreEqual(0, hits.Length()); }
} }
RangeQuery的第三个参数用于表示是否包含该起止日期.
RangeQuery和QueryParse
[Test]
public void TestQueryParser()
{
Query query = QueryParser.Parse("pubmonth:[200004 TO 200206]", "subject", new SimpleAnalyzer());
Assert.IsTrue(query is RangeQuery);
IndexSearcher searcher = new IndexSearcher(directory);
Hits hits = searcher.Search(query);
query = QueryParser.Parse("{200004 TO 200206}", "pubmonth", new SimpleAnalyzer()); hits = searcher.Search(query); Assert.AreEqual(0, hits.Length(), "JDwA in 200206"); }
Lucene用[] 和{}分别表示包含和不包含.
3. PrefixQuery
用于搜索是否包含某个特定前缀,常用于Catalog的检索.
[Test]
public void TestPrefixQuery()
{
PrefixQuery query = new PrefixQuery(new Term("category", "/Computers"));
IndexSearcher searcher = new IndexSearcher(directory); Hits hits = searcher.Search(query); Assert.AreEqual(2, hits.Length()); query = new PrefixQuery(new Term("category", "/Computers/JUnit")); hits = searcher.Search(query); Assert.AreEqual(1, hits.Length(), "JUnit in Action"); }
PrefixQuery和QueryParse
[Test] public void TestQueryParser() {
QueryParser qp = new QueryParser("category", new SimpleAnalyzer()); qp.SetLowercaseWildcardTerms(false); Query query =qp.Parse("/Computers*"); Console.Out.WriteLine("query = {0}", query.ToString()); IndexSearcher searcher = new IndexSearcher(directory); Hits hits = searcher.Search(query); Assert.AreEqual(2, hits.Length()); query =qp.Parse("/Computers/JUnit*"); hits = searcher.Search(query); Assert.AreEqual(1, hits.Length(), "JUnit in Action"); }
这里需要注意的是我们使用了QueryParser对象,而不是QueryParser类. 原因在于使用对象可以对QueryParser的一些默认属性进行修改.比如在上面的例子中我们的category是大写的,而QueryParser默认会把所有的含*的查询字符串变成小写/computer*. 这样我们就会查不到原文中的/Computers* ,所以我们需要通过设置QueryParser的默认属性来改变这一默认选项.即qp.SetLowercaseWildcardTerms(false)所做的工作.
4. BooleanQuery
用于测试满足多个条件.
下面两个例子用于分别测试了满足与条件和或条件的情况.
[Test]
public void And()
{
TermQuery searchingBooks =
new TermQuery(new Term("subject", "junit"));
RangeQuery currentBooks = new RangeQuery(new Term("pubmonth", "200301"), new Term("pubmonth", "200312"), true); BooleanQuery currentSearchingBooks = new BooleanQuery(); currentSearchingBooks.Add(searchingBooks, true, false); currentSearchingBooks.Add(currentBooks, true, false); IndexSearcher searcher = new IndexSearcher(directory); Hits hits = searcher.Search(currentSearchingBooks);
AssertHitsIncludeTitle(hits, "JUnit in Action"); } [Test] public void Or() { TermQuery methodologyBooks = new TermQuery( new Term("category", "/Computers/JUnit")); TermQuery easternPhilosophyBooks = new TermQuery( new Term("category", "/Computers/Ant")); BooleanQuery enlightenmentBooks = new BooleanQuery(); enlightenmentBooks.Add(methodologyBooks, false, false); enlightenmentBooks.Add(easternPhilosophyBooks, false, false); IndexSearcher searcher = new IndexSearcher(directory); Hits hits = searcher.Search(enlightenmentBooks); Console.Out.WriteLine("or = " enlightenmentBooks); AssertHitsIncludeTitle(hits, "Java Development with Ant"); AssertHitsIncludeTitle(hits, "JUnit in Action");
}
什么时候是与什么时候又是或? 关键在于BooleanQuery对象的Add方法的参数.
参数一是待添加的查询条件.
参数二Required表示这个条件必须满足吗? True表示必须满足, False表示可以不满足该条件.
参数三Prohibited表示这个条件必须拒绝吗? True表示这么满足这个条件的结果要排除, False表示可以满足该条件.
这样会有三种组合情况
BooleanQuery和QueryParse
[Test]
public void TestQueryParser()
{
Query query = QueryParser.Parse("pubmonth:[200301 TO 200312] AND junit", "subject", new SimpleAnalyzer());
IndexSearcher searcher = new IndexSearcher(directory);
Hits hits = searcher.Search(query);
Assert.AreEqual(1, hits.Length());
query = QueryParser.Parse("/Computers/JUnit OR /Computers/Ant", "category", new WhitespaceAnalyzer());
hits = searcher.Search(query);
Assert.AreEqual(2, hits.Length());
}
注意AND和OR的大小 如果想要A与非B 就用 A AND –B 表示, A –B也可以.
默认的情况下QueryParser会把空格认为是或关系,就象google一样.但是你可以通过QueryParser对象修改这一属性.
[Test] public void TestQueryParserDefaultAND() { QueryParser qp = new QueryParser("subject", new SimpleAnalyzer()); qp.SetOperator(QueryParser.DEFAULT_OPERATOR_AND ); Query query = qp.Parse("pubmonth:[200301 TO 200312] junit"); IndexSearcher searcher = new IndexSearcher(directory); Hits hits = searcher.Search(query); Assert.AreEqual(1, hits.Length());
} 5. PhraseQuery 查询短语,这里面主要有一个slop的概念, 也就是各个词之间的位移偏差, 这个值会影响到结果的评分.如果slop为0,当然最匹配.看看下面的例子就比较容易明白了,有关slop的计算用户就不需要理解了,不过slop太大的时候对查询效率是有影响的,所以在实际使用中要把该值设小一点. PhraseQuery对于短语的顺序是不管的,这点在查询时除了提高命中率外,也会对性能产生很大的影响, 利用SpanNearQuery可以对短语的顺序进行控制,提高性能. [SetUp] protected void Init() { // set up sample document RAMDirectory directory = new RAMDirectory(); IndexWriter writer = new IndexWriter(directory, new WhitespaceAnalyzer(), true); Document doc = new Document(); doc.Add(Field.Text("field", "the quick brown fox jumped over the lazy dog")); writer.AddDocument(doc); writer.Close();
searcher = new IndexSearcher(directory); } private bool matched(String[] phrase, int slop) { PhraseQuery query = new PhraseQuery(); query.SetSlop(slop);
for (int i = 0; i < phrase.Length; i ) { query.Add(new Term("field", phrase[i])); }
Hits hits = searcher.Search(query); return hits.Length() > 0; }
[Test] public void SlopComparison() { String[] phrase = new String[]{"quick", "fox"};
Assert.IsFalse(matched(phrase, 0), "exact phrase not found");
Assert.IsTrue(matched(phrase, 1), "close enough"); }
[Test] public void Reverse() { String[] phrase = new String[] {"fox", "quick"};
Assert.IsFalse(matched(phrase, 2), "exact phrase not found");
Assert.IsTrue(matched(phrase, 3), "close enough"); }
[Test] public void Multiple()- { Assert.IsFalse(matched(new String[] {"quick", "jumped", "lazy"}, 3), "not close enough"); Assert.IsTrue(matched(new String[] {"quick", "jumped", "lazy"}, 4), "just enough"); Assert.IsFalse(matched(new String[] {"lazy", "jumped", "quick"}, 7), "almost but not quite"); Assert.IsTrue(matched(new String[] {"lazy", "jumped", "quick"}, 8), "bingo"); }
PhraseQuery和QueryParse
利用QueryParse进行短语查询的时候要先设定slop的值,有两种方式如下所示
[Test]
public void TestQueryParser()
{
Query q1 = QueryParser.Parse(""quick fox"",
"field", new SimpleAnalyzer());
Hits hits1 = searcher.Search(q1);
Assert.AreEqual(hits1.Length(), 0);
Query q2 = QueryParser.Parse(""quick fox"~1", //第一种方式 "field", new SimpleAnalyzer()); Hits hits2 = searcher.Search(q2); Assert.AreEqual(hits2.Length(), 1);
QueryParser qp = new QueryParser("field", new SimpleAnalyzer());
qp.SetPhraseSlop(1); //第二种方式
Query q3=qp.Parse(""quick fox"");
Assert.AreEqual(""quick fox"~1", q3.ToString("field"),"sloppy, implicitly");
Hits hits3 = searcher.Search(q2);
Assert.AreEqual(hits3.Length(), 1);
}
6. WildcardQuery 通配符搜索,需要注意的是child, mildew的分值是一样的. [Test] public void Wildcard() { IndexSingleFieldDocs(new Field[] { Field.Text("contents", "wild"), Field.Text("contents", "child"), Field.Text("contents", "mild"), Field.Text("contents", "mildew") }); IndexSearcher searcher = new IndexSearcher(directory); Query query = new WildcardQuery( new Term("contents", "?ild*")); Hits hits = searcher.Search(query); Assert.AreEqual(3, hits.Length(), "child no match"); Assert.AreEqual(hits.Score(0), hits.Score(1), 0.0, "score the same"); Assert.AreEqual(hits.Score(1), hits.Score(2), 0.0, "score the same"); } WildcardQuery和QueryParse 需要注意的是出于性能的考虑使用QueryParse的时候,不允许在开头就使用就使用通配符. 同样处于性能考虑会将只在末尾含有*的查询词转换为PrefixQuery. [Test, ExpectedException(typeof (ParseException))] public void TestQueryParserException() { Query query = QueryParser.Parse("?ild*", "contents", new WhitespaceAnalyzer()); }
[Test] public void TestQueryParserTailAsterrisk() { Query query = QueryParser.Parse("mild*", "contents", new WhitespaceAnalyzer()); Assert.IsTrue(query is PrefixQuery); Assert.IsFalse(query is WildcardQuery);
}
[Test] public void TestQueryParser() { Query query = QueryParser.Parse("mi?d*", "contents", new WhitespaceAnalyzer()); Hits hits = searcher.Search(query); Assert.AreEqual(2, hits.Length()); } 7. FuzzyQuery 模糊查询, 需要注意的是两个匹配项的分值是不同的,这点和WildcardQuery是不同的
[Test] public void Fuzzy() { Query query = new FuzzyQuery(new Term("contents", "wuzza")); Hits hits = searcher.Search(query); Assert.AreEqual( 2, hits.Length(),"both close enough"); Assert.IsTrue(hits.Score(0) != hits.Score(1),"wuzzy closer than fuzzy"); Assert.AreEqual("wuzzy", hits.Doc(0).Get("contents"),"wuzza bear"); }
FuzzyQuery和QueryParse
注意和PhraseQuery中表示slop的区别,前者~后要跟数字.
[Test] public void TestQueryParser() { Query query =QueryParser.Parse("wuzza~","contents",new SimpleAnalyzer()); Hits hits = searcher.Search(query); Assert.AreEqual( 2, hits.Length(),"both close enough"); }