HM-SpringCloud微服务系列6.4【黑马旅游案例】

image
image


1 酒店搜索和分页

1.1 课件

image
image
image
image

1.2 需求分析

  1. 在项目的首页,有一个大大的搜索框,还有分页按钮
    点击搜索按钮,可以看到浏览器控制台发出了请求:
    image
    请求参数如下:
    image
  2. 由此可以知道,我们这个请求的信息如下:
    • 请求方式:POST
    • 请求路径:/hotel/list
    • 请求参数:JSON对象,包含4个字段:
      • key:搜索关键字
      • page:页码
      • size:每页大小
      • sortBy:排序,目前暂不实现
    • 返回值:分页查询,需要返回分页结果PageResult,包含两个属性:
      • total:总条数
      • List<HotelDoc>:当前页的数据
  3. 因此,我们实现业务的流程如下:
    • 步骤一:定义实体类,接收请求参数的JSON对象
    • 步骤二:编写controller,接收页面的请求
    • 步骤三:编写业务实现,利用RestHighLevelClient实现搜索、分页

1.3 定义实体类

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

1.3.1 请求参数

  1. 前端请求的json结构如下:
    {
    	"key": "搜索关键字",
    	"page": 1,
    	"size": 3,
    	"sortBy": "default"
    }
    
  2. 因此,我们在com.yppah.hoteldemo.pojo包下定义一个实体类:
    package com.yppah.hoteldemo.pojo;
    
    import lombok.Data;
    
    @Data
    public class RequestParams {
    	private String key;
    	private Integer page;
    	private Integer size;
    	private String sortBy;
    }
    
    image

1.3.2 返回值

  1. 分页查询,需要返回分页结果PageResult,包含两个属性:
    • total:总条数
    • List<HotelDoc>:当前页的数据
  2. 因此,我们在com.yppah.hoteldemo.pojo中定义返回结果:
    package com.yppah.hoteldemo.pojo;
    
    import lombok.Data;
    
    import java.util.List;
    
    @Data
    public class PageResult {
    	private Long total;
    	private List<HotelDoc> hotels;
    }
    
    image

1.4 定义Controller

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

    • 请求方式:Post
    • 请求路径:/hotel/list
    • 请求参数:对象,类型为RequestParam
    • 返回值:PageResult,包含两个属性
      • Long total:总条数
      • List<HotelDoc> hotels:酒店数据
  2. 因此,我们在com.yppah.hoteldemo.web中定义HotelController:

    点击查看代码
    package com.yppah.hoteldemo.web;
    
    import com.yppah.hoteldemo.pojo.PageResult;
    import com.yppah.hoteldemo.pojo.RequestParams;
    import com.yppah.hoteldemo.service.IHotelService;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.web.bind.annotation.PostMapping;
    import org.springframework.web.bind.annotation.RequestBody;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RestController;
    
    @RestController
    @RequestMapping("/hotel")
    public class HotelController {
    
    	@Autowired
    	private IHotelService hotelService;
    
    	@PostMapping("/list")
    	public PageResult search(@RequestBody RequestParams params) {
    		return hotelService.search(params);
    	}
    }
    

