目录
- 1 需求分析
- 2 酒店搜索和分页
- 2.1 请求和响应分析
- 2.2 定义实体类,接收请求参数的JSON对象
- 2.3 编写controller,接收页面的请求
- 2.4 编写业务实现,利用RestHighLevelClient实现搜索、分页
- 3. 酒店结果过滤
- 3.1 请求和响应分析
- 3.2 修改请求参数的对象RequestParams
- 3.2 修改业务逻辑,在搜索条件之外,添加一些过滤条件
- 4.实现 我周边的酒店
- 4.1 请求和响应分析
- 4.2 修改RequestParams参数,接收location字段
- 4.3 修改search方法,完成距离排序
- 4.4 排序距离显示
- 5 酒店竞价排名
- 5.1 请求和响应分析
- 5.2 修改Hoteldoc实体类 以及 es添加doc属性
- 5.3 修改业务层代码
- 6 实现品牌城市星级价格的聚合
- 6.1 什么意思
- 6.2 请求和响应分析
- 6.3 Controller层实现
- 6.4 业务层实现
代码请见: https://gitee.com/lhwebsite/es_practice_hotels
1 需求分析
实现四部分功能:
- 酒店搜索和分页
- 酒店结果过滤
- 我周边的酒店
- 酒店竞价排名
- 实现品牌城市星级价格的聚合
2 酒店搜索和分页
2.1 请求和响应分析
由上我们可知:
- 请求方式:POST
- 请求路径:/hotel/list
- 请求参数:JSON对象,包含4个字段:
- key:搜索关键字
- page:页码
- size:每页大小
- sortBy:排序,目前暂不实现
再分析下响应信息:
返回值:分页查询,需要返回分页结果PageResult,包含两个属性:
- total:总条数
- hotels:当前页的数据
因此,我们实现业务的流程如下:
- 步骤一:定义实体类,接收请求参数的JSON对象
- 步骤二:编写controller,接收页面的请求
- 步骤三:编写业务实现,利用RestHighLevelClient实现搜索、分页
2.2 定义实体类,接收请求参数的JSON对象
request请求pojo类:
@Data
public class requestParams {
private String key;
private Integer page;
private Integer size;
private String sortBy;
}
reponse响应pojo类:
@Data
@NoArgsConstructor
@AllArgsConstructor
public class PageResult {
private Long total;
private List<HotelDoc> hotels;
}
2.3 编写controller,接收页面的请求
@RestController
@RequestMapping("hotel")
public class HotelController {
@Autowired
private IHotelService hotelService;
@PostMapping("list")
public PageResult search(@RequestBody RequestParams params) {
return hotelService.search(params);
}
}
2.4 编写业务实现,利用RestHighLevelClient实现搜索、分页
@Service
public class HotelService extends ServiceImpl<HotelMapper, Hotel> implements IHotelService {
@Autowired
private RestHighLevelClient restHighLevelClient;
@Override
public PageResult searchPageInfo(RequestParams params) throws IOException {
//1 构建搜索请求对象
SearchRequest request = new SearchRequest("hotel");
//2 构建查询条件
if(params.getKey() == null){
//如果关键字为空 则无条件查询
request.source().query(QueryBuilders.matchAllQuery());
}else{
request.source().query(QueryBuilders.matchQuery("all",params.getKey()));
}
// 3 构建分页
Integer page = params.getPage()==null?1:params.getPage();
Integer pageSize = params.getSize()==null?1:params.getSize();
request.source().from((page - 1)*pageSize).size(pageSize);
//4 发起请求
SearchResponse response = restHighLevelClient.search(request, RequestOptions.DEFAULT);
//5 解析response
return handerResult(response);
}
private PageResult handerResult(SearchResponse response) {
//1 判断是不是null
if(response == null){
return null;
}
//2 解析数据
SearchHits hits = response.getHits();
//3 获取命中的文档数
long total = hits.getTotalHits().value;
//4 获取命中查询的内容
SearchHit[] hitsArray = hits.getHits();
List<HotelDoc> docs = new ArrayList<>();
if(hitsArray.length > 0){
for (SearchHit hit : hitsArray) {
String jsonData = hit.getSourceAsString();
HotelDoc hotelDoc = JSON.parseObject(jsonData, HotelDoc.class);
docs.add(hotelDoc);
}
}
//5 组装pageResult
return new PageResult(total, docs);
}
}
3. 酒店结果过滤
3.1 请求和响应分析
需求:添加品牌、城市、星级、价格等过滤功能
在页面搜索框下面,会有一些过滤项:
前端进入f12查看request和response
包含的过滤条件有:
- brand:品牌值
- city:城市
- minPrice~maxPrice:价格范围
- starName:星级
我们需要做两件事情:
- 修改请求参数的对象RequestParams,接收上述参数
- 修改业务逻辑,在搜索条件之外,添加一些过滤条件
3.2 修改请求参数的对象RequestParams
3.2 修改业务逻辑,在搜索条件之外,添加一些过滤条件
对业务层进行修改,使用bool查询进行组合查询条件:
- 品牌过滤:是keyword类型,用term查询
- 星级过滤:是keyword类型,用term查询
- 价格过滤:是数值类型,用range查询
- 城市过滤:是keyword类型,用term查询
- 关键字搜索放到must中,参与算分
- 其它过滤条件放到filter中,不参与算分
为了提高代码可阅读性,我把bool过滤查询封装到了一个函数:
/**
* bool多条件过滤查询构建方法
* @param request
* @param params
*/
private void buildBasicSearch(SearchRequest request, RequestParams params) {
BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();
//1 设置must全文过滤条件
if(params.getKey() == null){
//如果关键字为空 则无条件查询
boolQueryBuilder.must(QueryBuilders.matchAllQuery());
}else{
boolQueryBuilder.must(QueryBuilders.matchQuery("all",params.getKey()));
}
//2 设置filter过滤条件
if(params.getBrand() != null){
boolQueryBuilder.filter(QueryBuilders.termQuery("brand",params.getBrand()));
}
if(params.getCity() != null){
boolQueryBuilder.filter(QueryBuilders.termQuery("city",params.getCity()));
}
if(params.getStarName() != null){
boolQueryBuilder.filter(QueryBuilders.termQuery("starName",params.getStarName()));
}
if(params.getMinPrice() != null && params.getMaxPrice() != null){
boolQueryBuilder.filter(QueryBuilders.rangeQuery("price").gte(params.getMinPrice()).lte(params.getMaxPrice()));
}
request.source().query(boolQueryBuilder);
}
4.实现 我周边的酒店
4.1 请求和响应分析
在酒店列表页的右侧,有一个小地图,点击地图的定位按钮,地图会找到你所在的位置:
并且,在前端会发起查询请求,将你的坐标发送到服务端:
所以需求就是基于这个location坐标,然后按照距离对周围酒店排序。实现思路如下:
- 修改RequestParams参数,接收location字段
- 修改search方法业务逻辑,如果location有值,添加根据geo_distance排序的功能
4.2 修改RequestParams参数,接收location字段
@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;
// 我当前的地理坐标
private String location;
}
4.3 修改search方法,完成距离排序
业务层修改代码:距离排序规则 由近到远排序
if(params.getLocation() != null){
//距离排序规则 由近到远排序
request.source().sort(SortBuilders.geoDistanceSort("location",new GeoPoint(params.getLocation()))
.order(SortOrder.ASC).unit(DistanceUnit.KILOMETERS));
}
4.4 排序距离显示
实现以下功能:
这个实现也很简单,之前学习es时,在es终端输入这个距离排序,得到的结果是:
因此,我们在结果解析阶段,除了解析source部分以外,还要得到sort部分,也就是排序的距离,然后放到响应结果中。
我们要做两件事:
- 修改HotelDoc,添加排序距离字段,用于页面显示
- 修改HotelService类中的handleResponse方法,添加对sort值的获取
首先查看前端页面:
这里前端接受的是一个叫做distance的值(且保留两位小数),因此,HotelDoc实体类中应该添加一个distance成员变量
之后修改业务层代码:
//获取距离
Object[] sortValues = hit.getSortValues();
if(sortValues.length > 0){
hotelDoc.setDistance(sortValues[0]);
}
5 酒店竞价排名
5.1 请求和响应分析
充钱了就是牛逼
要让指定酒店在搜索结果中排名置顶,并且像淘宝一样有个“广告”标识
那怎样才能让指定的酒店排名置顶呢?
的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的酒店增加权重
5.2 修改Hoteldoc实体类 以及 es添加doc属性
修改实体类:
给几个酒店的doc添加isAD标签
首先,我们es原来索引的mapping中没有isAD,那么怎么进行添加?
其实不用改动mapping,es相对于mysql这中关系型数据库有个很强大的特性:我们只需要给某个索引doc添加isAD属性,那么hotel的索引mapping自动会检测到并添加isAD的mapping属性。如下所示:
POST /hotel/_update/2359697
{
"doc": {
"isAD":true
}
}
之后查一下hotel的mapping
5.3 修改业务层代码
首先我们回一下算分查询的语法把:
那么根据es的语法,修改代码:
FunctionScoreQueryBuilder functionScoreQueryBuilder = QueryBuilders.functionScoreQuery(boolQueryBuilder,
new FunctionScoreQueryBuilder.FilterFunctionBuilder[]{
new FunctionScoreQueryBuilder.FilterFunctionBuilder(
QueryBuilders.termQuery("isAD","true"),
ScoreFunctionBuilders.weightFactorFunction(20f)
)
});
最终达到这种效果:
6 实现品牌城市星级价格的聚合
6.1 什么意思
搜索页面的品牌、城市等信息不应该是在页面写死,而是通过聚合索引库中的酒店数据得来的
那么如何解决这个问题呢?
使用聚合功能,利用Bucket聚合,对搜索结果中的文档基于品牌分组、基于城市分组,就能得知包含哪些品牌、哪些城市了,之后在选项中显示包含的结果。
因为是对搜索结果聚合,因此聚合是限定范围的聚合,也就是说聚合的限定条件跟搜索文档的条件一致。
6.2 请求和响应分析
首先,这个功能是通过filters接口实现的:
那么,我们从前端页面看一下这个request和response的参数:
其中request和上面是一样的
response大概的样子:
6.3 Controller层实现
要求:
- 请求方式:
POST
- 请求路径:
/hotel/filters
- 请求参数:
RequestParams
,与搜索文档的参数一致 - 返回值类型:
Map<String, List<String>>
@PostMapping("/filters")
public Map<String, List<String>> getFilters(@RequestBody RequestParams params){
return hotelService.getFilters(params);
}
6.4 业务层实现
service代码如下:
/**
* 根据传入的条件 动态过滤出品牌星级城市价格等信息
* @param params
* @return
*/
@Override
public Map<String, List<String>> getFilters(RequestParams params) throws IOException {
SearchRequest request = new SearchRequest("hotel");
request.source().size(0);
buildBasicSearch(request,params);
//在上面条件的基础上构建聚合:brand city starName
buildAggs(request);
//发起请求
SearchResponse response = restHighLevelClient.search(request, RequestOptions.DEFAULT);
//解析聚合数据
List<String> cities = getBuckNames(response,"brandAggs");
List<String> brands = getBuckNames(response,"cityAggs");
List<String> starNames = getBuckNames(response,"starNameAggs");
//组装为响应结果
Map<String,List<String>> info = new HashMap<>();
info.put("city",cities);
info.put("brand",brands);
info.put("starName",starNames);
return info;
}
封装了两个函数
buildAggs:
/**
* 构建brand city starName的聚合
* @param request
*/
private void buildAggs(SearchRequest request) {
request.source().aggregation(AggregationBuilders
.terms("cityAggs").field("city").size(20));
request.source().aggregation(AggregationBuilders
.terms("brandAggs").field("brand").size(20));
request.source().aggregation(AggregationBuilders
.terms("starNameAggs").field("starName").size(20));
}
getBuckNames:
/**
* 根据response和聚合名称获取桶的数据
* @param response
* @param aggName
* @return
*/
private List<String> getBuckNames(SearchResponse response, String aggName) {
List<String> result = new ArrayList<>();
Aggregations aggregations = response.getAggregations();
Terms aggregation = aggregations.get(aggName);
if(aggregation == null || aggregation.getBuckets().size() == 0){
return result;
}
List<? extends Terms.Bucket> buckets = aggregation.getBuckets();
for (Terms.Bucket bucket : buckets) {
String key = bucket.getKeyAsString();
result.add(key);
}
return result;
}