天天看点

lucene&solr入门(一)一、 Lucene概述二、Lucene环境安装五、分词器六、Field详解八、搜索九、相关度排序

一、 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/

lucene&solr入门(一)一、 Lucene概述二、Lucene环境安装五、分词器六、Field详解八、搜索九、相关度排序

1.2 Lucene的工作流程

lucene&solr入门(一)一、 Lucene概述二、Lucene环境安装五、分词器六、Field详解八、搜索九、相关度排序

索引:绿色部分表示索引。在搜索前需要先对原始内容进行索引,构建索引库。

索引过程:确定原始内容 > 获得文档 > 创建文档 > 分析文档 > 索引文档。

搜索:红色部分代表搜索。搜索即从索引库中搜索内容。

搜索过程:创建查询 > 执行搜索 > 从索引库搜索 > 渲染搜索结果。

二、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目录下看到创建的索引文件。

lucene&amp;solr入门(一)一、 Lucene概述二、Lucene环境安装五、分词器六、Field详解八、搜索九、相关度排序

四、搜索索引

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条记录。

查询结果如下图所示:

lucene&amp;solr入门(一)一、 Lucene概述二、Lucene环境安装五、分词器六、Field详解八、搜索九、相关度排序

五、分词器

5.1 什么是分词

分词就是把采集到的数据存储到document对象的Field域中,分词就是将Document中Field的value值切分成一个一个的词。

5.2 分词器的执行过程

分词(Tokenizer)的主要过程就是先分词后过滤(TokenFilter)。过滤的工作主要包括:去除标点符号过滤、去除停用词过滤(的、是、a、an、the等)、大写转小写、词形还原(复数形式转成单数形参、过去式转成现在式)等等。

分词器的执行过程如下图所示:

lucene&amp;solr入门(一)一、 Lucene概述二、Lucene环境安装五、分词器六、Field详解八、搜索九、相关度排序

从图上可以看到,分词完成后会经过一系列的过滤,最后才得到一个个的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号等。

81.2 TermRangeQuery

多条件查询,通过一组Term来查找索引文档。

  • 参数说明:

参数一:要查询的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);