elasticsearch实现搜索逻辑

elasticsearch 搜索业务逻辑分析
elasticsearch 客户端对象[发送请求获取响应]  restHighLevelClient.search(searchRequest, RequestOptions.DEFUALT)
搜索请求对象  searchRequest   --> searchRquest.source(searchSourceBuilder)
依赖对象  查询请求构建对象  searchSourceBuilder    
                a: 设置分页查询 --> 每页数据的开始记录数
                        searchSourceBuilder.from(start);  Integer start = (pageNumber - 1) * PAGE_SIZE; [注意事项: 为了业务逻辑严谨性, 我们应该判断当前页,若当前页数据有误(<=0) 把当前页置为1]
                b:  设置每页显示条数
                          searchSourceBuilder.size(PAGE_SIZE);
                c: 设置排序查询(升序/降序)
                        searchSourceBuilder.sort(sort, SortOrder.ASC);   [ASC升序/DESC降序]
查询请求构建对象依赖于  
            1. 组合查询构建对象[布尔查询构建对象]  boolQueryBuilder     创建方法(通过查询构建对象构建)-->QueryBuilders.boolQuery();
                    1.1 设置查询条件 生成查询条件构建对象       MatchQueryBuilder nameQueryBuilder = QueryBuilders.matchQuery("name", keywords);
                    1.2 把查询条件构建对象 依赖到 布尔查询构建对象中    boolQueryBuilder.must(nameQueryBuilder);
                    1.3 把带查寻条件的布尔查询构建对象 依赖到到 查询请求对象中    searchSourceBuilder.query(boolQueryBuilder);
                    1.4 把查询请求构建对象 依赖到查询[搜索]请求对象中         searchRequest.source(searchSourceBuilder);
                    1.5 通过查询请求对象发送请求, 返回响应对象        SearchResponse searchResponse = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);
                    1.6 通过响应对象获取结果集    [获取普通结果并转换为map]hit.getSourceAsMap();
                    1.7 处理结果集
                    1.8 把结果集封装为前端需要的数据返回

            2. 高亮查询构建对象 highlightBuilder     创建方法  --> new HighlightBuilder();
                    2.1 设置高亮字段         highlightBuilder.field("name");
                    2.2 设置前缀         highlightBuilder.preTags("<em style=\"color:red\">");   [注意事项: 官方推荐使用em标签]
                    2.3 设置后缀        highlightBuilder.postTags("</em>");
                    2.4 把高亮构建对象 依赖到 到查询请求构建对象中  searchSourceBuilder.highlighter(highlightBuilder);
                    2.5 把查询请求构建对象 依赖到 查询请求对象中      searchRequest.source(searchSourceBuilder);
                    2.6 通过查询请求对象发送请获取响应对象   SearchResponse searchResponse = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);
                    2.7 通过响应对象获取数据  [注意事项: 和普通获取数据不一致, 这是获取高亮数据]  hit.getHighlightFields();
                    2.8 处理结果集    [注意事项: 这里应该应高亮数据覆盖掉普通查询的数据, 若没有高亮显示的数据, 依旧显示为普通数据, 若是全部显示为普通数据会出现name = null的现象]    
                    2.9 把结果集封装为前端需要的数据返回
                    
            3. 聚合构建对象[分组查询]  termsAggregationBuilder  创建方法  -- > AggregationBuilders.terms("categoryGroup").field("categoryName");  [注意事项: terms 代表精确查询(不分词查询)]
                    3.1 把聚合查询构建对象 依赖到 查询请求构建对象中   searchSourceBuilder.aggregation(termsAggregationBuilder);
                    3.2 把查询请求构建对象 依赖到查询[搜索]请求对象中         searchRequest.source(searchSourceBuilder);
                    3.3 通过查询请求对象发送请求, 返回响应对象        SearchResponse searchResponse = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);
                    3.4 通过响应对象获取结果集    Aggregations aggregations = searchResponse.getAggregations();
                    3.5 处理结果集
                    3.6 把结果集封装为前端需要的数据返回
                     Aggregations aggregations = searchResponse.getAggregations();
                    //将结果集转换为map集合
                    Map<String, Aggregation> aggregationMap = aggregations.getAsMap();
                    //根据分组名称获取具体的数据
                    Terms terms = (Terms) aggregationMap.get("categoryGroup");
                    //获取分组后集合中的具体内容s
                    List<? extends Terms.Bucket> buckets = terms.getBuckets();
                    ArrayList<String> categoryList = new ArrayList<>();

                    if (buckets != null) {
                        for (Terms.Bucket bucket : buckets) {
                            categoryList.add(bucket.getKeyAsString());
                        }
                    }
                    map.put("categoryList", categoryList);

                    [若消费者没有选择分类的解决方案]
                    //若消费者没有选择分类, 则根据聚合出来的分类的第一个分类进行查询
                    if (StringUtils.isEmpty(category)) {
                        if (categoryList != null && categoryList.size() > 0) {
                            //把聚合出来后的分类的第一个分类赋值给分类属性
                            category = categoryList.get(0);
                        }
                    }

                    //若分类名称不为空, 则根据分类名称查询具体分类对应的品牌和规格
                    if (StringUtils.isEmpty(category)) {
                        //方法封装
                        Map<String, Object> brandAndSpecList = findBrandAndSpecListByCategoryName(category);
                        map.putAll(brandAndSpecList);
                    }

                    return map;
