elasticserach项目实战
通过优步旅游项目中搜索查询项目,进行elasticsearch项目实战,完成酒店搜索和分页、酒店结果过滤、周边的酒店和酒店竞价排名
一、酒店搜索和分页
案例1:实现优步旅游的酒店搜索功能,完成关键字搜索和分页:
启动项目,访问搜索功能,对应的post类型的接口并没有实现,所以实现该接口即可:
1.1.步骤1:定义类,来接收前端请求参数
由于上面的接口是post请求,参数有四个,所以在pojo包下创建实体类 RequestParam,用于接受前端的参数如下:
@Data @AllArgsConstructor @NoArgsConstructor public class RequestParam{ private String key; private Integer page; private Integer size; private String sortBy; }
1.2.步骤2:编码实现,接收前端请求
定义一个Hotel Controller,声明查询接口,满足下列要求:
- 请求方式:Post
- 请求路径:/hotel/list
- 请求参数:对象,类型为RequestParam 返回值:PageResult,包含两个属性
- Long total:总条数
- List<Hotel Doc>hotels:酒店数据
1.2.1.在pojo下定义PageResult类:
返回结果对象类
@Data @NoArgsConstructor @AllArgsConstructor public class PageResult { private Long total; private List<HotelDoc> hotels; }
1.2.2.定义controller
在Controller包下创建类HotelController, 定义searchList方法的参数RequestParams,前端提交的是json,方法参数需要加注解@RequestBody
@RestController @RequestMapping("/hotel") public class HotelController { @Autowired private IHotelService iHotelService; /** * 搜索 * @param param 通过对象接收参数 * @return PageResult JSON对象 * @throws IOException */ @PostMapping("/list") public PageResult searchList(@RequestBody RequestParam param) throws IOException { return iHotelService.search(param); } }
1.2.3.创建配置类
通过Restclient创建连接对象,通过spring注入到容器中:可以在启动类中创建Bean,在config包下创建RestClientConfig,代码如下:
package cn.itcast.hotel.config; import org.apache.http.HttpHost; import org.elasticsearch.client.RestClient; import org.elasticsearch.client.RestHighLevelClient; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class RestClientConfig { @Bean public RestHighLevelClient client(){ return new RestHighLevelClient(RestClient.builder(HttpHost.create("http://192.168.42.150:9200"))); } }
1.2.4.创建service层
创建 IHotelService 定义接口如下:
public interface IHotelService extends IService<Hotel> { PageResult search(RequestParam param) throws IOException; }
创建HotelService,实现接口IHotelService,如下:
@Service public class HotelService extends ServiceImpl<HotelMapper, Hotel> implements IHotelService { @Autowired private RestHighLevelClient client; /** * 查询、分页 * @param param */ @Override public PageResult search(RequestParam param) { try { //1.准备request SearchRequest request = new SearchRequest("hotel"); //2.准备DSL //2.1.query String key = param.getKey(); //健壮性判断,如果前端传递的参数为null或者为空字符串,就进行全文检索,else则是按照条件搜索 if (key == null || "".equals(key)) { request.source().query(QueryBuilders.matchAllQuery()); } { request.source().query(QueryBuilders.matchQuery("all", param.getKey())); } //2.2.分页 Integer page = param.getPage(); Integer size = param.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 response */ private PageResult handleResponse(SearchResponse response){ //4.解析响应 SearchHits searchHits = response.getHits(); //4.1.获取总条数 long total = searchHits.getTotalHits().value; //4.2.文档数组 SearchHit[] hits = searchHits.getHits(); ArrayList<HotelDoc> hotelDocs = new ArrayList<>(); //4.3.遍历 for (SearchHit hit : hits) { //获取文档source String json = hit.getSourceAsString(); //反序列化 HotelDoc hotelDoc = JSON.parseObject(json, HotelDoc.class); //添加到 集合 hotelDocs.add(hotelDoc); //System.out.println("hotelDoc = "+hotelDoc); } //封装成PageResult对象 return new PageResult(total,hotelDocs); } }
1.2.5.创建mapper层
创建 HotelMapper,如下:
package cn.itcast.hotel.mapper; import cn.itcast.hotel.pojo.Hotel; import com.baomidou.mybatisplus.core.mapper.BaseMapper; public interface HotelMapper extends BaseMapper<Hotel> { }
1.3.测试
重启项目,然后进行搜索,尝试分页操作,发现已经可以:
二、酒店结果过滤
案例2: 页面是有过滤项的,用户点击这些可以做过滤,添加品牌、城市、星级和价格等过滤功能,如下:
尝试操作前端页面,发现需在之前的请求中添加参数如下:
2.1.步骤一:修改实体类RequestParams,添加brand、city、starName、minPrice、maxPrice等参数
修改后代码如下:
@Data @AllArgsConstructor @NoArgsConstructor public class RequestParam{ private String key; private Integer page; private Integer size; private String sortBy; private String brand; private String starName; private String city; private Integer minPrice; private Integer maxPrice; }
2.2.步骤二:修改search方法,在match查询基础上添加过滤条件
修改search方法的实现,在关键字搜索时,如果brand等参数存在,对其做过滤,过滤条件包括:
- city精确匹配
- brand精确匹配
- starName精确匹配
- price范围过滤
注意事项:
- 多个条件之间是AND关系,组合多条件用BooleanQuery
- 参数存在才需要过滤,做好非空判断
修改HotelService实现类中的search方法实现上面的操作添加过滤条件如下:
@Service public class HotelService extends ServiceImpl<HotelMapper, Hotel> implements IHotelService { @Autowired private RestHighLevelClient client; /** * 查询、分页 * @param param */ @Override public PageResult search(RequestParam param) { try { //1.准备request SearchRequest request = new SearchRequest("hotel"); //2.准备DSL //2.1.BooleanQuery BoolQueryBuilder boolQuery = QueryBuilders.boolQuery(); //2.2.关键字搜索 String key = param.getKey(); //健壮性判断,如果前端传递的参数为null或者为空字符串,就进行全文检索,else则是按照条件搜索 if (key == null || "".equals(key)) { boolQuery.must(QueryBuilders.matchAllQuery()); } { boolQuery.must(QueryBuilders.matchQuery("all", param.getKey())); } //2.3.条件:城市 String city = param.getCity(); if(city != null && !"".equals(city)){ boolQuery.filter(QueryBuilders.termQuery("city",city)); } //2.4.条件:品牌 String brand = param.getBrand(); if(brand != null && !"".equals(brand)){ boolQuery.filter(QueryBuilders.termQuery("brand",brand)); } //2.5.条件:星级 String starName = param.getStarName(); if(starName != null && !"".equals(starName)){ boolQuery.filter(QueryBuilders.termQuery("starName",starName)); } //2.6.条件:价格 Integer maxPrice = param.getMaxPrice(); Integer minPrice = param.getMinPrice(); if(maxPrice != null && minPrice != null){ boolQuery.filter(QueryBuilders.rangeQuery("price").gte(minPrice).lte(maxPrice)); } //添加boolQuery request.source().query(boolQuery); //2.7.分页 Integer page = param.getPage(); Integer size = param.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 response */ private PageResult handleResponse(SearchResponse response){ //4.解析响应 SearchHits searchHits = response.getHits(); //4.1.获取总条数 long total = searchHits.getTotalHits().value; //4.2.文档数组 SearchHit[] hits = searchHits.getHits(); ArrayList<HotelDoc> hotelDocs = new ArrayList<>(); //4.3.遍历 for (SearchHit hit : hits) { //获取文档source String json = hit.getSourceAsString(); //反序列化 HotelDoc hotelDoc = JSON.parseObject(json, HotelDoc.class); //添加到 集合 hotelDocs.add(hotelDoc); //System.out.println("hotelDoc = "+hotelDoc); } //封装成PageResult对象 return new PageResult(total,hotelDocs); } }
2.3.优化代码
上面的DSL查询条件进行抽离封装,代码如下:
@Service public class HotelService extends ServiceImpl<HotelMapper, Hotel> implements IHotelService { @Autowired private RestHighLevelClient client; /** * 查询、分页 * @param param */ @Override public PageResult search(RequestParam param) { try { //1.准备request SearchRequest request = new SearchRequest("hotel"); //2.准备DSL buildBaiscQuery(param,request); //2.7.分页 Integer page = param.getPage(); Integer size = param.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); } } /** * 抽离构建DSL条件的代码封装成方法 * @param param 前端参数对象 * @param request */ private void buildBaiscQuery(RequestParam param,SearchRequest request){ //2.1.BooleanQuery BoolQueryBuilder boolQuery = QueryBuilders.boolQuery(); //2.2.关键字搜索 String key = param.getKey(); //健壮性判断,如果前端传递的参数为null或者为空字符串,就进行全文检索,else则是按照条件搜索 if (key == null || "".equals(key)) { boolQuery.must(QueryBuilders.matchAllQuery()); } { boolQuery.must(QueryBuilders.matchQuery("all", param.getKey())); } //2.3.条件:城市 String city = param.getCity(); if(city != null && !"".equals(city)){ boolQuery.filter(QueryBuilders.termQuery("city",city)); } //2.4.条件:品牌 String brand = param.getBrand(); if(brand != null && !"".equals(brand)){ boolQuery.filter(QueryBuilders.termQuery("brand",brand)); } //2.5.条件:星级 String starName = param.getStarName(); if(starName != null && !"".equals(starName)){ boolQuery.filter(QueryBuilders.termQuery("starName",starName)); } //2.6.条件:价格 Integer maxPrice = param.getMaxPrice(); Integer minPrice = param.getMinPrice(); if(maxPrice != null && minPrice != null){ boolQuery.filter(QueryBuilders.rangeQuery("price").gte(minPrice).lte(maxPrice)); } //添加boolQuery request.source().query(boolQuery); } /** * 解析响应 * @param response SearchRequest对象 */ private PageResult handleResponse(SearchResponse response){ //4.解析响应 SearchHits searchHits = response.getHits(); //4.1.获取总条数 long total = searchHits.getTotalHits().value; //4.2.文档数组 SearchHit[] hits = searchHits.getHits(); ArrayList<HotelDoc> hotelDocs = new ArrayList<>(); //4.3.遍历 for (SearchHit hit : hits) { //获取文档source String json = hit.getSourceAsString(); //反序列化 HotelDoc hotelDoc = JSON.parseObject(json, HotelDoc.class); //添加到 集合 hotelDocs.add(hotelDoc); //System.out.println("hotelDoc = "+hotelDoc); } //封装成PageResult对象 return new PageResult(total,hotelDocs); } }
2.4.测试
重启项目,按照添加筛选查询,发现已经可以实现,如下:
三、周边的酒店
案例3:我附近的酒店,前端页面点击定位后,会将你所在的位置发送到后台,如下:
发送接口信息如下:
要根据上面的坐标,将酒店结果按照到这个点的距离升序排序。实现思路如下:
- 修改RequestParams参数,接收location字段
- 修改search方法业务逻辑,如果location有值,添加根据geo_distance排序的功能
距离排序
距离排序与普通字段排序有所差异,API如下:
3.1.添加字段
在实体类RequestParam,添加字段location字段,接受前端传递的经纬度
@Data @AllArgsConstructor @NoArgsConstructor public class RequestParam{ private String key; private Integer page; private Integer size; private String sortBy; private String brand; private String starName; private String city; private Integer minPrice; private Integer maxPrice; //经纬度 private String location; }
3.2.修改实现类sercher方法:添加排序的代码
添加排序代码如下:
/** * 查询、分页 * @param param */ @Override public PageResult search(RequestParam param) { try { //1.准备request SearchRequest request = new SearchRequest("hotel"); //2.准备DSL buildBaiscQuery(param,request); //2.7.分页 Integer page = param.getPage(); Integer size = param.getSize(); request.source().from((page - 1) * size).size(size); //2.8 排序 String location = param.getLocation(); if(location != null && !location.equals("")){ request.source().sort(SortBuilders .geoDistanceSort("location", new GeoPoint(location)) //指定排序的字段和原点位置 .order(SortOrder.ASC) //升序排序 .unit(DistanceUnit.KILOMETERS) //单位km ); } //3.发送请求,得到响应 SearchResponse response = client.search(request, RequestOptions.DEFAULT); //4.解析响应 return handleResponse(response); } catch (IOException e) { throw new RuntimeException(e); } }
3.3.排序回显距离信息实现
如下图:
需要把返回的距离定位距离通过 HotelDoc 对象返回,所以这里需要修改HotelDoc添加一个字段保存该值,
修改响应解析代码,获取距离信息,然后返回,如下:
/** * 解析响应 * @param response SearchRequest对象 */ private PageResult handleResponse(SearchResponse response){ //4.解析响应 SearchHits searchHits = response.getHits(); //4.1.获取总条数 long total = searchHits.getTotalHits().value; //4.2.文档数组 SearchHit[] hits = searchHits.getHits(); ArrayList<HotelDoc> hotelDocs = new ArrayList<>(); //4.3.遍历 for (SearchHit hit : hits) { //获取文档source String json = hit.getSourceAsString(); //反序列化 HotelDoc hotelDoc = JSON.parseObject(json, HotelDoc.class); //获取排序值 Object[] sortValues = hit.getSortValues(); //判断是否为空 if(sortValues.length > 0){ Object sortValue = sortValues[0]; //保存给返回的对象 hotelDoc.setDistance(sortValue); } //添加到 集合 hotelDocs.add(hotelDoc); //System.out.println("hotelDoc = "+hotelDoc); } //封装成PageResult对象 return new PageResult(total,hotelDocs); }
3.4.测试
重启项目,然后点击,获取当前位置,左侧会显示根据距离升序排序查询出来的酒店信息,如下:
四、酒店竞价排名
案例4:让指定的酒店在搜索结果中排名置顶
我们给需要置顶的酒店文档添加一个标记。然后利用function score给带有标记的文档增加权重。
实现步骤分析:
- 给HotelDoc类添加isAD字段,Boolean类型
- 挑选几个你喜欢的酒店,给它的文档数据添加isAD字段,值为true
- 修改search方法,添加function score功能,给isAD值为true的酒店增加权重
组合查询-function score
Function Score查询可以控制文档的相关性算分,使用方式如下:
4.1.步骤一:给HotelDoc类添加isAD字段,Boolean类型
修改后如下:
4.2.步骤二:挑选几个你喜欢的酒店,给它的文档数据添加isAD字段,值为true
根据文档ID修改酒店信息,添加isAD设置为true,DSL语法如下
# 添加isAD字段 POST /hotel/_update/434082 { "doc":{ "isAD": true } } # 添加isAD字段 POST /hotel/_update/432335 { "doc":{ "isAD": true } } # 添加isAD字段 POST /hotel/_update/60487 { "doc":{ "isAD": true } } # 添加isAD字段 POST /hotel/_update/2359697 { "doc":{ "isAD": true } }
4.3.修改HotelService中对于构建DSL查询代码,添加functionscore功能,给isAD值为true的酒店增加权重
就是在buildBaiscQuery中添加即可
/** * 抽离构建DSL条件的代码封装成方法 * @param param 前端参数对象 * @param request */ private void buildBaiscQuery(RequestParam param,SearchRequest request){ //2.1.BooleanQuery BoolQueryBuilder boolQuery = QueryBuilders.boolQuery(); //2.2.关键字搜索 String key = param.getKey(); //健壮性判断,如果前端传递的参数为null或者为空字符串,就进行全文检索,else则是按照条件搜索 if (key == null || "".equals(key)) { boolQuery.must(QueryBuilders.matchAllQuery()); }else { boolQuery.must(QueryBuilders.matchQuery("all", param.getKey())); } //2.3.条件:城市 String city = param.getCity(); if(city != null && !"".equals(city)){ boolQuery.filter(QueryBuilders.termQuery("city",city)); } //2.4.条件:品牌 String brand = param.getBrand(); if(brand != null && !"".equals(brand)){ boolQuery.filter(QueryBuilders.termQuery("brand",brand)); } //2.5.条件:星级 String starName = param.getStarName(); if(starName != null && !"".equals(starName)){ boolQuery.filter(QueryBuilders.termQuery("starName",starName)); } //2.6.条件:价格 Integer maxPrice = param.getMaxPrice(); Integer minPrice = param.getMinPrice(); if(maxPrice != null && minPrice != null){ boolQuery.filter(QueryBuilders.rangeQuery("price").gte(minPrice).lte(maxPrice)); } //TODO 控制算分,先传入boolQuery FunctionScoreQueryBuilder functionScoreQuery = QueryBuilders.functionScoreQuery(boolQuery, new FunctionScoreQueryBuilder.FilterFunctionBuilder[]{ // 一个function score元素 new FunctionScoreQueryBuilder.FilterFunctionBuilder( //过滤条件 QueryBuilders.termQuery("isAD", true), //算分函数 ScoreFunctionBuilders.weightFactorFunction(10) ) }); //添加boolQuery request.source().query(functionScoreQuery); }
4.4.测试
重启项目,搜索上海的酒店,就会查询出来的信息,之前增加的权重的会显示在最前面,如下: