一、 Lucene概述
1.1 Lucene是什么
Lucene是apache软件基金会4 jakarta项目组的一个子项目,是一个开放源代码的全文检索引擎工具包,但它不是一个完整的全文检索引擎,而是一个全文检索引擎的架构,提供了完整的查询引擎和索引引擎,部分文本分析引擎(英文与德文两种西方语言)。
Lucene的目的是为软件开发人员提供一个简单易用的工具包,以方便的在目标系统中实现全文检索的功能,或者是以此为基础建立起完整的全文检索引擎。
目前已经有很多应用程序的搜索功能是基于 Lucene 的,比如 Eclipse 的帮助系统的搜索功能。Lucene 能够为文本类型的数据建立索引,所以你只要能把你要索引的数据格式转化的文本的,Lucene 就能对你的文档进行索引和搜索。比如你要对一些 HTML 文档,PDF 文档进行索引的话你就首先需要把 HTML 文档和 PDF 文档转化成文本格式的,然后将转化后的内容交给 Lucene 进行索引,然后把创建好的索引文件保存到磁盘或者内存中,最后根据用户输入的查询条件在索引文件上进行查询。不指定要索引的文档的格式也使 Lucene 能够几乎适用于所有的搜索应用程序。
简单概括:
- Lucene是一套用于全文检索和搜寻的开源程式库,由Apache软件基金会支 持和提供;
- Lucene提供了一个简单却强大的应用程式接口,能够做全文索引和搜寻, 在Java开发环境里Lucene是一个成熟的免费开放源代码工具;
- Lucene并不是现成的搜索引擎产品,但可以用来制作搜索引擎产品;
官网地址: http://lucene.apache.org/
1.2 Lucene的工作流程
索引:绿色部分表示索引。在搜索前需要先对原始内容进行索引,构建索引库。
索引过程:确定原始内容 > 获得文档 > 创建文档 > 分析文档 > 索引文档。
搜索:红色部分代表搜索。搜索即从索引库中搜索内容。
搜索过程:创建查询 > 执行搜索 > 从索引库搜索 > 渲染搜索结果。
二、Lucene环境安装
开发环境:
JDK: 1.8.0_144
IDE: eclipse Luna
数据库: MySQL5.6.38
2.1 创建Maven工程,引入lucene相关坐标。
<dependencies>
<dependency>
<groupId>org.apache.lucene</groupId>
<artifactId>lucene-core</artifactId>
<version>8.2.0</version>
</dependency>
<dependency>
<groupId>org.apache.lucene</groupId>
<artifactId>lucene-queryparser</artifactId>
<version>8.2.0</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.22</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.16.18</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
</dependency>
</dependencies>
2.2 创建数据库
# 创建数据库
create database solr;
# 选定数据库
use solr;
# 创建图书表
CREATE TABLE `book` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(255) NOT NULL,
`price` int(11) NOT NULL DEFAULT '0',
`pic` varchar(255) DEFAULT NULL,
`remark` varchar(2000) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8;
# 初始化数据
INSERT INTO `book` VALUES ('1', 'jvm原理', '49', '/uploads/1.jpg', '深入浅出介绍jvm的实现原理');
INSERT INTO `book` VALUES ('2', '程序员基本功', '149', '/uploads/2.jpg', '学习jvm基本功');
INSERT INTO `book` VALUES ('3', 'lucene搜索引擎', '79', '/uploads/3.jpg', '深入浅出蜘蛛 爬虫 lutch 数据分析');
INSERT INTO `book` VALUES ('4', '搜索引擎内部剖析', '59', '/uploads/4.jpg', '呵呵');
2.3 创建Beans
@Data
public class Book {
// 图书ID
private Integer id;
// 图书名称
private String name;
// 图书价格
private Float price;
// 图书图片
private String pic;
// 图书描述
private String remark;
}
2.4 创建Dao接口
public interface BookDao {
List<Book> queryBookList();
}
2.5 创建Dao实现类
public class BookDaoImpl implements BookDao {
@Override
public List<Book> queryBookList() {
// 数据库链接
Connection connection = null;
// 预编译statement
PreparedStatement preparedStatement = null;
// 结果集
ResultSet resultSet = null;
// 图书列表
List<Book> list = new ArrayList<Book>();
try {
// 加载数据库驱动
Class.forName("com.mysql.jdbc.Driver");
// 连接数据库
connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/solr", "root", "root");
// SQL语句
String sql = "SELECT * FROM book";
// 创建preparedStatement
preparedStatement = connection.prepareStatement(sql);
// 获取结果集
resultSet = preparedStatement.executeQuery();
// 结果集解析
while (resultSet.next()) {
Book book = new Book();
book.setId(resultSet.getInt("id"));
book.setName(resultSet.getString("name"));
book.setPrice(resultSet.getFloat("price"));
book.setPic(resultSet.getString("pic"));
book.setRemark(resultSet.getString("remark"));
list.add(book);
}
} catch (Exception e) {
e.printStackTrace();
}
return list;
}
}
三、实现索引
第一步:创建Document对象;
第二步:创建解析器对象;
第三步:创建Directory对象,该对象声明了索引库的位置;
第四步:创建IndexWriter对象,该对象负责把Document写入索引库文件中;
第五步:释放资源;
@Test
public void testCreateIndex() throws Exception {
// 采集数据
BookDao bookDao = new BookDaoImpl();
List<Book> bookList = bookDao.queryBookList();
// 创建Document对象,每一个Document对象对应数据库表的一行记录
List<Document> documents = new ArrayList<>();
for (Book book : bookList) {
Document document = new Document();
// 添加Field域
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("remark", book.getRemark().toString(), Store.YES));
// 把Document放到list中
documents.add(document);
}
// 创建Analyzer分词器,分析文档,对文档进行分词
Analyzer analyzer = new StandardAnalyzer();
// 构建创建文件目录
Directory directory = FSDirectory.open(Paths.get("d:/lucene/index"));
// 创建IndexWriteConfig对象,写入索引需要的配置
IndexWriterConfig config = new IndexWriterConfig(analyzer);
// 创建IndexWriter写入对象
IndexWriter indexWriter = new IndexWriter(directory, config);
// 把Document写入到索引库
for (Document doc : documents) {
indexWriter.addDocument(doc);
}
// 8.释放资源
indexWriter.close();
}
运行程序,可以在d:/lucene/index目录下看到创建的索引文件。
四、搜索索引
4.1 搜索分词
和索引过程的分词一样,这里要对用户输入的关键字进行分词,一般情况索引和搜索使用的分词器一致。比如搜索“java培训”,分词处理后变成了“java”和“培训”两个词。这时候与java和培训相关的内容都会被查询出来。
4.2 使用QueryParser对象实现搜索
第一步:读取索引文件;
第二步:创建搜索器;
第三步:创建解析器对象;
第四步:创建查询解析器对象,该对象会使用刚创建的解析器对象对指定查询内容进行解析;
第五步:执行搜索;
第六步:处理搜索结果;
第七部:关闭资源;
@Test
public void testSearchIndex2() throws IOException, ParseException {
// 创建Directory流对象,声明索引库位置
Directory directory = FSDirectory.open(Paths.get("d:/lucene/index"));
// 创建索引读取对象IndexReader
IndexReader reader = DirectoryReader.open(directory);
// 创建索引搜索对象
IndexSearcher searcher = new IndexSearcher(reader);
// 创建分词器
Analyzer analyzer = new StandardAnalyzer();
// 创建搜索解析器,第一个参数:默认Field域,第二个参数:分词器
QueryParser queryParser = new QueryParser("name", analyzer);
Query query = queryParser.parse("name:jvm");
// 使用索引搜索对象,执行搜索,返回结果集TopDocs
TopDocs topDocs = searcher.search(query, 10);
System.out.println("查询到的数据总条数是:" + topDocs.totalHits);
// 获取查询结果集
ScoreDoc[] docs = topDocs.scoreDocs;
// 6. 解析结果集
for (ScoreDoc scoreDoc : docs) {
System.out.println("=============================");
// 获取文档ID
int docID = scoreDoc.doc;
// 根据文档ID获取文档
Document doc = searcher.doc(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("remark:" + doc.get("remark"));
}
// 7. 释放资源
reader.close();
}
上面search方法返回一个TopDocs对象,该对象封装了查询结果信息。 该对象包含两个常用属性:
totalHits:匹配搜索条件的总记录数;
scoreDocs:返回匹配度最高的记录;
比如:indexSearcher.search(query, 10),那么最多匹配10条记录。
查询结果如下图所示:
五、分词器
5.1 什么是分词
分词就是把采集到的数据存储到document对象的Field域中,分词就是将Document中Field的value值切分成一个一个的词。
5.2 分词器的执行过程
分词(Tokenizer)的主要过程就是先分词后过滤(TokenFilter)。过滤的工作主要包括:去除标点符号过滤、去除停用词过滤(的、是、a、an、the等)、大写转小写、词形还原(复数形式转成单数形参、过去式转成现在式)等等。
分词器的执行过程如下图所示:
从图上可以看到,分词完成后会经过一系列的过滤,最后才得到一个个的Token。我们可以把token理解为一个个的单词。
例如:
原文:Lucene is a full-text search enginie.
分词后:lucene、is、a、full、text、search、engine,一共7个词。
5.3 中文分词器
虽然lucene自带了中文分词器,但是许多人使用后都觉得lucene自带的中文分词功能比较弱,分词的效果不太令人满意。所以建议使用第三方的分词器。例如:IkAnalyzer。
下面列出一些常见的分词器:
分词器 | 描述 |
---|---|
StandardAnalyzer | 按照中文一个字一个字地进行分词,比如:中国,效果:中、国 |
CJKAnalyzer | 按两个字进行切分。如:“我是中国人”,效果:“我是”、“是中”、“中国”“国人” |
SmartChineseAnalyzer | 相对来说对中文支持较好,但扩展性差,扩展词库,禁用词库等不好处理 |
IK-analyzer | 对中文支持较好,实现了简单的分词 ,歧义排除算法,而且版本还不断更新,推荐使用 |
5.4 使用IkAnalyzer
第一步:引入坐标。
<dependency>
<groupId>com.jianggujin</groupId>
<artifactId>IKAnalyzer-lucene</artifactId>
<version>8.0.0</version>
</dependency>
第二步:创建分词器;
//Analyzer analyzer = new StandardAnalyzer();
Analyzer analyzer = new IKAnalyzer();
六、Field详解
Lucene中的Field相当于数据库表中的字段,一个文档可以包括多个Field,Document只是Field的一个承载体,Field值即为要索引的内容,也是要搜索的内容。
- 是否分词
如果是,则进行分词处理。比如:商品名称、商品描述等,这些内容用户要输入关键字搜索,由于搜索的内容格式大、内容多需要分词后将语汇单元建立索引。
如果否,则不进行分词处理。比如:商品id、订单号、身份证号等。
- 是否索引
如果是,则将分词后的词或整个Field值进行索引,存储到索引域。索引的目的是为了搜索。比如:商品名称、商品描述。订单号、身份证号不用分词但也要索引,这些将来都要作为查询条件。
如果否,则不进行索引。比如:图片路径、文件路径等,不用作为查询条件的不用索引。
- 是否存储
如果是,则将Field值存储在文档域中。存储在文档域中的Field才可以从Document中获取。比如:商品名称、订单号,凡是将来要从Document中获取的Field都要存储。
如果否,则不存储Field的值。比如:商品描述,内容较大不用存储。
6.1 常见Field类型
Field | 数据类型 | 是否分词 | 是否索引 | 是否存储 | 说明 |
---|---|---|---|---|---|
StringField | 字符串 | N | Y | Y或N | 这个Field用来构建一个字符串Field,但是不会进行分词,会将整个串存储在索引中,比如(订单号,身份证号等),是否存储在文档中用Store.YES或Store.NO决定 |
NumericDocValuesField | long | Y | Y | Y或N | 这个Field用来构建一个Long数字型Field,进行分词和索引,比如(价格),是否存储在文档中用Store.YES或Store.NO决定 |
StoredField | 支持多种类型 | N | N | Y | 这个Field用来构建不同类型Field不分析,不索引,但要Field存储在文档中 |
TextField | 字符串或流 | Y | Y | Y或N | 如果是一个Reader, lucene猜测内容比较多,会采用Unstored的策略 |
6.2 Field改造
// 图书ID,不分词,不索引,储存
document.add(new StoredField("id", book.getId().toString()));
// 图书名称,分词,索引,储存
document.add(new TextField("name", book.getName().toString(), Store.YES));
// 图书价格,分词,索引,不储存
document.add(new DoublePoint("price", book.getPrice()));
// 图片地址,不分词,不索引,储存
document.add(new StoredField("pic", book.getPic().toString()));
// 图书描述,分词,索引,不储存
document.add(new TextField("remark", book.getRemark().toString(), Store.NO));
七、索引维护
7.1 删除索引
IndexWriter提供了两个方法实现索引的删除:
1)deleteDocuments:根据条件删除索引;
2)deleteAll:删除所有索引;
@Test
public void testIndexDelete() throws Exception {
// 创建Directory流对象
Directory directory = FSDirectory.open(Paths.get("d:/lucene/index"));
// 创建分词器
Analyzer analyzer = new IKAnalyzer();
// 创建IndexWriterConfig对象,该对象封装了IndexWriter的配置信息
IndexWriterConfig config = new IndexWriterConfig(analyzer);
// 创建IndexWriter对象
IndexWriter indexWriter = new IndexWriter(directory, config);
// 根据Term删除索引库,name:lucene
indexWriter.deleteDocuments(new Term("name", "lucene"));
// 释放资源
indexWriter.close();
}
删除所有索引:
@Test
public void testIndexAllDelete() throws Exception {
// 创建Directory流对象
Directory directory = FSDirectory.open(Paths.get("d:/lucene/index"));
IndexWriterConfig config = new IndexWriterConfig();
// 创建写入对象
IndexWriter indexWriter = new IndexWriter(directory, config);
// 根据Term删除索引库,name:lucene
indexWriter.deleteAll();
// 释放资源
indexWriter.close();
}
7.2 修改索引
@Test
public void testIndexUpdate() throws Exception {
// 创建分词器
Analyzer analyzer = new IKAnalyzer();
// 创建Directory流对象
Directory directory = FSDirectory.open(Paths.get("d:/lucene/index"));
// 配置分词器
IndexWriterConfig config = new IndexWriterConfig(analyzer);
// 创建写入对象
IndexWriter indexWriter = new IndexWriter(directory, config);
// 创建Document
Document document = new Document();
// 设置要更新的字段
document.add(new StoredField("id", "3"));
document.add(new TextField("name", "全文搜索", Store.YES));
// 执行更新
indexWriter.updateDocument(new Term("name", "lucene"), document);
// 释放资源
indexWriter.close();
}
实际上,updateDocument方法会把所有符合条件的Document先删除,再执行添加操作。
八、搜索
8.1 通过Query子类实现搜索
8.1.1 TermQuery
词项查询,TermQuery不使用分析器,搜索关键词进行精确匹配Field域中的词,比如订单号、分类ID号等。
Query query = new TermQuery(new Term("name", "lucene"))
81.2 TermRangeQuery
多条件查询,通过一组Term来查找索引文档。
Query query2 = new TermRangeQuery("price", new BytesRef("50"), new BytesRef("99"), false, true);
- 参数说明:
参数一:要查询的Field;
参数二:Field的下限,不支持数值类型;
参数三:Field的上限,不支持数值类型;
参数四:是否包含下限值,如果true代表包含,否则不包含;
参数五:是否包含上限值,如果true代表包含,否则不包含;
8.1.3 BooleanQuery
布尔查询,实现组合条件查询。例如:
Query query1 = new TermQuery(new Term("name", "lucene"));
Query query2 = new TermRangeQuery("price", new BytesRef("50"), new BytesRef("99"), false, true);
// 组合多个条件
Query query = new BooleanQuery.Builder()
.add(query1, Occur.SHOULD)
.add(query2, Occur.SHOULD)
.build();
- 组合关系有:
MUST | MUST_NOT | SHOULD | |
---|---|---|---|
MUST | 交集 | 减集 | MUST |
MUST_NOT | 减集 | 没意义 | 减集 |
SHOULD | MUST | 减集 | 并集 |
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表示“或”的关系,即“并集”
8.2 通过QueryParser实现搜索
8.2.1 查询语法
基本查询:Field + “:” + 搜索的关键字
例如:name:java
范围查询:Field + “:” + [最小值 TO 最大值]
例如:size:[1 TO 1000]
组合条件查询:
+加号:相当于AND
空(没有字符):相当于OR
-
减号:相当于NOT
8.2.2 QueryParser
// 创建分词解析器
Analyzer analyzer = new IKAnalyzer();
// 创建搜索解析器,第一个参数:默认Field域,第二个参数:分词器
QueryParser queryParser = new QueryParser("remark", analyzer);
// 创建搜索对象
Query query = queryParser.parse("remark:java AND lucene");
// 打印生成的搜索语句
System.out.println(query);
8.2.3 MultiFieldQueryParser
String[] fields = {"name", "remark"};
MultiFieldQueryParser multiFieldQueryParser = new MultiFieldQueryParser(fields, analyzer);
Query query = multiFieldQueryParser.parse("jvm");
上面MultiFieldQueryParser解析器生成的查询条件:
name:jvm remark:jvm
九、相关度排序
9.1 相关度描述
相关度排序是查询结果按照与查询关键字的相关性进行排序,相关度越高的记录就越容易被查询到。比如搜索“Lucene”关键字,与该关键字最相关的文章应该排在前边。
Lucene对查询关键字和索引文档的相关度进行打分,得分高的就排在前边。
问题:如何打分呢?
Lucene是在用户进行检索时实时根据搜索的关键字计算出来的。计算步骤分为:
1)计算词(Term)的权重;
2)根据词的权重值,计算出文档相关度得分;
影响词权重的两个重要因素:
1)Term Frequency(tf):此Term在此文档中出现的次数。次数越多,tf的值就越大,此词对于该文档就越重要;
2)Document Frequency(df):指有多少文档包含此Term。df的值越大,代表此词对于该文档越不重要;
9.2 设置boost
boost是一个加权值(默认加权值为1.0f),它可以影响权重的计算。在索引时对某个文档中的field设置加权值,设置越高,在搜索时匹配到这个文档就可能排在前边。
例如:给id为1的文档设置加权值。
TextField descField = new TextField("desc", book.getDesc().toString(), Store.NO);
if (book.getId() == 1) {
descField.setBoost(100f);
}
document.add(descField);