目录
1.1.4、实现 IHotelService 接口的 search 方法
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 文档操作步骤一样~
- 创建 search 请求.
- 准备请求参数,具体的参数如下
- 通过 QueryBuilders 构建 match 查询(如果用户没有输入,直接点击搜索,那么这里构建 match_all 查询所有即可).
- 添加分页数据:from(offset 偏移量) 和 size(数据显示条数).
- 发送请求,接收响应.
- 解析响应,获取查询总数和酒店信息列表.
代码如下:
@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 个参数:
- city:城市
- starName:星级
- brand:品牌.
- minPrice:价格下限.
- 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 语句去看