基于Java的全文索引/检索引擎——Lucene
Lucene不是一个完整的全文索引应用,而是是一个用Java写的全文索引引擎工具包,它可以方便的嵌入到各种应用中实现针对应用的全文索引/检索功能。
Lucene的作者:Lucene的贡献者Doug Cutting是一位资深全文索引/检索专家,曾经是V-Twin搜索引擎(Apple的Copland操作系统的成就之一)的主要开发者,后在Excite担任高级系统架构设计师,目前从事于一些INTERNET底层架构的研究。他贡献出的Lucene的目标是为各种中小型应用程序加入全文检索功能。
Lucene的发展历程:早先发布在作者自己的www.lucene.com,后来发布在SourceForge,2001年年底成为APACHE基金会jakarta的一个子项目:http://jakarta.apache.org/lucene/
全文检索的实现机制
Lucene的API接口设计的比较通用,输入输出结构都很像数据库的表==>记录==>字段,所以很多传统的应用的文件、数据库等都可以比较方便的映射到Lucene的存储结构/接口中。总体上看:可以先把Lucene当成一个支持全文索引的数据库系统。
比较一下Lucene和数据库:
Lucene | 数据库 |
索引数据源:doc(field1,field2...) doc(field1,field2...) |
索引数据源:record(field1,field2...) record(field1..) |
Document:一个需要进行索引的“单元” 一个Document由多个字段组成 |
Record:记录,包含多个字段 |
Field:字段 | Field:字段 |
Hits:查询结果集,由匹配的Document组成 | RecordSet:查询结果集,由多个Record组成 |
通常比较厚的书籍后面常常附关键词索引表(比如:北京:12, 34页, 上海:3,77页……),它能够帮助读者比较快地找到相关内容的页码。
而数据库索引能够大大提高查询的速度原理也是一样,想像一下通过书后面的索引查找的速度要比一页一页地翻内容高多少倍……而索引之所以效率高,另外一个原因是它是排好序的。对于检索系统来说核心是一个排序问题。
建立一个高效检索系统的关键是建立一个类似于科技索引一样的反向索引机制,将数据源(比如多篇文章)排序顺序存储的同时,有另外一个排好序的关键词列表,用于存储关键词==>文章映射关系,利用这样的映射关系索引:[关键词==>出现关键词的文章编号,出现次数(甚至包括位置:起始偏移量,结束偏移量),出现频率],检索过程就是把模糊查询变成多个可以利用索引的精确查询的逻辑组合的过程。从而大大提高了多关键词查询的效率,所以,全文检索问题归结到最后是一个排序问题。
全文检索和数据库应用最大的不同在于:让最相关的头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个机制的结合。
lucene的组成结构
对于外部应用来说索引模块(index)和检索模块(search)是主要的外部应用入口
org.apache.Lucene.search/ | 搜索入口 |
org.apache.Lucene.index/ | 索引入口 |
org.apache.Lucene.analysis/ | 语言分析器 |
org.apache.Lucene.queryParser/ | 查询分析器 |
org.apache.Lucene.document/ | 存储结构 |
org.apache.Lucene.store/ | 底层IO/存储结构 |
org.apache.Lucene.util/ | 一些公用的数据结构 |
索引过程:从命令行读取文件名(多个),将文件分路径(path字段)和内容(body字段)2个字段进行存储,并对内容进行全文索引:
索引的单位是Document对象,每个Document对象包含多个字段Field对象,针对不同的字段属性和数据输出的需求,对字段还可以选择不同的索引/存储字段规则,
列表如下:
方法 | 切词 | 索引 | 存储 | 用途 |
---|---|---|---|---|
Field.Text(String name, String value) | Yes | Yes | Yes | 切分词索引并存储,比如:标题,内容字段 |
Field.Text(String name, Reader value) | Yes | Yes | No | 切分词索引不存储,比如:META信息, 不用于返回显示,但需要进行检索内容 |
Field.Keyword(String name, String value) | No | Yes | Yes | 不切分索引并存储,比如:日期字段 |
Field.UnIndexed(String name, String value) | No | No | Yes | 不索引,只存储,比如:文件路径 |
Field.UnStored(String name, String value) | Yes | Yes | No | 只全文索引,不存储 |
索引过程中可以看到:
- 语言分析器提供了抽象的接口,因此语言分析(Analyser)是可以定制的,虽然lucene缺省提供了2个比较通用的分析器SimpleAnalyser和StandardAnalyser,这2个分析器缺省都不支持中文,所以要加入对中文语言的切分规则,需要修改这2个分析器。
- Lucene并没有规定数据源的格式,而只提供了一个通用的结构(Document对象)来接受索引的输入,因此输入的数据源可以是:数据库,WORD文档,PDF文档,HTML文档……只要能够设计相应的解析转换器将数据源构造成成Docuement对象即可进行索引。
- 对于大批量的数据索引,还可以通过调整IndexerWrite的文件合并频率属性(mergeFactor)来提高批量索引的效率。
检索过程和结果显示:
- 搜索结果返回的是Hits对象,可以通过它再访问Document==>Field中的内容。
- 假设根据body字段进行全文检索,可以将查询结果的path字段和相应查询的匹配度(score)打印出来
- 在整个检索过程中,语言分析器,查询分析器,甚至搜索器(Searcher)都是提供了抽象的接口,可以根据需要进行定制。
简化的查询分析器
目前LUCENE支持的语法:
Query ::= ( Clause )* Clause ::= ["+", "-"] [":"] ( | "(" Query ")")
中间的逻辑包括:and or + - &&||等符号,而且还有"短语查询"和针对西文的前缀/模糊查询等,对于一般应用来说,这些功能有一些华而不实,其实能够实现目前类似于Google的查询语句分析功能其实对于大多数用户来说已经够了。
所以,Lucene早期版本的QueryParser仍是比较好的选择。
添加修改删除指定记录(Document)
Lucene提供了索引的扩展机制,因此索引的动态扩展应该是没有问题的,而指定记录的修改也似乎只能通过记录的删除,然后重新加入实现。
如何删除指定的记录呢?
删除的方法也很简单,只是需要在索引时根据数据源中的记录ID专门另建索引,然后利用IndexReader.delete(Termterm)方法通过这个记录ID删除相应的Document。
根据某个字段值的排序功能
lucene缺省是按照自己的相关度算法(score)进行结果排序的,但能够根据其他字段进行结果排序是一个在LUCENE的开发邮件列表中经常提到的问题,很多原先基于数据库应用都需要除了基于匹配度(score)以外的排序功能。
而从全文检索的原理我们可以了解到,任何不基于索引的搜索过程效率都会导致效率非常的低,如果基于其他字段的排序需要在搜索过程中访问存储字段,速度回大大降低,因此非常是不可取的。
但这里也有一个折中的解决方法:在搜索过程中能够影响排序结果的只有索引中已经存储的docID和score这2个参数,所以,基于score以外的排序,其实可以通过将数据源预先排好序,然后根据docID进行排序来实现。这样就避免了在LUCENE搜索结果外对结果再次进行排序和在搜索过程中访问不在索引中的某个字段值。
更通用的输入输出接口
虽然lucene没有定义一个确定的输入文档格式,但越来越多的人想到使用一个标准的中间格式作为Lucene的数据导入接口,然后其他数据,比如PDF只需要通过解析器转换成标准的中间格式就可以进行数据索引了。这个中间格式主要以XML为主,类似实现已经不下4,5个:
数据源: WORD PDF HTML DB other
\ | | | /
XML中间格式
|
Lucene INDEX
索引过程优化
索引一般分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的另外一个特点是在收集结果的过程中将匹配度低的结果自动过滤掉了。这也是和数据库应用需要将搜索的结果全部返回不同之处。
从Lucene学到更多
Luene的确是一个面向对象设计的典范
- 所有的问题都通过一个额外抽象层来方便以后的扩展和重用:你可以通过重新实现来达到自己的目的,而对其他模块则不需要;
- 简单的应用入口Searcher, Indexer,并调用底层一系列组件协同的完成搜索任务;
- 所有的对象的任务都非常专一:比如搜索过程:QueryParser分析将查询语句转换成一系列的精确查询的组合(Query),通过底层的索引读取结构IndexReader进行索引的读取,并用相应的打分器给搜索结果进行打分/排序等。所有的功能模块原子化程度非常高,因此可以通过重新实现而不需要修改其他模块。
- 除了灵活的应用接口设计,Lucene还提供了一些适合大多数应用的语言分析器实现(SimpleAnalyser,StandardAnalyser),这也是新用户能够很快上手的重要原因之一。
这些优点都是非常值得在以后的开发中学习借鉴的。作为一个通用工具包,Lunece的确给予了需要将全文检索功能嵌入到应用中的开发者很多的便利。
此外,通过对Lucene的学习和使用,理解了为什么很多数据库优化设计中要求,比如:
- 尽可能对字段进行索引来提高查询速度,但过多的索引会对数据库表的更新操作变慢,而对结果过多的排序条件,实际上往往也是性能的杀手之一。
- 很多商业数据库对大批量的数据插入操作会提供一些优化参数,这个作用和索引器的merge_factor的作用是类似的,
- 20%/80%原则:查的结果多并不等于质量好,尤其对于返回结果集很大,如何优化这头几十条结果的质量往往才是最重要的。
- 尽可能让应用从数据库中获得比较小的结果集,因为即使对于大型数据库,对结果集的随机访问也是一个非常消耗资源的操作。
Lucene学习示例
1.HelloWorld入门
1 import java.io.BufferedReader; 2 import java.io.File; 3 import java.io.FileReader; 4 import java.io.IOException; 5 6 import org.apache.lucene.analysis.standard.StandardAnalyzer; 7 import org.apache.lucene.document.Document; 8 import org.apache.lucene.document.Field; 9 import org.apache.lucene.index.IndexReader; 10 import org.apache.lucene.index.IndexWriter; 11 import org.apache.lucene.index.IndexWriterConfig; 12 import org.apache.lucene.queryParser.QueryParser; 13 import org.apache.lucene.search.IndexSearcher; 14 import org.apache.lucene.search.Query; 15 import org.apache.lucene.search.ScoreDoc; 16 import org.apache.lucene.search.TopDocs; 17 import org.apache.lucene.store.Directory; 18 import org.apache.lucene.store.FSDirectory; 19 import org.apache.lucene.util.Version; 20 21 /* 【Lucene3.6.2入门系列】第01节_HelloWord 22 * @see 这里只需用到一个lucene-core-3.6.2.jar 23 * @see Lucene官网:http://lucene.apache.org 24 * @see Lucene下载:http://archive.apache.org/dist/lucene/java/ 25 * @see Lucene文档:http://wiki.apache.org/lucene-java/ 26 * @see ------------------------------------------------------------------------------------------------------------- 27 * @see 1)对于全文搜索工具,都是由索引、分词、搜索三部分组成 28 * @see 2)被存储和被索引,是两个独立的概念 29 * @see ------------------------------------------------------------------------------------------------------------- 30 * @see 域的存储选项 31 * @see Field.Store.YES--会把该域中的内容存储到文件中,方便进行文本的还原 32 * @see Field.Store.NO---表示该域中的内容不存储到文件中,但允许被索引,且内容无法完全还原(doc.get("##")) 33 * @see ------------------------------------------------------------------------------------------------------------- 34 * @see 域的索引选项 35 * @see Field.Index.ANALYZED----------------进行分词和索引,适用于标题、内容等 36 * @see Field.Index.NOT_ANALYZED------------进行索引但不分词(如身份证号、姓名、ID等),适用于精确搜索 37 * @see Field.Index.ANALYZED_NOT_NORMS------进行分词但是不存储norms信息,这个norms中包括了创建索引的时间和权值等信息 38 * @see Field.Index.NOT_ANALYZED_NOT_NORMS--即不进行分词也不存储norms信息 39 * @see Field.Index.NO----------------------不进行索引 40 * @see norms:当数据被搜索出来后,便涉及到排序的问题,而排序是有一些评分规则的,于是NORMS中就存储了这些排序的信息 41 * @see ------------------------------------------------------------------------------------------------------------- 42 * @see 域选项最佳实践 43 * @see Field.Store Field.Index 域值 44 * @see YES NOT_ANALYZED_NOT_NORMS 标识符(主键、文件名),电话号码,身份证号,姓名,日期 45 * @see YES ANALYZED 文档标题和摘要 46 * @see NO ANALYZED 文档正文 47 * @see NO NOT_ANALYZED 隐藏关键字 48 * @see YES NO 文档类型,数据库主键(不进行索引) 49 * @see ------------------------------------------------------------------------------------------------------------- 50 * @create Jun 29, 2012 4:20:19 PM 51 * @author 玄玉<http://blog.csdn.net/jadyer> 52 */ 53 public class Lucene_01_HelloWord { 54 private static final String PATH_OF_FILE = "E:/lucene_test/01_file/"; // 待索引文件的目录 55 private static final String PATH_OF_INDEX = "E:/lucene_test/01_index/"; // 存放索引文件的目录2 56 57 /** 58 * 测试时,要在E:/lucene_test/01_file/文件夹中准备几个包含内容的文件(比如txt格式的) 59 * 然后先执行createIndex()方法,再执行searchFile()方法,最后观看控制台输出即可 60 */ 61 public static void main(String[] args) { 62 Lucene_01_HelloWord instance = new Lucene_01_HelloWord(); 63 instance.createIndex(); 64 instance.searchFile(); 65 } 66 67 /** 68 * 创建索引 69 * 70 * @see --------------------------------------------------------------------------------------------------------- 71 * @see 1、创建Directory-----------------指定索引被保存的位置 72 * @see 2、创建IndexWriter---------------通过IndexWriter写索引 73 * @see 3、创建Document对象---------------我们索引的有可能是一段文本or数据库中的一张表 74 * @see 4、为Document添加Field------------相当于Document的标题、大小、内容、路径等等,二者类似于数据库表中每条记录和字段的关系 75 * @see 5、通过IndexWriter添加文档到索引中 76 * @see 6、关闭IndexWriter----------------用完IndexWriter之后,必须关闭之 77 * @see --------------------------------------------------------------------------------------------------------- 78 * @see _0.fdt和_0.fdx文件--保存域中所存储的数据(Field.Store.YES条件下的) 79 * @see _0.fnm文件----------保存域选项的数据(即new Field(name, value)中的name) 80 * @see _0.frq文件----------记录相同的文件(或查询的关键字)出现的次数,它是用来做评分和排序的 81 * @see _0.nrm文件----------存储一些评分信息 82 * @see _0.prx文件----------记录偏移量 83 * @see _0.tii和_0.tis文件--存储索引里面的所有内容信息 84 * @see segments_1文件------它是段文件,Lucene首先会到段文件中查找相应的索引信息 85 * @see --------------------------------------------------------------------------------------------------------- 86 */ 87 private void createIndex() { 88 Directory directory = null; 89 IndexWriter writer = null; 90 Document doc = null; 91 try { 92 // FSDirectory会根据当前的运行环境打开一个合理的基于File的Directory(若在内存中创建索引则new RAMDirectory()) 93 // 这里是在硬盘上"E:/lucene_test/01_index/"文件夹中创建索引 94 directory = FSDirectory.open(new File(PATH_OF_INDEX)); 95 // 由于Lucene2.9之后,其索引的格式就不会再兼容Lucene的所有版本了,所以在创建索引前,要指定其所匹配的Lucene版本号 96 // 这里通过IndexWriterConfig()构造方法的Version.LUCENE_36参数值指明索引所匹配的版本号,并使用了Lucene的标准分词器 97 writer = new IndexWriter(directory, new IndexWriterConfig(Version.LUCENE_36, new StandardAnalyzer(Version.LUCENE_36))); 98 for (File file : new File(PATH_OF_FILE).listFiles()) { 99 doc = new Document(); 100 // 把内容添加到索引域中,即为该文档存储信息,供将来搜索时使用(下面的写法,其默认为Field.Store.NO和Field.Index.ANALYZED) 101 // 如果我们想把content的内容也存储到硬盘上,那就需要先把file转换成字符串,然后按照"fileName"的存储方式加到Field中 102 // 我们可以用commons-io-2.3.jar提供的FileUtils.readFileToString(file),这是很方便的工具包,有了它几乎都不用手写任何的IO方法了 103 // doc.add(new Field("content", FileUtils.readFileToString(file), Field.Store.YES, Field.Index.ANALYZED)); 104 doc.add(new Field("content", new FileReader(file))); 105 // Field.Store.YES-----------这里是将文件的全名存储到硬盘中 106 // Field.Index.NOT_ANALYZED--这里是不对文件名进行分词 107 doc.add(new Field("fileName", file.getName(), Field.Store.YES, Field.Index.NOT_ANALYZED)); 108 doc.add(new Field("filePath", file.getAbsolutePath(), Field.Store.YES, Field.Index.NOT_ANALYZED)); 109 // 通过IndexWriter添加文档到索引中 110 writer.addDocument(doc); 111 } 112 } 113 catch (Exception e) { 114 System.out.println("创建索引的过程中遇到异常,堆栈轨迹如下"); 115 e.printStackTrace(); 116 } 117 finally { 118 if (null != writer) { 119 try { 120 writer.close(); // IndexWriter在用完之后一定要关闭 121 } 122 catch (IOException ce) { 123 System.out.println("关闭IndexWriter时遇到异常,堆栈轨迹如下"); 124 ce.printStackTrace(); 125 } 126 } 127 } 128 } 129 130 private String getContentFromFile(File myFile) { 131 StringBuffer sb = new StringBuffer(); 132 if (!myFile.exists()) { 133 return ""; 134 } 135 try { 136 BufferedReader in = new BufferedReader(new FileReader(myFile)); 137 String str; 138 while ((str = in.readLine()) != null) { 139 sb.append(str); 140 } 141 in.close(); 142 } 143 catch (IOException e) { 144 e.getStackTrace(); 145 } 146 return sb.toString(); 147 } 148 149 /** 150 * 搜索文件 151 * 152 * @see 1、创建Directory 153 * @see 2、创建IndexReader 154 * @see 3、根据IndexReader创建IndexSearcher 155 * @see 4、创建搜索的Query 156 * @see 5、根据searcher搜索并返回TopDocs 157 * @see 6、根据TopDocs获取ScoreDoc对象 158 * @see 7、根据searcher和ScoreDoc对象获取具体的Document对象 159 * @see 8、根据Document对象获取需要的值 160 * @see 9、关闭IndexReader 161 */ 162 private void searchFile() { 163 IndexReader reader = null; 164 try { 165 reader = IndexReader.open(FSDirectory.open(new File(PATH_OF_INDEX))); 166 IndexSearcher searcher = new IndexSearcher(reader); 167 // 创建基于Parser搜索的Query,创建时需指定其"搜索的版本,默认搜索的域,分词器"....这里的域指的是创建索引时Field的名字 168 QueryParser parser = new QueryParser(Version.LUCENE_36, "content", new StandardAnalyzer(Version.LUCENE_36)); 169 Query query = parser.parse("java"); // 指定==>搜索域为content(即上一行代码指定的"content")中包含"java"的文档 170 TopDocs tds = searcher.search(query, 10); // 第二个参数指定搜索后显示的条数,若查到5条则显示为5条,查到15条则只显示10条 171 ScoreDoc[] sds = tds.scoreDocs; // TopDocs中存放的并不是我们的文档,而是文档的ScoreDoc对象 172 for (ScoreDoc sd : sds) { // ScoreDoc对象相当于每个文档的ID号,我们就可以通过ScoreDoc来遍历文档 173 Document doc = searcher.doc(sd.doc); // sd.doc得到的是文档的序号 174 System.out.println(doc.get("fileName") + "[" + doc.get("filePath") + "]"); // 输出该文档所存储的信息 175 } 176 } 177 catch (Exception e) { 178 System.out.println("搜索文件的过程中遇到异常,堆栈轨迹如下"); 179 e.printStackTrace(); 180 } 181 finally { 182 if (null != reader) { 183 try { 184 reader.close(); 185 } 186 catch (IOException e) { 187 System.out.println("关闭IndexReader时遇到异常,堆栈轨迹如下"); 188 e.printStackTrace(); 189 } 190 } 191 } 192 } 193 194 }
2.针对索引文件的CRUD
1 import java.io.File; 2 import java.io.IOException; 3 import java.text.SimpleDateFormat; 4 import java.util.Date; 5 6 import org.apache.lucene.analysis.standard.StandardAnalyzer; 7 import org.apache.lucene.document.Document; 8 import org.apache.lucene.document.Field; 9 import org.apache.lucene.document.NumericField; 10 import org.apache.lucene.index.IndexReader; 11 import org.apache.lucene.index.IndexWriter; 12 import org.apache.lucene.index.IndexWriterConfig; 13 import org.apache.lucene.index.Term; 14 import org.apache.lucene.search.IndexSearcher; 15 import org.apache.lucene.search.Query; 16 import org.apache.lucene.search.ScoreDoc; 17 import org.apache.lucene.search.TermQuery; 18 import org.apache.lucene.search.TopDocs; 19 import org.apache.lucene.store.Directory; 20 import org.apache.lucene.store.FSDirectory; 21 import org.apache.lucene.util.Version; 22 23 public class Lucene_02_HelloIndex { 24 /** 25 * 【Lucene3.6.2入门系列】第02节_针对索引文件的CRUD 26 * 27 * @see ============================================================================================================= 28 * @see Lucene官网:http://lucene.apache.org 29 * @see Lucene下载:http://archive.apache.org/dist/lucene/java/ 30 * @see Lucene文档:http://wiki.apache.org/lucene-java/ 31 * @see ============================================================================================================= 32 * @see 使用Luke查看分词信息(http://code.google.com/p/luke/) 33 * @see 1)引言:每一个Lucene版本都会有一个相应的Luke文件 34 * @see 2)打开:双击或java -jar lukeall-3.5.0.jar 35 * @see 3)选择索引的存放目录后点击OK即可 36 * @see 7)如果我们的索引有改变,可以点击右侧的Re-open按钮重新载入索引 37 * @see 4)Luke界面右下角的Top ranking terms窗口中显示的就是分词信息。其中Rank列表示出现频率 38 * @see 5)Luke菜单下的Documents选项卡中显示的就是文档信息,我们可以根据文档序号来浏览(点击向左和向右的方向箭头) 39 * @see 6)Luke菜单下的Search选项卡中可以根据我们输入的表达式来查文档内容 40 * @see 比如在Enter search expression here:输入content:my,再在右侧点击一个黑色粗体字的Search大按钮即可 41 * @see ============================================================================================================= 42 * @create Jun 30, 2012 4:34:09 PM 43 * @author 玄玉<http://blog.csdn.net/jadyer> 44 */ 45 /* 46 * 定义一组数据,用来演示搜索(这里有一封邮件为例) 47 * 假设每一个变量代表一个Document,这里就定义了6个Document 48 */ 49 // 邮件编号 50 private String[] ids = { "1", "2", "3", "4", "5", "6" }; 51 // 邮件主题 52 private String[] names = { "Michael", "Scofield", "Tbag", "Jack", "Jade", "Jadyer" }; 53 // 邮件地址 54 private String[] emails = { "[email protected]", "[email protected]", "[email protected]", "[email protected]", "[email protected]", "[email protected]" }; 55 // 邮件内容 56 private String[] contents = { "my blog", "my website", "my name", "I am JavaDeveloper", "I am from Haerbin", "I like Lucene" }; 57 // 邮件附件(为数字和日期加索引,与,字符串加索引的方式不同) 58 private int[] attachs = { 9, 3, 5, 4, 1, 2 }; 59 // 邮件日期 60 private Date[] dates = new Date[ids.length]; 61 // 它的创建是比较耗时耗资源的,所以这里只让它创建一次,此时reader处于整个生命周期中,实际应用中也可能直接放到ApplicationContext里面 62 private static IndexReader reader = null; 63 private Directory directory = null; 64 65 public static void main(String[] args) { 66 Lucene_02_HelloIndex instance = new Lucene_02_HelloIndex(); 67 instance.createIndex(); 68 instance.searchFile(); 69 instance.updateIndex(); 70 instance.getDocsCount(); 71 } 72 73 public Lucene_02_HelloIndex() { 74 SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd"); 75 try { 76 dates[0] = (Date) sdf.parse("20120601"); 77 dates[1] = (Date) sdf.parse("20120603"); 78 dates[2] = (Date) sdf.parse("20120605"); 79 dates[3] = (Date) sdf.parse("20120607"); 80 dates[4] = (Date) sdf.parse("20120609"); 81 dates[5] = (Date) sdf.parse("20120611"); 82 directory = FSDirectory.open(new File("E:/lucene_test/01_index/")); 83 } 84 catch (Exception e) { 85 e.printStackTrace(); 86 } 87 } 88 89 /** 90 * 获取IndexReader实例 91 */ 92 private IndexReader getIndexReader() { 93 try { 94 if (reader == null) { 95 reader = IndexReader.open(directory); 96 } 97 else { 98 // if the index was changed since the provided reader was opened, open and return a new reader; else,return null 99 // 如果当前reader在打开期间index发生改变,则打开并返回一个新的IndexReader,否则返回null 100 IndexReader ir = IndexReader.openIfChanged(reader); 101 if (ir != null) { 102 reader.close(); // 关闭原reader 103 reader = ir; // 赋予新reader 104 } 105 } 106 return reader; 107 } 108 catch (Exception e) { 109 e.printStackTrace(); 110 } 111 return null; // 发生异常则返回null 112 } 113 114 /** 115 * 通过IndexReader获取文档数量 116 */ 117 public void getDocsCount() { 118 System.out.println("maxDocs:" + this.getIndexReader().maxDoc()); 119 System.out.println("numDocs:" + this.getIndexReader().numDocs()); 120 System.out.println("deletedDocs:" + this.getIndexReader().numDeletedDocs()); 121 } 122 123 /** 124 * 创建索引 125 */ 126 public void createIndex() { 127 IndexWriter writer = null; 128 Document doc = null; 129 try { 130 writer = new IndexWriter(directory, new IndexWriterConfig(Version.LUCENE_36, new StandardAnalyzer(Version.LUCENE_36))); 131 writer.deleteAll(); // 创建索引之前,先把文档清空掉 132 for (int i = 0; i < ids.length; i++) { // 遍历ID来创建文档 133 doc = new Document(); 134 doc.add(new Field("id", ids[i], Field.Store.YES, Field.Index.NOT_ANALYZED_NO_NORMS)); 135 doc.add(new Field("name", names[i], Field.Store.YES, Field.Index.NOT_ANALYZED_NO_NORMS)); 136 doc.add(new Field("email", emails[i], Field.Store.YES, Field.Index.NOT_ANALYZED)); 137 doc.add(new Field("content", contents[i], Field.Store.NO, Field.Index.ANALYZED)); 138 doc.add(new NumericField("attach", Field.Store.YES, true).setIntValue(attachs[i])); // 为数字加索引(第三个参数指定是否索引) 139 doc.add(new NumericField("date", Field.Store.YES, true).setLongValue(dates[i].getTime())); // 为日期加索引 140 /* 141 * 建立索引时加权 142 * 定义排名规则,即加权,这里是为指定邮件名结尾的emails加权 143 */ 144 if (emails[i].endsWith("jadyer.cn")) { 145 doc.setBoost(2.0f); 146 } 147 else if (emails[i].endsWith("jadyer.me")) { 148 doc.setBoost(1.5f); // 为文档加权....默认为1.0,权值越高则排名越高,显示得就越靠前 149 } 150 else { 151 doc.setBoost(0.5f); // 注意它的参数类型是Float 152 } 153 writer.addDocument(doc); 154 } 155 } 156 catch (Exception e) { 157 e.printStackTrace(); 158 } 159 finally { 160 if (null != writer) { 161 try { 162 writer.close(); 163 } 164 catch (IOException ce) { 165 ce.printStackTrace(); 166 } 167 } 168 } 169 } 170 171 /** 172 * 搜索文件 173 */ 174 public void searchFile() { 175 IndexSearcher searcher = new IndexSearcher(this.getIndexReader()); 176 Query query = new TermQuery(new Term("content", "my")); // 精确搜索:搜索"content"中包含"my"的文档 177 try { 178 TopDocs tds = searcher.search(query, 10); 179 for (ScoreDoc sd : tds.scoreDocs) { 180 Document doc = searcher.doc(sd.doc); // sd.doc得到的是文档的序号 181 // doc.getBoost()得到的权值与创建索引时设置的权值之间是不相搭的,创建索引时的权值的查看需要使用Luke工具 182 // 之所以这样,是因为这里的Document对象(是获取到的)与创建索引时的Document对象,不是同一个对象 183 // sd.score得到的是该文档的评分,该评分规则的公式是比较复杂的,它主要与文档的权值和出现次数成正比 184 System.out.print("(" + sd.doc + "|" + doc.getBoost() + "|" + sd.score + ")" + doc.get("name") + "[" + doc.get("email") + "]-->"); 185 System.out.println(doc.get("id") + "," + doc.get("attach") + "," + new SimpleDateFormat("yyyyMMdd").format(new Date(Long.parseLong(doc.get("date"))))); 186 } 187 } 188 catch (Exception e) { 189 e.printStackTrace(); 190 } 191 finally { 192 if (null != searcher) { 193 try { 194 searcher.close(); 195 } 196 catch (IOException e) { 197 e.printStackTrace(); 198 } 199 } 200 } 201 } 202 203 /** 204 * 更新索引 205 * 206 * @see Lucene其实并未提供更新索引的方法,这里的更新操作内部是先删除再添加的方式 207 * @see 因为Lucene认为更新索引的代价,与删除后重建索引的代价,二者是差不多的 208 */ 209 public void updateIndex() { 210 IndexWriter writer = null; 211 Document doc = new Document(); 212 try { 213 writer = new IndexWriter(directory, new IndexWriterConfig(Version.LUCENE_36, new StandardAnalyzer(Version.LUCENE_36))); 214 doc.add(new Field("id", "1111", Field.Store.YES, Field.Index.NOT_ANALYZED_NO_NORMS)); 215 doc.add(new Field("name", names[0], Field.Store.YES, Field.Index.NOT_ANALYZED_NO_NORMS)); 216 doc.add(new Field("email", emails[0], Field.Store.YES, Field.Index.NOT_ANALYZED)); 217 doc.add(new Field("content", contents[0], Field.Store.NO, Field.Index.ANALYZED)); 218 doc.add(new NumericField("attach", Field.Store.YES, true).setIntValue(attachs[0])); 219 doc.add(new NumericField("date", Field.Store.YES, true).setLongValue(dates[0].getTime())); 220 // 其实它会先删除索引文档中id为1的文档,然后再将这里的doc对象重新索引,所以即便这里的1!=1111,但它并不会报错 221 // 所以在执行完该方法后:maxDocs=7,numDocs=6,deletedDocs=1,就是因为Lucene会先删除再添加 222 writer.updateDocument(new Term("id", "1"), doc); 223 } 224 catch (Exception e) { 225 e.printStackTrace(); 226 } 227 finally { 228 if (null != writer) { 229 try { 230 writer.close(); 231 } 232 catch (IOException ce) { 233 ce.printStackTrace(); 234 } 235 } 236 } 237 } 238 239 /** 240 * 删除索引 241 * 242 * @see ----------------------------------------------------------------------------------------------------- 243 * @see 在执行完该方法后,再执行本类的searchFile()方法,得知numDocs=5,maxDocs=6,deletedDocs=1 244 * @see 这说明此时删除的文档并没有被完全删除,而是存储在一个回收站中,它是可以恢复的 245 * @see ----------------------------------------------------------------------------------------------------- 246 * @see 从回收站中清空索引IndexWriter 247 * @see 对于清空索引,Lucene3.5之前叫做优化,调用的是IndexWriter.optimize()方法,但该方法已被禁用 248 * @see 因为optimize时它会全部更新索引,这一过程所涉及到的负载是很大的,于是弃用了该方法,使用forceMerge代替 249 * @see 使用IndexWriter.forceMergeDeletes()方法可以强制清空回收站中的内容 250 * @see 另外IndexWriter.forceMerge(3)方法会将索引合并为3段,这3段中的被删除的数据也会被清空 251 * @see 但其在Lucene3.5之后不建议使用,因为其会消耗大量的开销,而Lucene会根据情况自动处理的 252 * @see ----------------------------------------------------------------------------------------------------- 253 */ 254 public void deleteIndex() { 255 IndexWriter writer = null; 256 try { 257 writer = new IndexWriter(directory, new IndexWriterConfig(Version.LUCENE_36, new StandardAnalyzer(Version.LUCENE_36))); 258 // 其参数可以传Query或Term....Query指的是可以查询出一系列的结果并将其全部删掉,而Term属于精确查找 259 writer.deleteDocuments(new Term("id", "1")); // 删除索引文档中id为1的文档 260 } 261 catch (Exception e) { 262 e.printStackTrace(); 263 } 264 finally { 265 if (null != writer) { 266 try { 267 writer.close(); 268 } 269 catch (IOException ce) { 270 ce.printStackTrace(); 271 } 272 } 273 } 274 } 275 276 /** 277 * 恢复索引 278 * 279 * @see 建议弃用 280 */ 281 @Deprecated 282 public void unDeleteIndex() { 283 IndexReader reader = null; 284 try { 285 // IndexReader.open(directory)此时该IndexReader默认的readOnly=true,而在恢复索引时应该指定其为非只读的 286 reader = IndexReader.open(directory, false); 287 // Deprecated. Write support will be removed in Lucene 4.0. There will be no replacement for this method. 288 reader.undeleteAll(); 289 } 290 catch (Exception e) { 291 e.printStackTrace(); 292 } 293 finally { 294 if (null != reader) { 295 try { 296 reader.close(); 297 } 298 catch (IOException e) { 299 e.printStackTrace(); 300 } 301 } 302 } 303 } 304 }
3.简述Lucene中常见的搜索功能
1 import java.io.File; 2 import java.io.IOException; 3 import java.text.SimpleDateFormat; 4 import java.util.Date; 5 6 import org.apache.lucene.analysis.standard.StandardAnalyzer; 7 import org.apache.lucene.document.Document; 8 import org.apache.lucene.document.Field; 9 import org.apache.lucene.document.NumericField; 10 import org.apache.lucene.index.IndexReader; 11 import org.apache.lucene.index.IndexWriter; 12 import org.apache.lucene.index.IndexWriterConfig; 13 import org.apache.lucene.index.Term; 14 import org.apache.lucene.queryParser.ParseException; 15 import org.apache.lucene.queryParser.QueryParser; 16 import org.apache.lucene.search.BooleanClause.Occur; 17 import org.apache.lucene.search.BooleanQuery; 18 import org.apache.lucene.search.FuzzyQuery; 19 import org.apache.lucene.search.IndexSearcher; 20 import org.apache.lucene.search.NumericRangeQuery; 21 import org.apache.lucene.search.PhraseQuery; 22 import org.apache.lucene.search.PrefixQuery; 23 import org.apache.lucene.search.Query; 24 import org.apache.lucene.search.ScoreDoc; 25 import org.apache.lucene.search.TermQuery; 26 import org.apache.lucene.search.TermRangeQuery; 27 import org.apache.lucene.search.TopDocs; 28 import org.apache.lucene.search.WildcardQuery; 29 import org.apache.lucene.store.Directory; 30 import org.apache.lucene.store.FSDirectory; 31 import org.apache.lucene.util.Version; 32 33 /** 34 * 【Lucene3.6.2入门系列】第03节_简述Lucene中常见的搜索功能 35 * 36 * @create Aug 1, 2013 3:54:27 PM 37 * @author 玄玉<http://blog.csdn.net/jadyer> 38 */ 39 public class Lucene_03_HelloSearch { 40 private Directory directory; 41 private IndexReader reader; 42 private String[] ids = { "1", "2", "3", "4", "5", "6" }; 43 private String[] names = { "Michael", "Scofield", "Tbag", "Jack", "Jade", "Jadyer" }; 44 private String[] emails = { "[email protected]", "[email protected]", "[email protected]", "[email protected]", "[email protected]", "[email protected]" }; 45 private String[] contents = { "my java blog is http://blog.csdn.net/jadyer", "my website is http://www.jadyer.cn", "my name is jadyer", "I am JavaDeveloper", "I am from Haerbin", "I like Lucene" }; 46 private int[] attachs = { 9, 3, 5, 4, 1, 2 }; 47 private Date[] dates = new Date[ids.length]; 48 49 static Lucene_03_HelloSearch instance = new Lucene_03_HelloSearch(); 50 51 public static void main(String[] args) { 52 instance.searchByTerm("content", "my"); 53 instance.searchByTermRange("name", "M", "o");// 范围,M~O 54 instance.searchByNumericRange("attach", 2, 5); 55 instance.searchByPrefix("content", "b"); 56 instance.searchByWildcard("name", "Ja??er"); 57 instance.searchByFuzzy("name", "Jadk"); 58 instance.searchByPhrase(); 59 instance.searchByQueryParse(); 60 instance.searchPage(); 61 instance.searchPageByAfter(); 62 } 63 64 public void searchPage() { 65 for (File file : new File("E:/lucene_test/01_index/").listFiles()) { 66 file.delete(); 67 } 68 instance = new Lucene_03_HelloSearch(true); 69 instance.searchPage("mycontent:javase", 2, 10); 70 } 71 72 public void searchPageByAfter() { 73 for (File file : new File("E:/lucene_test/01_index/").listFiles()) { 74 file.delete(); 75 } 76 instance = new Lucene_03_HelloSearch(true); 77 instance.searchPageByAfter("mycontent:javase", 3, 10); 78 } 79 80 public Lucene_03_HelloSearch() { 81 IndexWriter writer = null; 82 Document doc = null; 83 SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd"); 84 try { 85 dates[0] = sdf.parse("20120601"); 86 dates[1] = sdf.parse("20120603"); 87 dates[2] = sdf.parse("20120605"); 88 dates[3] = sdf.parse("20120607"); 89 dates[4] = sdf.parse("20120609"); 90 dates[5] = sdf.parse("20120611"); 91 directory = FSDirectory.open(new File("E:/lucene_test/01_index/")); 92 writer = new IndexWriter(directory, new IndexWriterConfig(Version.LUCENE_36, new StandardAnalyzer(Version.LUCENE_36))); 93 writer.deleteAll(); // 创建索引之前,先把文档清空掉 94 for (int i = 0; i < ids.length; i++) { // 遍历ID来创建文档 95 doc = new Document(); 96 doc.add(new Field("id", ids[i], Field.Store.YES, Field.Index.NOT_ANALYZED_NO_NORMS)); 97 doc.add(new Field("name", names[i], Field.Store.YES, Field.Index.ANALYZED_NO_NORMS)); 98 doc.add(new Field("email", emails[i], Field.Store.YES, Field.Index.NOT_ANALYZED)); 99 doc.add(new Field("email", "test" + i + "" + i + "@jadyer.com", Field.Store.YES, Field.Index.NOT_ANALYZED)); 100 doc.add(new Field("content", contents[i], Field.Store.YES, Field.Index.ANALYZED)); 101 doc.add(new NumericField("attach", Field.Store.YES, true).setIntValue(attachs[i])); // 为数字加索引(第三个参数指定是否索引) 102 doc.add(new NumericField("attach", Field.Store.YES, true).setIntValue((i + 1) * 100)); // 假设有多个附件 103 doc.add(new NumericField("date", Field.Store.YES, true).setLongValue(dates[i].getTime())); // 为日期加索引 104 writer.addDocument(doc); 105 } 106 } 107 catch (Exception e) { 108 e.printStackTrace(); 109 } 110 finally { 111 if (null != writer) { 112 try { 113 writer.close(); 114 } 115 catch (IOException ce) { 116 ce.printStackTrace(); 117 } 118 } 119 } 120 } 121 122 /** 123 * 针对分页搜索创建索引 124 */ 125 public Lucene_03_HelloSearch(boolean pageFlag) { 126 String[] myNames = new String[50]; 127 String[] myContents = new String[50]; 128 for (int i = 0; i < 50; i++) { 129 myNames[i] = "file(" + i + ")"; 130 myContents[i] = "I love JavaSE, also love Lucene(" + i + ")"; 131 } 132 IndexWriter writer = null; 133 Document doc = null; 134 try { 135 directory = FSDirectory.open(new File("E:/lucene_test/01_index/")); 136 writer = new IndexWriter(directory, new IndexWriterConfig(Version.LUCENE_36, new StandardAnalyzer(Version.LUCENE_36))); 137 writer.deleteAll(); 138 for (int i = 0; i < myNames.length; i++) { 139 doc = new Document(); 140 doc.add(new Field("myname", myNames[i], Field.Store.YES, Field.Index.NOT_ANALYZED_NO_NORMS)); 141 doc.add(new Field("mycontent", myContents[i], Field.Store.YES, Field.Index.ANALYZED)); 142 writer.addDocument(doc); 143 } 144 } 145 catch (IOException e) { 146 e.printStackTrace(); 147 } 148 finally { 149 if (null != writer) { 150 try { 151 writer.close(); 152 } 153 catch (IOException ce) { 154 ce.printStackTrace(); 155 } 156 } 157 } 158 } 159 160 /** 161 * 获取IndexSearcher实例 162 */ 163 private IndexSearcher getIndexSearcher() { 164 try { 165 if (reader == null) { 166 reader = IndexReader.open(directory); 167 } 168 else { 169 // if the index was changed since the provided reader was opened, open and return a new reader; else,return null 170 // 如果当前reader在打开期间index发生改变,则打开并返回一个新的IndexReader,否则返回null 171 IndexReader ir = IndexReader.openIfChanged(reader); 172 if (ir != null) { 173 reader.close(); // 关闭原reader 174 reader = ir; // 赋予新reader 175 } 176 } 177 return new IndexSearcher(reader); 178 } 179 catch (Exception e) { 180 e.printStackTrace(); 181 } 182 return null; // 发生异常则返回null 183 } 184 185 /** 186 * 执行搜索操作 187 * 188 * @param query 189 * (搜索的Query对象) 190 */ 191 private void doSearch(Query query) { 192 IndexSearcher searcher = this.getIndexSearcher(); 193 try { 194 // 第二个参数指定搜索后显示的最多的记录数,其与tds.totalHits没有联系 195 TopDocs tds = searcher.search(query, 10); 196 System.out.println("本次搜索到[" + tds.totalHits + "]条记录"); 197 for (ScoreDoc sd : tds.scoreDocs) { 198 Document doc = searcher.doc(sd.doc); 199 System.out.println("content =" + doc.get("content") + " "); 200 System.out.print("文档编号=" + sd.doc + " 文档权值=" + doc.getBoost() + " 文档评分=" + sd.score + " "); 201 System.out.print("id=" + doc.get("id") + " email=" + doc.get("email") + " name=" + doc.get("name") + " "); 202 // 获取多个同名域的方式 203 String[] attachValues = doc.getValues("attach"); 204 for (String attach : attachValues) { 205 System.out.print("attach=" + attach + " "); 206 } 207 System.out.println(); 208 } 209 } 210 catch (IOException e) { 211 e.printStackTrace(); 212 } 213 finally { 214 if (null != searcher) { 215 try { 216 searcher.close(); // 记得关闭IndexSearcher 217 } 218 catch (IOException e) { 219 e.printStackTrace(); 220 } 221 } 222 } 223 } 224 225 /** 226 * 精确匹配搜索 227 * 228 * @param fieldName 229 * 域名(相当于表的字段名) 230 * @param keyWords 231 * 搜索的关键字 232 */ 233 public void searchByTerm(String fieldName, String keyWords) { 234 Query query = new TermQuery(new Term(fieldName, keyWords)); 235 this.doSearch(query); 236 } 237 238 /** 239 * 基于范围的搜索 240 * 241 * @param fieldName 242 * 域名(相当于表的字段名) 243 * @param start 244 * 开始字符 245 * @param end 246 * 结束字符 247 */ 248 public void searchByTermRange(String fieldName, String start, String end) { 249 Query query = new TermRangeQuery(fieldName, start, end, true, true); // 后面两个参数用于指定开区间或闭区间 250 this.doSearch(query); 251 } 252 253 /** 254 * 针对数字的搜索 255 */ 256 public void searchByNumericRange(String fieldName, int min, int max) { 257 Query query = NumericRangeQuery.newIntRange(fieldName, min, max, true, true); 258 this.doSearch(query); 259 } 260 261 /** 262 * 基于前缀的搜索 263 * 264 * @see 它是对Field分词后的结果进行前缀查找的结果 265 */ 266 public void searchByPrefix(String fieldName, String prefix) { 267 Query query = new PrefixQuery(new Term(fieldName, prefix)); 268 this.doSearch(query); 269 } 270 271 /** 272 * 基于通配符的搜索 273 * 274 * @see *-->任意多个字符 275 * @see ?-->一个字符 276 */ 277 public void searchByWildcard(String fieldName, String wildcard) { 278 Query query = new WildcardQuery(new Term(fieldName, wildcard)); 279 this.doSearch(query); 280 } 281 282 /** 283 * 模糊搜索 284 * 285 * @see 与通配符搜索不同 286 */ 287 public void searchByFuzzy(String fieldName, String fuzzy) { 288 Query query = new FuzzyQuery(new Term(fieldName, fuzzy)); 289 this.doSearch(query); 290 } 291 292 /** 293 * 多条件搜索 294 * 295 * @see 本例中搜索name值中以Ja开头,且content中包含am的内容 296 * @see Occur.MUST------表示此条件必须为true 297 * @see Occur.MUST_NOT--表示此条件必须为false 298 * @see Occur.SHOULD----表示此条件非必须 299 */ 300 public void searchByBoolean() { 301 BooleanQuery query = new BooleanQuery(); 302 query.add(new WildcardQuery(new Term("name", "Ja*")), Occur.MUST); 303 query.add(new TermQuery(new Term("content", "am")), Occur.MUST); 304 this.doSearch(query); 305 } 306 307 /** 308 * 短语搜索 309 * 310 * @see 很遗憾的是短语查询对中文搜索没有太大的作用,但对英文搜索是很好用的,但它的开销比较大,尽量少用 311 */ 312 public void searchByPhrase() { 313 PhraseQuery query = new PhraseQuery(); 314 query.setSlop(1); // 设置跳数 315 query.add(new Term("content", "am")); // 第一个Term 316 query.add(new Term("content", "Haerbin")); // 产生距离之后的第二个Term 317 this.doSearch(query); 318 } 319 320 /** 321 * 基于QueryParser的搜索 322 */ 323 public void searchByQueryParse() { 324 QueryParser parser = new QueryParser(Version.LUCENE_36, "content", new StandardAnalyzer(Version.LUCENE_36)); 325 Query query = null; 326 try { 327 // query = parser.parse("Haerbin"); //搜索content中包含[Haerbin]的记录 328 // query = parser.parse("I AND Haerbin"); //搜索content中包含[I]和[Haerbin]的记录 329 // query = parser.parse("Lucene OR Haerbin"); //搜索content中包含[Lucene]或者[Haerbin]的记录 330 // query = parser.parse("Lucene Haerbin"); //搜索content中包含[Lucene]或者[Haerbin]的记录 331 // parser.setDefaultOperator(Operator.AND); //将空格的默认操作OR修改为AND 332 // //1)如果name域在索引时,不进行分词,那么无论这里写成[name:Jadyer]还是[name:jadyer],最后得到的都是0条记录 333 // //2)由于name原值为大写[J],若索引时不对name分词,除非修改name原值为小写[j],并且搜索[name:jadyer]才能得到记录 334 // query = parser.parse("name:Jadyer"); //修改搜索域为name=Jadyer的记录 335 // query = parser.parse("name:Ja*"); //支持通配符 336 // query = parser.parse("\"I am\""); //搜索content中包含[I am]的记录(注意不能使用parse("content:'I am'")) 337 // parser.setAllowLeadingWildcard(true); //设置允许[*]或[?]出现在查询字符的第一位,即[name:*de],否则[name:*de]会报异常 338 // query = parser.parse("name:*de"); //Lucene默认的第一个字符不允许为通配符,因为这样效率比较低 339 // //parse("+am +name:Jade")--------------搜索content中包括[am]的,并且name=Jade的记录 340 // //parse("am AND NOT name:Jade")--------搜索content中包括[am]的,并且nam不是Jade的记录 341 // //parse("(blog OR am) AND name:Jade")--搜索content中包括[blog]或者[am]的,并且name=Jade的记录 342 // query = parser.parse("-name:Jack +I"); //搜索content中包括[I]的,并且name不是Jack的记录(加减号要放到域说明的前面) 343 // query = parser.parse("id:[1 TO 3]"); //搜索id值从1到3的记录(TO必须大写,且这种方式没有办法匹配数字) 344 // query = parser.parse("id:{1 TO 3}"); //搜索id=2的记录 345 query = parser.parse("name:Jadk~"); // 模糊搜索 346 } 347 catch (ParseException e) { 348 e.printStackTrace(); 349 } 350 this.doSearch(query); 351 } 352 353 /** 354 * 普通的分页搜索 355 * 356 * @see 适用于lucene3.5之前 357 * @param expr 358 * 搜索表达式 359 * @param pageIndex 360 * 页码 361 * @param pageSize 362 * 分页大小 363 */ 364 public void searchPage(String expr, int pageIndex, int pageSize) { 365 IndexSearcher searcher = this.getIndexSearcher(); 366 QueryParser parser = new QueryParser(Version.LUCENE_36, "mycontent", new StandardAnalyzer(Version.LUCENE_36)); 367 try { 368 Query query = parser.parse(expr); 369 TopDocs tds = searcher.search(query, pageIndex * pageSize); 370 ScoreDoc[] sds = tds.scoreDocs; 371 for (int i = (pageIndex - 1) * pageSize; i < pageIndex * pageSize; i++) { 372 Document doc = searcher.doc(sds[i].doc); 373 System.out.println("文档编号:" + sds[i].doc + "-->" + doc.get("myname") + "-->" + doc.get("mycontent")); 374 } 375 } 376 catch (Exception e) { 377 e.printStackTrace(); 378 } 379 finally { 380 if (null != searcher) { 381 try { 382 searcher.close(); 383 } 384 catch (IOException e) { 385 e.printStackTrace(); 386 } 387 } 388 } 389 } 390 391 /** 392 * 基于searchAfter的分页搜索 393 * 394 * @see 适用于Lucene3.5 395 * @param expr 396 * 搜索表达式 397 * @param pageIndex 398 * 页码 399 * @param pageSize 400 * 分页大小 401 */ 402 public void searchPageByAfter(String expr, int pageIndex, int pageSize) { 403 IndexSearcher searcher = this.getIndexSearcher(); 404 QueryParser parser = new QueryParser(Version.LUCENE_36, "mycontent", new StandardAnalyzer(Version.LUCENE_36)); 405 try { 406 Query query = parser.parse(expr); 407 TopDocs tds = searcher.search(query, (pageIndex - 1) * pageSize); 408 // 使用IndexSearcher.searchAfter()搜索,该方法第一个参数为上一页记录中的最后一条记录 409 if (pageIndex > 1) { 410 tds = searcher.searchAfter(tds.scoreDocs[(pageIndex - 1) * pageSize - 1], query, pageSize); 411 } 412 else { 413 tds = searcher.searchAfter(null, query, pageSize); 414 } 415 for (ScoreDoc sd : tds.scoreDocs) { 416 Document doc = searcher.doc(sd.doc); 417 System.out.println("文档编号:" + sd.doc + "-->" + doc.get("myname") + "-->" + doc.get("mycontent")); 418 } 419 } 420 catch (Exception e) { 421 e.printStackTrace(); 422 } 423 finally { 424 if (null != searcher) { 425 try { 426 searcher.close(); 427 } 428 catch (IOException e) { 429 e.printStackTrace(); 430 } 431 } 432 } 433 } 434 }
4.中文分词器
1 import java.io.IOException; 2 import java.io.StringReader; 3 4 import org.apache.lucene.analysis.Analyzer; 5 import org.apache.lucene.analysis.SimpleAnalyzer; 6 import org.apache.lucene.analysis.StopAnalyzer; 7 import org.apache.lucene.analysis.TokenStream; 8 import org.apache.lucene.analysis.WhitespaceAnalyzer; 9 import org.apache.lucene.analysis.standard.StandardAnalyzer; 10 import org.apache.lucene.analysis.tokenattributes.CharTermAttribute; 11 import org.apache.lucene.analysis.tokenattributes.OffsetAttribute; 12 import org.apache.lucene.analysis.tokenattributes.PositionIncrementAttribute; 13 import org.apache.lucene.analysis.tokenattributes.TypeAttribute; 14 import org.apache.lucene.util.Version; 15 16 import com.chenlb.mmseg4j.analysis.ComplexAnalyzer; 17 import com.chenlb.mmseg4j.analysis.MMSegAnalyzer; 18 19 /** 20 * 【Lucene3.6.2入门系列】第04节_中文分词器 21 * 22 * @see ----------------------------------------------------------------------------------------------------------------------- 23 * @see Lucene3.5推荐的四大分词器:SimpleAnalyzer,StopAnalyzer,WhitespaceAnalyzer,StandardAnalyzer 24 * @see 这四大分词器有一个共同的抽象父类,此类有个方法public final TokenStream tokenStream(),即分词的一个流 25 * @see 假设有这样的文本"how are you thank you",实际它是以一个java.io.Reader传进分词器中 26 * @see Lucene分词器处理完毕后,会把整个分词转换为TokenStream,这个TokenStream中就保存所有的分词信息 27 * @see TokenStream有两个实现类,分别为Tokenizer和TokenFilter 28 * @see Tokenizer---->用于将一组数据划分为独立的语汇单元(即一个一个的单词) 29 * @see TokenFilter-->过滤语汇单元 30 * @see ----------------------------------------------------------------------------------------------------------------------- 31 * @see 分词流程 32 * @see 1)将一组数据流java.io.Reader交给Tokenizer,由其将数据转换为一个个的语汇单元 33 * @see 2)通过大量的TokenFilter对已经分好词的数据进行过滤操作,最后产生TokenStream 34 * @see 3)通过TokenStream完成索引的存储 35 * @see ----------------------------------------------------------------------------------------------------------------------- 36 * @see Tokenizer的一些子类 37 * @see KeywordTokenizer-----不分词,传什么就索引什么 38 * @see StandardTokenizer----标准分词,它有一些较智能的分词操作,诸如将'[email protected]'中的'yeah.net'当作一个分词流 39 * @see CharTokenizer--------针对字符进行控制的,它还有两个子类WhitespaceTokenizer和LetterTokenizer 40 * @see WhitespaceTokenizer--使用空格进行分词,诸如将'Thank you,I am jadyer'会被分为4个词 41 * @see LetterTokenizer------基于文本单词的分词,它会根据标点符号来分词,诸如将'Thank you,I am jadyer'会被分为5个词 42 * @see LowerCaseTokenizer---它是LetterTokenizer的子类,它会将数据转为小写并分词 43 * @see ----------------------------------------------------------------------------------------------------------------------- 44 * @see TokenFilter的一些子类 45 * @see StopFilter--------它会停用一些语汇单元 46 * @see LowerCaseFilter---将数据转换为小写 47 * @see StandardFilter----对标准输出流做一些控制 48 * @see PorterStemFilter--还原一些数据,比如将coming还原为come,将countries还原为country 49 * @see ----------------------------------------------------------------------------------------------------------------------- 50 * @see eg:'how are you thank you'会被分词为'how','are','you','thank','you'合计5个语汇单元 51 * @see 那么应该保存什么东西,才能使以后在需要还原数据时保证正确的还原呢???其实主要保存三个东西,如下所示 52 * @see CharTermAttribute(Lucene3.5以前叫TermAttribute),OffsetAttribute,PositionIncrementAttribute 53 * @see 1)CharTermAttribute-----------保存相应的词汇,这里保存的就是'how','are','you','thank','you' 54 * @see 2)OffsetAttribute-------------保存各词汇之间的偏移量(大致理解为顺序),比如'how'的首尾字母偏移量为0和3,'are'为4和7,'thank'为12和17 55 * @see 3)PositionIncrementAttribute--保存词与词之间的位置增量,比如'how'和'are'增量为1,'are'和'you'之间的也是1,'you'和'thank'的也是1 56 * @see 但假设'are'是停用词(StopFilter的效果),那么'how'和'you'之间的位置增量就变成了2 57 * @see 当我们查找某一个元素时,Lucene会先通过位置增量来取这个元素,但如果两个词的位置增量相同,会发生什么情况呢 58 * @see 假设还有一个单词'this',它的位置增量和'how'是相同的,那么当我们在界面中搜索'this'时 59 * @see 也会搜到'how are you thank you',这样就可以有效的做同义词了,目前非常流行的一个叫做WordNet的东西,就可以做同义词的搜索 60 * @see ----------------------------------------------------------------------------------------------------------------------- 61 * @see 中文分词器 62 * @see Lucene默认提供的众多分词器完全不适用中文 63 * @see 1)Paoding--庖丁解牛分词器,官网为http://code.google.com/p/paoding(貌似已托管在http://git.oschina.net/zhzhenqin/paoding-analysis) 64 * @see 2)MMSeg4j--据说它使用的是搜狗的词库,官网为https://code.google.com/p/mmseg4j(另外还有一个https://code.google.com/p/jcseg) 65 * @ses 3)IK-------https://code.google.com/p/ik-analyzer/ 66 * @see ----------------------------------------------------------------------------------------------------------------------- 67 * @see MMSeg4j的使用 68 * @see 1)下载mmseg4j-1.8.5.zip并引入mmseg4j-all-1.8.5-with-dic.jar 69 * @see 2)在需要指定分词器的位置编写new MMSegAnalyzer()即可 70 * @see 注1)由于使用的mmseg4j-all-1.8.5-with-dic.jar中已自带了词典,故直接new MMSegAnalyzer()即可 71 * @see 注2)若引入的是mmseg4j-all-1.8.5.jar,则应指明词典目录,如new MMSegAnalyzer("D:\\Develop\\mmseg4j-1.8.5\\data") 72 * @see 但若非要使用new MMSegAnalyzer(),则要将mmseg4j-1.8.5.zip自带的data目录拷入classpath下即可 73 * @see 总结:直接引入mmseg4j-all-1.8.5-with-dic.jar就行了 74 * @see ----------------------------------------------------------------------------------------------------------------------- 75 * @create Aug 2, 2013 5:30:45 PM 76 * @author 玄玉<http://blog.csdn.net/jadyer> 77 */ 78 public class Lucene_04_HelloChineseAnalyzer { 79 /** 80 * 查看分词信息 81 * 82 * @see TokenStream还有两个属性,分别为FlagsAttribute和PayloadAttribute,都是开发时用的 83 * @see FlagsAttribute----标注位属性 84 * @see PayloadAttribute--做负载的属性,用来检测是否已超过负载,超过则可以决定是否停止搜索等等 85 * @param txt 86 * 待分词的字符串 87 * @param analyzer 88 * 所使用的分词器 89 * @param displayAll 90 * 是否显示所有的分词信息 91 */ 92 public static void displayTokenInfo(String txt, Analyzer analyzer, boolean displayAll) { 93 // 第一个参数没有任何意义,可以随便传一个值,它只是为了显示分词 94 // 这里就是使用指定的分词器将'txt'分词,分词后会产生一个TokenStream(可将分词后的每个单词理解为一个Token) 95 TokenStream stream = analyzer.tokenStream("此参数无意义", new StringReader(txt)); 96 // 用于查看每一个语汇单元的信息,即分词的每一个元素 97 // 这里创建的属性会被添加到TokenStream流中,并随着TokenStream而增加(此属性就是用来装载每个Token的,即分词后的每个单词) 98 // 当调用TokenStream.incrementToken()时,就会指向到这个单词流中的第一个单词,即此属性代表的就是分词后的第一个单词 99 // 可以形象的理解成一只碗,用来盛放TokenStream中每个单词的碗,每调用一次incrementToken()后,这个碗就会盛放流中的下一个单词 100 CharTermAttribute cta = stream.addAttribute(CharTermAttribute.class); 101 // 用于查看位置增量(指的是语汇单元之间的距离,可理解为元素与元素之间的空格,即间隔的单元数) 102 PositionIncrementAttribute pia = stream.addAttribute(PositionIncrementAttribute.class); 103 // 用于查看每个语汇单元的偏移量 104 OffsetAttribute oa = stream.addAttribute(OffsetAttribute.class); 105 // 用于查看使用的分词器的类型信息 106 TypeAttribute ta = stream.addAttribute(TypeAttribute.class); 107 try { 108 if (displayAll) { 109 // 等价于while(stream.incrementToken()) 110 for (; stream.incrementToken();) { 111 System.out.println(ta.type() + " " + pia.getPositionIncrement() + " [" + oa.startOffset() + "-" + oa.endOffset() + "] [" + cta + "]"); 112 } 113 } 114 else { 115 System.out.println(); 116 while (stream.incrementToken()) { 117 System.out.print("[" + cta + "]"); 118 } 119 } 120 } 121 catch (IOException e) { 122 e.printStackTrace(); 123 } 124 } 125 126 /** 127 * 测试一下中文分词的效果 128 */ 129 public static void main(String[] args) { 130 String txt = "测试一下中文分词的效果"; 131 // displayTokenInfo(txt, new StandardAnalyzer(Version.LUCENE_36), false); 132 // displayTokenInfo(txt, new StopAnalyzer(Version.LUCENE_36), false); 133 // displayTokenInfo(txt, new SimpleAnalyzer(Version.LUCENE_36), false); 134 // displayTokenInfo(txt, new WhitespaceAnalyzer(Version.LUCENE_36), false); 135 displayTokenInfo(txt, new MMSegAnalyzer(), true); 136 // displayTokenInfo(txt, new SimpleAnalyzer(), false); 137 // displayTokenInfo(txt, new ComplexAnalyzer(), false); 138 } 139 }
5.高级搜索之排序
1 import java.io.File; 2 import java.io.IOException; 3 import java.text.SimpleDateFormat; 4 import java.util.Date; 5 6 import org.apache.lucene.analysis.standard.StandardAnalyzer; 7 import org.apache.lucene.document.Document; 8 import org.apache.lucene.document.Field; 9 import org.apache.lucene.document.NumericField; 10 import org.apache.lucene.index.IndexReader; 11 import org.apache.lucene.index.IndexWriter; 12 import org.apache.lucene.index.IndexWriterConfig; 13 import org.apache.lucene.queryParser.QueryParser; 14 import org.apache.lucene.search.IndexSearcher; 15 import org.apache.lucene.search.ScoreDoc; 16 import org.apache.lucene.search.Sort; 17 import org.apache.lucene.search.SortField; 18 import org.apache.lucene.search.TopDocs; 19 import org.apache.lucene.store.Directory; 20 import org.apache.lucene.store.FSDirectory; 21 import org.apache.lucene.util.Version; 22 23 /** 24 * 【Lucene3.6.2入门系列】第06节_高级搜索之排序 25 * 26 * @create Aug 19, 2013 10:38:19 AM 27 * @author 玄玉<http://blog.csdn.net/jadyer> 28 */ 29 public class Lucene_05_AdvancedSearchBySort { 30 private Directory directory; 31 private IndexReader reader; 32 33 public Lucene_05_AdvancedSearchBySort() { 34 /** 文件大小 */ 35 int[] sizes = { 90, 10, 20, 10, 60, 50 }; 36 /** 文件名 */ 37 String[] names = { "Michael.java", "Scofield.ini", "Tbag.txt", "Jack", "Jade", "Jadyer" }; 38 /** 文件内容 */ 39 String[] contents = { "my java blog is http://blog.csdn.net/jadyer", "my Java Website is http://www.jadyer.cn", "my name is jadyer", "I am a Java Developer", "I am from Haerbin", "I like java of Lucene" }; 40 /** 文件日期 */ 41 Date[] dates = new Date[sizes.length]; 42 SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd HH:mm:ss"); 43 IndexWriter writer = null; 44 Document doc = null; 45 try { 46 dates[0] = sdf.parse("20130407 15:25:30"); 47 dates[1] = sdf.parse("20130407 16:30:45"); 48 dates[2] = sdf.parse("20130213 11:15:25"); 49 dates[3] = sdf.parse("20130808 09:30:55"); 50 dates[4] = sdf.parse("20130526 13:54:22"); 51 dates[5] = sdf.parse("20130701 17:35:34"); 52 directory = FSDirectory.open(new File("E:/lucene_test/01_index/")); 53 writer = new IndexWriter(directory, new IndexWriterConfig(Version.LUCENE_36, new StandardAnalyzer(Version.LUCENE_36))); 54 writer.deleteAll(); 55 for (int i = 0; i < sizes.length; i++) { 56 doc = new Document(); 57 doc.add(new NumericField("size", Field.Store.YES, true).setIntValue(sizes[i])); 58 doc.add(new Field("name", names[i], Field.Store.YES, Field.Index.ANALYZED_NO_NORMS)); 59 doc.add(new Field("content", contents[i], Field.Store.YES, Field.Index.ANALYZED)); 60 doc.add(new NumericField("date", Field.Store.YES, true).setLongValue(dates[i].getTime())); 61 writer.addDocument(doc); 62 } 63 } 64 catch (Exception e) { 65 e.printStackTrace(); 66 } 67 finally { 68 if (null != writer) { 69 try { 70 writer.close(); 71 } 72 catch (IOException ce) { 73 ce.printStackTrace(); 74 } 75 } 76 } 77 } 78 79 /** 80 * 获取IndexReader实例 81 */ 82 private IndexReader getIndexReader() { 83 try { 84 if (reader == null) { 85 reader = IndexReader.open(directory); 86 } 87 else { 88 // if the index was changed since the provided reader was opened, open and return a new reader; else,return null 89 // 如果当前reader在打开期间index发生改变,则打开并返回一个新的IndexReader,否则返回null 90 IndexReader ir = IndexReader.openIfChanged(reader); 91 if (ir != null) { 92 reader.close(); // 关闭原reader 93 reader = ir; // 赋予新reader 94 } 95 } 96 return reader; 97 } 98 catch (Exception e) { 99 e.printStackTrace(); 100 } 101 return null; // 发生异常则返回null 102 } 103 104 /** 105 * 搜索排序 106 * 107 * @see 关于Sort参数的可输入规则,如下所示 108 * @see 1)Sort.INDEXORDER--使用文档编号从小到大的顺序进行排序 109 * @see 2)Sort.RELEVANCE---使用文档评分从大到小的顺序进行排序,也是默认的排序规则,等价于search(query, 10) 110 * @see 3)new Sort(new SortField("size", SortField.INT))-----------使用文件大小从小到大的顺序排序 111 * @see 4)new Sort(new SortField("date", SortField.LONG))----------使用文件日期从以前到现在的顺序排序 112 * @see 5)new Sort(new SortField("name", SortField.STRING))--------使用文件名从A到Z的顺序排序 113 * @see 6)new Sort(new SortField("name", SortField.STRING, true))--使用文件名从Z到A的顺序排序 114 * @see 7)new Sort(new SortField("size", SortField.INT), SortField.FIELD_SCORE)--先按照文件大小排序,再按照文档评分排序(可以指定多个排序规则) 115 * @see 注意:以上7个Sort再打印文档评分时都是NaN,只有search(query, 10)才会正确打印文档评分 116 * @param expr 117 * 搜索表达式 118 * @param sort 119 * 排序规则 120 */ 121 public void searchBySort(String expr, Sort sort) { 122 IndexSearcher searcher = new IndexSearcher(this.getIndexReader()); 123 QueryParser parser = new QueryParser(Version.LUCENE_36, "content", new StandardAnalyzer(Version.LUCENE_36)); 124 TopDocs tds = null; 125 try { 126 if (null == sort) { 127 tds = searcher.search(parser.parse(expr), 10); 128 } 129 else { 130 tds = searcher.search(parser.parse(expr), 10, sort); 131 } 132 for (ScoreDoc sd : tds.scoreDocs) { 133 Document doc = searcher.doc(sd.doc); 134 System.out.println("content=" + doc.get("content")); 135 System.out.print("文档编号=" + sd.doc + " 文档权值=" + doc.getBoost() + " 文档评分=" + sd.score + " "); 136 System.out.println("size=" + doc.get("size") + " date=" + new SimpleDateFormat("yyyyMMdd HH:mm:ss").format(new Date(Long.parseLong(doc.get("date")))) + " name=" + doc.get("name")); 137 } 138 } 139 catch (Exception e) { 140 e.printStackTrace(); 141 } 142 finally { 143 if (searcher != null) { 144 try { 145 searcher.close(); 146 } 147 catch (IOException e) { 148 e.printStackTrace(); 149 } 150 } 151 } 152 } 153 154 /** 155 * 测试一下排序效果 156 */ 157 public static void main(String[] args) { 158 Lucene_05_AdvancedSearchBySort advancedSearch = new Lucene_05_AdvancedSearchBySort(); 159 // //使用文档评分从大到小的顺序进行排序,也是默认的排序规则 160 // advancedSearch.searchBySort("Java", null); 161 // advancedSearch.searchBySort("Java", Sort.RELEVANCE); 162 // //使用文档编号从小到大的顺序进行排序 163 // advancedSearch.searchBySort("Java", Sort.INDEXORDER); 164 // //使用文件大小从小到大的顺序排序 165 // advancedSearch.searchBySort("Java", new Sort(new SortField("size", SortField.INT))); 166 // //使用文件日期从以前到现在的顺序排序 167 // advancedSearch.searchBySort("Java", new Sort(new SortField("date", SortField.LONG))); 168 // //使用文件名从A到Z的顺序排序 169 // advancedSearch.searchBySort("Java", new Sort(new SortField("name", SortField.STRING))); 170 // //使用文件名从Z到A的顺序排序 171 // advancedSearch.searchBySort("Java", new Sort(new SortField("name", SortField.STRING, true))); 172 // 先按照文件大小排序,再按照文档评分排序(可以指定多个排序规则) 173 advancedSearch.searchBySort("Java", new Sort(new SortField("size", SortField.INT), SortField.FIELD_SCORE)); 174 } 175 }
6.高级搜索之普通Filter和自定义Filter
1 import java.io.File; 2 import java.io.IOException; 3 import java.text.ParseException; 4 import java.text.SimpleDateFormat; 5 import java.util.Date; 6 7 import org.apache.lucene.analysis.standard.StandardAnalyzer; 8 import org.apache.lucene.document.Document; 9 import org.apache.lucene.document.Field; 10 import org.apache.lucene.document.NumericField; 11 import org.apache.lucene.index.IndexReader; 12 import org.apache.lucene.index.IndexWriter; 13 import org.apache.lucene.index.IndexWriterConfig; 14 import org.apache.lucene.index.Term; 15 import org.apache.lucene.index.TermDocs; 16 import org.apache.lucene.queryParser.QueryParser; 17 import org.apache.lucene.search.DocIdSet; 18 import org.apache.lucene.search.Filter; 19 import org.apache.lucene.search.IndexSearcher; 20 import org.apache.lucene.search.NumericRangeFilter; 21 import org.apache.lucene.search.ScoreDoc; 22 import org.apache.lucene.search.TopDocs; 23 import org.apache.lucene.store.Directory; 24 import org.apache.lucene.store.FSDirectory; 25 import org.apache.lucene.util.OpenBitSet; 26 import org.apache.lucene.util.Version; 27 28 /** 29 * 【Lucene3.6.2入门系列】第07节_高级搜索之普通Filter和自定义Filter 30 * 31 * @create Aug 19, 2013 11:13:40 AM 32 * @author 玄玉<http://blog.csdn.net/jadyer> 33 */ 34 public class Lucene_06_AdvancedSearchByFilter { 35 private Directory directory; 36 private IndexReader reader; 37 38 /** 39 * 测试一下过滤效果 40 */ 41 public static void main(String[] args) throws ParseException { 42 Lucene_06_AdvancedSearchByFilter advancedSearch = new Lucene_06_AdvancedSearchByFilter(); 43 // //过滤文件名首字母从'h'到'n'的记录(注意hn要小写) 44 // advancedSearch.searchByFilter("Java", new TermRangeFilter("name", "h", "n", true, true)); 45 // //过滤文件大小在30到80以内的记录 46 // advancedSearch.searchByFilter("Java", NumericRangeFilter.newIntRange("size", 30, 80, true, true)); 47 // //过滤文件日期在20130701 00:00:00到20130808 23:59:59之间的记录 48 // Long min = Long.valueOf(new SimpleDateFormat("yyyyMMdd").parse("20130701").getTime()); 49 // Long max = Long.valueOf(new SimpleDateFormat("yyyyMMdd HH:mm:ss").parse("20130808 23:59:59").getTime()); 50 // advancedSearch.searchByFilter("Java", NumericRangeFilter.newLongRange("date", min, max, true, true)); 51 // //过滤文件名以'ja'打头的(注意ja要小写) 52 // advancedSearch.searchByFilter("Java", new QueryWrapperFilter(new WildcardQuery(new Term("name", "ja*")))); 53 // 自定义Filter 54 advancedSearch.searchByFilter("Java", advancedSearch.new MyFilter()); 55 } 56 57 public Lucene_06_AdvancedSearchByFilter() { 58 /** 文件大小 */ 59 int[] sizes = { 90, 10, 20, 10, 60, 50 }; 60 /** 文件名 */ 61 String[] names = { "Michael.java", "Scofield.ini", "Tbag.txt", "Jack", "Jade", "Jadyer" }; 62 /** 文件内容 */ 63 String[] contents = { "my java blog is http://blog.csdn.net/jadyer", "my Java Website is http://www.jadyer.cn", "my name is jadyer", "I am a Java Developer", "I am from Haerbin", "I like java of Lucene" }; 64 /** 文件日期 */ 65 Date[] dates = new Date[sizes.length]; 66 SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd HH:mm:ss"); 67 IndexWriter writer = null; 68 Document doc = null; 69 try { 70 dates[0] = sdf.parse("20130407 15:25:30"); 71 dates[1] = sdf.parse("20130407 16:30:45"); 72 dates[2] = sdf.parse("20130213 11:15:25"); 73 dates[3] = sdf.parse("20130808 09:30:55"); 74 dates[4] = sdf.parse("20130526 13:54:22"); 75 dates[5] = sdf.parse("20130701 17:35:34"); 76 directory = FSDirectory.open(new File("myExample/01_index/")); 77 writer = new IndexWriter(directory, new IndexWriterConfig(Version.LUCENE_36, new StandardAnalyzer(Version.LUCENE_36))); 78 writer.deleteAll(); 79 for (int i = 0; i < sizes.length; i++) { 80 doc = new Document(); 81 doc.add(new NumericField("size", Field.Store.YES, true).setIntValue(sizes[i])); 82 doc.add(new Field("name", names[i], Field.Store.YES, Field.Index.ANALYZED_NO_NORMS)); 83 doc.add(new Field("content", contents[i], Field.Store.NO, Field.Index.ANALYZED)); 84 doc.add(new NumericField("date", Field.Store.YES, true).setLongValue(dates[i].getTime())); 85 // 为每个文档添加一个fileID(与ScoreDoc.doc不同),专门在自定义Filter时使用 86 doc.add(new Field("fileID", String.valueOf(i), Field.Store.YES, Field.Index.NOT_ANALYZED_NO_NORMS)); 87 writer.addDocument(doc); 88 } 89 } 90 catch (Exception e) { 91 e.printStackTrace(); 92 } 93 finally { 94 if (null != writer) { 95 try { 96 writer.close(); 97 } 98 catch (IOException ce) { 99 ce.printStackTrace(); 100 } 101 } 102 } 103 } 104 105 /** 106 * 获取IndexReader实例 107 */ 108 private IndexReader getIndexReader() { 109 try { 110 if (reader == null) { 111 reader = IndexReader.open(directory); 112 } 113 else { 114 // if the index was changed since the provided reader was opened, open and return a new reader; else,return null 115 // 如果当前reader在打开期间index发生改变,则打开并返回一个新的IndexReader,否则返回null 116 IndexReader ir = IndexReader.openIfChanged(reader); 117 if (ir != null) { 118 reader.close(); // 关闭原reader 119 reader = ir; // 赋予新reader 120 } 121 } 122 return reader; 123 } 124 catch (Exception e) { 125 e.printStackTrace(); 126 } 127 return null; // 发生异常则返回null 128 } 129 130 /** 131 * 搜索过滤 132 */ 133 public void searchByFilter(String expr, Filter filter) { 134 IndexSearcher searcher = new IndexSearcher(this.getIndexReader()); 135 QueryParser parser = new QueryParser(Version.LUCENE_36, "content", new StandardAnalyzer(Version.LUCENE_36)); 136 TopDocs tds = null; 137 try { 138 if (null == filter) { 139 tds = searcher.search(parser.parse(expr), 10); 140 } 141 else { 142 tds = searcher.search(parser.parse(expr), filter, 10); 143 } 144 for (ScoreDoc sd : tds.scoreDocs) { 145 Document doc = searcher.doc(sd.doc); 146 System.out.print("文档编号=" + sd.doc + " 文档权值=" + doc.getBoost() + " 文档评分=" + sd.score + " "); 147 System.out.println("fileID=" + doc.get("fileID") + " size=" + doc.get("size") + " date=" + new SimpleDateFormat("yyyyMMdd HH:mm:ss").format(new Date(Long.parseLong(doc.get("date")))) + " name=" + doc.get("name")); 148 } 149 } 150 catch (Exception e) { 151 e.printStackTrace(); 152 } 153 finally { 154 if (searcher != null) { 155 try { 156 searcher.close(); 157 } 158 catch (IOException e) { 159 e.printStackTrace(); 160 } 161 } 162 } 163 } 164 165 /** 166 * 自定义Filter 167 * 168 * @see ------------------------------------------------------------------------------------------ 169 * @see 本例的应用场景 170 * @see 假设很多的数据,然后删除了其中的某几条数据,此时在接受搜索请求时为保证不会搜索到已删除的数据 171 * @see 那么可以更新索引,但更新索引会消耗很多时间(因为数据量大),而又要保证已删除的数据不会被搜索到 172 * @see 此时就可以自定义Filter,原理即搜索过程中,当发现此记录为已删除记录,则不添加到返回的搜索结果集中 173 * @see ------------------------------------------------------------------------------------------ 174 * @see 自定义Filter步骤如下 175 * @see 1)继承Filter类并重写getDocIdSet()方法 176 * @see 2)根据实际过滤要求返回新的DocIdSet对象 177 * @see ------------------------------------------------------------------------------------------ 178 * @see DocIdSet小解 179 * @see 这里Filter干的活其实就是创建一个DocIdSet,而DocIdSet其实就是一个数组,可以理解为其中只存放0或1的值 180 * @see 每个搜索出来的Document都有一个文档编号,所以搜索出来多少个Document,那么DocIdSet中就会有多少条记录 181 * @see 而DocIdSet中每一条记录的索引号与文档编号是一一对应的 182 * @see 所以当DocIdSet中的记录为1时,则对应文档编号的Document就会被添加到TopDocs中,为0就会被过滤掉 183 * @see ------------------------------------------------------------------------------------------ 184 * @create Aug 6, 2013 7:28:53 PM 185 * @author 玄玉<http://blog.csdn.net/jadyer> 186 */ 187 class MyFilter extends Filter { 188 private static final long serialVersionUID = -8955061358165068L; 189 190 // 假设这是已删除记录的fileID值的集合 191 private String[] deleteFileIDs = { "1", "3" }; 192 193 @Override 194 public DocIdSet getDocIdSet(IndexReader reader) throws IOException { 195 // 创建一个DocIdSet的子类OpenBitSet(创建之后默认所有元素都是0),传的参数就是本次"搜索到的"元素数目 196 OpenBitSet obs = new OpenBitSet(reader.maxDoc()); 197 // 先把元素填满,即全部设置为1 198 obs.set(0, reader.maxDoc()); 199 // 用于保存已删除元素的文档编号 200 int[] docs = new int[1]; 201 for (String deleteDataID : deleteFileIDs) { 202 // 获取已删除元素对应的TermDocs 203 TermDocs tds = reader.termDocs(new Term("fileID", deleteDataID)); 204 // 将已删除元素的文档编号放到docs中,将其出现的频率放到freqs中,最后返回查询出来的元素数目 205 int count = tds.read(docs, new int[1]); 206 if (count == 1) { 207 // 将这个位置docs[0]的元素删除 208 obs.clear(docs[0]); 209 } 210 } 211 return obs; 212 } 213 } 214 215 }
7.高级搜索之自定义QueryParser
1 import java.io.File; 2 import java.io.IOException; 3 import java.text.SimpleDateFormat; 4 import java.util.Date; 5 import java.util.regex.Pattern; 6 7 import org.apache.lucene.analysis.Analyzer; 8 import org.apache.lucene.analysis.standard.StandardAnalyzer; 9 import org.apache.lucene.document.Document; 10 import org.apache.lucene.document.Field; 11 import org.apache.lucene.document.NumericField; 12 import org.apache.lucene.index.IndexReader; 13 import org.apache.lucene.index.IndexWriter; 14 import org.apache.lucene.index.IndexWriterConfig; 15 import org.apache.lucene.queryParser.ParseException; 16 import org.apache.lucene.queryParser.QueryParser; 17 import org.apache.lucene.search.IndexSearcher; 18 import org.apache.lucene.search.NumericRangeQuery; 19 import org.apache.lucene.search.Query; 20 import org.apache.lucene.search.ScoreDoc; 21 import org.apache.lucene.search.TopDocs; 22 import org.apache.lucene.store.Directory; 23 import org.apache.lucene.store.FSDirectory; 24 import org.apache.lucene.util.Version; 25 26 /** 27 * 【Lucene3.6.2入门系列】第09节_高级搜索之自定义QueryParser 28 * 29 * @create Aug 19, 2013 2:07:32 PM 30 * @author 玄玉<http://blog.csdn.net/jadyer> 31 */ 32 public class Lucene_07_AdvancedSearch { 33 private Directory directory; 34 private IndexReader reader; 35 36 /** 37 * 测试一下搜索效果 38 */ 39 public static void main(String[] args) { 40 Lucene_07_AdvancedSearch advancedSearch = new Lucene_07_AdvancedSearch(); 41 advancedSearch.searchByCustomQueryParser("name:Jadk~"); 42 advancedSearch.searchByCustomQueryParser("name:Ja??er"); 43 System.out.println("------------------------------------------------------------------------"); 44 advancedSearch.searchByCustomQueryParser("name:Jade"); 45 System.out.println("------------------------------------------------------------------------"); 46 advancedSearch.searchByCustomQueryParser("name:[h TO n]"); 47 System.out.println("------------------------------------------------------------------------"); 48 advancedSearch.searchByCustomQueryParser("size:[20 TO 80]"); 49 System.out.println("------------------------------------------------------------------------"); 50 advancedSearch.searchByCustomQueryParser("date:[20130407 TO 20130701]"); 51 } 52 53 public Lucene_07_AdvancedSearch() { 54 /** 文件大小 */ 55 int[] sizes = { 90, 10, 20, 10, 60, 50 }; 56 /** 文件名 */ 57 String[] names = { "Michael.java", "Scofield.ini", "Tbag.txt", "Jack", "Jade", "Jadyer" }; 58 /** 文件内容 */ 59 String[] contents = { "my java blog is http://blog.csdn.net/jadyer", "my Java Website is http://www.jadyer.cn", "my name is jadyer", "I am a Java Developer", "I am from Haerbin", "I like java of Lucene" }; 60 /** 文件日期 */ 61 Date[] dates = new Date[sizes.length]; 62 SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd HH:mm:ss"); 63 IndexWriter writer = null; 64 Document doc = null; 65 try { 66 dates[0] = sdf.parse("20130407 15:25:30"); 67 dates[1] = sdf.parse("20130407 16:30:45"); 68 dates[2] = sdf.parse("20130213 11:15:25"); 69 dates[3] = sdf.parse("20130808 09:30:55"); 70 dates[4] = sdf.parse("20130526 13:54:22"); 71 dates[5] = sdf.parse("20130701 17:35:34"); 72 directory = FSDirectory.open(new File("E:/lucene_test/01_index/")); 73 writer = new IndexWriter(directory, new IndexWriterConfig(Version.LUCENE_36, new StandardAnalyzer(Version.LUCENE_36))); 74 writer.deleteAll(); 75 for (int i = 0; i < sizes.length; i++) { 76 doc = new Document(); 77 doc.add(new NumericField("size", Field.Store.YES, true).setIntValue(sizes[i])); 78 doc.add(new Field("name", names[i], Field.Store.YES, Field.Index.ANALYZED_NO_NORMS)); 79 doc.add(new Field("content", contents[i], Field.Store.NO, Field.Index.ANALYZED)); 80 doc.add(new NumericField("date", Field.Store.YES, true).setLongValue(dates[i].getTime())); 81 writer.addDocument(doc); 82 } 83 } 84 catch (Exception e) { 85 e.printStackTrace(); 86 } 87 finally { 88 if (null != writer) { 89 try { 90 writer.close(); 91 } 92 catch (IOException ce) { 93 ce.printStackTrace(); 94 } 95 } 96 } 97 } 98 99 /** 100 * 获取IndexReader实例 101 */ 102 private IndexReader getIndexReader() { 103 try { 104 if (reader == null) { 105 reader = IndexReader.open(directory); 106 } 107 else { 108 // if the index was changed since the provided reader was opened, open and return a new reader; else,return null 109 // 如果当前reader在打开期间index发生改变,则打开并返回一个新的IndexReader,否则返回null 110 IndexReader ir = IndexReader.openIfChanged(reader); 111 if (ir != null) { 112 reader.close(); // 关闭原reader 113 reader = ir; // 赋予新reader 114 } 115 } 116 return reader; 117 } 118 catch (Exception e) { 119 e.printStackTrace(); 120 } 121 return null; // 发生异常则返回null 122 } 123 124 /** 125 * 自定义QueryParser的搜索 126 * 127 * @param expr 128 * 搜索的表达式 129 */ 130 public void searchByCustomQueryParser(String expr) { 131 IndexSearcher searcher = new IndexSearcher(this.getIndexReader()); 132 QueryParser parser = new MyQueryParser(Version.LUCENE_36, "content", new StandardAnalyzer(Version.LUCENE_36)); 133 try { 134 Query query = parser.parse(expr); 135 TopDocs tds = searcher.search(query, 10); 136 for (ScoreDoc sd : tds.scoreDocs) { 137 Document doc = searcher.doc(sd.doc); 138 System.out.print("文档编号=" + sd.doc + " 文档权值=" + doc.getBoost() + " 文档评分=" + sd.score + " "); 139 System.out.println("size=" + doc.get("size") + " date=" + new SimpleDateFormat("yyyyMMdd HH:mm:ss").format(new Date(Long.parseLong(doc.get("date")))) + " name=" + doc.get("name")); 140 } 141 } 142 catch (ParseException e) { 143 System.err.println(e.getMessage()); 144 } 145 catch (Exception e) { 146 e.printStackTrace(); 147 } 148 finally { 149 if (null != searcher) { 150 try { 151 searcher.close(); // 记得关闭IndexSearcher 152 } 153 catch (IOException e) { 154 e.printStackTrace(); 155 } 156 } 157 } 158 } 159 160 /** 161 * 自定义QueryParser 162 * 163 * @see -------------------------------------------------------------------------------------------------- 164 * @see 实际使用QueryParser的过程中,通常会考虑两个问题 165 * @see 1)限制性能低的QueryParser--对于某些QueryParser在搜索时会使得性能降低,故考虑禁用这些搜索以提升性能 166 * @see 2)扩展基于数字和日期的搜索---有时需要进行一个数字的范围搜索,故需扩展原有的QueryParser才能实现此搜索 167 * @see -------------------------------------------------------------------------------------------------- 168 * @see 限制性能低的QueryParser 169 * @see 继承QueryParser类并重载相应方法,比如getFuzzyQuery和getWildcardQuery 170 * @see 这样造成的结果就是,当输入普通的搜索表达式时,如'I AND Haerbin'可以正常搜索 171 * @see 但输入'name:Jadk~'或者'name:Ja??er'时,就会执行到重载方法中,这时就可以自行处理了,比如本例中禁止该功能 172 * @see -------------------------------------------------------------------------------------------------- 173 * @see 扩展基于数字和日期的查询 174 * @see 思路就是继承QueryParser类后重载getRangeQuery()方法 175 * @see 再针对数字和日期的'域',做特殊处理(使用NumericRangeQuery.newIntRange()方法来搜索) 176 * @see -------------------------------------------------------------------------------------------------- 177 * @create Aug 6, 2013 4:13:42 PM 178 * @author 玄玉<http://blog.csdn.net/jadyer> 179 */ 180 public class MyQueryParser extends QueryParser { 181 public MyQueryParser(Version matchVersion, String f, Analyzer a) { 182 super(matchVersion, f, a); 183 } 184 185 @Override 186 protected Query getWildcardQuery(String field, String termStr) throws ParseException { 187 throw new ParseException("由于性能原因,已禁用通配符搜索,请输入更精确的信息进行搜索 ^_^ ^_^"); 188 } 189 190 @Override 191 protected Query getFuzzyQuery(String field, String termStr, float minSimilarity) throws ParseException { 192 throw new ParseException("由于性能原因,已禁用模糊搜索,请输入更精确的信息进行搜索 ^_^ ^_^"); 193 } 194 195 @Override 196 protected Query getRangeQuery(String field, String part1, String part2, boolean inclusive) throws ParseException { 197 if (field.equals("size")) { 198 // 默认的QueryParser.parse(String query)表达式中并不支持'size:[20 TO 80]'数字的域值 199 // 这样一来,针对数字的域值进行特殊处理,那么QueryParser表达式就支持数字了 200 return NumericRangeQuery.newIntRange(field, Integer.parseInt(part1), Integer.parseInt(part2), inclusive, inclusive); 201 } 202 else if (field.equals("date")) { 203 String regex = "\\d{8}"; 204 String dateType = "yyyyMMdd"; 205 if (Pattern.matches(regex, part1) && Pattern.matches(regex, part2)) { 206 SimpleDateFormat sdf = new SimpleDateFormat(dateType); 207 try { 208 long min = sdf.parse(part1).getTime(); 209 long max = sdf.parse(part2).getTime(); 210 // 使之支持日期的检索,应用时直接QueryParser.parse("date:[20130407 TO 20130701]") 211 return NumericRangeQuery.newLongRange(field, min, max, inclusive, inclusive); 212 } 213 catch (java.text.ParseException e) { 214 e.printStackTrace(); 215 } 216 } 217 else { 218 throw new ParseException("Unknown date format, please use '" + dateType + "'"); 219 } 220 } 221 // 如没找到匹配的Field域,那么返回默认的TermRangeQuery 222 return super.getRangeQuery(field, part1, part2, inclusive); 223 } 224 } 225 }
8.高亮
1 import java.io.File; 2 import java.io.IOException; 3 4 import org.apache.lucene.analysis.Analyzer; 5 import org.apache.lucene.document.Document; 6 import org.apache.lucene.document.Field; 7 import org.apache.lucene.index.IndexReader; 8 import org.apache.lucene.index.IndexWriter; 9 import org.apache.lucene.index.IndexWriterConfig; 10 import org.apache.lucene.queryParser.MultiFieldQueryParser; 11 import org.apache.lucene.queryParser.QueryParser; 12 import org.apache.lucene.search.IndexSearcher; 13 import org.apache.lucene.search.Query; 14 import org.apache.lucene.search.ScoreDoc; 15 import org.apache.lucene.search.TopDocs; 16 import org.apache.lucene.search.highlight.Formatter; 17 import org.apache.lucene.search.highlight.Fragmenter; 18 import org.apache.lucene.search.highlight.Highlighter; 19 import org.apache.lucene.search.highlight.QueryScorer; 20 import org.apache.lucene.search.highlight.SimpleHTMLFormatter; 21 import org.apache.lucene.search.highlight.SimpleSpanFragmenter; 22 import org.apache.lucene.store.Directory; 23 import org.apache.lucene.store.FSDirectory; 24 import org.apache.lucene.util.Version; 25 import org.apache.tika.Tika; 26 27 import com.chenlb.mmseg4j.analysis.MMSegAnalyzer; 28 29 /** 30 * 【Lucene3.6.2入门系列】第11节_高亮 31 * 32 * @see 高亮功能属于Lucene的扩展功能(或者叫做贡献功能) 33 * @see 其所需jar位于Lucene-3.6.2.zip中的/contrib/highlighter/文件夹中 34 * @see 本例中需要以下4个jar 35 * @see lucene-core-3.6.2.jar 36 * @see lucene-highlighter-3.6.2.jar 37 * @see mmseg4j-all-1.8.5-with-dic.jar 38 * @see tika-app-1.4.jar 39 * @create Aug 7, 2013 11:37:10 AM 40 * @author 玄玉<http://blog.csdn.net/jadyer> 41 */ 42 public class Lucene_08_HelloHighLighter { 43 private Directory directory; 44 private IndexReader reader; 45 46 public Lucene_08_HelloHighLighter() { 47 Document doc = null; 48 IndexWriter writer = null; 49 try { 50 directory = FSDirectory.open(new File("E:/lucene_test/01_index/")); 51 writer = new IndexWriter(directory, new IndexWriterConfig(Version.LUCENE_36, new MMSegAnalyzer())); 52 writer.deleteAll(); 53 for (File myFile : new File("E:/lucene_test/01_index/").listFiles()) { 54 doc = new Document(); 55 doc.add(new Field("filecontent", new Tika().parse(myFile))); // Field.Store.NO,Field.Index.ANALYZED 56 doc.add(new Field("filepath", myFile.getAbsolutePath(), Field.Store.YES, Field.Index.NOT_ANALYZED_NO_NORMS)); 57 writer.addDocument(doc); 58 } 59 } 60 catch (Exception e) { 61 e.printStackTrace(); 62 } 63 finally { 64 if (null != writer) { 65 try { 66 writer.close(); 67 } 68 catch (IOException ce) { 69 ce.printStackTrace(); 70 } 71 } 72 } 73 } 74 75 /** 76 * 获取IndexSearcher实例 77 */ 78 private IndexSearcher getIndexSearcher() { 79 try { 80 if (reader == null) { 81 reader = IndexReader.open(directory); 82 } 83 else { 84 // if the index was changed since the provided reader was opened, open and return a new reader; else,return null 85 // 如果当前reader在打开期间index发生改变,则打开并返回一个新的IndexReader,否则返回null 86 IndexReader ir = IndexReader.openIfChanged(reader); 87 if (ir != null) { 88 reader.close(); // 关闭原reader 89 reader = ir; // 赋予新reader 90 } 91 } 92 return new IndexSearcher(reader); 93 } 94 catch (Exception e) { 95 e.printStackTrace(); 96 } 97 return null; // 发生异常则返回null 98 } 99 100 /** 101 * 高亮搜索 102 * 103 * @see 高亮搜索时,不建议把高亮信息存到索引里,而是搜索到内容之后再进行高亮处理 104 * @see 这里用的是MMSeg4j中文分词器,有关其介绍详见http://blog.csdn.net/jadyer/article/details/10049525 105 * @param expr 106 * 搜索表达式 107 */ 108 public void searchByHignLighter(String expr) { 109 Analyzer analyzer = new MMSegAnalyzer(); 110 IndexSearcher searcher = this.getIndexSearcher(); 111 // 搜索多个Field 112 QueryParser parser = new MultiFieldQueryParser(Version.LUCENE_36, new String[] { "filepath", "filecontent" }, analyzer); 113 try { 114 Query query = parser.parse(expr); 115 TopDocs tds = searcher.search(query, 50); 116 for (ScoreDoc sd : tds.scoreDocs) { 117 Document doc = searcher.doc(sd.doc); 118 // 获取文档内容 119 String filecontent = new Tika().parseToString(new File(doc.get("filepath"))); 120 System.out.println("搜索到的内容为[" + filecontent + "]"); 121 // 开始高亮处理 122 QueryScorer queryScorer = new QueryScorer(query); 123 Fragmenter fragmenter = new SimpleSpanFragmenter(queryScorer, filecontent.length()); 124 Formatter formatter = new SimpleHTMLFormatter("", ""); 125 Highlighter hl = new Highlighter(formatter, queryScorer); 126 hl.setTextFragmenter(fragmenter); 127 System.out.println("高亮后的内容为[" + hl.getBestFragment(analyzer, "filecontent", filecontent) + "]"); 128 } 129 } 130 catch (Exception e) { 131 e.printStackTrace(); 132 } 133 finally { 134 if (null != searcher) { 135 try { 136 searcher.close(); // 记得关闭IndexSearcher 137 } 138 catch (IOException e) { 139 e.printStackTrace(); 140 } 141 } 142 } 143 } 144 145 /** 146 * 高亮的使用方式 147 * 148 * @see 这里用的是MMSeg4j中文分词器,有关其介绍详见http://blog.csdn.net/jadyer/article/details/10049525 149 */ 150 private static void testHighLighter() { 151 String fieldName = "myinfo"; // 这个可以随便写,就是起个标识的作用 152 String text = "我来自中国黑龙江省哈尔滨市巴彦县兴隆镇长春乡民权村4队"; 153 QueryParser parser = new QueryParser(Version.LUCENE_36, fieldName, new MMSegAnalyzer()); 154 try { 155 // MMSeg4j的new MMSegAnalyzer()默认只会对'中国'和'兴隆'进行分词,所以这里就只高亮它们俩了 156 Query query = parser.parse("中国 兴隆"); 157 // 针对查询出来的文本,查询其评分,以便于能够根据评分决定显示情况 158 QueryScorer queryScorer = new QueryScorer(query); 159 // 对字符串或文本进行分段,SimpleSpanFragmenter构造方法的第二个参数可以指定高亮的文本长度,默认为100 160 Fragmenter fragmenter = new SimpleSpanFragmenter(queryScorer); 161 // 高亮时的高亮格式,默认为,这里指定为红色字体 162 Formatter formatter = new SimpleHTMLFormatter("", ""); 163 // Highlighter专门用来做高亮显示 164 // 该构造方法还有一个参数为Encoder,它有两个实现类DefaultEncoder和SimpleHTMLEncoder 165 // SimpleHTMLEncoder可以忽略掉HTML标签,而DefaultEncoder则不会忽略HTML标签 166 Highlighter hl = new Highlighter(formatter, queryScorer); 167 hl.setTextFragmenter(fragmenter); 168 System.out.println(hl.getBestFragment(new MMSegAnalyzer(), fieldName, text)); 169 } 170 catch (Exception e) { 171 e.printStackTrace(); 172 } 173 } 174 175 /** 176 * 小测试一下 177 */ 178 public static void main(String[] args) { 179 // 测试高亮的基本使用效果 180 Lucene_08_HelloHighLighter.testHighLighter(); 181 // 测试高亮搜索的效果(测试前记得在myExample/myFile/文件夹中准备一个或多个内容包含"依赖"的doc或pdf的等文件) 182 // new Lucene_08_HelloHighLighter().searchByHignLighter("依赖"); 183 } 184 }
9.近实时搜索
1 import java.io.File; 2 import java.io.IOException; 3 4 import org.apache.lucene.analysis.standard.StandardAnalyzer; 5 import org.apache.lucene.document.Document; 6 import org.apache.lucene.document.Field; 7 import org.apache.lucene.index.IndexReader; 8 import org.apache.lucene.index.IndexWriter; 9 import org.apache.lucene.index.IndexWriterConfig; 10 import org.apache.lucene.index.Term; 11 import org.apache.lucene.search.IndexSearcher; 12 import org.apache.lucene.search.NRTManager; 13 import org.apache.lucene.search.NRTManager.TrackingIndexWriter; 14 import org.apache.lucene.search.NRTManagerReopenThread; 15 import org.apache.lucene.search.Query; 16 import org.apache.lucene.search.ScoreDoc; 17 import org.apache.lucene.search.TermQuery; 18 import org.apache.lucene.search.TopDocs; 19 import org.apache.lucene.store.Directory; 20 import org.apache.lucene.store.FSDirectory; 21 import org.apache.lucene.util.Version; 22 23 /** 24 * 【Lucene3.6.2入门系列】第12节_近实时搜索 25 * 26 * @see 实时搜索(near-real-time)---->只要数据发生变化,则马上更新索引(IndexWriter.commit()) 27 * @see 近实时搜索------------------>数据发生变化时,先将索引保存到内存中,然后在一个统一的时间再对内存中的所有索引执行commit提交动作 28 * @see 为了实现近实时搜索,Lucene3.0提供的方式叫做reopen,后来的版本中提供了两个线程安全的类NRTManager和SearcherManager 29 * @see 不过这俩线程安全的类在Lucene3.5和3.6版本中的用法有点不太一样,这点要注意 30 * @create Aug 7, 2013 4:19:58 PM 31 * @author 玄玉<http://blog.csdn.net/jadyer> 32 */ 33 public class Lucene_09_HelloNRTSearch { 34 private IndexWriter writer; 35 private NRTManager nrtManager; 36 private TrackingIndexWriter trackWriter; 37 38 /** 39 * 测试时,要在E:/lucene_test/01_file/文件夹中准备几个包含内容的文件(比如txt格式的) 40 * 然后先执行createIndex()方法,再执行searchFile()方法,最后观看控制台输出即可 41 */ 42 public static void main(String[] args) { 43 Lucene_09_HelloNRTSearch instance = new Lucene_09_HelloNRTSearch(); 44 instance.createIndex(); 45 instance.testSearchFile(); 46 instance.getDocsCount(); 47 } 48 49 public void testSearchFile() { 50 Lucene_09_HelloNRTSearch hello = new Lucene_09_HelloNRTSearch(); 51 for (int i = 0; i < 5; i++) { 52 hello.searchFile(); 53 System.out.println("-----------------------------------------------------------"); 54 hello.deleteIndex(); 55 if (i == 2) { 56 hello.updateIndex(); 57 } 58 try { 59 System.out.println(".........开始休眠2s(模拟近实时搜索情景)"); 60 Thread.sleep(2000); 61 System.out.println(".........休眠结束"); 62 } 63 catch (InterruptedException e) { 64 e.printStackTrace(); 65 } 66 } 67 // 不能单独去new HelloNRTSearch,要保证它们是同一个对象,否则所做的delete和update不会被commit 68 hello.commitIndex(); 69 } 70 71 public Lucene_09_HelloNRTSearch() { 72 try { 73 Directory directory = FSDirectory.open(new File("E:/lucene_test/01_index/")); 74 writer = new IndexWriter(directory, new IndexWriterConfig(Version.LUCENE_36, new StandardAnalyzer(Version.LUCENE_36))); 75 trackWriter = new NRTManager.TrackingIndexWriter(writer); 76 // /* 77 // * Lucene3.5中的NRTManager是通过下面的方式创建的 78 // * 并且Lucene3.5中可以直接使用NRTManager.getSearcherManager(true)获取到org.apache.lucene.search.SearcherManager 79 // */ 80 // nrtManager = new NRTManager(writer,new org.apache.lucene.search.SearcherWarmer() { 81 // @Override 82 // public void warm(IndexSearcher s) throws IOException { 83 // System.out.println("IndexSearcher.reopen时会自动调用此方法"); 84 // } 85 // }); 86 nrtManager = new NRTManager(trackWriter, null); 87 // 启动一个Lucene提供的后台线程来自动定时的执行NRTManager.maybeRefresh()方法 88 // 这里的后俩参数,是根据这篇分析的文章写的http://blog.mikemccandless.com/2011/11/near-real-time-readers-with-lucenes.html 89 NRTManagerReopenThread reopenThread = new NRTManagerReopenThread(nrtManager, 5.0, 0.025); 90 reopenThread.setName("NRT Reopen Thread"); 91 reopenThread.setDaemon(true); 92 reopenThread.start(); 93 } 94 catch (Exception e) { 95 e.printStackTrace(); 96 } 97 } 98 99 /** 100 * 创建索引 101 */ 102 public void createIndex() { 103 String[] ids = { "1", "2", "3", "4", "5", "6" }; 104 String[] names = { "Michael", "Scofield", "Tbag", "Jack", "Jade", "Jadyer" }; 105 String[] contents = { "my blog", "my website", "my name", "my job is JavaDeveloper", "I am from Haerbin", "I like Lucene" }; 106 IndexWriter writer = null; 107 Document doc = null; 108 try { 109 Directory directory = FSDirectory.open(new File("E:/lucene_test/01_index/")); 110 writer = new IndexWriter(directory, new IndexWriterConfig(Version.LUCENE_36, new StandardAnalyzer(Version.LUCENE_36))); 111 writer.deleteAll(); 112 for (int i = 0; i < names.length; i++) { 113 doc = new Document(); 114 doc.add(new Field("id", ids[i], Field.Store.YES, Field.Index.NOT_ANALYZED_NO_NORMS)); 115 doc.add(new Field("name", names[i], Field.Store.YES, Field.Index.NOT_ANALYZED_NO_NORMS)); 116 doc.add(new Field("content", contents[i], Field.Store.YES, Field.Index.ANALYZED)); 117 writer.addDocument(doc); 118 } 119 } 120 catch (Exception e) { 121 e.printStackTrace(); 122 } 123 finally { 124 if (null != writer) { 125 try { 126 writer.close(); 127 } 128 catch (IOException ce) { 129 ce.printStackTrace(); 130 } 131 } 132 } 133 } 134 135 /** 136 * 通过IndexReader获取文档数量 137 */ 138 public void getDocsCount() { 139 IndexReader reader = null; 140 try { 141 reader = IndexReader.open(FSDirectory.open(new File("E:/lucene_test/01_index/"))); 142 System.out.println("maxDocs:" + reader.maxDoc()); 143 System.out.println("numDocs:" + reader.numDocs()); 144 System.out.println("deletedDocs:" + reader.numDeletedDocs()); 145 } 146 catch (Exception e) { 147 e.printStackTrace(); 148 } 149 finally { 150 if (reader != null) { 151 try { 152 reader.close(); 153 } 154 catch (IOException e) { 155 e.printStackTrace(); 156 } 157 } 158 } 159 } 160 161 /** 162 * 搜索文件 163 */ 164 public void searchFile() { 165 // Lucene3.5里面可以直接使用NRTManager.getSearcherManager(true).acquire() 166 IndexSearcher searcher = nrtManager.acquire(); 167 Query query = new TermQuery(new Term("content", "my")); 168 try { 169 TopDocs tds = searcher.search(query, 10); 170 for (ScoreDoc sd : tds.scoreDocs) { 171 Document doc = searcher.doc(sd.doc); 172 System.out.print("文档编号=" + sd.doc + " 文档权值=" + doc.getBoost() + " 文档评分=" + sd.score + " "); 173 System.out.println("id=" + doc.get("id") + " name=" + doc.get("name") + " content=" + doc.get("content")); 174 } 175 } 176 catch (Exception e) { 177 e.printStackTrace(); 178 } 179 finally { 180 try { 181 // 这里就不要IndexSearcher.close()啦,而是交由NRTManager来释放 182 nrtManager.release(searcher); 183 // Lucene-3.6.2文档中ReferenceManager.acquire()方法描述里建议再手工设置searcher为null,以防止在其它地方被意外的使用 184 searcher = null; 185 } 186 catch (IOException e) { 187 e.printStackTrace(); 188 } 189 } 190 } 191 192 /** 193 * 更新索引 194 */ 195 public void updateIndex() { 196 Document doc = new Document(); 197 doc.add(new Field("id", "11", Field.Store.YES, Field.Index.NOT_ANALYZED_NO_NORMS)); 198 doc.add(new Field("name", "xuanyu", Field.Store.YES, Field.Index.NOT_ANALYZED_NO_NORMS)); 199 doc.add(new Field("content", "my name is xuanyu", Field.Store.YES, Field.Index.ANALYZED)); 200 try { 201 // Lucene3.5中可以直接使用org.apache.lucene.search.NRTManager.updateDocument(new Term("id", "1"), doc) 202 trackWriter.updateDocument(new Term("id", "1"), doc); 203 } 204 catch (IOException e) { 205 e.printStackTrace(); 206 } 207 } 208 209 /** 210 * 删除索引 211 */ 212 public void deleteIndex() { 213 try { 214 // Lucene3.5中可以直接使用org.apache.lucene.search.NRTManager.deleteDocuments(new Term("id", "2")) 215 trackWriter.deleteDocuments(new Term("id", "2")); 216 } 217 catch (IOException e) { 218 e.printStackTrace(); 219 } 220 } 221 222 /** 223 * 提交索引内容的变更情况 224 */ 225 public void commitIndex() { 226 try { 227 writer.commit(); 228 } 229 catch (IOException e) { 230 e.printStackTrace(); 231 } 232 } 233 }
参考文章
http://www.chedong.com/tech/lucene.html
http://blog.csdn.net/column/details/jadyerlucene.html