学习笔记:SpringCloud 微服务技术栈_实用篇②_黑马旅游案例


  • 若文章内容或图片失效,请留言反馈。部分素材来自网络,若不小心影响到您的利益,请联系博主删除。

前言



  • 写这篇博客旨在制作笔记,巩固知识。同时方便个人在线阅览,回顾知识。
  • 博客的内容主要来自视频内容和资料中提供的学习笔记。


系列目录


SpringCloud 微服务技术栈_实用篇①_基础知识

SpringCloud 微服务技术栈_实用篇②_黑马旅游案例


SpringCloud 微服务技术栈_高级篇①_微服务保护

SpringCloud 微服务技术栈_高级篇②_分布式事务

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,接收页面的请求,调用 IHotelServicesearch 方法
  • 步骤三:编写业务实现,定义 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.修改搜索业务


HotelServicesearch 方法中,只有一个地方需要修改: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.implHotelServicesearch 方法中,添加一个排序功能

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 scorequery score 如何运算

这里的需求是:让指定酒店排名靠前。

因此我们需要给这些酒店添加一个标记,这样在过滤条件中就可以根据这个标记来判断,是否要提高算分

比如,我们给酒店添加一个字段:isADBoolean 类型:

  • true:是广告
  • false:不是广告

这样 function_score 包含 3 个要素就很好确定了:

  • 过滤条件:判断 isAD 是否为 true
  • 算分函数:我们可以用最简单暴力的 weight,固定加权值
  • 加权方式:可以用默认的相乘,大大提高算分

因此,业务的实现步骤包括

  1. HotelDoc 类添加 isAD 字段,Boolean 类型
  2. 挑选几个你喜欢的酒店,给它的文档数据添加 isAD 字段,值为 true
  3. 修改 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 索引库还没有设置拼音分词器,需要修改索引库中的配置。

但是我们知道索引库是无法修改的,只能删除然后重新创建。

另外,我们需要添加一个字段,用来做自动补全,将 brandsuggestioncity 等都放进去,作为自动补全的提示。


因此,总结一下,我们需要做的事情包括:

  1. 修改 hotel 索引库结构,设置自定义拼音分词器
  2. 修改索引库的 nameall 字段,使用自定义分词器
  3. 索引库添加一个新字段 suggestion,类型为 completion 类型,使用自定义的分词器
  4. HotelDoc 类添加 suggestion 字段,内容包含 brandbusiness
  5. 重新导入数据到 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>,然后将 brandcitybusiness 等信息放到里面。

代码如下:

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>


  1. 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);
}

  1. cn.itcast.hotel.service 包下的 IhotelService 中添加方法

src/main/java/cn/itcast/hotel/service/IHotelService.java

List<String> getSuggestions(String prefix);

  1. 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 也必须跟着改变。

这个就是 elasticsearchmysql 之间的数据同步


在微服务中,负责酒店管理(操作 MySQL)的业务与负责酒店搜索(操作 ElasticSearch)的业务可能在两台不同的微服务上。

那么此时的数据同步应该如何实现呢?

常见的数据同步方案有三种:同步调用、异步通知、监听 binlog


案例基于 MQ 来实现 MySQLElsaticSearch 数据同步


8.1.思路


利用课前资料提供的 hotel-admin 项目作为酒店管理的微服务。

当酒店数据发生增、删、改时,要求对 elasticsearch 中数据也要完成相同操作。


步骤

  • 导入 课前资料 提供的 hotel-admin 项目,启动并测试酒店数据的 CRUD
  • 声明 exchangequeueRoutingKey
  • 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-adminhotel-demo 中引入 RabbitMQ 的依赖

pom.xml

<!-- AMQP -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-amqp</artifactId>
</dependency>

配置 RabbitMQ 地址

hotel-adminhotel-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-adminhotel-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 消息要做的事情包括

  • 新增消息:根据传递的 hotelid 查询 hotel 信息,然后新增一条数据到索引库
  • 删除消息:根据传递的 hotelid 删除索引库中的一条数据

8.5.1.新增和删除的业务


首先在 hotel-democn.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/ 页面,发现数据修改的数据同步成功

在这里插入图片描述


删除数据、修改的数据同步同理,此处不作演示。 (主要是懒得截图)


  • 6
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值