工程文件

1.分布式搜索引擎elasticsearch黑马旅游案例

下面,我们通过黑马旅游的案例来实战演练下之前学习的知识。

我们实现四部分功能:

  • 酒店搜索和分页
  • 酒店结果过滤
  • 我周边的酒店
  • 酒店竞价排名
  • 搜索框自动补全
  • 对搜索结果聚合(过滤词条)
  • 修改数据同步es

启动我们提供的hotel-demo项目,其默认端口是8089,访问http://localhost:8090,就能看到项目页面了:

image.png

1.酒店搜索和分页

案例需求:实现黑马旅游的酒店搜索功能,完成关键字搜索和分页

1.1.需求分析

在项目的首页,有一个大大的搜索框,还有分页按钮:

image.png

点击搜索按钮,可以看到浏览器控制台发出了请求:

image.png

请求参数如下:

image.png

由此可以知道,我们这个请求的信息如下:

  • 请求方式:POST
  • 请求路径:/hotel/list
  • 请求参数:JSON对象,包含4个字段:
    • key:搜索关键字
    • page:页码
    • size:每页大小
    • sortBy:排序,目前暂不实现
  • 返回值:分页查询,需要返回分页结果PageResult,包含两个属性:
    • total:总条数
    • List<HotelDoc>:当前页的数据

因此,我们实现业务的流程如下:

  • 步骤一:定义实体类,接收请求参数的JSON对象
  • 步骤二:编写controller,接收页面的请求
  • 步骤三:编写业务实现,利用RestHighLevelClient实现搜索、分页

1.2.定义实体类

实体类有两个,一个是前端的请求参数实体,一个是服务端应该返回的响应结果实体。

1)请求参数

前端请求的json结构如下:

  1. {
  2. "key": "搜索关键字",
  3. "page": 1,
  4. "size": 3,
  5. "sortBy": "default"
  6. }

因此,我们在cn.itcast.hotel.pojo包下定义一个实体类:

  1. package cn.itcast.hotel.pojo;
  2. import lombok.Data;
  3. @Data
  4. public class RequestParams {
  5. private String key;
  6. private Integer page;
  7. private Integer size;
  8. private String sortBy;
  9. }

2)返回值

分页查询,需要返回分页结果PageResult,包含两个属性:

  • total:总条数
  • List<HotelDoc>:当前页的数据

因此,我们在cn.itcast.hotel.pojo中定义返回结果:

  1. package cn.itcast.hotel.pojo;
  2. import lombok.Data;
  3. import java.util.List;
  4. @Data
  5. public class PageResult {
  6. private Long total;
  7. private List<HotelDoc> hotels;
  8. public PageResult() {
  9. }
  10. public PageResult(Long total, List<HotelDoc> hotels) {
  11. this.total = total;
  12. this.hotels = hotels;
  13. }
  14. }

1.3.定义controller

定义一个HotelController,声明查询接口,满足下列要求:

  • 请求方式:Post
  • 请求路径:/hotel/list
  • 请求参数:对象,类型为RequestParam
  • 返回值:PageResult,包含两个属性
    • Long total:总条数
    • List<HotelDoc> hotels:酒店数据

因此,我们在cn.itcast.hotel.web中定义HotelController:

  1. @RestController
  2. @RequestMapping("/hotel")
  3. public class HotelController {
  4. @Autowired
  5. private IHotelService hotelService;
  6. // 搜索酒店数据
  7. @PostMapping("/list")
  8. public PageResult search(@RequestBody RequestParams params){
  9. return hotelService.search(params);
  10. }
  11. }

1.4.实现搜索业务

我们在controller调用了IHotelService,并没有实现该方法,因此下面我们就在IHotelService中定义方法,并且去实现业务逻辑。

1)在cn.itcast.hotel.service中的IHotelService接口中定义一个方法:

  1. /**
  2. * 根据关键字搜索酒店信息
  3. * @param params 请求参数对象,包含用户输入的关键字
  4. * @return 酒店文档列表
  5. */
  6. PageResult search(RequestParams params);

2)实现搜索业务,肯定离不开RestHighLevelClient,我们需要把它注册到Spring中作为一个Bean。在cn.itcast.hotel中的HotelDemoApplication中声明这个Bean:

  1. @Bean
  2. public RestHighLevelClient client(){
  3. return new RestHighLevelClient(RestClient.builder(
  4. HttpHost.create("http://192.168.150.101:9200")
  5. ));
  6. }