1.5 实现搜索业务

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

  2. com.yppah.hoteldemo.service中的IHotelService接口中定义一个方法:

    点击查看代码
    package com.yppah.hoteldemo.service;
    
    import com.yppah.hoteldemo.pojo.Hotel;
    import com.baomidou.mybatisplus.extension.service.IService;
    import com.yppah.hoteldemo.pojo.PageResult;
    import com.yppah.hoteldemo.pojo.RequestParams;
    
    public interface IHotelService extends IService<Hotel> {
    
    	/**
    	 * 根据关键字搜索酒店信息
    	 * @param params 请求参数对象,包含用户输入的关键字
    	 * @return 酒店文档列表
    	 */
    	PageResult search(RequestParams params);
    }
    
  3. com.yppah.hoteldemo.service.impl中的HotelService中实现search方法:

    点击查看代码
    package com.yppah.hoteldemo.service.impl;
    
    import com.alibaba.fastjson.JSON;
    import com.yppah.hoteldemo.mapper.HotelMapper;
    import com.yppah.hoteldemo.pojo.Hotel;
    import com.yppah.hoteldemo.pojo.HotelDoc;
    import com.yppah.hoteldemo.pojo.PageResult;
    import com.yppah.hoteldemo.pojo.RequestParams;
    import com.yppah.hoteldemo.service.IHotelService;
    import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
    import org.elasticsearch.action.search.SearchRequest;
    import org.elasticsearch.action.search.SearchResponse;
    import org.elasticsearch.client.RequestOptions;
    import org.elasticsearch.client.RestHighLevelClient;
    import org.elasticsearch.index.query.QueryBuilders;
    import org.elasticsearch.search.SearchHit;
    import org.elasticsearch.search.SearchHits;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Service;
    
    import java.io.IOException;
    import java.util.ArrayList;
    import java.util.List;
    
    @Service
    public class HotelService extends ServiceImpl<HotelMapper, Hotel> implements IHotelService {
    
    	@Autowired
    	private RestHighLevelClient client; // 使用之前需要在项目启动类HoteldemoApplication中用@Bean将其注入到spring中
    
    	@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. 发送Request,得到Response
    			SearchResponse response = client.search(request, RequestOptions.DEFAULT);
    			// 4. 解析Response
    			return handleResponse(response);
    		} catch (IOException e) {
    			throw new RuntimeException(e);
    		}
    	}
    
    	private PageResult handleResponse(SearchResponse response) { // 需要在PageResult中添加构造函数两个
    		//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<>(); //用于存放PageResult所要求格式的数据
    		for (SearchHit hit : hits) {
    			//4.3.1 获取文档source
    			String json = hit.getSourceAsString();
    			//4.3.2 反序列化解析json
    			HotelDoc hotelDoc = JSON.parseObject(json, HotelDoc.class);
    			hotels.add(hotelDoc);
    		}
    		//4.4 封装返回
    		return new PageResult(total, hotels);
    	}
    }
    
  4. 实现搜索业务,肯定离不开RestHighLevelClient,我们需要把它注册到Spring中作为一个Bean。在com.yppah.hoteldemo中的HotelDemoApplication中声明这个Bean:

    	@Bean
    	public RestHighLevelClient client() {
    		return new RestHighLevelClient(RestClient.builder(
    				HttpHost.create("http://10.193.193.141:9200")
    		));
    	}
    

    image
    image

1.6 测试

image
image
image

2 酒店结果过滤

2.1 课件

image
image
image

2.2 需求分析

  1. 在页面搜索框下面,会有一些过滤项:
    image
  2. 传递的参数如图:
    image
  3. 包含的过滤条件有:
    • brand:品牌值
    • city:城市
    • minPrice~maxPrice:价格范围
    • starName:星级
  4. 我们需要做两件事情:
    • 修改请求参数的对象RequestParams,接收上述参数
    • 修改业务逻辑,在搜索条件之外,添加一些过滤条件

2.3 修改实体类RequestParams

@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;
}

image

2.4 修改搜索业务

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

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

    • 品牌过滤:是keyword类型,用term查询
    • 星级过滤:是keyword类型,用term查询
    • 价格过滤:是数值类型,用range查询
    • 城市过滤:是keyword类型,用term查询
  3. 多个查询条件组合,肯定是boolean查询来组合:

    • 关键字搜索放到must中,参与算分
    • 其它过滤条件放到filter中,不参与算分
  4. 因为条件构建的逻辑比较复杂,这里先封装为一个函数:
    image
    image

    点击查看代码
    	private void buildBasicQuery(RequestParams params, SearchRequest request) {
    		// 1. 构建booleanQuery
    		BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
    		// 2. 关键字搜索
    		String key = params.getKey();
    		if (key==null || "".equals(key)) {
    			request.source().query(QueryBuilders.matchAllQuery());
    		} else {
    			request.source().query(QueryBuilders.matchQuery("all", key));
    		}
    		// 3. 条件过滤
    		// 3.1 城市
    		if (params.getCity()!=null && !params.getCity().equals("")) {
    			boolQuery.filter(QueryBuilders.termQuery("city", params.getCity()));
    		}
    		// 3.2 品牌
    		if (params.getBrand()!=null && !params.getBrand().equals("")) {
    			boolQuery.filter(QueryBuilders.termQuery("brand", params.getBrand()));
    		}
    		// 3.3 星级
    		if (params.getStarName()!=null && !params.getStarName().equals("")) {
    			boolQuery.filter(QueryBuilders.termQuery("starName", params.getStarName()));
    		}
    		// 3.4 价格
    		if (params.getMinPrice()!=null && params.getMaxPrice()!=null) {
    			boolQuery.filter(QueryBuilders.rangeQuery("price")
    					.gte(params.getMinPrice())
    					.lte(params.getMaxPrice())
    			);
    		}
    		// 4. 放入resource
    		request.source().query(boolQuery);
    	}
    

2.5 测试

image
image
image
image
image

3 我周边的酒店

3.1 课件

image
image
image

3.2 需求分析

  1. 在酒店列表页的右侧,有一个小地图,点击地图的定位按钮,地图会找到你所在的位置:
    image
  2. 并且,在前端会发起查询请求,将你的坐标发送到服务端:
    image
  3. 我们要做的事情就是基于这个location坐标,然后按照距离对周围酒店排序。实现思路如下:
    • 修改RequestParams参数,接收location字段
    • 修改search方法业务逻辑,如果location有值,添加根据geo_distance排序的功能

3.3 修改实体类

image

3.4 距离排序API

image

3.5 添加距离排序

image

点击查看代码
    @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. 发送Request,得到Response
            SearchResponse response = client.search(request, RequestOptions.DEFAULT);
            // 4. 解析Response
            return handleResponse(response);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

image

3.6 距离排序显示

image
发现确实可以实现对我附近酒店的排序,不过并没有看到酒店到底距离我多远,这该怎么办?
排序完成后,页面还要获取我附近每个酒店的具体距离值,这个值在响应结果中是独立的:
image
因此,我们在结果解析阶段,除了解析source部分以外,还要得到sort部分,也就是排序的距离,然后放到响应结果中。
我们要做两件事:

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

3.6.1 修改HotelDoc类,添加距离字段

image

点击查看代码
package com.yppah.hoteldemo.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();
    }

    /*public HotelDoc() {
    }*/
}

3.6.2 修改HotelService中的handleResponse方法

image

点击查看代码
    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<>(); //用于存放PageResult所要求格式的数据
        for (SearchHit hit : hits) {
            //4.3.1 获取文档source
            String json = hit.getSourceAsString();
            //4.3.2 反序列化解析json
            HotelDoc hotelDoc = JSON.parseObject(json, HotelDoc.class);

            // 我附近的酒店功能-获取排序值
            Object[] sortValues = hit.getSortValues();
            if (sortValues.length > 0) {
                Object sortValue = sortValues[0];
                hotelDoc.setDistance(sortValue);
            }

            hotels.add(hotelDoc);
        }
        //4.4 封装返回
        return new PageResult(total, hotels);
    }

3.6.2 重启服务测试

image

4 酒店竞价排名

4.1 课件

image
image

4.2 需求分析

  1. 要让指定酒店在搜索结果中排名置顶,效果如图:
    image
  2. 页面会给指定的酒店添加广告标记。那怎样才能让指定的酒店排名置顶呢?
  3. 我们之前学习过的function_score查询可以影响算分,算分高了,自然排名也就高了。而function_score包含3个要素:
    • 过滤条件:哪些文档要加分
    • 算分函数:如何计算function score
    • 加权方式:function score 与 query score如何运算
  4. 这里的需求是:让指定酒店排名靠前。因此我们需要给这些酒店添加一个标记,这样在过滤条件中就可以根据这个标记来判断,是否要提高算分
  5. 比如,我们给酒店添加一个字段:isAD,Boolean类型:
    • true:是广告
    • false:不是广告
  6. 这样function_score包含3个要素就很好确定了:
    • 过滤条件:判断isAD 是否为true
    • 算分函数:我们可以用最简单暴力的weight,固定加权值
    • 加权方式:可以用默认的相乘,大大提高算分
  7. 因此,业务的实现步骤包括:
    1. 给HotelDoc类添加isAD字段,Boolean类型
    2. 挑选几个你喜欢的酒店,给它的文档数据添加isAD字段,值为true
    3. 修改search方法,添加function score功能,给isAD值为true的酒店增加权重

4.3 修改HotelDoc实体类

image

4.4 添加广告标记

挑几个酒店,利用ES控制台手动添加isAD字段,设置为true:

# 添加isAD字段
POST /hotel/_update/609372
{
    "doc": {
        "isAD": true
    }
}
POST /hotel/_update/5873072
{
    "doc": {
        "isAD": true
    }
}
POST /hotel/_update/2056298828
{
    "doc": {
        "isAD": true
    }
}
POST /hotel/_update/2062643512
{
    "doc": {
        "isAD": true
    }
}

image

4.5 添加算分函数查询

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

  2. function_score查询结构如下:
    image

  3. 对应的JavaAPI如下:
    image

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

  5. 修改com.yppah.hoteldemo.service.impl包下的HotelService类中的buildBasicQuery方法,添加算分函数查询:
    image

    点击查看代码
    	private void buildBasicQuery(RequestParams params, SearchRequest request) {
    		// 1. 构建booleanQuery
    		BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
    		// 2. 关键字搜索
    		String key = params.getKey();
    		if (key==null || "".equals(key)) {
    			request.source().query(QueryBuilders.matchAllQuery());
    		} else {
    			request.source().query(QueryBuilders.matchQuery("all", key));
    		}
    		// 3. 条件过滤
    		// 3.1 城市
    		if (params.getCity()!=null && !params.getCity().equals("")) {
    			boolQuery.filter(QueryBuilders.termQuery("city", params.getCity()));
    		}
    		// 3.2 品牌
    		if (params.getBrand()!=null && !params.getBrand().equals("")) {
    			boolQuery.filter(QueryBuilders.termQuery("brand", params.getBrand()));
    		}
    		// 3.3 星级
    		if (params.getStarName()!=null && !params.getStarName().equals("")) {
    			boolQuery.filter(QueryBuilders.termQuery("starName", params.getStarName()));
    		}
    		// 3.4 价格
    		if (params.getMinPrice()!=null && params.getMaxPrice()!=null) {
    			boolQuery.filter(QueryBuilders.rangeQuery("price")
    					.gte(params.getMinPrice())
    					.lte(params.getMaxPrice())
    			);
    		}
    
    		// 酒店竞价排名功能-算分控制
    		FunctionScoreQueryBuilder functionScoreQuery =
    				QueryBuilders.functionScoreQuery(
    						// 原始查询,相关性算分的查询
    						boolQuery,
    						// function score的数组
    						new FunctionScoreQueryBuilder.FilterFunctionBuilder[]{
    							// 其中的一个function score 元素
    							new FunctionScoreQueryBuilder.FilterFunctionBuilder(
    									// 过滤条件
    									QueryBuilders.termQuery("isAD", true),
    									// 算分函数
    									ScoreFunctionBuilders.weightFactorFunction(10) //*10
    							)
    						});
    
    		// 4. 放入resource
    //        request.source().query(boolQuery);
    		request.source().query(functionScoreQuery);
    	}
    

4.6 测试

注意:要把浏览器中的广告拦截插件暂时关掉
image
有点问题广告标识未显示QAQ

4.7 扩展思考

image

posted @   yub4by  阅读(111)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
点击右上角即可分享
微信分享提示