下面,我们通过黑马旅游的案例来实战演练下之前学习的知识。
资料:
代码资料
我们实现四部分功能:
启动我们提供的hotel-demo项目,其默认端口是8089,访问http://localhost:8090,就能看到项目页面了:
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wh4PMWOl-1679301990830)(assets/image-20210721223159598.png)]](/uploadfile/202505/3b1a9f0776ef508.png)
案例需求:实现黑马旅游的酒店搜索功能,完成关键字搜索和分页
在项目的首页,有一个大大的搜索框,还有分页按钮:
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-aFxZGwLH-1679301990831)(assets/image-20210721223859419.png)]](/uploadfile/202505/ff927145568259a.png)
点击搜索按钮,可以看到浏览器控制台发出了请求:
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WcGyf805-1679301990831)(assets/image-20210721224033789.png)]](/uploadfile/202505/5cfd95c63a6a1.png)
请求参数如下:
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qvvTFcj5-1679301990832)(assets/image-20210721224112708.png)]](/uploadfile/202505/5cfd95c63a6a1.png)
由此可以知道,我们这个请求的信息如下:
total:总条数List:当前页的数据因此,我们实现业务的流程如下:
实体类有两个,一个是前端的请求参数实体,一个是服务端应该返回的响应结果实体。
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:当前页的数据因此,我们在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 hotels;public PageResult() {}public PageResult(Long total, List hotels) {this.total = total;this.hotels = hotels;}
}
定义一个HotelController,声明查询接口,满足下列要求:
Long total:总条数List hotels :酒店数据因此,我们在cn.itcast.hotel.web中定义HotelController:
@RestController
@RequestMapping("/hotel")
public class HotelController {@Autowiredprivate IHotelService hotelService;// 搜索酒店数据@PostMapping("/list")public PageResult search(@RequestBody RequestParams params){return hotelService.search(params);}
}
我们在controller调用了IHotelService,并没有实现该方法,因此下面我们就在IHotelService中定义方法,并且去实现业务逻辑。
1)在cn.itcast.hotel.service中的IHotelService接口中定义一个方法:
/*** 根据关键字搜索酒店信息* @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方法:
@Override
public PageResult search(RequestParams params) {try {// 1.准备RequestSearchRequest request = new SearchRequest("hotel");// 2.准备DSL// 2.1.queryBoolQueryBuilder boolQuery = QueryBuilders.boolQuery();String key = params.getKey();if (key == null || "".equals(key)) {boolQuery.must(QueryBuilders.matchAllQuery());} else {boolQuery.must(QueryBuilders.matchQuery("all", key));}request.source().query(boolQuery);// 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 handleResponse(response);} catch (IOException e) {throw new RuntimeException(e);}
}// 结果解析
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 hotels = new ArrayList<>();for (SearchHit hit : hits) {// 获取文档sourceString json = hit.getSourceAsString();// 反序列化HotelDoc hotelDoc = JSON.parseObject(json, HotelDoc.class);// 放入集合hotels.add(hotelDoc);}// 4.4.封装返回return new PageResult(total, hotels);
}
需求:添加品牌、城市、星级、价格等过滤功能
在页面搜索框下面,会有一些过滤项:
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-V5Nszds5-1679301990834)(assets/image-20210722091940726.png)]](/uploadfile/202505/9cfbba211719795.png)
传递的参数如图:
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5JMNMz56-1679301990834)(assets/image-20210722092051994.png)]](/uploadfile/202505/93491dcc6792.png)
包含的过滤条件有:
我们需要做两件事情:
修改在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;
}
在HotelService的search方法中,只有一个地方需要修改:requet.source().query( … )其中的查询条件。
在之前的业务中,只有match查询,根据关键字搜索,现在要添加条件过滤,包括:
多个查询条件组合,肯定是boolean查询来组合:
因为条件构建的逻辑比较复杂,这里先封装为一个函数:
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kTVoYCxa-1679301990835)(assets/image-20210722092935453.png)]](/uploadfile/202505/20bbf03b009967d.png)
buildBasicQuery的代码如下:
private void buildBasicQuery(RequestParams params, SearchRequest request) {// 1.构建BooleanQueryBoolQueryBuilder boolQuery = QueryBuilders.boolQuery();// 2.关键字搜索String key = params.getKey();if (key == null || "".equals(key)) {boolQuery.must(QueryBuilders.matchAllQuery());} else {boolQuery.must(QueryBuilders.matchQuery("all", key));}// 3.城市条件if (params.getCity() != null && !params.getCity().equals("")) {boolQuery.filter(QueryBuilders.termQuery("city", params.getCity()));}// 4.品牌条件if (params.getBrand() != null && !params.getBrand().equals("")) {boolQuery.filter(QueryBuilders.termQuery("brand", params.getBrand()));}// 5.星级条件if (params.getStarName() != null && !params.getStarName().equals("")) {boolQuery.filter(QueryBuilders.termQuery("starName", params.getStarName()));}// 6.价格if (params.getMinPrice() != null && params.getMaxPrice() != null) {boolQuery.filter(QueryBuilders.rangeQuery("price").gte(params.getMinPrice()).lte(params.getMaxPrice()));}// 7.放入sourcerequest.source().query(boolQuery);
}
需求:我附近的酒店
在酒店列表页的右侧,有一个小地图,点击地图的定位按钮,地图会找到你所在的位置:
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-aEqghtKP-1679301990835)(assets/image-20210722093414542.png)]](/uploadfile/202505/bef2b4cc0ff8416.png)
并且,在前端会发起查询请求,将你的坐标发送到服务端:
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fglzTU6p-1679301990836)(assets/image-20210722093642382.png)]](/uploadfile/202505/4e7c4a1dd73c830.png)
我们要做的事情就是基于这个location坐标,然后按照距离对周围酒店排序。实现思路如下:
修改在cn.itcast.hotel.pojo包下的实体类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;// 我当前的地理坐标private String location;
}
我们以前学习过排序功能,包括两种:
我们只讲了普通字段排序对应的java写法。地理坐标排序只学过DSL语法,如下:
GET /hotel/_search
{"query": {"match_all": {}},"sort": [{"price": "asc" },{"_geo_distance" : {"FIELD" : "纬度,经度","order" : "asc","unit" : "km"}}]
}
对应的java代码示例:
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-aeAfQHXv-1679301990837)(assets/image-20210722095227059.png)]](/uploadfile/202505/2c3738c7205e7a6.png)
在cn.itcast.hotel.service.impl的HotelService的search方法中,添加一个排序功能:
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OnUtBYes-1679301990838)(assets/image-20210722095902314.png)]](/uploadfile/202505/79040b89996131d.png)
完整代码:
@Override
public PageResult search(RequestParams params) {try {// 1.准备RequestSearchRequest request = new SearchRequest("hotel");// 2.准备DSL// 2.1.querybuildBasicQuery(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.发送请求SearchResponse response = client.search(request, RequestOptions.DEFAULT);// 4.解析响应return handleResponse(response);} catch (IOException e) {throw new RuntimeException(e);}
}
重启服务后,测试我的酒店功能:
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6116lJ27-1679301990838)(assets/image-20210722100040674.png)]](/uploadfile/202505/020fe7a91d2fa48.png)
发现确实可以实现对我附近酒店的排序,不过并没有看到酒店到底距离我多远,这该怎么办?
排序完成后,页面还要获取我附近每个酒店的具体距离值,这个值在响应结果中是独立的:
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HB9x2eC5-1679301990839)(assets/image-20210722095648542.png)]](/uploadfile/202505/52de8c8b8c49edd.png)
因此,我们在结果解析阶段,除了解析source部分以外,还要得到sort部分,也就是排序的距离,然后放到响应结果中。
我们要做两件事:
1)修改HotelDoc类,添加距离字段
package cn.itcast.hotel.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();}
}
2)修改HotelService中的handleResponse方法
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wRQFU4HW-1679301990840)(assets/image-20210722100613966.png)]](/uploadfile/202505/35ba27ceb11cf8b.png)
重启后测试,发现页面能成功显示距离了:
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-trK3csHS-1679301990840)(assets/image-20210722100838604.png)]](/uploadfile/202505/bb904b0ed7af267.png)
需求:让指定的酒店在搜索结果中排名置顶
要让指定酒店在搜索结果中排名置顶,效果如图:
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-grJwC5U0-1679301990841)(assets/image-20210722100947292.png)]](/uploadfile/202505/e63b060d7d0c.png)
页面会给指定的酒店添加广告标记。
那怎样才能让指定的酒店排名置顶呢?
我们之前学习过的function_score查询可以影响算分,算分高了,自然排名也就高了。而function_score包含3个要素:
这里的需求是:让指定酒店排名靠前。因此我们需要给这些酒店添加一个标记,这样在过滤条件中就可以根据这个标记来判断,是否要提高算分。
比如,我们给酒店添加一个字段:isAD,Boolean类型:
这样function_score包含3个要素就很好确定了:
因此,业务的实现步骤包括:
给HotelDoc类添加isAD字段,Boolean类型
挑选几个你喜欢的酒店,给它的文档数据添加isAD字段,值为true
修改search方法,添加function score功能,给isAD值为true的酒店增加权重
给cn.itcast.hotel.pojo包下的HotelDoc类添加isAD字段:
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-P3wh7FpV-1679301990842)(assets/image-20210722101908062.png)]](/uploadfile/202505/cfea49c77a6c351.png)
接下来,我们挑几个酒店,添加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}
}
接下来我们就要修改查询条件了。之前是用的boolean 查询,现在要改成function_socre查询。
function_score查询结构如下:
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xYNy2Y13-1679301990842)(assets/image-20210721191544750.png)]](/uploadfile/202505/2f847f750507e69.png)
对应的JavaAPI如下:
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Yavx4SCA-1679301990843)(assets/image-20210722102850818.png)]](/uploadfile/202505/cf1d995f5e94938.png)
我们可以将之前写的boolean查询作为原始查询条件放到query中,接下来就是添加过滤条件、算分函数、加权模式了。所以原来的代码依然可以沿用。
修改cn.itcast.hotel.service.impl包下的HotelService类中的buildBasicQuery方法,添加算分函数查询:
private void buildBasicQuery(RequestParams params, SearchRequest request) {// 1.构建BooleanQueryBoolQueryBuilder 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);
}
就是添加过滤条件、算分函数、加权模式了。所以原来的代码依然可以沿用。
修改cn.itcast.hotel.service.impl包下的HotelService类中的buildBasicQuery方法,添加算分函数查询:
private void buildBasicQuery(RequestParams params, SearchRequest request) {// 1.构建BooleanQueryBoolQueryBuilder 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);
}
下一篇:2022-复习