3)在cn.itcast.hotel.service.impl中的HotelService中实现search方法:

  1. @Override
  2. public PageResult search(RequestParams params) {
  3. try {
  4. // 1.准备Request
  5. SearchRequest request = new SearchRequest("hotel");
  6. // 2.准备DSL
  7. // 2.1.query
  8. String key = params.getKey();
  9. if (key == null || "".equals(key)) {
  10. request.source().query(QueryBuilders.matchAllQuery());
  11. } else {
  12. request.source().query(QueryBuilders.matchQuery("all", key));
  13. }
  14. // 2.2.分页
  15. int page = params.getPage();
  16. int size = params.getSize();
  17. request.source().from((page - 1) * size).size(size);
  18. // 3.发送请求
  19. SearchResponse response = client.search(request, RequestOptions.DEFAULT);
  20. // 4.解析响应
  21. return handleResponse(response);
  22. } catch (IOException e) {
  23. throw new RuntimeException(e);
  24. }
  25. }
  26. // 结果解析
  27. private PageResult handleResponse(SearchResponse response) {
  28. // 4.解析响应
  29. SearchHits searchHits = response.getHits();
  30. // 4.1.获取总条数
  31. long total = searchHits.getTotalHits().value;
  32. // 4.2.文档数组
  33. SearchHit[] hits = searchHits.getHits();
  34. // 4.3.遍历
  35. List<HotelDoc> hotels = new ArrayList<>();
  36. for (SearchHit hit : hits) {
  37. // 获取文档source
  38. String json = hit.getSourceAsString();
  39. // 反序列化
  40. HotelDoc hotelDoc = JSON.parseObject(json, HotelDoc.class);
  41. // 放入集合
  42. hotels.add(hotelDoc);
  43. }
  44. // 4.4.封装返回
  45. return new PageResult(total, hotels);
  46. }

2.酒店结果过滤

需求:添加品牌、城市、星级、价格等过滤功能

2.1.需求分析

在页面搜索框下面,会有一些过滤项:

image.png

传递的参数如图:

image.png

包含的过滤条件有:

  • brand:品牌值
  • city:城市
  • minPrice~maxPrice:价格范围
  • starName:星级

我们需要做两件事情:

  • 修改请求参数的对象RequestParams,接收上述参数
  • 修改业务逻辑,在搜索条件之外,添加一些过滤条件

2.2.修改实体类

修改在cn.itcast.hotel.pojo包下的实体类RequestParams:

  1. @Data
  2. public class RequestParams {
  3. private String key;
  4. private Integer page;
  5. private Integer size;
  6. private String sortBy;
  7. // 下面是新增的过滤条件参数
  8. private String city;
  9. private String brand;
  10. private String starName;
  11. private Integer minPrice;
  12. private Integer maxPrice;
  13. }

2.3.修改搜索业务

在HotelService的search方法中,只有一个地方需要修改:requet.source().query( … )其中的查询条件。

在之前的业务中,只有match查询,根据关键字搜索,现在要添加条件过滤,包括:

  • 品牌过滤:是keyword类型,用term查询
  • 星级过滤:是keyword类型,用term查询
  • 价格过滤:是数值类型,用range查询
  • 城市过滤:是keyword类型,用term查询

多个查询条件组合,肯定是boolean查询来组合:

  • 关键字搜索放到must中,参与算分
  • 其它过滤条件放到filter中,不参与算分

因为条件构建的逻辑比较复杂,这里先封装为一个函数:

image.png

buildBasicQuery的代码如下:

  1. private void buildBasicQuery(RequestParams params, SearchRequest request) {
  2. // 1.构建BooleanQuery
  3. BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
  4. // 2.关键字搜索
  5. String key = params.getKey();
  6. if (key == null || "".equals(key)) {
  7. boolQuery.must(QueryBuilders.matchAllQuery());
  8. } else {
  9. boolQuery.must(QueryBuilders.matchQuery("all", key));
  10. }
  11. // 3.城市条件
  12. if (params.getCity() != null && !params.getCity().equals("")) {
  13. boolQuery.filter(QueryBuilders.termQuery("city", params.getCity()));
  14. }
  15. // 4.品牌条件
  16. if (params.getBrand() != null && !params.getBrand().equals("")) {
  17. boolQuery.filter(QueryBuilders.termQuery("brand", params.getBrand()));
  18. }
  19. // 5.星级条件
  20. if (params.getStarName() != null && !params.getStarName().equals("")) {
  21. boolQuery.filter(QueryBuilders.termQuery("starName", params.getStarName()));
  22. }
  23. // 6.价格
  24. if (params.getMinPrice() != null && params.getMaxPrice() != null) {
  25. boolQuery.filter(QueryBuilders
  26. .rangeQuery("price")
  27. .gte(params.getMinPrice())
  28. .lte(params.getMaxPrice())
  29. );
  30. }
  31. // 7.放入source
  32. request.source().query(boolQuery);
  33. }

3.我周边的酒店

需求:我附近的酒店

3.1.需求分析

在酒店列表页的右侧,有一个小地图,点击地图的定位按钮,地图会找到你所在的位置:

image.png

并且,在前端会发起查询请求,将你的坐标发送到服务端:

image.png

我们要做的事情就是基于这个location坐标,然后按照距离对周围酒店排序。实现思路如下:

  • 修改RequestParams参数,接收location字段
  • 修改search方法业务逻辑,如果location有值,添加根据geo_distance排序的功能

3.2.修改实体类

修改在cn.itcast.hotel.pojo包下的实体类RequestParams:

  1. package cn.itcast.hotel.pojo;
  2. import lombok.Data;
  3. @Data
  4. public class RequestParams {
  5. private String key;
  6. private Integer page;
  7. private Integer size;
  8. private String sortBy;
  9. private String city;
  10. private String brand;
  11. private String starName;
  12. private Integer minPrice;
  13. private Integer maxPrice;
  14. // 我当前的地理坐标
  15. private String location;
  16. }

3.3.距离排序API

我们以前学习过排序功能,包括两种:

  • 普通字段排序
  • 地理坐标排序

我们只讲了普通字段排序对应的java写法。地理坐标排序只学过DSL语法,如下:

  1. GET /indexName/_search
  2. {
  3. "query": {
  4. "match_all": {}
  5. },
  6. "sort": [
  7. {
  8. "price": "asc"
  9. },
  10. {
  11. "_geo_distance" : {
  12. "FIELD" : "纬度,经度",
  13. "order" : "asc",
  14. "unit" : "km"
  15. }
  16. }
  17. ]
  18. }

对应的java代码示例:

image.png

3.4.添加距离排序

cn.itcast.hotel.service.implHotelServicesearch方法中,添加一个排序功能:

image.png

完整代码:

  1. @Override
  2. public PageResult search(RequestParams params) {
  3. try {
  4. // 1.准备Request
  5. SearchRequest request = new SearchRequest("hotel");
  6. // 2.准备DSL
  7. // 2.1.query
  8. buildBasicQuery(params, request);
  9. // 2.2.分页
  10. int page = params.getPage();
  11. int size = params.getSize();
  12. request.source().from((page - 1) * size).size(size);
  13. // 2.3.排序
  14. String location = params.getLocation();
  15. if (location != null && !location.equals("")) {
  16. request.source().sort(SortBuilders
  17. .geoDistanceSort("location", new GeoPoint(location))
  18. .order(SortOrder.ASC)
  19. .unit(DistanceUnit.KILOMETERS)
  20. );
  21. }
  22. // 3.发送请求
  23. SearchResponse response = client.search(request, RequestOptions.DEFAULT);
  24. // 4.解析响应
  25. return handleResponse(response);
  26. } catch (IOException e) {
  27. throw new RuntimeException(e);
  28. }
  29. }

3.5.排序距离显示

重启服务后,测试我的酒店功能:

image.png

发现确实可以实现对我附近酒店的排序,不过并没有看到酒店到底距离我多远,这该怎么办?

排序完成后,页面还要获取我附近每个酒店的具体距离值,这个值在响应结果中是独立的:

image.png

因此,我们在结果解析阶段,除了解析source部分以外,还要得到sort部分,也就是排序的距离,然后放到响应结果中。

我们要做两件事:

  • 修改HotelDoc,添加排序距离字段,用于页面显示
  • 修改HotelService类中的handleResponse方法,添加对sort值的获取

1)修改HotelDoc类,添加距离字段

  1. package cn.itcast.hotel.pojo;
  2. import lombok.Data;
  3. import lombok.NoArgsConstructor;
  4. @Data
  5. @NoArgsConstructor
  6. public class HotelDoc {
  7. private Long id;
  8. private String name;
  9. private String address;
  10. private Integer price;
  11. private Integer score;
  12. private String brand;
  13. private String city;
  14. private String starName;
  15. private String business;
  16. private String location;
  17. private String pic;
  18. // 排序时的 距离值
  19. private Object distance;
  20. public HotelDoc(Hotel hotel) {
  21. this.id = hotel.getId();
  22. this.name = hotel.getName();
  23. this.address = hotel.getAddress();
  24. this.price = hotel.getPrice();
  25. this.score = hotel.getScore();
  26. this.brand = hotel.getBrand();
  27. this.city = hotel.getCity();
  28. this.starName = hotel.getStarName();
  29. this.business = hotel.getBusiness();
  30. this.location = hotel.getLatitude() + ", " + hotel.getLongitude();
  31. this.pic = hotel.getPic();
  32. }
  33. }

2)修改HotelService中的handleResponse方法

image.png

重启后测试,发现页面能成功显示距离了:

image.png

4.酒店竞价排名

需求:让指定的酒店在搜索结果中排名置顶

4.1.需求分析

要让指定酒店在搜索结果中排名置顶,效果如图:

image.png

页面会给指定的酒店添加广告标记。

那怎样才能让指定的酒店排名置顶呢?

我们之前学习过的function_score查询可以影响算分,算分高了,自然排名也就高了。而function_score包含3个要素:

  • 过滤条件:哪些文档要加分
  • 算分函数:如何计算function score
  • 加权方式:function score 与 query score如何运算

这里的需求是:让指定酒店排名靠前。因此我们需要给这些酒店添加一个标记,这样在过滤条件中就可以根据这个标记来判断,是否要提高算分

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

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

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

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

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

  1. 给HotelDoc类添加isAD字段,Boolean类型
  2. 挑选几个你喜欢的酒店,给它的文档数据添加isAD字段,值为true
  3. 修改search方法,添加function score功能,给isAD值为true的酒店增加权重

4.2.修改HotelDoc实体

cn.itcast.hotel.pojo包下的HotelDoc类添加isAD字段:

image.png

4.3.添加广告标记

