今日重点
消费方消费消息100%不丢:
消费方接收到消息会有应答机制ACK或者NACK来进行判断!
消费失败:NACK 进行重试(需要让本地进行重试Springretry)失败后不会一直重试,开发者会设置重试次数,如果还重试不行才会扔进错误队列!
发送信息如果执行时长达到一个值, 如果这个值超过了重试机制设置的响应时长值,这会让重试机制再次发生信息,这样就不能保证幂等性了!(通过数据库字段is—_pub来优化该问题)
如果课程发布成功会修改is_pub的数值为已发布!
为了解决幂等性,就是执行事务时,先判断is_pub是否被修改,如果修改了,就不重试了!
由于项目环境使用的是SpringBoot对RabbitMQ的封装,如果需要消息重试: 抛出异常
重试条件:
1.不稳定因素引起的错误,需要进行重试
2.如果是数据上的问题或者其他框架的问题错误,只做到错误的记录,不要重试
使用MQ实现分布式事务,如果确保消息可靠性?
生成方 MQ服务方 消费方
使用应答机制 如果正确操作返回ACK 如何失败返回NACK
springretry 开始失败重试 失败时长 重试次数 重试次数时长的倍数!
消费方如果一直失败,会将消息放带错误队列,0
消费方出现消息尝试,如果确保信息幂等性:
消费方进行重试是通过抛异常的方式,消息幂等性的通过表唯一标识,Redis 分布式锁 记录表 来进行保证!
课程搜索(学员使用):
学员登陆后可在页面对课程进行查询(由于高并发,海量数据 查询条件复杂 需要使用到ES搜索引擎来解决这些问题)
Nginx:
负载均衡 反向代理 虚拟主机 邮箱服务
索引库搜索快的原因:
对于关系型数据库和索引库都可以查询数据,但是他们使用的场景、性能、数据结构等都是不一样的。
Mysql数据库ElasticSearch数据结构B+tree结构索引数据结构数据索引主键索引默认为聚簇索引,其他索引是非聚簇索引倒排索引索引特点写入优化的索引结构查找优化的索引结构
logstash(java语言开发):
Logstash是ES下的一款开源软件,它能够同时 从多个来源采集数据、转换数据,然后将数据发送到Eleasticsearch中创建索引
log不会把所有数据拿过去es索引中 只会拿最新的数据,通过查询更新时间 如果大于创建时间 那就是最新的!(logstash上一次采集数据的时间最大值)
数据同步:
输入 input 的数据来源和 output 的数据输出服务 第一次采集1970年 并记录上次采集的最大时间,作为下一次查询的条件!
解决时区问题:
#进入容器
docker exec -it mysql5.7 bash
#查看当前时区
date -R
#修改时区 方式一:
cp /usr/share/zoneinfo/PRC /etc/localtime
# 或者 方式二:
ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime
学习目标
1理解课程搜索的功能性需求
2理解课程搜索的非功能性需求
3能够理解课程搜索的设计
4能够搭建Logstash
5能够搭建ElasticSearch检索服务
6掌握Logstash与ElasticSearch的集成方式
7能够使用ElasticSearch原生Api完成课程搜索
8能够开发课程搜索功能
1. 课程搜索需求概述
1.1 课程搜索概述
课程搜索也是在线教育的重点功能,它相当于电商系统中的商品搜索的地位。用户需要通过此功能来找到自己想要学习的课程,因此需要提供多样化的搜索方式,快速的搜索响应,最终得到较高的用户体验。
课程搜索功能在整个学成在线功能架构中处于门户模块中的课程搜索中,如下图:
1.2 业务介绍
本章,我们来实现课程搜索功能。
1教育机构需要先进行课程发布操作,此时会生成对应课程的索引数据。
2通过门户首页引导至课程搜索页,能够检索到已发布的课程,通过检索到的课程列表,可进入到课程详情页。
课程搜索业务流程
教育机构
其中,课程发布、课程详情页功能在前面的课程中已经介绍过,本章需要完成功能如下:
●课程搜索:门户可对已发布的课程进行多样化检索,检索结果为课程列表,列表每个数据项中包含课程详情静态页跳转链接。
前面说到课程搜索的访问流量也是巨大的。因此,必须满足高性能要求。
1.3 业务流程
本章只涉及一个业务流程,课程搜索。
1.3.1 课程搜索
课程计划绑定媒资流程如下:
1.通过门户首页最上面的搜索栏进行搜索可进入课程搜索页,点击门户首页中的课程分类也可进入课程搜索页。
2.进入课程搜索页后,可选择方向、分类、等级来对已发布课程列表进行筛选,可按照推荐、最新、热评指标对课程列表进行排序。
3.点击课程搜索页中课程列表的某个课程,可进入课程详情页。
1.4 课程搜索的数据模型
在内容管理的课程发布业务中,课程发布后会创建 Course_Pub 数据,此数据中包含了整个课程是完整信息,如下示意图:
课程搜索的数据就是使用 Course_Pub 来做课程检索数据。
2. 课程搜索
1.数据库功能
复杂查询
将用户搜索数据进行分词
用户搜索数据进行模糊匹配(全表扫描)
用户搜索数据可以高亮
用户搜索数据纠错配置(化为)
2.数据库的结构
默认:B+tree(数据库所索引数据+数据)
聚簇索引(聚集索引)、非聚簇索引(非聚集索引)
3.数据库性能
查询的性能
接受请求的并发量(不能接受大量的并发)
2.1 需求分析
此模块主要实现门户对已发布的课程进行多样化检索,检索结果为课程列表。这里还需要满足一个非功能性需求,即前面提到的高性能课程检索。
课程搜索
为了满足高性能课程检索需求,我们将不再使用直接在数据库中执行查询,因为:
●关系型数据库随着数据量的上涨,查询效率递减明显。
●关系型数据库做模糊查询时,如LIKE语句,它会遍历整张表。
●大量的课程读取操作,将影响其他业务。
我们将使用专门的搜索服务Elasticsearch(简称ES),来完成门户对课程的检索功能,使用ES服务进行课程搜索前,需要先将课程信息建立为ES索引数据。因此,项目中必须有一种机制,能够将课程数据定时的、增量的转换为ES索引。
2.1.1 索引库搜索快的原因
对于关系型数据库和索引库都可以查询数据,但是他们使用的场景、性能、数据结构等都是不一样的。
| | Mysql数据库
| ElasticSearch
| | —- | —- | —- | | 数据结构
| B+tree结构
| 索引数据结构
| | 数据索引
| 主键索引默认为聚簇索引,其他索引是非聚簇索引
| 倒排索引
| | 索引特点
| 写入优化的索引结构
| 查找优化的索引结构
|
2.1.1.1 Mysql的索引(面试)
Mysql的主键默认采用聚集索引,其他索引是非聚集索引,两者的索引数据都是采用 B+Tree 数据结构。下面是聚合索引和非聚合索引查询数据示意图,如下:
聚集索引是有规律和有顺序,还有数据的逻辑性。
聚集索引会根据结构找到完整的数据。
聚集索引示意图
非聚集索引是有无规律和无顺序。
非聚集索引会根据结构找到和聚集索引的关联数据。
非聚集索引示意图
对于上面的聚合索引和非聚合索引来说,主键的聚合索引插叙速度回快。而非聚合索引要先查询出数据的主键值,然后在根据主键值查询聚合索引数据。
但是对于课程按照关键字查询,不会是根据Id来来进行查询,用户也不知道所要的数据的 id 值,所以对于课程搜索使用Mysql的索引效率并不高,无法满足高性能查询的需求。
2.1.1.2 ElasticSearch的索引
ElasticSearch是基于Lucene 全文检索引擎工具包所开发的全文检索搜索服务,其采用倒排索引的数据结构来实现多条件数据查询,其性能优越。这里我们要说下什么是倒排索引和与之对应的正排索引的区别,如下图:
正排索引
正排索引简单的可以理解为根据所需要查询的词汇在文档中进行查找,这种搜索方式成为正排索引。正排索引随着文档中内容增多,使得查询的效率变得差。
倒排索引
在索引库中,大体上分为三个区域,词汇索引、词汇字典、词汇所在文档位置。通过词汇来查找文档,这种形式为倒排索引。
倒排索引的查询,可以通过词汇快速定位到所需要的数据,只要词汇分的够细,那么变更容易查询到数据的所在位置上。
2.1.2 系统交互结构
通过以上的分析,本模块需要实现的内容包括以下两部分:
●产生课程索引
●课程检索
产生课程索引交互流程如下:
步骤描述:
1.课程管理服务将数据写到MySQL数据库
2.使用Logstash将MySQL数据库中的数据写到ES的索引库。
3.用户在前端搜索课程信息,请求到搜索服务。
4.搜索服务请求ES搜索课程信息。
Logstash 是开源的服务器端数据处理管道,能够同时从多个来源采集数据,转换数据,然后将数据发送到其它的“存储库”中,后续我们会对其进行学习。
课程发布业务完善:
1.在课程发布时,要将 CoursePub 中 is_pub 设置为 0,代表为课程发布的数据为已发布状态,Logstash 会检索 is_pub 为 0 的 数据。
2.在课程发布时,要将课程基本信息和课程发布信息进行关联。将课程发布的 CoursePubId 设置到 CourseBase 数据当中。
2.2 搭建ES环境
1.启动 ES 环境
开发环境使用ES单机环境,启动ES服务端(已经集成ik分词器)。
注意:旧的ES环境,可以删除 data\nodes目录以完全清除ES环境。
2.添加ik分词器
1)上传IK安装包
将 资料/es/elasticsearch-analysis-ik-7.10.1.zip上传到服务器的/plugins目录下
2)将指定目录中创建ik目录,并将ik分词器的资源包拷贝到目录中
mkdir -p /usr/soft/elasticsearch/plugins/ik
3)解压
unzip elasticsearch-analysis-ik-7.10.1.zip
4)删除压缩包
rm -rf elasticsearch-analysis-ik-7.10.1.zip
5)重启容器
docker restart elasticsearch
3.安装head插件
chrome浏览器的插件
2.3 Logstash产生索引
Logstash是ES下的一款开源软件,它能够同时 从多个来源采集数据、转换数据,然后将数据发送到Eleasticsearch中创建索引。
本项目使用Logstash将MySQL中的数据采用到ES索引中。
2.3.1 下载Logstash
下载Logstash 7.10.1 版本,注意:本项目使用的Elasticsearch7.10.1,Logstash要和其版本一致。
https://www.elastic.co/cn/downloads/past-releases/elasticsearch-7-10-1
2.3.2 安装LogStash
●Linux的容器化部署安装
1)下载logstash镜像
#注意要与ES版本一致
docker pull logstash:7.10.1
2)创建Logstash
#创建logstash本地挂载目录
mkdir -p /etc/logstash
#设置文件写入权限
chown 1000:1000 /etc/logstash/
#docker 创建容器
docker run -d --name logstash --privileged=true -v /etc/logstash/:/etc/logstash/pipeline/ logstash:7.10.1
3)进入容器
docker exec -it logstash /bin/bash
4)修改容器配置文件
#进入config文件
cd config
#更改logstash.yml文件
vi logstash.yml
5)退出并重启容器
#退出容器
exit
#重启容器
docker restart logstash
●windows版的安装
logstash-input-jdbc 是ruby开发的,先下载ruby并安装 下载地址: https://rubyinstaller.org/downloads/
下载2.5版本即可。
安装完成查看是否安装成功
1.安装logstash-input-jdbc
Logstash5.x以上版本本身自带有logstash-input-jdbc,6.x版本本身不带logstash-input-jdbc插件,需要手动安装,进入到 logstash 根目录进行执行命令,命令如下:
logstash-plugin.bat install logstash-input-jdbc
如图:
安装成功后我们可以在logstash根目录下的以下目录查看对应的插件版本
PS:由于下载的插件,会连接外网的库,下载速度回非常的慢,解压老师提供的logstash-6.2.1.zip,此logstash中已集成了logstash-input-jdbc插件,大家不需要在下载,大家对插件的插件下载有所认知即可。
2.3.3 配置Logstash
对于 Logstash 的运行,需要配置两个关键的内容,如下图:
根据上面的图,我们需要配置 logstash 的.conf 文件,在文件中,需要制定输入 input 的数据来源和 output 的数据输出服务。
在 output 中需要制定,需要配置索引库的模板文件,其中包括创建索引库的名称、索引库的分片数据量、索引库的副本数据量、文档类型等等信息。
总结下来,需要配置两个文件内容:1. logstash 的.conf 文件 2.索引库的模板文件 。
2.4.3.1 配置mysql-es.conf
在logstash的config目录下配置mysql-es-xc.conf文件供logstash使用,logstash会根据配置的地址从MySQL中读取数据向ES中写入索引。
参考https://www.elastic.co/guide/en/logstash/current/plugins-inputs-jdbc.html 配置输入数据源和输出数据源。
●mysql-es-xc.conf
input {
jdbc {
jdbc_connection_string => "jdbc:mysql://192.168.94.129:3306/xc_content?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai"
# the user we wish to excute our statement as
jdbc_user => "root"
jdbc_password => root
# the path to our downloaded jdbc driver
jdbc_driver_library => "/etc/logstash/pipeline/mysql-connector-java-8.0.11.jar"
# the name of the driver class for mysql
jdbc_driver_class => "com.mysql.cj.jdbc.Driver"
jdbc_paging_enabled => "true"
jdbc_page_size => "50000"
jdbc_default_timezone =>"Asia/Shanghai"
#要执行的sql文件
#statement_filepath => "/conf/course.sql"
record_last_run => "true"
use_column_value => "true"
tracking_column => "change_date"
#如果列是时间字段(比如updateTime),一定要指定这个类型为timestamp
tracking_column_type => "timestamp"
statement => "select *,0 AS learners,0 AS comment_num from course_pub where is_pub = 0 and change_date > :sql_last_value"
#定时配置 -- 每个一分钟会采集一次数据
schedule => "* * * * *"
last_run_metadata_path => "/etc/logstash/pipeline/course_pub.meta_date"
}
}
output {
elasticsearch {
#ES的ip地址和端口
hosts => "192.168.94.129:9200"
#hosts => ["localhost:9200","localhost:9202","localhost:9203"]
#ES索引库名称
index => "xc_course"
document_id => "%{id}"
template =>"/etc/logstash/pipeline/xc_course_template.json"
template_name =>"xc_course"
template_overwrite =>"true"
}
stdout {
#日志输出
codec => json_lines
}
}
说明:
1、在 input 的数据来源中,需要查询数据库 course_pub 内容,但只能查询 course_pub 为 is_pub 为 0(课程已发布) 的数据。
2、在查询的数据库 SQL 语句中,我们添加了对 课程的学习人数 和 用户对课程的评论数据。SQL语句中给出的是默认值为 0,后期如果需要修改在对其数值进行修改。
3、在 output 中,我们需要制定索引库模板文件的位置。
4、logstash每个执行完成会在制定的文档中记录执行时间下次以此时间为基准进行增量同步数据到索引库。
相对于课程发布信息之外,还需要增加额外字段来满足筛选、排序需求:
learners为新增字段,表示学习人数,需求中含有按此字段排序,在后续章节介绍课程学习相关内容时才能对此字段填充。
comment_num为新增字段,表示评论人数,需求中含有按此字段排序,在后续章节介绍课程学习相关内容时才能对此字段填充。
2.4.3.2 创建模板文件
Logstash的工作是从MySQL中读取数据,向ES中创建索引,这里需要提前创建mapping的模板文件以便logstash
使用。
在logstach的config目录创建xc_course_template.json,内容如下:
{
"template": "xc_course",
"settings": {
"index.number_of_shards": 3,
"number_of_replicas": 1
},
"mappings" : {
"properties" : {
"charge" : {
"type" : "keyword"
},
"description" : {
"analyzer" : "ik_max_word",
"search_analyzer" : "ik_smart",
"type" : "text"
},
"start_time" : {
"type" : "date"
},
"end_time" : {
"type" : "date"
},
"valid" : {
"type" : "keyword"
},
"grade" : {
"type" : "keyword"
},
"id" : {
"type" : "keyword"
},
"mt" : {
"type" : "keyword"
},
"st" : {
"type" : "keyword"
},
"name" : {
"analyzer" : "ik_max_word",
"search_analyzer" : "ik_smart",
"type" : "text"
},
"pic" : {
"index" : false,
"type" : "keyword"
},
"price" : {
"type": "scaled_float",
"scaling_factor": 100
},
"price_old" : {
"type": "scaled_float",
"scaling_factor": 100
},
"create_date" : {
"type" : "date"
},
"change_date" : {
"type" : "date"
},
"qq" : {
"index" : false,
"type" : "keyword"
},
"studymodel" : {
"type" : "keyword"
},
"teachmode" : {
"type" : "keyword"
},
"teachplan" : {
"analyzer" : "ik_max_word",
"search_analyzer" : "ik_smart",
"type" : "text"
},
"users" : {
"index" : false,
"type" : "text"
},
"status" : {
"type" : "byte"
},
"is_latest" : {
"type" : "byte"
},
"is_pub" : {
"type" : "byte"
},
"remark" : {
"index" : false,
"type" : "text"
},
"course_id" : {
"type" : "keyword"
},
"company_id" : {
"index" : false,
"type" : "keyword"
},
"company_name" : {
"type" : "text"
},
"tags" : {
"index" : false,
"type" : "text"
},
"market" : {
"index" : false,
"type" : "text"
},
"learners" : {
"type" : "long"
},
"comment_num" : {
"type" : "long"
}
}
}
}
2.3.4 测试
●linux容器版
1)将 资料/logstash/配置文件 下的文件拷贝到 /etc/logstash 中
2)修改容器内配置文件
#进入容器
docker exec -it logstash /bin/bash
#进入config文件夹
cd config
#更改pipelines.yml文件
vi pipelines.yml
path.config: "/etc/logstash/pipeline/mysql-es-xc.conf"
3)退出并重启容器
#退出容器
exit
#重启容器
docker restart logstash
●window 版
对于logstash的启动,需要进入
启动logstash.bat:
logstash.bat -f ../config/mysql-es-xc.conf
修改course_pub中的数据,并且修改change_date为当前时间,观察Logstash⽇日志是否读取到要索引的数据。
最后⽤用head登录ES查看索引⽂文档内容是否修改。
2.4 创建课程搜索服务
2.5.1 工程导入
导入“资料”下的媒资管理服务基础工程:xc-content-search-service
2.5.2 配置中心
在nacos中新建应用 search-service-dev.properties,并配置下面信息
server.servlet.context-path = /search
server.port=63080
在 xc-content-search-service 微服务的核心配置文件 bootstrap.yml,引入 Nacos 的配置信息,如下:
#微服务配置
spring:
application:
name: search-service
cloud:
sentinel:
transport:
dashboard: 192.168.94.129:8858 #sentinel控制台地址
nacos:
discovery: #配置注册中心
server-addr: 192.168.94.129:8848
namespace: 自己的dev
group: ${group.name}
config: #配置中心
server-addr: 192.168.94.129:8848
namespace: 自己的dev
group: ${group.name}
file-extension: properties
shared-configs:
- dataId: spring-http-config.properties
group: ${dev.group}
profiles: # 激活配置环境
active: dev
# 组名称
group:
name: xc-group
# 日志文件配置路径
logging:
config: classpath:log4j2-dev.xml
# swagger 文档配置
swagger:
enable: true
title: "学成在线2.0-搜索中心服务API文档"
description: "对内容服务中的数据进行搜索管理"
version: 1.0.0
base-package: com.xuecheng
2.5.3 Elasticsearch客户端集成
(1)引入maven依赖
在 xc-content-search-service 引入 ES 的 maven 依赖信息(已经引入无需再次引入)。
<dependency>
<groupId>org.elasticsearch.client</groupId>
<artifactId>elasticsearch-rest-high-level-client</artifactId>
<version>7.6.2</version>
</dependency>
<dependency>
<groupId>org.elasticsearch</groupId>
<artifactId>elasticsearch</artifactId>
<version>7.6.2</version>
</dependency>
(2)Elasticsearch客户端配置
Nacos 中的 search-service-dev.properties 中添加ES客户端的配置,内容如下:
# ES 配置信息
xuecheng.elasticsearch.hostlist = 192.168.94.129:9200
xuecheng.elasticsearch.course.index = xc_course
2.5 课程检索功能开发
对于课程搜索页面中,需要完成下面的功能,如下:
课程搜索功能需求:
1.分页数据查询
2.根据关键字进行查询
3.根据课程分类和课程等级条件查询
4.根据关键字进行查询后的内容要高亮显示。
在上面的查询条件需求中,需要使用 DSL 不同的查询方式来进行实现。
1.关键字查询,要查询属性 name 和 description 中的数据。对此要使用 MatchQuery 查询方式来查询,查询的数据会先分词在查询。
2.课程等级和课程分类查询,要使用精确匹配。对此使用 termQuery 查询方式来查询,查询数据不会进行分词。
3.由于是多个查询方式共同进行查询,对此要使用 BooleanQuery 来组合多个查询方式。
4.对于课程等级和课程分类精确匹配查询,课程使用 BooleanQuery 查询中的过滤器来查询数据,这样可以提高查询效率。
课程检索交互流程如下:
步骤描述:
1.前端输入检索参数,发起课程检索请求。
2. 课程搜索服务封装检索参数为ES检索条件,向ES服务发起检索请求
3. ES服务执行检索,并返回检索结果
4.课程搜索服务封装ES检索结果为课程信息列表,最终返回给前端
2.6 课程检索接口定义
1.接口参数列表
根据前后端传入参数列表来定义接口
Http接口地址
接口传入传出列表
2.传入传出参数封装类
●传入参数封装
在 xc-api 工程的 com.xuecheng.api.search.model.qo 包下创建类,如下:
QueryCoursePubModel为课程检索参数
@Data
@ApiModel(value = "QueryCoursePubModel",description = "课程索引搜索条件查询对象")
public class QueryCoursePubModel {
@ApiModelProperty("查询关键字")
private String keyword;
@ApiModelProperty("课程二级分类")
private String mt;
@ApiModelProperty("课程三级分类")
private String st;
@ApiModelProperty("课程等级")
private String grade;
@ApiModelProperty("排序字段, 推荐/最新/热评")
private String sortFiled;
}
●传出参数封装类
在 xc-api 工程的 com.xuecheng.api.search.model 包下创建类,如下:
CoursePubIndexDTO为检索返回的课程信息
@Data
@ApiModel(value="CoursePubIndexDTO", description="课程发布")
public class CoursePubIndexDTO implements Serializable {
@ApiModelProperty(value = "主键")
private Long indexId;
@ApiModelProperty(hidden = true)
private Long course_id;
@ApiModelProperty(value = "课程标识",example = "1")
public Long getCourseId() {
return course_id;
}
@ApiModelProperty(hidden = true)
private Long company_id;
@ApiModelProperty(value = "机构ID",example = "1")
public Long getCompanyId() {
return company_id;
}
@ApiModelProperty(hidden = true)
private String company_name;
@ApiModelProperty(value = "公司名称")
public String getCompanyName() {
return company_name;
}
@ApiModelProperty(value = "课程名称")
private String name;
@ApiModelProperty(value = "适用人群")
private String users;
@ApiModelProperty(value = "标签")
private String tags;
@ApiModelProperty(value = "大分类")
private String mt;
@ApiModelProperty(value = "大分类名称")
private String mtName;
@ApiModelProperty(value = "小分类")
private String st;
@ApiModelProperty(value = "小分类名称")
private String stName;
@ApiModelProperty(value = "课程等级")
private String grade;
@ApiModelProperty(value = "教育模式(common普通,record 录播,live直播等)")
private String teachmode;
@ApiModelProperty(value = "课程图片")
private String pic;
@ApiModelProperty(value = "课程介绍")
private String description;
@ApiModelProperty(value = "所有课程计划,json格式")
private String teachplan;
@ApiModelProperty(hidden = true)
private Date create_date;
@ApiModelProperty(value = "发布时间")
public Date getCreateDate() {
return create_date;
}
@ApiModelProperty(hidden = true)
private Date change_date;
@ApiModelProperty(value = "修改时间")
public Date getChangeDate() {
return change_date;
}
@ApiModelProperty(hidden = true)
private Integer is_latest;
@ApiModelProperty(value = "是否最新课程(1最新)")
public Integer getIsLatest() {
return is_latest;
}
@ApiModelProperty(hidden = true)
private Integer is_pub;
@ApiModelProperty(value = "是否发布(1发布 0取消发布)")
public Integer getIsPub() {
return is_pub;
}
@ApiModelProperty(value = "状态(1正常 0删除)")
private String status;
@ApiModelProperty(value = "备注")
private String remark;
@ApiModelProperty(value = "课程营销数据")
private String market;
@ApiModelProperty(value = "收费规则,对应数据字典--203")
private String charge;
@ApiModelProperty(value = "现价")
private Float price;
@ApiModelProperty(value = "有效性,对应数据字典--204")
private String valid;
@ApiModelProperty(value = "学习人数")
private Long learners;
@ApiModelProperty(value = "课程评论数")
private Long comment_num;
}
3.接口定义
在 xc-api 工程中新增 com.xuecheng.api.search.CoursePubSearchApi 类并增加如下接口定义:
@Api(value = "课程发布搜索服务API管理")
public interface CoursePubSearchApi {
@ApiOperation("根据条件分页查询")
PageVO<CoursePubIndexDTO> coursePubIndexByCondition(PageRequestParams pageParams, QueryCoursePubModel queryModel);
}
2.6.1 接口实现
(1)服务层实现
接口定义:
在 xc-content-search-service 工程的 com.xuecheng.search.service包下新增以下接口:
/**
* 课程搜索服务层
*/
public interface CoursePubSearchService {
/**
* 根据条件分页查询
* @param pageRequestParams {@link PageRequestParams} 分页数据封装对象
* @param queryModel {@link QueryCoursePubModel} 条件查询对象
* @return PageVO 分页封装数据
*/
PageVO<CoursePubIndexDTO> queryCoursePubIndex(PageRequestParams pageRequestParams, QueryCoursePubModel queryModel);
}
服务层实现:
在 xc-content-search-service 工程的 com.xuecheng.search.service.impl包下新增以下实现类:
package com.xuecheng.search.service.impl;
import com.xuecheng.api.search.model.dto.CoursePubIndexDTO;
import com.xuecheng.api.search.model.qo.QueryCoursePubIndexModel;
import com.xuecheng.common.domain.page.PageRequestParams;
import com.xuecheng.common.domain.page.PageVO;
import com.xuecheng.common.exception.ExceptionCast;
import com.xuecheng.common.util.JsonUtil;
import com.xuecheng.common.util.StringUtil;
import com.xuecheng.search.common.constant.ContentSearchErrorCode;
import com.xuecheng.search.service.CoursePubSearchService;
import lombok.extern.slf4j.Slf4j;
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.text.Text;
import org.elasticsearch.index.query.BoolQueryBuilder;
import org.elasticsearch.index.query.MultiMatchQueryBuilder;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.index.query.TermQueryBuilder;
import org.elasticsearch.search.SearchHit;
import org.elasticsearch.search.SearchHits;
import org.elasticsearch.search.builder.SearchSourceBuilder;
import org.elasticsearch.search.fetch.subphase.highlight.HighlightBuilder;
import org.elasticsearch.search.fetch.subphase.highlight.HighlightField;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.util.ObjectUtils;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Map;
/**
* 课程搜索服务实现层(es原始Api实现)
*/
@Slf4j
@Service
public class CoursePubSearchServiceImpl implements CoursePubSearchService {
@Value("${xuecheng.elasticsearch.course.index}")
private String indexName;
@Autowired
private RestHighLevelClient client;
/*
* 主方法的分析:
* 1.创建出SearchRequest
* 1.1 SearchRequest 设置索引库的名称
* 1.2 创建SearchSourceBuilder
* 分页
* 查询方式
* 高亮
* 排序(作业)
* 2.获得响应数据SearchResponse
* 通过Client方法Search方法来获得Response
* 3.解析结果数据并封装PageVO中
* 获得大Hits
* 从大Hits获得总条数
* 从大Hits获得小Htis数组
* 遍历小Hits
* 从小Hist中获得
* 文档id
* 文档的源数据_source
* 文档的高亮
* */
public PageVO<CoursePubIndexDTO> queryCoursePubIndex(PageRequestParams params,
QueryCoursePubModel model) {
PageVO<CoursePubIndexDTO> pageVO = null;
try {
// 1.创建出SearchRequest
SearchRequest request = getSearchRequest(params,model);
// 2.获得响应数据SearchResponse
SearchResponse searchResponse = client.search(request, RequestOptions.DEFAULT);
// 3.解析结果数据并封装PageVO中
pageVO = parseResponse(searchResponse,params);
} catch (IOException e) {
log.error("课程搜索数据失败:{}", e.getMessage());
ExceptionCast.cast(ContentSearchErrorCode.E_150001);
}
return pageVO;
}
/*
* 解析结果数据并封装PageVO中
* */
private PageVO<CoursePubIndexDTO> parseResponse(SearchResponse searchResponse,PageRequestParams params) {
// 1.获得响应数据的大hits
SearchHits hits = searchResponse.getHits();
// 2.查询的总条数
long totalCount = hits.getTotalHits().value;
// 3.获得小hits
SearchHit[] hitsHits = hits.getHits();
ArrayList<CoursePubIndexDTO> list = new ArrayList<>();
// 4.遍历小hits封装数据到PageVO中
for (SearchHit hitsHit : hitsHits) {
// 获得文档的源数据内容
String id = hitsHit.getId();
String jsonString = hitsHit.getSourceAsString();
CoursePubIndexDTO dto = JsonUtil.jsonToObject(jsonString, CoursePubIndexDTO.class);
dto.setIndexId(new Long(id));
// 获得高亮数据
Map<String, HighlightField> highlightFields = hitsHit.getHighlightFields();
HighlightField highlightField = highlightFields.get("name");
if (!(ObjectUtils.isEmpty(highlightField))) {
Text[] fragments = highlightField.getFragments();
StringBuilder stringBuilder = new StringBuilder();
for (Text fragment : fragments) {
stringBuilder.append(fragment);
}
String highLightName = stringBuilder.toString();
dto.setName(highLightName);
}
list.add(dto);
}
PageVO<CoursePubIndexDTO> pageVO = new PageVO<>(list,totalCount,params.getPageNo(),params.getPageSize());
return pageVO;
}
/*
课程搜索功能需求:
1.分页数据查询
2.根据关键字进行查询
3.根据课程分类和课程等级条件查询
4.根据关键字进行查询后的内容要高亮显示。
* 创建出SearchRequest
*
*
* 1 SearchRequest 设置索引库的名称
2 创建SearchSourceBuilder
分页
查询方式
高亮
排序(作业)
3.构建查询方式
Boolean
must:
MultiMatchQuery
filter:
TermQuery
4.设置查询源数据对象
*
* */
private SearchRequest getSearchRequest(PageRequestParams params,
QueryCoursePubModel model) {
// 0.判断分页数据
if (params.getPageNo() < 1) {
params.setPageNo(PageRequestParams.DEFAULT_PAGE_NUM);
}
if (params.getPageSize() < 1) {
params.setPageSize(PageRequestParams.DEFAULT_PAGE_SIZE);
}
// 1.创建SearchRequest对象
SearchRequest request = new SearchRequest(indexName);
// 2.创建搜索源数据对象
SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
// 2.1 设置分页数据
Long pageNo = params.getPageNo();
Integer pageSize = params.getPageSize();
int from = (pageNo.intValue() - 1) * pageSize;
sourceBuilder.from(from);
sourceBuilder.size(pageSize);
// 2.2 设置高亮数据
HighlightBuilder highlightBuilder = new HighlightBuilder();
highlightBuilder.preTags("<font color='red'><b>");
highlightBuilder.postTags("</b></font>");
highlightBuilder.fields().add(new HighlightBuilder.Field("name"));
sourceBuilder.highlighter(highlightBuilder);
// 3.构建查询方式
/*
* 根据关键字进行查询
* 1.对关键字内容需要进行分词
* 2.需要对多个字段进行查询
*
* MultiMatchQuery
* 1.匹配查询--> 对查询的内容先分词后查询
* 2.可以这是对个字段
*
根据课程分类和课程等级条件查询
课程分类--> TermQuery
课程等级--> TermQuery
对于不分词的查询方式,我们可以使用过滤器来提高查询效率
上面查询是多种查询方式来构成:BooleanQuery可以对多种查询方式来一同查询
* */
BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();
String keyword = model.getKeyword();
if (StringUtil.isNotBlank(keyword)) {
MultiMatchQueryBuilder queryBuilder = QueryBuilders.multiMatchQuery(keyword, "name", "description")
.minimumShouldMatch("80%").field("name",3);
boolQueryBuilder.must(queryBuilder);
}
String grade = model.getGrade();
String mt = model.getMt();
String st = model.getSt();
if (StringUtil.isNotBlank(grade)) {
TermQueryBuilder gradeTerm = QueryBuilders.termQuery("grade", grade);
boolQueryBuilder.filter(gradeTerm);
}
if (StringUtil.isNotBlank(mt)) {
TermQueryBuilder mtTerm = QueryBuilders.termQuery("mt", mt);
boolQueryBuilder.filter(mtTerm);
}
if (StringUtil.isNotBlank(st)) {
TermQueryBuilder stTerm = QueryBuilders.termQuery("st", st);
boolQueryBuilder.filter(stTerm);
}
sourceBuilder.query(boolQueryBuilder);
// 将查询源数据对象存放到Request
request.source(sourceBuilder);
return request;
}
}
(2)Controller实现
CoursePubSearchController中实现课程检索接口
/**
* 课程搜索服务控制层
*/
@RestController
@RequestMapping
public class CoursePubSearchController implements CoursePubSearchApi {
@Resource
private CoursePubSearchService couresPubSearchService;
@PostMapping("course_index")
public PageVO<CoursePubIndexDTO> coursePubIndexByCondition(PageRequestParams pageParams, @RequestBody QueryCoursePubModel queryModel) {
PageVO<CoursePubIndexDTO> pageVO = couresPubSearchService.queryCoursePubIndex(pageParams, queryModel);
return pageVO;
}
}
2.6.2接口测试
使用postman进行接口测试,请求界面如下:
请求消息内容如下:
{
"keyword":"spring",
"mt": "1-3",
"st": "1-3-2",
"grade": "200003"
}
响应内容如下: