1. Lucene介绍
1.1 搜索流程
原来的方式实现搜索功能,我们的搜索流程如下图:
上图就是原始搜索引擎技术,如果用户比较少而且数据库的数据量比较小,那么这种方式实现搜索功能在企业中是比较常见的。
但是数据量过多时,数据库的压力就会变得很大,查询速度会变得非常慢。我们需要使用更好的解决方案来分担数据库的压力。
现在的方案(使用Lucene),如下图
为了解决数据库压力和速度的问题,我们的数据库换成了索引库,使用Lucene的API的来操作服务器上的索引库。
使用索引库专门实现查询功能,而且完全和数据库进行了隔离。
1.2 数据查询方法
1.2.1 顺序扫描法
所谓顺序扫描,例如要找内容包含一个字符串的文件,就是一个文档一个文档的看,对于每一个文档,从头看到尾,如果此文档包含此字符串,则此文档为我们要找的文件,接着看下一个文件,直到扫描完所有的文件。这种方法是顺序扫描方法,数据量大就搜索慢。
如利用windows的搜索也可以搜索文件内容,只是速度会相当的慢。
1.2.2 倒排索引
先举一个栗子:
例如我们使用新华字典查询汉字,新华字典有偏旁部首的目录(索引),我们查字首先查这个目录,找到这个目录中对应的偏旁部首,就可以通过这个目录中的偏旁部首找到这个字所在的位置(文档)。
倒排索引:
将数据加入到索引库(你可以理解成另外一个数据库)时,会先提取数据中的词汇(分词),将词汇加入到文档域,文档域中记录了词汇以及词汇在哪条数据记录中出现过的数据下标。用户在搜索数据时,先将用户搜索的数据进行词汇提取,然后把对应词汇拿到索引域中进行匹配查找,查找后会找到对应的下标ID,再根据对应下标ID到文档域中找真实数据。
1.3 Lucene与搜索引擎的区别
全文检索系统是按照全文检索理论建立起来的用于提供全文检索服务的软件系统,包括建立索引、处理查询返回结果集、增加索引、优化索引结构等功能。例如:百度搜索、eclipse帮助搜索、淘宝网商品搜索等。
搜索引擎是全文检索技术最主要的一个应用,例如百度。搜索引擎起源于传统的信息全文检索理论,即计算机程序通过扫描每一篇文章中的每一个词,建立以词为单位的倒排文件,检索程序根据检索词在每一篇文章中出现的频率和每一个检索词在一篇文章中出现的概率,对包含这些检索词的文章进行排序,最后输出排序的结果。全文检索技术是搜索引擎的核心支撑技术。
Lucene和搜索引擎不同,Lucene是一套用java或其它语言写的全文检索的工具包,为应用程序提供了很多个api接口去调用,可以简单理解为是一套实现全文检索的类库,搜索引擎是一个全文检索系统,它是一个单独运行的软件系统。
2. Lucene案例
实现这么一个案例,通过Java代码调用Lucene API实现对索引库的增删改查,索引库数据来源于数据库,所以增加操作需要先从数据库将数据查询出来,再调用Lucene API将数据加入到索引库中。
2.1 Lucene实现全文检索思路
全文检索的流程分为两大部分:索引流程、搜索流程。
索引流程:即采集数据=>构建文档对象=>分析文档(分词)=>创建索引。
搜索流程:即用户通过搜索界面输入=>创建查询=>执行搜索,搜索器从索引库搜=>渲染搜索结果。
2.2 案例创建
创建Book对象
public class Book {
// 图书ID
private Integer id;
// 图书名称
private String name;
// 图书价格
private Float price;
// 图书图片
private String pic;
// 图书描述
private String desc;
//get...set...
}
创建索引
public class TestIndex {
@Test
public void testCreateIndex() throws Exception{
//1.采集数据:(jdbc采集数据通过BookDao调用方法得到结果集)
BookDao bookDao = new BookDaoImpl();
//queryBookList为已封装好的方法,返回所有数据库中的book对象
List<Book> books = bookDao.queryBookList();
//2.遍历book结果集,组装Document数据列表
List<Document> docs = new ArrayList<>();
Document doc = null;
for (Book book : books) {
//3.构建Field域,说白了就是将要存储的数据字段需要用到new TextField对象三个参数的构造方法,
// book中有多个字段,所以创建多个Field对象。
Field id = new TextField("id", book.getId().toString(), Field.Store.YES);
Field name = new TextField("name", book.getName(), Field.Store.YES);
Field price = new TextField("price", book.getPrice().toString(), Field.Store.YES);
Field pic = new TextField("pic", book.getPic(), Field.Store.YES);
Field desc = new TextField("desc", book.getDesc(), Field.Store.YES);
//4.将Field域所有对象,添加到文档对象中。调用Document.add
doc = new Document();
doc.add(id);
doc.add(name);
doc.add(price);
doc.add(pic);
doc.add(desc);
//记录文档对象列表
docs.add(doc);
}
//5.创建一个标准分词器(Analyzer与StandardAnalyzer),对文档中的Field域进行分词
Analyzer analyzer = new StandardAnalyzer();
//6.指定索引储存目录,使用FSDirectory.open()方法。
Directory directory = FSDirectory.open(new File("C:\java\index").toPath());
//7.创建IndexWriterConfig对象,直接new,用于接下来创建IndexWriter对象
IndexWriterConfig indexWriterConfig = new IndexWriterConfig(analyzer);
//8.创建IndexWriter对象,直接new
IndexWriter indexWriter = new IndexWriter(directory,indexWriterConfig);
//9.添加文档对象到索引库输出对象中,使用IndexWriter.addDocuments方法
indexWriter.addDocuments(docs);
//10.释放资源IndexWriter.close();
indexWriter.close();
}
}
索引搜索实现
@Test
public void testQuery() throws Exception{
//1.创建一个Directory对象,FSDirectory.open指定索引库存放的位置
Directory directory = FSDirectory.open(new File("C:\java\index").toPath());
//2.创建一个IndexReader对象,DirectoryReader.open需要指定Directory对象
IndexReader indexReader = DirectoryReader.open(directory);
//3.创建一个Indexsearcher对象,直接new,需要指定IndexReader对象
IndexSearcher indexSearcher = new IndexSearcher(indexReader);
//4.创建一个标准分词器(Analyzer与StandardAnalyzer),对文档中的Field域进行分词
Analyzer analyzer = new StandardAnalyzer();
//5.创建一个QueryParser对象, new QueryParser (域名称,分词器)
QueryParser queryParser = new QueryParser("desc",analyzer);
//6.调用QueryParser.parser(搜索的内容),得到Query
Query query = queryParser.parse("java");
//7.执行查询,IndexSearcher.search(Query对象,查询排名靠多少名前的记录数),得到结果TopDocs
TopDocs topDocs = indexSearcher.search(query, 10);
//8.遍历查询结果并输出,TopDocs.totalHits总记录数,topDocs.scoreDocs数据列表,
// 通过scoreDoc.doc得到唯一id,再通过IndexSearcher.doc(id),
// 得到文档对象Document再Document.get(域名称)得到结果
System.out.println("总记录数为:" + topDocs.totalHits);
for (ScoreDoc scoreDoc : topDocs.scoreDocs) {
int docId = scoreDoc.doc;
Document doc = indexSearcher.doc(docId);
System.out.println(doc.get("id") + "->" + doc.get("name") + "," + doc.get("price"));
}
//9.关闭IndexReader对象
indexReader.close();
}
query对象的构建
QueryParser queryParser = new QueryParser("desc",analyzer);
//"Java"可更改为"name:java" 不指定情况下使用上面的默认域desc
Query query = queryParser.parse("java");
//查询语法为: desc:java
这个方法在大多数情况下都适用,可将下面的查询语法直接放入parse中进行查询
query子类查询
Query query = new MatchAllDocsQuery();
//查询语法为: *:*
//查询最小单元Term查询
Query query = new TermQuery(new Term("desc","java lucene"));
//查询语法为 desc:java lucene 但此时查询不到数据 term是最小查询单元 java lucene作为一个整体不会被拆分
//数值范围查询
Query query = NumericRangeQuery.newFloatRange("price", 5F, 56F, true, true);
//查询语法为: price:[5.0 TO 56.0] true/false 表示是否包含最大/最小值
//多域多条件搜索
BooleanQuery query = new BooleanQuery();
//追加一个条件
query.add(new TermQuery(new Term("desc","java")), BooleanClause.Occur.MUST);
query.add(NumericRangeQuery.newFloatRange("price", 5F, 56F, true, true), BooleanClause.Occur.MUST);
//查询语法为: +desc:java +price:[5.0 TO 56.0] +/- 对应MUST/MUST_NOT SHOULD在语法前不加符号
//query.add(/*这里放入query对象,可用上面的各种方法进行构造*/, BooleanClause.Occur./*MUST,MUST_NOT,SHOULD 等*/);
当追加两个以上的MUST_NOT条件时,会被认为是没有意义的查询,查询将不会进行
搜索方法
IndexSearcher搜索方法如下:
2.3 使用Luke查看索引
3. 分词器
中文分词器IKAnalyzer
IKAnalyzer继承Lucene的Analyzer抽象类,使用IKAnalyzer和Lucene自带的分析器方法一样,将Analyzer测试代码改为IKAnalyzer测试中文分词效果。
如果使用中文分词器ik-analyzer,就需要在索引和搜索程序中使用一致的分词器:IK-analyzer。
在pom.xml中引入依赖:
<!--IK分词器-->
<dependency>
<groupId>org.wltea.ik-analyzer</groupId>
<artifactId>ik-analyzer</artifactId>
<version>5.3.1.RELEASE</version>
</dependency>
<dependency>
<groupId>org.wltea.ik-analyzer</groupId>
<artifactId>ik-analyzer-extra</artifactId>
<version>5.3.1.RELEASE</version>
</dependency>
导入配置文件,分别将ext.dic,IKAnalyzer.cfg.xml,stopword.dic文件拷贝到工程的resources目录
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>
IK分词器安装好了,前面的案例想使用IK分词器只需要将StandardAnalyzer改成IKAnalyzer即可。
改造前:
Analyzer analyzer = new StandardAnalyzer();
改造后:
Analyzer analyzer = new IKAnalyzer();
四. Field 域
4.1 Field属性
Field是文档中的域,包括Field名和Field值两部分,一个文档可以包括多个Field,Document只是Field的一个承载体,Field值即为要索引的内容,也是要搜索的内容。
Field中三个非常重要的属性:
是否分词(tokenized)
是,将field的内容分成一个一个单词。分词的目的:分词目的为了索引
例如:商品的名称。
否,不分词,将内容作为一个整体存储。
例如:商品ID 身份证号,图片路径
是否索引(indexed)
是,将field的值建立索引,索引的目的:索引的目的为了搜索。
例如:商品的名称
否,不建立索引
例如:图片路径、文件路径等
是否存储(stored),存不存取决于查询结果展示不展示
是,存储field的值。存储的目的:(为了展示在页面)
例如:商品名称,图片路径
否,不存储field的值。
例如:商品介绍。如果需要展示,根据ID从数据库查询展示在详情页面。
4.2 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) FloatField(FieldName, FieldValue,Store.YES) | Long类型Float类型等数字类型 | 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的策略 |
lucene对数字型的值只要有搜索需求的都要分词和索引,因为lucene对数字型的内容要特殊分词处理
不存储是不在lucene的索引域中记录,节省lucene的索引文件空间。
4.3 代码修改
// id 不分词 不索引 要存储
//是否分词:不用分词,因为不会根据商品id来搜索商品
//是否索引:不索引,因为不需要根据图书ID进行搜索
//是否存储:要存储,因为查询结果页面需要使用id这个值。
Field id = new StringField("id", book.getId().toString(), Field.Store.YES);
// name 要分词 要索引 要存储 因为要根据图书名称的关键词搜索
Field name = new TextField("name", book.getName(), Field.Store.YES);
// price 要分词 要索引 要存储,数字比较特殊
Field price = new FloatField("price", book.getPrice(), Field.Store.YES);
// pic 不分词 不索引 要存储
Field pic = new StoredField("pic", book.getPic());
// description 要分词 要索引 不存储,原因详情数据量太大
Field desc = new TextField("desc", book.getDesc(), Field.Store.NO);
5. 索引维护
5.1 删除索引
删除指定索引
根据Term项删除索引,满足条件的将全部删除。
@Test
public void testDelele() throws Exception{
//5.创建一个标准分词器(Analyzer与StandardAnalyzer),对文档中的Field域进行分词
Analyzer analyzer = new IKAnalyzer();
//6.指定索引储存目录,使用FSDirectory.open()方法。
Directory directory = FSDirectory.open(new File("C:\java\index").toPath());
//7.创建IndexWriterConfig对象,直接new,用于接下来创建IndexWriter对象
IndexWriterConfig indexWriterConfig = new IndexWriterConfig(analyzer);
//8.创建IndexWriter对象,直接new
IndexWriter indexWriter = new IndexWriter(directory,indexWriterConfig);
//删除索引
indexWriter.deleteDocuments(new Term("name", "java"));
//释放资源
indexWriter.close();
}
删除全部索引(慎用)
@Test
public void testDelele() throws Exception{
//5.创建一个标准分词器(Analyzer与StandardAnalyzer),对文档中的Field域进行分词
Analyzer analyzer = new IKAnalyzer();
//6.指定索引储存目录,使用FSDirectory.open()方法。
Directory directory = FSDirectory.open(new File("C:\java\index").toPath());
//7.创建IndexWriterConfig对象,直接new,用于接下来创建IndexWriter对象
IndexWriterConfig indexWriterConfig = new IndexWriterConfig(analyzer);
//8.创建IndexWriter对象,直接new
IndexWriter indexWriter = new IndexWriter(directory,indexWriterConfig);
//删除索引
//indexWriter.deleteDocuments(new Term("name", "java"));
indexWriter.deleteAll();
indexWriter.close();
}
5.2 更新索引
更新索引是先删除再添加,建议对更新需求采用此方法并且要保证对已存在的索引执行更新,可以先查询出来,确定更新记录存在执行更新操作。
如果更新索引的目标文档对象不存在,则执行添加。
@Test
public void testUpdate() throws Exception{
//5.创建一个标准分词器(Analyzer与StandardAnalyzer),对文档中的Field域进行分词
Analyzer analyzer = new IKAnalyzer();
//6.指定索引储存目录,使用FSDirectory.open()方法。
Directory directory = FSDirectory.open(new File("C:\java\index").toPath());
//7.创建IndexWriterConfig对象,直接new,用于接下来创建IndexWriter对象
IndexWriterConfig indexWriterConfig = new IndexWriterConfig(analyzer);
//8.创建IndexWriter对象,直接new
IndexWriter indexWriter = new IndexWriter(directory,indexWriterConfig);
Document doc = new Document();
// id 不分词 要索引 要存储
Field id = new StringField("id","1", Field.Store.YES);
// name 要分词 要索引 要存储
Field name = new TextField("name","这是修改过的值", Field.Store.YES);
doc.add(id);
doc.add(name);
//执行更新,会把所有符合条件的Document删除,再新增。
indexWriter.updateDocument(new Term("name","java"),doc);
indexWriter.close();
}