ES旅游案例(完整的关键字搜索、条件过滤、附近酒店距离、公告竞价排位案例)

ES旅游案例

下面,我们通过ES旅游的案例来实战演练下之前学习的知识。

我们实现四部分功能:

  • 酒店搜索和分页
  • 酒店结果过滤
  • 我周边的酒店
  • 酒店竞价排名

启动我们提供的hotel-demo项目,其默认端口是8089,访问http://localhost:8090,就能看到项目页面了:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GDMt90cB-1654329626702)(images/image-20220604131153236.png)]

由于页面内容的确实,所以现在要使用postman软件进行接口的测试,访问的数据页数一样的

点击搜索按钮查看页面的请求数据

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kR4lRlfs-1654329626703)(images/image-20220604131328458.png)]

由此可以知道,我们这个请求的信息如下:

  • 请求方式:POST
  • 请求路径:/hotel/list
  • 请求参数:JSON对象,包含4个字段:
    • key:搜索关键字
    • page:页码
    • size:每页大小
    • sortBy:排序,目前暂不实现
  • 返回值:分页查询,需要返回分页结果PageResult,包含两个属性:
    • total:总条数
    • List<HotelDoc>:当前页的数据

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

  • 步骤一:定义实体类,接收请求参数的JSON对象
  • 步骤二:编写controller,接收页面的请求
  • 步骤三:编写业务实现,利用RestHighLevelClient实现搜索、分页

1、定义实体类

实体类有两个,一个是前端的请求参数实体,一个是服务端应该返回的响应结果实体。

1)请求参数

前端请求的json结构如下:

{
    
    
    "key": "搜索关键字",
    "page": 1,
    "size": 3,
    "sortBy": "default"
}

因此,我们在cn.itcast.hotel.pojo包下定义一个实体类:

package cn.itcast.hotel.pojo;

import lombok.Data;

@Data
public class RequestParams {
    
    
    private String key;
    private Integer page;
    private Integer size;
    private String sortBy;
}

2)返回值

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

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

因此,我们在cn.itcast.hotel.pojo中定义返回结果:

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

定义一个HotelController,声明查询接口,满足下列要求:

  • 请求方式:Post
  • 请求路径:/hotel/list
  • 请求参数:对象,类型为RequestParam
  • 返回值:PageResult,包含两个属性
    • Long total:总条数
    • List<HotelDoc> hotels:酒店数据

因此,我们在cn.itcast.hotel.web中定义HotelController:

package cn.itcast.hotel.web;

import cn.itcast.hotel.pojo.PageResult;
import cn.itcast.hotel.pojo.RequestParams;
import cn.itcast.hotel.service.IHotelService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * 项目名称:hotel-demo
 * 描述:请求控制器
 *
 * @author zhong
 * @date 2022-06-04 13:20
 */
@RestController
@RequestMapping("/hotel")
public class HotelController {
    
    
    /**
     * 注入业务层
     */
    @Autowired
    private IHotelService hotelService;

    /**
     * 请求查询分页信息并返回
     * @return
     */
    @PostMapping("/list")
    public PageResult search(@RequestBody RequestParams params){
    
    
        return hotelService.search(params);
    }
}

3、创建业务层以及实现方式

我们在controller调用了IHotelService,并没有实现该方法,因此下面我们就在IHotelService中定义方法,并且去实现业务逻辑。

1)在cn.itcast.hotel.service中的IHotelService接口中定义一个方法:

快捷键介绍:创建接口后,需要创建接口的实现类,可以按住键盘的Ctrl+Alt+B进行跳转到实现类上

/**
 * 根据关键字搜索酒店信息
 * @param params 请求参数对象,包含用户输入的关键字 
 * @return 酒店文档列表
 */
PageResult search(RequestParams params);

2)实现搜索业务,肯定离不开RestHighLevelClient,我们需要把它注册到Spring中作为一个Bean。在cn.itcast.hotel中的HotelDemoApplication中声明这个Bean:

@Bean
public RestHighLevelClient client(){
    
    
    return  new RestHighLevelClient(RestClient.builder(
        HttpHost.create("http://192.168.150.101:9200")
    ));
}

3)在cn.itcast.hotel.service.impl中的HotelService中实现search方法:

package cn.itcast.hotel.service.impl;

import cn.itcast.hotel.mapper.HotelMapper;
import cn.itcast.hotel.pojo.Hotel;
import cn.itcast.hotel.pojo.HotelDoc;
import cn.itcast.hotel.pojo.PageResult;
import cn.itcast.hotel.pojo.RequestParams;
import cn.itcast.hotel.service.IHotelService;
import com.alibaba.fastjson.JSON;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.elasticsearch.action.search.SearchRequest;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.search.SearchHit;
import org.elasticsearch.search.SearchHits;
import org.elasticsearch.search.fetch.subphase.highlight.HighlightField;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

