http://blog.csdn.NET/mdcmy/article/details/38167955?utm_source=tuicool&utm_medium=referral
*******************************************************************************
花了一段时间学习lucene今天有时间把所学的写下来,网上有很多文章但大部分都是2.X和3.X版本的(当前最新版本4.9),希望这篇文章对自己和初学者有所帮助。
学习目录
(1)什么是lucene
(2)lucene常用类详解
(3)lucene简单实例
(4)lucene常用分词器
(5)lucene多条件查询
(6)修改删除索引
(7)lucene优化、排序
(8)lucene高亮显示
(9)lucene分页
(10)lucene注意几点
一、什么是lucene
Lucene是一套用于全文检索和搜寻的开源程式库是全文检索的框架而不是产品(不像百度不同), lucene其实就做两种工作:一入一出。所谓入是写入,即将你提供的源(本质是字符串)写入索引或者将其从索引中删除;所谓出是读出,即向用户提供全文搜索服务,让用户可以通过关键词定位源。
jakarta项目组的一个子项目,是一个开放源代码的全文检索引擎工具包,即它不是一个完整的全文检索引擎,而是一个全文检索引擎的架构,提供了完整的查询引擎和索引引擎,部分文本分析引擎(英文与德文两种西方语言)。Lucene的目的是为软件开发人员提供一个简单易用的工具包,以方便的在目标系统中实现全文检索的功能,或者是以此为基础建立起完整的全文检索引擎。
二、lucene常用类
Field有两个属性可选:存储和索引。通过存储属性你可以控制是否对这个Field进行存储;通过索引属性你可以控制是否对该Field进行索引。这看起来似乎有些废话,事实上对这两个属性的正确组合很重要,下面举例说明:还是以刚才的文章为例子,我们需要对标题和正文进行全文搜索,所以我们要把索引属性设置为true,同时我们希望能直接从搜索结果中提取文章标题,所以我们把标题域的存储属性设置为true,但是由于正文域太大了,我们为了缩小索引文件大小,将正文域的存储属性设置为false,当需要时再直接读取文件;我们只是希望能从搜索解果中提取最后修改时间,不需要对它进行搜索,所以我们把最后修改时间域的存储属性设置为true,索引属性设置为false。上面的三个域涵盖了两个属性的三种组合,还有一种全为false的没有用到,事实上Field不允许你那么设置,因为既不存储又不索引的域是没有意义的。
三、lucene简单实例
- private String filePath = "F:/myEclipse10/workspace/luceneTest/src/resource.txt";// 源文件所在位置
- private String indexDir = "F:/myEclipse10/workspace/luceneTest/src/index";// 索引目录
- private static final Version VERSION = Version.LUCENE_47;// lucene版本
3、创建索引方法
- /**
- * 创建索引
- *
- * @throws IOException
- */
- @Test
- public void createIndex() throws IOException {
- Directory director = FSDirectory.open(new File(indexDir));// 创建Directory关联源文件
- Analyzer analyzer = new StandardAnalyzer(VERSION);// 创建一个分词器
- IndexWriterConfig indexWriterConfig = new IndexWriterConfig(VERSION, analyzer);// 创建索引的配置信息
- IndexWriter indexWriter = new IndexWriter(director, indexWriterConfig);
- Document doc = new Document();// 创建文档
- String str = fileToString();// 读取txt中内容
- Field field1 = new StringField("title", "lucene测试", Store.YES);// 标题 StringField索引存储不分词
- Field field2 = new TextField("content", str, Store.NO);// 内容 TextField索引分词不存储
- Field field3 = new DoubleField("version", 1.2, Store.YES);// 版本 DoubleField类型
- Field field4 = new IntField("score", 90, Store.YES);// 评分 IntField类型
- doc.add(field1);// 添加field域到文档中
- doc.add(field2);
- doc.add(field3);
- doc.add(field4);
- indexWriter.addDocument(doc);// 添加文本到索引中
- indexWriter.close();// 关闭索引
- }
4、查询搜索方法
- /**
- * 查询搜索
- *
- * @throws IOException
- * @throws ParseException
- */
- @Test
- public void query() throws IOException, ParseException {
- IndexReader reader = DirectoryReader.open(FSDirectory.open(new File(indexDir)));// 索引读取类
- IndexSearcher search = new IndexSearcher(reader);// 搜索入口工具类
- String queryStr = "life";// 搜索关键字
- QueryParser queryParser = new QueryParser(VERSION, "content", new StandardAnalyzer(VERSION));// 实例查询条件类
- Query query = queryParser.parse(queryStr);
- TopDocs topdocs = search.search(query, 100);// 查询前100条
- System.out.println("查询结果总数---" + topdocs.totalHits);
- ScoreDoc scores[] = topdocs.scoreDocs;// 得到所有结果集
- for (int i = 0; i < scores.length; i++) {
- int num = scores[i].doc;// 得到文档id
- Document document = search.doc(num);// 拿到指定的文档
- System.out.println("内容====" + document.get("content"));// 由于内容没有存储所以执行结果为null
- System.out.println("标题====" + document.get("title"));
- System.out.println("版本====" + document.get("version"));
- System.out.println("评分====" + document.get("score"));
- System.out.println("id--" + num + "---scors--" + scores[i].score + "---index--" + scores[i].shardIndex);
- }
- }
5、读取文本内容
- /**
- * 读取文件的内容
- *
- * @return
- * @throws IOException
- */
- public String fileToString() throws IOException {
- StringBuffer sb = new StringBuffer();
- InputStream inputStream = new FileInputStream(new File(filePath));
- InputStreamReader inputStreamReader = new InputStreamReader(inputStream);
- BufferedReader br = new BufferedReader(inputStreamReader);
- String line = null;
- while ((line = br.readLine()) != null) {
- sb.append(line);
- }
- br.close();
- inputStreamReader.close();
- inputStream.close();
- return sb.toString();
- }
运行结果:
can be undeniably, impossibly difficult. We are faced with challenges
and events that can seem overwhelming, life-destroying to the point
where it may be hard to decide whether to keep
going. But you always have a choice. Jessica Heslop shares her
powerful, inspiring journey from the worst times in her life to the new
life she has created for herself
四、lucene常用分词器
- public class AnalyzerTest {
- private static final Version VERSION = Version.LUCENE_47;// lucene版本
- @Test
- public void test() throws IOException {
- String txt = "我是中国人";
- Analyzer analyzer1 = new StandardAnalyzer(VERSION);// 标准分词器
- // Analyzer analyzer2 = new SimpleAnalyzer(VERSION);// 简单分词器
- // Analyzer analyzer3 = new CJKAnalyzer(VERSION);// 二元切分
- // Analyzer analyzer4 = new IKAnalyzer(false);// 语意分词
- TokenStream tokenstream = analyzer1.tokenStream("content", new StringReader(txt));// 生成一个分词流
- // TokenStream tokenstream = analyzer2.tokenStream("content", new StringReader(txt));
- // TokenStream tokenstream = analyzer3.tokenStream("content", new StringReader(txt));
- // TokenStream tokenstream = analyzer4.tokenStream("content", new StringReader(txt));
- CharTermAttribute termAttribute = tokenstream.addAttribute(CharTermAttribute.class);// 为token设置属性类
- tokenstream.reset();// 重新设置
- while (tokenstream.incrementToken()) {// 遍历得到token
- System.out.print(new String(termAttribute.buffer(), 0, termAttribute.length()) + " ");
- }
- }
- }
运行结果:
(2)我是中国人
(3)我是 是中 中国 国人
(4)我 是 中国人 中国 国人
五、lucene多条件查询
- public class MultiseQueryTest {
- private String indexDir = "F:/myEclipse10/workspace/luceneTest/src/index";// 索引目录
- private static final Version VERSION = Version.LUCENE_47;// lucene版本
- /**
- * 多条件查询 查询内容必须包含life内容和评分大于等于80分的结果
- *
- * @throws IOException
- * @throws ParseException
- */
- @Test
- public void query() throws IOException, ParseException {
- IndexReader reader = DirectoryReader.open(FSDirectory.open(new File(indexDir)));// 索引读取类
- IndexSearcher search = new IndexSearcher(reader);// 搜索入口工具类
- String queryStr1 = "life";// 搜索关键字
- BooleanQuery booleanQuery = new BooleanQuery();
- // 条件一内容中必须要有life内容
- QueryParser queryParser = new QueryParser(VERSION, "content", new StandardAnalyzer(VERSION));// 实例查询条件类
- Query query1 = queryParser.parse(queryStr1);
- // 条件二评分大于等于80
- Query query2 = NumericRangeQuery.newIntRange("score", 80, null, true, false);
- booleanQuery.add(query1, BooleanClause.Occur.MUST);
- booleanQuery.add(query2, BooleanClause.Occur.MUST);
- TopDocs topdocs = search.search(booleanQuery, 100);// 查询前100条
- System.out.println("查询结果总数---" + topdocs.totalHits);
- ScoreDoc scores[] = topdocs.scoreDocs;// 得到所有结果集
- for (int i = 0; i < scores.length; i++) {
- int num = scores[i].doc;// 得到文档id
- Document document = search.doc(num);// 拿到指定的文档
- System.out.println("内容====" + document.get("content"));// 由于内容没有存储所以执行结果为null
- System.out.println("标题====" + document.get("title"));
- System.out.println("版本====" + document.get("version"));
- System.out.println("评分====" + document.get("score"));
- System.out.println("id--" + num + "---scors--" + scores[i].score + "---index--" + scores[i].shardIndex);
- }
- }
- }
运行结果就不粘上去了。本例子运用了NumericRangeQuery创建一个查询条件(还有很多其他的类有兴趣可以一一实验下),五个参数分别为字段域、最小值、最大值、是否包含最小值、是否包含最大值。一开始很迷茫为什么必须要设置最大值和最小值呢?如果我是单范围查询呢?后来看api才发现单范围时可以用null或者把是否包含范围值设为false就行了。
1.MUST和MUST:取得连个查询子句的交集。
2.MUST和MUST_NOT:表示查询结果中不能包含MUST_NOT所对应得查询子句的检索结果。
3.MUST_NOT和MUST_NOT:无意义,检索无结果。
4.SHOULD与MUST、SHOULD与MUST_NOT:SHOULD与MUST连用时,无意义,结果为MUST子句的检索结果。与MUST_NOT连用时,功能同MUST。
5.SHOULD与SHOULD:表示“或”关系,最终检索结果为所有检索子句的并集。
六、修改删除索引
- /**
- * 修改索引
- *
- * @throws IOException
- */
- @Test
- public void updateIndex() throws IOException {
- Directory director = FSDirectory.open(new File(indexDir));// 创建Directory关联源文件
- Analyzer analyzer = new StandardAnalyzer(VERSION);// 创建一个分词器
- IndexWriterConfig indexWriterConfig = new IndexWriterConfig(VERSION, analyzer);// 创建索引的配置信息
- IndexWriter indexWriter = new IndexWriter(director, indexWriterConfig);
- Document doc = new Document();// 创建文档
- Field field1 = new StringField("title", "lucene", Store.YES);// 标题 StringField索引存储不分词
- Field field2 = new TextField("content", "Is there life on Mars", Store.NO);// 内容 TextField索引分词不存储
- Field field3 = new DoubleField("version", 2.0, Store.YES);// 版本 DoubleField类型
- Field field4 = new IntField("score", 90, Store.YES);// 评分 IntField类型
- doc.add(field1);// 添加field域到文档中
- doc.add(field2);
- doc.add(field3);
- doc.add(field4);
- indexWriter.updateDocument(new Term("title", "lucene测试"), doc);
- indexWriter.commit();
- indexWriter.close();
- }
运行结果:前后对比
注意:(1)所谓的更新索引是分两步进行的:先删除然后再添加索引,添加的索引占用删除前索引的位置;如果在删除索引时lucene在索引文件中找不到相应的数据,就会在索引文件的最后面添加新的索引。
(2)如果我把indexWriter.updateDocument(new Term("title", "lucene测试"),
doc);换成indexWriter.updateDocument(new Term("title"), doc);的话同样是新创建索引。
- /**
- * 删除索引
- *
- * @throws IOException
- */
- @Test
- public void deleteIndex() throws IOException {
- Directory director = FSDirectory.open(new File(indexDir));// 创建Directory关联源文件
- Analyzer analyzer = new StandardAnalyzer(VERSION);// 创建一个分词器
- IndexWriterConfig indexWriterConfig = new IndexWriterConfig(VERSION, analyzer);// 创建索引的配置信息
- IndexWriter indexWriter = new IndexWriter(director, indexWriterConfig);
- indexWriter.deleteDocuments(new Term("title", "lucene"));
- indexWriter.commit();
- // indexWriter.rollback();
- indexWriter.close();
- }
删除索引就比较简单了,直接找到对应的字段域调用deleteDocuments方法。
七、lucene优化、排序
- /**
- * 优化
- *
- * @throws IOException
- */
- @Test
- public void optimize() throws IOException {
- Directory director = FSDirectory.open(new File(indexDir));// 创建Directory关联源文件
- Analyzer analyzer = new StandardAnalyzer(VERSION);// 创建一个分词器
- IndexWriterConfig indexWriterConfig = new IndexWriterConfig(VERSION, analyzer);// 创建索引的配置信息
- IndexWriter indexWriter = new IndexWriter(director, indexWriterConfig);
- indexWriter.forceMerge(1);// 当小文件达到多少个时,就自动合并多个小文件为一个大文件
- indexWriter.close();
- }
(2)排序lucene lucene默认情况下是根据“评分机制”来进行排序的,也就是scores[i].score属性值。如果两个文档得分相同,那么就按照发布时间倒序排列;否则就按照分数排列。
- /**
- * 创建索引
- *
- * @throws IOException
- */
- @Test
- public void createIndex() throws IOException {
- Directory director = FSDirectory.open(new File(indexDir));// 创建Directory关联源文件
- Analyzer analyzer = new StandardAnalyzer(VERSION);// 创建一个分词器
- IndexWriterConfig indexWriterConfig = new IndexWriterConfig(VERSION, analyzer);// 创建索引的配置信息
- IndexWriter indexWriter = new IndexWriter(director, indexWriterConfig);
- for (int i = 1; i <= 5; i++) {
- Document doc = new Document();// 创建文档
- Field field1 = new StringField("title", "标题" + i, Store.YES);// 标题 StringField索引存储不分词
- Field field2 = new TextField("content", "201" + i + "文章内容", Store.NO);// 内容 TextField索引分词不存储
- Field field3 = new DoubleField("version", 1.2, Store.YES);// 版本 DoubleField类型
- Field field4 = new IntField("score", 90 + i, Store.YES);// 评分 IntField类型
- Field field5 = new StringField("date", "2014-07-0" + i, Store.YES);// 评分 IntField类型
- doc.add(field1);// 添加field域到文档中
- doc.add(field2);
- doc.add(field3);
- doc.add(field4);
- doc.add(field5);
- indexWriter.addDocument(doc);// 添加文本到索引中
- }
- indexWriter.close();// 关闭索引
- }
首先看下默认情况下排序结果:
- /**
- * 排序
- *
- * @throws IOException
- * @throws ParseException
- */
- @Test
- public void defaultSortTest() throws IOException, ParseException {
- IndexReader reader = DirectoryReader.open(FSDirectory.open(new File(indexDir)));// 索引读取类
- IndexSearcher search = new IndexSearcher(reader);// 搜索入口工具类
- String queryStr = "文章";// 搜索关键字
- QueryParser queryParser = new QueryParser(VERSION, "content", new StandardAnalyzer(VERSION));// 实例查询条件类
- Query query = queryParser.parse(queryStr);
- TopDocs topdocs = search.search(query, 100);// 查询前100条
- System.out.println("查询结果总数---" + topdocs.totalHits);
- ScoreDoc scores[] = topdocs.scoreDocs;// 得到所有结果集
- for (int i = 0; i < scores.length; i++) {
- int num = scores[i].doc;// 得到文档id
- Document document = search.doc(num);// 拿到指定的文档
- System.out.println("内容====" + document.get("content"));// 由于内容没有存储所以执行结果为null
- System.out.println("标题====" + document.get("title"));
- System.out.println("版本====" + document.get("version"));
- System.out.println("评分====" + document.get("score"));
- System.out.println("日期====" + document.get("date"));
- System.out.println("id--" + num + "---scors--" + scores[i].score + "---index--" + scores[i].shardIndex);
- }
- }
- }
- Sort sort = new Sort(new SortField("score", SortField.Type.INT, true));// false升序true降序
- TopDocs topdocs = search.search(query, 100, sort);// 查询前100条
运行结果:
八、lucene高亮显示
(1)Fragmenter接口。作用是将原始字符串拆分成独立的片段。有三个实现类: NullFragmenter
是该接口的一个具体实现类,它将整个字符串作为单个片段返回,这适合于处理title域和前台文本较短的域,而对于这些域来说,我们是希望在搜索结果中全部展示。SimpleFragmenter
是负责将文本拆分封固定字符长度的片段,但它并处理子边界。你可以指定每个片段的字符长度(默认情况100)但这类片段有点过于简单,在创建片段时,他并不限制查询语句的位置,因此对于跨度的匹配操作会轻易被拆分到两个片段中;
SimpleSpanFragmenter 是尝试将让片段永远包含跨度匹配的文档。
(2)Scorer接口。Fragmenter输出的是文本片段序列,而Highlighter必须从中挑选出最适合的一个或多个片段呈现给客户,为了做到这点,Highlighter会要求Scorer来对每个片段进行评分。有两个实现类:QueryTermScorer
基于片段中对应Query的项数进行评分。QueryScorer只对促成文档匹配的实际项进行评分。
(3)Formatter接口。它负责将片段转换成String形式,以及将被高亮显示的项一起用于搜索结果展示以及高亮显示。有两个类:SimpleHTMLFormatter简单的html格式。GradientFormatter复杂型式对不同的得分实现不同的样式。
- /**
- * 高亮
- *
- * @throws IOException
- * @throws ParseException
- * @throws InvalidTokenOffsetsException
- */
- @Test
- public void highlighter() throws IOException, ParseException, InvalidTokenOffsetsException {
- IndexReader reader = DirectoryReader.open(FSDirectory.open(new File(indexDir)));// 索引读取类
- IndexSearcher search = new IndexSearcher(reader);// 搜索入口工具类
- Analyzer analyzer = new StandardAnalyzer(Version.LUCENE_47);// 分词器
- QueryParser qp = new QueryParser(Version.LUCENE_47, "content", analyzer);// 实例查询条件类
- Query query = qp.parse("文章");
- TopDocs topDocs = search.search(query, 100);// 查询前100条
- System.out.println("共查询出:" + topDocs.totalHits + "条数据");
- ScoreDoc scoreDoc[] = topDocs.scoreDocs;// 结果集
- // 高亮
- Formatter formatter = new SimpleHTMLFormatter("<font color='red'>", "</font>");// 高亮html格式
- Scorer score = new QueryScorer(query);// 检索评份
- Fragmenter fragmenter = new SimpleFragmenter(100);// 设置最大片断为100
- Highlighter highlighter = new Highlighter(formatter, score);// 高亮显示类
- highlighter.setTextFragmenter(fragmenter);// 设置格式
- for (int i = 0; i < scoreDoc.length; i++) {// 遍历结果集
- int docnum = scoreDoc[i].doc;
- Document doc = search.doc(docnum);
- String content = doc.get("content");
- System.out.println(content);// 原内容
- if (content != null) {
- TokenStream tokenStream = analyzer.tokenStream("content", new StringReader(content));
- String str = highlighter.getBestFragment(tokenStream, content);// 得到高亮显示后的内容
- System.out.println(str);
- }
- }
- }
选择结果:
九、lucene分页
lucene的分页有两种方式:(1)查询出所有结果然后进行分布。(2)通过TopScoreDocCollector.topDocs(int num1,int num2)来实现分页。
- /**
- * 分页
- *
- * @throws IOException
- * @throws ParseException
- */
- @Test
- public void pageTest() throws IOException, ParseException {
- IndexReader reader = DirectoryReader.open(FSDirectory.open(new File(indexDir)));// 索引读取类
- IndexSearcher search = new IndexSearcher(reader);// 搜索入口工具类
- String queryStr = "文章";// 搜索关键字
- QueryParser queryParser = new QueryParser(VERSION, "content", new StandardAnalyzer(VERSION));// 实例查询条件类
- Query query = queryParser.parse(queryStr);// 查询
- TopScoreDocCollector results = TopScoreDocCollector.create(100, false);// 结果集
- search.search(query, results);// 查询前100条
- TopDocs topdocs = results.topDocs(1, 2);// 从结果集中第1条开始取2条
- ScoreDoc scores[] = topdocs.scoreDocs;// 得到所有结果集
- for (int i = 0; i < scores.length; i++) {
- int num = scores[i].doc;// 得到文档id
- Document document = search.doc(num);// 拿到指定的文档
- System.out.println("内容====" + document.get("content"));// 由于内容没有存储所以执行结果为null
- System.out.println("标题====" + document.get("title"));
- System.out.println("版本====" + document.get("version"));
- System.out.println("评分====" + document.get("score"));
- System.out.println("id--" + num + "---scors--" + scores[i].score + "---index--" + scores[i].shardIndex);
- }
- }
十、lucene注意几点
其实任何好的框架都有他的缺陷,lucene也不例外,下面这几点是网上找的也是我们开发中要注意的地方。
1、lucene的索引不能太大,要不然效率会很低。大于1G的时候就必须考虑分布索引的问题
2、不建议用多线程来建索引,产生的互锁问题很麻烦。经常发现索引被lock,无法重新建立的情况
3、中文分词是个大问题,目前免费的分词效果都很差。如果有能力还是自己实现一个分词模块,用最短路径的切分方法,网上有教材和demo源码,可以参考。
4、建增量索引的时候很耗cpu,在访问量大的时候会导致cpu的idle为0
5、默认的评分机制不太合理,需要根据自己的业务定制
整体来说lucene要用好不容易,必须在上述方面扩充他的功能,才能作为一个商用的搜索引擎。