7、黑马旅游案例
7、黑马旅游案例
下面,我们通过黑马旅游的案例来实战演练下之前学习的知识。
我们实现四部分功能:
- 酒店搜索和分页
- 酒店结果过滤
- 我周边的酒店
- 酒店竞价排名
启动我们提供的hotel-demo项目,其默认端口是8089,访问 旅游项目,就能看到项目页面了:
7.1 酒店搜索和分页
案例需求:实现黑马旅游的酒店搜索功能,完成关键字搜索和分页
需求分析
在项目的首页,有一个大大的搜索框,还有分页按钮:
点击搜索按钮,可以看到浏览器控制台发出了请求:
请求参数如下:
由此可得出请求信息如下:
- 请求方式:POST
- 请求路径:/hotel/list
- 请求参数:JSON对象
- key :关键字
- page:页码
- size:页面大小
- sortBy:排序方式
实现流程
- 一:定义实体类接收JSON参数
@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;
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();
}
}
- 二:分页结果集PageResult对象
@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;
}
}
- 三:业务流程
Controller
/**
* 酒店分页搜索
* @param requestParams 请求参数
* @return
*/
@PostMapping("/list")
public PageResult search(@RequestBody RequestParams requestParams) {
return hotelService.search(requestParams);
}
Service
@Override
public PageResult search(RequestParams params) {
try {
// 1.准备Request
SearchRequest request = new SearchRequest("hotel");
// 2.准备DSL
// 2.1.query
String key = params.getKey();
if (!StringUtils.isEmpty(params.getKey())) {//搜索框无数据时
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);
}
7.2 酒店结果过滤
需求:添加品牌、城市、星级、价格等过滤功能
需求分析
在页面搜索框下面,会有一些过滤项:
传递的参数如图:
包含的过滤条件有:
- brand:品牌值
- city:城市
- minPrice~maxPrice:价格范围
- starName:星级
实现流程
在HotelService的search方法中,只有一个地方需要修改:requet.source().query( ... )其中的查询条件。
在之前的业务中,只有match查询,根据关键字搜索,现在要添加条件过滤,包括:
- 品牌过滤:是keyword类型,用term查询
- 星级过滤:是keyword类型,用term查询
- 价格过滤:是数值类型,用range查询
- 城市过滤:是keyword类型,用term查询
多个查询条件组合,肯定是boolean查询来组合:
- 关键字搜索放到must中,参与算分
- 其它过滤条件放到filter中,不参与算分
Service
@Override
public PageResult search(RequestParams params) {
try {
// 1.准备Request
SearchRequest request = new SearchRequest("hotel");
// 2.准备DSL
// 2.1.query
String key = params.getKey();
if (!StringUtils.isEmpty(params.getKey())) {//搜索框无数据时
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);
}
}
/**
* 构造过滤器
* @param params
* @return
*/
private void buildBasicQuery(RequestParams params,SearchRequest request) {
//1.构造过滤条件
//创建过滤对象
BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();
//关键字过滤
if (!StringUtils.isEmpty(params.getKey())) {
boolQueryBuilder.must(QueryBuilders.matchQuery("all", params.getKey()));
} else {
boolQueryBuilder.must(QueryBuilders.matchAllQuery());
}
//城市过滤
if (!StringUtils.isEmpty(params.getCity())){
boolQueryBuilder.filter(QueryBuilders.termQuery("city",params.getCity()));
}
//星级过滤
if (!StringUtils.isEmpty(params.getStarName())){
boolQueryBuilder.filter(QueryBuilders.termQuery("starName",params.getStarName()));
}
//品牌过滤
if (!StringUtils.isEmpty(params.getBrand())){
boolQueryBuilder.filter(QueryBuilders.termQuery("brand",params.getBrand()));
}
//价格过滤
if(params.getMinPrice() != null && params.getMaxPrice() != null){
boolQueryBuilder.filter(
QueryBuilders.rangeQuery("price")
.gte(params.getMinPrice())
.lte(params.getMaxPrice())
);
}
//设置查询条件
request.source().query(boolQueryBuilder);
}
7.3 周边的酒店
需求:我附近的酒店
需求分析
在酒店列表页的右侧,有一个小地图,点击地图的定位按钮,地图会找到你所在的位置:
并且,在前端会发起查询请求,将你的坐标发送到服务端:
我们要做的事情就是基于这个location坐标,然后按照距离对周围酒店排序。实现思路如下:
- 修改RequestParams参数,接收location字段
- 修改search方法业务逻辑,如果location有值,添加根据geo_distance排序的功能
距离排序API
我们以前学习过排序功能,包括两种:
- 普通字段排序
- 地理坐标排序
我们只讲了普通字段排序对应的java写法。地理坐标排序只学过DSL语法,如下:
GET /indexName/_search
{
"query": {
"match_all": {}
},
"sort": [
{
"price": "asc"
},
{
"_geo_distance" : {
"FIELD" : "纬度,经度",
"order" : "asc",
"unit" : "km"
}
}
]
}
对应的java代码示例:
添加距离排序
/**
* 酒店分页搜索
*
* @param params 请求参数
* @return
*/
@Override
public PageResult search(RequestParams params) {
try {
// 1. 创建请求对象
SearchRequest request = new SearchRequest("hotel");
//2.准备DSL
//2.1 创建过滤器
buildBasicQuery1(params,request);
//2.2 分页 使用search after 解决深度分页问题
request.source().from((params.getPage() - 1) * params.getSize()).size(params.getSize());
//2.3 距离排序
if (!StringUtils.isEmpty(params.getLocation())){
request.source().sort(
SortBuilders
.geoDistanceSort("location",new GeoPoint(params.getLocation()))
.order(SortOrder.ASC)
.unit(DistanceUnit.KILOMETERS)
);
}
//3. 执行请求
SearchResponse search = client.search(request, RequestOptions.DEFAULT);
//4. 解析响应
PageResult pageResult = parseHits1(search);
//5. 返回结果集
return pageResult;
} catch (IOException e) {
throw new RuntimeException(e);
}
}
排序距离显示
重启服务后,测试我的酒店功能:
发现确实可以实现对我附近酒店的排序,不过并没有看到酒店到底距离我多远,这该怎么办?
排序完成后,页面还要获取我附近每个酒店的具体距离值,这个值在响应结果中是独立的:
因此,我们在结果解析阶段,除了解析source部分以外,还要得到sort部分,也就是排序的距离,然后放到响应结果中。
我们要做两件事:
- 修改HotelDoc,添加排序距离字段,用于页面显示
- 修改HotelService类中的handleResponse方法,添加对sort值的获取
/**
* 解析文档数据
*/
private PageResult parseHits(SearchResponse search) {
//获取文档数据
SearchHits hits = search.getHits();
List<HotelDoc> hotelDocs = new ArrayList<>();
//1.获取总条数
Long total = hits.getTotalHits().value;
//2.获取文档数组
SearchHit[] hits1 = hits.getHits();
Arrays.stream(hits1).forEach(hit -> {
//3.获取文档数据
String source = hit.getSourceAsString();
//4.封装文档数据 反序列化
HotelDoc hotel = JSON.parseObject(source, HotelDoc.class);
//5.获取排序值
Object[] sortValues = hit.getSortValues();
if (!ObjectUtils.isEmpty(sortValues)) {
hotel.setDistance(sortValues[0]);
}
hotelDocs.add(hotel);
});
return new PageResult(total, hotelDocs);
}
7.4 酒店竞价排名
需求:让指定的酒店在搜索结果中排名置顶
需求分析
要让指定酒店在搜索结果中排名置顶,效果如图:
页面会给指定的酒店添加广告标记。
那怎样才能让指定的酒店排名置顶呢?
我们之前学习过的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的酒店增加权重
流程实现
添加广告标记:
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
}
}
添加算分函数查询:
接下来我们就要修改查询条件了。之前是用的boolean 查询,现在要改成function_socre查询。
function_score查询结构如下:
对应的JavaAPI如下:
/**
* 构造过滤器(高亮)
* @param params
* @return
*/
private void buildBasicQuery1(RequestParams params,SearchRequest request) {
//1.构造过滤条件
//创建过滤对象
BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();
//关键字过滤
if (!StringUtils.isEmpty(params.getKey())) {
boolQueryBuilder.must(QueryBuilders.matchQuery("all", params.getKey()));
} else {
boolQueryBuilder.must(QueryBuilders.matchAllQuery());
}
//城市过滤
if (!StringUtils.isEmpty(params.getCity())){
boolQueryBuilder.filter(QueryBuilders.termQuery("city",params.getCity()));
}
//星级过滤
if (!StringUtils.isEmpty(params.getStarName())){
boolQueryBuilder.filter(QueryBuilders.termQuery("starName",params.getStarName()));
}
//品牌过滤
if (!StringUtils.isEmpty(params.getBrand())){
boolQueryBuilder.filter(QueryBuilders.termQuery("brand",params.getBrand()));
}
//价格过滤
if(params.getMinPrice() != null && params.getMaxPrice() != null){
boolQueryBuilder.filter(
QueryBuilders.rangeQuery("price")
.gte(params.getMinPrice())
.lte(params.getMaxPrice())
);
}
//2.算分控制
FunctionScoreQueryBuilder functionScoreQueryBuilder = QueryBuilders.functionScoreQuery(
//原始查询
boolQueryBuilder,
new FunctionScoreQueryBuilder.FilterFunctionBuilder[]{
//其中的一个function score 元素
new FunctionScoreQueryBuilder.FilterFunctionBuilder(
//过滤条件
QueryBuilders.termQuery("isAD", true),
//设置函数
ScoreFunctionBuilders.weightFactorFunction(10)
)
}
);
//3.高亮展示
request.source().highlighter(
new HighlightBuilder()
.field("name")
.requireFieldMatch(false)
);
//设置查询条件
request.source().query(functionScoreQueryBuilder);
}