- 学习视频链接
- SpringCloud + RabbitMQ + Docker + Redis + 搜索 + 分布式,史上最全面的 SpringCloud 微服务技术栈课程 | 黑马程序员 Java 微服务
- 学习资料链接
- https://pan.baidu.com/s/169SFtYEvel44hRJhmFTRTQ(
提取码:1234
)
- 强调:本博客主要是对 【SpringCloud 微服务技术栈 | 实用篇① | 基础知识】 内容的补充
SpringCloud 微服务技术栈_实用篇①_基础知识
SpringCloud 微服务技术栈_实用篇②_黑马旅游案例
SpringCloud 微服务技术栈_高级篇①_微服务保护
SpringCloud 微服务技术栈_高级篇②_分布式事务
SpringCloud 微服务技术栈_高级篇③_分布式缓存
SpringCloud 微服务技术栈_高级篇④_多级缓存
SpringCloud 微服务技术栈_高级篇⑤_可靠消息服务
通过该案例来实战演练下之前所学知识。
实现四部分功能:
启动资料中提供的 hotel-demo
项目,其默认端口是 8089,访问 http://localhost:8090
,就能看到项目页面了。
- 课前资料链接:https://pan.baidu.com/s/169SFtYEvel44hRJhmFTRTQ(
提取码:1234
)1.微服务开发框架 SpringCloud + RabbitMQ + Docker + Redis + 搜索 + 分布式史上最全面的微服务全技术栈课程>
实用篇>学习资料>day06-Elasticsearch02>代码
需求:实现黑马旅游的酒店搜索功能,完成关键字搜索和分页
在项目的首页,有一个大大的搜索框,还有分页按钮
点击搜索按钮,可以看到浏览器控制台发出了请求
请求参数如下
由此可以知道,我们这个请求的信息如下
/hotel/list
key
:搜索关键字page
:页码size
:每页大小sortBy
:排序,目前暂不实现total
:总条数List
:当前页的数据因此,我们实现业务的流程如下
实体类有两个,一个是前端的请求参数实体,一个是服务端应该返回的响应结果实体。
前端请求的 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;
}
分页查询,需要返回分页结果 PageResult,包含两个属性
total
:总条数List
:当前页的数据因此,我们在 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;
}
}
定义一个 HotelController,声明查询接口,满足下列要求:
/hotel/list
Long total
:总条数List 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);
}
}
我们在 controller 调用了 IHotelService,并没有实现该方法。
因此下面我们就在 IHotelService 中定义方法,并且去实现业务逻辑。
在 cn.itcast.hotel.service
中的 IHotelService
接口中定义一个方法
src/main/java/cn/itcast/hotel/service/IHotelService.java
/**
* 根据关键字搜索酒店信息
*
* @param params 请求参数对象,包含用户输入的关键字
* @return 酒店文档列表
*/
PageResult search(RequestParams params);
实现搜索业务,肯定离不开 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")
));
}
在 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);
}
需求:添加品牌、城市、星级、价格等过滤功能
在页面搜索框下面,会有一些过滤项
传递的参数如图
包含的过滤条件有
brand
:品牌值city
:城市minPrice~maxPrice
:价格范围starName
:星级我们需要做两件事情
修改在 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;
}
在 HotelService 的 search 方法中,只有一个地方需要修改:requet.source().query( ... )
其中的查询条件。
在之前的业务中,只有 match 查询,根据关键字搜索,现在要添加条件过滤,包括:
多个查询条件组合,肯定是 boolean 查询来组合:
因为条件构建的逻辑比较复杂,这里先封装为一个函数
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);
}
在酒店列表页的右侧,有一个小地图,点击地图的定位按钮,地图会找到你所在的位置
并且,在前端会发起查询请求,将你的坐标发送到服务端
我们要做的事情就是基于这个 location 坐标,然后按照距离对周围酒店排序。实现思路如下
geo_distance
排序的功能修改在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;
}
我们以前学习过排序功能,包括两种:
我们只讲了普通字段排序对应的 java 写法。地理坐标排序只学过 DSL 语法。
距离排序与普通字段的排序有所差异,具体情况如下:
GET /indexName/_search
{
"query": {
"match_all": {}
},
"sort": [
{
"price": "asc"
},
{
"_geo_distance" : {
"FIELD" : "纬度,经度",
"order" : "asc",
"unit" : "km"
}
}
]
}
对应的 java 代码示例
在 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);
}
}
重启服务后,测试功能
发现确实可以实现对我附近酒店的排序,不过并没有看到酒店到底距离我多远,这该怎么办?
排序完成后,页面还要获取我附近每个酒店的具体距离值,这个值在响应结果中是独立的
因此,我们在结果解析阶段,除了解析source部分以外,还要得到sort部分,也就是排序的距离,然后放到响应结果中。
我们要做两件事:
修改 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();
}
}
修改 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);
}
重启后测试,发现页面能成功显示距离了
需求:让指定的酒店在搜索结果中排名置顶
要让指定酒店在搜索结果中排名置顶,效果如图
页面会给指定的酒店添加广告标记。
那怎样才能让指定的酒店排名置顶呢?
我们之前学习过的 function_score
查询可以影响算分,算分高了,自然排名也就高了。
而 function_score
包含 3 个要素:
function score
function score
与 query score
如何运算这里的需求是:让指定酒店排名靠前。
因此我们需要给这些酒店添加一个标记,这样在过滤条件中就可以根据这个标记来判断,是否要提高算分。
比如,我们给酒店添加一个字段:isAD,Boolean 类型:
true
:是广告false
:不是广告这样 function_score
包含 3 个要素就很好确定了:
因此,业务的实现步骤包括
给 cn.itcast.hotel.pojo
包下的 HotelDoc 类添加 isAD 字段
src/main/java/cn/itcast/hotel/pojo/HotelDoc.java
private Boolean isAD;
接下来,我们挑几个酒店,添加 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
接下来我们就要修改查询条件了。之前是用的 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);
}
需求:搜索页面的品牌、城市等信息不应该是在页面写死,而是通过聚合索引库中的酒店数据得来的
分析
目前,页面的城市列表、星级列表、品牌列表都是写死的,并不会随着搜索结果的变化而变化。
但是用户搜索条件改变时,搜索结果会跟着变化。
例如
用户搜索 “东方明珠”,那搜索的酒店肯定是在上海东方明珠附近。
因此,城市只能是上海,此时城市列表中就不应该显示北京、深圳、杭州这些信息了。
也就是说,搜索结果中包含哪些城市,页面就应该列出哪些城市;搜索结果中包含哪些品牌,页面就应该列出哪些品牌。
那么如何得知搜索结果中包含哪些品牌?如何得知搜索结果中包含哪些城市?
使用聚合功能,利用 Bucket 聚合,对搜索结果中的文档基于品牌分组、基于城市分组,就能得知包含哪些品牌、哪些城市了。
因为是对搜索结果聚合,因此聚合是限定范围的聚合,也就是说聚合的限定条件跟搜索文档的条件一致。
查看浏览器可以发现,请求参数与之前 search 时的 RequestParam 完全一致,即请求参数与搜索文档的参数完全一致。
这是在限定聚合时的文档范围。
返回值类型就是页面要展示的最终结果
结果是一个 Map 结构:
key
是字符串,城市、星级、品牌、价格value
是集合,例如多个城市的名称在 cn.itcast.hotel.web
包的 HotelController
中添加一个方法,遵循下面的要求:
POST
/hotel/filters
RequestParams
,与搜索文档的参数一致Map>
这里调用了 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;
}
此时我们的 hotel 索引库还没有设置拼音分词器,需要修改索引库中的配置。
但是我们知道索引库是无法修改的,只能删除然后重新创建。
另外,我们需要添加一个字段,用来做自动补全,将 brand、suggestion、city 等都放进去,作为自动补全的提示。
因此,总结一下,我们需要做的事情包括:
先删除之前创建的索引库
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"
}
}
}
}
HotelDoc 中要添加一个字段,用来做自动补全,内容可以是酒店品牌、城市、商圈等信息。
按照自动补全字段的要求,最好是这些字段的数组。
因此我们在 HotelDoc 中添加一个 suggestion 字段,类型为 List
,然后将 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);
}
}
}
重新执行之前编写的导入数据功能,可以看到新的酒店数据中包含了 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
}
}
}
}
之前我们学习了自动补全查询的 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);
}
}
查看前端页面,可以发现当我们在输入框键入时,前端会发起 ajax 请求
返回值是补全词条的集合,类型为 List
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);
}
}
elasticsearch 中的酒店数据来自于 mysql 数据库,因此 mysql 数据发生改变时,elasticsearch 也必须跟着改变。
这个就是 elasticsearch 与 mysql 之间的数据同步。
在微服务中,负责酒店管理(操作 MySQL)的业务与负责酒店搜索(操作 ElasticSearch)的业务可能在两台不同的微服务上。
那么此时的数据同步应该如何实现呢?
常见的数据同步方案有三种:同步调用、异步通知、监听 binlog
案例:基于
MQ
来实现MySQL
与ElsaticSearch
数据同步
利用课前资料提供的 hotel-admin
项目作为酒店管理的微服务。
当酒店数据发生增、删、改时,要求对 elasticsearch 中数据也要完成相同操作。
步骤
hotel-admin
项目,启动并测试酒店数据的 CRUDhotel-admin
中的增、删、改业务中完成消息发送hotel-demo
中完成消息监听,并更新 elasticsearch 中数据导入 课前资料 提供的 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);
}
MQ 结构如图
引入依赖
在 hotel-admin
、hotel-demo
中引入 RabbitMQ 的依赖
pom.xml
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-amqpartifactId>
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
在 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";
}
一般都是在消费者中声明交换机、队列的。
故选择在 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);
}
}
在 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);
}
}
hotel-demo
接收到 MQ 消息要做的事情包括
首先在 hotel-demo
的 cn.itcast.hotel.service
包下的 IHotelService 中编写新增、删除业务的方法
src/main/java/cn/itcast/hotel/service/IHotelService.java
void deleteById(Long id);
void insertById(Long id);
给 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);
}
}
在 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);
}
}
直接访问 虚拟机的IP地址:15672
队列
交换机
点击上方的 hotel.topic
,查看交换机和队列绑定关系
将酒店价格由 2688 改为 2888
查询此条数据的 id
将价格修改为 2888
查看交换机的 hotel.topic
情况
回到 http://localhost:8089/
页面,发现数据修改的数据同步成功
删除数据、修改的数据同步同理,此处不作演示。 (主要是懒得截图)