👨🎓作者简介:一位大四、研0学生,正在努力准备大四暑假的实习
🌌上期文章:详解SpringCloud微服务技术栈:ElasticSearch实践2——RestClient查询并处理文档
📚订阅专栏:微服务技术全家桶
希望文章对你们有所帮助
经过前面的学习,需要扎实地掌握如何使用DSL语句对索引库、文档进行操作,如何使用DSL对搜索结果进行进一步处理(排序、分页、高亮),并且需要会使用RestClient,熟练使用API去实现DSL请求的发送,操作ElasticSearch。
现在需要用所学的东西在项目中做实践,这里要做的是一个旅游类的项目,很多的功能都由ElasticSearch来实现。
ElasticSearch实战(旅游类项目)
- 导入工程
- 搜索、分页
- 条件过滤
- 附近酒店
- 广告置顶
导入工程
在hotel-demo中,自带了前端页面,可以从网盘中下载并导入:
链接:https://pan.baidu.com/s/15lxTgc59WqLmq5B2dr3Ofg?pwd=3gz2
提取码:3gz2
打开启动类,访问端口8089:
页面中打开控制台,点击搜索键查看发送的请求,携带这些参数发起了POST请求:
接下来就要对这请求进行处理。key就是用户搜索的关键字,page分页的页码,size为每页的大小,sortBy表示参与排序的字段,例如点击评价来搜索,sortBy就会指定为score。
搜索、分页
1、定义实体类,接收前端的JSON请求:
@Data
public class RequestParams {
private String key;
private Integer page;
private Integer size;
private String sortBy;
}
2、定义controller接口,接收页面请求,调用IHotelService的search方法
请求方式为post,请求路径为/hotel/list,请求参数是RequestParam独享,返回PageResult(包含2个属性:总条数和当前酒店数据)来做分页查询。
(1)先在pojo下创建实体类PageResult:
@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;
}
}
(2)新建HotelController类:
@RestController
@RequestMapping("/hotel")
public class HotelController {
@Resource
private IHotelService hotelService;
@PostMapping("/list")
public PageResult search(@RequestBody RequestParams params){
return hotelService.search(params);
}
}
3、定义IHotelService的search方法,利用match查询实现根据关键字搜索酒店信息。
查询的实现放在实现类HotelService中。
在之前已经在测试类中编写过很多类似的代码,流程无非就是编写请求->编写DSL->发起请求得到响应->处理请求并返回。
而发起请求的操作是给RestHighLevelClient来做的,在之前我们是直接new出一个对象,并在启动的时候提前创建:
在这里我们可以将其注入到Spring中去,点开启动类,使用bean注入:
@Bean
public RestHighLevelClient client(){
return new RestHighLevelClient(RestClient.builder(
HttpHost.create("http://192.168.177.130:9200")
));
}
现在,实现类HotelService可以直接去注入这个对象了,实现类代码如下:
@Service
public class HotelService extends ServiceImpl<HotelMapper, Hotel> implements IHotelService {
@Resource
private RestHighLevelClient client;
@Override
public PageResult search(RequestParams params) {
try {
//准备request
SearchRequest request = new SearchRequest("hotel");
//准备DSL,先获取搜索的内容
String key = params.getKey();
if(key == null || "".equals(key)){
request.source().query(QueryBuilders.matchAllQuery());
}else{
//1.关键字搜索
request.source().query(QueryBuilders.matchQuery("all", key));
}
//2.分页
int page = params.getPage();
int size = params.getSize();
request.source().from((page - 1) * size).size(size);
//发送请求,有异常不能抛出,因为接口没有抛出异常这里也不适合抛,直接try...catch捕获异常即可
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
return handleResponse(response);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
private PageResult handleResponse(SearchResponse response) {
//解析响应
SearchHits searchHits = response.getHits();
//获取总条数
long total = searchHits.getTotalHits().value;
//System.out.println("共搜索到" + total + "条数据");
//文档数组
SearchHit[] hits = searchHits.getHits();
//准备好酒店的集合元素,放入PageResult
List<HotelDoc> hotels = new ArrayList<>();
for (SearchHit hit : hits) {
//获取文档source
String json = hit.getSourceAsString();
//将json反序列化为对象
HotelDoc hotelDoc = JSON.parseObject(json, HotelDoc.class);
//System.out.println("hotelDoc = " + hotelDoc);
hotels.add(hotelDoc);
}
//封装并返回
return new PageResult(total, hotels);
//System.out.println("response = " + response);
}
}
条件过滤
搜索框的下放有一些可选项,用户点击以后就需要根据这些字段进行过滤,当点击之后,前端就会发起对应的请求,需要接收这些参数并且去做过滤。步骤如下:
1、修改RequestParams类,添加brand、city、starName、minPrice、maxPrice等参数
@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、修改search方法的实现,在关键字搜索时,如果brand等参数存在,对其做过滤,不参与算分。
过滤条件包括:
city:精确匹配
brand:精确匹配
starName:精确匹配
price:范围过滤
显然,多个条件之间是AND关系,除了上述会包含的term查询和range查询,搜索框中的全文检索查询显然就是must查询,而组合多条件需要用BooleanQuery。
同时需要注意,参数存在才需要做过滤,所以要做好非空的判断。
由于参数多,所以DSL逻辑上的表达会很长,最好可以封装一下:
@Service
public class HotelService extends ServiceImpl<HotelMapper, Hotel> implements IHotelService {
@Resource
private RestHighLevelClient client;
@Override
public PageResult search(RequestParams params) {
try {
//准备request
SearchRequest request = new SearchRequest("hotel");
//编写DSL语句
buildBasicQuery(params, request);
//分页
int page = params.getPage();
int size = params.getSize();
request.source().from((page - 1) * size).size(size);
//发送请求,有异常不能抛出,因为接口没有抛出异常这里也不适合抛,直接try...catch捕获异常即可
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
return handleResponse(response);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
private void buildBasicQuery(RequestParams params, SearchRequest request) {
//先构建BooleanQuery,再把查询放进去组合
BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
//1、关键字must检索
String key = params.getKey();
if(key == null || "".equals(key)){
boolQuery.must(QueryBuilders.matchAllQuery());
}else{
boolQuery.must(QueryBuilders.matchQuery("all", key));
}
//2、条件过滤,不参与算分
//2.1 城市
if (params.getCity() != null && !params.getCity().equals("")){
boolQuery.filter(QueryBuilders.termQuery("city", params.getCity()));
}
//2.2 品牌
if (params.getBrand() != null && !params.getBrand().equals("")){
boolQuery.filter(QueryBuilders.termQuery("brand", params.getBrand()));
}
//2.3 星级
if (params.getStarName() != null && !params.getStarName().equals("")){
boolQuery.filter(QueryBuilders.termQuery("starName", params.getStarName()));
}
//3、范围过滤(价格),这里是链式编程要注意
if (params.getMinPrice() != null && params.getMaxPrice() != null){
boolQuery.filter(QueryBuilders
.rangeQuery("price").gte(params.getMinPrice()).lte(params.getMaxPrice()));
}
request.source().query(boolQuery);
}
private PageResult handleResponse(SearchResponse response) {
//和之前代码一样,略
}
}
附近酒店
前端页面点击定位后,会将你所在的地址location发送到后台:
需要注意,我只有Edge才能获取到位置,而谷歌浏览器和火狐浏览器都是不支持的。
后台可以根据这个坐标,将酒店的结果按照这个点的距离做升序排序。
实现流程:
1、修改RequestParams字段,接收location字段
2、修改search方法业务逻辑,若location有值,添加根据geo_distance排序的功能,并且还需要显示出距离,距离信息就在下面与source同级的sort下:
修改之前的信息处理函数handleResponse即可:
private PageResult handleResponse(SearchResponse response) {
//解析响应
SearchHits searchHits = response.getHits();
//获取总条数
long total = searchHits.getTotalHits().value;
//System.out.println("共搜索到" + total + "条数据");
//文档数组
SearchHit[] hits = searchHits.getHits();
//准备好酒店的集合元素,放入PageResult
List<HotelDoc> hotels = new ArrayList<>();
for (SearchHit hit : hits) {
//获取文档source
String json = hit.getSourceAsString();
//将json反序列化为对象
HotelDoc hotelDoc = JSON.parseObject(json, HotelDoc.class);
//获取排序值(距离信息)
Object[] sortValues = hit.getSortValues();
if (sortValues.length > 0 ){
Object sortValue = sortValues[0];
hotelDoc.setDistance(sortValue);
}
//System.out.println("hotelDoc = " + hotelDoc);
hotels.add(hotelDoc);
}
//封装并返回
return new PageResult(total, hotels);
//System.out.println("response = " + response);
}
注意需要修改HotelDoc,将distance也作为字段:
广告置顶
让指定的酒店在搜索结果中排名制定,就需要影响他们的打分。
给需要置顶的酒店文档添加一个标记,然后利用function score给带有标记的文档增加权重。实现步骤:
1、给HotelDoc类添加Boolean类型的isAD字段
2、挑选几个喜欢的酒店,给它的文档数据添加isAD字段,值为true。
实现的方式就不特意写后台了,直接在dev tools中暴力地用DSL语句来做:
POST /hotel/_update/2056126831
{
"doc": {
"isAD": true
}
}
POST /hotel/_update/19989806195
{
"doc": {
"isAD": true
}
}
POST /hotel/_update/2056105938
{
"doc": {
"isAD": true
}
}
3、修改search方法,添加function score功能,给isAD值为true的酒店添加权重。
在之前,根据function score来排序的功能只用了DSL语句来实现,java代码的实现相对还是有点复杂的,最好根据DSL语句做参考来写java代码:
function_score分为两个部分,一个是query里面放着原始的查询方式,其会返回对应的相关性分数(query score),比较关键的还是functions里面的构造,包括了过滤的条件和权重,最后指定function score和query score的运算方式即可(默认为“multiply”)。
上述DSL语句转换为java代码为:
FunctionScoreQueryBuilder functionScoreQueryBuilder =
QueryBuilders.functionScoreQuery(
QueryBuilders.matchQuery("name", "如家"),
new FunctionScoreQueryBuilder.FilterFunctionBuilder[]{ //需要创建出这个数组
new FunctionScoreQueryBuilder.FilterFunctionBuilder(
QueryBuilders.termQuery("brand", "如家"),
ScoreFunctionBuilders.weightFactorFunction(5)
)
}
);
因此需要修改search中的query条件,也就是修改buildBasicQuery函数,除了原始就该有的BoolQuery,还需要增加function score:
//算分控制
FunctionScoreQueryBuilder functionScoreQuery =
QueryBuilders.functionScoreQuery(
// 原始查询,即相关性算分
boolQuery,
// function score数组
new FunctionScoreQueryBuilder.FilterFunctionBuilder[]{
// 具体的一个function score元素
new FunctionScoreQueryBuilder.FilterFunctionBuilder(
//过滤条件,用精确查询
QueryBuilders.termQuery("isAD", true),
//算分函数,直接设置为10
ScoreFunctionBuilders.weightFactorFunction(10)
)
});
request.source().query(functionScoreQuery);
成功打上了广告标识。