从入门到实战:Elasticsearch全面指南及Spring Boot集成案例

文章目录


一、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:擅长海量数据的搜索、分析、计算
MySQLElasticsearch说明
TableIndex索引(index),就是文档的集合,类似数据库的表(table)
RowDocument文档(Document),就是一条条的数据,类似数据库中的行(Row),文档都是JSON格式
ColumnField字段(Field),就是JSON文档中的字段,类似数据库中的列(Column)
SchemaMappingMapping(映射)是索引中文档的约束,例如字段类型约束。类似数据库的表结构(Schema)
SQLDSLDSL是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

官网教程

  1. 创建网络 因为我们还需要部署kibana容器,因此需要让es和kibana容器互联。这里先创建一个网络:es-net 名字自己取。
docker network create es-net
  1. 拉取镜像或加载本地镜像
docker pull docker.elastic.co/elasticsearch/elasticsearch:8.13.3

加载本地镜像:

docker load -i elasticsearch.tar.gz
  1. 创建容器 创建数据卷: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);
        }
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值