ES 使用
ES 安装
ElasticSearch安装
1 | docker pull elasticsearch:7.7.1 # 安装es7.7.1版本 |
es容器内部需要挂载的文件如下所示
- 存放配置相关的:/usr/share/elasticsearch/config
- 存放数据相关的:/usr/share/elasticsearch/data
- 存放插件相关的:/usr/share/elasticsearch/plugins
如果要部署kibana,则需要让es和kibana互联,需要利用docker创建一个网络
docker network create es-net
详情可自己搜索
这里我们在宿主机创建相应的文件
为所有文件进行权限赋予
1 | chmod 777 /mydata/es/config |
直接启动一个简单的容器
1 | docker run -d --name elasticsearch -p 9200:9200 -p 9300:9300 -e "discovery.type=single-node" elasticsearch:7.7.1 |
将config里面的内容拷贝到宿主机
1 | docker cp elasticsearch:/usr/share/elasticsearch/config /mydata/es |
删除临时容器
1 | docker stop elasticsearch |
修改 elasticsearch.yml
文件
这里需要注意,elasticsearch 8.x
版本采用了默认配置,配置文件中没有之前那么多内容,如果需要修改的话,直接自己添加相应的内容即可。例如es8默认开启了账号校验,如果需要关闭的话,输入下图所示的配置即可。
修改后,重新启动容器(一定要给权限给挂载的文件夹!)
1 | docker run \ |
之后可以进行页面的访问http://你的ip:9200
es的ik分词器安装
es自带的分词器无法解析中文内容,所以我们需要安装ik分词器用于解析中文内容
https://github.com/medcl/elasticsearch-analysis-ik
这里把8.2.0版本的上传上来📎elasticsearch-analysis-ik-8.2.0.zip
找到与你es版本相应的ik分词器,我这里是8.2.0版本的es
下载完成后进行解压,将解压后的文件传输到linux服务器中,目录和文件内容如下
进入容器内部
可以看到,容器内没有任何插件,我们在启动的时候进行了文件挂载,可以直接在宿主机的plugins文件夹下操作,如果启动时没有挂载,可以利用docker cp
的命令进行文件的传输
这里我们直接在宿主机操作,把文件传输过来后,重启es
重启成功之后,我们进行测试
上面采用原生分词器,枫叶被拆开了
更换为ik分词器后,可以发现,枫叶没有被拆开
ES-head插件安装
下载好后,直接拖入chrome,安装插件
连接好es后,可以看到如下界面
ES基础http调用
使用http的put方法,传一个参数,代表创建了一个subject_index的索引
查看索引,查看所有索引的情况
查看单个索引
删除单个索引
创建文档
查看文档,需要传入上图的_id
的值
创建映射,为列设置属性
查看映射,将设置映射的方法改为get
查看匹配文档
查看特定字段匹配文档
原生分词器使用
spring-data-es基础调用
首先需要引入es的maven包
注意这里的版本,与是springboot
的版本要相对应,如果版本过高,可能会与springboot
产生冲突,导致项目无法启动。同时2.4.2版本对应的es
版本需要是7.x,如果es
版本过高,例如8.x,在调用的时候会报错。
导入成功后,就可以使用springdataes中的接口了
1 |
|
创建一个接口,注入到spring容器内,继承了ElasticsearchRepository
,可以直接使用其中的方法。
主要使用到的就是ElasticsearchRepository
接口以及一个名叫ElasticsearchRestTemplate
的类。
这里创建了一个自己定义的接口SubjectEsService
,直接展示实现类SubjectEsServiceImpl
的代码
1 |
|
createIndex
方法用于在 Elasticsearch 中创建索引。具体步骤如下:
- 获取索引操作对象:通过
elasticsearchRestTemplate.indexOps(SubjectInfoES.class)
获取IndexOperations
对象,用于执行索引操作。 - 创建索引:调用
indexOperations.create()
方法创建索引。 - 创建映射:通过
indexOperations.createMapping(SubjectInfoES.class)
创建索引的映射(mapping),并将其存储在Document
对象中。 - 设置映射:调用
indexOperations.putMapping(mapping)
方法,将映射应用到索引中。
addDocuments
方法确保了在 Elasticsearch 中为 SubjectInfoES
类创建相应的索引和映射。
addDocuments
方法用于向 Elasticsearch 中添加文档。具体步骤如下:
- 创建一个
List<SubjectInfoES>
对象list
,用于存储要添加的文档。 - 向
list
中添加两个SubjectInfoES
对象,分别包含模拟数据。 - 使用
subjectEsRepo.saveAll(list)
方法将list
中的所有文档保存到 Elasticsearch 中。
这个方法的主要作用是将一些模拟数据批量添加到 Elasticsearch 索引中。
find
方法从 Elasticsearch 数据库中检索所有 SubjectInfoES
实体,并将每个实体的信息记录到日志中。
search
方法用于搜索文档,在http的调用中我们可以看到,需要封装的json类型非常的复杂繁琐,这里利用了querybuilder
直接创建了一个查询对象用于查询,就类似于封装了一个http的请求
自定义 ES 工具
首先导入如下三个包
1 | <dependency> |
封装集群 ES 连接,统一配置
新建一个包名为es
,将与es有关的类都放在该包下,新建如下几个类
EsSourceData.java
1
2
3
4
5
6
7
8
9
10/**
* es数据源
*/
public class EsSourceData implements java.io.Serializable {
private String docId;
private Map<String, Object> data;
}
作用是表示从 Elasticsearch 中获取的数据源。它包含两个字段:
docId
:表示文档的唯一标识符。data
:表示文档的实际数据,以键值对的形式存储。EsSearchRequest.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
public class EsSearchRequest {
/**
* 查询条件
*/
private BoolQueryBuilder bq;
/**
* 查询字段
*/
private String[] fields;
/**
* 页数
*/
private int from;
/**
* 条数
*/
private int size;
/**
* 是否需要快照
*/
private Boolean needScroll;
/**
* 快照时间
*/
private Long minutes;
/**
* 排序字段
*/
private String sortName;
/**
* 排序类型
*/
private String sortOrder;
/**
* 高亮
*/
private HighlightBuilder highlightBuilder;
}
用于封装 Elasticsearch 查询请求的参数。它包含了查询条件、查询字段、分页信息、排序信息以及高亮设置等。通过这个类,可以方便地构建和传递 Elasticsearch 查询请求的各种参数。
EsIndexInfo.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class EsIndexInfo implements Serializable {
private static final long serialVersionUID = -6010237985622125807L;
/**
* 集群名称
*/
private String clusterName;
/**
* 索引名称
*/
private String indexName;
}
表示 Elasticsearch 索引的信息。它包含了集群名称 (clusterName
) 和索引名称 (indexName
) 两个字段,用于存储和传递与 Elasticsearch 索引相关的基本信息。
EsClusterConfig.java
title:EsClusterConfig.java 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15/**
* es集群配置
*/
public class EsClusterConfig {
/**
* 集群名称
*/
private String name;
/**
* 集群节点
*/
private String nodes;
}
用于表示 Elasticsearch 集群的配置
EsConfigProperties
unwrap title:EsConfigProperties 1
2
3
4
5
6
7
public class EsConfigProperties {
private List<EsClusterConfig> esConfigs = new ArrayList<>();
}
用于加载和存储 Elasticsearch 集群的配置信息。它通过 Spring Boot 的 @ConfigurationProperties
注解,将配置文件中的属性映射到类的字段中。
1 |
|
初始化和管理与 Elasticsearch 集群的连接。它通过读取配置文件中的集群信息,创建并维护 RestHighLevelClient
实例,以便与不同的 Elasticsearch 集群进行交互
主要功能:
- 读取配置:通过
EsConfigProperties
读取配置文件中的 Elasticsearch 集群信息。 - 初始化客户端:在
initialize
方法中,遍历所有配置的集群信息,调用initRestClient
方法为每个集群创建RestHighLevelClient
实例。 - 管理客户端:将创建的
RestHighLevelClient
实例存储在clientMap
中,以便后续使用。
通过这些连接方法,实际上是为了模拟与 Elasticsearch (ES) 进行 HTTP 连接以及发送请求
@PostConstruct 是一个 Java 标准注解,用于标记一个方法在依赖注入完成后执行。这个方法会在所有依赖注入完成后、但在该 bean 被任何其他 bean 使用之前调用。
初始化逻辑:在这个例子中,initialize 方法被标记为 @PostConstruct,表示该方法将在所有依赖注入完成后自动调用,用于初始化 Elasticsearch 客户端。
确保顺序:通过使用 @PostConstruct,可以确保在任何业务逻辑开始之前,Elasticsearch 客户端已经正确初始化。
ES 学习
来源 : [黑马Elasticsearch全套教程][https://www.bilibili.com/video/BV1b8411Z7w5]
1. 倒排索引
在 Elasticsearch(ES)中,倒排索引是一种高效的数据结构,主要用于快速全文搜索。它的名称来源于与传统的“正排索引”相比的数据组织方式。
1. 什么是倒排索引?
倒排索引是从文档到词的映射的反向过程。
正排索引:记录每个文档包含哪些词。
示例:
1
2
3
4
5文档1: "我爱学习"
文档2: "学习使我快乐"
正排索引:
文档1 -> 我, 爱, 学习
文档2 -> 学习, 使, 我, 快乐
倒排索引:记录每个词在哪些文档中出现。
示例:
1
2
3
4
5
6倒排索引:
我 -> 文档1, 文档2
爱 -> 文档1
学习 -> 文档1, 文档2
使 -> 文档2
快乐 -> 文档2
2. 为什么叫“倒排”索引?
- 在传统的正排索引中,系统需要从文档逐一扫描内容(比如按文档 ID 的顺序存储每个文档的内容)。
- 而倒排索引将这种“文档到词”的关系反转为“词到文档”,在搜索时不需要逐个扫描文档,而是可以直接查找到相关文档。
这种“词 -> 文档”的关系与传统正排的“文档 -> 词”相反,因此称为倒排索引。
3. 倒排索引的结构
倒排索引由两部分组成:
- 文档(
Document
):用来搜索的数据,其中的每一条数据就是一个文档。例如一个网页、一个商品信息
- 词典(Term Dictionary):包含所有词(去重后的关键词列表)。
- 例如:
["我", "学习", "快乐", "爱", "使"]
- 例如:
- 倒排表(Posting List):记录每个词在哪些文档中出现。
例如:
1
2
3
4
5我 -> [1, 2]
学习 -> [1, 2]
快乐 -> [2]
爱 -> [1]
使 -> [2]
扩展的倒排索引还可以存储以下信息:
- 词频:词在某个文档中出现的次数。
- 位置信息:词在文档中的具体位置(用于短语查询)。
- 文档权重:用于排序的权重信息。
4. 为什么 Elasticsearch 使用倒排索引?
倒排索引是全文检索的基础数据结构,能显著提升搜索效率,原因如下:
- 快速定位文档:查询一个词时,直接通过倒排表找到相关文档 ID,无需逐一扫描所有文档。
- 高效支持多种查询:如关键词查询、短语查询、布尔查询等。
- 节省存储空间:通过分词、去重和压缩技术,倒排索引能大幅减少存储开销。
- 灵活的相关性计算:结合词频和文档评分机制,可以为搜索结果排序,提高相关性。
5. 对比正排索引的劣势
倒排索引虽然在搜索场景下性能优越,但它并不是万能的:
- 实时性较差:新增文档需要更新倒排索引,而正排索引更适合快速写入和更新。
- 结构复杂:实现倒排索引需要更多的内存和计算资源。
- 索引限制:只能给词条创建索引,而不是字段
- 无法字段排序:无法根据字段做排序
因此,在实际系统中,倒排索引和正排索引通常结合使用(例如:ES 使用倒排索引用于全文检索,结合其他数据结构处理聚合查询或排序)。
总结
倒排索引因其“词到文档”的映射特点得名,是全文检索的核心技术。在 Elasticsearch 中,倒排索引通过高效的分词、压缩和优化技术,为海量文本数据的快速检索提供了坚实基础。
2. ES 数据库基本概念
elasticsearch中有很多独有的概念,与mysql中略有差别,但也有相似之处。
文档和字段
一个文档就像数据库里的一条数据,字段就像数据库里的列
elasticsearch是面向文档(Document)存储的,可以是数据库中的一条商品数据,一个订单信息。文档数据会被序列化为json格式后存储在elasticsearch中:
而Json文档中往往包含很多的字段(Field),类似于mysql数据库中的列。
索引和映射
索引就像数据库里的表,映射就像数据库中定义的表结构
例如:
- 所有用户文档,就可以组织在一起,称为用户的索引;
- 所有商品的文档,可以组织在一起,称为商品的索引;
- 所有订单的文档,可以组织在一起,称为订单的索引;
因此,我们可以把索引当做是数据库中的表。
数据库的表会有约束信息,用来定义表的结构、字段的名称、类型等信息。因此,索引库中就有映射(mapping),是索引中文档的字段约束信息,类似表的结构约束。
MySQL与Elasticsearch
各自长处:
- MySQL:擅长事务类型操作,可以确保数据的安全和一致性
- Elasticsearch:擅长海量数据的搜索、分析、计算
我们统一的把mysql与elasticsearch的概念做一下对比:
MySQL Elasticsearch 说明 Table Index 索引(index),就是文档的集合,类似数据库的表(table) Row Document 文档(Document),就是一条条的数据,类似数据库中的行(Row),文档都是JSON格式 Column Field 字段(Field),就是JSON文档中的字段,类似数据库中的列(Column) Schema Mapping Mapping(映射)是索引中文档的约束,例如字段类型约束。类似数据库的表结构(Schema) SQL DSL DSL是elasticsearch提供的JSON风格的请求语句,用来操作elasticsearch,实现CRUD 在企业中,往往是两者结合使用:
- 对安全性要求较高的写操作,使用mysql实现
- 对查询性能要求较高的搜索需求,使用elasticsearch实现
- 两者再基于某种方式,实现数据的同步,保证一致性
3. IK分词器
分词器的作用是什么?
- 创建倒排索引时对文档分词
- 用户搜索时,对输入的内容分词
IK分词器有几种模式?
ik_smart
:智能切分,粗粒度ik_max_word
:最细切分,细粒度
IK分词器如何拓展词条?如何停用词条?
- 利用config目录的IkAnalyzer.cfg.xml文件添加拓展词典和停用词典
- 在词典中添加拓展词条或者停用词条
IK分词器包含两种模式:
ik_smart
:最少切分ik_max_word
:最细切分
在kibana的Dev tools中输入以下代码:
”analyzer“ 就是选择分词器模式
1 | GET /_analyze |
结果:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68{
"tokens" : [
{
"token" : "黑马",
"start_offset" : 0,
"end_offset" : 2,
"type" : "CN_WORD",
"position" : 0
},
{
"token" : "程序员",
"start_offset" : 2,
"end_offset" : 5,
"type" : "CN_WORD",
"position" : 1
},
{
"token" : "程序",
"start_offset" : 2,
"end_offset" : 4,
"type" : "CN_WORD",
"position" : 2
},
{
"token" : "员",
"start_offset" : 4,
"end_offset" : 5,
"type" : "CN_CHAR",
"position" : 3
},
{
"token" : "学习",
"start_offset" : 5,
"end_offset" : 7,
"type" : "CN_WORD",
"position" : 4
},
{
"token" : "java",
"start_offset" : 7,
"end_offset" : 11,
"type" : "ENGLISH",
"position" : 5
},
{
"token" : "太棒了",
"start_offset" : 11,
"end_offset" : 14,
"type" : "CN_WORD",
"position" : 6
},
{
"token" : "太棒",
"start_offset" : 11,
"end_offset" : 13,
"type" : "CN_WORD",
"position" : 7
},
{
"token" : "了",
"start_offset" : 13,
"end_offset" : 14,
"type" : "CN_CHAR",
"position" : 8
}
]
}
扩展词词典
随着互联网的发展,“造词运动”也越发的频繁。出现了很多新的词语,在原有的词汇列表中并不存在。比如:“奥力给”,“白嫖” 等。
所以我们的词汇也需要不断的更新,IK分词器提供了扩展词汇的功能。
1)打开IK分词器config目录:
2)在IKAnalyzer.cfg.xml配置文件内容添加:
1 |
|
3)新建一个 ext.dic,可以参考config目录下复制一个配置文件进行修改
1 | 白嫖 |
4)重启elasticsearch
1 | docker restart es |
日志中已经成功加载ext.dic配置文件
5)测试效果:
1 | GET /_analyze |
注意当前文件的编码必须是 UTF-8 格式,严禁使用Windows记事本编辑
停用词词典
在互联网项目中,在网络间传输的速度很快,所以很多语言是不允许在网络上传递的,如:关于宗教、政治等敏感词语,那么我们在搜索时也应该忽略当前词汇。
IK分词器也提供了强大的停用词功能,让我们在索引时就直接忽略当前的停用词汇表中的内容。
1)IKAnalyzer.cfg.xml配置文件内容添加:
1 |
|
3)在 stopword.dic 添加停用词
1 | 大帅逼 |
4)重启elasticsearch
1 | # 重启服务 |
日志中已经成功加载stopword.dic配置文件
5)测试效果:
1 | GET /_analyze |
注意当前文件的编码必须是 UTF-8 格式,严禁使用Windows记事本编辑
4. 索引库操作
索引库就类似数据库表,mapping映射就类似表的结构。
我们要向es中存储数据,必须先创建“库”和“表”。
Mapping映射属性
mapping是对索引库中文档的约束,常见的mapping属性包括:
type:字段数据类型,常见的简单类型有:
字符串:text(可分词的文本)、keyword(精确值,例如:品牌、国家、ip地址)
keyword类型只能整体搜索,不支持搜索部分内容
数值:long、integer、short、byte、double、float、
布尔:boolean
日期:date
对象:object
index:是否创建索引,默认为true
实际上不是每个字段都需要创建倒排索引进行搜索,比如邮箱,商品图片的url地址类似字段不需要参与搜索,具体需要看业务需求,如果不参与,需要设置为false
analyzer:使用哪种分词器
properties:该字段的子字段
例如下面的json文档:
1 | { |
对应的每个字段映射(mapping):
- age:类型为 integer;参与搜索,因此需要index为true;无需分词器
- weight:类型为float;参与搜索,因此需要index为true;无需分词器
- isMarried:类型为boolean;参与搜索,因此需要index为true;无需分词器
- info:类型为字符串,需要分词,因此是text;参与搜索,因此需要index为true;分词器可以用ik_smart
- email:类型为字符串,但是不需要分词,因此是keyword;不参与搜索,因此需要index为false;无需分词器
- score:虽然是数组,但是我们只看元素的类型,类型为float;参与搜索,因此需要index为true;无需分词器
- name:类型为object,需要定义多个子属性
- name.firstName;类型为字符串,但是不需要分词,因此是keyword;参与搜索,因此需要index为true;无需分词器
- name.lastName;类型为字符串,但是不需要分词,因此是keyword;参与搜索,因此需要index为true;无需分词器
索引库 CRUD
CRUD简单描述:
- 创建索引库:PUT /索引库名
- 查询索引库:GET /索引库名
- 删除索引库:DELETE /索引库名
- 修改索引库(添加字段):PUT /索引库名/_mapping
这里统一使用Kibana编写DSL的方式来演示。
1. 创建索引库和映射
基本语法:
- 请求方式:PUT
- 请求路径:/索引库名,可以自定义
- 请求参数:mapping映射
格式:
1 | PUT /索引库名称 |
示例:
1 | PUT /conan |
2. 查询索引库
基本语法:
- 请求方式:GET
- 请求路径:/索引库名
- 请求参数:无
格式:
1 | GET /索引库名 |
示例:
3. 修改索引库
这里的修改是只能增加新的字段到mapping中
倒排索引结构虽然不复杂,但是一旦数据结构改变(比如改变了分词器),就需要重新创建倒排索引,这简直是灾难。因此索引库一旦创建,无法修改mapping。
虽然无法修改mapping中已有的字段,但是却允许添加新的字段到mapping中,因为不会对倒排索引产生影响。
语法说明:
1 | PUT /索引库名/_mapping |
示例:
4. 删除索引库
语法:
- 请求方式:DELETE
- 请求路径:/索引库名
- 请求参数:无
格式:
1 | DELETE /索引库名 |
在kibana中测试:
5. 文档操作
1. 新增文档
1 | POST /索引库名/_doc/文档id |
示例:
1 | copy |
响应:
2. 查询文档
根据rest风格,新增是post,查询应该是get,不过查询一般都需要条件,这里我们把文档id带上。
语法:
1 | GET /{索引库名称}/_doc/{id} |
通过kibana查看数据:
1 | GET /heima/_doc/1 |
查看结果:
3. 删除文档
删除使用DELETE请求,同样,需要根据id进行删除:
语法:
1 | DELETE /{索引库名}/_doc/id值 |
示例:
1 | # 根据id删除数据 |
结果:
4. 修改文档
修改有两种方式:
- 全量修改:直接覆盖原来的文档
- 增量修改:修改文档中的部分字段
4.1 全量修改
全量修改是覆盖原来的文档,其本质是:
跟新增类似,只不过POST改为了PUT
- 根据指定的id删除文档
- 新增一个相同id的文档
注意:如果根据id删除时,id不存在,第二步的新增也会执行,也就从修改变成了新增操作了。
语法:
1 | PUT /{索引库名}/_doc/文档id |
示例:
1 | PUT /heima/_doc/1 |
4.2 增量修改
增量修改是只修改指定id匹配的文档中的部分字段。
语法:
1 | POST /{索引库名}/_update/文档id |
示例:
1 | copy |
6. RestClient 操作索引库
ES官方提供了各种不同语言的客户端,用来操作ES。这些客户端的本质就是组装DSL语句,通过http请求发送给ES。官方文档地址:https://www.elastic.co/guide/en/elasticsearch/client/index.html
其中的Java Rest Client又包括两种:
- Java Low Level Rest Client
- Java High Level Rest Client
![[Pasted image 20241223190523.png]]
![[Pasted image 20241223190602.png]]
我们使用的是Java HighLevel Rest Client客户端API
API操作索引库
JavaRestClient操作elasticsearch的流程基本类似。核心是client.indices()方法来获取索引库的操作对象。
索引库操作的基本步骤:【可以根据发送请求那步的第一个参数,发过来判断需要创建什么XXXXRequest】
- 初始化
RestHighLevelClient
- 创建
XxxIndexRequest
。XXX是Create、Get、Delete- 准备DSL( Create时需要,其它是无参)
- 发送请求。调用
RestHighLevelClient#indices().xxx()
方法,xxx是create、exists、delete
1. mapping映射分析
根据MySQL数据库表结构(建表语句),去写索引库结构JSON。表和索引库一一对应
注意:地理坐标、组合字段。索引库里的地理坐标是一个字段:
坐标:维度,精度
。copy_to组合字段作用是供用户查询(输入关键字可以查询多个字段)
创建索引库,最关键的是mapping映射,而mapping映射要考虑的信息包括:
- 字段名
- 字段数据类型
- 是否参与搜索
- 是否需要分词
- 如果分词,分词器是什么?
其中:
- 字段名、字段数据类型,可以参考数据表结构的名称和类型
- 是否参与搜索要分析业务来判断,例如图片地址,就无需参与搜索
- 是否分词呢要看内容,内容如果是一个整体就无需分词,反之则要分词
- 分词器,我们可以统一使用ik_max_word
来看下酒店数据的索引库结构:
1 | PUT /hotel |
几个特殊字段说明:
- location:地理坐标,里面包含精度、纬度
- all:一个组合字段,其目的是将多字段的值 利用
copy_to
合并,提供给用户搜索
地理坐标说明:
![[Pasted image 20241223194718.png]]
copy_to说明:
![[Pasted image 20241223194725.png]]
在 Elasticsearch 中,copy_to
是一个有用的字段属性,用于将字段的内容复制到其他字段,通常是一个专门用于全文搜索的字段。它的主要作用包括:
copy_to
的作用仅仅是在索引过程中复制内容到目标字段用于倒排索引,而不会在存储过程中创建真实的字段值。
- 聚合多个字段进行全文搜索
- 如果你想对多个字段(如
name
、brand
、city
)进行统一的全文搜索,而不想分别对每个字段查询,可以使用copy_to
将这些字段的内容复制到一个单独的字段(如all
)。 - 查询时只需要对
all
字段进行查询,简化了查询逻辑。
- 如果你想对多个字段(如
- 提高查询效率
- 如果对多个字段进行
OR
查询,性能可能较低。 - 将多个字段合并到
all
中后,只需对单个字段进行查询,可以显著提升查询效率。
- 如果对多个字段进行
- 灵活性
- 使用
copy_to
,你仍然可以单独查询原始字段,同时还能使用聚合字段(如all
)进行更复杂的查询。
注意事项
- 使用
copy_to
不会影响原字段的存储和索引:- 原字段仍然可以正常索引和查询,
copy_to
只是额外生成一个字段用于特定用途。
- 原字段仍然可以正常索引和查询,
- 增加了索引存储成本:
- 被复制的内容会占用更多的存储空间,因为它在目标字段中会被重复存储。
2. 初始化RestClient
在elasticsearch提供的API中,与elasticsearch一切交互都封装在一个名为RestHighLevelClient的类中,必须先完成这个对象的初始化,建立与elasticsearch的连接。
分为三步:
1)引入es的RestHighLevelClient依赖:
1 | <dependency> |
2)因为SpringBoot默认的ES版本是7.6.2,所以我们需要覆盖默认的ES版本:
1 | <properties> |
![[Pasted image 20241223200821.png]]
3)初始化RestHighLevelClient:这里一般在启动类或者配置类里注入该Bean,用于告诉Java 访问ES的ip地址
初始化的代码如下:
1 |
|
这里为了单元测试方便,我们创建一个测试类HotelIndexTest
,然后将初始化的代码编写在@BeforeEach
方法中:
1 | package cn.itcast.hotel; |
- **
@BeforeEach
**:为每个测试方法运行前的初始化提供支持。 - **
@AfterEach
**:确保每个测试方法运行后正确清理资源。它们用于在测试方法之间初始化和关闭 Elasticsearch 客户端,保证连接资源的正确管理。
3. Client 索引库CRUD
索引库操作的基本步骤
- 初始化
RestHighLevelclient
- 创建
XxxIndexRequest
。XXX是CREATE、Get、Delete
- 准备
DSL
(CREATE时需要) - 发送请求。调用
RestHighLevelClient#indices().xxx()
方法 xxx是create、exists、delete
3.1 创建索引库
代码分为三步:
1)创建Request对象。因为是创建索引库的操作,因此Request是CreateIndexRequest。
2)添加请求参数,其实就是DSL的JSON参数部分。因为json字符串很长,这里是定义了静态字符串常量MAPPING_TEMPLATE,让代码看起来更加优雅。
3)发送请求,client.indices()方法的返回值是IndicesClient类型,封装了所有与索引库操作有关的方法。
创建索引库的API如下:
![[Pasted image 20241223200121.png]]
代码:
在hotel-demo的cn.itcast.hotel.constants包下,创建一个类,定义mapping映射的JSON字符串常量:
1 | package cn.itcast.hotel.constants; |
在hotel-demo中的HotelIndexTest测试类中,编写单元测试,实现创建索引:
1 |
|
3.2 删除索引库
三步走:
- 1)创建Request对象。这次是DeleteIndexRequest对象
- 2)准备参数。这里是无参
- 3)发送请求。改用delete方法
删除索引库的DSL语句非常简单:在hotel-demo中的HotelIndexTest测试类中,编写单元测试,实现删除索引:1
DELETE /hotel
1
2
3
4
5
6
7
void testDeleteHotelIndex() throws IOException {
// 1.创建Request对象
DeleteIndexRequest request = new DeleteIndexRequest("hotel");
// 2.发送请求
client.indices().delete(request, RequestOptions.DEFAULT);
}
3.3 查询索引库
三步走:
- 1)创建Request对象。这次是GetIndexRequest对象
- 2)准备参数。这里是无参
- 3)发送请求。改用exists方法
判断索引库是否存在,本质就是查询,对应的DSL是:1
判断索引库是否存在,本质就是查询,对应的DSL是:
1
2
3
4
5
6
7
8
9
void testExistsHotelIndex() throws IOException {
// 1.创建Request对象
GetIndexRequest request = new GetIndexRequest("hotel");
// 2.发送请求
boolean exists = client.indices().exists(request, RequestOptions.DEFAULT);
// 3.输出
System.err.println(exists ? "索引库已经存在!" : "索引库不存在!");
}
API操作文档
文档操作的基本步骤:
- 初始化
RestHighLevelclient
- 创建
XxxRequest
。XXX是Index、Get、Update、Delete
- 准备参数(Index和Update时需要)
- 发送请求。调用
RestHighLevelClient#.xxx()
方法,xxx是index、get、update、delete - 解析结果(Get时需要)
新增文档
![[Pasted image 20241223203317.png]]
这里操作文档,直接index()
即可,前面索引库需要indices()
查询文档
![[Pasted image 20241223204633.png]]
修改文档
![[Pasted image 20241223205459.png]]
删除文档
![[Pasted image 20241223210308.png]]
批量导入文档
三步走:
- 1)创建Request对象。这里是BulkRequest
- 2)准备参数。批处理的参数,就是其它Request对象,这里就是多个IndexRequest
- 3)发起请求。这里是批处理,调用的方法为client.bulk()方法
案例需求:利用BulkRequest批量将数据库数据导入到索引库中。
步骤如下:
- 利用mybatis-plus查询酒店数据
- 将查询到的酒店数据(Hotel)转换为文档类型数据(HotelDoc)
- 利用
JavaRestClient
中的BulkRequest
批处理,实现批量新增文档
语法说明:
批量处理BulkRequest,其本质就是将多个普通的CRUD请求组合在一起发送。
其中提供了一个add方法,用来添加其他请求:
![[Pasted image 20241223203057.png]]
可以看到,能添加的请求包括:
- IndexRequest,也就是新增
- UpdateRequest,也就是修改
- DeleteRequest,也就是删除
因此Bulk中添加了多个IndexRequest,就是批量新增功能了。示例:
![[Pasted image 20241223212143.png]]
我们在导入酒店数据时,将上述代码改造成for循环处理即可。
在hotel-demo的HotelDocumentTest测试类中,编写单元测试:
1 |
|
7. ES 搜索引擎
elasticsearch的查询依然是基于JSON风格的DSL来实现的。
7.1. DSL设置查询条件
7.1.1 DSL查询分类
Elasticsearch提供了基于JSON的DSL(Domain Specific Language)来定义查询。常见的查询类型包括:
查询所有:查询出所有数据,_一般测试用_。例如:match_all
全文检索(full text)查询:利用分词器对用户输入内容分词,然后去倒排索引库中匹配。例如:
- match_query
- multi_match_query
精确查询:根据精确词条值查找数据,一般是查找keyword、数值、日期、boolean等类型字段。例如:
- ids
- range
- term
地理(geo)查询:根据经纬度查询。例如:
- geo_distance
- geo_bounding_box
复合(compound)查询:复合查询可以将上述各种查询条件组合起来,合并查询条件。例如:
- bool
- function_score
查询的语法基本一致:
1 | GET /indexName/_search |
我们以查询所有为例,其中:
- 查询类型为match_all
- 没有查询条件![[Pasted image 20241223213814.png]]
1
2
3
4
5
6
7
8// 查询所有
GET /indexName/_search
{
"query": {
"match_all": {
}
}
}查询了所有,返回了所有的数据,可以看见
total
里面的value
的值,同时所有的数据都是存放在一个叫hits
的json
数组中,代表命中的数据
其它查询无非就是查询类型、查询条件的变化。
7.1.2 全文检索查询
match和multi_match的区别是什么?
match:根据一个字段查询【推荐:使用copy_to构造all字段】
multi_match:根据多个字段查询,参与查询字段越多,查询性能越差注:搜索字段越多,对查询性能影响越大,因此建议采用copy_to,然后单字段查询的方式。
7.1.2.1 使用场景
全文检索查询的基本流程如下:
- 对用户搜索的内容做分词,得到词条
- 根据词条去倒排索引库中匹配,得到文档id
- 根据文档id找到文档,返回给用户
比较常用的场景包括:
- 商城的输入框搜索
- 百度输入框搜索
例如京东:
![[Pasted image 20241223214146.png]]
因为是拿着词条去匹配,因此参与搜索的字段也必须是可分词的text类型的字段。
常见的全文检索查询包括:
- match 查询:单字段查询
- multi_match 查询:多字段查询,任意一个字段符合条件就算符合查询条件
7.1.2.2 match查询
match查询语法如下:
1 | GET /indexName/_search |
这里使用的all虽然看不见,但是在之前创建的时候,对一些字段用了copy_to,所以all在倒排索
表中是存在的。
![[Pasted image 20241223214257.png]]
每条数据会有一个score,也就是匹配的分数,分数越高,越靠前
7.1.2.3 mulit_match查询
mulit_match语法如下:
1 | GET /indexName/_search |
multi_match查询示例:
![[Pasted image 20241223214821.png]]
7.1.3 精准查询
精准查询类型:
term查询:根据词条精确匹配,一般搜索keyword类型、数值类型、布尔类型、日期类型字段
range查询:根据数值范围查询,可以是数值、日期的范围
精确查询一般是查找keyword、数值、日期、boolean等类型字段。所以不会对搜索条件分词。常见的有:
- term:根据词条精确值查询
- range:根据值的范围查询
1.3.1 term查询
因为精确查询的字段搜时不分词的字段,因此查询的条件也必须是不分词的词条。查询时,用户输入的内容跟自动值完全匹配时才认为符合条件。如果用户输入的内容过多,反而搜索不到数据。
语法说明:
1 | // term查询 |
示例:
当我搜索的是精确词条时,能正确查询出结果:
![[Pasted image 20241223214924.png]]
但是,当我搜索的内容不是词条,而是多个词语形成的短语时,反而搜索不到:
![[Pasted image 20241223214931.png]]
1.3.2 range查询
范围查询,一般应用在对数值类型做范围过滤的时候。比如做价格范围过滤。
基本语法:
1 | // range查询 |
示例:
![[Pasted image 20241223215339.png]]
7.1.4 地理坐标查询
所谓的地理坐标查询,其实就是根据经纬度查询,官方文档:https://www.elastic.co/guide/en/elasticsearch/reference/current/geo-queries.html
常见的使用场景包括:
- 携程:搜索我附近的酒店
- 滴滴:搜索我附近的出租车
- 微信:搜索我附近的人
附近的酒店:
![[Pasted image 20241223215623.png]]
附近的车:
![[Pasted image 20241223215633.png]]
1.4.1 矩形范围查询
很少有业务有这种需求
矩形范围查询,也就是geo_bounding_box查询,查询坐标落在某个矩形范围的所有文档:
查询时,需要指定矩形的左上、右下两个点的坐标,然后画出一个矩形,落在该矩形内的都是符合条件的点。
语法如下:
1 | // geo_bounding_box查询 |
1.4.2 附近(圆形)查询
附近查询,也叫做距离查询(geo_distance):查询到指定中心点小于某个距离值的所有文档。
换句话来说,在地图上找一个点作为圆心,以指定距离为半径,画一个圆,落在圆内的坐标都算符合条件:
![[Pasted image 20241223215847.png]]
语法说明:
1 | // geo_distance 查询 |
示例:
我们先搜索陆家嘴附近15km的酒店:
![[Pasted image 20241223215926.png]]
7.1.5.复合查询
复合(compound)查询:复合查询可以将其它简单查询组合起来,实现更复杂的搜索逻辑。常见的有两种:
- fuction score:算分函数查询,可以控制文档相关性算分,控制文档排名
- bool query:布尔查询,利用逻辑关系组合多个其它的查询,实现复杂搜索
1.5.1.相关性算分
当我们利用match查询时,文档结果会根据与搜索词条的关联度打分(_score),返回结果时按照分值降序排列。
例如,我们搜索 “虹桥如家”,结果如下:
1 |
|
在elasticsearch中,早期使用的打分算法是TF-IDF算法,公式如下
在后来的5.1版本升级中,elasticsearch将算法改进为BM25算法,公式如下:
TF-IDF算法有一各缺陷,就是词条频率越高,文档得分也会越高,单个词条对文档影响较大。而BM25则会让单个词条的算分有一个上限,曲线更加平滑:
小结:elasticsearch会根据词条和文档的相关度做打分,算法由两种:
- TF-IDF算法
- BM25算法,elasticsearch5.1版本后采用的算法
1.5.2.算分函数查询
根据相关度打分是比较合理的需求,但合理的不一定是产品经理需要的。
以百度为例,你搜索的结果中,并不是相关度越高排名越靠前,而是谁掏的钱多排名就越靠前。如图:
要想认为控制相关性算分,就需要利用elasticsearch中的function score 查询了。
1)语法说明
function score 查询中包含四部分内容:
- 原始查询条件:query部分,基于这个条件搜索文档,并且基于BM25算法给文档打分,原始算分(query score)
- 过滤条件:filter部分,符合该条件的文档才会重新算分
- 算分函数:符合filter条件的文档要根据这个函数做运算,得到的函数算分(function score),有四种函数
- weight:函数结果是常量
- field_value_factor:以文档中的某个字段值作为函数结果
- random_score:以随机数作为函数结果
- script_score:自定义算分函数算法 - 运算模式:算分函数的结果、原始查询的相关性算分,两者之间的运算方式,包括:
- multiply:相乘
- replace:用function score替换query score
- 其它,例如:sum、avg、max、min
function score的运行流程如下:
- 1)根据原始条件查询搜索文档,并且计算相关性算分,称为原始算分(query score)
- 2)根据过滤条件,过滤文档
- 3)符合过滤条件的文档,基于算分函数运算,得到函数算分(function score)
- 4)将原始算分(query score)和函数算分(function score)基于运算模式做运算,得到最终结果,作为相关性算分。
因此,其中的关键点是:
- 过滤条件:决定哪些文档的算分被修改
- 算分函数:决定函数算分的算法
- 运算模式:决定最终算分结果
2)示例
需求:给“如家”这个品牌的酒店排名靠前一些
翻译一下这个需求,转换为之前说的四个要点:
- 原始条件:不确定,可以任意变化
- 过滤条件:brand = “如家”
- 算分函数:可以简单粗暴,直接给固定的算分结果,weight
- 运算模式:比如求和
因此最终的DSL语句如下:
1 | GET /hotel/_search |
测试,在未添加算分函数时,如家得分如下:
添加了算分函数后,如家得分就提升了:
3)小结
function score query定义的三要素是什么?
- 过滤条件:哪些文档要加分
- 算分函数:如何计算function score
- 加权方式:function score 与 query score如何运算
1.5.3.布尔查询
布尔查询是一个或多个查询子句的组合,每一个子句就是一个子查询。子查询的组合方式有:
- must:必须匹配每个子查询,类似“与”
- should:选择性匹配子查询,类似“或”
- must_not:必须不匹配,不参与算分,类似“非”
- filter:必须匹配,不参与算分
比如在搜索酒店时,除了关键字搜索外,我们还可能根据品牌、价格、城市等字段做过滤:
每一个不同的字段,其查询的条件、方式都不一样,必须是多个不同的查询,而要组合这些查询,就必须用bool查询了。
需要注意的是,搜索时,参与打分的字段越多,查询的性能也越差。因此这种多条件查询时,建议这样做:
- 搜索框的关键字搜索,是全文检索查询,使用must查询,参与算分
- 其它过滤条件,采用filter查询。不参与算分
1)语法示例:
1 |
|
2)示例
题目需求:搜索名字包含“如家”,价格不高于400,在坐标31.21,121.5周围10km范围内的酒店。
分析:
- 名称搜索,属于全文检索查询,应该参与算分。放到must中
- 价格不高于400,用range查询,属于过滤条件,不参与算分。放到must_not中
- 周围10km范围内,用geo_distance查询,属于过滤条件,不参与算分。放到filter中
3)小结bool查询有几种逻辑关系?
- must:必须匹配的条件,可以理解为“与”
- should:选择性匹配的条件,可以理解为“或”
- must_not:必须不匹配的条件,不参与打分
- filter:必须匹配的条件,不参与打分
8.搜索结果处理
搜索的结果可以按照用户指定的方式去处理或展示。
8.1.排序
elasticsearch默认是根据相关度算分(_score)来排序,但是也支持自定义方式对搜索结果排序。可以排序字段类型有:keyword类型、数值类型、地理坐标类型、日期类型等。
8.1.1.普通字段排序
keyword、数值、日期类型排序的语法基本一致。
语法:
1 | GET /indexName/_search |
排序条件是一个数组,也就是可以写多个排序条件。按照声明的顺序,当第一个条件相等时,再按照第二个条件排序,以此类推
示例:
需求描述:
酒店数据按照用户评价(score)降序排序,评价相同的按照价格(price)升序排序
8.1.2.地理坐标排序
地理坐标排序略有不同。
语法说明:
1 | GET /indexName/_search |
这个查询的含义是:
- 指定一个坐标,作为目标点
- 计算每一个文档中,指定字段(必须是geo_point类型)的坐标 到目标点的距离是多少
- 根据距离排序
示例:
需求描述:实现对酒店数据按照到你的位置坐标的距离升序排序
提示:
获取你的位置的经纬度的方式:
https://lbs.amap.com/demo/jsapi-v2/example/map/click-to-get-lnglat/
假设我的位置是:31.034661,121.612282,寻找我周围距离最近的酒店。
排序之后不打分,因为打分无意义
8.2.分页
elasticsearch 默认情况下只返回top10的数据。而如果要查询更多数据就需要修改分页参数了。elasticsearch中通过修改from、size参数来控制要返回的分页结果:
- from:从第几个文档开始
- size:总共查询几个文档
类似于mysql中的limit ?, ?
8.2.1.基本的分页
分页的基本语法如下:
1 | GET /hotel/_search |
8.2.2.深度分页问题
现在,我要查询990~1000的数据,查询逻辑要这么写:
1 | GET /hotel/_search |
这里是查询990开始的数据,也就是 第990第1000条 数据。1000条,然后截取其中的990 ~ 1000的这10条**:
不过,elasticsearch内部分页时,**必须先查询 0
查询TOP1000,如果es是单点模式,这并无太大影响。
但是elasticsearch将来一定是集群,例如我集群有5个节点,我要查询TOP1000的数据,并不是每个节点查询200条就可以了。
因为节点A的TOP200,在另一个节点可能排到10000名以外了。
因此要想获取整个集群的TOP1000,必须先查询出每个节点的TOP1000,汇总结果后,重新排名,重新截取TOP1000。
那如果我要查询9900~10000的数据呢?是不是要先查询 TOP 10000 呢?那每个节点都要查询10000条?汇总到内存中?
当查询分页深度较大时,汇总数据过多,对内存和CPU会产生非常大的压力,因此elasticsearch会禁止from+ size 超过10000的请求。
针对深度分页,ES提供了两种解决方案,官方文档:
- search after:分页时需要排序,原理是从上一次的排序值开始,查询下一页数据。官方推荐使用的方式。
- scroll:原理将排序后的文档id形成快照,保存在内存。官方已经不推荐使用。
8.2.3.小结
分页查询的常见实现方案以及优缺点:
from + size
:
- 优点:支持随机翻页
- 缺点:深度分页问题,默认查询上限(from + size)是10000
- 场景:百度、京东、谷歌、淘宝这样的随机翻页搜索after search
:
- 优点:没有查询上限(单次查询的size不超过10000)
- 缺点:只能向后逐页查询,不支持随机翻页
- 场景:没有随机翻页需求的搜索,例如手机向下滚动翻页scroll
:
- 优点:没有查询上限(单次查询的size不超过10000)
- 缺点:会有额外内存消耗,并且搜索结果是非实时的
- 场景:海量数据的获取和迁移。从ES7.1开始不推荐,建议用 after search方案。
8.3.高亮
8.3.1.高亮原理
什么是高亮显示呢?
我们在百度,京东搜索时,关键字会变成红色,比较醒目,这叫高亮显示:
高亮显示的实现分为两步:
- 1)给文档中的所有关键字都添加一个标签,例如
<em>
标签 - 2)页面给
<em>
标签编写CSS样式
2.3.2.实现高亮
高亮的语法:
1 | GET /hotel/_search |
注意:
- 高亮是对关键字高亮,因此搜索条件必须带有关键字,而不能是范围这样的查询。
- 默认情况下,高亮的字段,必须与搜索指定的字段一致,否则无法高亮
- 如果要对非搜索字段高亮,则需要添加一个属性:
required_field_match=false
示例:
8.4.总结
查询的DSL是一个大的JSON对象,包含下列属性:
query:查询条件
from和size:分页条件
sort:排序条件
highlight:高亮条件
示例:
9.RestClient查询文档
文档的查询同样适用昨天学习的 RestHighLevelClient
对象,基本步骤包括:
- 1)准备Request对象
- 2)准备请求参数
- 3)发起请求
- 4)解析响应
9.1.快速入门
我们以match_all查询为例
9.1.1.发起查询请求
代码解读:
- 创建
SearchRequest
对象,指定索引库名 - 利用
request.source()
构建DSL,DSL中可以包含查询、分页、排序、高亮等
-query()
:代表查询条件,利用QueryBuilders.matchAllQuery()
构建一个match_all查询的DSL - 利用client.search()发送请求,得到响应
这里关键的API有两个,一个是request.source()
,其中包含了查询、排序、分页、高亮等所有功能:
另一个是QueryBuilders
,其中包含match、term、function_score、bool等各种查询:
9.1.2.解析响应
响应结果的解析:
elasticsearch返回的结果是一个JSON字符串,结构包含:
hits
:命中的结果
-total
:总条数,其中的value是具体的总条数值
-max_score
:所有结果中得分最高的文档的相关性算分
-hits
:搜索结果的文档数组,其中的每个文档都是一个json对象
-_source
:文档中的原始数据,也是json对象
因此,我们解析响应结果,就是逐层解析JSON字符串,流程如下:
SearchHits
:通过response.getHits()获取,就是JSON中的最外层的hits,代表命中的结果
-SearchHits#getTotalHits().value
:获取总条数信息
-SearchHits#getHits()
:获取SearchHit数组,也就是文档数组
-SearchHit#getSourceAsString()
:获取文档结果中的_source,也就是原始的json文档数据
9.1.3.完整代码
完整代码如下:
1 |
|
9.1.4.小结
查询的基本步骤是:
- 创建SearchRequest对象
- 准备Request.source(),也就是DSL。
① QueryBuilders来构建查询条件
② 传入Request.source() 的 query() 方法 - 发送请求,得到结果
- 解析结果(参考JSON结果,从外到内,逐层解析)
9.2.match查询
全文检索的match和multi_match查询与match_all的API基本一致。差别是查询条件,也就是query的部分。
因此,Java代码上的差异主要是request.source().query()中的参数了。同样是利用QueryBuilders提供的方法:
而结果解析代码则完全一致,可以抽取并共享。
完整代码如下:
1 |
|
9.3.精确查询
精确查询主要是两者:
- term:词条精确匹配
- range:范围查询
与之前的查询相比,差异同样在查询条件,其它都一样。
查询条件构造的API如下:
9.4.布尔查询
布尔查询是用must、must_not、filter等方式组合其它查询,代码示例如下:
可以看到,API与其它查询的差别同样是在查询条件的构建,QueryBuilders,结果解析等其他代码完全不变。
完整代码如下:
1 |
|
9.5.排序、分页
搜索结果的排序和分页是与query
同级的参数,因此同样是使用request.source()
来设置。
对应的API如下:
完整代码示例:
1 |
|
9.6.高亮
高亮的代码与之前代码差异较大,有两点:
- 查询的DSL:其中除了查询条件,还需要添加高亮条件,同样是与query同级。
- 结果解析:结果除了要解析_source文档数据,还要解析高亮结果
9.6.1.高亮请求构建
高亮请求的构建API如下:
上述代码省略了查询条件部分,但是大家不要忘了:
高亮查询必须使用全文检索查询,并且要有搜索关键字,将来才可以对关键字高亮。
完整代码如下:
1 |
|
9.6.2.高亮结果解析
高亮的结果与查询的文档结果默认是分离的,并不在一起。
因此解析高亮的代码需要额外处理:
代码解读:
- 第一步:从结果中获取source。
hit.getSourceAsString()
,这部分是非高亮结果,json字符串。还需要反序列为HotelDoc对象 - 第二步:获取高亮结果。
hit.getHighlightFields()
,返回值是一个Map,key是高亮字段名称,值是HighlightField对象,代表高亮值 - 第三步:从map中根据高亮字段名称,获取高亮字段值对象HighlightField
- 第四步:从HighlightField中获取Fragments,并且转为字符串。这部分就是真正的高亮字符串了
- 第五步:用高亮的结果替换HotelDoc中的非高亮结果
完整代码如下:
1 | private void handleResponse(SearchResponse response) { |
10.旅游案例
下面,我们通过旅游的案例来实战演练下之前学习的知识。
我们实现四部分功能:
- 酒店搜索和分页
- 酒店结果过滤
- 我周边的酒店
- 酒店竞价排名
启动我们提供的hotel-demo项目,其默认端口是8089,访问http://localhost:8090,就能看到项目页面了:
10.1.酒店搜索和分页
案例需求:实现黑马旅游的酒店搜索功能,完成关键字搜索和分页
10.1.1.需求分析
在项目的首页,有一个大大的搜索框,还有分页按钮:
点击搜索按钮,可以看到浏览器控制台发出了请求:
请求参数如下:
由此可以知道,我们这个请求的信息如下:
- 请求方式:POST
- 请求路径:/hotel/list
- 请求参数:JSON对象,包含4个字段:
- key:搜索关键字
- page:页码
- size:每页大小
- sortBy:排序,目前暂不实现 - 返回值:分页查询,需要返回分页结果PageResult,包含两个属性:
-total
:总条数
-List<HotelDoc>
:当前页的数据
因此,我们实现业务的流程如下: - 步骤一:定义实体类,接收请求参数的JSON对象
- 步骤二:编写controller,接收页面的请求
- 步骤三:编写业务实现,利用RestHighLevelClient实现搜索、分页
10.1.2.定义实体类
实体类有两个,一个是前端的请求参数实体,一个是服务端应该返回的响应结果实体。
1)请求参数
前端请求的json结构如下:
1 | { |
因此,我们在cn.itcast.hotel.pojo
包下定义一个实体类:
1 | package cn.itcast.hotel.pojo; |
2)返回值
分页查询,需要返回分页结果PageResult,包含两个属性:
total
:总条数List<HotelDoc>
:当前页的数据
因此,我们在cn.itcast.hotel.pojo
中定义返回结果:title:PageResult 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17package cn.itcast.hotel.pojo;
import lombok.Data;
import java.util.List;
public class PageResult {
private Long total;
private List<HotelDoc> hotels;
public PageResult() {
}
public PageResult(Long total, List<HotelDoc> hotels) {
this.total = total;
this.hotels = hotels;
}
}
10.1.3.定义controller
定义一个HotelController,声明查询接口,满足下列要求:
- 请求方式:Post
- 请求路径:/hotel/list
- 请求参数:对象,类型为RequestParam
- 返回值:PageResult,包含两个属性
-Long total
:总条数
-List<HotelDoc> hotels
:酒店数据
因此,我们在cn.itcast.hotel.web
中定义HotelController:
1 |
|
10.1.4.实现搜索业务
我们在controller调用了IHotelService,并没有实现该方法,因此下面我们就在IHotelService中定义方法,并且去实现业务逻辑。
1)在cn.itcast.hotel.service
中的IHotelService
接口中定义一个方法:
1 | /** |
2)实现搜索业务,肯定离不开RestHighLevelClient,我们需要把它注册到Spring中作为一个Bean。在cn.itcast.hotel
中的HotelDemoApplication
中声明这个Bean:
1 |
|
3)在cn.itcast.hotel.service.impl
中的HotelService
中实现search方法:
1 |
|
10.2.酒店结果过滤
需求:添加品牌、城市、星级、价格等过滤功能
10.2.1.需求分析
在页面搜索框下面,会有一些过滤项:
传递的参数如图:
包含的过滤条件有:
- brand:品牌值
- city:城市
- minPrice~maxPrice:价格范围
- starName:星级
我们需要做两件事情: - 修改请求参数的对象RequestParams,接收上述参数
- 修改业务逻辑,在搜索条件之外,添加一些过滤条件
10.2.2.修改实体类
修改在cn.itcast.hotel.pojo
包下的实体类RequestParams:
1 |
|
10.2.3.修改搜索业务
在HotelService的search方法中,只有一个地方需要修改:requet.source().query( … )其中的查询条件。
在之前的业务中,只有match查询,根据关键字搜索,现在要添加条件过滤,包括:
- 品牌过滤:是keyword类型,用term查询
- 星级过滤:是keyword类型,用term查询
- 价格过滤:是数值类型,用range查询
- 城市过滤:是keyword类型,用term查询
多个查询条件组合,肯定是boolean查询来组合:
- 关键字搜索放到must中,参与算分
- 其它过滤条件放到filter中,不参与算分
因为条件构建的逻辑比较复杂,这里先封装为一个函数:
buildBasicQuery
的代码如下:
1 | private void buildBasicQuery(RequestParams params, SearchRequest request) { |
10.3.我周边的酒店
需求:我附近的酒店
10.3.1.需求分析
在酒店列表页的右侧,有一个小地图,点击地图的定位按钮,地图会找到你所在的位置:
并且,在前端会发起查询请求,将你的坐标发送到服务端:
我们要做的事情就是基于这个location坐标,然后按照距离对周围酒店排序。实现思路如下:
- 修改RequestParams参数,接收location字段
- 修改search方法业务逻辑,如果location有值,添加根据geo_distance排序的功能
10.3.2.修改实体类
修改在cn.itcast.hotel.pojo
包下的实体类RequestParams:
1 | package cn.itcast.hotel.pojo; |
10.3.3.距离排序API
我们以前学习过排序功能,包括两种:
- 普通字段排序
- 地理坐标排序
我们只讲了普通字段排序对应的java写法。地理坐标排序只学过DSL语法,如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18GET /indexName/_search
{
"query": {
"match_all": {}
},
"sort": [
{
"price": "asc"
},
{
"_geo_distance" : {
"FIELD" : "纬度,经度",
"order" : "asc",
"unit" : "km"
}
}
]
}
对应的java代码示例:
10.3.4.添加距离排序
在cn.itcast.hotel.service.impl
的HotelService
的search
方法中,添加一个排序功能:
完整代码:
1 |
|
10.3.5.排序距离显示
重启服务后,测试我的酒店功能:
发现确实可以实现对我附近酒店的排序,不过并没有看到酒店到底距离我多远,这该怎么办?
排序完成后,页面还要获取我附近每个酒店的具体距离值,这个值在响应结果中是独立的:
因此,我们在结果解析阶段,除了解析source部分以外,还要得到sort部分,也就是排序的距离,然后放到响应结果中。
我们要做两件事:
- 修改HotelDoc,添加排序距离字段,用于页面显示
- 修改HotelService类中的handleResponse方法,添加对sort值的获取
1)修改HotelDoc类,添加距离字段1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34package cn.itcast.hotel.pojo;
import lombok.Data;
import lombok.NoArgsConstructor;
public class HotelDoc {
private Long id;
private String name;
private String address;
private Integer price;
private Integer score;
private String brand;
private String city;
private String starName;
private String business;
private String location;
private String pic;
// 排序时的 距离值
private Object distance;
public HotelDoc(Hotel hotel) {
this.id = hotel.getId();
this.name = hotel.getName();
this.address = hotel.getAddress();
this.price = hotel.getPrice();
this.score = hotel.getScore();
this.brand = hotel.getBrand();
this.city = hotel.getCity();
this.starName = hotel.getStarName();
this.business = hotel.getBusiness();
this.location = hotel.getLatitude() + ", " + hotel.getLongitude();
this.pic = hotel.getPic();
}
}
2)修改HotelService中的handleResponse方法
重启后测试,发现页面能成功显示距离了:
10.4.酒店竞价排名
需求:让指定的酒店在搜索结果中排名置顶
10.4.1.需求分析
要让指定酒店在搜索结果中排名置顶,效果如图:
页面会给指定的酒店添加广告标记。
那怎样才能让指定的酒店排名置顶呢?
我们之前学习过的function_score查询可以影响算分,算分高了,自然排名也就高了。而function_score包含3个要素:
- 过滤条件:哪些文档要加分
- 算分函数:如何计算function score
- 加权方式:function score 与 query score如何运算
这里的需求是:让指定酒店排名靠前。因此我们需要给这些酒店添加一个标记,这样在过滤条件中就可以根据这个标记来判断,是否要提高算分。
比如,我们给酒店添加一个字段:isAD,Boolean类型:
- true:是广告
- false:不是广告
这样function_score包含3个要素就很好确定了: - 过滤条件:判断isAD 是否为true
- 算分函数:我们可以用最简单暴力的weight,固定加权值
- 加权方式:可以用默认的相乘,大大提高算分
因此,业务的实现步骤包括:
- 给HotelDoc类添加isAD字段,Boolean类型
- 挑选几个你喜欢的酒店,给它的文档数据添加isAD字段,值为true
- 修改search方法,添加function score功能,给isAD值为true的酒店增加权重
10.4.2.修改HotelDoc实体
给cn.itcast.hotel.pojo
包下的HotelDoc类添加isAD字段:
10.4.3.添加广告标记
接下来,我们挑几个酒店,添加isAD字段,设置为true:
1 | POST /hotel/_update/1902197537 |
10.4.4.添加算分函数查询
接下来我们就要修改查询条件了。之前是用的boolean 查询,现在要改成function_socre查询。
function_score查询结构如下:
对应的JavaAPI如下:
我们可以将之前写的boolean查询作为原始查询条件放到query中,接下来就是添加过滤条件、算分函数、加权模式了。所以原来的代码依然可以沿用。
在 Elasticsearch 中,function_score
查询是一个非常强大的工具,它允许在执行查询的同时,通过自定义的函数来修改相关文档的得分。它主要分为两大部分:
Query (原始查询)
这是function_score
的核心查询部分,用于筛选出符合条件的文档。可以是任何标准的查询,比如match
,term
,range
, 或bool
查询。这个部分决定了哪些文档会进入后续的打分阶段。Functions (函数部分)
这是自定义的函数,用于基于某些文档字段的值或其他特性(如地理位置、随机值、时间衰减等)来调整文档的相关性分数。通过这些函数,可以对原始查询返回的文档进行进一步的打分优化或排序操作。函数的定义可以是以下几种类型:- field_value_factor: 根据文档字段的值调整分数,比如字段值的线性变换。
- decay functions: 提供距离、时间等的衰减函数,包括
gauss
,exp
,linear
。 - random_score: 用于生成随机排序。
- script_score: 使用脚本灵活地定义计算逻辑。
- weight: 为每个函数赋予一个权重。
修改cn.itcast.hotel.service.impl
包下的HotelService
类中的buildBasicQuery
方法,添加算分函数查询:
1 | private void buildBasicQuery(RequestParams params, SearchRequest request) { |