---------------------方法封装[基于种类名称查询品牌和规格]--------------------
                     //方法封装 基于种类查询品牌和规格
                    private Map<String, Object> findBrandAndSpecListByCategoryName(String categoryName) {
                        Map<String, Object> specAndBrandMap = new HashMap<>();
                        //基于分类查询品牌信息, 首先应该从redis数据库里面去拿数据,
                        // 若reids数据库里面没有了数据才执行feign  从myql数据库里面去查询数据,然后存一份在redis数据库
                        List<Map> brandList = (List<Map>) redisTemplate.boundHashOps(Constants.REDIS_BRANDLIST).get(categoryName);
                        if (brandList == null || brandList.size() <= 0) {
                            //基于分类查询品牌信息
                            Result brandListResult = brandFeign.findListByCategoryName(categoryName);
                            //取出result里面的数据
                            if (brandListResult.isFlag()) {
                                brandList = (List<Map>) brandListResult.getData();
                                //把结果存储到redis中
                                redisTemplate.boundHashOps(Constants.REDIS_BRANDLIST).put(categoryName, brandList);
                            }
                        }

                        //基于分类查询品牌信息首先应该从redis数据库里面去拿数据,
                        // 若reids数据库里面没有了数据才执行feign  从myql数据库里面去查询数据,然后存一份在redis数据库
                        List<Map> specList = (List<Map>) redisTemplate.boundHashOps(Constants.REDIS_SPECLIST).get(categoryName);
                        if (specList == null || specList.size() <= 0) {
                            //基于分类查询规格信息
                            Result specListResult = specFeign.findSpecListByCategoryId(categoryName);
                            if (specListResult.isFlag()) {
                                 specList = (List<Map>) specListResult.getData();
                                //把结果存储到redis中
                                redisTemplate.boundHashOps(Constants.REDIS_SPECLIST).put(categoryName, specList);
                            }
                        }
                        specAndBrandMap.put("brandList", brandList);
                        specAndBrandMap.put("specList", specList);
                        //封装结果返回数据
                        return specAndBrandMap;
                    }

