1. 课程计划
- Lucene的Field
- Lucene的索引库维护
- lucene的查询
a) Query子对象
b) QueryParser
4.Lucene相关度排序(了解)
2. Field域
2.1. Field属性
Field是文档中的域,包括Field名和Field值两部分,一个文档可以包括多个Field,Document只是Field的一个承载体,Field值即为要索引的内容,也是要搜索的内容。
1.是否分词(tokenized)
是:作分词处理,即将Field值进行分词,分词的目的是为了索引。
比如:商品名称、商品描述等,这些内容用户要输入关键字搜索,由于搜索的内容格式大、内容多需要分词后将语汇单元建立索引
否:不作分词处理
比如:商品id、订单号、身份证号等
2. 是否索引(indexed)
是:进行索引。将Field分词后的词或整个Field值进行索引,存储到索引域,索引的目的是为了搜索。
比如:商品名称、商品描述分析后进行索引,订单号、身份证号不用分词但也要索引,这些将来都要作为查询条件。
否:不索引。
比如:图片路径、文件路径等,不用作为查询条件的不用索引。
3.是否存储(stored)
是:将Field值存储在文档域中,存储在文档域中的Field才可以从Document中获取。
比如:商品名称、订单号,凡是将来要从Document中获取的Field都要存储。
否:不存储Field值
比如:商品描述,内容较大不用存储。如果要向用户展示商品描述可以从系统的关系数据库中获取。
2.2. Field常用类型
下边列出了开发中常用 的Filed类型,注意Field的属性,根据需求选择:
Field类 |
数据类型 |
Analyzed 是否分词 |
Indexed 是否索引 |
Stored 是否存储 |
说明 |
StringField(FieldName, FieldValue,Store.YES))
|
字符串 |
N |
Y |
Y或N |
这个Field用来构建一个字符串Field,但是不会进行分词,会将整个串存储在索引中,比如(订单号,身份证号等) 是否存储在文档中用Store.YES或Store.NO决定 |
LongField(FieldName, FieldValue,Store.YES) |
Long型 |
Y |
Y |
Y或N |
这个Field用来构建一个Long数字型Field,进行分词和索引,比如(价格)FloatField 是否存储在文档中用Store.YES或Store.NO决定 |
StoredField(FieldName, FieldValue) |
重载方法,支持多种类型 |
N |
N |
Y |
这个Field用来构建不同类型Field 不分析,不索引,但要Field存储在文档中 链接 |
TextField(FieldName, FieldValue, Store.NO) 或 TextField(FieldName, reader) |
字符串 或 流 |
Y |
Y |
Y或N |
如果是一个Reader, lucene猜测内容比较多,会采用Unstored的策略. |
2.3. Field修改
2.3.1. 修改分析
图书id:StringField
是否分词:不用分词,因为不会根据商品id来搜索商品
是否索引:不索引,因为不需要根据图书ID进行搜索
是否存储:要存储,因为查询结果页面需要使用id这个值。
图书名称:TextField
是否分词:要分词,因为要根据图书名称的关键词搜索。
是否索引:要索引。
是否存储:要存储。
图书价格:
是否分词:要分词,lucene对数字型的值只要有搜索需求的都要分词和索引,因 为lucene对数字型的内容要特殊分词处理,需要分词和索引。
是否索引:要索引
是否存储:要存储
图书图片地址:
是否分词:不分词
是否索引:不索引
是否存储:要存储
图书描述:
是否分词:要分词
是否索引:要索引
是否存储:因为图书描述内容量大,不在查询结果页面直接显示,不存储。
不存储是不在lucene的索引域中记录,节省lucene的索引文件空间。
如果要在详情页面显示描述,解决方案:
从lucene中取出图书的id,根据图书的id查询关系数据库(MySQL)中book表得到描述信息。
2.3.2. 代码修改
对之前编写的testCreateIndex()方法进行修改。
代码片段
1 // Document文档中添加域 2 3 // 图书Id 4 5 // Store.YES:表示存储到文档域中 6 7 // 不分词,不索引,储存 8 9 document.add(new StoredField("id", book.getId().toString())); 10 11 // 图书名称 12 13 // 分词,索引,储存 14 15 document.add(new TextField("name", book.getName().toString(), Store.YES)); 16 17 // 图书价格 18 19 // 分词,索引,储存 20 21 document.add(new FloatField("price", book.getPrice(), Store.YES)); 22 23 // 图书图片地址 24 25 // 不分词,不索引,储存 26 27 document.add(new StoredField("pic", book.getPic().toString())); 28 29 // 图书描述 30 31 // 分词,索引,不储存 32 33 document.add(new TextField("desc", book.getDesc().toString(), Store.NO));
3. 索引维护
3.1. 需求
管理人员通过电商系统更改图书信息,这时更新的是关系数据库,如果使用lucene搜索图书信息,需要在数据库表book信息变化时及时更新lucene索引库。
3.2. 添加索引
调用 indexWriter.addDocument(doc)添加索引。
参考入门程序的创建索引。
3.3. 删除索引
3.3.1. 删除指定索引
根据Term项删除索引,满足条件的将全部删除。
1 /** 2 *@author 作者: WangXS 3 *@version 日期: 2018年10月9日 下午4:29:05 4 * 5 * 索引维护 --删除索引 6 *删除指定索引 7 *删除全部索引 8 */ 9 public class DeleteIndexTest { 10 //获取IndexWriter写入对象 11 public IndexWriter getIndexWriter() throws IOException { 12 //3.创建分析器(分词器)支持中文 13 IKAnalyzer analyzer = new IKAnalyzer(); 14 //4.创建IndexWriterConfig配置信息类 15 IndexWriterConfig config = new IndexWriterConfig(Version.LUCENE_4_10_3, analyzer); 16 //5.创建Directory对象,声明索引库存储位置 17 Directory directory = FSDirectory.open(new File("F:\temp\index")); 18 //6.创建IndexWriter写入对象 19 IndexWriter indexWriter = new IndexWriter(directory, config); 20 return indexWriter; 21 } 22 23 @Test 24 public void testDeleteIndex() throws Exception { 25 // 获取写入对象 26 IndexWriter indexWriter = getIndexWriter(); 27 //小心被祭天~ 28 //删除全部索引 29 //indexWriter.deleteAll(); 30 //删除指定索引 31 Query query = new TermQuery(new Term("name", "apache")); 32 //indexWriter.deleteDocuments(new Term("name", "apache")); 33 //释放资源 34 indexWriter.close(); 35 } 36 }
3.3.2. 删除全部索引(慎用)
将索引目录的索引信息全部删除,直接彻底删除,无法恢复。
建议参照关系数据库基于主键删除方式,所以在创建索引时需要创建一个主键Field,删除时根据此主键Field删除。
索引删除后将放在Lucene的回收站中,Lucene3.X版本可以恢复删除的文档,3.X之后无法恢复。
3.4. 修改索引
更新索引是先删除再添加,建议对更新需求采用此方法并且要保证对已存在的索引执行更新,可以先查询出来,确定更新记录存在执行更新操作。
如果更新索引的目标文档对象不存在,则执行添加。
代码
1 /** 2 *@author 作者: WangXS 3 *@version 日期: 2018年10月9日 下午4:29:05 4 *索引维护 --更新索引 5 */ 6 public class UpdateIndexTest { 7 @Test 8 public void testUpdateIndex() throws Exception { 9 //创建文档对象 10 Document document = new Document(); 11 // Document文档中添加Field域 12 // 图书Id 13 // Store.YES:表示存储到文档域中 14 // 不分词,不索引,储存 15 document.add(new StoredField("ID", "007")); 16 // 图书名称 17 // 分词,索引,储存 18 document.add(new TextField("NAME", "大内密探", Store.YES)); 19 // 图书描述 20 // 分词,索引,不储存 21 document.add(new TextField("desc", "零零八.........", Store.NO)); 22 //3.创建分析器(分词器) 23 //StandardAnalyzer standardAnalyzer = new StandardAnalyzer(); 24 IKAnalyzer analyzer = new IKAnalyzer(); 25 //4.创建IndexWriterConfig配置信息类 26 IndexWriterConfig config = new IndexWriterConfig(Version.LUCENE_4_10_3, analyzer); 27 //5.创建Directory对象,声明索引库存储位置 28 Directory directory = FSDirectory.open(new File("F:\temp\index")); 29 //6.创建IndexWriter写入对象 30 IndexWriter indexWriter = new IndexWriter(directory, config); 31 //7.把Document写入到索引库中 32 indexWriter.updateDocument(new Term("name","apache"), document); 33 //8.释放资源 34 indexWriter.close(); 35 36 } 37 }
4. 搜索
4.1. 创建查询的两种方法
对要搜索的信息创建Query查询对象,Lucene会根据Query查询对象生成最终的查询语法。类似关系数据库Sql语法一样,Lucene也有自己的查询语法,比如:“name:lucene”表示查询名字为name的Field域中的“lucene”的文档信息。
可通过两种方法创建查询对象:
1)使用Lucene提供Query子类
Query是一个抽象类,lucene提供了很多查询对象,比如TermQuery项精确查询,NumericRangeQuery数字范围查询等。
如下代码:
Query query = new TermQuery(new Term("name", "lucene"));
2)使用QueryParse解析查询表达式
QueryParser会将用户输入的查询表达式解析成Query对象实例。
如下代码:
QueryParser queryParser = new QueryParser("name", new IKAnalyzer());
Query query = queryParser.parse("name:lucene");
4.2. 通过Query子类搜索
4.2.1. TermQuery
TermQuery词项查询,TermQuery不使用分析器,搜索关键词进行精确匹配Field域中的词,比如订单号、分类ID号等。 Where name =思念Spring
搜索对象创建:
1 public class QueryIndexTest { 2 //获取IndexWriter写入对象 3 public IndexSearcher getIndexSearcher() throws IOException { 4 //2. 创建Directory流对象,声明索引库位置 5 Directory directory = FSDirectory.open(new File("F:\temp\index")); 6 //3. 创建索引读取对象IndexReader 7 IndexReader indexReader = DirectoryReader.open(directory); 8 //4. 创建索引搜索对象IndexSearcher 9 IndexSearcher search = new IndexSearcher(indexReader); 10 return search; 11 } 12 @Test 13 public void testSearchIndex() throws Exception { 14 //1. 创建Query搜索对象 15 // 创建搜索解析器,第一个参数:默认Field域,第二个参数:分词器 16 IndexSearcher searcher = getIndexSearcher(); 17 Query query = new TermQuery(new Term("name","java")); 18 //5. 使用索引搜索对象,执行搜索,返回结果集TopDocs 19 // 第一个参数:搜索对象,第二个参数:返回的数据条数,指定查询结果最顶部的n条数据返回 20 printResult(searcher, query); 21 } 22 //打印结果 23 public void printResult(IndexSearcher searcher, Query query) throws IOException { 24 TopDocs topDocs = searcher.search(query, 5); 25 System.out.println("查询到的数据总条数是:" + topDocs.totalHits); 26 //6. 解析结果集 27 ScoreDoc[] scoreDocs = topDocs.scoreDocs; 28 for (ScoreDoc scoreDoc : scoreDocs) { 29 //获取文档 30 int docId = scoreDoc.doc; 31 Document doc = searcher.doc(docId); 32 System.out.println("============================="); 33 System.out.println("docID:" + docId); 34 System.out.println("bookId:" + doc.get("id")); 35 System.out.println("name:" + doc.get("name")); 36 System.out.println("price:" + doc.get("price")); 37 System.out.println("pic:" + doc.get("pic")); 38 // System.out.println("desc:" + doc.get("desc")); 39 } 40 //7. 释放资源 41 searcher.getIndexReader().close(); 42 } 43 }
4.2.2. NumericRangeQuery
NumericRangeQuery,指定数字范围查询.
1 @Test 2 3 public void testSearchNumericRangeQuery() throws Exception { 4 5 // 创建NumericRangeQuery搜索对象,数字范围查询. 6 7 // 五个参数分别是:域名、最小值、最大值、是否包含最小值,是否包含最大值 8 9 Query query = NumericRangeQuery.newFloatRange("price", 54f, 56f, false, true); 10 11 doSearch(query); 12 13 }
4.2.3. BooleanQuery
BooleanQuery,布尔查询,实现组合条件查询。
1 //BooleanQuery,布尔查询,实现组合条件查询。 2 @Test 3 public void testBooleanQuery() throws Exception { 4 //1. 创建Query搜索对象 5 // 创建搜索解析器,第一个参数:默认Field域,第二个参数:分词器 6 IndexSearcher searcher = getIndexSearcher(); 7 Query query1 = NumericRangeQuery.newFloatRange("price", 70f, 80f, true, true); 8 Query query2 = new TermQuery(new Term("name","java")); 9 BooleanQuery query = new BooleanQuery(); 10 query.add(query1, BooleanClause.Occur.MUST); 11 query.add(query2, BooleanClause.Occur.MUST); 12 //5. 使用索引搜索对象,执行搜索,返回结果集TopDocs 13 // 第一个参数:搜索对象,第二个参数:返回的数据条数,指定查询结果最顶部的n条数据返回 14 printResult(searcher, query); 15 }
组合关系代表的意思如下:
1、MUST和MUST表示“与”的关系,即“交集”。
2、MUST和MUST_NOT前者包含后者不包含。
3、MUST_NOT和MUST_NOT没意义
4、SHOULD与MUST表示MUST,SHOULD失去意义;
5、SHOULD与MUST_NOT相当于MUST与MUST_NOT。
6、SHOULD与SHOULD表示“或”的关系,即“并集”。
4.3. 通过QueryParser搜索
通过QueryParser也可以创建Query,QueryParser提供一个Parse方法,此方法可以直接根据查询语法来查询。可以通过打印Query对象的方式,查看生成的查询语句。
4.3.1. 查询语法
1、基础的查询语法,关键词查询:
域名+“:”+搜索的关键字
例如:name:java
2、范围查询
域名+“:”+[最小值 TO 最大值]
例如:size:[1 TO 1000]
注意:QueryParser不支持对数字范围的搜索,它支持字符串范围。数字范围搜索建议使用NumericRangeQuery。
3、组合条件查询
Occur.MUST 查询条件必须满足,相当于AND |
+(加号) |
Occur.SHOULD 查询条件可选,相当于OR |
空(不用符号) |
Occur.MUST_NOT 查询条件不能满足,相当于NOT非 |
-(减号) |
4.3.2. QueryParser
1 @Test 2 3 public void testSearchIndex() throws Exception { 4 5 // 创建分词器 6 7 Analyzer analyzer = new StandardAnalyzer(); 8 9 // 1. 创建Query搜索对象 10 11 // 创建搜索解析器,第一个参数:默认Field域,第二个参数:分词器 12 13 QueryParser queryParser = new QueryParser("desc", analyzer); 14 15 // 创建搜索对象 16 17 // Query query = queryParser.parse("desc:java学习"); 18 19 Query query = queryParser.parse("desc:java AND lucene"); 20 21 // 打印生成的搜索语句 22 23 System.out.println(query); 24 25 // 执行搜索 26 27 doSearch(query); 28 29 }
4.3.3. MultiFieldQueryParser
通过MultiFieldQueryParse对多个域查询。
1 @Test 2 3 public void testSearchMultiFieldQueryParser() throws Exception { 4 5 // 创建分词器 6 7 Analyzer analyzer = new IKAnalyzer(); 8 9 // 1. 创建MultiFieldQueryParser搜索对象 10 11 String[] fields = { "name", "desc" }; 12 13 MultiFieldQueryParser multiFieldQueryParser = new MultiFieldQueryParser(fields, analyzer); 14 15 // 创建搜索对象 16 17 Query query = multiFieldQueryParser.parse("lucene"); 18 19 // 打印生成的搜索语句 20 21 System.out.println(query); 22 23 // 执行搜索 24 25 doSearch(query); 26 27 }
生成的查询语句:
name:lucene desc:lucene
4.4. TopDocs
Lucene搜索结果可通过TopDocs遍历,TopDocs类提供了少量的属性,如下:
方法或属性 |
说明 |
totalHits |
匹配搜索条件的总记录数 |
scoreDocs |
顶部匹配记录 |
注意:
Search方法需要指定匹配记录数量n:indexSearcher.search(query, n)
TopDocs.totalHits:是匹配索引库中所有记录的数量
TopDocs.scoreDocs:匹配相关度高的前边记录数组,scoreDocs的长度小于等于search方法指定的参数n
5. 相关度排序(了解)
5.1. 什么是相关度排序
相关度排序是查询结果按照与查询关键字的相关性进行排序,越相关的越靠前。比如搜索“Lucene”关键字,与该关键字最相关的文章应该排在前边。
5.2. 相关度打分
Lucene对查询关键字和索引文档的相关度进行打分,得分高的就排在前边。如何打分呢?Lucene是在用户进行检索时实时根据搜索的关键字计算出来的,分两步:
1)计算出词(Term)的权重
2)根据词的权重值,计算文档相关度得分。
什么是词的权重?
通过索引部分的学习,明确索引的最小单位是一个Term(索引词典中的一个词)。搜索也是从索引域中查询Term,再根据Term找到文档。Term对文档的重要性称为权重,影响Term权重有两个因素:
l Term Frequency (tf):
指此Term在此文档中出现了多少次。tf 越大说明越重要。
词(Term)在文档中出现的次数越多,说明此词(Term)对该文档越重要,如“Lucene”这个词,在文档中出现的次数很多,说明该文档主要就是讲Lucene技术的。
l Document Frequency (df):
指有多少文档包含此Term。df 越大说明越不重要。
比如,在一篇英语文档中,this出现的次数更多,就说明越重要吗?不是的,有越多的文档包含此词(Term), 说明此词(Term)太普通,不足以区分这些文档,因而重要性越低。
5.3. 设置boost值影响相关度排序
boost是一个加权值(默认加权值为1.0f),它可以影响权重的计算。在索引时对某个文档中的field设置加权值,设置越高,在搜索时匹配到这个文档就可能排在前边。
未设置权重:
希望把name为spring的排名提高
先清空索引库,然后修改创建索引的代码,添加设置加权值的逻辑
修改创建索引代码:
1 public class CreateIndexTest { 2 @Test 3 public void testCreateIndex() throws Exception { 4 //1.采集数据 5 BookDao bd = new BookDaoImpl(); 6 List<Book> bookList = bd.queryBookList(); 7 //2.创建Document文档对象 8 List<Document> documents = new ArrayList<>(); 9 for (Book book : bookList) { 10 Document document = new Document(); 11 // Document文档中添加Field域 12 // 图书Id 13 // Store.YES:表示存储到文档域中 14 // 不分词,不索引,储存 15 document.add(new StoredField("id", book.getId().toString())); 16 // 图书名称 17 // 分词,索引,储存 18 TextField nameField = new TextField("name", book.getName().toString(), Store.YES); 19 if (book.getId()==4) { 20 nameField.setBoost(10); 21 } 22 document.add(nameField); 23 // 图书价格 24 // 分词,索引,储存 25 document.add(new FloatField("price", book.getPrice(), Store.YES)); 26 // 图书图片地址 27 // 不分词,不索引,储存 28 document.add(new StoredField("pic", book.getPic().toString())); 29 // 图书描述 30 // 分词,索引,不储存 31 document.add(new TextField("desc", book.getDesc().toString(), Store.NO)); 32 // 把Document放到list中 33 documents.add(document); 34 } 35 //3.创建分析器(分词器) 36 //StandardAnalyzer standardAnalyzer = new StandardAnalyzer(); 37 IKAnalyzer analyzer = new IKAnalyzer(); 38 //4.创建IndexWriterConfig配置信息类 39 IndexWriterConfig config = new IndexWriterConfig(Version.LUCENE_4_10_3, analyzer); 40 //5.创建Directory对象,声明索引库存储位置 41 Directory directory = FSDirectory.open(new File("F:\temp\index")); 42 //6.创建IndexWriter写入对象 43 IndexWriter indexWriter = new IndexWriter(directory, config); 44 //7.把Document写入到索引库中 45 for (Document document : documents) { 46 indexWriter.addDocument(document); 47 } 48 //8.释放资源 49 indexWriter.close(); 50 51 } 52 }