目录
1.1.4、实现 IHotelService 接口的 search 方法
1.4.3、修改查询,使用 function score 进行算分排序
在搜索框中输入 “如家酒店”,之后点击搜索,可以看到发送了如下请求,参数如下

调到请求头可以看到,请求的格式为 JSON,如下图

那么需求就是当用户点击搜索框后,后端进行处理,然后返回搜索到的数据总数,以及酒店数据列表(返回格式为 JSON).
Ps:响应的数据和格式是提前约定好的.

定义实体类,用于接收前端请求以及返回响应.
请求实体类如下:
- @Data
- public class RequestParams {
-
- private String key;
- private Integer page;
- private Integer size;
- private String sortBy;
-
- }
响应实体类如下:
- @Data
- public class PageResult {
-
- private Long total;
- private List
hotels; -
- public PageResult() {
- }
-
- public PageResult(Long total, List
hotels) { - this.total = total;
- this.hotels = hotels;
- }
-
- }
Controller 这边负责接收前端传入的 JSON 参数,然后调用接口 IHotelService 的 search 方法.
- @RestController
- @RequestMapping("/hotel")
- public class HotelController {
-
- @Autowired
- private IHotelService hotelService;
-
- @RequestMapping("/list")
- public PageResult search(@RequestBody RequestParams params) {
- return hotelService.search(params);
- }
-
- }
IHotelService 接口如下:
-
- public interface IHotelService extends IService
{ - PageResult search(RequestParams params);
- }
将 ElasticSearch 的高级客户端注入到 Spring 容器中,方便后续通过 es 实现搜索功能.
- @Component
- public class ESComponent {
-
- @Bean
- public RestHighLevelClient restHighLevelClient() {
- RestHighLevelClient client = new RestHighLevelClient(RestClient.builder(
- HttpHost.create("http://140.143.166.138:9200")
- ));
- return client;
- }
-
- }
这里的实现思路就和上一章中讲到 JavaRestClient 文档操作步骤一样~
代码如下:
- @Service
- public class HotelService extends ServiceImpl
implements IHotelService { -
- @Autowired
- private RestHighLevelClient client;
-
- @Autowired
- private ObjectMapper objectMapper;
-
- @Override
- public PageResult search(RequestParams params) {
- try {
- //1.创建请求
- SearchRequest request = new SearchRequest("hotel");
- //2.准备参数
- // 1) 查询
- String searchContent = params.getKey();
- if(!StringUtils.hasLength(searchContent)) {
- request.source().query(QueryBuilders.matchAllQuery());
- } else {
- request.source().query(QueryBuilders.matchQuery("all", searchContent));
- }
- // 2) 分页
- Integer page = params.getPage();
- Integer size = params.getSize();
- if(page == null || size == null) {
- throw new IOException("分页数据不能为空!");
- }
- request.source().from((page - 1) * size).size(size);
- //3.发送请求,接收响应
- SearchResponse response = client.search(request, RequestOptions.DEFAULT);
- //4.解析响应
- return handlerResponse(response);
- } catch (IOException e) {
- System.out.println("[HotelService] 搜索失败!");
- e.printStackTrace();
- return null;
- }
- }
-
- private PageResult handlerResponse(SearchResponse response) throws JsonProcessingException {
- //1.解析结果
- SearchHits hits = response.getHits();
- long total = hits.getTotalHits().value;
- SearchHit[] hits1 = hits.getHits();
- List
hotelDocList = new ArrayList<>(); - for(SearchHit searchHit : hits1) {
- //获取source
- String json = searchHit.getSourceAsString();
- hotelDocList.add(objectMapper.readValue(json, HotelDoc.class));
- }
- return new PageResult(total, hotelDocList);
- }
-
- }
在搜索框中输入 “如家” 关键字,点击搜索,下方展示有关 “如家” 关键词的酒店数据.

给搜索增加如下图过滤条件后,点击搜索,展示过滤后的数据.