@Service
public class HotelService extends ServiceImpl<HotelMapper, Hotel> implements IHotelService {
    
    
    /**
     * 注入
     */
    @Autowired
    RestHighLevelClient client;

    /**
     *
     * @param params
     * @return
     */
    @Override
    public PageResult search(RequestParams params) {
    
    
        try {
    
    
            // 1、准备requeue
            SearchRequest request = new SearchRequest("hotel");
            // 2、准备DSL
            // 2.1、关键字搜索
            String key = params.getKey();
            if(key==null || "".equals(key)){
    
    
                request.source().query(QueryBuilders.matchAllQuery());
            }else{
    
    
                request.source().query(QueryBuilders.matchQuery("all",key));
            }
            // 2.2、分页搜索
            Integer page = params.getPage();
            Integer size = params.getSize();
            request.source().from((page -1)*size).size(size);

            // 3、发送请求,得到响应
            SearchResponse response = client.search(request, RequestOptions.DEFAULT);
            // 4、解析响应
            return extracted(response);
        } catch (IOException e) {
    
    
            throw new RuntimeException();
        }
    }

    /**
     * 封装的提统一使用的重构步骤
     * @param search
     */
    private PageResult extracted(SearchResponse search) {
    
    
        // 4、解析响应数据
        SearchHits hits = search.getHits();
        // 4.1、获取总条数
        long value = hits.getTotalHits().value;
        // 4.2、获取文档数组
        SearchHit[] hitsArray = hits.getHits();
        List<HotelDoc> hotelDocs = new ArrayList<>();
        // 4.3、遍历数组
        for (SearchHit documentFields : hitsArray) {
    
    
            // 获取文档
            String sourceAsString = documentFields.getSourceAsString();
            // 将文档放序列化为json对象
            HotelDoc hotelDoc = JSON.parseObject(sourceAsString, HotelDoc.class);
            hotelDocs.add(hotelDoc);
        }
        return new PageResult(value,hotelDocs);
    }
}

4、酒店结果过滤

需求:添加品牌、城市、星级、价格等过滤功能

包含的过滤条件有:

  • brand:品牌值
  • city:城市
  • minPrice~maxPrice:价格范围
  • starName:星级

我们需要做两件事情:

  • 修改请求参数的对象RequestParams,接收上述参数
  • 修改业务逻辑,在搜索条件之外,添加一些过滤条件

4.1、添加实体类属性

修改在cn.itcast.hotel.pojo包下的实体类RequestParams:

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

4.2、修改搜索业务

在HotelService的search方法中,只有一个地方需要修改:requet.source().query( … )其中的查询条件。

在之前的业务中,只有match查询,根据关键字搜索,现在要添加条件过滤,包括:

  • 品牌过滤:是keyword类型,用term查询
  • 星级过滤:是keyword类型,用term查询
  • 价格过滤:是数值类型,用range查询
  • 城市过滤:是keyword类型,用term查询

多个查询条件组合,肯定是boolean查询来组合:

  • 关键字搜索放到must中,参与算分
  • 其它过滤条件放到filter中,不参与算分

因为条件构建的逻辑比较复杂,这里先封装为一个函数:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Xvwk9O6d-1654329626704)(images/image-20220604144139791.png)]

buildBasicQuery的代码如下:

/**
 * 拼接筛选条件
 * @param params
 * @return
 */
@Override
public PageResult search(RequestParams params) {
    
    
    try {
    
    
        // 1、准备requeue
        SearchRequest request = new SearchRequest("hotel");
        // 2、准备DSL
        buildBaicQuery(params, request);

        // 2.2、分页搜索
        int page = params.getPage();
        int size = params.getSize();
        request.source().from((page -1)*size).size(size);

        // 3、发送请求,得到响应
        SearchResponse response = client.search(request, RequestOptions.DEFAULT);
        // 4、解析响应
        return extracted(response);
    } catch (IOException e) {
    
    
        throw new RuntimeException();
    }
}

/**
 * 重构筛选条件
 * @param params
 * @param request
 */
private void buildBaicQuery(RequestParams params, SearchRequest request) {
    
    
    // 将查询的条件较多,所以封装在一起
    BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
    // 2.1、关键字搜索
    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.matchQuery("city", params.getCity()));
    }
    // 匹配条件
    if(params.getBrand() != null && !params.getBrand().equals("")){
    
    
        boolQuery.filter(QueryBuilders.matchQuery("brand", params.getBrand()));
    }
    // 星级条件
    if(params.getStarName() != null && !params.getStarName().equals("")){
    
    
        boolQuery.filter(QueryBuilders.matchQuery("starName", params.getStarName()));
    }
    // 价格判断
    if(params.getMinPrice() != null && !params.getMaxPrice().equals("")){
    
    
        // 大于等于和小于等于
        boolQuery.filter(QueryBuilders.rangeQuery("price").gte(params.getMinPrice()).lte(params.getMaxPrice()));
    }

    request.source().query(boolQuery);
}

