https://www.bilibili.com/video/BV1eJ411q7nw/?spm_id_from=333.337.search-card.all.click
文章目录
全文检索Lucene
课程计划
-
lucene入门
什么是lucene
Lucene的作用
使用场景
优点和缺点
-
lucene应用
索引流程
搜索流程
fifield域的使用
索引库维护
分词器
高级搜索实战案例
-
Lucene高级
Lucene底层存储结构
词典排序算法
Lucene优化
Lucene使用的一些注意事项
1.搜索技术理论基础
1.1. 为什么要学习Lucene
原来的方式实现搜索功能,我们的搜索流程如下图:
上图就是原始搜索引擎技术,如果用户比较少而且数据库的数据量比较小,那么这种方式实现搜索功能在企业中是比较常见的。
现在的方案(使用Lucene),如下图
为了解决数据库压力和速度的问题,我们的数据库就变成了索引库,我们使用Lucene的API的来操作服务器上的索引库。这样完全和数据库进行了隔离。
1.2. 数据查询方法
1.2.1. 顺序扫描法
算法描述:
所谓顺序扫描,例如要找内容包含一个字符串的文件,就是一个文档一个文档的看,对于每一个文档,从头看到尾,如果此文档包含此字符串,则此文档为我们要找的文件,接着看下一个文件,直到扫描完所有的文件。
优点:
查询准确率高
缺点:
查询速度会随着查询数据量的增大, 越来越慢
使用场景:
-
数据库中的like关键字模糊查询
-
文本编辑器的Ctrl + F 查询功能
1.2.2. 倒排索引
先举一个栗子:
例如我们使用新华字典查询汉字,新华字典有偏旁部首的目录(索引),我们查字首先查这个目录,找到这个目录中对应的偏旁部首,就可以通过这个目录中的偏旁部首找到这个字所在的位置(文档)。
Lucene会对文档建立倒排索引
1、 提取资源中关键信息, 建立索引 (目录)
2、 搜索时,根据关键字(目录),找到资源的位置
算法描述:
查询前会先将查询的内容提取出来组成文档(正文), 对文档进行切分词组成索引(目录), 索引和文档有关联关系, 查询的时候先查询索引, 通过索引找文档的这个过程叫做全文检索。
为什么倒排索引比顺序扫描快**?**
理解 : 因为索引可以去掉重复的词, 汉语常用的字和词大概等于, 字典加词典, 常用的英文在牛津词典也有收录.如果用计算机的速度查询, 字典+词典+牛津词典这些内容是非常快的. 但是用这些字典, 词典组成的文章却是千千万万不计其数. 索引的大小最多也就是字典+词典. 所以通过查询索引, 再通过索引和文档的关联关系找到文档速度比较快. 顺序扫描法则是直接去逐个查询那些不计其数的文章就算是计算的速度也会很慢.
优点:
查询准确率高
查询速度快, 并且不会因为查询内容量的增加, 而使查询速度逐渐变慢
缺点:
索引文件会占用额外的磁盘空间, 也就是占用磁盘量会增大。
使用场景:
海量数据查询
1.3. 全文检索技术应用场景
应用场景 :
1、 站内搜索 (baidu贴吧、论坛、 京东、 taobao)
2、 垂直领域的搜索 (818工作网)
3、 专业搜索引擎公司 (google、baidu)
2.Lucene介绍
2.1. 什么是全文检索
计算机索引程序通过扫描文章中的每一个词,对每一个词建立一个索引,指明该词在文章中出现的次数和位置,当用户查询时,检索程序就根据事先建立的索引进行查找,并将查找的结果反馈给用户的检索方式
2.2.什么是Lucene
他是Lucene、Nutch 、Hadoop等项目的发起人Doug Cutting
Lucene是apache软件基金会4 jakarta项目组的一个子项目,是一个开放源代码的全文检索引擎工具包,但它不是一个完整的全文检索引擎,而是一个全文检索引擎的架构,提供了完整的查询引擎和索引引擎,部分文本分析引擎(英文与德文两种西方语言)。
Lucene的目的是为软件开发人员提供一个简单易用的工具包,以方便的在目标系统中实现全文检索的功能,或者是以此为基础建立起完整的全文检索引擎。
目前已经有很多应用程序的搜索功能是基于 Lucene 的,比如 Eclipse 的帮助系统的搜索功能。Lucene能够为文本类型的数据建立索引,所以你只要能把你要索引的数据格式转化的文本的,Lucene 就能对你的文档进行索引和搜索。比如你要对一些 HTML 文档,PDF 文档进行索引的话你就首先需要把HTML 文档和 PDF 文档转化成文本格式的,然后将转化后的内容交给 Lucene 进行索引,然后把创建好的索引文件保存到磁盘或者内存中,最后根据用户输入的查询条件在索引文件上进行查询。不指定要索引的文档的格式也使 Lucene 能够几乎适用于所有的搜索应用程序。
-
Lucene是一套用于全文检索和搜寻的开源程式库,由Apache软件基金会支持和提供
-
Lucene提供了一个简单却强大的应用程式接口,能够做全文索引和搜寻, 在Java开发环境里Lucene是一个成熟的免费开放源代码工具
-
Lucene并不是现成的搜索引擎产品,但可以用来制作搜索引擎产品
2.3. Lucene官网
官网: http://lucene.apache.org/
3. Lucene全文检索的流程
3.1.索引和搜索流程图
1、绿色表示索引过程,对要搜索的原始内容进行索引构建一个索引库,索引过程包括:
确定原始内容即要搜索的内容
-
获得文档
-
创建文档
-
分析文档
-
索引文档
用户通过搜索界面
-
创建查询
-
执行搜索,从索引库搜索
-
渲染搜索结果
3.2. 索引流程
对文档索引的过程,将用户要搜索的文档内容进行索引,索引存储在索引库(index)中。
3.2.1. 原始内容
原始内容是指要索引和搜索的内容。
原始内容包括互联网上的网页、数据库中的数据、磁盘上的文件等。
3.2.2. 获得文档(采集数据)
从互联网上、数据库、文件系统中等获取需要搜索的原始信息,这个过程就是信息采集,采集数据的目的是为了对原始内容进行索引。
采集数据分类:
1、对于互联网上网页,可以使用工具将网页抓取到本地生成html文件。
2、数据库中的数据,可以直接连接数据库读取表中的数据。
3、文件系统中的某个文件,可以通过I/O操作读取文件的内容。
在Internet上采集信息的软件通常称为爬虫或蜘蛛,也称为网络机器人,爬虫访问互联网上的每一个网页,将获取到的网页内容存储起来。
3.2.3. 创建文档
获取原始内容的目的是为了索引,在索引前需要将原始内容创建成文档(Document),文档中包括一个一个的域(Field),域中存储内容。
这里我们可以将磁盘上的一个文件当成一个document,Document中包括一些Field,如下图:
注意:每个Document可以有多个Field,不同的Document可以有不同的Field,同一个Document可以
有相同的Field(域名和域值都相同)
3.2.4. 分析文档
将原始内容创建为包含域(Field)的文档(document),需要再对域中的内容进行分析,分析成为一个一个的单词。
比如下边的文档经过分析如下:
原文档内容:
vivo X23 8GB+128GB 幻夜蓝 全网通4G手机
华为 HUAWEI 麦芒7 6G+64G 亮黑色 全网通4G手机
分析后得到的词:
vivo, x23, 8GB, 128GB, 幻夜, 幻夜蓝, 全网, 全网通, 网通, 4G, 手机, 华为, HUAWEI, 麦芒7。。。。
3.2.5. 索引文档
对所有文档分析得出的语汇单元进行索引,索引的目的是为了搜索,最终要实现只搜索被索引的语汇单元从而找到Document(文档)。
创建索引是对语汇单元索引,通过词语找文档,这种索引的结构叫倒排索引结构。
倒排索引结构是根据内容(词汇)找文档,如下图:
倒排索引结构也叫反向索引结构,包括索引和文档两部分,索引即词汇表,它的规模较小,而文档集合较大。
3.2.6 Lucene底层存储结构
3.3. 搜索流程
搜索就是用户输入关键字,从索引中进行搜索的过程。根据关键字搜索索引,根据索引找到对应的文档,从而找到要搜索的内容。
3.3.1. 用户
就是使用搜索的角色,用户可以是自然人,也可以是远程调用的程序。
3.3.2. 用户搜索界面
全文检索系统提供用户搜索的界面供用户提交搜索的关键字,搜索完成展示搜索结果。如下图:
Lucene不提供制作用户搜索界面的功能,需要根据自己的需求开发搜索界面。
3.3.3. 创建查询
用户输入查询关键字执行搜索之前需要先构建一个查询对象,查询对象中可以指定查询要查询关键字、要搜索的Field文档域等,查询对象会生成具体的查询语法,比如:
name:手机 : 表示要搜索name这个Field域中,内容为“手机”的文档。
name:华为AND 手机 : 表示要搜索即包括关键字“华为” 并且也包括“手机”的文档。
3.3.4. 执行搜索
搜索索引过程:
1.根据查询语法在倒排索引词典表中分别找出对应搜索词的索引,从而找到索引所链接的文档链表。
例如搜索语法为 “name:华为 AND 手机 ” 表示搜索出的文档中既要包括"华为"也要包括"手机"。
2、由于是AND,所以要对包含 华为 和 手机 词语的链表进行交集,得到文档链表应该包括每一个搜索
词语
3、获取文档中的Field域数据。
以一个友好的界面将查询结果展示给用户,用户根据搜索结果找自己想要的信息,为了帮助用户很快找
到自己的结果,提供了很多展示的效果,比如搜索结果中将关键字高亮显示,百度提供的快照等。
4. Lucene入门
4.1. Lucene准备
Lucene可以在官网上下载。课程已经准备好了Lucene的文件,我们使用的是7.7.2版本,文件位置如下图:
解压后的效果:
使用这三个文件的jar包,就可以实现lucene功能
4.2. 开发环境
JDK: 1.8 (Lucene7以上,必须使用JDK1.8及以上版本)
数据库: MySQL
数据库脚本位置如下图:
导入到MySQL效果如下图:
4.3. 创建Java工程
创建maven工程不依赖骨架, 测试即可,效果如下:
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>cn.itheima</groupId>
<artifactId>luceneDemo</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<skipTests>true</skipTests>
</properties>
<!--springboot -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.4.RELEASE</version>
</parent>
<dependencies>
<!--####################lucene包####################-->
<!--lucene核心包-->
<dependency>
<groupId>org.apache.lucene</groupId>
<artifactId>lucene-core</artifactId>
<version>7.7.2</version>
</dependency>
<!--分词器包-->
<dependency>
<groupId>org.apache.lucene</groupId>
<artifactId>lucene-analyzers-common</artifactId>
<version>7.7.2</version>
</dependency>
<!--做查询用的包-->
<dependency>
<groupId>org.apache.lucene</groupId>
<artifactId>lucene-queryparser</artifactId>
<version>7.7.2</version>
</dependency>
<!-- IK中文分词器 -->
<dependency>
<groupId>org.wltea.ik-analyzer</groupId>
<artifactId>ik-analyzer</artifactId>
<version>8.1.0</version>
</dependency>
<!--####################工具包####################-->
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.6</version>
</dependency>
<!-- Json转换工具 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.51</version>
</dependency>
<!--####################环境包####################-->
<!-- 测试 -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
<!-- mysql数据库驱动 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.48</version>
</dependency>
<!--web起步依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- 引入thymeleaf -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
</dependencies>
</project>
4.4. 索引流程
4.4.1. 数据采集
在电商网站中,全文检索的数据源在数据库中,需要通过jdbc访问数据库中 sku 表的内容。
4.4.1.1. 创建pojo
package cn.itheima.pojo;
/**
*
*/
public class Sku {
//商品主键id
private String id;
//商品名称
private String name;
//价格
private Integer price;
//库存数量
private Integer num;
//图片
private String image;
//分类名称
private String categoryName;
//品牌名称
private String brandName;
//规格
private String spec;
//销量
private Integer saleNum;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getPrice() {
return price;
}
public void setPrice(Integer price) {
this.price = price;
}
public Integer getNum() {
return num;
}
public void setNum(Integer num) {
this.num = num;
}
public String getImage() {
return image;
}
public void setImage(String image) {
this.image = image;
}
public String getCategoryName() {
return categoryName;
}
public void setCategoryName(String categoryName) {
this.categoryName = categoryName;
}
public String getBrandName() {
return brandName;
}
public void setBrandName(String brandName) {
this.brandName = brandName;
}
public String getSpec() {
return spec;
}
public void setSpec(String spec) {
this.spec = spec;
}
public Integer getSaleNum() {
return saleNum;
}
public void setSaleNum(Integer saleNum) {
this.saleNum = saleNum;
}
}
4.4.1.2. 创建dao
package cn.itheima.dao;
import cn.itheima.pojo.Sku;
import java.util.List;
/**
*
*/
public interface SkuDao {
/**
* 查询所有的Sku数据
* @return
**/
public List<Sku> querySkuList();
}
package cn.itheima.dao;
import cn.itheima.pojo.Sku;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.util.ArrayList;
import java.util.List;
/**
*
*/
public class SkuDaoImpl implements SkuDao {
public List<Sku> querySkuList() {
// 数据库链接
Connection connection = null;
// 预编译statement
PreparedStatement preparedStatement = null;
// 结果集
ResultSet resultSet = null;
// 商品列表
List<Sku> list = new ArrayList<Sku>();
try {
// 加载数据库驱动
Class.forName("com.mysql.jdbc.Driver");
// 连接数据库
connection = DriverManager.getConnection("jdbc:mysql://rm-bp1pulu04813wr8ryzo.mysql.rds.aliyuncs.com:3306/wlplat" +
"?useUnicode=true&characterEncoding=utf-8&useSSL=false&autoReconnect=true&failOverReadOnly=false", "iplat62", "iplat62");
// SQL语句
String sql = "SELECT * FROM tb_sku";
// 创建preparedStatement
preparedStatement = connection.prepareStatement(sql);
// 获取结果集
resultSet = preparedStatement.executeQuery();
// 结果集解析
while (resultSet.next()) {
Sku sku = new Sku();
sku.setId(resultSet.getString("id"));
sku.setName(resultSet.getString("name"));
sku.setSpec(resultSet.getString("spec"));
sku.setBrandName(resultSet.getString("brand_name"));
sku.setCategoryName(resultSet.getString("category_name"));
sku.setImage(resultSet.getString("image"));
sku.setNum(resultSet.getInt("num"));
sku.setPrice(resultSet.getInt("price"));
sku.setSaleNum(resultSet.getInt("sale_num"));
list.add(sku);
}
} catch (Exception e) {
e.printStackTrace();
}
return list;
}
}
4.4.1.3. 测试类创建索引文件
package cn.itheima.test;
import cn.itheima.dao.SkuDao;
import cn.itheima.dao.SkuDaoImpl;
import cn.itheima.pojo.Sku;
import org.apache.lucene.analysis.Analyzer;
import org.apache.lucene.analysis.standard.StandardAnalyzer;
import org.apache.lucene.document.*;
import org.apache.lucene.index.IndexWriter;
import org.apache.lucene.index.IndexWriterConfig;
import org.apache.lucene.index.Term;
import org.apache.lucene.store.Directory;
import org.apache.lucene.store.FSDirectory;
import org.apache.lucene.store.MMapDirectory;
import org.junit.Test;
import org.wltea.analyzer.lucene.IKAnalyzer;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;
/**
* 索引库维护
*/
public class TestIndexManager {
/**
* 创建索引库
* 1. 采集数据
* 2. 创建文档对象
* 3. 创建分词器
* 4. 创建Directory目录对象, 目录对象表示索引库的位置
* 5. 创建IndexWriterConfig对象, 这个对象中指定切分词使用的分词器
* 6. 创建IndexWriter输出流对象, 指定输出的位置和使用的config初始化对象
* 7. 写入文档到索引库
* 8. 释放资源
*/
@Test
public void createIndexTest() throws Exception {
//1. 采集数据
SkuDao skuDao = new SkuDaoImpl();
List<Sku> skuList = skuDao.querySkuList();
//文档集合
List<Document> docList = new ArrayList<>();
for (Sku sku : skuList) {
//2. 创建文档对象
Document document = new Document();
//创建域对象并且放入文档对象中
/**
* 是否分词: 否, 因为主键分词后无意义
* 是否索引: 是, 如果根据id主键查询, 就必须索引
* 是否存储: 是, 因为主键id比较特殊, 可以确定唯一的一条数据, 在业务上一般有重要所用, 所以存储
* 存储后, 才可以获取到id具体的内容
*/
document.add(new StringField("id", sku.getId(), Field.Store.YES));
/**
* 是否分词: 是, 因为名称字段需要查询, 并且分词后有意义所以需要分词
* 是否索引: 是, 因为需要根据名称字段查询
* 是否存储: 是, 因为页面需要展示商品名称, 所以需要存储
*/
document.add(new TextField("name", sku.getName(), Field.Store.YES));
/**
* 是否分词: 是(因为lucene底层算法规定, 如果根据价格范围查询, 必须分词)
* 是否索引: 是, 需要根据价格进行范围查询, 所以必须索引
* 是否存储: 是, 因为页面需要展示价格
*/
document.add(new IntPoint("price", sku.getPrice()));
document.add(new StoredField("price", sku.getPrice()));
/**
* 是否分词: 否, 因为不查询, 所以不索引, 因为不索引所以不分词
* 是否索引: 否, 因为不需要根据图片地址路径查询
* 是否存储: 是, 因为页面需要展示商品图片
*/
document.add(new StoredField("image", sku.getImage()));
/**
* 是否分词: 否, 因为分类是专有名词, 是一个整体, 所以不分词
* 是否索引: 是, 因为需要根据分类查询
* 是否存储: 是, 因为页面需要展示分类
*/
document.add(new StringField("categoryName", sku.getCategoryName(), Field.Store.YES));
/**
* 是否分词: 否, 因为品牌是专有名词, 是一个整体, 所以不分词
* 是否索引: 是, 因为需要根据品牌进行查询
* 是否存储: 是, 因为页面需要展示品牌
*/
document.add(new StringField("brandName", sku.getBrandName(), Field.Store.YES));
//将文档对象放入到文档集合中
docList.add(document);
}
//3. 创建分词器, StandardAnalyzer标准分词器, 对英文分词效果好, 对中文是单字分词, 也就是一个字就认为是一个词.
Analyzer analyzer = new IKAnalyzer();
//4. 创建Directory目录对象, 目录对象表示索引库的位置
// Directory dir = FSDirectory.open(Paths.get("E:\\dir"));
Directory dir = FSDirectory.open(Paths.get("/Users/miyufeng/Downloads/lucene_dev"));
//5. 创建IndexWriterConfig对象, 这个对象中指定切分词使用的分词器
IndexWriterConfig config = new IndexWriterConfig(analyzer);
//6. 创建IndexWriter输出流对象, 指定输出的位置和使用的config初始化对象
IndexWriter indexWriter = new IndexWriter(dir, config);
//7. 写入文档到索引库
for (Document doc : docList) {
indexWriter.addDocument(doc);
}
//8. 释放资源
indexWriter.close();
}
}
4.4.1.4. 使用luke 查看索引文件
下图是索引域的展示效果:
下图是文档域展示效果
Lucene可以通过query对象输入查询语句。同数据库的sql一样,lucene也有固定的查询语法:
最基本的有比如:AND, OR, NOT 等(必须大写)
举个栗子:
用户想找一个 name 域中包括 手 或 机 关键字的文档。
它对应的查询语句:**name:**手 **OR name:**机
如下图是使用luke搜索的例子:
和索引过程的分词一样,这里要对用户输入的关键字进行分词,一般情况索引和搜索使用的分词器一
致。
比如:输入搜索关键字“java学习”,分词后为java和学习两个词,与java和学习有关的内容都搜索出来
了,如下:
4.5. 代码实现查找索引文件
-
创建Query搜索对象
-
创建Directory流对象,声明索引库位置
-
创建索引读取对象IndexReader
-
创建索引搜索对象IndexSearcher
-
使用索引搜索对象,执行搜索,返回结果集TopDocs
-
解析结果集
-
释放资源
IndexSearcher搜索方法如下:
方法 | 说明 |
---|---|
indexSearcher.search(query, n) | 根据Query搜索,返回评分最高的n条记录 |
indexSearcher.search(query, fifilter, n) | 根据Query搜索,添加过滤策略,返回评分最高的n条记录 |
indexSearcher.search(query, n, sort) | 根据Query搜索,添加排序策略,返回评分最高的n条记录 |
indexSearcher.search(booleanQuery,fifilter, n, sort) | 根据Query搜索,添加过滤策略,添加排序策略,返回评分最高的n条记录 |
/**
* 测试搜索过程
*/
public class TestSearch {
/*
1. 创建分词器(对搜索的关键词进行分词使用)
2. 创建查询对象,
3. 设置搜索关键词
4. 创建Directory目录对象, 指定索引库的位置
5. 创建输入流对象
6. 创建搜索对象
7. 搜索, 并返回结果
*/
@Test
public void testIndexSearch() throws Exception {
//1. 创建分词器(对搜索的关键词进行分词使用)
//注意: 分词器要和创建索引的时候使用的分词器一模一样
// Analyzer analyzer = new StandardAnalyzer();
Analyzer analyzer = new IKAnalyzer();
//2. 创建查询对象,
//第一个参数: 默认查询域, 如果查询的关键字中带搜索的域名, 则从指定域中查询, 如果不带域名则从, 默认搜索域中查询
//第二个参数: 使用的分词器
QueryParser queryParser = new QueryParser("name", analyzer);
//3. 设置搜索关键词
//华 OR 为 手 机
Query query = queryParser.parse("华为手机");
//4. 创建Directory目录对象, 指定索引库的位置
// Directory dir = FSDirectory.open(Paths.get("E:\\dir"));
Directory dir = FSDirectory.open(Paths.get("/Users/miyufeng/Downloads/lucene_dev"));
//5. 创建输入流对象
IndexReader indexReader = DirectoryReader.open(dir);
//6. 创建搜索对象
IndexSearcher indexSearcher = new IndexSearcher(indexReader);
//7. 搜索, 并返回结果
//第二个参数: 是返回多少条数据用于展示, 分页使用
TopDocs topDocs = indexSearcher.search(query, 10);
//获取查询到的结果集的总数, 打印
System.out.println("=======count=======" + topDocs.totalHits);
//8. 获取结果集
ScoreDoc[] scoreDocs = topDocs.scoreDocs;
//9. 遍历结果集
if (scoreDocs != null) {
for (ScoreDoc scoreDoc : scoreDocs) {
//获取查询到的文档唯一标识, 文档id, 这个id是lucene在创建文档的时候自动分配的
int docID = scoreDoc.doc;
//通过文档id, 读取文档
Document doc = indexSearcher.doc(docID);
System.out.println("==================================================");
//通过域名, 从文档中获取域值
System.out.println("===id==" + doc.get("id"));
System.out.println("===name==" + doc.get("name"));
System.out.println("===price==" + doc.get("price"));
System.out.println("===image==" + doc.get("image"));
System.out.println("===brandName==" + doc.get("brandName"));
System.out.println("===categoryName==" + doc.get("categoryName"));
}
}
//10. 关闭流
}
5. Field域类型
5.1. Field属性
Field是文档中的域,包括Field名和Field值两部分,一个文档可以包括多个Field,Document只是Field的一个承载体,Field值即为要索引的内容,也是要搜索的内容。
- 是否分词(tokenized)
是:作分词处理,即将Field值进行分词,分词的目的是为了索引。
比如:商品名称、商品描述等,这些内容用户要输入关键字搜索,由于搜索的内容格式大、内容多需要
分词后将语汇单元建立索引
否:不作分词处理
比如:商品id、订单号、身份证号等
- 是否索引(indexed)
是:进行索引。将Field分词后的词或整个Field值进行索引,存储到索引域,索引的目的是为了搜索。
比如:商品名称、商品描述分析后进行索引,订单号、身份证号不用分词但也要索引,这些将来都要作
为查询条件。
否:不索引。
比如:图片路径、文件路径等,不用作为查询条件的不用索引。
- 是否存储(stored)
是:将Field值存储在文档域中,存储在文档域中的Field才可以从Document中获取。
比如:商品名称、订单号,凡是将来要从Document中获取的Field都要存储。
否:不存储Field值
比如:商品描述,内容较大不用存储。如果要向用户展示商品描述可以从系统的关系数据库中获取。
- 5.2. Field常用类型
下边列出了开发中常用 的Filed类型,注意Field的属性,根据需求选择:
Field类 | 数据类型 | Analyzed是否分词 | Indexed是否索引 | Stored是否存储 | 说明 |
---|---|---|---|---|---|
StringField(FieldName, FieldValue,Store.YES)) | 字符串 | N | Y | Y或N | 这个Field用来构建一个字符串Field,但是不会进行分词,会将整个串存储在索引中,比如(订单号,身份证号等)是否存储在文档中用Store.YES或Store.NO决定 |
FloatPoint(FieldName, FieldValue) | Float型 | Y | Y | N | 这个Field用来构建一个Float数字型Field,进行分词和索引,不存储, 比如(价格) 存储在文档中 |
DoublePoint(FieldName,FieldValue) | Double型 | Y | Y | N | 这个Field用来构建一个Double数字型Field,进行分词和索引,不存储 |
LongPoint(FieldName, FieldValue) | Long型 | Y | Y | N | 这个Field用来构建一个Long数字型Field,进行分词和索引,不存储 |
IntPoint(FieldName, FieldValue) | Integer 型 | Y | Y | N | 这个Field用来构建一个Integer数字型Field,进行分词和索引,不存储 |
StoredField(FieldName, FieldValue) | 重载方法,支持多种类型 | N | N | Y | 这个Field用来构建不同类型Field不分析,不索引,但要Field存储在文档中 |
TextField(FieldName, FieldValue,Store.NO) 或 TextField(FieldName,reader) | 字符串或 流 | Y | Y | Y或N | 如果是一个Reader, lucene猜测内容比较多,会采用Unstored的策略. |
NumericDocValuesField(FieldName,FieldValue) | 数值 | _ | _ | _ | 配合其他域排序使用 |
6. 索引维护
6.1. 需求
管理人员通过电商系统更改图书信息,这时更新的是关系数据库,如果使用lucene搜索图书信息,需要
在数据库表book信息变化时及时更新lucene索引库。
6.2. 添加索引
调用 indexWriter.addDocument(doc)添加索引。
参考入门程序的创建索引。
6.3. 修改索引
更新索引是先删除再添加,建议对更新需求采用此方法并且要保证对已存在的索引执行更新,可以先查询出来,确定更新记录存在执行更新操作。
如果更新索引的目标文档对象不存在,则执行添加。
代码
/**
* 索引库修改操作
* @throws Exception
*/
@Test
public void updateIndexTest() throws Exception {
//需要变更成的内容
Document document = new Document();
document.add(new StringField("id", "100000003145", Field.Store.YES));
document.add(new TextField("name", "xxxx", Field.Store.YES));
document.add(new IntPoint("price", 123));
document.add(new StoredField("price", 123));
document.add(new StoredField("image", "xxxx.jpg"));
document.add(new StringField("categoryName", "手机", Field.Store.YES));
document.add(new StringField("brandName", "华为", Field.Store.YES));
//3. 创建分词器, StandardAnalyzer标准分词器, 对英文分词效果好, 对中文是单字分词, 也就是一个字就认为是一个词.
Analyzer analyzer = new StandardAnalyzer();
//4. 创建Directory目录对象, 目录对象表示索引库的位置
Directory dir = FSDirectory.open(Paths.get("E:\\dir"));
//5. 创建IndexWriterConfig对象, 这个对象中指定切分词使用的分词器
IndexWriterConfig config = new IndexWriterConfig(analyzer);
//6. 创建IndexWriter输出流对象, 指定输出的位置和使用的config初始化对象
IndexWriter indexWriter = new IndexWriter(dir, config);
//修改, 第一个参数: 修改条件, 第二个参数: 修改成的内容
indexWriter.updateDocument(new Term("id", "100000003145"), document);
//8. 释放资源
indexWriter.close();
}
6.4. 删除索引
6.4.1. 删除指定索引
根据Term项删除索引,满足条件的将全部删除。
/**
* 测试根据条件删除
* @throws Exception
*/
@Test
public void deleteIndexTest() throws Exception {
//3. 创建分词器, StandardAnalyzer标准分词器, 对英文分词效果好, 对中文是单字分词, 也就是一个字就认为是一个词.
Analyzer analyzer = new IKAnalyzer();
//4. 创建Directory目录对象, 目录对象表示索引库的位置
Directory dir = FSDirectory.open(Paths.get("/Users/miyufeng/Downloads/lucene_dev"));
//5. 创建IndexWriterConfig对象, 这个对象中指定切分词使用的分词器
IndexWriterConfig config = new IndexWriterConfig(analyzer);
//6. 创建IndexWriter输出流对象, 指定输出的位置和使用的config初始化对象
IndexWriter indexWriter = new IndexWriter(dir, config);
//测试根据条件删除
indexWriter.deleteDocuments(new Term("id", "100000003145"));
//测试删除所有内容
// indexWriter.deleteAll();
//8. 释放资源
indexWriter.close();
}
6.4.2. 删除全部索引(慎用)
将索引目录的索引信息全部删除,直接彻底删除,无法恢复。
建议参照关系数据库基于主键删除方式,所以在创建索引时需要创建一个主键Field,删除时根据此主键Field删除。
索引删除后将放在Lucene的回收站中,Lucene3.X版本可以恢复删除的文档,3.X之后无法恢复。
代码:
/**
* 测试根据条件删除
* @throws Exception
*/
@Test
public void deleteIndexTest() throws Exception {
//3. 创建分词器, StandardAnalyzer标准分词器, 对英文分词效果好, 对中文是单字分词, 也就是一个字就认为是一个词.
Analyzer analyzer = new IKAnalyzer();
//4. 创建Directory目录对象, 目录对象表示索引库的位置
Directory dir = FSDirectory.open(Paths.get("/Users/miyufeng/Downloads/lucene_dev"));
//5. 创建IndexWriterConfig对象, 这个对象中指定切分词使用的分词器
IndexWriterConfig config = new IndexWriterConfig(analyzer);
//6. 创建IndexWriter输出流对象, 指定输出的位置和使用的config初始化对象
IndexWriter indexWriter = new IndexWriter(dir, config);
//测试根据条件删除
// indexWriter.deleteDocuments(new Term("id", "100000003145"));
//测试删除所有内容
indexWriter.deleteAll();
//8. 释放资源
indexWriter.close();
}
索引域数据清空
7. 分词器
7.1. 分词理解
在对Document中的内容进行索引之前,需要使用分词器进行分词 ,分词的目的是为了搜索。分词的主要过程就是先分词后过滤。
- 过滤:包括去除标点符号过滤、去除停用词过滤(的、是、a、an、the等)、大写转小写、词的形还原(复数形式转成单数形参、过去式转成现在式。。。)等。
什么是停用词?停用词是为节省存储空间和提高搜索效率,搜索引擎在索引页面或处理搜索请求时会自动忽略某些字或词,这些字或词即被称为Stop Words(停用词)。比如语气助词、副词、介词、连接词等,通常自身并无明确的意义,只有将其放入一个完整的句子中才有一定作用,如常见的“的”、“在”、“是”、“啊”等。
对于分词来说,不同的语言,分词规则不同。Lucene作为一个工具包提供不同国家的分词器
7.2. Analyzer使用时机
输入关键字进行搜索,当需要让该关键字与文档域内容所包含的词进行匹配时需要对文档域内容进行分析,需要经过Analyzer分析器处理生成语汇单元(Token)。分析器分析的对象是文档中的Field域。当Field的属性tokenized(是否分词)为true时会对Field值进行分析,如下图:
对于一些Field可以不用分析:
1、不作为查询条件的内容,比如文件路径
2、不是匹配内容中的词而匹配Field的整体内容,比如订单号、身份证号等。
7.2.2. 搜索时使用Analyzer
对搜索关键字进行分析和索引分析一样,使用Analyzer对搜索关键字进行分析、分词处理,使用分析后每个词语进行搜索。比如:搜索关键字:spring web ,经过分析器进行分词,得出:spring web拿词去索引词典表查找 ,找到索引链接到Document,解析Document内容。
对于匹配整体Field域的查询可以在搜索时不分析,比如根据订单号、身份证号查询等。
注意:搜索使用的分析器要和索引使用的分析器一致。
以下是Lucene中自带的分词器
7.3 原生分词器
7.3.1. StandardAnalyzer
特点 :
Lucene提供的标准分词器, 可以对用英文进行分词,对中文是单字分词, 也就是一个字就认为是一个词.
如下是org.apache.lucene.analysis.standard.standardAnalyzer的部分源码:
/repository_lucene/org/apache/lucene/lucene-core/7.7.2/lucene-core-7.7.2.jar!/org/apache/lucene/analysis/standard/StandardAnalyzer.class:50
protected TokenStreamComponents createComponents(String fieldName) {
final StandardTokenizer src = new StandardTokenizer();
src.setMaxTokenLength(this.maxTokenLength);
TokenStream tok = new LowerCaseFilter(src);
TokenStream tok = new StopFilter(tok, this.stopwords);
return new TokenStreamComponents(src, tok) {
protected void setReader(Reader reader) {
src.setMaxTokenLength(StandardAnalyzer.this.maxTokenLength);
super.setReader(reader);
}
};
}
Tokenizer就是分词器,负责将reader转换为语汇单元即进行分词处理,Lucene提供了很多的分词器,也可以使用第三方的分词,比如IKAnalyzer一个中文分词器。
TokenFilter是分词过滤器,负责对语汇单元进行过滤,TokenFilter可以是一个过滤器链儿,Lucene提供了很多的分词器过滤器,比如大小写转换、去除停用词等。
如下图是语汇单元的生成过程:
从一个Reader字符流开始,创建一个基于Reader的Tokenizer分词器,经过三个TokenFilter生成语汇单元Token。
比如下边的文档经过分析器分析如下:
原文档内容:
分析后得到的多个语汇单元:
7.3.2. WhitespaceAnalyzer
特点 :
仅仅是去掉了空格,没有其他任何操作,不支持中文。
/**
* 去掉空格分词器, 不支持中文
* @throws Exception
*/
@Test
public void TestWhitespaceAnalyzer() throws Exception{
// 1. 创建分词器,分析文档,对文档进行分词
Analyzer analyzer = new WhitespaceAnalyzer();
// 2. 创建Directory对象,声明索引库的位置
Directory directory = FSDirectory.open(Paths.get("E:\\dir"));
// 3. 创建IndexWriteConfig对象,写入索引需要的配置
IndexWriterConfig config = new IndexWriterConfig(analyzer);
// 4.创建IndexWriter写入对象
IndexWriter indexWriter = new IndexWriter(directory, config);
// 5.写入到索引库,通过IndexWriter添加文档对象document
Document doc = new Document();
doc.add(new TextField("name", "vivo X23 8GB+128GB 幻夜蓝", Field.Store.YES));
indexWriter.addDocument(doc);
// 6.释放资源
indexWriter.close();
}
7.3.3. SimpleAnalyzer
特点 :
将除了字母以外的符号全部去除,并且将所有字母变为小写,需要注意的是这个分词器同样把数字也去除了,同样不支持中文。
/**
* 简单分词器: 不支持中文, 将除了字母之外的所有符号全部取出, 所有大写字母转换成小写字母, 对于数字也会去除
* @throws Exception
*/
@Test
public void TestSimpleAnalyzer() throws Exception{
// 1. 创建分词器,分析文档,对文档进行分词
Analyzer analyzer = new SimpleAnalyzer();
// 2. 创建Directory对象,声明索引库的位置
Directory directory = FSDirectory.open(Paths.get("E:\\dir"));
// 3. 创建IndexWriteConfig对象,写入索引需要的配置
IndexWriterConfig config = new IndexWriterConfig(analyzer);
// 4.创建IndexWriter写入对象
IndexWriter indexWriter = new IndexWriter(directory, config);
// 5.写入到索引库,通过IndexWriter添加文档对象document
Document doc = new Document();
doc.add(new TextField("name", "vivo,X23。 8GB+128GB; 幻夜蓝", Field.Store.YES));
indexWriter.addDocument(doc);
// 6.释放资源
indexWriter.close();
}
7.3.4. CJKAnalyzer
特点 :
这个支持中日韩文字,前三个字母也就是这三个国家的缩写。对中文是二分法分词, 去掉空格,去掉标点符号。个人感觉对中文支持依旧很烂。
/**
* 中日韩分词器: 使用二分法分词, 去掉空格, 去掉标点符号, 所有大写字母转换成小写字母
* @throws Exception
*/
@Test
public void TestCJKAnalyzer() throws Exception{
// 1. 创建分词器,分析文档,对文档进行分词
Analyzer analyzer = new CJKAnalyzer();
// 2. 创建Directory对象,声明索引库的位置
Directory directory = FSDirectory.open(Paths.get("E:\\dir"));
// 3. 创建IndexWriteConfig对象,写入索引需要的配置
IndexWriterConfig config = new IndexWriterConfig(analyzer);
// 4.创建IndexWriter写入对象
IndexWriter indexWriter = new IndexWriter(directory, config);
// 5.写入到索引库,通过IndexWriter添加文档对象document
Document doc = new Document();
doc.add(new TextField("name", "vivo,X23。 8GB+128GB; 幻夜蓝", Field.Store.YES));
indexWriter.addDocument(doc);
// 6.释放资源
indexWriter.close();
}
7.3.5. SmartChineseAnalyzer
特点 :
对中文支持也不是很好,扩展性差,扩展词库,禁用词库和同义词库等不好处理。
7.4. 第三方中文分词器
7.4.1. 什么是中文分词器
学过英文的都知道,英文是以单词为单位的,单词与单词之间以空格或者逗号句号隔开。所以对于英文,我们可以简单以空格判断某个字符串是否为一个单词,比如I love China,love 和 China很容易被程序区分开来。
而中文则以字为单位,字又组成词,字和词再组成句子。中文“我爱中国”就不一样了,电脑不知道“中 国”是一个词语还是“爱中”是一个词语。
把中文的句子切分成有意义的词,就是中文分词,也称切词。我爱中国,分词的结果是:我、爱、中国。
7.4.2. 第三方中文分词器简介
-
paoding: 庖丁解牛最新版在 https://code.google.com/p/paoding/ 中最多支持Lucene 3.0,且最新提交的代码在 2008-06-03,在svn中最新也是2010年提交,已经过时,不予考虑。
-
mmseg4j:最新版已从 https://code.google.com/p/mmseg4j/ 移至 https://github.com/chenlb/mmseg4j-solr,支持Lucene 4.10,且在github中最新提交代码是2014年6月,从09年~14年一共有:18个版本,也就是一年几乎有3个大小版本,有较大的活跃度,用了mmseg算法。
-
IK-analyzer: 最新版在https://code.google.com/p/ik-analyzer/上,支持Lucene 4.10从2006年12月推出1.0版开始, IKAnalyzer已经推出了4个大版本。最初,它是以开源项目Luence为应用主体的,结合词典分词和文法分析算法的中文分词组件。从3.0版本开 始,IK发展为面向Java的公用分词组件,独立于Lucene项目,同时提供了对Lucene的默认优化实现。在2012版本中,IK实现了简单的分词 歧义排除算法,标志着IK分词器从单纯的词典分词向模拟语义分词衍化。 但是也就是2012年12月后没有在更新。
-
ansj_seg:最新版本在 https://github.com/NLPchina/ansj_seg tags仅有1.1版本,从2012年到2014年更新了大小6次,但是作者本人在2014年10月10日说明:“可能我以后没有精力来维护ansj_seg了”,现在由”nlp_china”管理。2014年11月有更新。并未说明是否支持Lucene,是一个由CRF(条件随机场)算法所做的分词算法。
-
imdict-chinese-analyzer:最新版在 https://code.google.com/p/imdict-chinese-analyzer/ , 最新更新也在2009年5月,下载源码,不支持Lucene 4.10 。是利用HMM(隐马尔科夫链)算法。
-
Jcseg:最新版本在git.oschina.net/lionsoul/jcseg,支持Lucene 4.10,作者有较高的活跃度。利用mmseg算法。
7.4.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>8.1.0</version>
</dependency>
- 加入配置文件:
/**
* 使用第三方分词器(IK分词)
* 特点: 支持中文语义分析, 提供停用词典, 提供扩展词典, 供程序员扩展使用
* @throws Exception
*/
@Test
public void TestIKAnalyzer() throws Exception{
// 1. 创建分词器,分析文档,对文档进行分词
Analyzer analyzer = new IKAnalyzer();
// 2. 创建Directory对象,声明索引库的位置
Directory directory = FSDirectory.open(Paths.get("E:\\dir"));
// 3. 创建IndexWriteConfig对象,写入索引需要的配置
IndexWriterConfig config = new IndexWriterConfig(analyzer);
// 4.创建IndexWriter写入对象
IndexWriter indexWriter = new IndexWriter(directory, config);
// 5.写入到索引库,通过IndexWriter添加文档对象document
Document doc = new Document();
doc.add(new TextField("name", "vivo X23 8GB+128GB 幻夜蓝,水滴屏全面屏,游戏手机.移动联通电信全网通4G手机", Field.Store.YES));
indexWriter.addDocument(doc);
// 6.释放资源
indexWriter.close();
}
7.4.4. 扩展中文词库
如果想配置扩展词和停用词,就创建扩展词的文件和停用词的文件。
从ikanalyzer包中拷贝配置文件
拷贝到资源文件夹中
停用词典stopword.dic作用:
停用词典中的词例如: a, an, the, 的, 地, 得等词汇, 凡是出现在停用词典中的字或者词, 在切分词的时候会被过滤掉.
扩展词典ext.dic作用 :
扩展词典中的词例如: 传智播客, 黑马程序员, 贵州茅台等专有名词, 在汉语中一些公司名称, 行业名称, 分 类, 品牌等不是汉语中的词汇, 是专有名词. 这些分词器默认不识别, 所以需要放入扩展词典中, 效果是被强制分成一个词.
8. Lucene高级搜索
8.1.文本搜索
QueryParser支持默认搜索域, 第一个参数为默认搜索域.
如果在执行parse方法的时候, 查询语法中包含域名则从指定的这个域名中搜索, 如果只有查询的关键字,则从默认搜索域中搜索结果.
需求描述 : 查询名称中包含华为手机关键字的结果.
/*
1. 创建分词器(对搜索的关键词进行分词使用)
2. 创建查询对象,
3. 设置搜索关键词
4. 创建Directory目录对象, 指定索引库的位置
5. 创建输入流对象
6. 创建搜索对象
7. 搜索, 并返回结果
*/
@Test
public void testIndexSearch() throws Exception {
//1. 创建分词器(对搜索的关键词进行分词使用)
//注意: 分词器要和创建索引的时候使用的分词器一模一样
// Analyzer analyzer = new StandardAnalyzer();
Analyzer analyzer = new IKAnalyzer();
//2. 创建查询对象,
//第一个参数: 默认查询域, 如果查询的关键字中带搜索的域名, 则从指定域中查询, 如果不带域名则从, 默认搜索域中查询
//第二个参数: 使用的分词器
QueryParser queryParser = new QueryParser( "name",analyzer);
//3. 设置搜索关键词
//华为 AND 手机 求交集 华为 OR 手机 求并集
Query query = queryParser.parse("华为 AND 手机");
// Query query = queryParser.parse("price:[0 TO 9999]");
// Query query = queryParser.parse("price:[0 TO 9999]");
//4. 创建Directory目录对象, 指定索引库的位置
// Directory dir = FSDirectory.open(Paths.get("E:\\dir"));
Directory dir = FSDirectory.open(Paths.get("/Users/miyufeng/Downloads/lucene_dev"));
//5. 创建输入流对象
IndexReader indexReader = DirectoryReader.open(dir);
//6. 创建搜索对象
IndexSearcher indexSearcher = new IndexSearcher(indexReader);
//7. 搜索, 并返回结果
//第二个参数: 是返回多少条数据用于展示, 分页使用
TopDocs topDocs = indexSearcher.search(query, 10);
//获取查询到的结果集的总数, 打印
System.out.println("=======count=======" + topDocs.totalHits);
//8. 获取结果集
ScoreDoc[] scoreDocs = topDocs.scoreDocs;
//9. 遍历结果集
if (scoreDocs != null) {
for (ScoreDoc scoreDoc : scoreDocs) {
//获取查询到的文档唯一标识, 文档id, 这个id是lucene在创建文档的时候自动分配的
int docID = scoreDoc.doc;
//通过文档id, 读取文档
Document doc = indexSearcher.doc(docID);
System.out.println("==================================================");
//通过域名, 从文档中获取域值
System.out.println("===id==" + doc.get("id"));
System.out.println("===name==" + doc.get("name"));
System.out.println("===price==" + doc.get("price"));
System.out.println("===image==" + doc.get("image"));
System.out.println("===brandName==" + doc.get("brandName"));
System.out.println("===categoryName==" + doc.get("categoryName"));
}
}
//10. 关闭流
}
8.2.数值范围搜索
需求描述 : 查询价格大于等于100, 小于等于1000的商品
测试代码:
/**
* 数值范围查询
* @throws Exception
*/
@Test
public void testRangeQuery() throws Exception {
//1. 创建分词器(对搜索的关键词进行分词使用)
//注意: 分词器要和创建索引的时候使用的分词器一模一样
Analyzer analyzer = new IKAnalyzer();
//2. 创建查询对象,
Query query = IntPoint.newRangeQuery("price", 100, 1000);
//4. 创建Directory目录对象, 指定索引库的位置
Directory dir = FSDirectory.open(Paths.get("/Users/miyufeng/Downloads/lucene_dev"));
//5. 创建输入流对象
IndexReader indexReader = DirectoryReader.open(dir);
//6. 创建搜索对象
IndexSearcher indexSearcher = new IndexSearcher(indexReader);
//7. 搜索, 并返回结果
//第二个参数: 是返回多少条数据用于展示, 分页使用
TopDocs topDocs = indexSearcher.search(query, 10);
//获取查询到的结果集的总数, 打印
System.out.println("=======count=======" + topDocs.totalHits);
//8. 获取结果集
ScoreDoc[] scoreDocs = topDocs.scoreDocs;
//9. 遍历结果集
if (scoreDocs != null) {
for (ScoreDoc scoreDoc : scoreDocs) {
//获取查询到的文档唯一标识, 文档id, 这个id是lucene在创建文档的时候自动分配的
int docID = scoreDoc.doc;
//通过文档id, 读取文档
Document doc = indexSearcher.doc(docID);
System.out.println("==================================================");
//通过域名, 从文档中获取域值
System.out.println("===id==" + doc.get("id"));
System.out.println("===name==" + doc.get("name"));
System.out.println("===price==" + doc.get("price"));
System.out.println("===image==" + doc.get("image"));
System.out.println("===brandName==" + doc.get("brandName"));
System.out.println("===categoryName==" + doc.get("categoryName"));
}
}
//10. 关闭流
}
8.3.组合搜索
需求描述 : 查询价格大于等于100, 小于等于1000, 并且名称中不包含华为手机关键字的商品
BooleanClause.Occur.MUST 必须 相当于and, 并且
BooleanClause.Occur.MUST_NOT 不必须 相当于not, 非
BooleanClause.Occur.SHOULD 应该 相当于or, 或者
注意 : 如果逻辑条件中, 只有MUST_NOT, 或者多个逻辑条件都是MUST_NOT, 无效, 查询不出任何数据.
/**
* 组合查询
* @throws Exception
*/
@Test
public void testBooleanQuery() throws Exception {
Analyzer analyzer = new IKAnalyzer();
Query query1 = IntPoint.newRangeQuery("price", 100, 1000);
QueryParser queryParser = new QueryParser("name", analyzer);
Query query2 = queryParser.parse("name:华为手机");
//创建布尔查询对象(组合查询对象)
/**
* BooleanClause.Occur.MUST 必须相当于and, 也就是并且的关系
* BooleanClause.Occur.SHOULD 应该相当于or, 也就是或者的关系
* BooleanClause.Occur.MUST_NOT 必须不, 相当于not, 非
* 注意: 如果查询条件都是MUST_NOT, 或者只有一个查询条件, 然后这一个查询条件是MUST_NOT则
* 查询不出任何数据.
*/
BooleanQuery.Builder query = new BooleanQuery.Builder();
query.add(query1, BooleanClause.Occur.MUST);
query.add(query2, BooleanClause.Occur.MUST);
Directory dir = FSDirectory.open(Paths.get("/Users/miyufeng/Downloads/lucene_dev"));
IndexReader indexReader = DirectoryReader.open(dir);
IndexSearcher indexSearcher = new IndexSearcher(indexReader);
TopDocs topDocs = indexSearcher.search(query.build(), 10);
System.out.println("=======count=======" + topDocs.totalHits);
ScoreDoc[] scoreDocs = topDocs.scoreDocs;
if (scoreDocs != null) {
for (ScoreDoc scoreDoc : scoreDocs) {
//获取查询到的文档唯一标识, 文档id, 这个id是lucene在创建文档的时候自动分配的
int docID = scoreDoc.doc;
//通过文档id, 读取文档
Document doc = indexSearcher.doc(docID);
System.out.println("==================================================");
//通过域名, 从文档中获取域值
System.out.println("===id==" + doc.get("id"));
System.out.println("===name==" + doc.get("name"));
System.out.println("===price==" + doc.get("price"));
System.out.println("===image==" + doc.get("image"));
System.out.println("===brandName==" + doc.get("brandName"));
System.out.println("===categoryName==" + doc.get("categoryName"));
}
}
//10. 关闭流
}
9. 搜索案例
成品效果:
9.1. 引入依赖
在项目的pom.xml中引入依赖:
9.2. 项目加入页面和资源
将Lucene课程资料\资源\页面和静态资源, 下的页面和静态资源拷贝到项目的resources目录下
9.3. 创建包和启动类
创建目录, 并加入启动类:
启动类代码:
/**
*
*/
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
9.4. 配置文件
项目的resources目录下创建application.yml内容如下:
spring:
thymeleaf:
cache: false
9.5. 业务代码:
9.5.1. 封装pojo
pojo包下加入ResultModel实体类
/**
* 自定义分页实体类
*/
public class ResultModel {
// 商品列表
private List<Sku> skuList;
// 商品总数
private Long recordCount;
// 总页数
private Long pageCount;
// 当前页
private long curPage;
9.5.2. controller代码
/**
*
*/
@Controller
@RequestMapping("/list")
public class SearchController {
@Autowired
private SearchService searchService;
/**
* 搜索
* @param queryString 查询的关键字
* @param price 查询价格范围
* @param page 当前页
* @return
* @throws Exception
*/
@RequestMapping
public String query(String queryString, String price, Integer page, Model model) throws Exception{
//处理当前页
if (StringUtils.isEmpty(page)) {
page = 1;
}
if (page <= 0) {
page = 1;
}
//调用service查询
ResultModel resultModel = searchService.query(queryString, price, page);
model.addAttribute("result", resultModel);
//查询条件回显到页面
model.addAttribute("queryString", queryString);
model.addAttribute("price", price);
model.addAttribute("page", page);
return "search";
}
}
9.5.3. service代码
service接口:
/**
*
*/
public interface SearchService {
public ResultModel query(String queryString, String price, Integer page) throws Exception;
}
/**
*
*/
@Service
public class SearchServiceImpl implements SearchService {
//每页查询20条数据
public final static Integer PAGE_SIZE = 20;
@Override
public ResultModel query(String queryString, String price, Integer page) throws Exception {
long startTime = System.currentTimeMillis();
//1. 需要使用的对象封装
ResultModel resultModel = new ResultModel();
//从第几条开始查询
int start = (page - 1) * PAGE_SIZE;
//查询到多少条为止
Integer end = page * PAGE_SIZE;
//创建分词器
Analyzer analyzer = new IKAnalyzer();
//创建组合查询对象
BooleanQuery.Builder builder = new BooleanQuery.Builder();
//2. 根据查询关键字封装查询对象
QueryParser queryParser = new QueryParser("name", analyzer);
Query query1 = null;
//判断传入的查询关键字是否为空, 如果为空查询所有, 如果不为空, 则根据关键字查询
if (StringUtils.isEmpty(queryString)) {
query1 = queryParser.parse("*:*");
} else {
query1 = queryParser.parse(queryString);
}
//将关键字查询对象, 封装到组合查询对象中
builder.add(query1, BooleanClause.Occur.MUST);
//3. 根据价格范围封装查询对象
if (!StringUtils.isEmpty(price)) {
String[] split = price.split("-");
Query query2 = IntPoint.newRangeQuery("price", Integer.parseInt(split[0]), Integer.parseInt(split[1]));
//将价格查询对象, 封装到组合查询对象中
builder.add(query2, BooleanClause.Occur.MUST);
}
//4. 创建Directory目录对象, 指定索引库的位置
/**
* 使用MMapDirectory消耗的查询时间
* ====消耗时间为=========324ms
* ====消耗时间为=========18ms
*/
Directory directory = FSDirectory.open(Paths.get("E:\\dir"));
//5. 创建输入流对象
IndexReader reader = DirectoryReader.open(directory);
//6. 创建搜索对象
IndexSearcher indexSearcher = new IndexSearcher(reader);
//7. 搜索并获取搜索结果
TopDocs topDocs = indexSearcher.search(builder.build(), end);
//8. 获取查询到的总条数
resultModel.setRecordCount(topDocs.totalHits);
//9. 获取查询到的结果集
ScoreDoc[] scoreDocs = topDocs.scoreDocs;
long endTime = System.currentTimeMillis();
System.out.println("====消耗时间为=========" + (endTime - startTime) + "ms");
//10. 遍历结果集封装返回的数据
List<Sku> skuList = new ArrayList<>();
if (scoreDocs != null) {
for (int i = start; i < end; i ++) {
//通过查询到的文档编号, 找到对应的文档对象
Document document = reader.document(scoreDocs[i].doc);
//封装Sku对象
Sku sku = new Sku();
sku.setId(document.get("id"));
sku.setPrice(Integer.parseInt(document.get("price")));
sku.setImage(document.get("image"));
sku.setName(document.get("name"));
sku.setBrandName(document.get("brandName"));
sku.setCategoryName(document.get("categoryName"));
skuList.add(sku);
}
}
//封装查询到的结果集
resultModel.setSkuList(skuList);
//封装当前页
resultModel.setCurPage(page);
//总页数
Long pageCount = topDocs.totalHits % PAGE_SIZE > 0 ? (topDocs.totalHits/PAGE_SIZE) + 1 : topDocs.totalHits/PAGE_SIZE;
resultModel.setPageCount(pageCount);
return resultModel;
}
}
10. Lucene底层储存结构(高级)
10.1. 详细理解lucene存储结构
存储结构 :
索引(Index) :
- 一个目录一个索引,在Lucene中一个索引是放在一个文件夹中的。
段(Segment) :
-
一个索引(逻辑索引)由多个段组成, 多个段可以合并, 以减少读取内容时候的磁盘IO.
-
Lucene中的数据写入会先写内存的一个Buffffer,当Buffffer内数据到一定量后会被flflush成一个Segment,每个Segment有自己独立的索引,可独立被查询,但数据永远不能被更改。这种模式避免了随机写,数据写入都是批量追加,能达到很高的吞吐量。Segment中写入的文档不可被修改,但可被删除,删除的方式也不是在文件内部原地更改,而是会由另外一个文件保存需要被删除的文档的DocID,保证数据文件不可被修改。Index的查询需要对多个Segment进行查询并对结果进行合并,还需要处理被删除的文档,为了对查询进行优化,Lucene会有策略对多个Segment进行合并。
文档(Document) :
-
文档是我们建索引的基本单位,不同的文档是保存在不同的段中的,一个段可以包含多篇文档。
-
新添加的文档是单独保存在一个新生成的段中,随着段的合并,不同的文档合并到同一个段中。
域(Field) :
- 一篇文档包含不同类型的信息,可以分开索引,比如标题,时间,正文,描述等,都可以保存在不同的域里。不同域的索引方式可以不同。
词(Term)
- 词是索引的最小单位,是经过词法分析和语言处理后的字符串。
10.2. 索引库物理文件
10.3. 索引库文件扩展名对照表
名称 | 文件扩展名 | 简短描述 |
---|---|---|
Segments File | segments_N | 保存一个提交点(a commit point )的信息 |
Lock File | Write.lock | 防止多个IndexWriter同时写到一份索引文件中 |
Segment Info | .si | 保存了索引段的元数据信息 |
Compound File | .cfs, .cfe | 一个可选的虚拟文件,把所有索引信息都存储到符合索引文件中 |
Fields | .fnm | 保存fields的相关信息 |
Field Data | .fdt | 保存指定field data的指针 |
Term Dictonary | .tim | term词典,存储term信息 |
Term Index | .tip | 到Term Dictionary的索引 |
Frequencies | .doc | 由包含每个term以及频率的docs列表组成 |
Positions | .pos | 存储出现在索引中的term的位置信息 |
Payloads | .pay | 存储额外的per-position元数据信息,例如字符偏移和用户payloads |
Norms | .nvd,.nvm | .nvm文件保存索引字段加权因子的元数据,.nvd文件保存索引字段加权数据 |
Per-Document Values | .dvd,.dvm | .dvm文件保存索引文档评分因子的元数据,.dvd文件保存索引文档评分数据 |
Term Vector Index | .tvx | 将偏移存储到文档数据文件中 |
Term Vector Documents | .tvd | 包含有term vectors的每个文档信息 |
Term Vector Fields | .tvf | 字段级别有关term vectors的信息 |
Live Documents | .liv | 哪些是有效文件的信息 |
Point values | .dii,.dim | 保留索引点,如果有的话 |
10.4. 词典的构建
为何Lucene大数据量搜索快,要分两部分来看 :
-
一点是因为底层的倒排索引存储结构.
-
另一点就是查询关键字的时候速度快, 因为词典的索引结构.
10.4.1. 词典数据结构对比
倒排索引中的词典位于内存,其结构尤为重要,有很多种词典结构,各有各的优缺点,最简单如排序数组,通过二分查找来检索数据,更快的有哈希表,磁盘查找有B树、B+树,但一个能支持TB级数据的倒排索引结构需要在时间和空间上有个平衡,下图列了一些常见词典的优缺点:
数据结构 | 优缺点 |
---|---|
跳跃表 | 占用内存小,且可调,但是对模糊查询支持不好 |
排序列表Array/List | 使用二分法查找,不平衡 |
字典树 | 查询效率跟字符串长度有关,但只适合英文词典 |
哈希表 | 性能高,内存消耗大,几乎是原始数据的三倍 |
双数组字典树 | 适合做中文词典,内存占用小,很多分词工具均采用此种算法 |
FST(Finite State Transducers) | 一种有限状态转移机,Lucene4有开源实现,并大量使用 |
B树 | 磁盘索引,更新方便,但检索速度慢,多用于数据库 |
Lucene3.0之前使用的也是跳跃表结构,后换成了FST,但跳跃表在Lucene其他地方还有应用如倒排表合并和文档号索引。
10.4.2. 跳跃表原理
Lucene3.0版本之前使用的跳跃表结构后换成了FST结构
**优点 :**结构简单、跳跃间隔、级数可控,Lucene3.0之前使用的也是跳跃表结构,,但跳跃表在Lucene其他地方还有应用如倒排表合并和文档号索引。
**缺点 :**模糊查询支持不好.
单链表 :
单链表中查询一个元素即使是有序的,我们也不能通过二分查找法的方式缩减查询时间。
通俗的讲也就是按照链表顺序一个一个找.
举例: 查找85这个节点, 需要查找7次.
跳跃表:
举例: 查询85这个节点, 一共需要查询6次.
-
在level3层, 查询3次, 查询到1结尾, 退回到37节点
-
在level2层, 从37节点开始查询, 查询2次, 查询到1结尾, 退回到71节点
-
在level1层, 从71节点开始查询, 查询1次, 查询到85节点.
10.4.3. FST原理简析
Lucene现在采用的数据结构为FST,它的特点就是: **优点:**内存占用率低,压缩率一般在3倍~20倍之间、模糊查询支持好、查询快 **缺点:**结构复杂、输入要求有序、更新不易
已知FST要求输入有序,所以Lucene会将解析出来的文档单词预先排序,然后构建FST,我们假设输入为abd,abe,acf,acg,那么整个构建过程如下:
输入数据:
String inputValues[] = {"hei","ma","cheng","xu","yuan","good"};
long outputValues[] = {0,1,2,3,4,5};
输入的数据如下:
hei/0 ma/1 cheng/2 xu/3 yuan/4 good/5
存储结果如下:
11. Lucene优化(高级)
11.1. 解决大量磁盘IO
-
confifig.setMaxBufffferedDocs(100000); 控制写入一个新的segment前内存中保存的document的数目,设置较大的数目可以加快建索引速度。
数值越大索引速度越快, 但是会消耗更多的内存
-
indexWriter.forceMerge(文档数量); 设置N个文档合并为一个段
数值越大索引速度越快, 搜索速度越慢; 值越小索引速度越慢, 搜索速度越快
更高的值意味着索引期间更低的段合并开销,但同时也意味着更慢的搜索速度,因为此时的索引通常会包含更多的段。如果该值设置的过高,能获得更高的索引性能。但若在最后进行索引优化,那么较低的值会带来更快的搜索速度,因为在索引操作期间程序会利用并发机制完成段合并操作。故建议对程序分别进行高低多种值的测试,利用计算机的实际性能来告诉你最优值。
创建索引代码优化测试:
/**
* 测试创建索引速度优化
* @throws Exception
*/
@Test
public void createIndexTest2() throws Exception {
//1. 采集数据
SkuDao skuDao = new SkuDaoImpl();
List<Sku> skuList = skuDao.querySkuList();
//文档集合
List<Document> docList = new ArrayList<>();
for (Sku sku : skuList) {
//2. 创建文档对象
Document document = new Document();
document.add(new StringField("id", sku.getId(), Field.Store.YES));
document.add(new TextField("name", sku.getName(), Field.Store.YES));
document.add(new IntPoint("price", sku.getPrice()));
document.add(new StoredField("price", sku.getPrice()));
document.add(new StoredField("image", sku.getImage()));
document.add(new StringField("categoryName", sku.getCategoryName(), Field.Store.YES));
document.add(new StringField("brandName", sku.getBrandName(), Field.Store.YES));
//将文档对象放入到文档集合中
docList.add(document);
}
long start = System.currentTimeMillis();
//3. 创建分词器, StandardAnalyzer标准分词器, 对英文分词效果好, 对中文是单字分词, 也就是一个字就认为是一个词.
Analyzer analyzer = new StandardAnalyzer();
//4. 创建Directory目录对象, 目录对象表示索引库的位置
Directory dir = FSDirectory.open(Paths.get("/Users/miyufeng/Downloads/lucene_dev"));
//5. 创建IndexWriterConfig对象, 这个对象中指定切分词使用的分词器
/**
* 没有优化 小100万条数据, 创建索引需要7725ms
*
*/
IndexWriterConfig config = new IndexWriterConfig(analyzer);
//设置在内存中多少个文档向磁盘中批量写入一次数据
//如果设置的数字过大, 会过多消耗内存, 但是会提升写入磁盘的速度
config.setMaxBufferedDocs(500000);
//6. 创建IndexWriter输出流对象, 指定输出的位置和使用的config初始化对象
IndexWriter indexWriter = new IndexWriter(dir, config);
//设置多少给文档合并成一个段文件,数值越大索引速度越快, 搜索速度越慢; 值越小索引速度越慢, 搜索速度越快
indexWriter.forceMerge(1000000);
//7. 写入文档到索引库
for (Document doc : docList) {
indexWriter.addDocument(doc);
}
//8. 释放资源
indexWriter.close();
long end = System.currentTimeMillis();
System.out.println("=====消耗的时间为:==========" + (end - start) + "ms");
}
11.2. 选择合适的分词器
不同的分词器分词效果不同, 所用时间也不同
虽然StandardAnalyzer切分词速度快过IKAnalyzer, 但是由于StandardAnalyzer对中文支持不好, 所以为了追求好的分词效果,为了追求查询时的准确率,也只能用IKAnalyzer分词器, IKAnalyzer支持停用词典和扩展词典,可以通过调整两个词典中的内容,来提升查询匹配的精度
11.3. 选择合适的位置存放索引库
类 | 写操作 | 读操作 | 特点 |
---|---|---|---|
SimpleFSDirectory | Java.io.RandomAccessFile | Java.io.RandomAccessFile | 简单实现,并发能力差 |
NIOFSDirectory | Java.nio.FileChannel | FSDirectory.FSIndexOutput | 并发能力强,windows平台下有重大bug |
MMapDirectory | 内存映射 | FSDirectory.FSIndexOutput | 读取操作基于内存 |
测试代码修改:
Directory directory = MMapDirectory.open(Paths.get("E:\\dir"));
11.4. 搜索api的选择
-
尽量使用TermQuery代替QueryParser
-
尽量避免大范围的日期查询
12. Lucene相关度排序(高级)
12.1. 什么是相关度排序
Lucene对查询关键字和索引文档的相关度进行打分,得分高的就排在前边。
12.1.1. 如何打分
Lucene是在用户进行检索时实时根据搜索的关键字计算出来的,分两步:
-
计算出词(Term)的权重
-
根据词的权重值,计算文档相关度得分。
12.1.2. 什么是词的权重
明确索引的最小单位是一个Term(索引词典中的一个词),搜索也是要从Term中搜索,再根据Term找到文档,Term对文档的重要性称为权重,影响Term权重有两个因素:
-
Term Frequency (tf): 指此Term在此文档中出现了多少次。tf 越大说明越重要。 词(Term)在文档中出现的次数越多,说明此词(Term)对该文档越重要,如“Lucene”这个词,在文档中出现的次数很多,说明该文档主要就是讲Lucene技术的。
-
Document Frequency (df): 指有多少文档包含次Term。df 越大说明越不重要。 比如,在一篇英语文档中,this出现的次数更多,就说明越重要吗?不是的,有越多的文档包含此词(Term), 说明此词(Term)太普通,不足以区分这些文档,因而重要性越低。
12.1.3. 怎样影响相关度排序
boost是一个加权值(默认加权值为1.0f),它可以影响权重的计算。
-
在索引时对某个文档中的fifield设置加权值高,在搜索时匹配到这个文档就可能排在前边。
-
在搜索时对某个域进行加权,在进行组合域查询时,匹配到加权值高的域最后计算的相关度得分就高。
设置boost是给域(fifield)或者Document设置的。
12.2.人为影响相关度排序
查询的时候, 通过设置查询域的权重, 可以人为影响查询结果.
/**
* 测试相关度排序
* @throws Exception
*/
@Test
public void testIndexSearch2() throws Exception {
//1. 创建分词器(对搜索的关键词进行分词使用)
//注意: 分词器要和创建索引的时候使用的分词器一模一样
Analyzer analyzer = new IKAnalyzer();
//需求: 不管是名称域还是品牌域或者是分类域有关于手机关键字的查询出来
//查询的多个域名
String[] fields = {"name", "categoryName", "brandName"};
//设置影响排序的权重, 这里设置域的权重
Map<String, Float> boots = new HashMap<>();
boots.put("categoryName", 10000000000f);
//从多个域查询对象
MultiFieldQueryParser multiFieldQueryParser = new MultiFieldQueryParser(fields, analyzer, boots);
//设置查询的关键词
Query query = multiFieldQueryParser.parse("手机");
//4. 创建Directory目录对象, 指定索引库的位置
Directory dir = FSDirectory.open(Paths.get("E:\\dir"));
//5. 创建输入流对象
IndexReader indexReader = DirectoryReader.open(dir);
//6. 创建搜索对象
IndexSearcher indexSearcher = new IndexSearcher(indexReader);
//7. 搜索, 并返回结果
//第二个参数: 是返回多少条数据用于展示, 分页使用
TopDocs topDocs = indexSearcher.search(query, 10);
//获取查询到的结果集的总数, 打印
System.out.println("=======count=======" + topDocs.totalHits);
//8. 获取结果集
ScoreDoc[] scoreDocs = topDocs.scoreDocs;
//9. 遍历结果集
if (scoreDocs != null) {
for (ScoreDoc scoreDoc : scoreDocs) {
//获取查询到的文档唯一标识, 文档id, 这个id是lucene在创建文档的时候自动分配的
int docID = scoreDoc.doc;
//通过文档id, 读取文档
Document doc = indexSearcher.doc(docID);
System.out.println("==================================================");
//通过域名, 从文档中获取域值
System.out.println("===id==" + doc.get("id"));
System.out.println("===name==" + doc.get("name"));
System.out.println("===price==" + doc.get("price"));
System.out.println("===image==" + doc.get("image"));
System.out.println("===brandName==" + doc.get("brandName"));
System.out.println("===categoryName==" + doc.get("categoryName"));
}
}
//10. 关闭流
}
13. Lucene使用注意事项(高级)
-
关键词区分大小写 OR AND TO等关键词是区分大小写的,lucene只认大写的,小写的当做普通单词。
-
读写互斥性 同一时刻只能有一个对索引的写操作,在写的同时可以进行搜索
-
文件锁 在写索引的过程中强行退出将在tmp目录留下一个lock文件,使以后的写操作无法进行,可以将其手工删除
-
时间格式 lucene只支持一种时间格式yyMMddHHmmss,所以你传一个yy-MM-dd HH:mm:ss的时间给lucene它是不会当作时间来处理的
-
设置boost 有些时候在搜索时某个字段的权重需要大一些,例如你可能认为标题中出现关键词的文章比正文中出现关键词的文章更有价值,你可以把标题的boost设置的更大,那么搜索结果会优先显示标题中出现关键词的文章.