组合查询构建对象[布尔查询构建对象] boolQueryBuilder 依赖于
            1.     查询构建对象  QueryBuilders  
                    1.1 设置过滤 [注意事项: 若是用户没有选择分类的请求怎么处理?]  
                            1.1.1 设置根据分类过滤查询   TermQueryBuilder termQueryBuilder = QueryBuilders.termQuery("category", category);
                            1.1.2 设置根据品牌过滤查询   TermQueryBuilder termQueryBuilder = QueryBuilders.termQuery("brand", brand);

                            1.1.3 设置根据规格过滤查询  
                            [注意事项: 1. 因为规格存入的是object类型, 因为规格有多个, 所以导致规格不管是key, 还是value在查询的时候都会按照规则进行切分词, 我们过去查询, key也就是字段是一个整体, 不需要切分词
                                            2. 使用在key中拼接上.keyword这样的字符串, 让他临时转换为keyword类型不进行切分词]
                            TermQueryBuilder termQueryBuilder = QueryBuilders.termQuery(key + ".keyword", paramMap.get(key));   

                            1.1.4 设置价格区间查询
                            [注意事项: 1. 以字符串过过来的参数格式为  0-1000   1000-3000 5000-* 这样的 所以我们要切分一下 2. 切分后形成两个元素,  数组第一个元素就是价格区间的最小值. 第二个元素就是价格区间的最大值]
                                    tring[] priceArr = price.split("-");
                                    //最小值不等于0
                                    if (!"0".equals(priceArr[0])) {
                                        RangeQueryBuilder rangQueryBuilder = QueryBuilders.rangeQuery(price).gte(priceArr[0]);
                                        boolQueryBuilder.filter(rangQueryBuilder);
                                    }
                                    //最大值不等于*
                                    if (!"*".equals(priceArr[1])) {
                                        RangeQueryBuilder rangeQueryBuilder = QueryBuilders.rangeQuery(price).lte(priceArr[1]);
                                        boolQueryBuilder.filter(rangeQueryBuilder);
                                    }
                    1.2 把查询构建对象依赖到到分组查询对象中   
                                1.2.1 boolQueryBuilder.filter(termQueryBuilder);
                    1.3 把带过滤条件的布尔查询构建对象 依赖到 查询请求对象中    searchSourceBuilder.query(boolQueryBuilder);
                    1.4 把查询请求构建对象 依赖到查询[搜索]请求对象中         searchRequest.source(searchSourceBuilder);
                    1.5 通过查询请求对象发送请求, 返回响应对象        SearchResponse searchResponse = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);
                    1.6 通过响应对象获取结果集    [获取普通结果并转换为map]hit.getSourceAsMap();
                    1.7 处理结果集
                    1.8 把结果集封装为前端需要的数据返回    
********************完整代码***********************
@Service
public class SearchServiceImpl implements SearchService {

    @Autowired
    private RestHighLevelClient restHighLevelClient;

    @Autowired
    private BrandFeign brandFeign;

    @Autowired
    private SpecFeign specFeign;

    @Autowired
    private RedisTemplate redisTemplate;

    //设置每页查询条数据
    public final static Integer PAGE_SIZE = 4;

