Pangu分词Lucene.Net搜索使用说明
首先对Lucene做一个简短的介绍:
Lucene不是一个完整的全文检索应用,而是一个用java写的全文索引引擎工具包,它可以方便的嵌入到各种应用中实现针对应用的全文索引/检索功能。
Lucene的作者Doug Cutting是一位资深全文索引/检索专家,曾经是V-Twin搜索引擎(Apple的Copland操作系统的成就之一)的主要开发者,后在Excite担任高级系统架构设计师,目前从事于一些INTERNET底层架构的研究。他贡献出的Lucene的目标是为各种中小型应用程序加入全文检索功能。
全文检索的实现机制
Lucene的API接口设计的比较通用,输入输出结构都很像数据库的表=>记录=>字段,所以很多传统的应用的文件、数据库等都可以比较方便的映射到Lucene的存储结构/接口中。总体上看:可以先把Lucene当成一个支持全文检索的数据库系统。
比较一下Lucene和数据库:
Lucene | 数据库 |
索引数据源:doc(field1,field2...) doc(field1,field2...) \ indexer / ------------------- | Lucene Index | ------------------- / searcher \ 结果输出:Hits(doc(field1,field2) doc(field1,field2)) |
索引数据源:record(field1,field2)record(field1,field2) \SQL:Insert/ --------------------- | DB Index | --------------------- / SQL:Select \ 结果输出:results(record(field1,field2)record(field1,field2)) |
Document:一个需要进行索引的“单元” 一个Document由多个字段组成 | Record:记录,包含多个字段 |
Field:字段 | Field:字段 |
Hits:查询结果集,由匹配的Document组成 | RecordSet:查询结果集,由多个Record组成 |
全文检索 ≠ like "%keyword%"
通常比较厚的书籍后面常常附关键词索引表(比如:北京:12,34页 上海:3,77页),它能够帮助读者比较快的找到相关内容的页码。而数据库索引能够大大提高查询的速度也是一样的原理,想象一下通过书后面的索引查找的速度比一页一页地翻内容高多少倍...而索引之所以效率高,另外一个原因是它是排好序的。对于检索系统来说核心是一个排序问题。
由于数据库索引不是为全文检索设计的,因此,使用like "%keyWord%"时,数据库索引是不起作用的,在使用like查询时,搜索过程又变成了类似于一页页翻书的遍历过程了,所以对于含有模糊查询的数据库服务来说,like对性能的危害是极大的。如果是需要对多个关键词进行模糊匹配:like "%keyword%" and like "%keyword%"...其效率也就可想而知了。
所以建立一个高效检索系统的关键是建立一个类似于科技索引一样的反向索引机制,将数据源(比如多篇文章)排序顺序存储的同时,有另外一个排好序的关键词列表,用于存储关键词=>文章映射关系,利用这样的映射关系索引:[关键词=>出现关键词的文章编号,出现次数(甚至包括位置:起始偏移量,结束偏移量),出现频率],检索过程就是把模糊查询变成多个可以利用索引的精确查询的逻辑组合的过程。从而大大提高了多关键词查询的效率,所以,全文检索问题归结到底是一个排序问题。
由此可以看出模糊查询相对于数据库的精确查询是一个非常不确定的问题,这也是大部分数据库对全文检索支持有限的原因。Lucene最核心的特征是通过特殊的索引结构实现了传统数据库不擅长的全文索引机制,并提供了扩展接口,以方便针对不同应用的定制。
可以通过以下表格对比一下数据库的模糊查询:
Lucene全文索引引擎 | 数据库 | |
索引 | 将数据源中的数据都通过全文索引--- 建立反向索引 |
对于like查询来说,数据传统的索引是根本用不上的。数据需要逐个便利记录进行GREP式的模糊匹配,比有索引的搜索速度要有多个数量级的下降。GREP(global search regular expression(RE) ,全面搜索正则表达式,能使用正则表达式搜索文本) |
匹配效果 | 通过词元(term)进行匹配,通过语言分析接口的实现,可以实现对中文等非英语的支持。 | 使用:like "%net%"会把netherlands也匹配出来,多个关键词的模糊匹配:使用like "%com%net%" 就不能匹配词序颠倒的XXX.net.XXX.com |
匹配度 | 有匹配度算法,将匹配程度(相似度)比较高的结果排在前面。 | 没有匹配程度的控制:比如有记录中net出现5词和出现1次的结果是一样的。 |
结果输出 | 通过特别的算法,将匹配度最高的头100条结果输出,结果集是缓冲式的小批量读取的。 | 返回所有的结果集,在匹配条目非常多的时候(比如上万条)需要大量的内存存放这些临时结果集。 |
可定制性 | 通过不同的语言分析接口实现,可以方便的定制出符合应用需要的索引规则(包括对中文的支持) | 没有接口或接口复杂,无法定制 |
结论 | 高负载的模糊查询应用,需要负责的模糊查询的规则,索引的资料量比较大 | 使用率低,模糊匹配规则简单或者需要模糊查询的资料量少 |
全文检索和数据库应用最大不同之处在于:让最相关的头100条结果满足98%以上的用户的需求
Lucene的创新之处:
大部分的搜索(数据库)引擎都是用B树结构来维护索引的,索引的更新会导致大量的IO操作,Lucene在实现中,对此稍微有所改进:不是维护一个索引文件,而是在扩展索引的时候不断创建新的索引文件,然后定期的把这些新的小索引文件合并到原先的大索引中(针对不同的更新策略,批次的大小可以调整),这样在不影响检索的效率的前提下,提高了索引的效率。
Lucene和其他一些全文检索系统/应用的比较:
Lucene | 其他开源全文检索系统 | |
增量索引和批量索引 | 可以进行增量索引(Append),可以对于大量数据进行批量索引,并且接口设计用于优化批量索引和小批量的增量索引。 | 很多系统只支持批量的索引,有时数据源有一点增量也需要重建索引。 |
数据源 | Lucene没有定义具体的数据源,而是一个文档的结构,因此可以非常灵活的适应各种应用(只要前端有合适的转换器把数据源转换成相应的结构) | 很多系统只针对网页,缺乏其他格式文档的灵活性 |
索引内容的抓取 | Lucene的文档是由多个字段组成的,甚至可以控制哪些字段需要进行索引,哪些字段不需要索引,进一步索引的字段也分为需要分词和不需要分词 | 缺乏通用性,往往将文档整个索引了 |
语言分析 | 通过语言分析器的不同扩展实现:可以过滤掉不需要的词:an the of等,西文语法分析:将jumps jumped jumper都归结成jump进行索引/检索 | 缺乏通用接口实现 |
查询分许 | 通过查询分析接口的实现,可以定制自己的查询语法规则:比如:多个关键词之间的+ - and or关系等 | |
并发访问 | 能够支持多用户的使用 |
关于亚洲语言的的切分词问题(Word Segment)
对于中文来说,全文索引首先还要解决一个语言分析的问题,对于英文来说,语句中单词之间是天然通过空格分开的,但亚洲语言的中日韩文语句中的字是一个字挨一个,所有,首先要把语句中按“词”进行索引的话,这个词如何切分出来就是一个很大的问题。
首先,肯定不能用单个字符作(si-gram)为索引单元,否则查“上海”时,不能让含有“海上”也匹配。但一句话:“北京天安门”,计算机如何按照中文的语言习惯进行切分呢?“北京 天安门” 还是“北 京 天安门”?让计算机能够按照语言习惯进行切分,往往需要机器有一个比较丰富的词库才能够比较准确的识别出语句中的单词。
另外一个解决的办法是采用自动切分算法:将单词按照2元语法(bigram)方式切分出来,比如:"北京天安门" ==> "北京 京天 天安 安门"。这样,在查询的时候,无论是查询"北京" 还是查询"天安门",将查询词组按同样的规则进行切分:"北京","天安安门",多个关键词之间按与"and"的关系组合,同样能够正确地映射到相应的索引中。这种方式对于其他亚洲语言:韩文,日文都是通用的。
基于自动切分的最大优点是没有词表维护成本,实现简单,缺点是索引效率低,但对于中小型应用来说,基于2元语法的切分还是够用的。基于2元切分后的索引一般大小和源文件差不多,而对于英文,索引文件一般只有原文件的30%-40%不同。
自动切分 | 词表切分 | |
实现 | 实现非常简单 | 实现复杂 |
查询 | 增加了查询分析的复杂程度 | 适于实现比较复杂的查询语法规则 |
存储效率 | 索引冗余大,索引几乎和原文一样大 | 索引效率高,为原文大小的30%左右 |
维护成本 | 无词表维护成本 | 词表维护成本非常高:中日韩等语言需要分别维护。 还需要包括词频统计等内容 |
适用领域 | 嵌入式系统:运行环境资源有限 分布式系统:无词表同步问题 多语言环境:无词表维护成本 |
对查询和存储效率要求高的专业搜索引擎 |
目前比较大的搜索引擎的语言分析算法一般是基于以上2个机制的结合。关于中文的语言分析算法,大家可以在Google查关键词"wordsegment search"能找到更多相关的资料。
索引过程优化
索引一般分2种情况,一种是小批量的索引扩展,一种是大批量的索引重建。在索引过程中,并不是每次新的DOC加入进去索引都重新进行一次索引文件的写入操作(文件I/O是一件非常消耗资源的事情)。
Lucene先在内存中进行索引操作,并根据一定的批量进行文件的写入。这个批次的间隔越大,文件的写入次数越少,但占用内存会很多。反之占用内存少,但文件IO操作频繁,索引速度会很慢。在IndexWriter中有一个MERGE_FACTOR参数可以帮助你在构造索引器后根据应用环境的情况充分利用内存减少文件的操作。根据我的使用经验:缺省Indexer是每20条记录索引后写入一次,每将MERGE_FACTOR增加50倍,索引速度可以提高1倍左右。
搜索过程优化
Lucene面向全文检索的优化在于首次索引检索后,并不把所有的记录(Document)具体内容读取出来,而起只将所有结果中匹配度最高的头100条结果(TopDocs)的ID放到结果集缓存中并返回,这里可以比较一下数据库检索:如果是一个10,000条的数据库检索结果集,数据库是一定要把所有记录内容都取得以后再开始返回给应用结果集的。所以即使检索匹配总数很多,Lucene的结果集占用的内存空间也不会很多。对于一般的模糊检索应用是用不到这么多的结果的,头100条已经可以满足90%以上的检索需求。
如果首批缓存结果数用完后还要读取更后面的结果时Searcher会再次检索并生成一个上次的搜索缓存数大1倍的缓存,并再重新向后抓取。所以如果构造一个Searcher去查1-120条结果,Searcher其实是进行了2次搜索过程:头100条取完后,缓存结果用完,Searcher重新检索再构造一个200条的结果缓存,依此类推,400条缓存,800条缓存。由于每次Searcher对象消失后,这些缓存也访问那不到了,你有可能想将结果记录缓存下来,缓存数尽量保证在100以下以充分利用首次的结果缓存,不让Lucene浪费多次检索,而且可以分级进行结果缓存。
Lucene的另外一个特点是在收集结果的过程中将匹配度低的结果自动过滤掉了。这也是和数据库应用需要将搜索的结果全部返回不同之处。
http://www.chedong.com/tech/lucene.html
下面以一个简单的例子说明一下使用Lucene.Net的方法:
这个例子是一个搜索博客的例子。(字段有ID、Title、Body、HitCount、Author、DateCreated)
首先我们先来认识几个Lucene.Net索引有关的概念:
索引(Index) 可以理解为最终建好的索引文件
段(Segment) 可以理解为一个子索引,前面也有提到,Lucene并不是维护一个索引文件,而是在扩展索引的时候不断创建新的索引文件,然后定期的把这些新的小索引文件合并到原先的大索引中
文档(Document) 可以理解为数据库中的一条记录(一个 HTML 页面,一封电子邮件、一个文本文件、字符串等),用户提供的一条记录经过索引之后,就是以一个Document的形式存储在索引文件中的。
字段(Field) 可以理解为数据库的一个字段,用来描述一个文档的某个属性,一个document通常被分成几个field,用于保存不同的信息。Field有两个属性可选:存储和索引。通过存储属性你可以控制是否对这个Field进行存储;通过索引属性你可以控制是否对该Field进行索引。
词条(Term)就是一个字串,是搜索的基本单位,表示文档的一个词语。一般每个field由多个term组成, term由两部分组成:它表示的词语(及本身的值)和这个词语所出现的field(及属于哪一field)。所以两个不同的field里的相同的词语并不是一个Term.
tocken:tocken是term的一次出现,它包含trem文本和相应的起止偏移,以及一个类型字符串。一句话中可以出现多次相同的词语,它们都用同一个term表示,但是用不同的tocken,每个tocken标记该词语出现的地方。
个人理解term就是通过分词后的那些词语(字符串)(当然可以不分词直接索引整个内容,如id什么的),然后这些term属于哪个Field属于哪个document都被记录了下来,甚至这个term在Field中出现的次数及距离文本起始位置的偏移量都可以被记录了下来。
http://www.cnblogs.com/jevo/archive/2013/04/24/3079807.html
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 public static void BuildIndex(string indexPath,bool isCreated,bool isOptimize,List<Document> docs) 2 { 3 //创建盘古分词器 4 PanGuAnalyzer analyzer = new PanGuAnalyzer(); 5 6 //创建写索引器(索引路径、分词器实例、是覆盖索引还是追加索引) 7 IndexWriter writer = new IndexWriter(indexPath, analyzer, isCreated); 8 9 try 10 { 11 //将记录写入索引 12 foreach (Document doc in docs) 13 { 14 writer.AddDocument(doc); 15 } 16 17 //优化创建索引 18 if (isOptimize) 19 { 20 writer.Optimize(); 21 } 22 } 23 finally { 24 25 //关闭写索引器 26 writer.Close(); 27 } 28 }
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 public static void DeleteIndex(string indexPath,Term term) 2 { 3 //创建盘古分词器 4 PanGuAnalyzer analyzer = new PanGuAnalyzer(); 5 6 //创建写索引器(索引路径、分词器实例、是覆盖索引还是追加索引) 7 IndexWriter writer = new IndexWriter(indexPath, analyzer, isCreated); 8 write.DeleteDocuments(term); 9 write.Close(); 10 }
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 public static void UpdateIndex(string indexPath, Term term, Document document) 2 { 3 //创建盘古分词器 4 PanGuAnalyzer analyzer = new PanGuAnalyzer(); 5 6 //创建写索引器(索引路径、分词器实例、是覆盖索引还是追加索引) 7 IndexWriter writer = new IndexWriter(indexPath, analyzer, isCreated); 8 write.UpdateDocument(term, document); 9 write.Close(); 10 }
值得一提的是,Lucene的更新实际上是先删除再添加,因此在更新时对于那些没有添加到Document的Field,其值将被致空。
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 /// <summary> 2 /// 查询 3 /// </summary> 4 /// <param name="indexPath">索引物理路径</param> 5 /// <param name="Query">查询对象</param> 6 /// <param name="sort">排序对象</param> 7 /// <param name="pageIndex">当前页码</param> 8 /// <param name="pageSize">每页大小</param> 9 /// <returns>Document对象集合</returns> 10 public static List<Document> QuerySearch(string indexPath, Query query,Filter filter,Sort sort,int pageIndex,int pageSize,out int totalRecord,out string totalTime) 11 { 12 Stopwatch stopwatch = new Stopwatch(); 13 14 //创建读索引器及索引搜索器 15 IndexReader reader=null; 16 IndexSearcher searcher = SearchIntance(indexPath,out reader); 17 18 //搜索 19 stopwatch.Start(); 20 Hits hits = searcher.Search(query,filter,sort); 21 stopwatch.Stop(); 22 23 //对搜索结果进行分页处理 24 List<Document> documents = pagingDocument(pageIndex, pageSize, hits); 25 26 //总记录数和总耗时 27 totalRecord = hits.Length(); 28 totalTime = stopwatch.Elapsed.TotalMilliseconds.ToString(); 29 30 //关闭读索引器及索引搜索器 31 SearchClose(searcher, reader); 32 33 return documents; 34 } 35 36 /// <summary> 37 /// 对搜索结果进行分页处理 38 /// </summary> 39 /// <param name="pageIndex">当前页码</param> 40 /// <param name="pageSize">每页大小</param> 41 /// <param name="hits">searcher.Search的搜索结果集</param> 42 /// <returns>Document对象集合</returns> 43 private static List<Document> pagingDocument(int pageIndex, int pageSize, Hits hits) 44 { 45 //对结果进行分页 46 List<Document> documents = new List<Document>(); 47 int currentIndex = (pageIndex - 1) * pageSize; 48 int maxIndex = pageIndex * pageSize - 1; 49 for (int i = 0; i < hits.Length(); i++) 50 { 51 if (i < currentIndex) 52 { 53 continue; 54 } 55 documents.Add(hits.Doc(i)); 56 if (i > maxIndex) 57 { 58 break; 59 } 60 } 61 return documents; 62 }
Lucene提供的搜索方式有很多,下面会提到,另外最新版本的Lucene已经支持分页了。
接着是在项目中根据具体业务调用上述方法:
在创建索引之前,我们先来看看创建Field时需要知道的内容:
对Filed中相关参数的解释:
Field.Store参数指示是否对这个Field进行存储。
Field.Store.YES:存储字段值(未分词前的字段值)
Field.Store.NO:不存储,存储与索引没有关系
Field.Store.COMPRESS:压缩存储,用于长文本或二进制,但性能受损
Field.Index参数指示记录是否进行索引
Field.Index.ANALYZED:分词且建索引
Field.Index.ANALYZED_NO_NORMS:分词建索引,但是Field的值不像通常那样被保存,而是只取一个byte,这样节约存储空间(ANALYZED存储了index time,boost information等norms,而ANALYZED_NO_NORMS不存储)
Field.Index.NOT_ANALYZED:不分词但索引,即不使用 analyzer分析,整体作为一个token,常用语精确匹配,例如文件名,ID号等就用这个
Field.Index.NOT_ANALYZED_NO_NORMS:不分词建索引,Field的值取一个byte保存
term 就是analyzer分词后的词组。 每一个document都含有一个term vector,TermVector表示文档的条目(由一个Document和Field定位)和它们在当前文档中所出现的次数, 存储了这个document含有的term(unique,如果某个term出现多次也只存一个),以及这个term出现在field 中的position,以及offset。这些信息可以用来以后高亮一个选中的term等等。Field.TermVector参数指示index是否存储term vector。
Field.TermVector.YES:为每个文档(Document)存储该字段的TermVector
Field.TermVector.NO:不存储TermVector
Field.TermVector.WITH_POSITIONS:存储位置
Field.TermVector.WITH_OFFSETS:存储偏移量
Field.TermVector.WITH_POSITIONS_OFFSETS:存储位置和偏移量
存储和索引的组合如下:
Field.Index Field.Store 说明
TOKENIZED(分词) YES 被分词索引且存储
TOKENIZED NO 被分词索引但不存储
NO YES 将不能被搜索,但可作用被搜索内容的附属内容
UN_TOKENIZED YES/NO 不分词,将被作为一个整体被搜索,不能进行部分搜索
NO NO 无此种用法
故:
1.如果某字段需要进行搜索,则要使用Field.Index.TOKENIZED或Field.Index.UN_TOKENIZED。通常进行模糊搜索的字段就用TOKENIZED,TOKENIZED会对Field的内容进行分词;需要精心精确搜索的字段就用UN_TOKENIZED,UN_TOKENIZED不会进行分词,只有全词匹配,该Field才会被选中。
2.对于只需要随搜索结果显示而不需要按照其内容进行搜索的字段,使用Field.Index.NO。
3.如果Field.Store是No,则无法在搜索结果中从索引数据直接提取该域的值,会返回null。
权重Boost
默认情况下,搜索结果以Document.Score作为排序依据,该数值越大排名越靠前。
Score其大致影响因素包括:
1与关键字在文档中出现的频率成正比
2与权重成正比
3与反转文档频率成正比(该值主要受文档总数和包含关键字的文档数量影响,与文档总数成正比,与包含关键字的文档总数成反比,即索引库中文档越多,包含此关键字的文档越少,反转文档频率越高)
4与保有率成正比(保有率主要受到关键字在Fleld中出现的次数(词频)和Field的长度(Field包含的词数)影响,与词频成正比,与Field的长度成反比,即从越短的Field中搜索出越多的关键字,我们就认为保有率高)
Boost的默认值为1,通过改变权重我们可以影响查询结果。其代码形如:
“document.SetBoost(2F);” 改变Document的权重,将影响所有Field的搜索得分。
“document.GetField("FieldName").SetBoost(2F);” 只改变某个Field的权重。
boost的数值存储在Norms中,因此要注意Index的设置,设置NO_NORMS将节省索引空间,但是将不支持权重。
权重的调整建议:
标题权重一般比内容高 标题更能够非常准确地描述文档的内容,而且长度比较短,提高权重不会造成严重的影响。
不要把包含大量索引的文档的权重设置过高 文档中能索引的词越多,对搜索的影响越大,例如在搜索如“好的”这样常用的词汇时
,这篇文章也将位列榜首,但并不是我们需要的。
如果能靠设置Field的权重来解决,就不要设置Document的权重 原因与上面的类似,当我们要改变某些关键字的搜索结果时,要尽量
减少对其它关键字搜索的影响。
考虑降低权重 对于某些没有意义的文档,考虑降低权重来为相对提升其它文档的搜索排位。
http://www.cnblogs.com/MeteorSeed/archive/2012/12/24/2703716.html
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 protected void Button1_Click(object sender, EventArgs e) 2 { 3 string indexPath=Server.MapPath("~/App_Data/Blog"); 4 5 Document document = new Document(); 6 document.Add(new Field("ID",TextBox1.Text,Field.Store.YES,Field.Index.UN_TOKENIZED)); 7 document.Add(new Field("Title", TextBox2.Text, Field.Store.YES, Field.Index.TOKENIZED)); 8 document.Add(new Field("Content", TextBox3.Text, Field.Store.YES, Field.Index.TOKENIZED)); 9 document.Add(new Field("Author", TextBox4.Text, Field.Store.YES, Field.Index.UN_TOKENIZED)); 10 document.Add(new Field("HitCount", TextBox11.Text, Field.Store.YES, Field.Index.UN_TOKENIZED)); 11 document.Add(new Field("DateCreated",DateTime.Now.ToString(),Field.Store.YES, Field.Index.UN_TOKENIZED)); 12 13 List<Document> documents = new List<Document>(); 14 documents.Add(document); 15 16 LuceneIndex.BuildIndex(indexPath, true, true,documents); 17 }
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 protected void Button4_Click(object sender, EventArgs e) 2 { 3 string indexPath = Server.MapPath("~/App_Data/Blog"); 4 5 Document document = new Document(); 6 document.Add(new Field("ID", TextBox7.Text, Field.Store.YES, Field.Index.TOKENIZED)); 7 document.Add(new Field("Title", TextBox8.Text, Field.Store.YES, Field.Index.TOKENIZED)); 8 document.Add(new Field("Content", TextBox9.Text, Field.Store.YES, Field.Index.TOKENIZED)); 9 document.Add(new Field("Author", TextBox10.Text, Field.Store.YES, Field.Index.UN_TOKENIZED)); 10 document.Add(new Field("HitCount", TextBox13.Text, Field.Store.YES, Field.Index.UN_TOKENIZED)); 11 document.Add(new Field("DateCreated", DateTime.Now.ToString(), Field.Store.YES, Field.Index.UN_TOKENIZED)); 12 13 Term term = new Term("ID",TextBox7.Text); 14 15 LuceneIndex.UpdateIndex(indexPath, term, document); 16 }
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 protected void Button5_Click(object sender, EventArgs e) 2 { 3 string indexPath = Server.MapPath("~/App_Data/Blog"); 4 5 Term term = new Term("ID",TextBox14.Text); 6 7 LuceneIndex.DeleteIndex(indexPath, term); 8 }
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 protected void Button3_Click(object sender, EventArgs e) 2 { 3 string keyword = TextBox5.Text; 4 5 string indexPath = Server.MapPath("~/App_Data/Blog"); 6 7 string[] panguKeywords = LuceneIndex.GetKeyWordSplitBySpace(keyword,true).Split(','); 8 BooleanQuery booleanQuery = new BooleanQuery(); 9 booleanQuery.Add(new TermQuery(new Term("Title", keyword)), BooleanClause.Occur.SHOULD); 10 booleanQuery.Add(new TermQuery(new Term("Content", keyword)), BooleanClause.Occur.SHOULD); 11 booleanQuery.Add(new TermQuery(new Term("Author", keyword)), BooleanClause.Occur.SHOULD); 12 foreach (string panguKeyword in panguKeywords) 13 { 14 if (!string.IsNullOrEmpty(panguKeyword)) 15 { 16 booleanQuery.Add(new TermQuery(new Term("Title", panguKeyword)), BooleanClause.Occur.SHOULD); 17 booleanQuery.Add(new TermQuery(new Term("Content", panguKeyword)), BooleanClause.Occur.SHOULD); 18 booleanQuery.Add(new TermQuery(new Term("Author", panguKeyword)), BooleanClause.Occur.SHOULD); 19 } 20 } 21 22 Sort sort=new Sort(); 23 if (!string.IsNullOrEmpty(TextBox6.Text) || !string.IsNullOrEmpty(TextBox12.Text)) 24 { 25 SortField sf = null; 26 if (!string.IsNullOrEmpty(TextBox6.Text)) 27 { 28 sf = new SortField("DateCreated", SortField.STRING, bool.Parse(TextBox6.Text)); 29 } 30 if (!string.IsNullOrEmpty(TextBox12.Text)) 31 { 32 sf = new SortField("HitCount", SortField.INT, bool.Parse(TextBox12.Text)); 33 } 34 sort.SetSort(sf); 35 } 36 37 if (!string.IsNullOrEmpty(TextBox15.Text) || !string.IsNullOrEmpty(TextBox16.Text)) 38 { 39 Term startTime=new Term("DateCreated",TextBox15.Text); 40 Term endTime=new Term("DateCreated",TextBox16.Text); 41 RangeQuery rangeQuery = new RangeQuery(startTime, endTime, false); 42 booleanQuery.Add(rangeQuery, BooleanClause.Occur.MUST); 43 } 44 45 int totalRecord = 0; 46 string totalTime = null; 47 List<Document> documents = LuceneIndex.QuerySearch(indexPath,booleanQuery,null,sort,1,10000,out totalRecord,out totalTime); 48 49 Label1.Text = totalRecord.ToString(); 50 Label2.Text = totalTime+"毫秒"; 51 string htmlStr = null; 52 foreach (Document document in documents) 53 { 54 htmlStr += "<div><br/><span>ID:" + document.Get("ID") + "</span><br/><br/>" + 55 "<span>作者:" + LuceneIndex.HighLight(keyword, document.Get("Author"),100) + "</span><br/><br/>" + 56 "<span>时间:" + LuceneIndex.HighLight(keyword, document.Get("DateCreated"), 100) + "</span><br/><br/>" + 57 "<span>点击:" + LuceneIndex.HighLight(keyword, document.Get("HitCount"), 100) + "</span><br/><br/>" + 58 "<span>标题:" + LuceneIndex.HighLight(keyword, document.Get("Title"),100) + "</span><br/><br/></div>" + 59 "<div>内容:" + LuceneIndex.HighLight(keyword,document.Get("Content"),1000) + "</div>"; 60 } 61 62 Label3.Text = htmlStr; 63 }
以上第7到第18行是组装查询条件的过程,其中把关键字的分词及不分词形式都加到了查询对象里了,对关键字的分词用了Pangu的分词方法,代码如下:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 /// <summary> 2 /// 对关键字进行分词(分词后得到的字符串进行查询可参考下面注释掉的代码) 3 /// </summary> 4 /// <param name="keyWords">要搜索的词</param> 5 /// <param name="IsComma">分的词用逗号分隔true 以{0}^{1}.0的形式false</param> 6 /// <returns>分词后的结果</returns> 7 public static string GetKeyWordSplitBySpace(string keyWords,bool IsComma) 8 { 9 PanGuTokenizer panGuTokenizer = new PanGuTokenizer(); 10 11 StringBuilder result = new StringBuilder(); 12 ICollection<WordInfo> words = panGuTokenizer.SegmentToWordInfos(keyWords); 13 14 foreach (WordInfo word in words) 15 { 16 if (word == null) 17 { 18 continue; 19 } 20 if (IsComma) 21 { 22 result.Append(word.Word + ","); 23 } 24 else 25 { 26 result.AppendFormat("{0}^{1}.0 ", word.Word, (int)Math.Pow(3, word.Rank)); 27 } 28 } 29 30 return result.ToString().Trim(); 31 32 //keyWords = GetKeyWordsSplitBySpace(keyWords, new PanGuTokenizer()); 33 //QueryParser queryParser = new QueryParser("contents", new PanGuAnalyzer(true)); 34 //Query query = queryParser.Parse(keyWords); 35 36 //QueryParser titleQueryParser = new QueryParser("title", new PanGuAnalyzer(true)); 37 //Query titleQuery = titleQueryParser.Parse(keyWords); 38 39 //BooleanQuery bq = new BooleanQuery(); 40 //bq.Add(query, BooleanClause.Occur.SHOULD); 41 //bq.Add(titleQuery, BooleanClause.Occur.SHOULD); 42 43 //Hits hits = search.Search(bq); 44 }
Lucene的搜索相当强大,它提供了很多辅助查询类,每个类都继承自Query类,各自完成一种特殊的查询,你可以像搭积木一样将它
们任意组合使用,完成一些复杂查询;另外lucene还提供了Sort类对结果进行排序,提供了Filter类对查询条件进行限制。
TermQuery: 首先介绍最基本的查询,如果你想执行一个这样的查询:在content字段中查询包含‘刘备的document”,那么你可以
用TermQuery
Term t = new Term("content", "刘备");
Query query = new TermQuery(t);
BooleanQuery :如果你想这么查询:在content字段中包含”刘备“或在title字段包含”三国“的document”,那么你可以建立
两个TermQuery并把它们用BooleanQuery连接起来:
TermQuery termQuery1 = new TermQuery(new Term("content", "刘备"));
TermQuery termQuery2 = new TermQuery(new Term("title", "三国"));
BooleanQuery booleanQuery = new BooleanQuery();
booleanQuery.Add(termQuery1, BooleanClause.Occur.SHOULD);
booleanQuery.Add(termQuery2, BooleanClause.Occur.SHOULD);
WildcardQuery :如果你想对某单词进行通配符查询,你可以用WildcardQuery,通配符包括’?’匹配一个任意字符和’*’匹配零
个或多个任意字符,例如你搜索’三国*’,你可能找到’三国演义’或者’三国志’:
Query query = new WildcardQuery(new Term("content", "三国*"));
PhraseQuery :你可能对中日关系比较感兴趣,想查找‘中’和‘日’挨得比较近(5个字的距离内)的文章,超过这个距离的不予
考虑,你可以:
PhraseQuery query = new PhraseQuery();
query.SetSlop(5);
query.Add(new Term("content ", "中"));
query.Add(new Term("content", "日"));
那么它可能搜到“中日合作……”、“中方和日方……”,但是搜不到“中国某高层领导说日本欠扁”。
PrefixQuery :如果你想搜以‘中’开头的词语,你可以用PrefixQuery:
PrefixQuery query = new PrefixQuery(new Term("content ", "中"));
FuzzyQuery :FuzzyQuery用来搜索相似的term,使用Levenshtein算法。假设你想搜索跟‘wuzza’相似的词语,你可以:
Query query = new FuzzyQuery(new Term("content", "wuzza")); 你可能得到‘fuzzy’和‘wuzzy’。
RangeQuery:另一个常用的Query是RangeQuery,你也许想搜索时间域从20060101到20060130之间的document,你可以用RangeQuery
:
RangeQuery query = new RangeQuery(new Term("time","20060101"), new Term("time","20060130"), true);
最后的true表示用闭合区间。
过滤
使用Filter及其派生类完成对结果集的过滤,也可以定义自己的过滤器。使用过滤的代码形如:
RangeFilter filter = new RangeFilter("CreateTime", "19990101", "29991010", true, true);
Search.Hits hits = searcher.Search(query, filter,sort); //搜索
接下里了解一下有关盘古分词的使用
首先,盘古是一款不错的国产分词工具 http://pangusegment.codeplex.com/
http://www.cnblogs.com/eaglet/archive/2009/08/13/1545420.html
其专门实现了Lucene.Net分词器接口,封装在了PanGu.Lucene.Analyzer.dll里面,使用时代码如下:
IndexWriter writer = new IndexWriter(indexPath, new PanGuAnalyzer(), isCreated);
这样一来,在创建索引时就会使用盘古的分词算法对语句进行分词了。
有一点需要说明的是盘古分词还提供了一个字典文件位于Dictionaries目录下---Dict.dct
这里面默认有10多万的词语,上面提到的词表分词就是通过这个字典进行分词的。另外,该目录下还有其他几个txt文件,比如:ChsSingleName.txt是专门针对中国人名进行分词的词表,Synonym.txt是近义词文件。
当启动盘古分词时,盘古分词会根据路径去找这个dct文件,而这个路径是在PanGu.xml里配置的,而这个PanGu.xml需要放在与PanGu.dll、PanGu.Lucene.Analyzer.dll等同一个目录下,即bin目录下。而Dict.dct的路径可以通过修改PanGu.xml进行配置。接下来我们就看一下PanGu.xml
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
<?xml version="1.0" encoding="utf-8"?> <PanGuSettings xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns="http://www.codeplex.com/pangusegment"> <DictionaryPath>Dictionaries</DictionaryPath> <MatchOptions> <ChineseNameIdentify>true</ChineseNameIdentify><!--中文名识别--> <FrequencyFirst>true</FrequencyFirst><!--词频优先--> <MultiDimensionality>true</MultiDimensionality><!--多元分词--> <EnglishMultiDimensionality>false</EnglishMultiDimensionality><!--英文多元分词,这个开关,会将英文中的字母与数字分开--> <FilterStopWords>true</FilterStopWords><!--过滤停用词--> <IgnoreSpace>true</IgnoreSpace><!---忽略空格、回车、Tab--> <ForceSingleWord>false</ForceSingleWord><!--强制一元分词--> <TraditionalChineseEnabled>false</TraditionalChineseEnabled><!--繁体字分词--> <OutputSimplifiedTraditional>false</OutputSimplifiedTraditional><!--同时输出简体和繁体--> <UnknowWordIdentify>true</UnknowWordIdentify><!--未登录词识别--> <FilterEnglish>false</FilterEnglish><!--过滤英文,这个选项只有在过滤停用词选项生效时才有效--> <FilterNumeric>false</FilterNumeric><!--过滤数字,这个选项只有在过滤停用词选项生效时才有效--> <IgnoreCapital>false</IgnoreCapital><!--忽略英文大小写--> <EnglishSegment>true</EnglishSegment><!--英文分词--> <SynonymOutput>false</SynonymOutput><!--同义词输出,一般用于对搜索字符串的分词,不建议在索引时使用--> <WildcardOutput>false</WildcardOutput><!--通配符匹配输出,一般用于对搜索字符串的分词,不建议在索引时使用--> <WildcardSegment>false</WildcardSegment><!--对通配符匹配的结果分词--> <CustomRule>false</CustomRule><!--是否进行用户自定义规则匹配--> </MatchOptions> <Parameters> <Redundancy>0</Redundancy><!--多元分词冗余度--> <UnknowRank>1</UnknowRank><!--未登录词权值--> <BestRank>5</BestRank><!--最匹配词权值--> <SecRank>3</SecRank><!--次匹配词权值--> <ThirdRank>2</ThirdRank><!--再次匹配词权值--> <SingleRank>1</SingleRank><!--强行输出的单字权值--> <NumericRank>1</NumericRank><!--数字的权值--> <EnglishRank>5</EnglishRank><!--英文词汇的权值--> <SymbolRank>1</SymbolRank><!--符号的权值 --> <SimplifiedTraditionalRank>1</SimplifiedTraditionalRank><!--强制同时输出简繁汉字时,非原来文本的汉字输出权值(比如原来文本是简体,这里就是输出的繁体字的权值,反之亦然。)--> <SynonymRank>1</SynonymRank><!--同义词权值--> <WildcardRank>1</WildcardRank><!--通配符匹配结果的权值--> <FilterEnglishLength>50</FilterEnglishLength><!--过滤英文选项生效时,过滤大于这个长度的英文。--> <FilterNumericLength>50</FilterNumericLength><!--过滤数字选项生效时,过滤大于这个长度的数字。--> </Parameters> </PanGuSettings>
http://wenku.baidu.com/view/50d19515f18583d0496459ef.html
对于Dict.dct里的词语,PanGu专门提供了一个桌面程序进行维护,注意每次修改完必须保存到Dict.dct才能保证修改生效。
同样通过一个Demo.exe可以维护PanGu.xml里的配置
这样一来bin目录如下:
有一个PanGu提供的高亮工具PanGu.HightLight.dll
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 /// <summary> 2 /// 高亮 3 /// </summary> 4 /// <param name="keyword">要高亮的字符串</param> 5 /// <param name="content">content</param> 6 /// <param name="fragmentSize">每个摘要字段的字符数</param> 7 /// <returns>高亮后的content</returns> 8 public static string HighLight(string keyword,string content,int fragmentSize) 9 { 10 //创建HTMLFormatter,参数为高亮搜索词的HTML代码 11 SimpleHTMLFormatter simpleHTMLFormatter = new SimpleHTMLFormatter("<font color='red'>","</font>"); 12 13 //创建高亮,输入HTML代码和 盘古对象Semgent 14 Highlighter highter = new Highlighter(simpleHTMLFormatter, new Segment()); 15 //设置每个摘要字段的字符数 16 highter.FragmentSize = fragmentSize; 17 18 string highlightStr = highter.GetBestFragment(keyword, content); 19 20 if (string.IsNullOrEmpty(highlightStr)) 21 return content; 22 23 return highlightStr; 24 }