文章目录
一、Elasticsearch
1.1 简介
官网:https://www.elastic.co/cn/
Elasticsearch简介:Elasticsearch是一个分布式的搜索和分析引擎,位于 Elastic Stack(以前称为 ELK Stack)的核心。它设计用于处理所有类型的数据,包括结构化和非结构化文本、数字数据以及地理空间数据。Elasticsearch 能够提供近乎实时的搜索和分析能力,支持快速搜索和高效存储数据。它的分布式特性使得随着数据和查询量的增长,部署能够无缝扩展。Elasticsearch 使用 Lucene 作为其核心,并通过 RESTful API 提供其功能,支持多租户和各种编程语言的客户端库
在 Elasticsearch 中,数据以文档的形式存储,每个文档都被序列化为 JSON 格式。文档被存储在索引(index)中,索引是相同类型文档的集合。映射(mapping)定义了文档字段的约束,如字段名和数据类型。Elasticsearch 集群由一个或多个节点组成,每个节点是一个 Elasticsearch 实例,通常运行在独立的服务器上。数据在节点之间进行复制和分片以提高可用性和性能。。
1.2 Elasticsearch背景
- 1999年Doug Cutting基于Lucene开发了Lucene Lucene 是 Apache 基金会开发的开源搜索引擎。它是一个 Java 语言的搜索引擎类库,是 Apache 公司的顶级项目。Lucene 可以快速、高效地处理大量的文档,并且提供全文搜索、结构化搜索、Faceted Search、排序、地理空间搜索等功能。
- 2000年Sun公司收购了Apache基金会,并将Lucene作为Sun的基础软件之一。
- 2004年Shay Banon基于Lucene开发了Compass
- 2010年Shay Banon 重写了Compass,取名为Elasticsearch
1.3 什么是ELK Stack?
ELK Stack(Elasticsearch、Logstash、Kibana)是 Elasticsearch、Logstash、Kibana 的简称,是 Elasticsearch、Logstash、Kibana 三者组合而成的开源日志分析工具。
- Elasticsearch:是一个开源的搜索和分析引擎,能够轻松地存储、搜索、分析大量的数据。
- Logstash:是一个开源的数据收集引擎,能够实时地从不同来源采集数据,并将其转化为 Elasticsearch 可以理解的格式。
- Kibana:是一个开源的可视化分析工具,能够帮助用户从 Elasticsearch 中提取数据,并通过图表、表格、地图等方式呈现。
1.4 Elasticsearch优势
- 快速、高效:ES 采用 Lucene 作为其核心,Lucene 是一个开源的全文搜索引擎类库,能够快速、高效地处理大量的数据。 ES 能够快速地搜索、分析数据,并提供近乎实时的搜索和分析能力。
- 全文搜索:ES 提供全文搜索功能,能够对文本数据进行索引,并支持多种查询方式,包括模糊查询、布尔查询、正则表达式查询等。
- 结构化搜索:ES 提供结构化搜索功能,能够对结构化数据进行索引,并支持多种查询方式,包括精确值查询、范围查询、排序、聚合等。
- 地理空间搜索:ES 提供地理空间搜索功能,能够对地理空间数据进行索引,并支持多种查询方式,包括距离查询、地理位置聚合等。
- 高可用性:ES 采用分布式架构,能够在集群中自动分配数据,并提供高可用性。
- 易于扩展:ES 采用 RESTful API 接口,支持多种编程语言的客户端库,能够方便地集成到各种应用系统中。
- 灵活的数据模型:ES 支持多种数据模型,包括文档、字段、对象、嵌套、数组等,能够灵活地存储和索引数据。
- 自动发现:ES 可以自动发现数据中的模式,并对其进行索引,能够对复杂的数据进行快速搜索和分析。
- 易于部署:ES 可以方便地部署和管理,支持多种部署方式,包括 Docker、Kubernetes、云平台等。
1.5 Elasticsearch相关概念
- 集群(Cluster):一个或多个节点(Node)组成的集群,提供分布式的存储、搜索和分析功能。
- 节点(Node):一个服务器,作为集群中的一个成员,存储数据,提供搜索和分析功能。
- 索引(Index):一个逻辑上的数据库,存储数据,类似于关系型数据库中的表。
- 类型(Type):索引的一种逻辑分区,类似于关系型数据库中的表中的字段。
- 文档(Document):一个可被索引的记录,类似于关系型数据库中的行。
- 字段(Field):文档中的一个属性,类似于关系型数据库中的列。
- 映射(Mapping):定义文档的字段和数据类型,类似于关系型数据库中的表结构。
- 路由(Routing):决定将文档路由到哪个分片(Shard)的过程。
- 分片(Shard):一个 Lucene 实例,存储一个或多个索引。
- 副本(Replica):一个 Lucene 实例的复制品,用于提高搜索和分析性能。
- 倒排索引(Inverted Index):一种索引方法,能够快速地检索文档。
- 分析器(Analyzer):用于对文本进行分词、词干提取、停用词过滤等操作的组件。
- 聚合(Aggregation):对搜索结果进行汇总、分析的过程。
- 脚本(Script):一种基于 JavaScript 的语言,用于对文档进行复杂的操作。
- 客户端(Client):与 Elasticsearch 交互的工具,如 Kibana、Logstash、Beats 等。
- 集群管理器(Cluster Manager):用于管理集群的工具,如 Elasticsearch 自带的 Elasticsearch 管理器、Kibana 管理器等。
1.6 倒排索引
倒排索引:倒排索引是一种索引方法,能够快速地检索文档。倒排索引是一种索引方法,它将文档中的每个词条映射到一个或多个文档的列表中。倒排索引的主要作用是快速地检索文档。
理解倒排索引搜索原理:
假设我们有一篇文章,文章中有如下内容:“Elasticsearch 是一款开源的搜索和分析引擎”。
我们将文章中的每个词条都视为一个单词,并将其倒排索引。倒排索引的结构如下:
- 词条:Elasticsearch、是、一款、开源、搜索、分析、引擎
- 文档列表:文章1 当用户搜索“Elasticsearch”时,可以快速地找到包含该词条的文档列表,并返回给用户。
倒排索引中有两个非常重要的概念:
- 文档(Document):一个可被索引的记录,类似于关系型数据库中的行。例如:一篇文章就是一个文档。一个商品信息就是一个文档。
- 词条(Term):对文档数据或用户搜索数据,利用某种算法分词,得到的具备含义的词语就是词条。例如:“我时中国人”,就可以分为:我、是、中国人、中国、国人、国、人这样的几个词条
创建倒排索引是对正向索引的一种特殊处理,流程如下:
- 将每一个文档的数据利用算法分词,得到一个个词条
- 创建表,每行数据包括词条、词条所在文档id、位置等信息
- 因为词条唯一性,可以给词条创建索引,例如hash表结构索引 如图:
倒排索引的搜索流程如下(以搜索"华为手机"为例):
- 输入“华为手机”
- 词条分词:“华为”、“手机”
- 遍历倒排索引表,找到包含“华为”和“手机”的文档列表
- 返回文档列表
虽然要先查询倒排索引,再查询倒排索引,但是无论是词条、还是文档id都建立了索引,查询速度非常快!无需全表扫描。
1.7 文档(Document)
文档是 Elasticsearch 数据库中的基本数据单元。它是一个可序列化的 JSON 对象,用于描述实体(例如用户、产品等)的属性和其对应的值。每个文档都必须属于一个特定的类型,并被存储在相应的索引中。
文档相当于关系型数据库中的行,字段相当于关系型数据库中的列。
elasticsearch是面向文档(Document)存储的,可以是数据库中的一条商品数据,一个订单信息。文档数据会被序列化为json格式后存储在elasticsearch中:
{
"name": "iphone",
"price": 8999,
"description": "这是一个很棒的手机"
}
1.8 字段(Field)
字段是文档的属性,它是一个键值对,用于描述文档的一些特性。字段可以是数字、字符串、布尔值、日期、数组、对象等。
字段的类型决定了字段的值的类型,例如:字符串字段可以存储字符串,数字字段可以存储整数、浮点数等。
字段可以被索引,使得 Elasticsearch 可以对其进行快速的检索。
1.9 索引(Index)
索引是 Elasticsearch 数据库中的逻辑存储单元。它是一个相互关联的文档集合,类似于关系型数据库中的表。索引由一个或多个分片(Shard)组成,每个分片是一个 Lucene 实例,存储一个或多个索引。
索引相当于关系型数据库中的数据库,文档相当于关系型数据库中的表,字段相当于关系型数据库中的列。
索引的名称必须全部小写,并且不能以下划线开头。
索引是文档(Document)的容器,是一类文档的集合。 例如:
- 所有用户文档,就可以组织在一起,称为用户的索引;
- 所有商品的文档,可以组织在一起,称为商品的索引;
- 所有订单的文档,可以组织在一起,称为订单的索引;
因此,我们可以把索引当做是数据库中的表,把文档当做是表中的行,把字段当做是列。
1.10 映射(Mapping)
映射是索引的元数据,它定义了索引的字段名称、类型、是否索引、是否存储等。
当我们创建索引时,必须指定映射,否则 Elasticsearch 不会对文档进行索引。
映射定义了文档的字段名称、类型、是否索引、是否存储等。
类似于关系型数据库中的表结构。
例如:
{
"properties": {
"name": {
"type": "text"
},
"price": {
"type": "float"
},
"description": {
"type": "text"
}
}
}
1.11 ES与MySQL对比
MySQL是关系型数据库,而Elasticsearch是非关系型数据库。
MySQL的优点:
- 结构化数据:MySQL支持结构化数据,可以存储结构化的数据,如表格、关系图等。
- 关系型数据库:MySQL支持关系型数据库,可以存储和处理关系型数据,如表、关系等。
- 事务处理:MySQL支持事务处理,可以确保数据的一致性。
- 完整性:MySQL支持完整性约束,可以确保数据的准确性。
- 性能:MySQL的性能非常高,可以处理大量的数据。
Elasticsearch的优点:
- 灵活的数据模型:Elasticsearch支持多种数据模型,如文档、字段、对象、嵌套、数组等,可以灵活地存储和索引数据。
- 全文搜索:Elasticsearch支持全文搜索,可以对文本数据进行索引,并支持多种查询方式,包括模糊查询、布尔查询、正则表达式查询等。
- 地理空间搜索:Elasticsearch支持地理空间搜索,可以对地理空间数据进行索引,并支持多种查询方式,包括距离查询、地理位置聚合等。
- 高可用性:Elasticsearch采用分布式架构,可以自动分配数据,并提供高可用性。
- 易于扩展:Elasticsearch支持易于扩展,可以方便地部署和管理,支持多种部署方式,包括 Docker、Kubernetes、云平台等。
各自长处:
- 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实现
- 两者再基于某种方式,实现数据的同步,保证一致性
二、ElasticSearch安装
2.1 Windows安装ES
下载地址:elasticseach下载地址
下载ES安装包,解压至任意非中文路径,如D:\elasticsearch-8.13.3
。
打开bin
目录,双击elasticsearch.bat
文件,启动ES。
测试ES是否安装成功,在浏览器中输入http://localhost:9200/
,如果出现如下页面,则说明ES安装成功。
2.2 Docker安装ES
- 创建网络 因为我们还需要部署kibana容器,因此需要让es和kibana容器互联。这里先创建一个网络:es-net 名字自己取。
docker network create es-net
- 拉取镜像或加载本地镜像
docker pull docker.elastic.co/elasticsearch/elasticsearch:8.13.3
加载本地镜像:
docker load -i elasticsearch.tar.gz
- 创建容器 创建数据卷:es-data,es-plugins, es-config
docker run -d \
--name es \
-e "ES_JAVA_OPTS=-Xms512m -Xmx512m" \
-e "discovery.type=single-node" \
-v es-data:/usr/share/elasticsearch/data \
-v es-plugins:/usr/share/elasticsearch/plugins \
-v es-config:/usr/share/elasticsearch/config \
--privileged \
--network es-net \
-p 9200:9200 \
-p 9300:9300 \
elasticsearch:8.13.3
--name es
:指定容器名称为es。-e "ES_JAVA_OPTS=-Xms512m -Xmx512m"
:设置JVM参数。-e "discovery.type=single-node"
:设置单节点集群。-v es-data:/usr/share/elasticsearch/data
:挂载数据卷,数据将保存在es-data目录中。-v es-plugins:/usr/share/elasticsearch/plugins
:挂载插件目录,可以安装第三方插件。--privileged
:以root权限运行容器。--network es-net
:指定容器加入到es-net网络。-p 9200:9200
:将容器的9200端口映射到主机的9200端口。-p 9300:9300
:将容器的9300端口映射到主机的9300端口。
4.关闭安全验证 找到挂载配置文件的路径,修改配置文件elasticsearch.yml,添加以下配置:
xpack.security.enabled: false
5.重启容器
docker restart es
测试ES是否安装成功,在浏览器中输入http://Linux主机IP地址:9200/
,如果有内容返回,则说明ES安装成功。
2.3 安装Kibana
2.3.1 什么是Kibana?
Kibana是一个开源的分析和可视化平台,它提供了一个界面,让用户可以对Elasticsearch的数据进行可视化、分析和搜索。
2.3.2 Windos安装Kibana
下载地址:Kibana下载地址
下载Kibana安装包,解压至任意非中文路径,如D:\kibana-8.13.3
。
打开bin
目录,双击kibana.bat
文件,启动Kibana。
测试Kibana是否安装成功,在浏览器中输入http://localhost:5601/
,如果出现如下页面,则说明Kibana安装成功。
kibana左侧中提供了一个DevTools界面:
这个界面中可以编写DSL来操作elasticsearch。并且对DSL语句有自动补全功能。
重要
Elasticsearch和Kibana的版本要保持一致,否则可能出现版本不兼容的问题。
2.3.3 Docker安装Kibana
1.创建网络(之前创建了可以不用)
docker network create es-net
2.拉取镜像或加载本地镜像
docker pull docker.elastic.co/kibana/kibana:8.13.3
加载本地镜像:
docker load -i kibana.tar.gz
3.创建容器
docker run -d \
--name kibana \
-e ELASTICSEARCH_HOSTS=http://es:9200 \
--network es-net \
-p 5601:5601 \
kibana:8.13.3
相关信息
--name kibana
:指定容器名称为kibana。-e ELASTICSEARCH_HOSTS=http://es:9200
:设置elasticsearch的连接地址。es为之前创建的es容器的名称。--network es-net
:指定容器加入到es-net网络。-p 5601:5601
:将容器的5601端口映射到主机的5601端口。
kibana启动一般比较慢,需要多等待一会,可以通过命令查看日志:
docker logs kibana
4.启动容器
docker start kibana
测试Kibana是否安装成功,在浏览器中输入http://Linux主机IP地址:5601/
,如果有内容返回,则说明Kibana安装成功。
2.4 安装IK分词器
2.4.1 什么是分词器?
分词器主要作用将用户输入的一段文本,按照一定逻辑,分析成多个词语的一种工具。
例如:
- 华为手机 —> 华为、手、手机
- 这是一个好消息! —> 这、一个、好、消息、!
ElasticSearch默认的分词器是standard,它是基于词典的分词器,它对英文、数字、符号等进行分词,但是对于中文分词效果不好。
IK分词器是ElasticSearch官方提供的中文分词器,它可以对中文文本进行分词、提取关键词、生成拼音、提供搜索建议等功能。
ElasticSearch 内置分词器有以下几种:
- Standard Analyzer ○ 默认分词器,按词/字切分,小写处理 (英文)华 为 手 机
- Simple Analyzer ○ 按照非字母切分(符号被过滤),小写处理
- Stop Analyzer ○ 小写处理,停用词过滤(the,a,is)
- Whitespace Analyzer ○ 按照空格切分,不转小写
- Keyword Analyzer ○ 不分词,直接将输入当作输出
- Patter Analyzer ○ 正则表达式,默认\W+(非字符分割) (中文会被去掉)
- Language ○ 提供了30多种常见语言的分词器
ES提供了一个接口给我们来验证分词效果,如下所示:
# 分词效果验证
GET _analyze
{
"text": "我爱写代码",
"analyzer": "standard"
}
返回结果:
Elasticsearch内置分词器对中文很不友好(偏好英文),处理方式为一个字一个词,所以分词效果不好。
2.4.2 IK分词器
IKAnalyzer是一个开源的,基于java语言开发的轻量级的中文分词工具包。是一个基于Maven构建的项目,具有60万字/秒的高速处理能力,并且支持用户词典扩展定义。
IKAnalyzer又称庖丁解牛分词器
下载地址:IKAnalyzer下载地址
分词器的核心
词库
分词算法
○ ik_smart:最小分词法
■ 我是程序员 -> 我、是、程序员
○ ik_max_word:最细分词法
■ 我是程序员 -> 我、是、程序员、程序、员
2.4.3 window安装IK分词器
1.下载IKAnalyzer压缩,如D:\elasticsearch-analysis-ik-8.13.3
。
2.在es目录下的plugin目录中新建文件夹ik,将解压后的IKAnalyzer文件夹拷贝到ik目录下。
3.启动ES
4.验证分词器效果
2.4.4 Docker安装IK分词器
- 在线安装ik插件(较慢)
# 进入容器内部
docker exec -it es /bin/bash
# 下载ik插件
cd /usr/share/elasticsearch/bin
./elasticsearch-plugin install https://github.com/medcl/elasticsearch-analysis-ik/releases/download/v8.13.3/elasticsearch-analysis-ik-8.13.3.zip
#退出
exit
#重启容器
docker restart es
- 离线安装ik插件(推荐)
1.查看数据卷目录:
安装插件需要知道elasticsearch的plugins目录位置,而我们用了数据卷挂载,因此需要查看elasticsearch的数据卷目录,通过下面命令查看:
docker volume inspect es-plugins
es-plugins是对es容器内plugins目录的挂载,因此我们需要查看es-plugins的挂载位置。
结果:
[
{
"CreatedAt": "2022-05-06T10:06:34+08:00",
"Driver": "local",
"Labels": null,
"Mountpoint": "/var/lib/docker/volumes/es-plugins/_data",
"Name": "es-plugins",
"Options": null,
"Scope": "local"
}
]
可以看到,es-plugins的挂载位置是/var/lib/docker/volumes/es-plugins/_data。
2.解压分词器安装包并重命名为ik
3.将解压的ik目录上传至/var/lib/docker/volumes/es-plugins/_data目录
4.重启容器
docker restart es
2.4.5 扩展词词典
随着互联网的发展,“造词运动”也越发的频繁。出现了很多新的词语,在原有的词汇列表中并不存在。比如:“奥力给”,“白嫖” 等。 所以我们的词汇也需要不断的更新,IK分词器提供了扩展词汇的功能。
1.打开IK分词器config目录: 文件路径:/usr/share/elasticsearch/plugins/ik/config/IKAnalyzer.cfg.xml
2.IKAnalyzer.cfg.xml配置文件内容添加:
<entry key="ext_dict">custom/mydict.dic</entry>
3.自定义词典文件mydict.dic,文件路径:/usr/share/elasticsearch/plugins/ik/config/custom/mydict.dic
4.重启ES
重启ES后,我们就可以使用自定义词典了。
5.验证分词效果
2.4.6 停用词词典
在互联网项目中,在网络间传输的速度很快,所以很多语言是不允许在网络上传递的,如:关于宗教、政治等敏感词语,那么我们在搜索时也应该忽略当前词汇。
IK分词器也提供了强大的停用词功能,让我们在索引时就直接忽略当前的停用词汇表中的内容。
1.IKAnalyzer.cfg.xml配置文件内容添加:
<entry key="ext_stopwords">custom/stopwords.dic</entry>
2.在 stopword.dic 添加停用词词典
3.重启ES
三、ES基本操作
3.1 索引库的操作
索引库就类似数据库表,mapping映射就类似表的结构。
要向es中存储数据,必须先创建“库”和“表”
3.1.1 Mapping
Mapping是es中用来定义字段类型和属性的。
Mapping是对索引库中文档的约束,常见的mapping属性包括:
- type:字段类型,常见的简单类型有
相关信息
- 字符串:text(可分词的文本)、keyword(精确值,例如:品牌、国家、ip地址)keyword类型- - 只能整体搜索,不支持搜索部分内容
- 数值:long、integer、short、byte、double、float
- 布尔:boolean
- 日期:date
- 复杂类型:object、nested、geo_point、geo_shape、completion
- index:是否索引,true或false
- store:是否存储,true或false
- analyzer:分词器,用于分词处理
- fields:复杂类型字段的子字段,用于定义子字段的mapping属性
3.1.2 创建索引库和映射
基本语法:
- 请求方式:PUT
- 请求路径:/索引库名,可以自定义
- 请求参数:mapping映射
PUT /{index}
{
"mappings": { # 定义mapping
"{type}": { # 定义类型, 如doc。如果没有定义,则默认为_doc
"properties": { # 定义字段
"{field}": { # 定义字段名, 如name
"type": "{type}", # 定义字段类型, 如text
"index": true, # 是否索引, true
"store": true, # 是否存储
"analyzer": "standard", # 分词器
"fields": { # 定义子字段
"{sub_field}": { # 定义子字段名
"type": "{type}", # 定义子字段类型
"index": true, # 是否索引
"store": true # 是否存储
}
}
}
}
}
}
}
例如:
# 创建索引库
PUT /user
{
"mappings": { // 结构
"properties": { // 属性
"username": { // 属性名
"type": "keyword" // 属性类型,keyword 关键字,表示不需要分词
},
"age": {
"type": "integer",
"index": false // 不创建索引
},
"info": {
"type": "text",
"analyzer": "ik_smart"
},
"child": {
"properties": {
"name": {
"type": "keyword"
},
"age": {
"type": "integer",
"index": false
}
}
},
"createTime": {
"type": "date", //时间类型
"format": "yyyy-MM-dd HH:mm:ss", //约定时间格式
"index": false
}
}
}
}
3.1.3 查询索引库
基本语法:
- 请求方式:GET
- 请求路径:/索引库名/_search
- 请求参数:无
GET /索引库名
3.1.4 删除索引库
基本语法:
- 请求方式:DELETE
- 请求路径:/索引库名
DELETE /索引库名
3.1.5 修改索引库
基本语法:
- 请求方式:PUT
- 请求路径:/索引库名/_mapping
- 请求参数:修改参数
注意:这里的修改是只能增加新的字段到mapping中,不能修改已有字段的属性。
倒排索引结构虽然不复杂,但是一旦数据结构改变(比如改变了分词器),就需要重新创建倒排索引,这简直是灾难。因此索引库一旦创建,无法修改mapping。 虽然无法修改mapping中已有的字段,但是却允许添加新的字段到mapping中,因为不会对倒排索引产生影响。
PUT /索引库名/_mapping
{
"properties": {
"新字段名":{
"type": "integer"
}
}
}
3.2 文档操作
文档是索引库中的数据记录,可以理解为数据库表中的一条记录。
3.2.1 新增文档
语法:
POST /索引库名/_doc/文档id
{
"字段1": "值1",
"字段2": "值2",
"字段3": {
"子属性1": "值3",
"子属性2": "值4"
},
// ...
}
案例:
3.2.2 查询文档
根据rest风格,新增是post,查询应该是get,不过查询一般都需要条件,这里我们把文档id带上,表示查询某个文档。 语法:
GET /{索引库名称}/_doc/{id}
//批量查询:查询该索引库下的全部文档
GET /{索引库名称}/_search
案例:
3.2.3 删除文档
语法:
DELETE /{索引库名称}/_doc/{id}
案例:
3.2.4 修改文档
修改有两种方式:
- 全量修改:直接覆盖原来的文档
- 增量修改:修改文档中的部分字段
全量修改:
全量修改是覆盖原来的文档,其本质是:
- 根据指定的id删除文档
- 新增一个相同id的文档
注意:如果根据id删除时,id不存在,第二步的新增也会执行,也就从修改变成了新增操作了。 语法: json PUT /{索引库名}/_doc/文档id { “字段1”: “值1”, “字段2”: “值2”, // … 略 } 案例:
#### 增量修改 增量修改是只修改指定id匹配的文档中的部分字段。
语法:
POST /{索引库名}/_update/文档id
{
"doc": {
"字段名": "新的值",
}
}
案例:
四. DSL查询语言
4.1 DSL简介
DSL(Domain Specific Language)是一种专门为某一领域设计的语言,它是一种用来与特定领域的专家进行沟通的语言,它是一种抽象的语言,它是一种用来描述某一领域的语言。DSL的目的是为了简化复杂的查询,使得查询更加简单、易于理解。
Elasticsearch提供了丰富的DSL,包括查询语言、过滤语言、聚合语言、排序语言、脚本语言等。这些DSL可以帮助用户快速、高效地查询、过滤、聚合、排序数据。
DSL的语法与Elasticsearch的RESTful API相似,但有一些差异。DSL的语法更加简洁、易于理解,并且支持更丰富的查询功能。
4.2 ES的查询方式
Elasticsearch提供了两种查询方式:
1.基于RESTful API的查询方式
GET /user/_search?q=name:张三
说明:这种查询方式使用HTTP协议的GET方法,通过URL参数的方式指定查询条件。 查询的是索引user,查询条件是name为张三。
2.基于DSL的查询方式 Elasticsearch提供丰富且灵活的查询语言叫做DSL查询(Query DSL),它允许你构建更加复杂、强大的查询。 DSL(Domain Specific Language特定领域语言)以JSON请求体的形式出现
POST /user/_search
{
"query": { # 查询条件
"match": { # 匹配查询
"name": "张三" # 字段名和查询值
}
}
}
平时更多采用这种方式,因为可操作性更强,处理复杂请求时更得心应手。
4.3 全文检索
4.3.1 match_all查询
一般生产环境下不会这么做,因为数据量有可能非常大,所以查询非常耗时,因此一般用于测试用。
match_all查询会匹配所有文档,它的查询条件是match_all,语法如下:
GET /indexName/_search
{
"query": {
"match_all": {} // 由于这里是查询所有数据,因此没有查询条件
}
}
4.3.2 match查询
match查询是最常用的查询,它可以用于全文检索。match查询会对查询条件进行分词,然后进行匹配。
match:根据一个字段查询
语法:
GET /indexName/_search
{
"query": {
"match": {
"FIELD": "TEXT"
}
}
}
相关信息
- FIELD:字段名
- TEXT:查询条件
- match查询会对查询条件进行分词,默认分词器是standard,可以自定义分词器。相应字段中分词后有满足的即可命中
GET /user/_search
{
"query": {
"match": {
"remark": {
"query": "中国",
"analyzer": "ik_smart" # 指定分词器
}
}
}
}
4.3.3 multi_match查询
multi_match查询可以同时对多个字段进行全文检索(任意一个字段符合条件就算符合查询条件)。它会对查询条件进行分词,然后进行匹配。
根据多个字段查询,参与查询字段越多,查询性能越差。【推荐:使用copy_to构造all字段】
语法:
GET /indexName/_search
{
"query": {
"multi_match": {
"query": "TEXT",
"fields": ["FIELD1", "FIELD2"]
}
}
}
4.3.4 match_phrase查询
match_phrase查询可以用于短语查询。
语法:
GET /indexName/_search
{
"query": {
"match_phrase": {
"FIELD": "TEXT"
}
}
}
案例:
```json
GET /test1/_search
{
"query": {
"match_phrase": {
"address": {
"query": "中国"
}
}
}
}
相关信息
- match_phrase查询会对查询条件进行分词,并且按照正确的顺序出现。也就是说,它不仅检查词语的存在性,还检查它们的位置关系。
- 可以使用slop参数控制匹配的位置关系。slop参数指定了词语之间的最大距离。
GET /user/_search
{
"query": {
"match_phrase": {
"reamrk": {
"query": "是中国人",
"slop": 1 # 最大允许词语间隔
}
}
}
}
4.3.5 match_phrase_prefix查询
match_phrase_prefix查询可以用于短语前缀查询。智能搜索–以什么开头。
语法:
GET /indexName/_search
{
"query": {
"match_phrase_prefix": {
"FIELD": "TEXT"
}
}
}
案例
GET /test1/_search
{
"query": {
"match_phrase_prefix": {
"remark": "印度"
}
}
}
相关信息
- match_phrase_prefix查询类似于match_phrase查询,但它允许最后一个词作为一个前缀来匹配。换句话说,它可以看作是一个自 动完成或即时搜索功能,其中用户输入的部分字符串被视为完整单词的开始。
- 例如,如果搜索“quick br”,match_phrase_prefix将会寻找以“br”开头的所有单词,并且这些单词需要紧跟在“quick”之后。所 以它可能会匹配到“quick brown”。
- 此查询非常适合于实现搜索建议或自动完成功能。
4.4 精确查询
4.4.1 term查询
term查询是最基本的查询,它可以用于精确匹配某个字段的值。 term查询不会分析查询条件(不会对条件分词),只有当词条和查询字符串完全匹配时才匹配,也就是精确查找,比如数字,日期,布尔值或 not_analyzed 的字符串(未经分词数据类型):
{
"term": {
"field": "value"
}
}
案例:
当我搜索的是精确词条时,能正确查询出结果:
但是,当我搜索的内容不是词条,而是多个词语形成的短语时,反而搜索不到:
4.4.2 terms查询
terms查询多个精确匹配的词条。 term 查询对于查找单个值非常有用,但通常我们可能想搜索多个值。 如果我们想要查找价格字段值为 $20 或 $30 的文档该如何处理呢? 这时,我们可以使用 terms 查询:
语法:
GET /indexName/_search
{
"query": {
"terms": {
"FIELD": ["VALUE1", "VALUE2"]
}
}
}
4.4.3 range查询
范围查询,一般应用在对数值类型做范围过滤的时候。比如做价格范围过滤。
语法:
GET /indexName/_search
{
"query": {
"range": {
"FIELD": {
"GT": VALUE1,
"LT": VALUE2
}
}
}
}
相关信息
- GT: greater than,大于
- LT: less than,小于
- GTE: greater than or equal,大于等于
- LTE: less than or equal,小于等于
案例
4.4.4 ids查询
ids查询可以根据文档的id精确查询。
GET /indexName/_search
{
"query": {
"ids": {
"values": ["ID1", "ID2"]
}
}
}
案例
GET hotel/_search
{
"query": {
"ids": {
"values": ["36934","38665"]
}
}
}
4.5 通配符查询(wildcard)
wildcard查询:会对查询条件进行分词。还可以使用通配符 ?(任意单个字符) 和 * (0个或多个字符)
语法:
GET /indexName/_search
{
"query": {
"wildcard": {
"FIELD": {
"VALUE": "PATTERN"
}
}
}
}
案例:
GET /test1/_search
{
"query": {
"wildcard": {
"name": {
"value": "李*"
}
}
}
}
查询name字段中李开头
4.6 复合查询
4.6.1 布尔查询(bool)
布尔查询可以组合多个查询条件,并对其进行逻辑组合。 bool查询可以包含多个子查询,每个子查询可以是布尔查询,也可以是其他查询。
子查询的组合方式有:
- must:必须匹配每个子查询,类似“与”
- should:选择性匹配子查询,类似“或”
- must_not:必须不匹配,不参与算分,类似“非”
- filter:过滤满足条件的数据 注意:尽量在筛选的时候多使用不参与算分的must_not和filter,以保证性能良好
语法:
GET /indexName/_search
{
"query": {
"bool": {
"must": [ // 必须匹配 and
{
"match": {
"FIELD1": "TEXT1"
}
},
{
"match": {
"FIELD2": "TEXT2"
}
}
],
"should": [ // 选择性匹配 or
{
"match": {
"FIELD3": "TEXT3"
}
},
{
"match": {
"FIELD4": "TEXT4"
}
}
],
"must_not": [ // 必须不匹配 not
{
"match": {
"FIELD5": "TEXT5"
}
}
],
"filter": [ // 条件过滤查询
{
"range": {
"FIELD6": {
"GT": VALUE1,
"LT": VALUE2
}
}
}
]
}
}
}
4.6.2 must
多个查询条件必须完全匹配,相当于关系型数据库中的 and。如果有一个条件不满足则不返回数据。
案例:
GET /test1/_search
{
"query": {
"bool": {
"must": [
{
"match": {
"name": "张三"
}
},
{
"match": {
"age": "25"
}
}
]
}
}
}
查询的结果中必须包含name为张三,age为25的文档。
4.6.3 should
多个查询条件只要满足一个即可,相当于关系型数据库中的 or。
案例:
GET /test1/_search
{
"query": {
"bool": {
"should": [
{
"match": {
"name": "张三"
}
},
{
"match": {
"age": "25"
}
}
]
}
}
}
查询的结果中只要包含name为张三或age为25的文档。
4.6.4 must_not
查询条件必须不匹配,相当于关系型数据库中的 not。
案例:
GET /test1/_search
{
"query": {
"bool": {
"must_not": [
{
"match": {
"name": "李四"
}
},
{
"match": {
"age": 30
}
}
]
}
}
}
查询的结果:name不是李四并且年龄不是30的文档信息
4.6.5 filter
查询条件必须满足,但不参与计算分值,相当于关系型数据库中的 where。filter查询可以用于过滤数据,比如只显示满足条件的数据,而不显示不满足条件的数据。
案例:
GET /test1/_search
{
"query": {
"bool": {
"must": [
{
"match": {
"name": "李四"
}
}
],
"filter": {
"range": {
"age": {
"gte": 20,
"lt": 30
}
}
}
}
}
}
查询出用户年龄在20-30岁之间名称为 李四 的用户
4.7 设置查询结果
4.7.1 查询结果字段过滤
我们在查询数据的时候,返回的结果中,所有字段都给我们返回了,但是有时候我们并不需要那么多,所以可以对结果进行过滤处理。
语法:
GET /indexName/_search
{
"query": {
"match_all": {}
},
"_source": ["FIELD1", "FIELD2"] // 只返回指定字段
}
4.7.2 排序
排序可以对查询结果进行排序,Elasticsearch支持多种排序方式,在使用排序后就不会进行算分了。
普通字段排序
排序条件是一个数组,也就是可以写多个排序条件。按照声明的顺序,当第一个条件相等时,再按照第二个条件排序,以此类推
语法:
GET /indexName/_search
{
"query": {
"match_all": {}
},
"sort": [
{
"FIELD1": {
"order": "DESC" // 降序
}
},
{
"FIELD2": {
"order": "ASC" // 升序
}
}
]
}
案例:
- 地理坐标排序
语法:
GET /indexName/_search
{
"query": {
"match_all": {}
},
"sort": [
{
"_geo_distance": {
"location": [13.404954, 52.520008], // 经纬度
"order": "asc",
"unit": "km" // 单位
}
}
]
}
相关信息
这个查询的含义是:
- 指定一个坐标,作为目标点
- 计算每一个文档中,指定字段(必须是geo_point类型)的坐标 到目标点的距离是多少
- 根据距离排序,距离越近的文档排在前面
案例:
需求描述:实现对酒店数据按照到你的位置坐标的距离升序排序
提示:获取你的位置的经纬度的方式:获取经纬度
假设我的位置是:31.034661,121.612282,寻找我周围距离最近的酒店。
4.7.3 分页
分页可以对查询结果进行分页,Elasticsearch默认每页显示10条数据,可以通过from和size参数进行分页。 语法:
GET /indexName/_search
{
"query": {
"match_all": {}
},
"from": 0, // 跳过的文档数量,从0开始查询
"size": 10 // 每页显示的文档数量
}
4.7.4 高亮
注意:
- 高亮是对关键字高亮,因此搜索条件必须带有关键字,而不能是范围这样的查询。
- 默认情况下,高亮的字段,必须与搜索指定的字段一致,否则无法高亮
- 如果要对非搜索字段高亮,则需要添加一个属性:required_field_match=false
使用场景:在百度等搜索后,会对结果中出现搜索字段的部分进行高亮处理。 语法:
GET /hotel/_search
{
"query": {
"match": {
"FIELD": "TEXT" // 查询条件,高亮一定要使用全文检索查询
}
},
"highlight": {
"fields": { // 指定要高亮的字段
"FIELD": { //【要和上面的查询字段FIELD一致】
"pre_tags": "<em>", // 用来标记高亮字段的前置标签
"post_tags": "</em>" // 用来标记高亮字段的后置标签
}
}
}
}
案例:组合字段all的案例
4.8 聚合
4.8.1 概念及分类:
在Elasticsearch中,聚合查询(Aggregations)是一种用于对搜索结果进行统计分析的强大工具。它允许用户基于搜索的数据提取更高级的信息和洞察,而不仅仅是检索文档。聚合可以非常简单,也可以相当复杂。
聚合的常见种类:
- 桶(Bucket)聚合:将数据分成一组固定大小的桶(分组查询),并对桶中的数据进行聚合操作。
- 度量(Metric)聚合:对数据进行度量统计,如求和(sum)、平均值(avg)、最大值(max)、最小值(min)等。
- 管道(Pipeline)聚合:其它聚合的结果为基础做聚合如:用桶聚合实现种类排序,然后使用度量聚合实现各个桶的最大值、最小值、平均值等。
4.8.2 桶聚合
桶聚合是最常见的聚合,它可以将数据分成一组固定大小的桶,并对桶中的数据进行聚合操作。
语法:
GET /indexName/_search
{
"query": { // 限定聚合的数据范围
"match_all": {}
},
"aggs": {
"NAME": { // 聚合名称
"TYPE": { // 聚合类型
"FIELD": "VALUE" // 聚合字段
}
}
}
}
相关信息
聚合三要素:
- NAME:聚合名称,自定义,用于区分不同的聚合
- TYPE:聚合类型,包括term, terms、date_histogram、range等
- FIELD:聚合字段,根据聚合类型不同,字段类型也不同
配合聚合的属性有:
- size:terms聚合的桶大小
- order:terms聚合的排序方式
- field: 指定聚合的字段
案例:对酒店品牌进行聚合查询。(对酒店品牌进行分组,统计每个品牌的数量)
GET /hotel/_search
{
"size": 0, // 不显示查询结果,只显示聚合结果
"query": { //限定聚合的数据范围
"match_all": {}
},
"aggs": { //
"brand": { // 聚合名称,自定义
"terms": { // 聚合类型,terms,按照字段值分组
"field": "brand", ////聚合字段,品牌字段
"size": 10, // 希望获取的聚合结果数量
"order": {
"_count": "desc" // 聚合桶的排序方式,按照数量降序
}
}
}
}
}
案例:对酒店品牌和所在城市进行聚合查询。(对酒店品牌进行分组,统计每个品牌的数量,并按照所在城市进行分组,统计每个城市的数量)
GET /hotel/_search
{
"size": 0,
"query": {
"match_all": {}
},
"aggs": {
"brandAgg": { // 品牌聚合
"terms": {
"field": "brand", // 聚合字段
"size": 20, // 聚合桶的大小
"order": {
"_count": "desc" // 聚合桶的排序方式,按照数量降序
}
}
},
"cityAgg": { // 城市聚合
"terms": {
"field": "city",
"size": 10
}
}
}
}
4.8.3 度量聚合
度量聚合很少单独使用,一般是和桶聚合一并结合使用
我们对酒店按照品牌分组,形成了一个个桶。现在我们需要对桶内的酒店做运算,获取每个品牌的用户评分的min、max、avg等值。
这就要用到Metric聚合了,例如stat聚合:就可以获取min、max、avg等结果。
语法:
GET /indexName/_search
{
"query": {
"match_all": {}
},
"aggs": {
"NAME": {
"TYPE": {
"FIELD": "VALUE"
},
"aggs": { // 子聚合
"SUB_NAME": {
"TYPE": { // avg, min, max, sum, stats(包括count, min, max, avg, sum)等
"FIELD": "VALUE" // 聚合字段
}
}
}
}
}
}
案例:对酒店品牌进行分组,统计每个品牌的max、min、avg、sun、count值。
GET /hotel/_search
{
"size": 0,
"aggs": {
"brandAgg": {
"terms": {
"field": "brand",
"order": {
"_count": "desc"
},
"size": 20
},
"aggs": {
"score_stats": {
"stats": {
"field": "price"
}
}
}
}
}
}
另外,我们还可以给聚合结果做个排序,例如按照每个桶的酒店平均分做排序:
GET /hotel/_search
{
"size": 0,
"aggs": {
"brandAgg": {
"terms": {
"field": "brand",
"order": {
"score_stats.avg": "desc" // 按照每个桶的酒店平均分做排序
},
"size": 20
},
"aggs": {
"score_stats": {
"stats": {
"field": "price"
}
}
}
}
}
}
4.8.4 管道聚合
管道聚合是一种特殊的聚合,它可以将多个聚合的结果作为输入,进行更复杂的聚合操作。
五. SpringBoot操作Elasticsearch
SpringBoot操作Elasticsearch的方式有哪些?
- Spring Data Elasticsearch
- Elasticsearch Java High Level REST Client
5.1 Elasticsearch Java API Client
Elasticsearch Java API Client是官方推荐的Java客户端,它提供了丰富的API,可以方便地操作Elasticsearch。
5.1.1 连接到Elasticsearch
<project>
<dependencies>
<dependency>
<groupId>co.elastic.clients</groupId>
<artifactId>elasticsearch-java</artifactId>
<version>8.17.3</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.17.0</version>
</dependency>
</dependencies>
</project>
你可以使用API密钥和Elasticsearch端点来连接到Elastic 。
RestClient这个类主要是用作于与服务端IP以及端口的配置,在其的builder()方法可以设置登陆权限的账号密码、连接时长等等。总而言之就是服务端配置。
RestClientTransport 这是Jackson映射器创建传输。建立客户端与服务端之间的连接传输数据。这是在创建ElasticsearchClient需要的参数,而创建RestClientTransport就需要上面创建的RestClient。
ElasticsearchClient 这个就是Elasticsearch的客户端。调用Elasticsearch语法所用到的类,其就需要传入上面介绍的RestClientTransport。
package com.syh.es.config;
import co.elastic.clients.elasticsearch.ElasticsearchClient;
import co.elastic.clients.json.JsonpMapper;
import co.elastic.clients.json.jackson.JacksonJsonpMapper;
import co.elastic.clients.transport.ElasticsearchTransport;
import co.elastic.clients.transport.rest_client.RestClientTransport;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.http.HttpHost;
import org.elasticsearch.client.RestClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Lazy;
import java.io.IOException;
/**
* @author shan
* @create 2024/10/16 9:47
*/
@Configuration
public class ElasticSearchConfig {
@Bean
public ElasticsearchClient client(){
RestClient restClient = RestClient.builder(
new HttpHost("192.168.229.133", 9200)).build();
ElasticsearchTransport transport = new RestClientTransport(
restClient, new JacksonJsonpMapper());
ElasticsearchClient client = new ElasticsearchClient(transport);
return client;
}
}
或者使用直接封装好的pom依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-elasticsearch</artifactId>
</dependency>
然后在application.properties中配置Elasticsearch的连接信息:
spring:
elasticsearch:
uris: http://192.168.229.161:9200 # ES服务器地址
5.1.2 索引和映射、文档操作
- 建议采用 ElasticsearchTemplate 类来操作索引和文档,它提供了丰富的索引、查询、更新、删除等方法。
5.1.3 高级查询
- 查询所有
@Test
public void test01() throws IOException {
SearchResponse<HotelDoc> search = esClient.search(
req -> req.index("hotel")
, HotelDoc.class);
log.info("search:{}", search.hits().total().value());
List<Hit<HotelDoc>> hits = search.hits().hits();
for (Hit<HotelDoc> hit : hits) {
System.out.println(hit.source());
}
}
如果报错:Caused by: jakarta.json.JsonException: Jackson exception
在文档实体类上添加注解:
@Data
@NoArgsConstructor
@JsonIgnoreProperties(ignoreUnknown = true) // 忽略未知属性
@Document(indexName = "hotel") //写在类上,表示该类是一个文档对象,indexName指定索引名称
public class HotelDoc {}
- 其他查询
@Test
public void test02() throws IOException {
SearchResponse<HotelDoc> search = esClient.search(
req -> req.index("hotel")
.query(q -> q.match(m -> m.field("name").query("北京"))),
HotelDoc.class
);
List<Hit<HotelDoc>> hits = search.hits().hits();
for (Hit<HotelDoc> hit : hits) {
System.out.println(hit.source());
}
}
或者
@Test
public void test02() throws IOException {
Query q = Query.of(m -> m.match( match -> match.field("name").query("北京")));
SearchResponse<HotelDoc> search = esClient.search(
req -> req.index("hotel")
.query(q),
HotelDoc.class
);
List<Hit<HotelDoc>> hits = search.hits().hits();
for (Hit<HotelDoc> hit : hits) {
System.out.println(hit.source());
}
}
图示:
5.2 Spring Data Elasticsearch
Spring Data Elasticsearch是Spring官方提供的Elasticsearch的ORM框架,它可以自动配置Elasticsearch Java API Client,并提供丰富的查询方法。
六. 酒店案例查询
6.1 需求背景
某酒店网站希望能够根据用户的搜索条件进行酒店的查询,并返回相关的酒店信息。并高亮显示用户搜索的关键字。
- 用户可以输入关键字搜索酒店名称、地址、商圈,显示相关的酒店信息。
- 用户可以选择价格范围查询
- 用户可以选择星级评分查询
- 用户可以选择城市查询
- 用户可以选择酒店品牌查询
- 并且可以对结果按照 价格、评分、等排序查询
6.2 文档实体类
package com.syh.hotel.model.doc;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.syh.hotel.model.entity.Hotel;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.data.annotation.Id;
import org.springframework.data.elasticsearch.annotations.Document;
import org.springframework.data.elasticsearch.annotations.Field;
import org.springframework.data.elasticsearch.annotations.FieldType;
import org.springframework.data.elasticsearch.annotations.GeoPointField;
import java.io.Serializable;
/**
*
* @TableName tb_hotel
*/
@Data
@Document(indexName = "hotel")
@NoArgsConstructor
@JsonIgnoreProperties(ignoreUnknown = true)
public class HotelDoc implements Serializable {
/**
* 酒店id
*/
@Id
private Long id;
/**
* 酒店名称
*/
@Field(name = "name",type = FieldType.Text,analyzer = "ik_max_word",copyTo = "keywords")
private String name;
/**
* 酒店地址
*/
@Field(name = "address",type = FieldType.Text,analyzer = "ik_max_word",copyTo = "keywords")
private String address;
/**
* 酒店价格
*/
@Field(name = "price",type = FieldType.Integer)
private Integer price;
/**
* 酒店评分
*/
@Field(name = "score",type = FieldType.Integer)
private Integer score;
/**
* 酒店品牌
*/
@Field(name = "brand",type = FieldType.Keyword)
private String brand;
/**
* 所在城市
*/
@Field(name = "city",type = FieldType.Keyword)
private String city;
/**
* 酒店星级,1星到5星,1钻到5钻
*/
@Field(name = "starName",type = FieldType.Keyword)
private String starName;
/**
* 商圈
*/
@Field(name = "business",type = FieldType.Text,analyzer = "ik_max_word",copyTo = "keywords")
private String business;
/**
* 酒店位置
*/
@GeoPointField
private String location;
/**
* 查询关键词
*/
@Field(name = "keywords",type = FieldType.Text,analyzer = "ik_max_word")
private String keywords;
/**
* 酒店图片
*/
@Field(name = "pic", type = FieldType.Keyword)
private String pic;
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();
}
}
6.3 查询实体类
@Data
public class BaseQuery {
private int pageNum;
private int pageSize;
}
@Data
public class HotelQuery extends BasePageQuery {
private String keywords;// 关键字搜索
private List<String> brands; // 品牌搜索
private List<String> city;//城市搜索
private List<String> starName; // 星级搜索
private Integer minPrice;
private Integer maxPrice;
private String orderColum; // 排序的列
}
@Data
public class HotelGroup {
//城市分组
private List<String> cityGroup;
//品牌分组
private List<String> brandGroup;
//星级分组
private List<String> starGroup;
}
6.4 业务接口
- 统计查询酒店的城市、品牌、星级分组
@Service
@RequiredArgsConstructor
public class HotelServiceImpl extends ServiceImpl<HotelMapper, Hotel>
implements HotelService{
private final ElasticsearchClient elasticsearchClient;
@Override
public HotelGroup searchHotelGroup() throws IOException {
HotelGroup hotelGroup = new HotelGroup();
//聚合统计城市,品牌,星级
// GET /hotel/_search
// {
// "size": 0,
// "aggs": {
// "brand_aggs": {
// "terms": {
// "field": "brand"
// }
// },
// "city_aggs": {
// "terms": {
// "field": "city"
// }
// },
// "start_aggs": {
// "terms": {
// "field": "star"
// }
// }
// }
// }
SearchResponse<Void> search = elasticsearchClient.search(
req -> req.index("hotel")
.size(0)
.aggregations(
"brand_aggs",
a -> a.terms(a2 -> a2.field("brand"))
)
.aggregations(
"city_aggs",
a -> a.terms(a2 -> a2.field("city"))
)
.aggregations(
"star_aggs",
a -> a.terms(a2 -> a2.field("starName"))
)
, Void.class);
List<StringTermsBucket> brandAggs = search.aggregations().get("brand_aggs").sterms().buckets().array();
List<String> brandList = new ArrayList<>();
for(StringTermsBucket bucket : brandAggs){
brandList.add(bucket.key().stringValue());
}
hotelGroup.setBrandGroup(brandList);
List<StringTermsBucket> cityAggs = search.aggregations().get("city_aggs").sterms().buckets().array();
List<String> cityList = new ArrayList<>();
for(StringTermsBucket bucket : cityAggs){
cityList.add(bucket.key().stringValue());
}
hotelGroup.setCityGroup(cityList);
List<StringTermsBucket> starAggs = search.aggregations().get("star_aggs").sterms().buckets().array();
List<String> starList = new ArrayList<>();
for(StringTermsBucket bucket : starAggs){
starList.add(bucket.key().stringValue());
}
hotelGroup.setStarGroup(starList);
return hotelGroup;
}
}
6.5 测试接口
@RestController
@RequestMapping("/hotels")
@RequiredArgsConstructor
public class HotelController {
private final HotelService hotelService;
/**
* 获取酒店分组统计(城市/品牌/星级)
*/
@GetMapping("/groups")
public Result<HotelGroup> getHotelGroups() {
try {
HotelGroup groups = hotelService.searchHotelGroup();
return Result.success(groups);
} catch (IOException e) {
return Result.error("获取分组信息失败:" + e.getMessage());
}
}
/**
* 酒店综合搜索接口
*/
@PostMapping("/search")
public Result<Page<HotelDoc>> searchHotels(@RequestBody HotelQuery query) {
try {
// 参数校验
if (query.getPageNum() < 1) query.setPageNum(1);
if (query.getPageSize() < 1 || query.getPageSize() > 100) {
query.setPageSize(10);
}
Page<HotelDoc> result = hotelService.searchHotelList(query);
return Result.success(result);
} catch (IOException e) {
return Result.error("搜索失败:" + e.getMessage());
}
}
/**
* 通用返回结果封装
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public static class Result<T> {
private int code;
private String msg;
private T data;
public static <T> Result<T> success(T data) {
return new Result<>(200, "success", data);
}
public static <T> Result<T> error(String message) {
return new Result<>(500, message, null);
}
}
}