接下来,我们挑几个酒店,添加isAD字段,设置为true:

  1. POST /hotel/_update/1902197537
  2. {
  3. "doc": {
  4. "isAD": true
  5. }
  6. }
  7. POST /hotel/_update/2056126831
  8. {
  9. "doc": {
  10. "isAD": true
  11. }
  12. }
  13. POST /hotel/_update/1989806195
  14. {
  15. "doc": {
  16. "isAD": true
  17. }
  18. }
  19. POST /hotel/_update/2056105938
  20. {
  21. "doc": {
  22. "isAD": true
  23. }
  24. }

4.4.添加算分函数查询

接下来我们就要修改查询条件了。之前是用的boolean 查询,现在要改成function_socre查询。

function_score查询结构如下:

image.png

对应的JavaAPI如下:

image.png

我们可以将之前写的boolean查询作为原始查询条件放到query中,接下来就是添加过滤条件算分函数加权模式了。所以原来的代码依然可以沿用。

修改cn.itcast.hotel.service.impl包下的HotelService类中的buildBasicQuery方法,添加算分函数查询:

  1. private void buildBasicQuery(RequestParams params, SearchRequest request) {
  2. // 1.构建BooleanQuery
  3. BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
  4. // 关键字搜索
  5. String key = params.getKey();
  6. if (key == null || "".equals(key)) {
  7. boolQuery.must(QueryBuilders.matchAllQuery());
  8. } else {
  9. boolQuery.must(QueryBuilders.matchQuery("all", key));
  10. }
  11. // 城市条件
  12. if (params.getCity() != null && !params.getCity().equals("")) {
  13. boolQuery.filter(QueryBuilders.termQuery("city", params.getCity()));
  14. }
  15. // 品牌条件
  16. if (params.getBrand() != null && !params.getBrand().equals("")) {
  17. boolQuery.filter(QueryBuilders.termQuery("brand", params.getBrand()));
  18. }
  19. // 星级条件
  20. if (params.getStarName() != null && !params.getStarName().equals("")) {
  21. boolQuery.filter(QueryBuilders.termQuery("starName", params.getStarName()));
  22. }
  23. // 价格
  24. if (params.getMinPrice() != null && params.getMaxPrice() != null) {
  25. boolQuery.filter(QueryBuilders
  26. .rangeQuery("price")
  27. .gte(params.getMinPrice())
  28. .lte(params.getMaxPrice())
  29. );
  30. }
  31. // 2.算分控制
  32. FunctionScoreQueryBuilder functionScoreQuery =
  33. QueryBuilders.functionScoreQuery(
  34. // 原始查询,相关性算分的查询
  35. boolQuery,
  36. // function score的数组
  37. new FunctionScoreQueryBuilder.FilterFunctionBuilder[]{
  38. // 其中的一个function score 元素
  39. new FunctionScoreQueryBuilder.FilterFunctionBuilder(
  40. // 过滤条件
  41. QueryBuilders.termQuery("isAD", true),
  42. // 算分函数
  43. ScoreFunctionBuilders.weightFactorFunction(10)
  44. )
  45. });
  46. request.source().query(functionScoreQuery);
  47. }

5.实现酒店搜索框自动补全

image.png

现在,我们的hotel索引库还没有设置拼音分词器,需要修改索引库中的配置。但是我们知道索引库是无法修改的,只能删除然后重新创建。

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

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

  1. 修改hotel索引库结构,设置自定义拼音分词器
  2. 修改索引库的name、all字段,使用自定义分词器
  3. 索引库添加一个新字段suggestion,类型为completion类型,使用自定义的分词器
  4. 给HotelDoc类添加suggestion字段,内容包含brand、business
  5. 重新导入数据到hotel库

5.1.修改酒店映射结构

代码如下:

  1. # 酒店数据索引库
  2. PUT /hotel
  3. {
  4. "settings": {
  5. "analysis": {
  6. "analyzer": {
  7. "text_anlyzer": {
  8. "tokenizer": "ik_max_word",
  9. "filter": "py"
  10. },
  11. "completion_analyzer": {
  12. "tokenizer": "keyword",
  13. "filter": "py"
  14. }
  15. },
  16. "filter": {
  17. "py": {
  18. "type": "pinyin",
  19. "keep_full_pinyin": false,
  20. "keep_joined_full_pinyin": true,
  21. "keep_original": true,
  22. "limit_first_letter_length": 16,
  23. "remove_duplicated_term": true,
  24. "none_chinese_pinyin_tokenize": false
  25. }
  26. }
  27. }
  28. },
  29. "mappings": {
  30. "properties": {
  31. "id":{
  32. "type": "keyword"
  33. },
  34. "name":{
  35. "type": "text",
  36. "analyzer": "text_anlyzer",
  37. "search_analyzer": "ik_smart",
  38. "copy_to": "all"
  39. },
  40. "address":{
  41. "type": "keyword",
  42. "index": false
  43. },
  44. "price":{
  45. "type": "integer"
  46. },
  47. "score":{
  48. "type": "integer"
  49. },
  50. "brand":{
  51. "type": "keyword",
  52. "copy_to": "all"
  53. },
  54. "city":{
  55. "type": "keyword"
  56. },
  57. "starName":{
  58. "type": "keyword"
  59. },
  60. "business":{
  61. "type": "keyword",
  62. "copy_to": "all"
  63. },
  64. "location":{
  65. "type": "geo_point"
  66. },
  67. "pic":{
  68. "type": "keyword",
  69. "index": false
  70. },
  71. "all":{
  72. "type": "text",
  73. "analyzer": "text_anlyzer",
  74. "search_analyzer": "ik_smart"
  75. },
  76. "suggestion":{
  77. "type": "completion",
  78. "analyzer": "completion_analyzer"
  79. }
  80. }
  81. }
  82. }