使用postman软件进行测试

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6DINiZfJ-1654329626705)(images/image-20220604150609832.png)]

5、附近酒店查询

需求:我附近的酒店

酒店信息的坐标key是location

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

  • 修改RequestParams参数,接收location字段
  • 修改search方法业务逻辑,如果location有值,添加根据geo_distance排序的功能

5.1、修改实体类

在返回封装的实体类上进行一个坐标距离的添加

// 我当前的地理坐标
private String location;

5.2、距离排序API

我们以前学习过排序功能,包括两种:

  • 普通字段排序
  • 地理坐标排序

我们只讲了普通字段排序对应的java写法。地理坐标排序只学过DSL语法,如下:

GET /indexName/_search
{
    
    
  "query": {
    
    
    "match_all": {
    
    }
  },
  "sort": [
    {
    
    
      "price": "asc"  
    },
    {
    
    
      "_geo_distance" : {
    
    
          "FIELD" : "纬度,经度",
          "order" : "asc",
          "unit" : "km"
      }
    }
  ]
}

5.3、添加距离排序

// 坐标范围排序
String location = params.getLocation();
if(location != null && !location.equals("")){
    
    
    request.source().sort(SortBuilders.geoDistanceSort("location",new GeoPoint(location))
            .order(SortOrder.ASC).unit(DistanceUnit.KILOMETERS));

}

5.4、完善解析数据,回显酒店距离

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-icu4uGqa-1654329626706)(images/image-20220604153748352.png)]

6、酒店竞价排名

需求:让指定的酒店在搜索结果中排名置顶

我们之前学习过的function_score查询可以影响算分,算分高了,自然排名也就高了。而function_score包含3个要素:

  • 过滤条件:哪些文档要加分
  • 算分函数:如何计算function score
  • 加权方式:function score 与 query score如何运算

这里的需求是:让指定酒店排名靠前。因此我们需要给这些酒店添加一个标记,这样在过滤条件中就可以根据这个标记来判断,是否要提高算分

比如,我们给酒店添加一个字段:isAD,Boolean类型:

  • true:是广告
  • false:不是广告

这样function_score包含3个要素就很好确定了:

  • 过滤条件:判断isAD 是否为true
  • 算分函数:我们可以用最简单暴力的weight,固定加权值
  • 加权方式:可以用默认的相乘,大大提高算分

因此,业务的实现步骤包括:

  1. 给HotelDoc类添加isAD字段,Boolean类型

  2. 挑选几个你喜欢的酒店,给它的文档数据添加isAD字段,值为true

  3. 修改search方法,添加function score功能,给isAD值为true的酒店增加权重

6.1、修改HotelDoc实体

添加多一个isAD布尔字段

private Boolean isAD;

6.3、在Dev Tools编写页面手动的添加这几个字段的属性为公告的

添加广告标记

# 手动添加公告字段
POST /hotel/_update/1514269829
{
    
    
  "doc":{
    
    
    "isAD":true
  }
}

POST /hotel/_update/541619
{
    
    
  "doc":{
    
    
    "isAD":true
  }
}

POST /hotel/_update/485775
{
    
    
  "doc":{
    
    
    "isAD":true
  }
}

6.4、修改原有的查询方式

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XEbfTIvV-1654329626707)(images/image-20220604155923236.png)]

java对应的api如下

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-G7No1aZJ-1654329626708)(images/image-20220604155942976.png)]

添加一个新的算分方法

我们可以将之前写的boolean查询作为原始查询条件放到query中,接下来就是添加过滤条件算分函数加权模式了。所以原来的代码依然可以沿用。

// 2、算分控制
FunctionScoreQueryBuilder functionScoreQueryBuilder =
        QueryBuilders.functionScoreQuery(
                // 原始算分方法
                boolQuery,
                // function score的数组
                new FunctionScoreQueryBuilder.FilterFunctionBuilder[]{
    
    
                        // 其中一个function score原始
                   new FunctionScoreQueryBuilder.FilterFunctionBuilder(
                           // 过滤条件
                           QueryBuilders.termQuery("isAD",true),
                           // 算分函数
                           ScoreFunctionBuilders.weightFactorFunction(10)
                   )
                }
        );

完整的业务代码

package cn.itcast.hotel.service.impl;

