Lucene简介
Lucene全文检索技术简介
Lucene简介
作者:doug cutting
Lucene是apache下的一个全文检索引擎工具包,底层封装的是一个全文检索算法。因为在大并发和大数据量的访问下,我们在传统数据库中进行like模糊查找会很慢,因为like分装的是顺序扫描法,所以在这个时候,面对这个问题就产生了全文检索算法,而Lucene就是这个算法的一个工具包。
算法介绍;
Sql语句中的like使用的是顺序扫描法:
顺序扫描算法
将需要查询的文本或者是数据,从头开始检索,直到找到需要查询的关键字为止
优点:准确度高
缺点:效率会随着数据量的提高越来越慢
全文检索算法
是在查询之前。需要先将需要查询的内容中的词提取出来(切分词—就是将一句话中的一个一个的词提取出来,去掉停用词,去掉标点符号,去掉空格,大写字母转换成小写字母)组成索引查询的时候,需要先查询索引,根据索引找文档,这样的算法叫做全文检索方法
优点:查询速度快,而且不会随着数据量的增大而使查询速度变慢
缺点:此算法是空间换时间,也就是索引会额外占用更多的磁盘空间,但是速度会变快。
Lucene的实现过程
在这里我们可以吧一本书当成一个文档,文档中包括一个一个的域,域中存的是这本书的信息,域是以Key——value的 存在的,我们会把域中的对应的描述分为若干个词语,在分词的过程中建立索引,并生成权量(表示匹配的程度),
Lucene基于java的索引创建
import java.io.File;
import java.util.ArrayList;
import java.util.List;
import org.apache.lucene.analysis.Analyzer;
import org.apache.lucene.analysis.standard.StandardAnalyzer;
import org.apache.lucene.document.Document;
import org.apache.lucene.document.Field.Store;
import org.apache.lucene.document.TextField;
import org.apache.lucene.index.IndexWriter;
import org.apache.lucene.index.IndexWriterConfig;
import org.apache.lucene.store.Directory;
import org.apache.lucene.store.FSDirectory;
import org.apache.lucene.util.Version;
import org.junit.Test;
//创建索引
public class CreatIndex {
@Test
public void testCreateIndex() throws Exception{
//1:采集数据
Dao dao=new DaoImpl();
List<Book> list =dao.queryBookList();
//2.创建Document文当对象
List<Document> documents=new ArrayList<>();
for (Book book : list) {
Document document=new Document();
// Document文档中添加Field域
// Store.YES:表示存储到文档域中
document.add(new TextField("id", book.getId().toString(), Store.YES));
// 图书名称
document.add(new TextField("name", book.getName().toString(), Store.YES));
// 图书价格
document.add(new TextField("price", book.getPrice().toString(), Store.YES));
// 图书图片地址
document.add(new TextField("pic", book.getPic().toString(), Store.YES));
// 图书描述
document.add(new TextField("desc", book.getDesc().toString(), Store.YES));
// 把Document放到list中
documents.add(document);
}
//3.创建Analyzer分词器,分析文档,对文档进行分词
Analyzer analyzer = new StandardAnalyzer();
//4.创建Directory对象,声明索引库的位置
Directory directory = FSDirectory.open(new File("D:/luceneindex"));
//5.创建IndexWriteConfig对象,写入索引需要的配置 需要版本 和 分词器
IndexWriterConfig config = new IndexWriterConfig(Version.LUCENE_4_10_3, analyzer);
// 6.创建IndexWriter写入对象
IndexWriter indexWriter = new IndexWriter(directory, config);
// 7.写入到索引库,通过IndexWriter添加文档对象document
for (Document doc : documents) {
//
indexWriter.addDocument(doc);
}
// 8.释放资源
indexWriter.close();
}
}
Lucene基于java的索引查询
import java.io.File;
import org.apache.lucene.analysis.Analyzer;
import org.apache.lucene.analysis.standard.StandardAnalyzer;
import org.apache.lucene.document.Document;
import org.apache.lucene.index.DirectoryReader;
import org.apache.lucene.index.IndexReader;
import org.apache.lucene.queryparser.classic.QueryParser;
import org.apache.lucene.search.IndexSearcher;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.ScoreDoc;
import org.apache.lucene.search.TopDocs;
import org.apache.lucene.store.Directory;
import org.apache.lucene.store.FSDirectory;
import org.junit.Test;
public class findIndex {
@Test
public void findIndexTest() throws Exception {
//1.创建query搜索对象
//创建分词器
Analyzer analyzer=new StandardAnalyzer();
//创建搜索解析器。第一个参数:默认field域,第二个参数:分词器
QueryParser queryParser=new QueryParser("name",analyzer );
//创建搜索对象
Query query =queryParser.parse("name:java AND 思想");
//2.创建Directory流对象,声明索引库位置
Directory directory=FSDirectory.open(new File("D:/luceneindex"));
//3.创建索引读取对象indexReader
IndexReader reader=DirectoryReader.open(directory);
// 4. 创建索引搜索对象
IndexSearcher searcher = new IndexSearcher(reader);
// 5. 使用索引搜索对象,执行搜索,返回结果集TopDocs
// 第一个参数:搜索对象,第二个参数:返回的数据条数,指定查询结果最顶部的n条数据返回
TopDocs topDocs = searcher.search(query, 10);
System.out.println("查询到的数据总条数是:" + topDocs.totalHits);
// 获取查询结果集
ScoreDoc[] docs = topDocs.scoreDocs;
// 6. 解析结果集
for (ScoreDoc scoreDoc : docs) {
// 获取文档
int docID = scoreDoc.doc;
Document doc = searcher.doc(docID);
System.out.println("=============================");
System.out.println("docID:" + docID);
System.out.println("bookId:" + doc.get("id"));
System.out.println("name:" + doc.get("name"));
System.out.println("price:" + doc.get("price"));
System.out.println("pic:" + doc.get("pic"));
System.out.println("desc:" + doc.get("desc"));
}
// 7. 释放资源
reader.close();
}
}
-
Field域的属性
Field是文档中的域,包括Field名和Field值两部分,一个文档可以包括多个Field,Document只是Field的一个承载体,Field值即为要索引的内容,也是要搜索的内容。
是否分词(tokenized)
是:作分词处理,即将Field值进行分词,分词的目的是为了索引。
比如:商品名称、商品描述等,这些内容用户要输入关键字搜索,由于搜索的内容格式大、内容多需要分词后将语汇单元建立索引
否:不作分词处理
比如:商品id、订单号、身份证号等
是否索引(indexed)
是:进行索引。将Field分词后的词或整个Field值进行索引,存储到索引域,索引的目的是为了搜索。
比如:商品名称、商品描述分析后进行索引,订单号、身份证号不用分词但也要索引,这些将来都要作为查询条件。
否:不索引。
比如:图片路径、文件路径等,不用作为查询条件的不用索引。
是否存储(stored)
是:将Field值存储在文档域中,存储在文档域中的Field才可以从Document中获取。
比如:商品名称、订单号,凡是将来要从Document中获取的Field都要存储。
否:不存储Field值
比如:商品描述,内容较大不用存储。如果要向用户展示商品描述可以从系统的关系数据库中获取。
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,进行分词和索引,比如(价格) 是否存储在文档中用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的策略. |
对之前编写的CreatIndex ()方法进行修改。
代码片段
// Document文档中添加域
// 图书Id
// Store.YES:表示存储到文档域中
// 不分词,不索引,储存
document.add(new StoredField("id", book.getId().toString()));
// 图书名称
// 分词,索引,储存
document.add(new TextField("name", book.getName().toString(), Store.YES));
// 图书价格
// 分词,索引,储存
document.add(new FloatField("price", book.getPrice(), Store.YES));
// 图书图片地址
// 不分词,不索引,储存
document.add(new StoredField("pic", book.getPic().toString()));
// 图书描述
// 分词,索引,不储存
document.add(new TextField("desc", book.getDesc().toString(), Store.NO));
-
索引的增删改
添加索引
调用 indexWriter.addDocument(doc)添加索引。
参考入门程序的创建索引。
-
删除索引
删除指定索引
根据Term项删除索引,满足条件的将全部删除。
@Test
public void testIndexDelete() throws Exception {
// 创建Directory流对象
Directory directory = FSDirectory.open(new File("C:/itcast/lucene/index"));
IndexWriterConfig config = new IndexWriterConfig(Version.LUCENE_4_10_3, null);
// 创建写入对象
IndexWriter indexWriter = new IndexWriter(directory, config);
// 根据Term删除索引库,name:java
indexWriter.deleteDocuments(new Term("name", "java"));
// 释放资源
indexWriter.close();
}
效果如下图:索引域没有变化
文档域数据被删除掉
删除全部索引(慎用)
将索引目录的索引信息全部删除,直接彻底删除,无法恢复。
建议参照关系数据库基于主键删除方式,所以在创建索引时需要创建一个主键Field,删除时根据此主键Field删除。
索引删除后将放在Lucene的回收站中,Lucene3.X版本可以恢复删除的文档,3.X之后无法恢复。
代码:
@Test
public void testIndexDelete() throws Exception {
// 创建Directory流对象
Directory directory = FSDirectory.open(new File("D:/itcast/lucene/index"));
IndexWriterConfig config = new IndexWriterConfig(Version.LUCENE_4_10_3, null);
// 创建写入对象
IndexWriter indexWriter = new IndexWriter(directory, config);
// 根据Term删除索引库,name:java
// indexWriter.deleteDocuments(new Term("name", "java"));
// 全部删除
indexWriter.deleteAll();
// 释放资源
indexWriter.close();
}
索引域数据清空
文档域数据也清空
修改索引
更新索引是先删除再添加,建议对更新需求采用此方法并且要保证对已存在的索引执行更新,可以先查询出来,确定更新记录存在执行更新操作。
如果更新索引的目标文档对象不存在,则执行添加。
代码
@Test
public void testIndexUpdate() throws Exception {
// 创建分词器
Analyzer analyzer = new IKAnalyzer();
// 创建Directory流对象
Directory directory = FSDirectory.open(new File("C:/itcast/lucene/index"));
IndexWriterConfig config = new IndexWriterConfig(Version.LUCENE_4_10_3, analyzer);
// 创建写入对象
IndexWriter indexWriter = new IndexWriter(directory, config);
// 创建Document
Document document = new Document();
document.add(new TextField("id", "1002", Store.YES));
document.add(new TextField("name", "lucene测试test 002", Store.YES));
// 执行更新,会把所有符合条件的Document删除,再新增。
indexWriter.updateDocument(new Term("name", "test"), document);
// 释放资源
indexWriter.close();
}
-
搜索
创建查询的两种方法
对要搜索的信息创建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");
-
通过Query子类搜索
TermQuery
TermQuery词项查询,TermQuery不使用分析器,搜索关键词进行精确匹配Field域中的词,比如订单号、分类ID号等。
搜索对象创建:
@Test
public void testSearchTermQuery() throws Exception {
// 创建TermQuery搜索对象
Query query = new TermQuery(new Term("name", "lucene"));
doSearch(query);
}
抽取搜索逻辑:
private void doSearch(Query query) throws IOException {
// 2. 执行搜索,返回结果集
// 创建Directory流对象
Directory directory = FSDirectory.open(new File("D:/itcast/lucene/index"));
// 创建索引读取对象IndexReader
IndexReader reader = DirectoryReader.open(directory);
// 创建索引搜索对象
IndexSearcher searcher = new IndexSearcher(reader);
// 使用索引搜索对象,执行搜索,返回结果集TopDocs
// 第一个参数:搜索对象,第二个参数:返回的数据条数,指定查询结果最顶部的n条数据返回
TopDocs topDocs = searcher.search(query, 10);
System.out.println("查询到的数据总条数是:" + topDocs.totalHits);
// 获取查询结果集
ScoreDoc[] docs = topDocs.scoreDocs;
// 解析结果集
for (ScoreDoc scoreDoc : docs) {
// 获取文档id
int docID = scoreDoc.doc;
Document doc = searcher.doc(docID);
System.out.println("======================================");
System.out.println("docID:" + docID);
System.out.println("bookId:" + doc.get("id"));
System.out.println("name:" + doc.get("name"));
System.out.println("price:" + doc.get("price"));
System.out.println("pic:" + doc.get("pic"));
// System.out.println("desc:" + doc.get("desc"));
}
// 3. 释放资源
reader.close();
}
NumericRangeQuery
NumericRangeQuery,指定数字范围查询.
@Test
public void testSearchNumericRangeQuery() throws Exception {
// 创建NumericRangeQuery搜索对象,数字范围查询.
// 五个参数分别是:域名、最小值、最大值、是否包含最小值,是否包含最大值
Query query = NumericRangeQuery.newFloatRange("price", 54f, 56f, false, true);
doSearch(query);
}
BooleanQuery
BooleanQuery,布尔查询,实现组合条件查询。
@Test
public void testSearchBooleanQuery() throws Exception {
// 创建TermQuery搜索对象
Query query1 = new TermQuery(new Term("name", "lucene"));
// 创建NumericRangeQuery搜索对象,数字范围查询.
// 四个参数分别是:域名、最小值、最大值、是否包含最小值,是否包含最大值
Query query2 = NumericRangeQuery.newFloatRange("price", 54f, 66f, false, true);
// 创建BooleanQuery搜索对象,组合查询条件
BooleanQuery boolQuery = new BooleanQuery();
// 组合条件,
// 第一个参数,查询条件,第二个参数,组合方式
boolQuery.add(query1, Occur.MUST_NOT);
boolQuery.add(query2, Occur.MUST);
doSearch(boolQuery);
}
组合关系代表的意思如下:
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表示"或"的关系,即"并集"。
通过QueryParser搜索
通过QueryParser也可以创建Query,QueryParser提供一个Parse方法,此方法可以直接根据查询语法来查询。可以通过打印Query对象的方式,查看生成的查询语句。
-
查询语法
1、基础的查询语法,关键词查询:
域名+":"+搜索的关键字
例如:name:java
-
范围查询
域名+":"+[最小值 TO 最大值]
例如:size:[1 TO 1000]
注意:QueryParser不支持对数字范围的搜索,它支持字符串范围。数字范围搜索建议使用NumericRangeQuery。
- 组合条件查询
+(加号) |
|
Occur.SHOULD 查询条件可选,相当于OR |
空(不用符号) |
Occur.MUST_NOT 查询条件不能满足,相当于NOT非 |
-(减号) |
QueryParser
@Test
public void testSearchIndex() throws Exception {
// 创建分词器
Analyzer analyzer = new StandardAnalyzer();
// 1. 创建Query搜索对象
// 创建搜索解析器,第一个参数:默认Field域,第二个参数:分词器
QueryParser queryParser = new QueryParser("desc", analyzer);
// 创建搜索对象
// Query query = queryParser.parse("desc:java学习");
Query query = queryParser.parse("desc:java AND lucene");
// 打印生成的搜索语句
System.out.println(query);
// 执行搜索
doSearch(query);
}
MultiFieldQueryParser
通过MultiFieldQueryParse对多个域查询。
@Test
public void testSearchMultiFieldQueryParser() throws Exception {
// 创建分词器
Analyzer analyzer = new IKAnalyzer();
// 1. 创建MultiFieldQueryParser搜索对象
String[] fields = { "name", "desc" };
MultiFieldQueryParser multiFieldQueryParser = new MultiFieldQueryParser(fields, analyzer);
// 创建搜索对象
Query query = multiFieldQueryParser.parse("lucene");
// 打印生成的搜索语句
System.out.println(query);
// 执行搜索
doSearch(query);
}
生成的查询语句:
name:lucene desc:lucene
TopDocs
Lucene搜索结果可通过TopDocs遍历,TopDocs类提供了少量的属性,如下:
方法或属性 |
说明 |
totalHits |
匹配搜索条件的总记录数 |
scoreDocs |
顶部匹配记录 |
注意:
Search方法需要指定匹配记录数量n:indexSearcher.search(query, n)
TopDocs.totalHits:是匹配索引库中所有记录的数量
TopDocs.scoreDocs:匹配相关度高的前边记录数组,scoreDocs的长度小于等于search方法指定的参数n
-
替换分词器
使用IKAnalyzer分词器
导入IKAnalyzer2012FF_u1.jar
把分词对象替换为
-
自定义停用词和扩展词
拷贝文件到资源文件夹
注意:不要用window自带的记事本保存扩展词文件和停用词文件,那样的话,格式中是含有bom的。
IKAnalyzer.cfg.xml配置文件
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd">
<properties>
<comment>IK Analyzer 扩展配置</comment>
<!--用户可以在这里配置自己的扩展字典 -->
<entry key="ext_dict">ext.dic;</entry>
<!--用户可以在这里配置自己的扩展停止词字典-->
<entry key="ext_stopwords">stopword.dic;</entry>
</properties>