5.2.修改HotelDoc实体

HotelDoc中要添加一个字段,用来做自动补全,内容可以是酒店品牌、城市、商圈等信息。按照自动补全字段的要求,最好是这些字段的数组。

因此我们在HotelDoc中添加一个suggestion字段,类型为List<String>,然后将brand、city、business等信息放到里面。

代码如下:

  1. package cn.itcast.hotel.pojo;
  2. import lombok.Data;
  3. import lombok.NoArgsConstructor;
  4. import java.util.ArrayList;
  5. import java.util.Arrays;
  6. import java.util.Collections;
  7. import java.util.List;
  8. @Data
  9. @NoArgsConstructor
  10. public class HotelDoc {
  11. private Long id;
  12. private String name;
  13. private String address;
  14. private Integer price;
  15. private Integer score;
  16. private String brand;
  17. private String city;
  18. private String starName;
  19. private String business;
  20. private String location;
  21. private String pic;
  22. private Object distance;
  23. private Boolean isAD;
  24. private List<String> suggestion;
  25. public HotelDoc(Hotel hotel) {
  26. this.id = hotel.getId();
  27. this.name = hotel.getName();
  28. this.address = hotel.getAddress();
  29. this.price = hotel.getPrice();
  30. this.score = hotel.getScore();
  31. this.brand = hotel.getBrand();
  32. this.city = hotel.getCity();
  33. this.starName = hotel.getStarName();
  34. this.business = hotel.getBusiness();
  35. this.location = hotel.getLatitude() + ", " + hotel.getLongitude();
  36. this.pic = hotel.getPic();
  37. // 组装suggestion
  38. if(this.business.contains("/")){
  39. // business有多个值,需要切割
  40. String[] arr = this.business.split("/");
  41. // 添加元素
  42. this.suggestion = new ArrayList<>();
  43. this.suggestion.add(this.brand);
  44. Collections.addAll(this.suggestion, arr);
  45. }else {
  46. this.suggestion = Arrays.asList(this.brand, this.business);
  47. }
  48. }
  49. }

5.3.重新导入

重新执行之前编写的导入数据功能,可以看到新的酒店数据中包含了suggestion:

image.png

5.4.自动补全查询的JavaAPI

之前我们学习了自动补全查询的DSL,而没有学习对应的JavaAPI,这里给出一个示例:

image.png

而自动补全的结果也比较特殊,解析的代码如下:

image.png

  1. package cn.itcast.hotel;
  2. import cn.itcast.hotel.service.IHotelService;
  3. import org.apache.http.HttpHost;
  4. import org.elasticsearch.action.search.SearchRequest;
  5. import org.elasticsearch.action.search.SearchResponse;
  6. import org.elasticsearch.client.RequestOptions;
  7. import org.elasticsearch.client.RestClient;
  8. import org.elasticsearch.client.RestHighLevelClient;
  9. import org.elasticsearch.index.query.QueryBuilders;
  10. import org.elasticsearch.search.aggregations.AggregationBuilders;
  11. import org.elasticsearch.search.aggregations.Aggregations;
  12. import org.elasticsearch.search.aggregations.bucket.terms.Terms;
  13. import org.elasticsearch.search.builder.SearchSourceBuilder;
  14. import org.elasticsearch.search.suggest.Suggest;
  15. import org.elasticsearch.search.suggest.SuggestBuilder;
  16. import org.elasticsearch.search.suggest.SuggestBuilders;
  17. import org.elasticsearch.search.suggest.completion.CompletionSuggestion;
  18. import org.junit.jupiter.api.AfterEach;
  19. import org.junit.jupiter.api.BeforeEach;
  20. import org.junit.jupiter.api.Test;
  21. import org.springframework.beans.factory.annotation.Autowired;
  22. import org.springframework.boot.test.context.SpringBootTest;
  23. import java.io.IOException;
  24. import java.util.List;
  25. @SpringBootTest
  26. public class Hotel06SuggestionTests {
  27. private RestHighLevelClient client;
  28. @Autowired
  29. private IHotelService hotelService;
  30. @Test
  31. public void test01() throws IOException {
  32. //1.创建请求语义对象
  33. SearchRequest request = new SearchRequest("hotel");
  34. //2.自动补全
  35. request.source().suggest(
  36. new SuggestBuilder().addSuggestion(
  37. "mySuggestion",
  38. SuggestBuilders.completionSuggestion("suggestion")
  39. .prefix("s").skipDuplicates(true).size(10))
  40. );
  41. //3.发送请求
  42. SearchResponse response = client.search(request, RequestOptions.DEFAULT);
  43. //4.解析结果
  44. Suggest suggest = response.getSuggest();
  45. CompletionSuggestion suggestion = suggest.getSuggestion("mySuggestion");
  46. // 遍历自动补全结果
  47. for (CompletionSuggestion.Entry.Option option : suggestion.getOptions()) {
  48. System.out.println(option.getText().string());
  49. }
  50. }
  51. /**
  52. * 在单元测试前执行
  53. */
  54. @BeforeEach
  55. public void init(){
  56. client = new RestHighLevelClient(
  57. RestClient.builder(
  58. //new HttpHost("localhost", 9200, "http"),
  59. new HttpHost("192.168.163.140", 9200, "http")));
  60. // RestHighLevelClient client = new RestHighLevelClient(RestClient.builder(
  61. // HttpHost.create("http://192.168.163.140:9200")
  62. // ));
  63. System.out.println(client);
  64. }
  65. /**
  66. * 在单元测试执行后执行
  67. * @throws IOException
  68. */
  69. @AfterEach
  70. public void destory() throws IOException {
  71. client.close();
  72. }
  73. }