import cn.itcast.hotel.mapper.HotelMapper;
import cn.itcast.hotel.pojo.Hotel;
import cn.itcast.hotel.pojo.HotelDoc;
import cn.itcast.hotel.pojo.PageResult;
import cn.itcast.hotel.pojo.RequestParams;
import cn.itcast.hotel.service.IHotelService;
import com.alibaba.fastjson.JSON;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.elasticsearch.action.search.SearchRequest;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.common.geo.GeoPoint;
import org.elasticsearch.common.unit.DistanceUnit;
import org.elasticsearch.index.query.BoolQueryBuilder;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.index.query.functionscore.FunctionScoreQueryBuilder;
import org.elasticsearch.index.query.functionscore.ScoreFunctionBuilders;
import org.elasticsearch.search.SearchHit;
import org.elasticsearch.search.SearchHits;
import org.elasticsearch.search.sort.SortBuilders;
import org.elasticsearch.search.sort.SortOrder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

@Service
public class HotelService extends ServiceImpl<HotelMapper, Hotel> implements IHotelService {
    
    
    /**
     * 注入
     */
    @Autowired
    RestHighLevelClient client;

    /**
     * 拼接筛选条件
     *
     * @param params
     * @return
     */
    @Override
    public PageResult search(RequestParams params) {
    
    
        try {
    
    
            // 1、准备requeue
            SearchRequest request = new SearchRequest("hotel");
            // 2、准备DSL
            buildBaicQuery(params, request);

            // 2.2、分页搜索
            int page = params.getPage();
            int size = params.getSize();
            request.source().from((page - 1) * size).size(size);

            // 坐标范围排序
            String location = params.getLocation();
            if (location != null && !location.equals("")) {
    
    
                request.source().sort(SortBuilders.geoDistanceSort("location", new GeoPoint(location))
                        .order(SortOrder.ASC).unit(DistanceUnit.KILOMETERS));

            }

            // 3、发送请求,得到响应
            SearchResponse response = client.search(request, RequestOptions.DEFAULT);
            // 4、解析响应
            return extracted(response);
        } catch (IOException e) {
    
    
            throw new RuntimeException();
        }
    }

    /**
     * 重构筛选条件
     *
     * @param params
     * @param request
     */
    private void buildBaicQuery(RequestParams params, SearchRequest request) {
    
    
        // 将查询的条件较多,所以封装在一起
        BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
        // 2.1、关键字搜索
        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.matchQuery("city", params.getCity()));
        }
        // 匹配条件
        if (params.getBrand() != null && !params.getBrand().equals("")) {
    
    
            boolQuery.filter(QueryBuilders.matchQuery("brand", params.getBrand()));
        }
        // 星级条件
        if (params.getStarName() != null && !params.getStarName().equals("")) {
    
    
            boolQuery.filter(QueryBuilders.matchQuery("starName", params.getStarName()));
        }
        // 价格判断
        if (params.getMinPrice() != null && !params.getMaxPrice().equals("")) {
    
    
            // 大于等于和小于等于
            boolQuery.filter(QueryBuilders.rangeQuery("price").gte(params.getMinPrice()).lte(params.getMaxPrice()));
        }

        // 2、算分控制
        FunctionScoreQueryBuilder functionScoreQueryBuilder =
                QueryBuilders.functionScoreQuery(
                        // 原始算分方法
                        boolQuery,
                        // function score的数组
                        new FunctionScoreQueryBuilder.FilterFunctionBuilder[]{
    
    
                                // 其中一个function score原始
                           new FunctionScoreQueryBuilder.FilterFunctionBuilder(
                                   // 过滤条件
                                   QueryBuilders.termQuery("isAD",true),
                                   // 算分函数
                                   ScoreFunctionBuilders.weightFactorFunction(10)
                           )
                        }
                );

        request.source().query(boolQuery);
    }

    /**
     * 封装的提统一使用的重构步骤
     *
     * @param search
     */
    private PageResult extracted(SearchResponse search) {
    
    
        // 4、解析响应数据
        SearchHits hits = search.getHits();
        // 4.1、获取总条数
        long value = hits.getTotalHits().value;
        // 4.2、获取文档数组
        SearchHit[] hitsArray = hits.getHits();
        List<HotelDoc> hotelDocs = new ArrayList<>();
        // 4.3、遍历数组
        for (SearchHit documentFields : hitsArray) {
    
    
            // 获取文档
            String sourceAsString = documentFields.getSourceAsString();
            // 将文档放序列化为json对象
            HotelDoc hotelDoc = JSON.parseObject(sourceAsString, HotelDoc.class);
            // 获取到距离排序的值
            Object[] rawSortValues = documentFields.getSortValues();
            if (rawSortValues.length > 0) {
    
    
                Object sortValue = rawSortValues[0];
                // 添加距离
                hotelDoc.setDistance(sortValue);
                System.out.println("查询返回的公里数:" + sortValue);
            }
            hotelDocs.add(hotelDoc);
        }
        return new PageResult(value, hotelDocs);
    }
}

猜你喜欢

转载自blog.csdn.net/baidu_39378193/article/details/125122497
今日推荐