因此请求中需要增加 5 个参数:
后端根据请求计算响应数据:搜索出的数据总数、酒店数据列表.
- @Data
- public class RequestParams {
-
- 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;
-
- }
根据需求所述,这里可以使用 复合查询(BoolQuery).
需要过滤的字段有 city、brand、starName、price. 前三个字段都有一个特点——不可分词,都是 keyword 类型,因此这里可以使用 term 精确查询. 而 price 这里就是用 range 范围查询即可.
Ps:这里为了提高代码的可读性,我这里将查询过滤逻辑封装到一个方法中了.
- private BoolQueryBuilder getHandlerBoolQueryBuilder(RequestParams params) {
- //使用 boolean 查询
- BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();
- // 1) 查询
- String searchContent = params.getKey();
- if(!StringUtils.hasLength(searchContent)) {
- boolQueryBuilder.must(QueryBuilders.matchAllQuery());
- } else {
- boolQueryBuilder.must(QueryBuilders.matchQuery("all", searchContent));
- }
- // 2) 城市过滤
- String city = params.getCity();
- if(StringUtils.hasLength(city)) {
- boolQueryBuilder.filter(QueryBuilders.termQuery("city", city));
- }
- // 3) 品牌过滤
- String brand = params.getBrand();
- if(StringUtils.hasLength(brand)) {
- boolQueryBuilder.filter(QueryBuilders.termQuery("brand", brand));
- }
- // 4) 星级过滤
- String star = params.getStarName();
- if(StringUtils.hasLength(star)) {
- boolQueryBuilder.filter(QueryBuilders.termQuery("starName", star));
- }
- // 5) 价格范围过滤
- if(params.getMinPrice() != null && params.getMaxPrice() != null) {
- boolQueryBuilder.filter(QueryBuilders
- .rangeQuery("price").gte(params.getMinPrice()).lte(params.getMaxPrice()));
- }
- return boolQueryBuilder;
- }
-
- @Override
- public PageResult search(RequestParams params) {
- try {
- //1.创建请求
- SearchRequest request = new SearchRequest("hotel");
- //2.准备参数
- BoolQueryBuilder boolQuery = getHandlerBoolQueryBuilder(params);
- request.source().query(boolQuery);
- //3.分页
- Integer page = params.getPage();
- Integer size = params.getSize();
- if(page == null || size == null) {
- throw new IOException("分页数据不能为空!");
- }
- request.source().from((page - 1) * size).size(size);
- //4.发送请求,接收响应
- SearchResponse response = client.search(request, RequestOptions.DEFAULT);
- //5.解析响应
- return handlerResponse(response);
- } catch (IOException e) {
- System.out.println("[HotelService] 搜索失败!");
- e.printStackTrace();
- return null;
- }
- }
当用户点击小地图上的定位点时,可以自动定位到自己的位置,然后显示附近的酒店列表,并展示出距离.

点击定位按钮以后前端会返回当前你所在位置的经纬度信息,如下

那么请求中就需要增加一个 经纬度 参数.
这里只需要添加一个 String 参数即可,用来接收经纬度信息.
- @Data
- public class RequestParams {
-
- 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;
-
- }
需要给 search 方法添加按照距离排序的逻辑.
- //4.根据距离排序
- request.source().sort(SortBuilders.geoDistanceSort("location",
- new GeoPoint(params.getLocation()))
- .order(SortOrder.ASC)
- .unit(DistanceUnit.KILOMETERS));
这里如果不太清楚,可以对比一下 DSL 语句.

从需求分析中可以看出,酒店信息中还需要显示 “距离您 xx km” 的信息,因此,这里我们还需要解析出响应中的排序后的 “目的地与你当前位置的距离” 这个属性,如下:

因此这里还需要给 HotelDoc 添加一个 Object 属性,表示距离.
- @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;
-
- 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();
- }
- }
解析方式代码如下:
- private PageResult handlerResponse(SearchResponse response) throws JsonProcessingException {
- //1.解析结果
- SearchHits hits = response.getHits();
- long total = hits.getTotalHits().value;
- SearchHit[] hits1 = hits.getHits();
- List
hotelDocList = new ArrayList<>(); - for(SearchHit searchHit : hits1) {
- //获取source
- String json = searchHit.getSourceAsString();
- HotelDoc hotelDoc = objectMapper.readValue(json, HotelDoc.class);
- Object[] sortValues = searchHit.getSortValues();
- //通过 getSortValues 得到的是一个 Object 类型数组,因为有可能是根据多个条件排序(价格、评价、距离...)
- //获取的下标,就要看你先给谁排序,谁就是 0 下标(以此类推)
- if(sortValues != null && sortValues.length > 0) {
- hotelDoc.setDistance(sortValues[0]);
- }
- hotelDocList.add(hotelDoc);
- }
- return new PageResult(total, hotelDocList);
- }
Ps:通过 getSortValues 得到的是一个 Object 类型数组,因为有可能是根据多个条件排序(价格、评价、距离...) 获取的下标,就要看你先给谁排序,谁就是 0 下标(以此类推).
1.3.5、演示效果
点击定位点后,会根据与你的距离进行升序排序,然后通过分页显示出对应酒店数据.
Ps:下图中的距离过远,是因为我没有像数据库中添加我当前位置附近的酒店(都是跨省的).

用户点击搜索之后,会优先将广告数据(特殊标记的酒店信息)置顶.
因此需要在 HotelDoc 中添加一个字段,用来标记当前酒店信息是否存在是广告,然后后端对广告信息多分配一些算分权重,让广告数据置顶.
在 HotelDoc 中添加一个 Boolean 类型的字段,用来标记当前酒店信息是否存在是广告.
- @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();
- }
- }
function score 进行查询的过滤条件,就可以使用 term 精确查询 isAD 的值是否 true,如果是,就通过 weight 修改权重(这里没有指定加权模式,因此默认是相乘).
- private void HandlerBoolQueryBuilder(SearchRequest request, RequestParams params) {
- //1.使用 boolean 查询
- BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();
- // 1) 查询
- String searchContent = params.getKey();
- if(!StringUtils.hasLength(searchContent)) {
- boolQueryBuilder.must(QueryBuilders.matchAllQuery());
- } else {
- boolQueryBuilder.must(QueryBuilders.matchQuery("all", searchContent));
- }
- // 2) 城市过滤
- String city = params.getCity();
- if(StringUtils.hasLength(city)) {
- boolQueryBuilder.filter(QueryBuilders.termQuery("city", city));
- }
- // 3) 品牌过滤
- String brand = params.getBrand();
- if(StringUtils.hasLength(brand)) {
- boolQueryBuilder.filter(QueryBuilders.termQuery("brand", brand));
- }
- // 4) 星级过滤
- String star = params.getStarName();
- if(StringUtils.hasLength(star)) {
- boolQueryBuilder.filter(QueryBuilders.termQuery("starName", star));
- }
- // 5) 价格范围过滤
- if(params.getMinPrice() != null && params.getMaxPrice() != null) {
- boolQueryBuilder.filter(QueryBuilders
- .rangeQuery("price").gte(params.getMinPrice()).lte(params.getMaxPrice()));
- }
-
- //2.算分控制
- FunctionScoreQueryBuilder functionScoreQuery = QueryBuilders.functionScoreQuery(
- //原始查询,相关性算分查询
- boolQueryBuilder,
- //function score 的数组
- new FunctionScoreQueryBuilder.FilterFunctionBuilder[]{
- //其中的一个 function score 元素
- new FunctionScoreQueryBuilder.FilterFunctionBuilder(
- //过滤条件
- QueryBuilders.termQuery("isAD", true),
- //算分函数
- ScoreFunctionBuilders.weightFactorFunction(10)
- )
- }
- );
- request.source().query(functionScoreQuery);
- }
这里可以对应着 DSL 语句去看