5.5.实现搜索框自动补全

查看前端页面,可以发现当我们在输入框键入时,前端会发起ajax请求:

image.png

返回值是补全词条的集合,类型为List<String>

1)在cn.itcast.hotel.web包下的HotelController中添加新接口,接收新的请求:

  1. @GetMapping("suggestion")
  2. public List<String> getSuggestions(@RequestParam("key") String prefix) {
  3. return hotelService.getSuggestions(prefix);
  4. }

2)在cn.itcast.hotel.service包下的IhotelService中添加方法:

  1. List<String> getSuggestions(String prefix);

3)在cn.itcast.hotel.service.impl.HotelService中实现该方法:

  1. @Override
  2. public List<String> getSuggestions(String prefix) {
  3. try {
  4. // 1.准备Request
  5. SearchRequest request = new SearchRequest("hotel");
  6. // 2.准备DSL
  7. request.source().suggest(new SuggestBuilder().addSuggestion(
  8. "suggestions",
  9. SuggestBuilders.completionSuggestion("suggestion")
  10. .prefix(prefix)
  11. .skipDuplicates(true)
  12. .size(10)
  13. ));
  14. // 3.发起请求
  15. SearchResponse response = client.search(request, RequestOptions.DEFAULT);
  16. // 4.解析结果
  17. Suggest suggest = response.getSuggest();
  18. // 4.1.根据补全查询名称,获取补全结果
  19. CompletionSuggestion suggestions = suggest.getSuggestion("suggestions");
  20. // 4.2.获取options
  21. List<CompletionSuggestion.Entry.Option> options = suggestions.getOptions();
  22. // 4.3.遍历
  23. List<String> list = new ArrayList<>(options.size());
  24. for (CompletionSuggestion.Entry.Option option : options) {
  25. String text = option.getText().toString();
  26. list.add(text);
  27. }
  28. return list;
  29. } catch (IOException e) {
  30. throw new RuntimeException(e);
  31. }
  32. }

6.对搜索结果聚合(过滤词条)

6.1.业务需求

需求:搜索页面的品牌、城市等信息不应该是在页面写死,而是通过聚合索引库中的酒店数据得来的:

image.png

分析:

目前,页面的城市列表、星级列表、品牌列表都是写死的,并不会随着搜索结果的变化而变化。但是用户搜索条件改变时,搜索结果会跟着变化。

例如:用户搜索“东方明珠”,那搜索的酒店肯定是在上海东方明珠附近,因此,城市只能是上海,此时城市列表中就不应该显示北京、深圳、杭州这些信息了。

也就是说,搜索结果中包含哪些城市,页面就应该列出哪些城市;搜索结果中包含哪些品牌,页面就应该列出哪些品牌。

如何得知搜索结果中包含哪些品牌?如何得知搜索结果中包含哪些城市?

使用聚合功能,利用Bucket聚合,对搜索结果中的文档基于品牌分组、基于城市分组,就能得知包含哪些品牌、哪些城市了。

因为是对搜索结果聚合,因此聚合是限定范围的聚合,也就是说聚合的限定条件跟搜索文档的条件一致。

查看浏览器可以发现,前端其实已经发出了这样的一个请求:

image.png

请求参数与搜索文档的参数完全一致

返回值类型就是页面要展示的最终结果:

image.png

结果是一个Map结构:

  • key是字符串,城市、星级、品牌、价格
  • value是集合,例如多个城市的名称

6.2.业务实现

cn.itcast.hotel.web包的HotelController中添加一个方法,遵循下面的要求:

  • 请求方式:POST
  • 请求路径:/hotel/filters
  • 请求参数:RequestParams,与搜索文档的参数一致
  • 返回值类型:Map<String, List<String>>

代码:

  1. @PostMapping("filters")
  2. public Map<String, List<String>> getFilters(@RequestBody RequestParams params){
  3. return hotelService.getFilters(params);
  4. }

这里调用了IHotelService中的getFilters方法,尚未实现。

cn.itcast.hotel.service.IHotelService中定义新方法:

  1. Map<String, List<String>> filters(RequestParams params);

cn.itcast.hotel.service.impl.HotelService中实现该方法:

  1. @Override
  2. public Map<String, List<String>> filters(RequestParams params) {
  3. try {
  4. // 1.准备Request
  5. SearchRequest request = new SearchRequest("hotel");
  6. // 2.准备DSL
  7. // 2.1.query
  8. buildBasicQuery(params, request);
  9. // 2.2.设置size
  10. request.source().size(0);
  11. // 2.3.聚合
  12. buildAggregation(request);
  13. // 3.发出请求
  14. SearchResponse response = client.search(request, RequestOptions.DEFAULT);
  15. // 4.解析结果
  16. Map<String, List<String>> result = new HashMap<>();
  17. Aggregations aggregations = response.getAggregations();
  18. // 4.1.根据品牌名称,获取品牌结果
  19. List<String> brandList = getAggByName(aggregations, "brandAgg");
  20. result.put("brand", brandList);
  21. // 4.2.根据品牌名称,获取品牌结果
  22. List<String> cityList = getAggByName(aggregations, "cityAgg");
  23. result.put("city", cityList);
  24. // 4.3.根据品牌名称,获取品牌结果
  25. List<String> starList = getAggByName(aggregations, "starAgg");
  26. result.put("starName", starList);
  27. return result;
  28. } catch (IOException e) {
  29. throw new RuntimeException(e);
  30. }
  31. }
  32. private void buildAggregation(SearchRequest request) {
  33. request.source().aggregation(AggregationBuilders
  34. .terms("brandAgg")
  35. .field("brand")
  36. .size(100)
  37. );
  38. request.source().aggregation(AggregationBuilders
  39. .terms("cityAgg")
  40. .field("city")
  41. .size(100)
  42. );
  43. request.source().aggregation(AggregationBuilders
  44. .terms("starAgg")
  45. .field("starName")
  46. .size(100)
  47. );
  48. }
  49. private List<String> getAggByName(Aggregations aggregations, String aggName) {
  50. // 4.1.根据聚合名称获取聚合结果
  51. Terms brandTerms = aggregations.get(aggName);
  52. // 4.2.获取buckets
  53. List<? extends Terms.Bucket> buckets = brandTerms.getBuckets();
  54. // 4.3.遍历
  55. List<String> brandList = new ArrayList<>();
  56. for (Terms.Bucket bucket : buckets) {
  57. // 4.4.获取key
  58. String key = bucket.getKeyAsString();
  59. brandList.add(key);
  60. }
  61. return brandList;
  62. }

7.实现数据同步(异步通知MQ)

7.1.思路

利用课前资料提供的hotel-admin项目作为酒店管理的微服务。当酒店数据发生增、删、改时,要求对elasticsearch中数据也要完成相同操作。

步骤:

  • 导入课前资料提供的hotel-admin项目,启动并测试酒店数据的CRUD
  • 声明exchange、queue、RoutingKey
  • 在hotel-admin中的增、删、改业务中完成消息发送
  • 在hotel-demo中完成消息监听,并更新elasticsearch中数据
  • 启动并测试数据同步功能

7.2.导入demo

导入课前资料提供的hotel-admin项目:

image.png

运行后,访问 http://localhost:8099

image.png

其中包含了酒店的CRUD功能:

image.png

7.3.声明交换机、队列

MQ结构如图:

image.png

1)引入依赖

在hotel-admin、hotel-demo中引入rabbitmq的依赖:

  1. <!--amqp-->
  2. <dependency>
  3. <groupId>org.springframework.boot</groupId>
  4. <artifactId>spring-boot-starter-amqp</artifactId>
  5. </dependency>
  1. spring:
  2. rabbitmq:
  3. host: 192.168.163.130
  4. port: 5672
  5. virtual-host: /
  6. username: itcast
  7. password: 123321

2)声明队列交换机名称

在hotel-admin和hotel-demo中的cn.itcast.hotel.constatnts包下新建一个类MqConstants

  1. package cn.itcast.hotel.constants;
  2. public class MqConstants {
  3. /**
  4. * 交换机
  5. */
  6. public final static String HOTEL_EXCHANGE = "hotel.topic";
  7. /**
  8. * 监听新增和修改的队列
  9. */
  10. public final static String HOTEL_INSERT_QUEUE = "hotel.insert.queue";
  11. /**
  12. * 监听删除的队列
  13. */
  14. public final static String HOTEL_DELETE_QUEUE = "hotel.delete.queue";
  15. /**
  16. * 新增或修改的RoutingKey
  17. */
  18. public final static String HOTEL_INSERT_KEY = "hotel.insert";
  19. /**
  20. * 删除的RoutingKey
  21. */
  22. public final static String HOTEL_DELETE_KEY = "hotel.delete";
  23. }

3)声明队列交换机

