下面,我们通过酒店搜索案例来实战演练下之前学习的知识。
我们实现四部分功能:
- 酒店搜索和分页
- 酒店结果过滤
- 我周边的酒店
- 酒店竞价排名
启动 hotel-demo 项目,其默认端口是 8089,访问 http://localhost:8090,就能看到项目页面了。
酒店搜索和分页
案例需求:实现黑马旅游的酒店搜索功能,完成关键字搜索和分页
需求分析
- 请求方式:POST
- 请求路径:/hotel/list
- 请求参数:JSON 对象,包含4个字段:
- key:搜索关键字
- page:页码
- size:每页大小
- sortBy:排序,目前暂不实现
- 返回值:分页查询,需要返回分页结果 PageResult,包含两个属性:
total:总条数List<HotelDoc>:当前页的数据
因此,我们实现业务的流程如下:
- 步骤一:定义实体类,接收请求参数的 JSON 对象
- 步骤二:编写 controller,接收页面的请求
- 步骤三:编写业务实现,利用 RestHighLevelClient 实现搜索、分页
定义实体类
实体类有两个,一个是前端的请求参数实体,一个是服务端应该返回的响应结果实体。
① 请求参数,前端请求的 JSON 结构如下:
{"key": "搜索关键字","page": 1,"size": 3,"sortBy": "default"}
因此,我们在 cn.itcast.hotel.pojo 包下定义一个实体类:
package cn.itcast.hotel.pojo;import lombok.Data;@Datapublic class RequestParams {private String key;private Integer page;private Integer size;private String sortBy;}
② 返回值,分页查询,需要返回分页结果 PageResult,包含两个属性:
total:总条数List<HotelDoc>:当前页的数据
因此,我们在 cn.itcast.hotel.pojo 中定义返回结果:
package cn.itcast.hotel.pojo;import lombok.Data;import java.util.List;@Datapublic 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
定义一个 HotelController,声明查询接口,满足下列要求:
- 请求方式:Post
- 请求路径:/hotel/list
- 请求参数:对象,类型为 RequestParam
- 返回值:PageResult,包含两个属性
Long total:总条数List<HotelDoc> hotels:酒店数据
因此,我们在 cn.itcast.hotel.web 中定义 HotelController:
@RestController@RequestMapping("/hotel")public class HotelController {@Autowiredprivate IHotelService hotelService;// 搜索酒店数据@PostMapping("/list")public PageResult search(@RequestBody RequestParams params){return hotelService.search(params);}}
实现搜索业务
我们在 controller 调用了 IHotelService,并没有实现该方法,因此下面我们就在 IHotelService 中定义方法,并且去实现业务逻辑。
① 在 cn.itcast.hotel.service 中的 IHotelService 接口中定义一个方法:
/*** 根据关键字搜索酒店信息* @param params 请求参数对象,包含用户输入的关键字* @return 酒店文档列表*/PageResult search(RequestParams params);
② 实现搜索业务,肯定离不开 RestHighLevelClient,我们需要把它注册到 Spring 中作为一个 Bean。在 cn.itcast.hotel 中的 HotelDemoApplication 中声明这个 Bean:
@Beanpublic RestHighLevelClient client() {return new RestHighLevelClient(RestClient.builder(HttpHost.create("http://halo:9200")));}
③ 在 cn.itcast.hotel.service.impl 中的 HotelService 中实现 search 方法:
@Autowiredprivate RestHighLevelClient client;@Overridepublic PageResult search(RequestParams params) {try {// 1.准备RequestSearchRequest request = new SearchRequest("hotel");// 2.准备DSL// 2.1.queryString 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) {// 获取文档sourceString json = hit.getSourceAsString();// 反序列化HotelDoc hotelDoc = JSON.parseObject(json, HotelDoc.class);// 放入集合hotels.add(hotelDoc);}// 4.4.封装返回return new PageResult(total, hotels);}
酒店结果过滤
需求:添加品牌、城市、星级、价格等过滤功能
需求分析
包含的过滤条件有:
- brand:品牌值
- city:城市
- minPrice~maxPrice:价格范围
- starName:星级
我们需要做两件事情:
- 修改请求参数的对象 RequestParams,接收上述参数
- 修改业务逻辑,在搜索条件之外,添加一些过滤条件
修改实体类
修改在 cn.itcast.hotel.pojo 包下的实体类 RequestParams:
@Datapublic 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;}
修改搜索业务
在 HotelService 的 search 方法中,只有一个地方需要修改:requet.source().query( ... ) 其中的查询条件。
在之前的业务中,只有 match 查询,根据关键字搜索,现在要添加条件过滤,包括:
- 品牌过滤:是 keyword 类型,用 term 查询
- 星级过滤:是 keyword 类型,用 term 查询
- 价格过滤:是数值类型,用 range 查询
- 城市过滤:是 keyword 类型,用 term 查询
多个查询条件组合,肯定是 boolean 查询来组合:
- 关键字搜索放到 must 中,参与算分
- 其它过滤条件放到 filter 中,不参与算分
因为条件构建的逻辑比较复杂,这里封装为一个函数,getBoolQueryBuilder 的代码如下:
private BoolQueryBuilder getBoolQueryBuilder(RequestParams params) {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()));}return boolQuery;}
我周边的酒店
需求:我附近的酒店
需求分析
在酒店列表页的右侧,有一个小地图,点击地图的定位按钮,地图会找到你所在的位置,并且,在前端会发起查询请求,将你的坐标发送到服务端。
我们要做的事情就是基于这个 location 坐标,然后按照距离对周围酒店排序。实现思路如下:
- 修改 RequestParams 参数,接收 location 字段
- 修改 search 方法业务逻辑,如果 location 有值,添加根据 geo_distance 排序的功能
修改实体类
修改在 cn.itcast.hotel.pojo 包下的实体类 RequestParams:
package cn.itcast.hotel.pojo;import lombok.Data;@Datapublic 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;}
距离排序 API
我们以前学习过排序功能,包括两种:
- 普通字段排序
- 地理坐标排序
我们只讲了普通字段排序对应的 Java 写法。地理坐标排序只学过 DSL 语法,如下:
GET /indexName/_search{"query": {"match_all": {}},"sort": [{"price": "asc"},{"_geo_distance" : {"FIELD" : "纬度,经度","order" : "asc","unit" : "km"}}]}
对应 Java 代码
// 价格排序request.source().sort("price", SortOrder.ASC);// 距离排序request.source().sort(SortBuilders.geoDistanceSort("location", new GeoPoint("31.21, 121.5")).order(SortOrder.ASC).unit(DistanceUnit.KILOMETERS));
添加距离排序
在 cn.itcast.hotel.service.impl 的 HotelService 的 search 方法中,添加一个排序功能:
@Overridepublic PageResult search(RequestParams params) {try {// 1.准备RequestSearchRequest request = new SearchRequest("hotel");// 2.准备DSL// 2.1.query// 构建 boolQueryBoolQueryBuilder boolQuery = getBoolQueryBuilder(params);request.source().query(boolQuery);// 2.2.分页int page = params.getPage();int size = params.getSize();request.source().from((page - 1) * size).size(size);// 排序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);}}
排序距离显示
排序完成后,页面还要获取我附近每个酒店的具体距离值,这个值在响应结果中是独立的:
因此,我们在结果解析阶段,除了解析 source 部分以外,还要得到 sort 部分,也就是排序的距离,然后放到响应结果中。
我们要做两件事:
- 修改 HotelDoc,添加排序距离字段,用于页面显示
- 修改 HotelService 类中的 handleResponse 方法,添加对 sort 值的获取
① 修改HotelDoc类,添加距离字段
package cn.itcast.hotel.pojo;import lombok.Data;import lombok.NoArgsConstructor;@Data@NoArgsConstructorpublic 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();}}
② 修改 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<>();
for (SearchHit hit : hits) {
// 获取文档source
String json = hit.getSourceAsString();
// 反序列化
HotelDoc hotelDoc = JSON.parseObject(json, HotelDoc.class);
// 获取排序值 - location
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);
}
酒店竞价排名
需求分析
要让指定酒店在搜索结果中排名置顶,页面会给指定的酒店添加广告标记。
我们之前学习过的 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 的酒店增加权重
修改 HotelDoc 实体
给 cn.itcast.hotel.pojo 包下的 HotelDoc 类添加 isAD 字段:
@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();
}
}
添加广告标记
用 DSL 添加酒店广告标记
POST /hotel/_update/36934
{
"doc": {
"isAD": true
}
}
添加算分函数查询
接下来我们就要修改查询条件了。之前是用的 boolean 查询,现在要改成 function_socre 查询。
function_score 查询结构如下:
GET /hotel/_search
{
"query": {
"function_score": {
"query": {
"match": {
"name": "外滩"
}
},
"functions": [
{
"filter": {
"term": {
"brand": "如家"
}
},
"weight": 5
}
]
}
}
}
对应的 JavaAPI 如下
FunctionScoreQueryBuilder functionScoreQueryBuilder =
QueryBuilders.functionScoreQuery(
QueryBuilders.matchQuery("name", "外滩"),
new FunctionScoreQueryBuilder.FilterFunctionBuilder[]{
new FunctionScoreQueryBuilder.FilterFunctionBuilder(
QueryBuilders.termQuery("brand", "如家"),
ScoreFunctionBuilders.weightFactorFunction(5)
)
}
);
sourceBuilder.query(functionScoreQueryBuilder);
我们可以将之前写的 boolean 查询作为原始查询条件放到 query 中,接下来就是添加过滤条件、算分函数、加权模式了。所以原来的代码依然可以沿用。
修改 cn.itcast.hotel.service.impl 包下的 HotelService 类中的 getQueryBuilder 方法,添加算分函数查询:
private FunctionScoreQueryBuilder getQueryBuilder(RequestParams params) {
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()));
}
// 算分控制
FunctionScoreQueryBuilder functionScoreQueryBuilder =
QueryBuilders.functionScoreQuery(
// 原始查询,相关性算分
boolQuery,
// function score
new FunctionScoreQueryBuilder.FilterFunctionBuilder[]{
// 一个 function score 元素
new FunctionScoreQueryBuilder.FilterFunctionBuilder(
// 过滤条件
QueryBuilders.termQuery("isAD", true),
// 算分函数
ScoreFunctionBuilders.weightFactorFunction(10)
)
});
return functionScoreQueryBuilder;
}
