一、项目分析
启动hotel-demo项目,访问localhost:servicePort,即可访问static下的index.html:
从页面分析,我们需要实现搜索、分页、排序等功能。点击页面,可以看到list接口的传参为:
二、需求1:酒店搜索功能
接下来实现酒店搜索功能,完成关键字搜索和分页。
- 定义接参的Dto类
@Data
public class RequestParam {
private String key;
private Integer page; //pageNum
private Integer size; //pageSize
private String sortBy;
}
- 定义返回的结果类
@AllArgsConstructor
@NoArgsConstructor
@Data
public class PageResult {
private Long total;
private List<HotelDoc> hotelDocList;
}
- 定义controller接口,接收页面请求
@RestController
@RequestMapping("/hotel")
public class HotelSearchController {
@Resource
IHotelService hotelService;
@PostMapping("/list")
public PageResult searchHotel(@RequestBody RequestParam requestParam){
return hotelService.search(requestParam);
}
}
- Service层要用到JavaRestHighLevelClient对象,在启动类中定义这个Bean
@SpringBootApplication
public class HotelDemoApplication {
public static void main(String[] args) {
SpringApplication.run(HotelDemoApplication.class, args);
}
@Bean
public RestHighLevelClient client(){
return new RestHighLevelClient(RestClient.builder(
HttpHost.create("http://10.4.130.220:9200")
));
}
}
- 完成Service层,利用match查询实现根据关键字搜索酒店信息
public interface IHotelService extends IService<Hotel> {
PageResult search(RequestParam requestParam);
}
@Service
public class HotelService extends ServiceImpl<HotelMapper, Hotel> implements IHotelService {
@Resource
RestHighLevelClient client; //注入客户端操作的bean
@Override
public PageResult search(RequestParam requestParam) {
try {
SearchRequest request = new SearchRequest("hotel");
//搜索关键字
String key = requestParam.getKey();
if (StringUtils.isNotEmpty(key)) { //有key就走全文检索
request.source().query(QueryBuilders.matchQuery("all", key));
} else { //没key就走查所有
request.source().query(QueryBuilders.matchAllQuery());
}
//分页
request.source().from((requestParam.getPage() - 1) * requestParam.getSize()) //(pageNum-1)*pageSize
.size(requestParam.getSize());
//发送请求
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
//处理响应结果
return handleResponse(response);
} catch (IOException e) {
throw new RuntimeException();
}
}
//处理响应结果的方法
private PageResult handleResponse(SearchResponse response) {
SearchHits searchHits = response.getHits();
long total = searchHits.getTotalHits().value;
SearchHit[] hits = searchHits.getHits();
//Stream流将hits中的每条数据都转为HotelDoc对象
List<HotelDoc> hotelDocList = Arrays.stream(hits).map(t -> {
String json = t.getSourceAsString();
HotelDoc hotelDoc = JSON.parseObject(json, HotelDoc.class);
return hotelDoc;
}).collect(Collectors.toList());
return new PageResult(total, hotelDocList);
}
}
重启服务,搜索和分页已实现。
三、需求2:添加过滤功能
接下来添加品牌、城市、星级、价格的过滤功能。这里参与搜索的条件对应着不同的搜索类型,有全文检索,有精确查找,自然要用复合查询Boolean Search
- 修改接参dto类:
@Data
public class RequestParam {
private String key;
private Integer page; //pageNum
private Integer size; //pageSize
private String sortBy;
private String brand;
private String starName;
private String city;
private Integer minPrice;
private Integer maxPrice;
}
- 修改Service层实现,这里把搜索条件的构建单独抽取成方法,一来方便后面复用,二来让代码看着清爽点
@Service
public class HotelService extends ServiceImpl<HotelMapper, Hotel> implements IHotelService {
@Resource
RestHighLevelClient client;
@Override
public PageResult search(RequestParam requestParam) {
try {
//准备request
SearchRequest request = new SearchRequest("hotel");
//构建查询条件
buildBasicQuery(requestParam, request);
//分页
request.source().from((requestParam.getPage() - 1) * requestParam.getSize())
.size(requestParam.getSize());
//发送请求
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
//处理响应结果
return handleResponse(response);
} catch (IOException e) {
throw new RuntimeException();
}
}
private void buildBasicQuery(RequestParam requestParam, SearchRequest request) {
BoolQueryBuilder booleanQuery = QueryBuilders.boolQuery();
//关键字
String key = requestParam.getKey();
if (! isEmpty(key)) {
booleanQuery.must(QueryBuilders.matchQuery("all", key));
} else {
booleanQuery.must(QueryBuilders.matchAllQuery());
}
//城市
if (! isEmpty(requestParam.getCity())) {
booleanQuery.filter(QueryBuilders.termQuery("city", requestParam.getCity()));
}
//品牌
if (! isEmpty(requestParam.getBrand())) {
booleanQuery.filter(QueryBuilders.termQuery("brand", requestParam.getBrand()));
}
//星级
if (! isEmpty(requestParam.getStarName())) {
booleanQuery.filter(QueryBuilders.termQuery("startName", requestParam.getStarName()));
}
//价格
if (requestParam.getMaxPrice() != null && requestParam.getMinPrice() != null) {
booleanQuery.filter(QueryBuilders.rangeQuery("price")
.lte(requestParam.getMaxPrice())
.gte(requestParam.getMinPrice()));
}
request.source().query(booleanQuery);
}
private static boolean isEmpty(String str){
return str == null || "".equals(str);
}
}
四、需求3:我附近的酒店
前端页面点击定位后,会将你所在的位置发送到后台:
接下来实现根据这个坐标,将酒店结果按照到这个点的距离升序排序。
距离排序与普通字段排序有所差异,对比如下:
开始实现需求:
- 修改RequestParams参数,接收location字段
@Data
public class RequestParam {
private String key;
private Integer page; //pageNum
private Integer size; //pageSize
private String sortBy;
private String brand;
private String starName;
private String city;
private Integer minPrice;
private Integer maxPrice;
private String location; //经纬度位置
}
- 修改Service中,在分页前加排序逻辑
@Override
public PageResult search(RequestParam requestParam) {
try {
//准备request
SearchRequest request = new SearchRequest("hotel");
//构建查询条件
buildBasicQuery(requestParam, request);
//排序
String myLocation = requestParam.getLocation();
if(! isEmpty(myLocation)){
request.source().sort(SortBuilders
.geoDistanceSort("location",new GeoPoint(myLocation))
.order(SortOrder.ASC)
.unit(DistanceUnit.KILOMETERS));
}
//分页
request.source().from((requestParam.getPage() - 1) * requestParam.getSize())
.size(requestParam.getSize());
//发送请求
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
//处理响应结果
return handleResponse(response);
} catch (IOException e) {
throw new RuntimeException();
}
}
但此时发现返回结果中少了距离你xxx千米的信息:
查看DSL返回结果,看到距离是在sort字段中:
因此需要修改结果处理的方法,且最后pageResult中是HotelDoc对象的集合,因此,修改Hoteldoc类,加distance距离字段:
@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 handleResponse(SearchResponse response) {
SearchHits searchHits = response.getHits();
long total = searchHits.getTotalHits().value;
SearchHit[] hits = searchHits.getHits();
List<HotelDoc> hotelDocList = Arrays.stream(hits).map(t -> {
String json = t.getSourceAsString();
HotelDoc hotelDoc = JSON.parseObject(json, HotelDoc.class);
//开始加入距离
Object[] sortValues = t.getSortValues(); //排序字段可能不止一个
if(sortValues.length > 0 ){
Object sortValue = sortValues[0];
hotelDoc.setDistance(sortValue); 拿到sort值赋值给距离
}
return hotelDoc;
}).collect(Collectors.toList());
return new PageResult(total, hotelDocList);
}
到此,需求实现:
五、需求4:置顶花广告费的酒店
实现让指定的酒店在搜索结果中排名置顶:
实现思路为:
- HotelDoc类添加标记字段isAD,Boolean类型
- 对于出广告费的酒店,isAD为true,前端可用这个字段给酒店打广告标签
- 使用function score给花钱的酒店人为增加权重,干涉排序
代码实现:
- hotelDoc类:
@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;
- 更新ES数据,模拟某酒店出广告费
- 加入function score算分认为控制,给isAD为true的加权
private void buildBasicQuery(RequestParam requestParam, SearchRequest request) {
//BoolQuery原始查询条件,原始算分
BoolQueryBuilder booleanQuery = QueryBuilders.boolQuery();
//关键字
String key = requestParam.getKey();
if (!isEmpty(key)) {
booleanQuery.must(QueryBuilders.matchQuery("all", key));
} else {
booleanQuery.must(QueryBuilders.matchAllQuery());
}
//城市
if (!isEmpty(requestParam.getCity())) {
booleanQuery.filter(QueryBuilders.termQuery("city", requestParam.getCity()));
}
//品牌
if (!isEmpty(requestParam.getBrand())) {
booleanQuery.filter(QueryBuilders.termQuery("brand", requestParam.getBrand()));
}
//星级
if (!isEmpty(requestParam.getStarName())) {
booleanQuery.filter(QueryBuilders.termQuery("startName", requestParam.getStarName()));
}
//价格
if (requestParam.getMaxPrice() != null && requestParam.getMinPrice() != null) {
booleanQuery.filter(QueryBuilders.rangeQuery("price")
.lte(requestParam.getMaxPrice())
.gte(requestParam.getMinPrice()));
}
//function score算分控制
FunctionScoreQueryBuilder functionScoreQuery = QueryBuilders.functionScoreQuery(
booleanQuery, //第一个参数传入booleanQuery为原始查询,对应原始的相关性算分
new FunctionScoreQueryBuilder.FilterFunctionBuilder[]{ //第二个形参,function score数组,里面有个function score元素
new FunctionScoreQueryBuilder.FilterFunctionBuilder( //function score元素对象,第一个参数传入筛选字段
QueryBuilders.termQuery("isAD", true), //不再用酒店品牌筛选,而是isAD字段
ScoreFunctionBuilders.weightFactorFunction(10) //算分函数,用默认的乘法,权重为10
)
});
request.source().query(functionScoreQuery);
}
实现效果;
Function Score查询可以控制文档的相关性算分,使用方式如下:
最后贴上以上四个需求Service层代码:
import cn.itcast.hotel.domain.dto.RequestParam;
import cn.itcast.hotel.domain.pojo.HotelDoc;
import cn.itcast.hotel.domain.vo.PageResult;
import cn.itcast.hotel.mapper.HotelMapper;
import cn.itcast.hotel.domain.pojo.Hotel;
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.stereotype.Service;
import javax.annotation.Resource;
import java.io.IOException;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
@Service
public class HotelService extends ServiceImpl<HotelMapper, Hotel> implements IHotelService {
@Resource
RestHighLevelClient client;
@Override
public PageResult search(RequestParam requestParam) {
try {
//准备request
SearchRequest request = new SearchRequest("hotel");
//构建查询条件
buildBasicQuery(requestParam, request);
//排序
String myLocation = requestParam.getLocation();
if (!isEmpty(myLocation)) {
request.source().sort(SortBuilders
.geoDistanceSort("location", new GeoPoint(myLocation))
.order(SortOrder.ASC)
.unit(DistanceUnit.KILOMETERS));
}
//分页
request.source().from((requestParam.getPage() - 1) * requestParam.getSize())
.size(requestParam.getSize());
//发送请求
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
//处理响应结果
return handleResponse(response);
} catch (IOException e) {
throw new RuntimeException();
}
}
private void buildBasicQuery(RequestParam requestParam, SearchRequest request) {
//BoolQuery原始查询条件,原始算分
BoolQueryBuilder booleanQuery = QueryBuilders.boolQuery();
//关键字
String key = requestParam.getKey();
if (!isEmpty(key)) {
booleanQuery.must(QueryBuilders.matchQuery("all", key));
} else {
booleanQuery.must(QueryBuilders.matchAllQuery());
}
//城市
if (!isEmpty(requestParam.getCity())) {
booleanQuery.filter(QueryBuilders.termQuery("city", requestParam.getCity()));
}
//品牌
if (!isEmpty(requestParam.getBrand())) {
booleanQuery.filter(QueryBuilders.termQuery("brand", requestParam.getBrand()));
}
//星级
if (!isEmpty(requestParam.getStarName())) {
booleanQuery.filter(QueryBuilders.termQuery("startName", requestParam.getStarName()));
}
//价格
if (requestParam.getMaxPrice() != null && requestParam.getMinPrice() != null) {
booleanQuery.filter(QueryBuilders.rangeQuery("price")
.lte(requestParam.getMaxPrice())
.gte(requestParam.getMinPrice()));
}
//function score算分控制
FunctionScoreQueryBuilder functionScoreQuery = QueryBuilders.functionScoreQuery(
booleanQuery, //第一个参数传入booleanQuery为原始查询,对应原始的相关性算分
new FunctionScoreQueryBuilder.FilterFunctionBuilder[]{ //第二个形参,function score数组,里面有个function score元素
new FunctionScoreQueryBuilder.FilterFunctionBuilder( //function score元素对象,第一个参数传入筛选字段
QueryBuilders.termQuery("isAD", true), //不再用酒店品牌筛选,而是isAD字段
ScoreFunctionBuilders.weightFactorFunction(10) //算分函数,用默认的乘法,权重为10
)
});
request.source().query(functionScoreQuery);
}
private PageResult handleResponse(SearchResponse response) {
SearchHits searchHits = response.getHits();
long total = searchHits.getTotalHits().value;
SearchHit[] hits = searchHits.getHits();
List<HotelDoc> hotelDocList = Arrays.stream(hits).map(t -> {
String json = t.getSourceAsString();
HotelDoc hotelDoc = JSON.parseObject(json, HotelDoc.class);
//开始加入距离
Object[] sortValues = t.getSortValues();
if (sortValues.length > 0) {
Object sortValue = sortValues[0];
hotelDoc.setDistance(sortValue);
}
return hotelDoc;
}).collect(Collectors.toList());
return new PageResult(total, hotelDocList);
}
private static boolean isEmpty(String str) {
return str == null || "".equals(str);
}
}
最后,页面上其他地方的需求实现思路:
排序:
前端会传递sortBy参数,就是排序方式,后端需要判断sortBy值是什么:文章来源:https://www.toymoban.com/news/detail-625478.html
- default:相关度算分排序,这个不用管,es的默认排序策略
- score:根据酒店的score字段排序,也就是用户评价,降序
- price:根据酒店的price字段排序,就是价格,升序
高亮:
文章来源地址https://www.toymoban.com/news/detail-625478.html
request.source()
.query(QueryBuilders.matchQuery("all",requestParam.getKey()))
.highlighter(new HighlightBuilder().field("name")
.requireFieldMatch(false)
.preTags("<strong>")
.postTags("</strong")
);
到了这里,关于【ElasticSearch】ES案例:旅游酒店搜索的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!