1 酒店搜索和分页
1.1 课件
1.2 需求分析
- 在项目的首页,有一个大大的搜索框,还有分页按钮
点击搜索按钮,可以看到浏览器控制台发出了请求:
请求参数如下: - 由此可以知道,我们这个请求的信息如下:
- 请求方式:POST
- 请求路径:/hotel/list
- 请求参数:JSON对象,包含4个字段:
- key:搜索关键字
- page:页码
- size:每页大小
- sortBy:排序,目前暂不实现
- 返回值:分页查询,需要返回分页结果PageResult,包含两个属性:
total
:总条数List<HotelDoc>
:当前页的数据
- 因此,我们实现业务的流程如下:
- 步骤一:定义实体类,接收请求参数的JSON对象
- 步骤二:编写controller,接收页面的请求
- 步骤三:编写业务实现,利用RestHighLevelClient实现搜索、分页
1.3 定义实体类
实体类有两个,一个是前端的请求参数实体,一个是服务端应该返回的响应结果实体。
1.3.1 请求参数
- 前端请求的json结构如下:
- {
- "key": "搜索关键字",
- "page": 1,
- "size": 3,
- "sortBy": "default"
- }
- 因此,我们在
com.yppah.hoteldemo.pojo
包下定义一个实体类:- package com.yppah.hoteldemo.pojo;
- import lombok.Data;
- @Data
- public class RequestParams {
- private String key;
- private Integer page;
- private Integer size;
- private String sortBy;
- }
1.3.2 返回值
- 分页查询,需要返回分页结果PageResult,包含两个属性:
total
:总条数List<HotelDoc>
:当前页的数据
- 因此,我们在
com.yppah.hoteldemo.pojo
中定义返回结果:- package com.yppah.hoteldemo.pojo;
- import lombok.Data;
- import java.util.List;
- @Data
- public class PageResult {
- private Long total;
- private List<HotelDoc> hotels;
- }
1.4 定义Controller
-
定义一个HotelController,声明查询接口,满足下列要求:
- 请求方式:Post
- 请求路径:/hotel/list
- 请求参数:对象,类型为RequestParam
- 返回值:PageResult,包含两个属性
Long total
:总条数List<HotelDoc> hotels
:酒店数据
-
因此,我们在
com.yppah.hoteldemo.web
中定义HotelController:点击查看代码
- package com.yppah.hoteldemo.web;
- import com.yppah.hoteldemo.pojo.PageResult;
- import com.yppah.hoteldemo.pojo.RequestParams;
- import com.yppah.hoteldemo.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;
- @RestController
- @RequestMapping("/hotel")
- public class HotelController {
- @Autowired
- private IHotelService hotelService;
- @PostMapping("/list")
- public PageResult search(@RequestBody RequestParams params) {
- return hotelService.search(params);
- }
- }
1.5 实现搜索业务
-
我们在controller调用了IHotelService,并没有实现该方法,因此下面我们就在IHotelService中定义方法,并且去实现业务逻辑。
-
在
com.yppah.hoteldemo.service
中的IHotelService
接口中定义一个方法:点击查看代码
- package com.yppah.hoteldemo.service;
- import com.yppah.hoteldemo.pojo.Hotel;
- import com.baomidou.mybatisplus.extension.service.IService;
- import com.yppah.hoteldemo.pojo.PageResult;
- import com.yppah.hoteldemo.pojo.RequestParams;
- public interface IHotelService extends IService<Hotel> {
- /**
- * 根据关键字搜索酒店信息
- * @param params 请求参数对象,包含用户输入的关键字
- * @return 酒店文档列表
- */
- PageResult search(RequestParams params);
- }
-
在
com.yppah.hoteldemo.service.impl
中的HotelService
中实现search方法:点击查看代码
- package com.yppah.hoteldemo.service.impl;
- import com.alibaba.fastjson.JSON;
- import com.yppah.hoteldemo.mapper.HotelMapper;
- import com.yppah.hoteldemo.pojo.Hotel;
- import com.yppah.hoteldemo.pojo.HotelDoc;
- import com.yppah.hoteldemo.pojo.PageResult;
- import com.yppah.hoteldemo.pojo.RequestParams;
- import com.yppah.hoteldemo.service.IHotelService;
- 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.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
- private RestHighLevelClient client; // 使用之前需要在项目启动类HoteldemoApplication中用@Bean将其注入到spring中
- @Override
- public PageResult search(RequestParams params) {
- try {
- // 1. 准备Request
- SearchRequest request = new SearchRequest("hotel");
- // 2. 准备DSL
- // 2.1 关键字搜索query
- String key = params.getKey();
- if (key==null || "".equals(key)) {
- request.source().query(QueryBuilders.matchAllQuery());
- } else {
- request.source().query(QueryBuilders.matchQuery("all", key));
- }
- // 2.2 查询结果分页处理
- int page = params.getPage(); // 自动拆箱
- int size = params.getSize();
- request.source().from((page-1)*size).size(size);
- // 3. 发送Request,得到Response
- SearchResponse response = client.search(request, RequestOptions.DEFAULT);
- // 4. 解析Response
- return handleResponse(response);
- } catch (IOException e) {
- throw new RuntimeException(e);
- }
- }
- private PageResult handleResponse(SearchResponse response) { // 需要在PageResult中添加构造函数两个
- //4 解析响应
- SearchHits searchHits = response.getHits();
- //4.1 获取总条数
- long total = searchHits.getTotalHits().value;
- //4.2 获取文档数组
- SearchHit[] hits = searchHits.getHits();
- //4.3 遍历数组
- List<HotelDoc> hotels = new ArrayList<>(); //用于存放PageResult所要求格式的数据
- for (SearchHit hit : hits) {
- //4.3.1 获取文档source
- String json = hit.getSourceAsString();
- //4.3.2 反序列化解析json
- HotelDoc hotelDoc = JSON.parseObject(json, HotelDoc.class);
- hotels.add(hotelDoc);
- }
- //4.4 封装返回
- return new PageResult(total, hotels);
- }
- }
-
实现搜索业务,肯定离不开RestHighLevelClient,我们需要把它注册到Spring中作为一个Bean。在
com.yppah.hoteldemo
中的HotelDemoApplication
中声明这个Bean:- @Bean
- public RestHighLevelClient client() {
- return new RestHighLevelClient(RestClient.builder(
- HttpHost.create("http://10.193.193.141:9200")
- ));
- }
1.6 测试
2 酒店结果过滤
2.1 课件
2.2 需求分析
- 在页面搜索框下面,会有一些过滤项:
- 传递的参数如图:
- 包含的过滤条件有:
- brand:品牌值
- city:城市
- minPrice~maxPrice:价格范围
- starName:星级
- 我们需要做两件事情:
- 修改请求参数的对象RequestParams,接收上述参数
- 修改业务逻辑,在搜索条件之外,添加一些过滤条件
2.3 修改实体类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;
- }
2.4 修改搜索业务
-
在HotelService的search方法中,只有一个地方需要修改:requet.source().query( ... )其中的查询条件。
-
在之前的业务中,只有match查询,根据关键字搜索,现在要添加条件过滤,包括:
- 品牌过滤:是keyword类型,用term查询
- 星级过滤:是keyword类型,用term查询
- 价格过滤:是数值类型,用range查询
- 城市过滤:是keyword类型,用term查询
-
多个查询条件组合,肯定是boolean查询来组合:
- 关键字搜索放到must中,参与算分
- 其它过滤条件放到filter中,不参与算分
-
因为条件构建的逻辑比较复杂,这里先封装为一个函数:
点击查看代码
- private void buildBasicQuery(RequestParams params, SearchRequest request) {
- // 1. 构建booleanQuery
- BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
- // 2. 关键字搜索
- String key = params.getKey();
- if (key==null || "".equals(key)) {
- request.source().query(QueryBuilders.matchAllQuery());
- } else {
- request.source().query(QueryBuilders.matchQuery("all", key));
- }
- // 3. 条件过滤
- // 3.1 城市
- if (params.getCity()!=null && !params.getCity().equals("")) {
- boolQuery.filter(QueryBuilders.termQuery("city", params.getCity()));
- }
- // 3.2 品牌
- if (params.getBrand()!=null && !params.getBrand().equals("")) {
- boolQuery.filter(QueryBuilders.termQuery("brand", params.getBrand()));
- }
- // 3.3 星级
- if (params.getStarName()!=null && !params.getStarName().equals("")) {
- boolQuery.filter(QueryBuilders.termQuery("starName", params.getStarName()));
- }
- // 3.4 价格
- if (params.getMinPrice()!=null && params.getMaxPrice()!=null) {
- boolQuery.filter(QueryBuilders.rangeQuery("price")
- .gte(params.getMinPrice())
- .lte(params.getMaxPrice())
- );
- }
- // 4. 放入resource
- request.source().query(boolQuery);
- }
2.5 测试
3 我周边的酒店
3.1 课件
3.2 需求分析
- 在酒店列表页的右侧,有一个小地图,点击地图的定位按钮,地图会找到你所在的位置:
- 并且,在前端会发起查询请求,将你的坐标发送到服务端:
- 我们要做的事情就是基于这个location坐标,然后按照距离对周围酒店排序。实现思路如下:
- 修改RequestParams参数,接收location字段
- 修改search方法业务逻辑,如果location有值,添加根据geo_distance排序的功能
3.3 修改实体类
3.4 距离排序API
3.5 添加距离排序
点击查看代码
- @Override
- public PageResult search(RequestParams params) {
- try {
- // 1. 准备Request
- SearchRequest request = new SearchRequest("hotel");
- // 2. 准备DSL
- // 2.1 封装自定义query函数
- buildBasicQuery(params, request);
- // 2.2 查询结果分页处理
- int page = params.getPage(); // 自动拆箱
- int size = params.getSize();
- request.source().from((page-1)*size).size(size);
- // 2.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)
- );
- }
- // 3. 发送Request,得到Response
- SearchResponse response = client.search(request, RequestOptions.DEFAULT);
- // 4. 解析Response
- return handleResponse(response);
- } catch (IOException e) {
- throw new RuntimeException(e);
- }
- }
3.6 距离排序显示
发现确实可以实现对我附近酒店的排序,不过并没有看到酒店到底距离我多远,这该怎么办?
排序完成后,页面还要获取我附近每个酒店的具体距离值,这个值在响应结果中是独立的:
因此,我们在结果解析阶段,除了解析source部分以外,还要得到sort部分,也就是排序的距离,然后放到响应结果中。
我们要做两件事:
- 修改HotelDoc,添加排序距离字段,用于页面显示
- 修改HotelService类中的handleResponse方法,添加对sort值的获取
3.6.1 修改HotelDoc类,添加距离字段
点击查看代码
- package com.yppah.hoteldemo.pojo;
-
- import lombok.Data;
- import lombok.NoArgsConstructor;
-
- @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();
- }
-
- /*public HotelDoc() {
- }*/
- }
3.6.2 修改HotelService中的handleResponse方法
点击查看代码
- private PageResult handleResponse(SearchResponse response) {
- //4 解析响应
- SearchHits searchHits = response.getHits();
- //4.1 获取总条数
- long total = searchHits.getTotalHits().value;
- //4.2 获取文档数组
- SearchHit[] hits = searchHits.getHits();
- //4.3 遍历数组
- List<HotelDoc> hotels = new ArrayList<>(); //用于存放PageResult所要求格式的数据
- for (SearchHit hit : hits) {
- //4.3.1 获取文档source
- String json = hit.getSourceAsString();
- //4.3.2 反序列化解析json
- HotelDoc hotelDoc = JSON.parseObject(json, HotelDoc.class);
-
- // 我附近的酒店功能-获取排序值
- Object[] sortValues = hit.getSortValues();
- if (sortValues.length > 0) {
- Object sortValue = sortValues[0];
- hotelDoc.setDistance(sortValue);
- }
-
- hotels.add(hotelDoc);
- }
- //4.4 封装返回
- return new PageResult(total, hotels);
- }
3.6.2 重启服务测试
4 酒店竞价排名
4.1 课件
4.2 需求分析
- 要让指定酒店在搜索结果中排名置顶,效果如图:
- 页面会给指定的酒店添加广告标记。那怎样才能让指定的酒店排名置顶呢?
- 我们之前学习过的function_score查询可以影响算分,算分高了,自然排名也就高了。而function_score包含3个要素:
- 过滤条件:哪些文档要加分
- 算分函数:如何计算function score
- 加权方式:function score 与 query score如何运算
- 这里的需求是:让指定酒店排名靠前。因此我们需要给这些酒店添加一个标记,这样在过滤条件中就可以根据这个标记来判断,是否要提高算分。
- 比如,我们给酒店添加一个字段:isAD,Boolean类型:
- true:是广告
- false:不是广告
- 这样function_score包含3个要素就很好确定了:
- 过滤条件:判断isAD 是否为true
- 算分函数:我们可以用最简单暴力的weight,固定加权值
- 加权方式:可以用默认的相乘,大大提高算分
- 因此,业务的实现步骤包括:
- 给HotelDoc类添加isAD字段,Boolean类型
- 挑选几个你喜欢的酒店,给它的文档数据添加isAD字段,值为true
- 修改search方法,添加function score功能,给isAD值为true的酒店增加权重
4.3 修改HotelDoc实体类
4.4 添加广告标记
挑几个酒店,利用ES控制台手动添加isAD字段,设置为true:
- # 添加isAD字段
- POST /hotel/_update/609372
- {
- "doc": {
- "isAD": true
- }
- }
- POST /hotel/_update/5873072
- {
- "doc": {
- "isAD": true
- }
- }
- POST /hotel/_update/2056298828
- {
- "doc": {
- "isAD": true
- }
- }
- POST /hotel/_update/2062643512
- {
- "doc": {
- "isAD": true
- }
- }
4.5 添加算分函数查询
-
接下来我们就要修改查询条件了。之前是用的boolean 查询,现在要改成function_socre查询。
-
function_score查询结构如下:
-
对应的JavaAPI如下:
-
我们可以将之前写的boolean查询作为原始查询条件放到query中,接下来就是添加过滤条件、算分函数、加权模式了。所以原来的代码依然可以沿用。
-
修改
com.yppah.hoteldemo.service.impl
包下的HotelService
类中的buildBasicQuery
方法,添加算分函数查询:点击查看代码
- private void buildBasicQuery(RequestParams params, SearchRequest request) {
- // 1. 构建booleanQuery
- BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
- // 2. 关键字搜索
- String key = params.getKey();
- if (key==null || "".equals(key)) {
- request.source().query(QueryBuilders.matchAllQuery());
- } else {
- request.source().query(QueryBuilders.matchQuery("all", key));
- }
- // 3. 条件过滤
- // 3.1 城市
- if (params.getCity()!=null && !params.getCity().equals("")) {
- boolQuery.filter(QueryBuilders.termQuery("city", params.getCity()));
- }
- // 3.2 品牌
- if (params.getBrand()!=null && !params.getBrand().equals("")) {
- boolQuery.filter(QueryBuilders.termQuery("brand", params.getBrand()));
- }
- // 3.3 星级
- if (params.getStarName()!=null && !params.getStarName().equals("")) {
- boolQuery.filter(QueryBuilders.termQuery("starName", params.getStarName()));
- }
- // 3.4 价格
- if (params.getMinPrice()!=null && params.getMaxPrice()!=null) {
- boolQuery.filter(QueryBuilders.rangeQuery("price")
- .gte(params.getMinPrice())
- .lte(params.getMaxPrice())
- );
- }
- // 酒店竞价排名功能-算分控制
- FunctionScoreQueryBuilder functionScoreQuery =
- QueryBuilders.functionScoreQuery(
- // 原始查询,相关性算分的查询
- boolQuery,
- // function score的数组
- new FunctionScoreQueryBuilder.FilterFunctionBuilder[]{
- // 其中的一个function score 元素
- new FunctionScoreQueryBuilder.FilterFunctionBuilder(
- // 过滤条件
- QueryBuilders.termQuery("isAD", true),
- // 算分函数
- ScoreFunctionBuilders.weightFactorFunction(10) //*10
- )
- });
- // 4. 放入resource
- // request.source().query(boolQuery);
- request.source().query(functionScoreQuery);
- }
4.6 测试
注意:要把浏览器中的广告拦截插件暂时关掉
有点问题广告标识未显示QAQ