详解SpringCloud微服务技术栈:ElasticSearch实战(旅游类项目)

‍作者简介:一位大四、研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:
详解SpringCloud微服务技术栈:ElasticSearch实战(旅游类项目)_第1张图片
页面中打开控制台,点击搜索键查看发送的请求,携带这些参数发起了POST请求:
详解SpringCloud微服务技术栈:ElasticSearch实战(旅游类项目)_第2张图片
接下来就要对这请求进行处理。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方法
详解SpringCloud微服务技术栈:ElasticSearch实战(旅游类项目)_第3张图片
请求方式为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出一个对象,并在启动的时候提前创建:
详解SpringCloud微服务技术栈:ElasticSearch实战(旅游类项目)_第4张图片
在这里我们可以将其注入到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);
    }
}

详解SpringCloud微服务技术栈:ElasticSearch实战(旅游类项目)_第5张图片

条件过滤

搜索框的下放有一些可选项,用户点击以后就需要根据这些字段进行过滤,当点击之后,前端就会发起对应的请求,需要接收这些参数并且去做过滤。步骤如下:
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) {
    	//和之前代码一样,略
    }
}

详解SpringCloud微服务技术栈:ElasticSearch实战(旅游类项目)_第6张图片

附近酒店

前端页面点击定位后,会将你所在的地址location发送到后台:
详解SpringCloud微服务技术栈:ElasticSearch实战(旅游类项目)_第7张图片
详解SpringCloud微服务技术栈:ElasticSearch实战(旅游类项目)_第8张图片
需要注意,我只有Edge才能获取到位置,而谷歌浏览器和火狐浏览器都是不支持的。

后台可以根据这个坐标,将酒店的结果按照这个点的距离做升序排序。
实现流程:
1、修改RequestParams字段,接收location字段
详解SpringCloud微服务技术栈:ElasticSearch实战(旅游类项目)_第9张图片
2、修改search方法业务逻辑,若location有值,添加根据geo_distance排序的功能,并且还需要显示出距离,距离信息就在下面与source同级的sort下:
详解SpringCloud微服务技术栈:ElasticSearch实战(旅游类项目)_第10张图片
修改之前的信息处理函数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也作为字段:
详解SpringCloud微服务技术栈:ElasticSearch实战(旅游类项目)_第11张图片

广告置顶

让指定的酒店在搜索结果中排名制定,就需要影响他们的打分。
给需要置顶的酒店文档添加一个标记,然后利用function score给带有标记的文档增加权重。实现步骤:
1、给HotelDoc类添加Boolean类型的isAD字段
详解SpringCloud微服务技术栈:ElasticSearch实战(旅游类项目)_第12张图片
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代码:
详解SpringCloud微服务技术栈:ElasticSearch实战(旅游类项目)_第13张图片
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);

详解SpringCloud微服务技术栈:ElasticSearch实战(旅游类项目)_第14张图片
成功打上了广告标识。

你可能感兴趣的:(微服务技术全家桶,spring,cloud,微服务,elasticsearch,RestClient,spring)