SpringCloud + RabbitMQ + Docker + Redis + 搜索 + 分布式--实用篇笔记

目录

 1、微服务框架

1.1、认识微服务

2、服务拆分及远程调用

2.1、微服务远程调用

2.1.1.微服务调用方式

2.2、分布式服务案例

2.2.1.服务调用关系

3、Eureka注册中心

3.1、搭建EurekaServer

3.2、服务注册

3.3、服务发现

4、Ribbon负载均衡

4.1、Ribbon负载均衡规则

4.2、负载均衡自定义方式

4.3、饥饿加载

5、Nacos注册中心

5.1、Nacos安装及入门

5.1.1、Nacos服务搭建

5.1.2、Nacos服务注册或发现

5.2、Nacos服务分级模型

5.2.1、Nacos服务分级存储模型

5.2.2、如何设置实例的集群属性

5.3、Nacos注册中心-NacosRule负载均衡策略

5.4、Nacos注册中心-加权负载均衡

5.5、Nacos环境隔离

5.6、Nacos与eureka对比

5.6.1、Nacos与eureka的共同点

5.6.2、Nacos与Eureka的区别

6、Nacos配置管理

7、http客户端Feign

7.1、基于feign远程调用

7.2、Feign的日志配置

7.3、Feign的性能优化

7.4、Feign的最佳实践

8、Gateway网关

8.1、网关的作用

8.2、统一网关Gateway-搭建网关服务

8.2.1、网关搭建步骤

8.2.2、路由配置包括

8.3、统一网关Gateway-路由断言工厂

8.4、统一网关Gateway-过滤器工厂

8.5、统一网关Gateway-全局过滤器

8.6、统一网关Gateway-过滤器执行顺序

9、Docker

9.1、初识Docker

9.2、Docker和虚拟机的差别

9.3、Docker架构

9.4、镜像命令

9.5、容器命令

9.6、数据卷命令

9.7、镜像结构

9.8、自定义镜像

9.9、自定义镜像仓库

 10、MQ

10.1、初始MQ-同步通讯的优缺点

10.2、初始MQ-异步通讯的优缺点

10.3、RabbitMQ快速入门

10.4、SpringAMQP

10.4.1、SpringAMQP-FanoutExchange

10.4.2、SpringAMQP-DirectExhange、TopicExchange

10.4.3、SpringAMQP-消息转换器

 11、ElasticSearch

11.1、初识elasticsearch

11.1.1、初识elasticsearch-正向索引和倒排索引

11.1.2、初识elasticsearch-分词器

11.2、操作索引库

11.2.1、mapping属性

11.2.2、创建索引库

11.2.3、RestClient操作索引库

11.2.4、RestClient操作文档

11.3、DSL查询语法

11.3.1、DSL查询语法-match查询

11.3.2、DSL查询语法-全文检索查询

11.3.3、DSL查询语法-精确查询

11.3.4、DSL查询语法-地理查询

11.3.5、DSL查询语法-相关性算分

11.3.6、DSL查询语法-Function Score Query

11.3.7、DSL查询语句-Boolean Query

11.4、搜索结果处理

11.4.1、搜索结果处理-排序

11.4.2、搜索结果处理-分页

11.4.3、搜索结果处理-高亮

11.4.4、搜索结果处理整体语法

11.5、RestClient查询文档

11.5.1、快速入门

11.5.2、结果处理

11.5.3、案例

11.6、数据聚合

11.6.1、数据聚合-聚合的分类

11.6.2、数据聚合-DSL实现Buckey聚合(桶聚合)

11.6.3、数据聚合-DSL实现Metrics聚合(嵌套聚合)

11.7、自动补全

11.7.1、自动补全-自定义分词器

11.7.2、自动补全-DSL实现自动补全查询

11.8、数据同步

11.8.1、数据同步-同步方案分析

11.9、ES集群

11.9.1、ES集群-集群职责及脑裂

11.9.2、ES集群-分布式新增和查询流程

11.9.3、ES集群-故障转移


 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节点监控分片、节点状态,将故障节点上的分片转移到正常节点,确保数据安全
要构建一个基于上述技术栈的应用程序,涉及多个组件和技术,下面是一些关键点的简介: 1. **Spring Boot**: 是一个快速开发框架,简化了Java应用的配置和启动过程。 - 示例:用于创建简单的RESTful API服务[^4]。 2. **Spring Cloud**: 提供了一组工具和服务来扩展微服务架构。 - 功能包括服务发现、配置中心、API网关等[^5]。 3. **RabbitMQ**: 消息队列服务,支持异步通信和解耦。 - 在Spring Cloud中集成,可以用来实现消息驱动架构[^6]。 4. **Redis**: 缓存数据库,提高应用程序性能。 - 可以缓存热点数据或会话信息[^7]。 5. **Elasticsearch**: 分布式搜索和分析引擎,常用于全文检索。 - 支持复杂查询和实时数据分析[^8]。 6. **Xxl-sso**: 企业级权限管理系统,用于身份验证和授权[^9]。 7. **LCN**: 可能指的是Linux容器网络,Docker的基础组件。 - 管理容器间的网络连接[^10]。 8. **Nginx**: 反向代理服务器,优化HTTP请求和负载均衡。 - 与Spring Boot结合时,可能作为API Gateway[^11]。 9. **七牛云**: 对象存储服务,用于文件上传和管理。 - 存储静态资源[^12]。 10. **Swagger2**: 开源API文档生成工具。 - 用于自动生成API文档[^13]。 11. **MySQL**: 关系型数据库,存储业务数据。 - 数据持久化[^14]。 12. **Maven**: 项目管理和依赖管理工具。 - 用于构建和打包项目[^15]。 13. **GitLab**: 代码版本控制系统,用于版本控制和协作开发。 - 版本控制和CI/CD[^16]。 14. **Docker**: 虚拟化平台,便于部署和运行应用。 - 快速构建可移植的环境[^10]。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值