    @Override
    public Map search(Map<String, String> paramMap) throws Exception {
        Map<String, Object> resultMap = new HashMap<>();
        
        /**
         * 1. 获取查询参数
         */
        //查询关键字
        String keywords = paramMap.get("keywords");
        //当前页数
        String pageNo = paramMap.get("pageNo");
        //按照什么域进行排序
        String sort = paramMap.get("sort");
        //排序方式是升序还是降序
        String sortOrder = paramMap.get("sortOrder");
        //获取消费者选择的分类名称
        String category = paramMap.get("category");
        //消费者选中的品牌过滤条件
        String brand = paramMap.get("brand");
        //消费者选中的价格过滤条件
        String price = paramMap.get("price");

        /**
         * 2. 创建查询需要的对象
         */
        //创建查询对象, 指定索引库名称 , _index
        SearchRequest searchRequest = new SearchRequest("sku");
        //指定查询的索引库中的类型 _type
        searchRequest.types("doc");
        //创建查询构建对象
        SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
        //创建布尔查询(组合查询对象)
        BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();


        /**
         * 3. 设置根据关键字查询(关键字需要中文分词)
         */
        MatchQueryBuilder nameQueryBuilder = QueryBuilders.matchQuery("name", keywords);
        //将根据名称查询条件放入组合查询对象中
        boolQueryBuilder.must(nameQueryBuilder);

        /**
         * 4. 设置高亮查询
         */
        HighlightBuilder highlightBuilder = new HighlightBuilder();
        //设置在哪个域中高亮显示
        highlightBuilder.field("name");
        //设置高亮前缀
        highlightBuilder.preTags("<em style=\"color:red\">");
        //设置高亮后缀
        highlightBuilder.postTags("</em>");
        searchSourceBuilder.highlighter(highlightBuilder);

        /**
         * 5. 设置分页查询
         */
        if (StringUtils.isEmpty(pageNo)) {
            paramMap.put("pageNo", "1");
            resultMap.put("pageNo", "1");
        }
        Integer pageNoTemp = Integer.parseInt(pageNo);
        if (pageNoTemp < 1) {
            paramMap.put("pageNo", "1");
            resultMap.put("pageNo", "1");
            pageNoTemp = 1;
        }
        //从第几条开始查询
        Integer start = (pageNoTemp - 1) * PAGE_SIZE;
        //从第几条开始查询
        searchSourceBuilder.from(start);
        //每页查询多少条数据
        searchSourceBuilder.size(PAGE_SIZE);

        /**
         * 6. 设置排序查询
         */
        if (!StringUtils.isEmpty(sort)) {
            //升序
            if ("ASC".equals(sortOrder)) {
                searchSourceBuilder.sort(sort, SortOrder.ASC);
            }
            //降序
            if ("DESC".equals(sortOrder)) {
                searchSourceBuilder.sort(sort, SortOrder.DESC);
            }
        }


        /**
         * 7. 设置根据分类聚合查询
         * 其实就是按照分类名称分组查询, 目的是找到根据当前的关键字, 查询出对应的分类, 并且要去除掉重复的分类
         * tems中传入的是聚合组的名称, 可以随意起名, 但是不要重复
         */
        TermsAggregationBuilder termsAggregationBuilder = AggregationBuilders.terms("categoryGroup").field("categoryName");
        searchSourceBuilder.aggregation(termsAggregationBuilder);

        /**
         * 8. 设置根据分类过滤查询
         */
        if (!StringUtils.isEmpty(category)) {
            TermQueryBuilder categoryFileter = QueryBuilders.termQuery("categoryName", category);
            boolQueryBuilder.filter(categoryFileter);
        }

        /**
         * 9. 设置根据品牌过滤查询
         */
        if (!StringUtils.isEmpty(brand)) {
            TermQueryBuilder brandFileter = QueryBuilders.termQuery("brandName", brand);
            boolQueryBuilder.filter(brandFileter);
        }

        /**
         * 10. 设置根据规格过滤查询
         */
        if (paramMap != null) {
            //遍历传入参数所有的key
            for (String paramKey : paramMap.keySet()) {
                //找到传入参数的key以spec.开头这样的key作为过滤条件
                if (paramKey.startsWith("spec.")) {
                    //因为规格存入的是object类型, 因为规格有多个, 所以导致规格不管是key, 还是value在查询的
                    //时候都会按照规则进行切分词, 我们过去查询, key也就是域名是一个整体, 不需要切分词
                    //使用在key中拼接上.keyword这样的字符串, 让他临时转换为keyword类型不进行切分词.
                    TermQueryBuilder specFileter = QueryBuilders.termQuery(paramKey + ".keyword", paramMap.get(paramKey));
                    boolQueryBuilder.filter(specFileter);
                }
            }
        }


        /**
         * 11. 设置根据价格过滤查询
         *  价格是区间范围值: 0-1000   1000-2000   2000-3000  5000-*
         */
        if (!StringUtils.isEmpty(price)) {
            //切分价格, 得到价格最小值和最大值
            String[] priceArray = price.split("-");
            //最小值不为0
            if (!"0".equals(priceArray[0])) {
                //大于等于最小值
                RangeQueryBuilder gteRange = QueryBuilders.rangeQuery("price").gte(priceArray[0]);
                boolQueryBuilder.filter(gteRange);
            }
            //最大值不为*
            if (!"*".equals(priceArray[1])) {
                //小于等于最大值
                RangeQueryBuilder lteRange = QueryBuilders.rangeQuery("price").lte(priceArray[1]);
                boolQueryBuilder.filter(lteRange);
            }
        }



        /**
         * 12. 查询并获取返回的结果
         */
        //将组合查询条件放入查询构建对象中
        searchSourceBuilder.query(boolQueryBuilder);
        //将查询构建对象放入查询请求中
        searchRequest.source(searchSourceBuilder);
        SearchResponse searchResponse = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);
        /**
         * 13. 获取查询到的结果集, 封装后返回
         */
        SearchHits searchHits = searchResponse.getHits();
        //获取查询到的中条数据
        long totalHits = searchHits.getTotalHits();
        resultMap.put("total", totalHits);

