一、酒店搜索和分页
这个请求的信息如下:
-
请求方式:POST
-
请求路径:/hotel/list
-
请求参数:JSON对象,包含4个字段:
-
key:搜索关键字
-
page:页码
-
size:每页大小
-
sortBy:排序,目前暂不实现
-
-
返回值:分页查询,需要返回分页结果PageResult,包含两个属性:
total:总条数List<HotelDoc>:当前页的数据
因此,我们实现业务的流程如下:
- 定义实体类,接收请求参数的JSON对象,返回响应JSON对象
- 编写controller,接收页面的请求
- 编写业务实现,利用RestHighLevelClient实现搜索、分页
1.定义实体类
package cn.itcast.hotel.pojo;
import lombok.Data;
@Data
public class RequestParams {
private String key;
private Integer page;
private Integer size;
private String sortBy;
}
package cn.itcast.hotel.pojo;
import lombok.Data;
import java.util.List;
@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;
}
}
2.编写Controller层
@RestController
@RequestMapping("hotel")
public class HotelController {
@Autowired
private IHotelService hotelService;
@GetMapping("list")
public PageResult search(@RequestBody RequestParams requestParams) {
return hotelService.search(requestParams);
}
}
3.编写service层
先在启动类中初始化RestClient,加载到spring容器后,方便后续autowired注入
@MapperScan("cn.itcast.hotel.mapper")
@SpringBootApplication
public class HotelDemoApplication {
public static void main(String[] args) {
SpringApplication.run(HotelDemoApplication.class, args);
}
@Bean
public RestHighLevelClient client() {
return new RestHighLevelClient(RestClient.builder(HttpHost.create("http://120.55.95.185:9200")));
}
}
@Service
public class HotelService extends ServiceImpl<HotelMapper, Hotel> implements IHotelService {
@Autowired
private HotelMapper hotelMapper;
@Autowired
private RestHighLevelClient client;
@Override
public PageResult search(RequestParams requestParams) {
try {
SearchRequest request = new SearchRequest("hotel");
//获取查询内容
String key = requestParams.getKey();
//当搜索内容为空时,返回全部数据
if(key == null || key.equals("")) {
request.source().query(QueryBuilders.matchAllQuery());
} else {
request.source().query(QueryBuilders.matchQuery("all", requestParams.getKey()));
}
//分页配置
request.source().from((requestParams.getPage() - 1) * requestParams.getSize()).size(requestParams.getSize());
//发送请求
SearchResponse search = client.search(request, RequestOptions.DEFAULT);
//解析响应
return handleResponse(search);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
private PageResult handleResponse(SearchResponse search) {
SearchHits hits = search.getHits();
long value = hits.getTotalHits().value;
SearchHit[] hits1 = hits.getHits();
List<HotelDoc> list = new ArrayList<>();
for(SearchHit hit : hits1) {
String json = hit.getSourceAsString();
HotelDoc hotelDoc = JSON.parseObject(json, HotelDoc.class);
list.add(hotelDoc);
}
return new PageResult(value, list);
}
}
二、酒店条件过滤
我们可以在页面搜索框下面,看到一些过滤项:
过滤项发送的请求与搜索相同,但是传递的参数却多了几个:
- brand:品牌值
- city:城市
- minPrice~maxPrice:价格范围
- starName:星级
1.修改RequestParams实体类
package cn.itcast.hotel.pojo;
import lombok.Data;
@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.修改service层,实现过滤(使用filter不算分)
- 品牌过滤:是keyword类型,用term查询
- 星级过滤:是keyword类型,用term查询
- 价格过滤:是数值类型,用range查询
- 城市过滤:是keyword类型,用term查询
涉及多个查询条件组合,肯定需要boolean查询来组合:
- 关键字搜索放到must中,参与算分
- 其它过滤条件放到filter中,不参与算分
@Override
public PageResult search(RequestParams requestParams) {
try {
SearchRequest request = new SearchRequest("hotel");
//获取查询内容
String key = requestParams.getKey();
//构造bool查询
request.source().query(boolQueryBuilder(requestParams));
//分页配置
request.source().from((requestParams.getPage() - 1) * requestParams.getSize()).size(requestParams.getSize());
//发送请求
SearchResponse search = client.search(request, RequestOptions.DEFAULT);
//解析响应
return handleResponse(search);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
由于构造bool查询太过复杂,我们抽取一个方法来写
(String类型判空判两次==null和equals(""),数值类型判空只判==null即可):
private BoolQueryBuilder boolQueryBuilder(RequestParams requestParams) {
BoolQueryBuilder boolQueryBuilder = new BoolQueryBuilder();
//1.关键字搜索
String key = requestParams.getKey();
if(key == null || key.equals("")) {
boolQueryBuilder.must(QueryBuilders.matchAllQuery());
} else {
boolQueryBuilder.must(QueryBuilders.matchQuery("all", key));
}
//2.城市条件
String city = requestParams.getCity();
if(city != null && !city.equals("")) {
boolQueryBuilder.filter(QueryBuilders.termQuery("city", city));
}
//3.品牌条件
String brand = requestParams.getBrand();
if(brand != null && !brand.equals("")) {
boolQueryBuilder.filter(QueryBuilders.termQuery("brand", brand));
}
//4.星级条件
String start = requestParams.getStarName();
if(start != null && !start.equals("")) {
boolQueryBuilder.filter(QueryBuilders.termQuery("startName", start));
}
//5.价格条件
Integer maxPrice = requestParams.getMaxPrice(), minPrice = requestParams.getMinPrice();
if(minPrice != null && maxPrice != null) {
boolQueryBuilder.filter(QueryBuilders.rangeQuery("price").gte(minPrice).lte(maxPrice));
}
return boolQueryBuilder;
}
三、我附近的酒店
在酒店列表页的右侧,有一个小地图,点击地图的定位按钮,地图会找到你所在的位置:
并且,在前端会通过搜索接口发起查询请求,将坐标发送到服务端:
我们要做的事情就是基于这个location坐标,然后按照距离对周围酒店排序并显示距离。思路如下:
- 修改RequestParams参数,接收location字段
- 修改hotdoc对象,返回distance距离信息
- 修改search方法业务逻辑,如果location有值,添加根据geo_distance排序的功能
- 排序完成后,页面还要获取我附近每个酒店的具体距离值,这个值在es响应结果中是独立的:
可以看到距离信息存在sort数组中
1.修改RequestParams实体类
添加location属性
// 增加我当前的地理坐标
private String location;
2.修改hotdoc属性
添加distance属性
// 添加记录排序时的 距离值
private Object distance;
3.修改service层
地理位置排序的DSL:
GET /indexName/_search
{
"query": {
"match_all": {}
},
"sort": [
{
"_geo_distance" : {
"FIELD" : "纬度,经度", // 文档中geo_point类型的字段名、目标坐标点
"order" : "asc", // 排序方式
"unit" : "km" // 排序的距离单位
}
}
]
}
可以看到sort里字段里还有一层构造_geo_distance,所以代码中要使用SortBuilders构造出geoDistanceSort
sort与query同层,所以要单独配置sort
@Override
public PageResult search(RequestParams requestParams) {
try {
SearchRequest request = new SearchRequest("hotel");
//获取查询内容
String key = requestParams.getKey();
//构造bool查询
request.source().query(boolQueryBuilder(requestParams));
//分页配置
request.source().from((requestParams.getPage() - 1) * requestParams.getSize()).size(requestParams.getSize());
//sort与query同层
String location = requestParams.getLocation();
if(location != null && !location.equals("")) {
request.source().sort(SortBuilders.
geoDistanceSort("location", new GeoPoint(location))
.order(SortOrder.ASC)
.unit(DistanceUnit.KILOMETERS));
}
//发送请求
SearchResponse search = client.search(request, RequestOptions.DEFAULT);
//解析响应
return handleResponse(search);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
private PageResult handleResponse(SearchResponse search) {
SearchHits hits = search.getHits();
long value = hits.getTotalHits().value;
SearchHit[] hits1 = hits.getHits();
List<HotelDoc> list = new ArrayList<>();
for(SearchHit hit : hits1) {
String json = hit.getSourceAsString();
HotelDoc hotelDoc = JSON.parseObject(json, HotelDoc.class);
//获取es响应中的sort数组
Object[] sortValues = hit.getSortValues();
if(sortValues.length > 0) {
hotelDoc.setDistance(sortValues[0]); //距离值位于sort数组第一个元素
}
list.add(hotelDoc);
}
return new PageResult(value, list);
}
四、广告置顶
搜索内容时,我们常常可以看到位于顶部的是广告。接下来我们实现指定酒店在搜索结果中排名靠前。
我们之前学习过的function_score查询可以影响算分,算分高了,自然排名也就高了。
因此我们需要给这些酒店添加一个标记,这样在过滤条件中就可以根据这个标记来判断,是否要提高算分。
比如,我们给酒店添加一个字段:isAD,Boolean类型:
- true:是广告
- false:不是广告
这样function_score包含3个要素就很好确定了:
- 过滤条件:判断isAD 是否为true
- 算分函数:这里我们可以用最简单暴力的weight,固定加权值
- 加权方式:可以用默认的相乘,大大提高算分
1.修改hotdoc实体类
2.添加广告标记
接下来,作为测试效果,我们挑几个酒店,在kinbana中添加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
}
}
3.修改service层
接下来我们就要修改查询条件了。之前是用的boolean 查询,现在要改成function_socre查询。
我们可以将之前写的boolean查询放到算分查询中,然后添加过滤条件、算分函数、加权模式。所以原来的代码依然可以沿用。
private void buildBasicQuery(RequestParams params, SearchRequest request) {
// 1.构建BooleanQuery
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())
);
}
// 添加算分查询
// 2.算分控制
FunctionScoreQueryBuilder functionScoreQuery =
QueryBuilders.functionScoreQuery(
// 原始查询,相关性算分的查询
boolQuery,
// function score的数组
new FunctionScoreQueryBuilder.FilterFunctionBuilder[]{
// 其中的一个function score 元素
new FunctionScoreQueryBuilder.FilterFunctionBuilder(
// 过滤条件
QueryBuilders.termQuery("isAD", true),
// 算分函数
ScoreFunctionBuilders.weightFactorFunction(10)
)
});
request.source().query(functionScoreQuery);
}