目录
10.4.1、SpringAMQP-FanoutExchange
10.4.2、SpringAMQP-DirectExhange、TopicExchange
11.1.1、初识elasticsearch-正向索引和倒排索引
11.3.6、DSL查询语法-Function Score Query
11.6.2、数据聚合-DSL实现Buckey聚合(桶聚合)
11.6.3、数据聚合-DSL实现Metrics聚合(嵌套聚合)
1、微服务框架
1.1、认识微服务
单体架构特点?
- 简单方便,高度耦合,扩展性差,适合小型项目。例如:学生管理系统
分布式架构特点?
- 松耦合,扩展性好,但架构复杂,难度大。适合大型互联网项目,例如:京东、淘宝。
- 微服务:一种良好的分布式架构方案
- 优点:拆分粒度更小、服务更独立、耦合度更低
- 缺点:架构非常复杂,运维、监控、部署难度更高
2、服务拆分及远程调用
1.微服务需要根据业务模块拆分,做到单一职责,不要重复开发相同业务
2.微服务可以将业务暴露为接口,供其它微服务使用
3.不同微服务都应该有自己独立的数据库
2.1、微服务远程调用
2.1.1.微服务调用方式
- 基于RestTemplate发起的http请求实现远程调用。
- http请求做远程调用是与语言无关的调用,只要知道对方的ip、端口、接口路径、请求参数即可。
2.2、分布式服务案例
2.2.1.服务调用关系
- 服务提供者:暴露接口给其它微服务调用
- 服务消费者:调用其它微服务提供的接口
- 提供者与消费者角色其实是相对的
- 一个服务可以同时是服务提供者和服务消费者
3、Eureka注册中心
在Eureka架构中,微服务角色有两类:
1.EurekaServer:服务端,注册中心
- 记录服务信息
- 心跳监控
2.EurekaClient:客户端
Provider:服务提供者,例如案例中的user-service
- 注册自己的信息到EurekaServer
- 每隔30秒向EurekaServer发送心跳
Consumer:服务消费者,例如案例中的order-service
- 根据服务名称从EurekaServer拉去服务列表
- 基于服务列表做负载均衡,选中一个微服务后发起远程调用
3.1、搭建EurekaServer
- 引入eureka-server依赖
- 添加@EnableEurekaServer注解
- 在application.yml中配置eureka地址
3.2、服务注册
- 引入eureka-client依赖
- 在application.yml中配置eureka地址
3.3、服务发现
- 引入eureka-client依赖
- 在application.yml中配置eureka地址
- 给RestTemplate添加@LoadBalaned注解
- 用服务提供者的服务名称远程调用
4、Ribbon负载均衡
4.1、Ribbon负载均衡规则
- 规则接口是IRule
- 默认实现是ZoneAvoidanceRule,根据zone选中服务列表,然后轮询
4.2、负载均衡自定义方式
- 代码方式:配置灵活,但修改时需要重新打包发布
- 配置方式:直观,方便,无需重新打包发布,但是无法做全局配置
4.3、饥饿加载
- 开启饥饿加载
- 指定饥饿加载的微服务名称
5、Nacos注册中心
5.1、Nacos安装及入门
5.1.1、Nacos服务搭建
- ①下载安装包
- ②解压
- ③在bin目录下运行指令:startup.cmd -m standalone
5.1.2、Nacos服务注册或发现
- ①引入nacos.discovery依赖
- ②配置nacos地址spring.cloud.nacos.server-addr
5.2、Nacos服务分级模型
5.2.1、Nacos服务分级存储模型
- ①一级是服务,例如userservice
- ②二级是集群。例如杭州或者上海
- ③三级是实例,例如杭州机房的某台部署了userservice的服务器
5.2.2、如何设置实例的集群属性
- ①在application.yml文件,添加spring.cloud.nacos.discovery.cluster-name属性即可
5.3、Nacos注册中心-NacosRule负载均衡策略
- ①优先选择同集群服务实例列表
- ②本地集群找不到提供者,才去其它集群寻找,并且会报警告
- ③确定了可用实例列表后,再采用随机负载均衡挑选实例
5.4、Nacos注册中心-加权负载均衡
- ①Nacos控制台可以设置实例的权重值,0~1之间
- ②同集群内的多个实例,权重越高被访问的频率越高
- ③权重设置为0则完全不会被访问
5.5、Nacos环境隔离
- ①每个namespace都有唯一的id
- ②服务设置namespace时要写id而不是服务名称
- ③不同namespace下的服务互相不可见
5.6、Nacos与eureka对比
5.6.1、Nacos与eureka的共同点
- ①都支持服务注册和服务拉取
- ②都支持服务提供者心跳方式做健康检测
5.6.2、Nacos与Eureka的区别
- ①Nacos支持服务端主动检测提供者状态:临时实例采用心跳模式,非临时实例采用主动检测模式
- ②临时实例心跳不正常会被剔除,非临时实例则不会被剔除
- ③Nacos支持服务列表变更的消息推送模式,服务列表更新更及时
- ④Nacos集群默认采用AP方式,当集群中存在非临时实例时,采用CP模式;Eureka采用AP方式
6、Nacos配置管理
将配置交给Nacos管理步骤
- ①在Nacos中添加配置文件
- ②在微服务中引入nacos的config依赖
- ③在微服务中添加bootstrap.yml,配置nacos地址、当前环境、服务名称、文件后缀名。这些决定了程序启动时去nacos读取哪个文件
Nacos配置更改后,微服务可以实现热更新,方式:
- ①通过@Value注解注入,结合@RefreshScope来刷新
- ②通过@ConfigurationProperties注入,自动刷新
注意事项:
- 不是所有的配置都适合放到配置中心,维护起来比较麻烦
- 建议将一些关键参数,需要运行时调整的参数放到nacos配置中心,一般都是自定义配置
微服务会从nacos读取的配置文件:
- ①[服务名]-[spring.profile.active].yaml,配置环境
- ②[服务名].yaml,默认配置,多环境共享
优先级:
- ①[服务名]-[环境].yaml > [服务名].yaml > 本地配置
微服务默认读取的配置文件:
- ①[服务名]-[spring.profile.active].yaml,默认配置
- ②[服务名].yaml,多环境共享
不同微服务共享的配置文件:
- ①通过shared-config指定
- ②通过extension-configs指定
优先级:
- ①环境配置 > 服务名.yaml > extension-config > extension-configs > shared-configs > 本地配置
Nacos集群搭建步骤:
- ①搭建MySQL集群并初始化数据库表
- ②下载解压nacos
- ③修改集群配置(节点信息)、数据库配置
- ④分别启动多个nacos节点
- ⑤nginx反向代理
7、http客户端Feign
7.1、基于feign远程调用
- ① 引入依赖
- ② 添加@EnableFeignClient注解
- ③ 编写FeignClient接口
- ④ 使用FeignClient中定义的方法替代RestTemplate
7.2、Feign的日志配置
1、方式一是配置文件,feign.client.config.xxx.loggerLever
- ① 如果xxx是default则代表全局
- ② 如果xxx是服务名称,例如userservice则代表某服务
2、方式二是java代码配置Logger.Lever这个Bean
- ① 如果在@EnableFeignClients注解声明则代表全局
- ② 如果在@FeignClient注解中声明则代表某服务
7.3、Feign的性能优化
1、日志级别尽量用Basic
2、使用HttpClient或OKHttp替代URLConnection
- ① 引入feign-httpClient依赖
- ② 配置文件开启httpClient功能,设置连接池参数
7.4、Feign的最佳实践
方式一(继承):给消费者的FeignClient和提供者的controller定义同一的父接口为标准
弊端:
- ① 服务紧耦合
- ② 父接口参数列表中的映射不会被继承
方式二(抽取):将FeignClient抽取为独立模块,并且把接口有关的POJO、默认的Feign配置都放到这个模块中,提供给所有消费者使用
实现最佳实践方式二的步骤如下:
- 1. 首先创建一个moudle,命名为feign-api,然后引入feign的starter依赖
- 2. 将order-service中编写的UserClient、User、DefaultFeignConfiguration都复制到feign-api项目中
- 3. 在order-service中引入feign-api的依赖
- 4. 修改order-service中的所有与上述三个组件有关的import部分,改成导入feign-api中的包
- 5. 重启测试
重启之后出现的问题,因为新建的模块在不同包下,所以在order-service项目启动时,无法引入
不同包的FeignClient的导入有两种方式:
- ① 在@EnableFeignClient注解中添加basePackages,指定FeignClient所在的包
- ② 在@EnableFeignClient注解中添加clients,指定具体FeignClient的字节码
8、Gateway网关
8.1、网关的作用
- 1、对用户请求做身份认证、权限校验
- 2、将用户请求路由到微服务,并实现负载均衡
- 3、对用户请求做限流
8.2、统一网关Gateway-搭建网关服务
8.2.1、网关搭建步骤
- 1、创建项目,引入nacos服务发现和gateway依赖
- 2、配置application.yml,包括服务基本信息、nacos地址、路由
8.2.2、路由配置包括
- 1、路由id:路由的唯一标识
- 2、路由目标(uri):路由的目标地址,http代表固定地址,lb代表根据服务名负载均衡
- 3、路由断言(predicates):判断路由的规则
- 4、路由过滤器(filters):对请求或响应做处理
8.3、统一网关Gateway-路由断言工厂
PredicateFactory的作用是什么?
- 读取用户的断言条件,对请求做出判断
Path = /user/**是什么含义?
- 路径以/user开头的就认为是符合的
8.4、统一网关Gateway-过滤器工厂
过滤器的作用是什么?
- ① 对路由的请求或响应做加工处理,比如添加请求头
- ② 配置在路由下的过滤器只对当前路由的请求生效
defaultFilters的作用是什么?
- ① 对所有都生效的过滤器
8.5、统一网关Gateway-全局过滤器
全局过滤器的作用是什么?
- 对所有路由都生效的过滤器,并且可以自定义处理逻辑
实现全局过滤器的步骤?
- ① 实现GlobalFilter接口
- ② 添加@Order注解或实现Ordered接口
- ③ 编写处理逻辑
8.6、统一网关Gateway-过滤器执行顺序
路由过滤器、defaultFilter、全局过滤器的执行顺序?
- ① order值越小,优先级越高
- ② 当order值一样时,顺序是defaultFilter最先,然后是局部的路由过滤器,最后是全局过滤器
9、Docker
9.1、初识Docker
Docker是一个快速交付应用、运行应用的技术:
- 1、可以将程序及其依赖、运行环境一起打包为一个镜像,可以迁移到任意Linux操作系统
- 2、运行时利用沙箱机制形成隔离容器,各个应用互补干扰
- 3、启动、移除都可以通过一行命令完成,方便快捷
9.2、Docker和虚拟机的差别
- 1、docker是一个系统进程;虚拟机是在操作系统中的操作系统
- 2、docker体积小、启动速度快、性能好;虚拟机体积大、启动速度慢、性能一般
9.3、Docker架构
镜像:
- 将应用程序及其依赖、环境、配置打包在一起
容器:
- 镜像运行起来就是容器,一个镜像可以运行多个容器
Docker结构:
- 服务端:接收命令或远程请求,操作镜像或容器
- 客户端:发送命令或者请求到Docker服务端
DockerHub:
- 一个镜像托管的服务器,类似的还有阿里云镜像服务,统称为DockerRegistry
9.4、镜像命令
- docker images 查看镜像
- docker rmi 删除镜像
- docker pull 拉取镜像
- docker push
- docker save 将镜像保存为tar文件
- docker load 将tar文件保存为镜像
9.5、容器命令
docker run 命令的常见参数有哪些?
- --name : 指定容器名称
- -p : 指定端口映射
- -d : 让容器后台运行
查看容器日志的命令:
- docker logs
- 添加 -f 参数可以持续查看日志
查看容器状态
- docker ps
删除容器
- docker rm
- 不能删除运行中的容器,除非添加 -f 参数
进入容器
- 命令是docker exec -it [容器名] [要执行的命令]
- exec命令可以进入容器修改文件,但是在容器内修改文件是不推荐的
9.6、数据卷命令
数据卷的作用:
- 将容器与数据分离,解耦合,方便操作容器内数据,保证数据安全
数据卷操作:
- docker volume create 创建数据卷
- docker volume ls 查看数据卷
- docker volume inspect 查看某个数据卷的详细信息
- docker volume rm 删除一个或多个数据卷
- docker volume prune 删除未使用过的数据卷
数据卷挂载方式:
- -v valomeName:/targetContainerPath
- 如果容器运行时volume不存在,会自动创建出来
数据卷管理:
1、docker run 的命令中通过 -v 参数挂载文件或目录到容器中
- ① -v volume名称 : 容器内目录
- ② -v 宿主机文件 : 容器内文件
- ③ -v 宿主机目录 : 容器内目录
2、数据卷挂载与目录直接挂载的
- ① 数据卷挂载耦合度低,由docker来管理目录,但是目录较深,不好找
- ② 目录挂载耦合度高,需要我们自己管理目录,不过目录容易寻找查看
9.7、镜像结构
镜像是分层结构,每一层称为一个Layer
- BaseImage层:包含基本的系统函数库、环境变量、文件系统
- Entrypoint:入口,是镜像中应用启动的命令
- 其它:在BaseImage基础上添加依赖、安装程序、完成整个应用的安装和配置
9.8、自定义镜像
- 1、Dockerfile的本质是一个文件,通过指令描述镜像的构建过程
- 2、Dockerfile第一行必须是FROM,从一个基础镜像来构建
- 3、基础镜像可以是基本操作系统,如Ububtu。也可以是其它制作好的镜像,例如:java:8-apline
DockerCompose有什么作用?
- 帮助我们快速部署分布式应用,无需一个个微服务去构建镜像和部署。
9.9、自定义镜像仓库
- 1、推送本地镜像到仓库前都必须重命名(docker tag)镜像,以镜像仓库地址为前缀
- 2、镜像仓库推送前需要把仓库地址配置到docker服务的daemon.json文件中,被docker信任
- 3、推送使用 docker push 命令
- 4、拉取使用 docker pull 命令
10、MQ
10.1、初始MQ-同步通讯的优缺点
同步调用的优点:
- 时效性较强,可以立即得到结果
同步调用的问题:
- 耦合度高
- 性能和吞吐能力下降
- 有额外的资源消耗
- 有级联失效问题
10.2、初始MQ-异步通讯的优缺点
异步通讯的优点:
- 耦合度低
- 吞吐量提升
- 故障隔离
- 流量削峰
异步通讯的缺点:
- 依赖Broker的可靠性、安全性、吞吐能力
- 架构复杂了,业务没有明显的流程线,不好追踪管理
10.3、RabbitMQ快速入门
RabbitMQ的几个概念:
- channel:操作MQ的工具
- exchange:路由消息到队列中
- queue:缓存消息
- virtual host:虚拟主机,是对queue、exchange等资源逻辑分组
基本消息队列的消息发送流程:
- 1、建立connection
- 2、创建channel
- 3、利用channel声明队列
- 4、利用channel向队列发送消息
package cn.itcast.mq.helloworld;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import org.junit.Test;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
public class PublisherTest {
@Test
public void testSendMessage() throws IOException, TimeoutException {
// 1.建立连接
ConnectionFactory factory = new ConnectionFactory();
// 1.1.设置连接参数,分别是:主机名、端口号、vhost、用户名、密码
factory.setHost("192.168.16.3");
factory.setPort(5672);
factory.setVirtualHost("/");
factory.setUsername("liufang");
factory.setPassword("123456");
// 1.2.建立连接
Connection connection = factory.newConnection();
// 2.创建通道Channel
Channel channel = connection.createChannel();
// 3.创建队列
String queueName = "simple.queue";
channel.queueDeclare(queueName, false, false, false, null);
// 4.发送消息
String message = "hello, rabbitmq!";
channel.basicPublish("", queueName, null, message.getBytes());
System.out.println("发送消息成功:【" + message + "】");
// 5.关闭通道和连接
channel.close();
connection.close();
}
}
基本消息队列的消息接收流程:
- 1、建立connection
- 2、创建channel
- 3、利用channel声明队列
- 4、定义consumer的消费行为handleDelivery()
- 5、利用channel将消费者与队列绑定
package cn.itcast.mq.helloworld;
import com.rabbitmq.client.*;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
public class ConsumerTest {
public static void main(String[] args) throws IOException, TimeoutException {
// 1.建立连接
ConnectionFactory factory = new ConnectionFactory();
// 1.1.设置连接参数,分别是:主机名、端口号、vhost、用户名、密码
factory.setHost("192.168.16.3");
factory.setPort(5672);
factory.setVirtualHost("/");
factory.setUsername("liufang");
factory.setPassword("123456");
// 1.2.建立连接
Connection connection = factory.newConnection();
// 2.创建通道Channel
Channel channel = connection.createChannel();
// 3.创建队列
String queueName = "simple.queue";
channel.queueDeclare(queueName, false, false, false, null);
// 4.订阅消息
channel.basicConsume(queueName, true, new DefaultConsumer(channel){
@Override
public void handleDelivery(String consumerTag, Envelope envelope,
AMQP.BasicProperties properties, byte[] body) throws IOException {
// 5.处理消息
String message = new String(body);
System.out.println("接收到消息:【" + message + "】");
}
});
System.out.println("等待接收消息。。。。");
}
}
10.4、SpringAMQP
什么是AMQP?
- 应用间消息通信的一种协议,与语言和平台无关。
SpringAMQP如何发送消息?
- 引入amqp的starter依赖
- 配置RabbitMQ地址
- 利用RabbitTemplate的convertAndSend方法
SpringAMQP如何接收消息?
- 引入amqp的starter依赖
- 配置RabbitMQ地址
- 定义类,添加@Component注解
- 类中声明方法,添加@RabbitListener注解,方法参数接收消息
注意:消息一旦消费就会从队列删除,RabbitMQ没有消息回溯功能
Work模型的使用:
- 多个消费者绑定到一个队列,同一条消息只会被一个消费者处理
- 通过设置prefetch来控制消费者预取的消息数量
10.4.1、SpringAMQP-FanoutExchange
交换机的作用是什么?
- 接收publisher发送的消息
- 将消息按照规则路由到与之绑定的队列
- 不能缓存消息,路由失败,消息丢失
- FanoutExchange会将消息路由到每个绑定的队列中
声明队列、交换机、绑定关系的Bean是什么?
- Queue
- FanoutExchange
- Binding
10.4.2、SpringAMQP-DirectExhange、TopicExchange
描述下Direct交换机与Fanout交换机的差别?
- Fanout交换机将消息路由给每一个与之绑定的队列
- Direct交换机根据RoutingKey判断路由给哪个队列
- 如果多个队列具有相同的RoutingKey,则与Fanout功能类似
基于@RabbitListener注解声明队列和交换机有哪些常见注解?
- @Queue
- @Exchange
描述下Topic交换机与Direct交换机的差异?
- 交换机类型不同,一个是TOPIC,一个是DIRECT
- TopicExchange交换机的RoutingKey使用的是通配符,而DirectExchange交换机的RoutingKey使用的是完整的key名称
10.4.3、SpringAMQP-消息转换器
SpringAMQP中消息的序列化和反序列化是怎么实现的?
- 利用MessageConverter实现的,默认是JDK的序列化
- 注意发送方与接收方必须使用相同的MessageConverter
RabbitMQ只支持字节,Spring却允许我们发Object对象,说明它会将我们的对象做序列化,用的是java序列化也就是JDK序列化(ObjetOutputStream)
11、ElasticSearch
11.1、初识elasticsearch
什么是elasticsearch?
- 一个开源的分布式搜索引擎,可以用来实现搜索、日志统计、分析、系统监控等功能
什么是elastic stack(ELK)?
- 是以elasticsearch为核心的技术栈,包括beats、Logstash、kibana、elasticsearch
什么是Lucene?
- 是Apache的开源搜索引擎类库,提供了搜索引擎的核心API
11.1.1、初识elasticsearch-正向索引和倒排索引
什么是文档和词条?
- 每一条数据就是一个文档
- 对文档中的内容分词,得到的词语就是词条
什么是正向索引?
- 基于文档id创建索引,查询词条时必须先找到文档,而后判断是否包含词条
什么是倒排索引?
- 对文档内容分词,对词条创建索引,并记录词条所在文档的信息。查询时先根据词条查询到文档id,而后获得到文档
11.1.2、初识elasticsearch-分词器
分词器的作用是什么?
- 创建倒排索引时对文档分词
- 用户搜索时,对输入的内容分词
IK分词器有几种模式?
- ik_smart:智能切分,粗粒度
- ik_max_word:最细切分,细粒度
IK分词器如何拓展词条?如何停用词条?
- 利用config目录的IKAnalyzer.cfg.xml文件添加拓展词典和停用词典
- 在词典中添加拓展词条或者停用词条
11.2、操作索引库
11.2.1、mapping属性
mapping常见属性有哪些?
- type:数据类型
- index:是否索引
- analyzer:分词器
- properties:子字段
type常见的有哪些?
- 字符串:text、keyword
- 数字:long、integer、short、byte、double、float
- 布尔:boolean
- 日期:date
- 对象:object
11.2.2、创建索引库
索引库操作有哪些?
- 创建索引库:PUT /索引库名
- 查询索引库:GET /索引库名
- 删除索引库:DELETE /索引库名
- 添加字段:DELETE /索引库名/_mapping
文档操作有哪些?
- 创建文档:POST /索引库名/_doc/文档id {json文档}
- 查询文档:GET /索引库名/_doc/文档id
- 删除文档:DELETE /索引库名/_doc/文档id
- 修改文档:
- 全量修改:PUT /索引库名/_doc/文档id {json文档}
- 增量修改:POST /索引库名/_update/文档id {"doc": {字段}}
#创建索引库
PUT /heima
{
"mappings": {
"properties": {
"info": {
"type": "text",
"analyzer": "ik_smart"
},
"email": {
"type": "keyword",
"index": false
},
"name": {
"properties": {
"firstName": {
"type": "keyword"
},
"lastName": {
"type": "keyword"
}
}
}
}
}
}
#查询
GET /heima
#修改,添加字段
PUT /heima/_mapping
{
"properties": {
"age": {
"type": "integer"
}
}
}
#删除
DELETE /heima
#插入文档
POST /heima/_doc/1
{
"info": "程序员",
"email": "2531994628@qq.com",
"name": {
"firstName": "云",
"lastName": "赵"
}
}
#查询文档
GET /heima/_doc/1
#删除文档
DELETE /heima/_doc/1
#全量修改文档
PUT /heima/_doc/1
{
"info": "程序员",
"email": "ZhaoYun@qq.com",
"name": {
"firstName": "云",
"lastName": "赵"
}
}
#局部修改文档字段
POST /heima/_update/1
{
"doc": {
"email": "253@qq.com"
}
}
11.2.3、RestClient操作索引库
索引库操作的基本步骤
- 初始化RestHighLevelClient
- 创建xxxIndexRequest。XXX是CREATE、Get、Delete
- 准备DSL(CREATE时需要)
- 发起请求。调用RestHighLevelClient#indices().xxx方法
- xxx是create、exists、delete
package cn.itcast.hotel.constants;
public class HotelConstants {
public final static String MAPPING_TEMPLATE = "{\n" +
" \"mappings\": {\n" +
" \"properties\": {\n" +
" \"id\": {\n" +
" \"type\": \"keyword\"\n" +
" },\n" +
" \"name\": {\n" +
" \"type\": \"text\",\n" +
" \"analyzer\": \"ik_max_word\",\n" +
" \"copy_to\": \"all\"\n" +
" },\n" +
" \"address\": {\n" +
" \"type\": \"keyword\",\n" +
" \"index\": false\n" +
" },\n" +
" \"price\": {\n" +
" \"type\": \"integer\"\n" +
" },\n" +
" \"score\": {\n" +
" \"type\": \"integer\"\n" +
" },\n" +
" \"brand\": {\n" +
" \"type\": \"keyword\"\n" +
" },\n" +
" \"city\": {\n" +
" \"type\": \"keyword\"\n" +
" },\n" +
" \"standName\": {\n" +
" \"type\": \"keyword\"\n" +
" },\n" +
" \"business\": {\n" +
" \"type\": \"keyword\",\n" +
" \"copy_to\": \"all\"\n" +
" },\n" +
" \"location\": {\n" +
" \"type\": \"geo_point\"\n" +
" },\n" +
" \"pic\": {\n" +
" \"type\": \"keyword\",\n" +
" \"index\": false\n" +
" },\n" +
" \"all\": {\n" +
" \"type\": \"text\",\n" +
" \"analyzer\": \"ik_max_word\"\n" +
" }\n" +
" }\n" +
" }\n" +
"}";
}
package cn.itcast.hotel;
import cn.itcast.hotel.constants.HotelConstants;
import org.apache.http.HttpHost;
import org.elasticsearch.action.admin.indices.delete.DeleteIndexRequest;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestClient;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.client.indices.CreateIndexRequest;
import org.elasticsearch.client.indices.GetIndexRequest;
import org.elasticsearch.common.xcontent.XContentType;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import java.io.IOException;
@SpringBootTest
public class HotelIndexTest {
private RestHighLevelClient client;
/**
* 创建索引库
* @throws IOException
*/
@Test
void testCreatHotelIndex() throws IOException {
//1、创建Request对象
CreateIndexRequest request = new CreateIndexRequest("hotel");
//2、请求参数,MAPPING_TEMPLATE是静态常量字符串,内容是创建索引库的DSL语句
request.source(HotelConstants.MAPPING_TEMPLATE, XContentType.JSON);
//3、发起请求
client.indices().create(request, RequestOptions.DEFAULT);
}
/**
* 删除索引库
* @throws IOException
*/
@Test
void testDeleteHotelIndex() throws IOException{
//1、创建Request对象
DeleteIndexRequest request = new DeleteIndexRequest("hotel");
//2、发起请求,删除索引库
client.indices().delete(request,RequestOptions.DEFAULT);
}
/**
* 判断索引库是否存在
* @throws IOException
*/
@Test
void testExistHotelIndex() throws IOException{
//1、创建Request对象
GetIndexRequest request = new GetIndexRequest("hotel");
//2、发起请求,判断是否存在
boolean exists = client.indices().exists(request,RequestOptions.DEFAULT);
//3、输出
System.out.println(exists ? "索引库已经存在!" : "索引库不存在!");
}
@BeforeEach
void setUp(){
this.client = new RestHighLevelClient(RestClient.builder(
HttpHost.create("http://192.168.16.3:9200")
));
}
@AfterEach
void tearDown(){
try {
client.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
11.2.4、RestClient操作文档
文档操作的基本步骤:
- 初始化RestHighLevelClient
- 创建xxxRequest。xxx是Index、Get、Update、Delete
- 准备参数(Index和Update时需要)
- 发送请求。调用RestHighLevelClient#.xxx()方法,xxx时index、get、update、delete
- 解析结果(Get时需要)
package cn.itcast.hotel;
import cn.itcast.hotel.pojo.Hotel;
import cn.itcast.hotel.pojo.HotelDoc;
import cn.itcast.hotel.service.IHotelService;
import com.alibaba.fastjson.JSON;
import org.apache.http.HttpHost;
import org.elasticsearch.action.delete.DeleteRequest;
import org.elasticsearch.action.get.GetRequest;
import org.elasticsearch.action.get.GetResponse;
import org.elasticsearch.action.index.IndexRequest;
import org.elasticsearch.action.update.UpdateRequest;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestClient;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.common.xcontent.XContentType;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import java.io.IOException;
@SpringBootTest
public class HotelDocumentTest {
@Autowired
private IHotelService hotelService;
@Autowired
private RestHighLevelClient client;
/**
* 新增文档
* @throws IOException
*/
@Test
void testAddDocument() throws IOException {
//根据id查询酒店数据
Hotel hotel = hotelService.getById(61083L);
//转换为文档类型
HotelDoc hotelDoc = new HotelDoc(hotel);
//1、准备Request对象
IndexRequest request = new IndexRequest("hotel").id(hotelDoc.getId().toString());
//2、准备JSON文档
request.source(hotelDoc, XContentType.JSON);
//3、发送请求
client.index(request, RequestOptions.DEFAULT);
}
/**
* 查询文档
* @throws IOException
*/
@Test
void GetDocumentById() throws IOException{
//1、准备request对象
GetRequest request = new GetRequest("hotel","61083");
//2、发送请求,得到响应
GetResponse response = client.get(request,RequestOptions.DEFAULT);
//3、解析响应结果
String json = response.getSourceAsString();
// HotelDoc hotelDoc = JSON.parseObject(json,HotelDoc.class);
System.out.println(json);
}
/**
* 更新文档
* @throws IOException
*/
@Test
void testUpdateDocument() throws IOException {
//1、准备Request对象
UpdateRequest request = new UpdateRequest("hotel","61083");
//2、准备请求参数
request.doc(
"price","952",
"starName","四钻"
);
//3、发送请求
client.update(request,RequestOptions.DEFAULT);
}
/**
* 删除文档
* @throws IOException
*/
@Test
void testDeleteDocument() throws IOException{
//1、准备Request对象
DeleteRequest request = new DeleteRequest("hotel","61083");
//2、发送请求
client.delete(request,RequestOptions.DEFAULT);
}
@BeforeEach
void setUp(){
this.client = new RestHighLevelClient(RestClient.builder(
HttpHost.create("http://192.168.16.3:9200")
));
}
@AfterEach
void tearDown() throws IOException{
this.client.close();
}
}
将数据库中的数据批量导入到索引库中
/**
* 批量导入文档
* @throws IOException
*/
@Test
void testBulkDocument() throws IOException{
//批量查询酒店数据
List<Hotel> hotels = hotelService.list();
//1、创建request对象
BulkRequest request = new BulkRequest();
//2、准备参数,添加多个新增的Request
for (Hotel hotel : hotels) {
//转换文档类型HotelDoc
HotelDoc hotelDoc = new HotelDoc(hotel);
//创建新增文档的Request对象
request.add(new IndexRequest("hotel")
.id(hotelDoc.getId().toString())
.source(JSON.toJSONString(hotelDoc),XContentType.JSON));
}
//3、发送请求
client.bulk(request,RequestOptions.DEFAULT);
}
11.3、DSL查询语法
11.3.1、DSL查询语法-match查询
查询DSL的基本语法是什么?
- GET /索引库名/_search
- { "query": { "查询类型": { "FIELD: TEXT" } } }
###查询所有
GET /hotel/_search
{
"query": {
"match_all": {}
}
}
11.3.2、DSL查询语法-全文检索查询
match和multi_match的区别是什么?
- match:根据一个字段查询
- multi_match:根据多个字段查询,参与查询字段越多,查询性能越差
###match查询
GET /hotel/_search
{
"query": {
"match": {
"all": "如家外滩"
}
}
}
###multi_match查询
GET /hotel/_search
{
"query": {
"multi_match": {
"query": "如家外滩",
"fields": ["brand","name","business"]
}
}
}
11.3.3、DSL查询语法-精确查询
精确查询常见的有哪些?
- term查询:根据词条精确匹配,一般搜索keyword类型、数值类型、布尔类型、日期类型字段
- range查询:根据数值范围查询,可以是数值、日期范围
### term查询
GET /hotel/_search
{
"query": {
"term": {
"city": {
"value": "广东"
}
}
}
}
### range查询
GET /hotel/_search
{
"query": {
"range": {
"price": {
"gte": 1000,
"lte": 2000
}
}
}
}
11.3.4、DSL查询语法-地理查询
- 以点为坐标,distance为半径,画圆,查找在圆内的所有酒店数据
### geo_distance查询
GET /hotel/_search
{
"query": {
"geo_distance": {
"distance": "2km",
"location": "31.21,121.5"
}
}
}
11.3.5、DSL查询语法-相关性算分
elasticsearch中的相关性打分算法是什么?
- TF-IDF:在elasticsearch5.0之前,会随着词频增加而越来越大
- BM25:在elasticsearch5.0之后,会随着词频增加而增大,但增长水平会趋于水平
11.3.6、DSL查询语法-Function Score Query
function score query定义的三要素是什么?
- 过滤条件:哪些文档要加分
- 算分函数:如何计算function score
- 加权方式:function score 与 query score 如何运算
### function score query 打分算法查询
GET /hotel/_search
{
"query": {
"function_score": {
"query": {
"match": {
"all": "外滩"
}
},
"functions": [
{
"filter": {
"term": {
"brand": "如家"
}
},
"weight": 10
}
],
"boost_mode": "multiply"
}
}
}
11.3.7、DSL查询语句-Boolean Query
bool查询有几种逻辑关系?
- must:必须匹配的条件,可以理解为“与”
- should:选择性匹配的条件,可以理解为“或”
- must_not:必须不匹配的条件,不参与打分
- filter:必须匹配的条件,不参与打分
利用bool查询实现功能
需求:搜索名字包含“如家”,价格不高于400,在坐标31.21,121.5周围10km范围内的酒店数据。
### bool查询
GET /hotel/_search
{
"query": {
"bool": {
"must": [
{
"match": {
"name": "如家"
}
}
],
"must_not": [
{
"range": {
"price": {
"gt": 400
}
}
}
],
"filter": [
{
"geo_distance": {
"distance": "10km",
"location": {
"lat": 31.21,
"lon": 121.5
}
}
}
]
}
}
}
11.4、搜索结果处理
11.4.1、搜索结果处理-排序
案例1:对酒店数据按照用户评价降序排序,评价相同的按照价格升序排序
### sort排序
GET /hotel/_search
{
"query": {
"match_all": {}
},
"sort": [
{
"score": "desc"
},
{
"price": "asc"
}
]
}
案例2:实现对酒店数据按照到你的位置坐标的距离升序排序
### 找到121.5,31.21周围的酒店,距离升序排序
GET /hotel/_search
{
"query": {
"match_all": {}
},
"sort": [
{
"_geo_distance": {
"location": {
"lat": 31.21,
"lon": 121.5
},
"order": "asc",
"unit": "km"
}
}
]
}
11.4.2、搜索结果处理-分页
from + size:
- 优点:支持随机翻页
- 缺点:深度分页问题,默认查询上限(from + size)是10000
- 场景:百度、京东、谷歌、淘宝这样的随机翻页搜索
after search:
- 优点:没有查询上限(单次查询的size不超过10000)
- 缺点:只能向后逐页查询,不支持随机翻页
- 场景:没有随机翻页需求的搜索,例如手机向下滚动翻页
scroll:
- 优点:没有查询上限(单次查询的size不超过10000)
- 缺点:会有额外内存消耗,并且搜索结果是非实时的
- 场景:海量数据的获取和迁移。从ES7.1开始不推荐,建议用after search方案
###分页查询
GET /hotel/_search
{
"query": {
"match_all": {}
},
"sort": [
{
"price": "asc"
}
],
"from": 20,
"size": 10
}
11.4.3、搜索结果处理-高亮
###高亮查询,默认情况下,ES搜索字段必须与高亮字段一致
GET /hotel/_search
{
"query": {
"match": {
"all": "如家"
}
},
"highlight": {
"fields": {
"name": {
"require_field_match": "false"
}
}
}
}
11.4.4、搜索结果处理整体语法
GET /hotel/_search
{
"query": {
"match": {
"name": "如家"
}
},
"from": 20, //分页开始的位置
"size": 10, //期望获取的文档总数
"sort": [
{
"price": "desc" //普通排序
},
{
"_geo_distance": { //距离排序
"location": {
"lat": 31.21,
"lon": 121.5
},
"order": "asc",
"unit": "km"
}
}
],
"highlight": {
"fields": { //高亮字段
"name": {
"pre_tags": "<em>", //用来标记高亮字段的前置标签
"post_tags": "<em>", //用来标记高亮字段的后置标签
"require_field_match": "false" //搜索字段与高亮是否需要匹配,默认是true
}
}
}
}
11.5、RestClient查询文档
11.5.1、快速入门
查询的基本步骤:
- 1、创建SearchRequest对象
- 2、准备Request.source(),也就是DSL
①QueryBuilders来构建查询条件
②传入Request.source()的query()方法
- 3、发送请求,得到结果
- 4、解析结果(参考JSON结果,从外到内,逐层解析)
11.5.2、结果处理
- 所有搜索DSL的构建,记住一个API:SearchRequest的source()方法
- 高亮结果解析是参考JSON结果,逐层解析
package cn.itcast.hotel;
import cn.itcast.hotel.pojo.HotelDoc;
import com.alibaba.fastjson.JSON;
import org.apache.http.HttpHost;
import org.elasticsearch.action.search.SearchRequest;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestClient;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.index.query.BoolQueryBuilder;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.search.SearchHit;
import org.elasticsearch.search.SearchHits;
import org.elasticsearch.search.fetch.subphase.highlight.HighlightBuilder;
import org.elasticsearch.search.fetch.subphase.highlight.HighlightField;
import org.elasticsearch.search.sort.SortOrder;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.util.CollectionUtils;
import java.io.IOException;
import java.util.Map;
@SpringBootTest
public class HotelSearchTest {
private RestHighLevelClient client;
/**
* 查询所有酒店数据
* @throws IOException
*/
@Test
void testMatchAll() throws IOException{
//1、准备Request
SearchRequest request = new SearchRequest("hotel");
//2、准备DSL
request.source().query(QueryBuilders.matchAllQuery());
//3、发送请求
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
handlerResponce(response);
}
/**
* 全文检索
* @throws IOException
*/
@Test
void testMatch() throws IOException{
//1、准备Request对象
SearchRequest request = new SearchRequest("hotel");
//2、准备DSL
request.source().query(QueryBuilders.matchQuery("name","如家"));
//3、发送请求
SearchResponse response = client.search(request,RequestOptions.DEFAULT);
handlerResponce(response);
}
/**
* bool查询
* @throws IOException
*/
@Test
void testBool() throws IOException{
//1、准备Request对象
SearchRequest request = new SearchRequest("hotel");
//2、准备DSL
//2.1、准备BooleanQuery
BoolQueryBuilder queryBuilder = QueryBuilders.boolQuery();
//2.2、添加term
queryBuilder.must(QueryBuilders.termQuery("city","上海"));
//2.3、添加range
queryBuilder.filter(QueryBuilders.rangeQuery("price").lt(250));
request.source().query(queryBuilder);
//3、发送请求
SearchResponse response = client.search(request,RequestOptions.DEFAULT);
handlerResponce(response);
}
/**
* 排序和分页
* @throws IOException
*/
@Test
void testPageAndSort() throws IOException{
int page = 1,size = 5;
//1、创建Request对象
SearchRequest request = new SearchRequest("hotel");
//2、准备DSL
//2.1、查询所有数据matchAll
request.source().query(QueryBuilders.matchAllQuery());
//2.2、sort 排序
request.source().sort("price", SortOrder.ASC);
//2.3、page 分页
request.source().from((page - 1) * size).size(size);
//3、发送请求
SearchResponse response = client.search(request,RequestOptions.DEFAULT);
handlerResponce(response);
}
/**
* 高亮显示查询
* @throws IOException
*/
@Test
void testHighLighter() throws IOException{
//1、创建Request对象
SearchRequest request = new SearchRequest("hotel");
//2、准备DSL
//2.1、query
request.source().query(QueryBuilders.matchQuery("all","如家"));
//2.2、高亮
request.source().highlighter(new HighlightBuilder().field("name").requireFieldMatch(false));
//3、发送请求
SearchResponse response = client.search(request,RequestOptions.DEFAULT);
handlerResponce(response);
}
/**
* 解析
* @param response
*/
private void handlerResponce(SearchResponse response) {
//4、解析结果
SearchHits searchHits = response.getHits();
//4.1查询的总条数
long total = searchHits.getTotalHits().value;
System.out.println("共搜索到" + total + "条数据");
//4.2文档数组
SearchHit[] hits = searchHits.getHits();
//4.3遍历
for (SearchHit hit : hits) {
//获取文档source
String json = hit.getSourceAsString();
//反序列化
HotelDoc hotelDoc = JSON.parseObject(json,HotelDoc.class);
//获取高亮结果
Map<String, HighlightField> highlightFields = hit.getHighlightFields();
if (!CollectionUtils.isEmpty(highlightFields)){
//根据字段名获取高亮结果
HighlightField highlightField = highlightFields.get("name");
if (highlightField != null){
//获取高亮值
String name = highlightField.getFragments()[0].string();
//覆盖非高亮结果
hotelDoc.setName(name);
}
}
System.out.println("hotelDoc = "+ hotelDoc);
}
}
@BeforeEach
void setUp(){
this.client = new RestHighLevelClient(RestClient.builder(
HttpHost.create("http://192.168.16.3:9200")
));
}
@AfterEach
void tearDown() throws IOException {
client.close();
}
}
11.5.3、案例
搜索、分页、条件过滤、附近的酒店、广告置顶
package cn.itcast.hotel.service.impl;
import cn.itcast.hotel.mapper.HotelMapper;
import cn.itcast.hotel.pojo.Hotel;
import cn.itcast.hotel.pojo.HotelDoc;
import cn.itcast.hotel.pojo.PageResult;
import cn.itcast.hotel.pojo.RequestParams;
import cn.itcast.hotel.service.IHotelService;
import com.alibaba.fastjson.JSON;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.elasticsearch.action.search.SearchRequest;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.common.geo.GeoPoint;
import org.elasticsearch.common.unit.DistanceUnit;
import org.elasticsearch.index.query.BoolQueryBuilder;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.index.query.functionscore.FunctionScoreQueryBuilder;
import org.elasticsearch.index.query.functionscore.ScoreFunctionBuilders;
import org.elasticsearch.search.SearchHit;
import org.elasticsearch.search.SearchHits;
import org.elasticsearch.search.sort.SortBuilders;
import org.elasticsearch.search.sort.SortOrder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
@Service
public class HotelService extends ServiceImpl<HotelMapper, Hotel> implements IHotelService {
@Autowired
private RestHighLevelClient client;
@Override
public PageResult search(RequestParams params) {
try {
//1、创建request对象
SearchRequest request = new SearchRequest("hotel");
//2、准备DSL
//2.1、query
bulidBasicQuery(params, request);
//2.3、分页
int page = params.getPage();
int size = params.getSize();
request.source().from((page - 1) * size).size(size);
//2.4、排序
if (params.getLocation() != null && !params.getLocation().equals("")){
request.source().sort(SortBuilders.geoDistanceSort("location",new GeoPoint(params.getLocation()))
.order(SortOrder.ASC)
.unit(DistanceUnit.KILOMETERS));
}
//3、发送请求,得到响应
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
//4、解析响应
return handlerResponce(response);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
/**
* 条件过滤
* @param params 参数
* @param request 搜索请求
*/
private void bulidBasicQuery(RequestParams params, SearchRequest request) {
//2.2构建BooleanQuery
BoolQueryBuilder queryBuilder = new BoolQueryBuilder();
String key = params.getKey();
if (key == null || "".equals(key)) {
queryBuilder.must(QueryBuilders.matchAllQuery());
} else {
queryBuilder.must(QueryBuilders.matchQuery("all", key));
}
//2.3、条件过滤
//2.3.1、城市条件
if (params.getCity() != null && !params.getCity().equals("")){
queryBuilder.filter(QueryBuilders.termQuery("city",params.getCity()));
}
//2.3.2、品牌条件
if (params.getBrand() != null && !params.getBrand().equals("")){
queryBuilder.filter(QueryBuilders.termQuery("brand",params.getBrand()));
}
//2.3.3、星级条件
if (params.getStarName() != null && !params.getStarName().equals("")){
queryBuilder.filter(QueryBuilders.termQuery("starName",params.getStarName()));
}
//2.3.4、价格条件
if (params.getMinPrice() != null && params.getMaxPrice() != null){
queryBuilder.filter(QueryBuilders.rangeQuery("price").gte(params.getMinPrice()).lte(params.getMaxPrice()));
}
//3、算分控制
FunctionScoreQueryBuilder functionScoreQueryBuilder = QueryBuilders.functionScoreQuery(
//原始查询,相关性算分的查询
queryBuilder,
//function score的数组
new FunctionScoreQueryBuilder.FilterFunctionBuilder[]{
//其中的一个 function score 元素
new FunctionScoreQueryBuilder.FilterFunctionBuilder(
//过滤条件
QueryBuilders.termQuery("isAD",true),
//算分函数
ScoreFunctionBuilders.weightFactorFunction(10)
)
});
request.source().query(functionScoreQueryBuilder);
}
/**
* 解析
*
* @param response
*/
private PageResult handlerResponce(SearchResponse response) {
//4、解析结果
SearchHits searchHits = response.getHits();
//4.1查询的总条数
long total = searchHits.getTotalHits().value;
//4.2文档数组
SearchHit[] hits = searchHits.getHits();
//4.3遍历
List<HotelDoc> hotels = new ArrayList<>();
for (SearchHit hit : hits) {
//获取文档source
String json = hit.getSourceAsString();
//反序列化
HotelDoc hotelDoc = JSON.parseObject(json, HotelDoc.class);
//获取排序值
Object[] sortValues = hit.getSortValues();
if (sortValues.length > 0){
Object sortValue = sortValues[0];
hotelDoc.setDistance(sortValue);
}
hotels.add(hotelDoc);
}
//4.4、封装返回
return new PageResult(total,hotels);
}
}
11.6、数据聚合
11.6.1、数据聚合-聚合的分类
什么是聚合?
- 聚合是对文档数据的统计、分析、计算
聚合的常见种类有哪些?
- Bucket:对文档数据分组,并统计每组数量
- Metric:对文档数据做计算,例如avg
- Pipline:基于其它聚合结果再做聚合
参与聚合的字段类型必须是:
- keyword
- 数值
- 日期
- 布尔
11.6.2、数据聚合-DSL实现Buckey聚合(桶聚合)
aggs代表聚合,与query同级,此时query的作用是?
- 限定文档的聚合范围
聚合必须的三要素:
- 聚合名称
- 聚合类型
- 聚合字段
聚合可配置属性有:
- size:指定聚合结果数量
- order:指定聚合结果排序方式
- field:指定聚合字段
###聚合功能,自定义排序规则
GET /hotel/_search
{
"size": 0,
"aggs": {
"brandAgg": {
"terms": {
"field": "brand",
"size": 10,
"order": {
"_count": "asc"
}
}
}
}
}
###聚合功能,限定聚合范围
GET /hotel/_search
{
"query": {
"range": {
"price": {
"lte": 200
}
}
},
"size": 0,
"aggs": {
"brandAgg": {
"terms": {
"field": "brand",
"size": 10,
"order": {
"_count": "asc"
}
}
}
}
}
11.6.3、数据聚合-DSL实现Metrics聚合(嵌套聚合)
### 嵌套聚合Metrics
GET /hotel/_search
{
"size": 0,
"aggs": {
"brandAgg": {
"terms": {
"field": "brand",
"size": 10,
"order": {
"scoreAgg.avg": "desc"
}
},
"aggs": {
"scoreAgg": {
"stats": {
"field": "score"
}
}
}
}
}
}
/**
* 数据聚合 多条件聚合
* @return
*/
@Override
public Map<String, List<String>> filters() {
try {
//1、创建Request对象
SearchRequest request = new SearchRequest("hotel");
//2、准备DSL
//2.1、query
// bulidBasicQuery(params, request);
//2.2、设置size
request.source().size(0);
//2.3、聚合
bulidAggregation(request);
//3、发送请求
SearchResponse response = client.search(request,RequestOptions.DEFAULT);
//4、解析结果
Map<String,List<String>> result = new HashMap<>();
Aggregations aggregations = response.getAggregations();
List<String> brandList = getAggByName(aggregations, "brandAgg");
//4.4、放入map
result.put("品牌",brandList);
List<String> cityList = getAggByName(aggregations, "cityAgg");
//4.4、放入map
result.put("城市",cityList);
List<String> starList = getAggByName(aggregations, "starAgg");
//4.4、放入map
result.put("星级",starList);
return result;
} catch (IOException e) {
throw new RuntimeException(e);
}
}
/**
* 封装 聚合
* @param request
*/
private void bulidAggregation(SearchRequest request) {
request.source().aggregation(AggregationBuilders.terms("brandAgg").field("brand").size(100));
request.source().aggregation(AggregationBuilders.terms("cityAgg").field("city").size(100));
request.source().aggregation(AggregationBuilders.terms("starAgg").field("starName").size(100));
}
/**
* 根据聚合名称获取聚合结果
* @param aggregations
* @return
*/
private List<String> getAggByName(Aggregations aggregations,String aggName) {
//4.1、根据聚合名称获取聚合结果
Terms terms = aggregations.get(aggName);
//4.2、获取buckets
List<? extends Terms.Bucket> buckets = terms.getBuckets();
//4.3、遍历
List<String> list = new ArrayList<>();
for (Terms.Bucket bucket : buckets) {
//4.4、获取key
String key = bucket.getKeyAsString();
list.add(key);
}
return list;
}
11.7、自动补全
11.7.1、自动补全-自定义分词器
如何使用拼音分词器?
①下载pinyin分词器
②解压并放到elasticsearch的plugin目录
③重启即可
如何自定义分词器?
创建索引时,在settings中配置,可以包含三部分:
- character filter
- tockenizer
- filter
拼音分词器注意事项?
为了避免搜索到同音字,搜索时尽量不要使用拼音分词器
### 自定义拼音分词器
PUT /test
{
"settings": {
"analysis": {
"analyzer": {
"my_analyzer": {
"tokenizer": "ik_max_word",
"filter": "py"
}
},
"filter": {
"py": {
"type": "pinyin",
"keep_full_pinyin": false,
"keep_joined_full_pinyin": true,
"keep_original": true,
"limit_first_letter_length": 16,
"remove_duplicated_term": true,
"none_chinese_pinyin_tokenize": false
}
}
}
}
, "mappings": {
"properties": {
"name": {
"type": "text",
"analyzer": "my_analyzer",
"search_analyzer": "ik_smart"
}
}
}
}
### 查询自定义分词器数据
GET /test/_analyze
{
"text": ["查询自定义分词器数据"],
"analyzer": "my_analyzer"
}
### 添加文档数据
POST /test/_doc/1
{
"id": 1,
"name": "狮子"
}
POST /test/_doc/2
{
"id": 2,
"name": "虱子"
}
### 查询文档数据
GET /test/_search
{
"query": {
"match": {
"name": "掉入狮子笼咋办"
}
}
}
11.7.2、自动补全-DSL实现自动补全查询
自动补全对字段的要求?
- 类型是completion类型
- 字段值是多词条的数组
### 自动补全的索引库
PUT /test2
{
"mappings": {
"properties": {
"title":{
"type": "completion"
}
}
}
}
### 示例数据
POST /test2/_doc
{
"title": ["Sony", "WH-1000XM3"]
}
POST /test2/_doc
{
"title": ["SK-II", "PITERA"]
}
POST /test2/_doc
{
"title": ["Nintendo", "switch"]
}
### 自动补全查询
POST /test2/_search
{
"suggest": {
"title_suggest": {
"text": "w",
"completion": {
"field": "title",
"skip_duplicates": true,
"size": 10
}
}
}
}
/**
* 自动补全查询
* @param prefix
* @return
*/
@Override
public List<String> getSuggestions(String prefix) {
try {
//1、创建Request对象
SearchRequest request = new SearchRequest("hotel");
//2、准备DSL
request.source().suggest(new SuggestBuilder().addSuggestion(
"suggestions",
SuggestBuilders.completionSuggestion("suggestion")
.prefix(prefix)
.skipDuplicates(true)
.size(10)));
//3、发送请求
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
//4、解析结果
Suggest suggest = response.getSuggest();
//4.1、根据补全名称获取补全结果
CompletionSuggestion suggestion = suggest.getSuggestion("suggestions");
//4.2、获取options
List<CompletionSuggestion.Entry.Option> options = suggestion.getOptions();
//4.3、遍历
List<String> list = new ArrayList<>();
for (CompletionSuggestion.Entry.Option option : options) {
String text = option.getText().toString();
list.add(text);
}
return list;
} catch (IOException e) {
throw new RuntimeException(e);
}
}
11.8、数据同步
11.8.1、数据同步-同步方案分析
方式一:同步调用
- 优点:实现简单,粗暴
- 缺点:业务耦合度高
方式二:异步通知
- 优点:低耦合,实现难度一般
- 缺点:依赖mq的可靠性
方式三:监听binlog
- 优点:完全解除服务间耦合
- 缺点:开启binlog增加数据库负担、实现复杂度高
11.9、ES集群
11.9.1、ES集群-集群职责及脑裂
master eligible节点的作用是什么?
- 参与集群选主
- 主节点可以管理集群状态、管理分片信息、处理创建和删除索引库的请求
data节点的作用是什么?
- 数据库的CRUD
coordinator节点的作用是什么?
- 路由请求到其它节点
- 合并查询到的结果,返回给用户
11.9.2、ES集群-分布式新增和查询流程
分布式新增如何确定分片?
- coordinating node(协调节点)根据id做hash运算,得到结果为shard数量取余,余数就是对应的分片
分布式查询:
- 分散阶段:coordinating node将查询请求分发给不同分片
- 收集阶段:将查询结果汇总到coordinating node,整理并返回给用户
11.9.3、ES集群-故障转移
故障转移:
- master宕机后,EligibleMaster(候选节点)选举为新的主节点
- master节点监控分片、节点状态,将故障节点上的分片转移到正常节点,确保数据安全