目录
- 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 业务层实现
- 7 实现搜索的自动补全
- 7.1 自动补全功能思路
- 7.2 修改索引库
- 7.3 修改HotelDoc类
- 7.4 分析前端request和response
- 7.5 RestAPI实现自动补全
代码请见: 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 {@Autowiredprivate 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 {@Autowiredprivate RestHighLevelClient restHighLevelClient;@Overridepublic 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 解析responsereturn handerResult(response);}private PageResult handerResult(SearchResponse response) {//1 判断是不是nullif(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 组装pageResultreturn 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*/@Overridepublic Map<String, List<String>> getFilters(RequestParams params) throws IOException {SearchRequest request = new SearchRequest("hotel");request.source().size(0);buildBasicSearch(request,params);//在上面条件的基础上构建聚合:brand city starNamebuildAggs(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;}
7 实现搜索的自动补全
7.1 自动补全功能思路
- 修改hotel索引库结构,设置自定义拼音分词器
- 修改索引库的name、all字段,使用自定义分词器
- 索引库添加一个新字段suggestion,类型为completion类型,使用自定义的分词器
- 给HotelDoc类添加suggestion字段,内容包含brand、business
- 重新导入数据到hotel库
- RestAPI实现自动补全
7.2 修改索引库
对于hotel索引库,进行了以下修改:
- 定义了两个分词器analyzer,text_anlyzerr是分词+拼音过滤 ,completion_analyzer是单纯进行拼音过滤
- 构建倒排索引时使用text_anlyzerr,查询时使用ik_smart
- 添加了一个suggestion字段,使用自定义的分词器,包含brand、suggestion、city等信息,作为自动补全的提示
// 酒店数据索引库
PUT /hotel
{"settings": {"analysis": {"analyzer": {"text_anlyzer": {"tokenizer": "ik_max_word","filter": "py"},"completion_analyzer": { "tokenizer": "keyword","filter": "py"}},"filter": {"py": {"type": "pinyin","keep_full_pinyin": false,"keep_joined_full_pinyin": true,"keep_original": true,"limit_first_letter_length": 16,"remove_duplicated_term": true,"none_chinese_pinyin_tokenize": false}}}},"mappings": {"properties": {"id":{"type": "keyword"},"name":{"type": "text","analyzer": "text_anlyzer","search_analyzer": "ik_smart","copy_to": "all"},"address":{"type": "keyword","index": false},"price":{"type": "integer"},"score":{"type": "integer"},"brand":{"type": "keyword","copy_to": "all"},"city":{"type": "keyword"},"starName":{"type": "keyword"},"business":{"type": "keyword","copy_to": "all"},"location":{"type": "geo_point"},"pic":{"type": "keyword","index": false},"all":{"type": "text","analyzer": "text_anlyzer","search_analyzer": "ik_smart"},"suggestion":{"type": "completion","analyzer": "completion_analyzer"}}}
}
7.3 修改HotelDoc类
由于这个bussiness属性可能包含多个(用‘/’间隔),所以在构造方法要进行一些处理,将bussiness的每个值形成独立的词条
@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;//自动提示的字段private List<String> suggestion;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();//维护当前文档搜索提示的关键字if (this.business.contains("/")){String[] arrs = this.business.split("/");this.suggestion = new ArrayList<>();this.suggestion.add(this.brand);Collections.addAll(this.suggestion,arrs);}else{this.suggestion = Arrays.asList(this.brand,this.business);}}
}
之后重新运行插入文档的代码即可
再来复习下批量导入doc
@Testpublic void batchTest() throws IOException {//1 获取所有的酒店数据List<Hotel> hotels = hotelMapper.selectList(null);//2转化为文档对象 转化为IndexRequest对象//3 添加到bulk对象BulkRequest bulkRequest = new BulkRequest("hotel");hotels.stream().forEach(dbHotel->{IndexRequest index = new IndexRequest("hotel").id(dbHotel.getId().toString()).source(JSON.toJSONString(new HotelDoc(dbHotel)), XContentType.JSON);bulkRequest.add(index);});BulkResponse result = restHighLevelClient.bulk(bulkRequest, RequestOptions.DEFAULT);System.out.println(result);}
}
运行成功后查询下doc
7.4 分析前端request和response
当我们在前端的搜索栏,输入拼音,会发出这样的请求:
接受的响应根据查看前端代码得知应该是一个list集合:
7.5 RestAPI实现自动补全
首先,我们来写一下,自动补全查询的语句:
GET /hotel/_search
{"suggest": {"mySuggestion": {"text": "h", //查询关键字"completion":{"field":"suggestion", //补全字段"skip_duplicates":true, //跳过重复的内容"size":10 //获取前十条结果}}}
}
那么再来看下RestAPi:
再来看下如何获取返回结果:
因此得到最终业务层代码:
@Overridepublic List<String> getSuggestion(String key) throws IOException {SearchRequest request = new SearchRequest("hotel");request.source().suggest(new SuggestBuilder().addSuggestion("hotelSuggestion",SuggestBuilders.completionSuggestion("suggestion").prefix(key).skipDuplicates(true).size(5)));SearchResponse response = restHighLevelClient.search(request, RequestOptions.DEFAULT);//解析Suggest suggest = response.getSuggest();CompletionSuggestion hotelSuggestion = suggest.getSuggestion("hotelSuggestion");List<String> sug = new ArrayList<>();if(hotelSuggestion == null || hotelSuggestion.getOptions() == null){return sug;}else {List<CompletionSuggestion.Entry.Option> options = hotelSuggestion.getOptions();for (CompletionSuggestion.Entry.Option option : options) {String suggestion = option.getText().toString();sug.add(suggestion);}return sug;}}
最终得到以下效果: