分布式搜索--ElasticSearch 旅游网站案例实战(四)

137 阅读6分钟

一、酒店搜索和分页

image.png

这个请求的信息如下:

  • 请求方式:POST

  • 请求路径:/hotel/list

  • 请求参数:JSON对象,包含4个字段:

    • key:搜索关键字

    • page:页码

    • size:每页大小

    • sortBy:排序,目前暂不实现

  • 返回值:分页查询,需要返回分页结果PageResult,包含两个属性:

    • total:总条数
    • List<HotelDoc>:当前页的数据

因此,我们实现业务的流程如下:

  1. 定义实体类,接收请求参数的JSON对象,返回响应JSON对象
  2. 编写controller,接收页面的请求
  3. 编写业务实现,利用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);
    }
}

二、酒店条件过滤

我们可以在页面搜索框下面,看到一些过滤项:

image.png

过滤项发送的请求与搜索相同,但是传递的参数却多了几个:

  • 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;
}

三、我附近的酒店

在酒店列表页的右侧,有一个小地图,点击地图的定位按钮,地图会找到你所在的位置:

image.png

并且,在前端会通过搜索接口发起查询请求,将坐标发送到服务端:

image.png

我们要做的事情就是基于这个location坐标,然后按照距离对周围酒店排序并显示距离。思路如下:

  • 修改RequestParams参数,接收location字段
  • 修改hotdoc对象,返回distance距离信息
  • 修改search方法业务逻辑,如果location有值,添加根据geo_distance排序的功能
  • 排序完成后,页面还要获取我附近每个酒店的具体距离值,这个值在es响应结果中是独立的:

image.png 可以看到距离信息存在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实体类

image.png

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);
}