HM-SpringCloud微服务系列6.4【黑马旅游案例】
1 酒店搜索和分页
1.1 课件
1.2 需求分析
- 在项目的首页,有一个大大的搜索框,还有分页按钮
点击搜索按钮,可以看到浏览器控制台发出了请求:
请求参数如下:
- 由此可以知道,我们这个请求的信息如下:
- 请求方式:POST
- 请求路径:/hotel/list
- 请求参数:JSON对象,包含4个字段:
- key:搜索关键字
- page:页码
- size:每页大小
- sortBy:排序,目前暂不实现
- 返回值:分页查询,需要返回分页结果PageResult,包含两个属性:
total
:总条数List<HotelDoc>
:当前页的数据
- 因此,我们实现业务的流程如下:
- 步骤一:定义实体类,接收请求参数的JSON对象
- 步骤二:编写controller,接收页面的请求
- 步骤三:编写业务实现,利用RestHighLevelClient实现搜索、分页
1.3 定义实体类
实体类有两个,一个是前端的请求参数实体,一个是服务端应该返回的响应结果实体。
1.3.1 请求参数
- 前端请求的json结构如下:
{ "key": "搜索关键字", "page": 1, "size": 3, "sortBy": "default" }
- 因此,我们在
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; }
1.3.2 返回值
- 分页查询,需要返回分页结果PageResult,包含两个属性:
total
:总条数List<HotelDoc>
:当前页的数据
- 因此,我们在
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; }
1.4 定义Controller
-
定义一个HotelController,声明查询接口,满足下列要求:
- 请求方式:Post
- 请求路径:/hotel/list
- 请求参数:对象,类型为RequestParam
- 返回值:PageResult,包含两个属性
Long total
:总条数List<HotelDoc> hotels
:酒店数据
-
因此,我们在
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 实现搜索业务
-
我们在controller调用了IHotelService,并没有实现该方法,因此下面我们就在IHotelService中定义方法,并且去实现业务逻辑。
-
在
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); }
-
在
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); } }
-
实现搜索业务,肯定离不开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") )); }
1.6 测试
2 酒店结果过滤
2.1 课件
2.2 需求分析
- 在页面搜索框下面,会有一些过滤项:
- 传递的参数如图:
- 包含的过滤条件有:
- brand:品牌值
- city:城市
- minPrice~maxPrice:价格范围
- starName:星级
- 我们需要做两件事情:
- 修改请求参数的对象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;
}
2.4 修改搜索业务
-
在HotelService的search方法中,只有一个地方需要修改:requet.source().query( ... )其中的查询条件。
-
在之前的业务中,只有match查询,根据关键字搜索,现在要添加条件过滤,包括:
- 品牌过滤:是keyword类型,用term查询
- 星级过滤:是keyword类型,用term查询
- 价格过滤:是数值类型,用range查询
- 城市过滤:是keyword类型,用term查询
-
多个查询条件组合,肯定是boolean查询来组合:
- 关键字搜索放到must中,参与算分
- 其它过滤条件放到filter中,不参与算分
-
因为条件构建的逻辑比较复杂,这里先封装为一个函数:
点击查看代码
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 测试
3 我周边的酒店
3.1 课件
3.2 需求分析
- 在酒店列表页的右侧,有一个小地图,点击地图的定位按钮,地图会找到你所在的位置:
- 并且,在前端会发起查询请求,将你的坐标发送到服务端:
- 我们要做的事情就是基于这个location坐标,然后按照距离对周围酒店排序。实现思路如下:
- 修改RequestParams参数,接收location字段
- 修改search方法业务逻辑,如果location有值,添加根据geo_distance排序的功能
3.3 修改实体类
3.4 距离排序API
3.5 添加距离排序
点击查看代码
@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);
}
}
3.6 距离排序显示
发现确实可以实现对我附近酒店的排序,不过并没有看到酒店到底距离我多远,这该怎么办?
排序完成后,页面还要获取我附近每个酒店的具体距离值,这个值在响应结果中是独立的:
因此,我们在结果解析阶段,除了解析source部分以外,还要得到sort部分,也就是排序的距离,然后放到响应结果中。
我们要做两件事:
- 修改HotelDoc,添加排序距离字段,用于页面显示
- 修改HotelService类中的handleResponse方法,添加对sort值的获取
3.6.1 修改HotelDoc类,添加距离字段
点击查看代码
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方法
点击查看代码
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 重启服务测试
4 酒店竞价排名
4.1 课件
4.2 需求分析
- 要让指定酒店在搜索结果中排名置顶,效果如图:
- 页面会给指定的酒店添加广告标记。那怎样才能让指定的酒店排名置顶呢?
- 我们之前学习过的function_score查询可以影响算分,算分高了,自然排名也就高了。而function_score包含3个要素:
- 过滤条件:哪些文档要加分
- 算分函数:如何计算function score
- 加权方式:function score 与 query score如何运算
- 这里的需求是:让指定酒店排名靠前。因此我们需要给这些酒店添加一个标记,这样在过滤条件中就可以根据这个标记来判断,是否要提高算分。
- 比如,我们给酒店添加一个字段:isAD,Boolean类型:
- true:是广告
- false:不是广告
- 这样function_score包含3个要素就很好确定了:
- 过滤条件:判断isAD 是否为true
- 算分函数:我们可以用最简单暴力的weight,固定加权值
- 加权方式:可以用默认的相乘,大大提高算分
- 因此,业务的实现步骤包括:
- 给HotelDoc类添加isAD字段,Boolean类型
- 挑选几个你喜欢的酒店,给它的文档数据添加isAD字段,值为true
- 修改search方法,添加function score功能,给isAD值为true的酒店增加权重
4.3 修改HotelDoc实体类
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
}
}
4.5 添加算分函数查询
-
接下来我们就要修改查询条件了。之前是用的boolean 查询,现在要改成function_socre查询。
-
function_score查询结构如下:
-
对应的JavaAPI如下:
-
我们可以将之前写的boolean查询作为原始查询条件放到query中,接下来就是添加过滤条件、算分函数、加权模式了。所以原来的代码依然可以沿用。
-
修改
com.yppah.hoteldemo.service.impl
包下的HotelService
类中的buildBasicQuery
方法,添加算分函数查询:
点击查看代码
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 测试
注意:要把浏览器中的广告拦截插件暂时关掉
有点问题广告标识未显示QAQ
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY