文章目录
- 一、项目分析
- 二、需求1:酒店搜索功能
- 三、需求2:添加过滤功能
- 四、需求3:我附近的酒店
- 五、需求4:置顶花广告费的酒店
一、项目分析
启动hotel-demo项目,访问localhost:servicePort,即可访问static下的index.html:
从页面分析,我们需要实现搜索、分页、排序等功能。点击页面,可以看到list接口的传参为:
二、需求1:酒店搜索功能
接下来实现酒店搜索功能,完成关键字搜索和分页。
- 定义接参的Dto类
@Data
public class RequestParam {private String key;private Integer page; //pageNumprivate Integer size; //pageSizeprivate String sortBy;
}
- 定义返回的结果类
@AllArgsConstructor
@NoArgsConstructor
@Data
public class PageResult {private Long total;private List<HotelDoc> hotelDocList;
}
- 定义controller接口,接收页面请求
@RestController
@RequestMapping("/hotel")
public class HotelSearchController {@ResourceIHotelService 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);}@Beanpublic 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 {@ResourceRestHighLevelClient client; //注入客户端操作的bean@Overridepublic 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; //pageNumprivate Integer size; //pageSizeprivate 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 {@ResourceRestHighLevelClient client;@Overridepublic PageResult search(RequestParam requestParam) {try {//准备requestSearchRequest 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; //pageNumprivate Integer size; //pageSizeprivate 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 {//准备requestSearchRequest 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 {@ResourceRestHighLevelClient client;@Overridepublic PageResult search(RequestParam requestParam) {try {//准备requestSearchRequest 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值是什么:
- default:相关度算分排序,这个不用管,es的默认排序策略
- score:根据酒店的score字段排序,也就是用户评价,降序
- price:根据酒店的price字段排序,就是价格,升序
高亮:
request.source().query(QueryBuilders.matchQuery("all",requestParam.getKey())).highlighter(new HighlightBuilder().field("name").requireFieldMatch(false).preTags("<strong>").postTags("</strong"));