        //获取查询结果集
        SearchHit[] hits = searchHits.getHits();
        List rows = new ArrayList();
        if (hits != null) {
            for (SearchHit hit : hits) {
                //获取普通的查询出来的一条数据(不带高亮名称)
                Map<String, Object> sourceAsMap = hit.getSourceAsMap();

                //获取高亮结果
                Map<String, HighlightField> highlightFields = hit.getHighlightFields();
                if (highlightFields != null) {
                    HighlightField highlightField = highlightFields.get("name");
                    Text[] fragments = highlightField.fragments();
                    if (fragments != null && fragments.length > 0) {
                        //将高亮名称放入查询出来的数据中, 覆盖原来不带高亮的名称
                        sourceAsMap.put("name", fragments[0].toString());
                    }
                }

                rows.add(sourceAsMap);
            }
        }
        resultMap.put("rows", rows);
        
        
        /**
         * 14. 获取根据分类聚合查询结果
         */
        Aggregations aggregations = searchResponse.getAggregations();
        //将聚合结果转换成Map
        Map<String, Aggregation> asMap = aggregations.getAsMap();
        //根据分组名称获取聚合的具体结果
        Terms terms =  (Terms)asMap.get("categoryGroup");
        //获取分组后集合中的具体内容
        List<? extends Terms.Bucket> buckets = terms.getBuckets();
        List<String> categoryList = new ArrayList<>();
        if (buckets != null) {
            for (Terms.Bucket bucket : buckets) {
                categoryList.add(bucket.getKeyAsString());
            }
        }
        resultMap.put("categoryList", categoryList);

        /**
         * 15. 获取根据哪个分类名称来查询对应的品牌集合和规格集合
         */
        //如果消费者没有选择具体的分类, 则根据聚合出来的分类集合中的第一个分类查询对应的品牌集合和规格集合
        if (StringUtils.isEmpty(category)) {
            if (categoryList != null && categoryList.size() > 0) {
                category = categoryList.get(0);
            }
        }

        //判断如果分类名称不为空, 则根据分类名称查询对应的品牌集合和规格集合
        if (!StringUtils.isEmpty(category)) {
            Map<String, Object> brandAndSpecList = findBrandAndSpecListByCategoryName(category);
            resultMap.putAll(brandAndSpecList);
        }

        return resultMap;
    }


    /**
     * 根据分类名称查询对应的品牌集合和规格集合
     * @param categoryName  分类名称
     * @return
     */
    private Map<String, Object> findBrandAndSpecListByCategoryName(String categoryName) {
        Map<String, Object> specAndBrandMap = new HashMap<>();
        /**
         * 1. 根据分类名称查询对应的品牌集合
         */
        List<Map> brandList = (List<Map>)redisTemplate.boundHashOps(Constants.REDIS_BRANDLIST).get(categoryName);
        if (brandList == null || brandList.size() == 0) {
            Result brandResult = brandFeign.findListByCategoryName(categoryName);
            if (brandResult.isFlag()) {
                brandList = (List<Map>) brandResult.getData();
                redisTemplate.boundHashOps(Constants.REDIS_BRANDLIST).put(categoryName, brandList);
            }
        }


        /***
         * 2. 根据分类名称查询对应的规格集合
         */
        List<Map> specList = (List<Map>)redisTemplate.boundHashOps(Constants.REDIS_SPECLIST).get(categoryName);
        if (specList == null || specList.size() == 0) {
            Result specResult = specFeign.findSpecListByCategoryId(categoryName);
            if (specResult.isFlag()) {
                specList = (List<Map>) specResult.getData();
                redisTemplate.boundHashOps(Constants.REDIS_SPECLIST).put(categoryName, specList);
            }
        }

        /**
         * 3. 将品牌集合和规格集合返回
         */
        specAndBrandMap.put("brandList", brandList);
        specAndBrandMap.put("specList", specList);
        return specAndBrandMap;
    }
}

posted on 2019-07-16 22:28  多边形  阅读(196)  评论(0)    收藏  举报