- 若文章内容或图片失效,请留言反馈。部分素材来自网络,若不小心影响到您的利益,请联系博主删除。
前言
- 强调:本博客主要是对 【SpringCloud 微服务技术栈 | 实用篇① | 基础知识】 内容的补充
系列目录
SpringCloud 微服务技术栈_实用篇②_黑马旅游案例
SpringCloud 微服务技术栈_高级篇⑤_可靠消息服务
0.微服务技术栈导学
- 以下为视频中的截图
# SpringCloudDay06
1.项目简述
通过该案例来实战演练下之前所学知识。
实现四部分功能:
- 酒店搜索和分页
- 酒店结果过滤
- 周边的酒店
- 酒店竞价排名
启动资料中提供的 hotel-demo
项目,其默认端口是 8089,访问 http://localhost:8090
,就能看到项目页面了。
- 课前资料链接:https://pan.baidu.com/s/169SFtYEvel44hRJhmFTRTQ(
提取码:1234
)1.微服务开发框架 SpringCloud + RabbitMQ + Docker + Redis + 搜索 + 分布式史上最全面的微服务全技术栈课程>
实用篇>学习资料>day06-Elasticsearch02>代码
2.酒店搜索和分页
需求:实现黑马旅游的酒店搜索功能,完成关键字搜索和分页
2.1.需求分析
在项目的首页,有一个大大的搜索框,还有分页按钮
点击搜索按钮,可以看到浏览器控制台发出了请求
请求参数如下
由此可以知道,我们这个请求的信息如下
- 请求方式:POST
- 请求路径:
/hotel/list
- 请求参数:JSON 对象,包含 4 个字段:
key
:搜索关键字page
:页码size
:每页大小sortBy
:排序,目前暂不实现
- 返回值:分页查询,需要返回分页结果 PageResult,包含两个属性:
total
:总条数List<HotelDoc>
:当前页的数据
因此,我们实现业务的流程如下
- 步骤一:定义实体类,接收前端请求:请求参数的 JSON 对象
- 步骤二:编写 controller,接收页面的请求,调用 IHotelService 的 search 方法
- 步骤三:编写业务实现,定义 IHotelService 中的 search 方法,利用 RestHighLevelClient 中的 match 查询实现搜索、分页
2.2.定义实体类
实体类有两个,一个是前端的请求参数实体,一个是服务端应该返回的响应结果实体。
2.2.1.请求参数
前端请求的 json 结构如下
{
"key": "搜索关键字",
"page": 1,
"size": 3,
"sortBy": "default"
}
因此,我们在 cn.itcast.hotel.pojo
包下定义一个实体类
src/main/java/cn/itcast/hotel/pojo/RequestParams.java
package cn.itcast.hotel.pojo;
import lombok.Data;
@Data
public class RequestParams {
private String key;
private Integer page;
private Integer size;
private String sortBy;
}
2.2.2.返回值
分页查询,需要返回分页结果 PageResult,包含两个属性
total
:总条数List<HotelDoc>
:当前页的数据
因此,我们在 cn.itcast.hotel.pojo
中定义返回结果
src/main/java/cn/itcast/hotel/pojo/PageResult.java
package cn.itcast.hotel.pojo;
import lombok.Data;
import java.util.List;
@Data
public class PageResult {
private Long total;
private List<HotelDoc> hotels;
public PageResult() {
}
public PageResult(Long total, List<HotelDoc> hotels) {
this.total = total;
this.hotels = hotels;
}
}
2.3.定义 controller
定义一个 HotelController,声明查询接口,满足下列要求:
- 请求方式:Post
- 请求路径:
/hotel/list
- 请求参数:对象,类型为 RequestParam
- 返回值:PageResult,包含两个属性
Long total
:总条数List<HotelDoc> hotels
:酒店数据
因此,我们在 cn.itcast.hotel.web
中定义 HotelController
src/main/java/cn/itcast/hotel/web/HotelController.java
@RestController
@RequestMapping("/hotel")
public class HotelController {
@Autowired
private IHotelService hotelService;
// 搜索酒店数据
@PostMapping("/list")
public PageResult search(@RequestBody RequestParams params){
return hotelService.search(params);
}
}
2.4.实现搜索业务
我们在 controller 调用了 IHotelService,并没有实现该方法。
因此下面我们就在 IHotelService 中定义方法,并且去实现业务逻辑。
2.4.1.在接口中定义方法
在 cn.itcast.hotel.service
中的 IHotelService
接口中定义一个方法
src/main/java/cn/itcast/hotel/service/IHotelService.java
/**
* 根据关键字搜索酒店信息
*
* @param params 请求参数对象,包含用户输入的关键字
* @return 酒店文档列表
*/
PageResult search(RequestParams params);
2.4.2.注入 es 客户端组件
实现搜索业务,肯定离不开 RestHighLevelClient,我们需要把它注册到 Spring 中作为一个 Bean。
在 cn.itcast.hotel
中的HotelDemoApplication
中声明这个 Bean
src/main/java/cn/itcast/hotel/HotelDemoApplication.java
@Bean
public RestHighLevelClient client(){
return new RestHighLevelClient(RestClient.builder(
HttpHost.create("http://192.168.150.101:9200")
));
}
2.4.3.实现业务逻辑
在 cn.itcast.hotel.service.impl
中的 HotelService
中实现 search 方法
src/main/java/cn/itcast/hotel/service/impl/HotelService.java
@Autowired
private RestHighLevelClient client;
@Override
public PageResult search(RequestParams params) {
try {
// 1.准备 Request
SearchRequest request = new SearchRequest("hotel");
// 2.准备 DSL
// 2.1.query
String key = params.getKey();
if (key == null || "".equals(key)) {
request.source().query(QueryBuilders.matchAllQuery());
} else {
request.source().query(QueryBuilders.matchQuery("all", key));
}
// 2.2.分页
int page = params.getPage();
int size = params.getSize();
request.source().from((page - 1) * size).size(size);
// 3.发送请求
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
// 4.解析响应
return handleResponse(response);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
// 结果解析
private PageResult handleResponse(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);
// 放入集合
hotels.add(hotelDoc);
}
// 4.4.封装返回
return new PageResult(total, hotels);
}
3.酒店结果过滤
需求:添加品牌、城市、星级、价格等过滤功能
3.1.需求分析
在页面搜索框下面,会有一些过滤项
传递的参数如图
包含的过滤条件有
brand
:品牌值city
:城市minPrice~maxPrice
:价格范围starName
:星级
我们需要做两件事情
- 修改请求参数的对象 RequestParams,接收上述参数
- 修改业务逻辑,在搜索条件之外,添加一些过滤条件
3.2.修改实体类
修改在 cn.itcast.hotel.pojo
包下的实体类 RequestParams
src/main/java/cn/itcast/hotel/pojo/RequestParams.java
@Data
public class RequestParams {
private String key;
private Integer page;
private Integer size;
private String sortBy;
/* 下面是新增的过滤条件参数 */
private String city;
private String brand;
private String starName;
private Integer minPrice;
private Integer maxPrice;
}
3.3.修改搜索业务
在 HotelService 的 search 方法中,只有一个地方需要修改:requet.source().query( ... )
其中的查询条件。
在之前的业务中,只有 match 查询,根据关键字搜索,现在要添加条件过滤,包括:
- 品牌过滤:是 keyword 类型,用 term 查询
- 星级过滤:是 keyword 类型,用 term 查询
- 价格过滤:是数值类型,用 range 查询
- 城市过滤:是 keyword 类型,用 term 查询
多个查询条件组合,肯定是 boolean 查询来组合:
- 关键字搜索放到 must 中,参与算分
- 其它过滤条件放到 filter 中,不参与算分
因为条件构建的逻辑比较复杂,这里先封装为一个函数
src/main/java/cn/itcast/hotel/service/impl/HotelService.java
buildBasicQuery(params, request);
补充:封装方法快捷键:
Ctrl + Alt + M
方法 buildBasicQuery 的代码如下
src/main/java/cn/itcast/hotel/service/impl/HotelService.java
private void buildBasicQuery(RequestParams params, SearchRequest request) {
// 1.构建 BooleanQuery
BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
// 2.关键字搜索
String key = params.getKey();
if (key == null || "".equals(key)) {
boolQuery.must(QueryBuilders.matchAllQuery());
} else {
boolQuery.must(QueryBuilders.matchQuery("all", key));
}
// 3.城市条件
if (params.getCity() != null && !params.getCity().equals("")) {
boolQuery.filter(QueryBuilders.termQuery("city", params.getCity()));
}
// 4.品牌条件
if (params.getBrand() != null && !params.getBrand().equals("")) {
boolQuery.filter(QueryBuilders.termQuery("brand", params.getBrand()));
}
// 5.星级条件
if (params.getStarName() != null && !params.getStarName().equals("")) {
boolQuery.filter(QueryBuilders.termQuery("starName", params.getStarName()));
}
// 6.价格
if (params.getMinPrice() != null && params.getMaxPrice() != null) {
boolQuery.filter(QueryBuilders
.rangeQuery("price")
.gte(params.getMinPrice())
.lte(params.getMaxPrice()));
}
// 7.放入 source
request.source().query(boolQuery);
}
4.周边的酒店
4.1.需求分析
在酒店列表页的右侧,有一个小地图,点击地图的定位按钮,地图会找到你所在的位置
并且,在前端会发起查询请求,将你的坐标发送到服务端
我们要做的事情就是基于这个 location 坐标,然后按照距离对周围酒店排序。实现思路如下
- 修改 RequestParams 参数,接收 location 字段
- 修改 search 方法业务逻辑,如果 location 有值,添加根据
geo_distance
排序的功能
4.2.修改实体类
修改在cn.itcast.hotel.pojo
包下的实体类RequestParams:
src/main/java/cn/itcast/hotel/pojo/RequestParams.java
package cn.itcast.hotel.pojo;
import lombok.Data;
@Data
public class RequestParams {
private String key;
private Integer page;
private Integer size;
private String sortBy;
private String city;
private String brand;
private String starName;
private Integer minPrice;
private Integer maxPrice;
//当前的地理坐标
private String location;
}
4.3.距离排序 API
我们以前学习过排序功能,包括两种:
- 普通字段排序
- 地理坐标排序
我们只讲了普通字段排序对应的 java 写法。地理坐标排序只学过 DSL 语法。
距离排序与普通字段的排序有所差异,具体情况如下:
GET /indexName/_search
{
"query": {
"match_all": {}
},
"sort": [
{
"price": "asc"
},
{
"_geo_distance" : {
"FIELD" : "纬度,经度",
"order" : "asc",
"unit" : "km"
}
}
]
}
对应的 java 代码示例
4.4.添加距离排序
在 cn.itcast.hotel.service.impl
的 HotelService
的 search
方法中,添加一个排序功能
src/main/java/cn/itcast/hotel/service/impl/HotelService.java
完整代码
@Override
public PageResult search(RequestParams params) {
try {
// 1.准备Request
SearchRequest request = new SearchRequest("hotel");
// 2.准备DSL
// 2.1.query
buildBasicQuery(params, request);
// 2.2.分页
int page = params.getPage();
int size = params.getSize();
request.source().from((page - 1) * size).size(size);
// 2.3.排序
String location = params.getLocation();
if (location != null && !location.equals("")) {
request.source().sort(SortBuilders
.geoDistanceSort("location", new GeoPoint(location))
.order(SortOrder.ASC)
.unit(DistanceUnit.KILOMETERS)
);
}
// 3.发送请求
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
// 4.解析响应
return handleResponse(response);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
4.5.排序距离显示
4.5.1.解析
重启服务后,测试功能
发现确实可以实现对我附近酒店的排序,不过并没有看到酒店到底距离我多远,这该怎么办?
排序完成后,页面还要获取我附近每个酒店的具体距离值,这个值在响应结果中是独立的
因此,我们在结果解析阶段,除了解析source部分以外,还要得到sort部分,也就是排序的距离,然后放到响应结果中。
我们要做两件事:
- 修改 HotelDoc,添加排序距离字段,用于页面显示
- 修改 HotelService 类中的 handleResponse 方法,添加对 sort 值的获取
4.5.2.实体类添加距离字段
修改 HotelDoc 类,添加距离字段
src/main/java/cn/itcast/hotel/pojo/HotelDoc.java
package cn.itcast.hotel.pojo;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
public class HotelDoc {
private Long id;
private String name;
private String address;
private Integer price;
private Integer score;
private String brand;
private String city;
private String starName;
private String business;
private String location;
private String pic;
// 排序时的 距离值
private Object distance;
public HotelDoc(Hotel hotel) {
this.id = hotel.getId();
this.name = hotel.getName();
this.address = hotel.getAddress();
this.price = hotel.getPrice();
this.score = hotel.getScore();
this.brand = hotel.getBrand();
this.city = hotel.getCity();
this.starName = hotel.getStarName();
this.business = hotel.getBusiness();
this.location = hotel.getLatitude() + ", " + hotel.getLongitude();
this.pic = hotel.getPic();
}
}
4.5.3.添加排序业务
修改 HotelService 中的 handleResponse 方法
src/main/java/cn/itcast/hotel/service/impl/HotelService.java
// 获取排序值
Object[] sortValues = hit.getSortValues();
if (sortValues.length > 0) {
Object sortValue = sortValues[0];
hotelDoc.setDistance(sortValue);
}
重启后测试,发现页面能成功显示距离了
5.酒店竞价排名
需求:让指定的酒店在搜索结果中排名置顶
5.1.需求分析
要让指定酒店在搜索结果中排名置顶,效果如图
页面会给指定的酒店添加广告标记。
那怎样才能让指定的酒店排名置顶呢?
我们之前学习过的 function_score
查询可以影响算分,算分高了,自然排名也就高了。
而 function_score
包含 3 个要素:
- 过滤条件:哪些文档要加分
- 算分函数:如何计算
function score
- 加权方式:
function score
与query score
如何运算
这里的需求是:让指定酒店排名靠前。
因此我们需要给这些酒店添加一个标记,这样在过滤条件中就可以根据这个标记来判断,是否要提高算分。
比如,我们给酒店添加一个字段:isAD,Boolean 类型:
true
:是广告false
:不是广告
这样 function_score
包含 3 个要素就很好确定了:
- 过滤条件:判断 isAD 是否为 true
- 算分函数:我们可以用最简单暴力的 weight,固定加权值
- 加权方式:可以用默认的相乘,大大提高算分
因此,业务的实现步骤包括
- 给 HotelDoc 类添加 isAD 字段,Boolean 类型
- 挑选几个你喜欢的酒店,给它的文档数据添加 isAD 字段,值为 true
- 修改 search 方法,添加 function score 功能,给 isAD 值为 true 的酒店增加权重
5.2.修改 HotelDoc 实体
给 cn.itcast.hotel.pojo
包下的 HotelDoc 类添加 isAD 字段
src/main/java/cn/itcast/hotel/pojo/HotelDoc.java
private Boolean isAD;
5.3.添加广告标记
接下来,我们挑几个酒店,添加 isAD 字段,设置为 true
# 事实上这个值(1902197537 )是没有的
POST /hotel/_update/1902197537
{
"doc": {
"isAD": true
}
}
POST /hotel/_update/2056126831
{
"doc": {
"isAD": true
}
}
POST /hotel/_update/1989806195
{
"doc": {
"isAD": true
}
}
POST /hotel/_update/2056105938
{
"doc": {
"isAD": true
}
}
然后就报错了。
究其原因是视频里创建索引库的时候,并没有创建 isAD 这个字段。
参考博客:https://blog.csdn.net/weixin_44757863/article/details/120959505
只需在 kibana 控制台执行(追加该字段)代码即可
# 给索引库新增一个叫 isAD 的字段,类型是布尔类型
PUT /hotel/_mapping
{
"properties":{
"isAD":{
"type": "boolean"
}
}
}
# 给索引库 id 为 45845 的记录赋值,让其 isAD 字段为 true(用于测试广告竞价排名,该记录会靠前)
POST /hotel/_update/45845
{
"doc": {
"isAD":true
}
}
GET hotel/_doc/45845
5.4.添加算法函数查询
接下来我们就要修改查询条件了。之前是用的 boolean 查询,现在要改成 function_socre
查询。
function_score
查询结构如下
对应的 JavaAPI 如下
我们可以将之前写的 boolean 查询作为原始查询条件放到 query 中,
接下来就是添加过滤条件、算分函数、加权模式了。所以原来的代码依然可以沿用。
修改 cn.itcast.hotel.service.impl
包下的 HotelService
类中的 buildBasicQuery
方法,添加算分函数查询:
src/main/java/cn/itcast/hotel/service/impl/HotelService.java
private void buildBasicQuery(RequestParams params, SearchRequest request) {
// 1.构建BooleanQuery
BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
// 关键字搜索
String key = params.getKey();
if (key == null || "".equals(key)) {
boolQuery.must(QueryBuilders.matchAllQuery());
} else {
boolQuery.must(QueryBuilders.matchQuery("all", key));
}
// 城市条件
if (params.getCity() != null && !params.getCity().equals("")) {
boolQuery.filter(QueryBuilders.termQuery("city", params.getCity()));
}
// 品牌条件
if (params.getBrand() != null && !params.getBrand().equals("")) {
boolQuery.filter(QueryBuilders.termQuery("brand", params.getBrand()));
}
// 星级条件
if (params.getStarName() != null && !params.getStarName().equals("")) {
boolQuery.filter(QueryBuilders.termQuery("starName", params.getStarName()));
}
// 价格
if (params.getMinPrice() != null && params.getMaxPrice() != null) {
boolQuery.filter(QueryBuilders
.rangeQuery("price")
.gte(params.getMinPrice())
.lte(params.getMaxPrice())
);
}
// 2.算分控制
FunctionScoreQueryBuilder functionScoreQuery =
QueryBuilders.functionScoreQuery(
// 原始查询,相关性算分的查询
boolQuery,
// function score的数组
new FunctionScoreQueryBuilder.FilterFunctionBuilder[]{
// 其中的一个function score 元素
new FunctionScoreQueryBuilder.FilterFunctionBuilder(
// 过滤条件
QueryBuilders.termQuery("isAD", true),
// 算分函数
ScoreFunctionBuilders.weightFactorFunction(10)
)
});
request.source().query(functionScoreQuery);
}
# SpringCloudDay07
6.多条件的数据聚合
6.1.业务需求
需求:搜索页面的品牌、城市等信息不应该是在页面写死,而是通过聚合索引库中的酒店数据得来的
分析
目前,页面的城市列表、星级列表、品牌列表都是写死的,并不会随着搜索结果的变化而变化。
但是用户搜索条件改变时,搜索结果会跟着变化。
例如
用户搜索 “东方明珠”,那搜索的酒店肯定是在上海东方明珠附近。
因此,城市只能是上海,此时城市列表中就不应该显示北京、深圳、杭州这些信息了。
也就是说,搜索结果中包含哪些城市,页面就应该列出哪些城市;搜索结果中包含哪些品牌,页面就应该列出哪些品牌。
那么如何得知搜索结果中包含哪些品牌?如何得知搜索结果中包含哪些城市?
使用聚合功能,利用 Bucket 聚合,对搜索结果中的文档基于品牌分组、基于城市分组,就能得知包含哪些品牌、哪些城市了。
因为是对搜索结果聚合,因此聚合是限定范围的聚合,也就是说聚合的限定条件跟搜索文档的条件一致。
查看浏览器可以发现,请求参数与之前 search 时的 RequestParam 完全一致,即请求参数与搜索文档的参数完全一致。
这是在限定聚合时的文档范围。
返回值类型就是页面要展示的最终结果
结果是一个 Map 结构:
key
是字符串,城市、星级、品牌、价格value
是集合,例如多个城市的名称
6.2.业务实现
在 cn.itcast.hotel.web
包的 HotelController
中添加一个方法,遵循下面的要求:
- 请求方式:
POST
- 请求路径:
/hotel/filters
- 请求参数:
RequestParams
,与搜索文档的参数一致 - 返回值类型:
Map<String, List<String>>
这里调用了 IHotelService 中的 getFilters 方法,但尚未实现。
src/main/java/cn/itcast/hotel/web/HotelController.java
@PostMapping("filters")
public Map<String, List<String>> getFilters(@RequestBody RequestParams params){
return hotelService.getFilters(params);
}
在cn.itcast.hotel.service.IHotelService
中定义新方法
src/main/java/cn/itcast/hotel/service/IHotelService.java
/**
* 查询城市、星级、品牌的聚合结果
*
* @return 聚合结果,格式:{“城市”:[“上海”],“品牌”:[“如家”,“希尔顿”]}
*/
Map<String, List<String>> filters(RequestParams params);
在cn.itcast.hotel.service.impl.HotelService
中实现该方法
src/main/java/cn/itcast/hotel/service/impl/HotelService.java
@Override
public Map<String, List<String>> filters(RequestParams params) {
try {
// 1.准备Request
SearchRequest request = new SearchRequest("hotel");
// 2.准备DSL
// 2.1.query
buildBasicQuery(params, request);
// 2.2.设置size
request.source().size(0);
// 2.3.聚合
buildAggregation(request);
// 3.发出请求
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
// 4.解析结果
Map<String, List<String>> result = new HashMap<>();
Aggregations aggregations = response.getAggregations();
// 4.1.根据品牌名称,获取品牌结果
List<String> brandList = getAggByName(aggregations, "brandAgg");
result.put("品牌", brandList);
// 4.2.根据品牌名称,获取品牌结果
List<String> cityList = getAggByName(aggregations, "cityAgg");
result.put("城市", cityList);
// 4.3.根据品牌名称,获取品牌结果
List<String> starList = getAggByName(aggregations, "starAgg");
result.put("星级", starList);
return result;
} catch (IOException e) {
throw new RuntimeException(e);
}
}
获取聚合名称
private void buildAggregation(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)
);
}
封装聚合条件
private List<String> getAggByName(Aggregations aggregations, String aggName) {
// 4.1.根据聚合名称获取聚合结果
Terms brandTerms = aggregations.get(aggName);
// 4.2.获取buckets
List<? extends Terms.Bucket> buckets = brandTerms.getBuckets();
// 4.3.遍历
List<String> brandList = new ArrayList<>();
for (Terms.Bucket bucket : buckets) {
// 4.4.获取key
String key = bucket.getKeyAsString();
brandList.add(key);
}
return brandList;
}
7.实现酒店搜索框自动补全
此时我们的 hotel 索引库还没有设置拼音分词器,需要修改索引库中的配置。
但是我们知道索引库是无法修改的,只能删除然后重新创建。
另外,我们需要添加一个字段,用来做自动补全,将 brand、suggestion、city 等都放进去,作为自动补全的提示。
因此,总结一下,我们需要做的事情包括:
- 修改 hotel 索引库结构,设置自定义拼音分词器
- 修改索引库的 name、all 字段,使用自定义分词器
- 索引库添加一个新字段 suggestion,类型为 completion 类型,使用自定义的分词器
- 给 HotelDoc 类添加 suggestion 字段,内容包含 brand、business
- 重新导入数据到 hotel 库
7.1.修改酒店映射结构
先删除之前创建的索引库
DELETE /hotel
再创建新的索引库(映射结构发生变化)
// 酒店数据索引库
PUT /hotel
{
"settings": {
"analysis": {
"analyzer": {
"text_anlyzer": {
"tokenizer": "ik_max_word",
"filter": "py"
},
"completion_analyzer": {
"tokenizer": "keyword",
"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": {
"id":{
"type": "keyword"
},
"name":{
"type": "text",
"analyzer": "text_anlyzer",
"search_analyzer": "ik_smart",
"copy_to": "all"
},
"address":{
"type": "keyword",
"index": false
},
"price":{
"type": "integer"
},
"score":{
"type": "integer"
},
"brand":{
"type": "keyword",
"copy_to": "all"
},
"city":{
"type": "keyword"
},
"starName":{
"type": "keyword"
},
"business":{
"type": "keyword",
"copy_to": "all"
},
"location":{
"type": "geo_point"
},
"pic":{
"type": "keyword",
"index": false
},
"all":{
"type": "text",
"analyzer": "text_anlyzer",
"search_analyzer": "ik_smart"
},
"suggestion":{
"type": "completion",
"analyzer": "completion_analyzer"
}
}
}
}
7.2.修改 HotelDoc 实体
HotelDoc 中要添加一个字段,用来做自动补全,内容可以是酒店品牌、城市、商圈等信息。
按照自动补全字段的要求,最好是这些字段的数组。
因此我们在 HotelDoc 中添加一个 suggestion 字段,类型为 List<String>
,然后将 brand、city、business 等信息放到里面。
代码如下:
src/main/java/cn/itcast/hotel/pojo/HotelDoc.java
package cn.itcast.hotel.pojo;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
@Data
@NoArgsConstructor
public class HotelDoc {
private Long id;
private String name;
private String address;
private Integer price;
private Integer score;
private String brand;
private String city;
private String starName;
private String business;
private String location;
private String pic;
private Object distance;
private Boolean isAD;
private List<String> suggestion;
public HotelDoc(Hotel hotel) {
this.id = hotel.getId();
this.name = hotel.getName();
this.address = hotel.getAddress();
this.price = hotel.getPrice();
this.score = hotel.getScore();
this.brand = hotel.getBrand();
this.city = hotel.getCity();
this.starName = hotel.getStarName();
this.business = hotel.getBusiness();
this.location = hotel.getLatitude() + ", " + hotel.getLongitude();
this.pic = hotel.getPic();
// 组装 suggestion
if(this.business.contains("/")){
// business 有多个值,需要切割
String[] arr = this.business.split("/");
// 添加元素
this.suggestion = new ArrayList<>();
this.suggestion.add(this.brand);
Collections.addAll(this.suggestion, arr);
}else {
this.suggestion = Arrays.asList(this.brand, this.business);
}
}
}
7.3.重新导入
重新执行之前编写的导入数据功能,可以看到新的酒店数据中包含了 suggestion
相关的导入功能在 src/test/java/cn/itcast/hotel/HotelDocumentTest.java
中的 testBulkRequest()
方法中实现了。
GET /hotel/_search
{
"query": {
"match_all": {}
}
}
GET /hotel/_search
{
"suggest": {
"suggestions": {
"text": "s",
"completion": {
"field": "suggestion",
"skip_duplicates": true,
"size": 10
}
}
}
}
7.4.自动补全查询的 JavaAPI
之前我们学习了自动补全查询的 DSL,而没有学习对应的 JavaAPI,这里给出一个示例
而自动补全的结果也比较特殊,解析的代码如下
src/test/java/cn/itcast/hotel/HotelSearchTest.java
/**
* 自动补全查询
*
* @throws IOException
*/
@Test
void testSuggest() throws IOException {
//1.准备 Request
SearchRequest request = new SearchRequest("hotel");
//2.准备 DSL
request.source().suggest(new SuggestBuilder().addSuggestion(
"suggestions",
SuggestBuilders.completionSuggestion("suggestion")
.prefix("h")
.skipDuplicates(true)
.size(10)
));
//3.发送请求
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
//4.解析结果
//System.out.println(response);
handleCompletionResponse(response);
}
/**
* 处理补全结果
*
* @param response
*/
private void handleCompletionResponse(SearchResponse response) {
//4.处理结果
Suggest suggest = response.getSuggest();
//4.1.根据名称获取补全结果
CompletionSuggestion suggestion = suggest.getSuggestion("suggestions");
//4.2.获取 options 并遍历
for (CompletionSuggestion.Entry.Option option : suggestion.getOptions()) {
//4.3.获取一个 option 的 text ,也就是补全的词条
String text = option.getText().string();
System.out.println(text);
}
}
7.5.实现搜索框自动补全
查看前端页面,可以发现当我们在输入框键入时,前端会发起 ajax 请求
返回值是补全词条的集合,类型为 List<String>
- 在
cn.itcast.hotel.web
包下的HotelController
中添加新接口,接收新的请求
src/main/java/cn/itcast/hotel/web/HotelController.java
@GetMapping("suggestion")
public List<String> getSuggestions(@RequestParam("key") String prefix) {
return hotelService.getSuggestions(prefix);
}
- 在
cn.itcast.hotel.service
包下的IhotelService
中添加方法
src/main/java/cn/itcast/hotel/service/IHotelService.java
List<String> getSuggestions(String prefix);
- 在
cn.itcast.hotel.service.impl.HotelService
中实现该方法
src/main/java/cn/itcast/hotel/service/impl/HotelService.java
@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 suggestions = suggest.getSuggestion("suggestions");
// 4.2.获取 options
List<CompletionSuggestion.Entry.Option> options = suggestions.getOptions();
// 4.3.遍历
List<String> list = new ArrayList<>(options.size());
for (CompletionSuggestion.Entry.Option option : options) {
String text = option.getText().toString();
list.add(text);
}
return list;
} catch (IOException e) {
throw new RuntimeException(e);
}
}
8.实现数据同步
elasticsearch 中的酒店数据来自于 mysql 数据库,因此 mysql 数据发生改变时,elasticsearch 也必须跟着改变。
这个就是 elasticsearch 与 mysql 之间的数据同步。
在微服务中,负责酒店管理(操作 MySQL)的业务与负责酒店搜索(操作 ElasticSearch)的业务可能在两台不同的微服务上。
那么此时的数据同步应该如何实现呢?
常见的数据同步方案有三种:同步调用、异步通知、监听 binlog
案例:基于
MQ
来实现MySQL
与ElsaticSearch
数据同步
8.1.思路
利用课前资料提供的 hotel-admin
项目作为酒店管理的微服务。
当酒店数据发生增、删、改时,要求对 elasticsearch 中数据也要完成相同操作。
步骤
- 导入 课前资料 提供的
hotel-admin
项目,启动并测试酒店数据的 CRUD - 声明 exchange、queue、RoutingKey
- 在
hotel-admin
中的增、删、改业务中完成消息发送 - 在
hotel-demo
中完成消息监听,并更新 elasticsearch 中数据 - 启动并测试数据同步功能
8.2.导入 demo
导入 课前资料 提供的 hotel-admin
项目
运行后,访问 http://localhost:8099
其中包含了酒店的 CRUD 功能
hotel-admin
项目下的 src/main/java/cn/itcast/hotel/web/HotelController.java
@PostMapping
public void saveHotel(@RequestBody Hotel hotel){
hotelService.save(hotel);
}
@PutMapping()
public void updateById(@RequestBody Hotel hotel){
if (hotel.getId() == null) {
throw new InvalidParameterException("id 不能为空");
}
hotelService.updateById(hotel);
}
@DeleteMapping("/{id}")
public void deleteById(@PathVariable("id") Long id) {
hotelService.removeById(id);
}
8.3.声明交换机、队列
MQ 结构如图
8.3.1.配置文件
引入依赖
在 hotel-admin
、hotel-demo
中引入 RabbitMQ 的依赖
pom.xml
<!-- AMQP -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
配置 RabbitMQ 地址
在 hotel-admin
、hotel-demo
项目中的 src/main/resources/application.yaml
下配置 RabbitMQ 的地址
spring:
rabbitmq:
host: 192.168.150.101
port: 5672
username: itcast
password: 123456
virtual-host: /
这里补充一句,记得开启 RabbitMQ 服务
使用 docker start [你使用 Docker 创建的 RabbitMQ 容器的名称]
启动即可
docker start mq
8.3.2.声明队列交换机的名称
在 hotel-admin
和 hotel-demo
中的 cn.itcast.hotel.constatnts
包下新建一个类 MqConstants
src/main/java/cn/itcast/hotel/constants/MqConstants.java
package cn.itcast.hotel.constatnts;
public class MqConstants {
/**
* 交换机
*/
public final static String HOTEL_EXCHANGE = "hotel.topic";
/**
* 监听新增和修改的队列
*/
public final static String HOTEL_INSERT_QUEUE = "hotel.insert.queue";
/**
* 监听删除的队列
*/
public final static String HOTEL_DELETE_QUEUE = "hotel.delete.queue";
/**
* 新增或修改的 RoutingKey
*/
public final static String HOTEL_INSERT_KEY = "hotel.insert";
/**
* 删除的 RoutingKey
*/
public final static String HOTEL_DELETE_KEY = "hotel.delete";
}
8.3.3.声明队列交换机
一般都是在消费者中声明交换机、队列的。
故选择在 hotel-demo
项目中定义配置类(声明队列、交换机)
一般来说,有两种方式来声明交换机和队列的绑定关系、以及队列和交换机的对象
两种方式:1.基于注解的方式;2.基于 Bean 的方式
这里是基于 Bean 的方式来声明队列交换机。资料中提供的最终代码则是基于注解来声明队列交换机的。
src/main/java/cn/itcast/hotel/config/MqConfig.java
package cn.itcast.hotel.config;
import cn.itcast.hotel.constants.MqConstants;
import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.Queue;
import org.springframework.amqp.core.TopicExchange;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class MqConfig {
@Bean
public TopicExchange topicExchange(){
return new TopicExchange(MqConstants.HOTEL_EXCHANGE, true, false);
}
@Bean
public Queue insertQueue(){
return new Queue(MqConstants.HOTEL_INSERT_QUEUE, true);
}
@Bean
public Queue deleteQueue(){
return new Queue(MqConstants.HOTEL_DELETE_QUEUE, true);
}
@Bean
public Binding insertQueueBinding(){
return BindingBuilder.bind(insertQueue()).to(topicExchange()).with(MqConstants.HOTEL_INSERT_KEY);
}
@Bean
public Binding deleteQueueBinding(){
return BindingBuilder.bind(deleteQueue()).to(topicExchange()).with(MqConstants.HOTEL_DELETE_KEY);
}
}
8.4.发送 MQ 消息
在 hotel-admin
中的增、删、改业务中分别发送 MQ 消息
src/main/java/cn/itcast/hotel/web/HotelController.java
完整代码
package cn.itcast.hotel.web;
import cn.itcast.hotel.constants.MqConstants;
import cn.itcast.hotel.pojo.Hotel;
import cn.itcast.hotel.pojo.PageResult;
import cn.itcast.hotel.service.IHotelService;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.security.InvalidParameterException;
@RestController
@RequestMapping("hotel")
public class HotelController {
@Autowired
private IHotelService hotelService;
@Autowired
private RabbitTemplate rabbitTemplate;
@GetMapping("/{id}")
public Hotel queryById(@PathVariable("id") Long id) {
return hotelService.getById(id);
}
@GetMapping("/list")
public PageResult hotelList(
@RequestParam(value = "page", defaultValue = "1") Integer page,
@RequestParam(value = "size", defaultValue = "1") Integer size
) {
Page<Hotel> result = hotelService.page(new Page<>(page, size));
return new PageResult(result.getTotal(), result.getRecords());
}
@PostMapping
public void saveHotel(@RequestBody Hotel hotel) {
hotelService.save(hotel);
//交换机、RoutingKey、要发送的内容
rabbitTemplate.convertAndSend(MqConstants.HOTEL_EXCHANGE, MqConstants.HOTEL_INSERT_KEY, hotel.getId());
}
@PutMapping()
public void updateById(@RequestBody Hotel hotel) {
if (hotel.getId() == null) {
throw new InvalidParameterException("id 不能为空");
}
hotelService.updateById(hotel);
rabbitTemplate.convertAndSend(MqConstants.HOTEL_EXCHANGE, MqConstants.HOTEL_INSERT_KEY, hotel.getId());
}
@DeleteMapping("/{id}")
public void deleteById(@PathVariable("id") Long id) {
hotelService.removeById(id);
rabbitTemplate.convertAndSend(MqConstants.HOTEL_EXCHANGE, MqConstants.HOTEL_DELETE_KEY, id);
}
}
8.5.接收 MQ 消息
hotel-demo
接收到 MQ 消息要做的事情包括
- 新增消息:根据传递的 hotel 的 id 查询 hotel 信息,然后新增一条数据到索引库
- 删除消息:根据传递的 hotel 的 id 删除索引库中的一条数据
8.5.1.新增和删除的业务
首先在 hotel-demo
的 cn.itcast.hotel.service
包下的 IHotelService 中编写新增、删除业务的方法
src/main/java/cn/itcast/hotel/service/IHotelService.java
void deleteById(Long id);
void insertById(Long id);
8.5.2.业务实现
给 hotel-demo
中的 cn.itcast.hotel.service.impl
包下的 HotelService 中实现业务
src/main/java/cn/itcast/hotel/service/impl/HotelService.java
@Override
public void deleteById(Long id) {
try {
// 1.准备 Request
DeleteRequest request = new DeleteRequest("hotel", id.toString());
// 2.发送请求
client.delete(request, RequestOptions.DEFAULT);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
@Override
public void insertById(Long id) {
try {
// 0.根据 id 查询酒店数据
Hotel hotel = getById(id);
// 转换为文档类型
HotelDoc hotelDoc = new HotelDoc(hotel);
// 1.准备 Request 对象
IndexRequest request = new IndexRequest("hotel").id(hotel.getId().toString());
// 2.准备 Json 文档
request.source(JSON.toJSONString(hotelDoc), XContentType.JSON);
// 3.发送请求
client.index(request, RequestOptions.DEFAULT);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
8.5.3.编写监听器
在 hotel-demo
中的 cn.itcast.hotel.mq
包新增一个类
src/main/java/cn/itcast/hotel/mq/HotelListener.java
package cn.itcast.hotel.mq;
import cn.itcast.hotel.constants.MqConstants;
import cn.itcast.hotel.service.IHotelService;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
public class HotelListener {
@Autowired
private IHotelService hotelService;
/**
* 监听酒店新增或修改的业务
* @param id 酒店id
*/
@RabbitListener(queues = MqConstants.HOTEL_INSERT_QUEUE)
public void listenHotelInsertOrUpdate(Long id){
hotelService.insertById(id);
}
/**
* 监听酒店删除的业务
* @param id 酒店id
*/
@RabbitListener(queues = MqConstants.HOTEL_DELETE_QUEUE)
public void listenHotelDelete(Long id){
hotelService.deleteById(id);
}
}
8.6.测试同步功能
直接访问 虚拟机的IP地址:15672
队列
交换机
点击上方的 hotel.topic
,查看交换机和队列绑定关系
将酒店价格由 2688 改为 2888
查询此条数据的 id
将价格修改为 2888
查看交换机的 hotel.topic
情况
回到 http://localhost:8089/
页面,发现数据修改的数据同步成功
删除数据、修改的数据同步同理,此处不作演示。 (主要是懒得截图)