ElasticSearch - 基于 “黑马旅游” 案例,实现搜索框、分页、条件过滤、附近酒店、广告置顶功能

目录

一、黑马旅游案例

1.1、实现 搜索框 和 分页 功能

1.1.1、需求分析

a)首先搜索框需求

b)分页需求

1.1.2、定义实体类

1.1.2、定义 controller

1.1.3、注入 RestHighLevelClient

1.1.4、实现 IHotelService 接口的 search 方法

1.1.5、功能展示

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

1.2.1、需求分析

1.2.2、给 RequestParam 添加参数

1.2.3、修改 search 方法

1.3、附近的酒店

1.3.1、需求分析

1.3.2、给 RequestParam 添加参数

1.3.3、修改 search 方法

1.3.4、修改解析方式

1.4、广告置顶(让指定酒店在搜索排名中置顶)

1.4.1、需求分析

1.4.2、HotelDoc 实体类添加属性

1.4.3、修改查询,使用 function score 进行算分排序


一、黑马旅游案例


1.1、实现 搜索框 和 分页 功能

1.1.1、需求分析

a)首先搜索框需求

在搜索框中输入 “如家酒店”,之后点击搜索,可以看到发送了如下请求,参数如下

  • key:搜索框的内容
  • page:页码.
  • size:一页展示多少条数据.
  • sortBy:排序规则(默认按照查询关键词的匹配度,降序排序)

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

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

Ps:响应的数据和格式是提前约定好的.

b)分页需求
点击分页后,也会触发相同的请求,响应格式也一样.

1.1.2、定义实体类

定义实体类,用于接收前端请求以及返回响应.

请求实体类如下:

@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<HotelDoc> hotels;

    public PageResult() {
    }

    public PageResult(Long total, List<HotelDoc> hotels) {
        this.total = total;
        this.hotels = hotels;
    }
    
}

1.1.2、定义 controller

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<Hotel> {
    PageResult search(RequestParams params);
}

1.1.3、注入 RestHighLevelClient

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

}

1.1.4、实现 IHotelService 接口的 search 方法

这里的实现思路就和上一章中讲到 JavaRestClient 文档操作步骤一样~

  1. 创建 search 请求.
  2. 准备请求参数,具体的参数如下
    1. 通过 QueryBuilders 构建 match 查询(如果用户没有输入,直接点击搜索,那么这里构建 match_all 查询所有即可).
    2. 添加分页数据:from(offset 偏移量) 和 size(数据显示条数).
  3. 发送请求,接收响应.
  4. 解析响应,获取查询总数和酒店信息列表.

代码如下:

@Service
public class HotelService extends ServiceImpl<HotelMapper, Hotel> 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<HotelDoc> hotelDocList = new ArrayList<>();
        for(SearchHit searchHit : hits1) {
            //获取source
            String json = searchHit.getSourceAsString();
            hotelDocList.add(objectMapper.readValue(json, HotelDoc.class));
        }
        return new PageResult(total, hotelDocList);
    }

}

1.1.5、功能展示

在搜索框中输入 “如家” 关键字,点击搜索,下方展示有关 “如家” 关键词的酒店数据.

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

1.2.1、需求分析

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

因此请求中需要增加 5 个参数:

  1. city:城市
  2. starName:星级
  3. brand:品牌.
  4. minPrice:价格下限.
  5. maxPrice:价格上限.

后端根据请求计算响应数据:搜索出的数据总数、酒店数据列表.

1.2.2、给 RequestParam 添加参数

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

}

1.2.3、修改 search 方法

根据需求所述,这里可以使用 复合查询(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;
        }
    }

1.3、附近的酒店

1.3.1、需求分析

当用户点击小地图上的定位点时,可以自动定位到自己的位置,然后显示附近的酒店列表,并展示出距离.

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

那么请求中就需要增加一个 经纬度 参数.

1.3.2、给 RequestParam 添加参数

这里只需要添加一个 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;

}

1.3.3、修改 search 方法

需要给 search 方法添加按照距离排序的逻辑.

            //4.根据距离排序
            request.source().sort(SortBuilders.geoDistanceSort("location",
                    new GeoPoint(params.getLocation()))
                    .order(SortOrder.ASC)
                    .unit(DistanceUnit.KILOMETERS));

这里如果不太清楚,可以对比一下 DSL 语句.

1.3.4、修改解析方式

从需求分析中可以看出,酒店信息中还需要显示 “距离您 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<HotelDoc> 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:下图中的距离过远,是因为我没有像数据库中添加我当前位置附近的酒店(都是跨省的). 

1.4、广告置顶(让指定酒店在搜索排名中置顶)

1.4.1、需求分析

用户点击搜索之后,会优先将广告数据(特殊标记的酒店信息)置顶.

因此需要在 HotelDoc 中添加一个字段,用来标记当前酒店信息是否存在是广告,然后后端对广告信息多分配一些算分权重,让广告数据置顶.

1.4.2、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();
    }
}

1.4.3、修改查询,使用 function score 进行算分排序

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 语句去看

猜你喜欢

转载自blog.csdn.net/CYK_byte/article/details/133314299