在hotel-demo中,定义配置类,声明队列、交换机:

  1. package cn.itcast.hotel.config;
  2. import cn.itcast.hotel.constants.MqConstants;
  3. import org.springframework.amqp.core.Binding;
  4. import org.springframework.amqp.core.BindingBuilder;
  5. import org.springframework.amqp.core.Queue;
  6. import org.springframework.amqp.core.TopicExchange;
  7. import org.springframework.context.annotation.Bean;
  8. import org.springframework.context.annotation.Configuration;
  9. @Configuration
  10. public class MqConfig {
  11. @Bean
  12. public TopicExchange topicExchange(){
  13. return new TopicExchange(MqConstants.HOTEL_EXCHANGE, true, false);
  14. }
  15. @Bean
  16. public Queue insertQueue(){
  17. return new Queue(MqConstants.HOTEL_INSERT_QUEUE, true);
  18. }
  19. @Bean
  20. public Queue deleteQueue(){
  21. return new Queue(MqConstants.HOTEL_DELETE_QUEUE, true);
  22. }
  23. @Bean
  24. public Binding insertQueueBinding(){
  25. return BindingBuilder.bind(insertQueue()).to(topicExchange()).with(MqConstants.HOTEL_INSERT_KEY);
  26. }
  27. @Bean
  28. public Binding deleteQueueBinding(){
  29. return BindingBuilder.bind(deleteQueue()).to(topicExchange()).with(MqConstants.HOTEL_DELETE_KEY);
  30. }
  31. }

7.4.发送MQ消息

在hotel-admin中的增、删、改业务中分别发送MQ消息:

image.png

  1. @PostMapping
  2. public void saveHotel(@RequestBody Hotel hotel){
  3. hotelService.save(hotel);
  4. rabbitTemplate.convertAndSend(MqConstants.HOTEL_EXCHANGE,MqConstants.HOTEL_INSERT_KEY,hotel.getId());
  5. }
  6. @PutMapping()
  7. public void updateById(@RequestBody Hotel hotel){
  8. if (hotel.getId() == null) {
  9. throw new InvalidParameterException("id不能为空");
  10. }
  11. hotelService.updateById(hotel);
  12. rabbitTemplate.convertAndSend(MqConstants.HOTEL_EXCHANGE,MqConstants.HOTEL_INSERT_KEY,hotel.getId());
  13. }
  14. @DeleteMapping("/{id}")
  15. public void deleteById(@PathVariable("id") Long id) {
  16. hotelService.removeById(id);
  17. rabbitTemplate.convertAndSend(MqConstants.HOTEL_EXCHANGE,MqConstants.HOTEL_DELETE_KEY,id);
  18. }

7.5.接收MQ消息

hotel-demo接收到MQ消息要做的事情包括:

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

1)首先在hotel-demo的cn.itcast.hotel.service包下的IHotelService中新增新增、删除业务

  1. void deleteById(Long id);
  2. void insertById(Long id);

2)给hotel-demo中的cn.itcast.hotel.service.impl包下的HotelService中实现业务:

  1. @Override
  2. public void deleteById(Long id) {
  3. try {
  4. // 1.准备Request
  5. DeleteRequest request = new DeleteRequest("hotel", id.toString());
  6. // 2.发送请求
  7. client.delete(request, RequestOptions.DEFAULT);
  8. } catch (IOException e) {
  9. throw new RuntimeException(e);
  10. }
  11. }
  12. @Override
  13. public void insertById(Long id) {
  14. try {
  15. // 0.根据id查询酒店数据
  16. Hotel hotel = getById(id);
  17. // 转换为文档类型
  18. HotelDoc hotelDoc = new HotelDoc(hotel);
  19. // 1.准备Request对象
  20. IndexRequest request = new IndexRequest("hotel").id(hotel.getId().toString());
  21. // 2.准备Json文档
  22. request.source(JSON.toJSONString(hotelDoc), XContentType.JSON);
  23. // 3.发送请求
  24. client.index(request, RequestOptions.DEFAULT);
  25. } catch (IOException e) {
  26. throw new RuntimeException(e);
  27. }
  28. }

3)编写监听器

在hotel-demo中的cn.itcast.hotel.mq包新增一个类:

  1. package cn.itcast.hotel.mq;
  2. import cn.itcast.hotel.constants.MqConstants;
  3. import cn.itcast.hotel.service.IHotelService;
  4. import org.springframework.amqp.rabbit.annotation.RabbitListener;
  5. import org.springframework.beans.factory.annotation.Autowired;
  6. import org.springframework.stereotype.Component;
  7. @Component
  8. public class HotelListener {
  9. @Autowired
  10. private IHotelService hotelService;
  11. /**
  12. * 监听酒店新增或修改的业务
  13. * @param id 酒店id
  14. */
  15. @RabbitListener(queues = MqConstants.HOTEL_INSERT_QUEUE)
  16. public void listenHotelInsertOrUpdate(Long id){
  17. hotelService.insertById(id);
  18. }
  19. /**
  20. * 监听酒店删除的业务
  21. * @param id 酒店id
  22. */
  23. @RabbitListener(queues = MqConstants.HOTEL_DELETE_QUEUE)
  24. public void listenHotelDelete(Long id){
  25. hotelService.deleteById(id);
  26. }
  27. }