Elasticsearch仿京东、淘宝APP客户端的商品侧边栏筛选条件过滤和分页列表查询的实现案例

目录

需求分析

项目环境

API接口实现


需求分析

通过Elasticsearch完成商品列表查询和分类、品牌、规格参数的分组聚合统计查询

Elasticsearch仿京东、淘宝APP客户端的商品侧边栏筛选条件过滤和分页列表查询的实现案例_第1张图片

  1. 当用户输入关键字搜索后,查询出商品列表后点击右上角筛选按钮,能够展示该关键词下的全部商品分类(京东用的是二级分类,共3级分类)、品牌、和规格参数作为筛选条件。
  2. 商品分类支持单选、品牌支持多选、规格参数支持多选。
  3. 每当选中或取消选中筛选条件时,都能够实时更新筛选条件商品数量,并更新商品列表或点击确定更新。
  4. 注:这里需要注意的是,各个条件中的列表数据都会随着当前已选中的条件变化而变化,但当前操作的条件栏目(如:分类)中的数据是不变的(这里一般由前端控制),言外之意就是,这里的所有筛选条件都是从所有满足选中条件的商品中聚合出的数据,(例:当选中分类为【手机】时,需要重新调用获取查询条件的接口,这是接口返回的条件数据中分类类目中只有手机的分类,品牌类目里只有商品分类为手机的全部品牌,规格参数也同品牌一样,这样就实现了一个不断下钻聚合的过程)

 

项目环境

springboot + spring-boot-starter-data-elasticsearch(其中spring boot版本:2.1.6)

elasticsearch(版本:6.8.5)

这里环境搭建就不多赘述了,具体详看源代码


API接口实现

首先我们先实现商品列表的过滤分页查询接口

Spring Data Elasticsearch给我们提供了一系列的聚合查询方法

  1. 索引创建
    @Data
    @Document(indexName = "test", type = "product")
    //indexName索引名称 可以理解为数据库名 必须为小写 不然会报org.elasticsearch.indices.InvalidIndexNameException异常
    //type类型 可以理解为表名
    public class Product {
        /*
         * @Name: 商品Id
         * @Example: 1
         * @Description:
         */
        private Long id;
        /*
         * @Name: 品牌名称
         * @Example: 小米
         * @Description:
         */
        @Field(type = FieldType.Keyword)
        private String brand;
        /*
         * @Name: 分类id (1-手机 | 2-笔记本)
         * @Example:
         * @Description:
         */
        @Field(type = FieldType.Long)
        private Long category;
        /*
         * @Name: 商品名称
         * @Example: 小米9pro 5G版手机 钛银黑 12G+512G
         * @Description:
         */
        @Field(type = FieldType.Text)
        private String name;
        /*
         * @Name: 商品规格
         * @Example: [
                {
                    "attrName": "颜色",
                    "attrValue": "钛银黑"
                },
                {
                    "attrName": "内存",
                    "attrValue": "512G"
                }
            ]
         * @Description: 这里一定要使用nested(嵌套类型)
         * 【与object区别在于可以很好的处理数组对象的内部关系,在内部,嵌套对象将数组中的每个对象索引为单独的隐藏文档,这意味着可以独立于其他对象查询每个嵌套对象】
         * 详见:https://blog.csdn.net/laoyang360/article/details/82950393
         */
        @Field(type = FieldType.Nested)
        private List attrs;
    
        @Data
        public static class Attr{
            @Field(type = FieldType.Keyword)
            private String attrName;
            @Field(type = FieldType.Keyword)
            private String attrValue;
        }
    
    
        public interface FieldName {
            String ID = "id";
            String NAME = "name";
            String CATEGORY = "category";
            String BRAND = "brand";
            String ATTRS = "attrs";
            String ATTRS_NAME = "attrs.attrName";
            String ATTRS_VALUE = "attrs.attrValue";
        }
    
    }

     

  2. 首先定义查询参数
    @Data
    public class QueryVO {
    
        private Long id;
        /*
         * @Name: 品牌名称
         * @Example: ["小米", "华为"]
         * @Description:
         */
        private List brands = Lists.newArrayList();
        /*
         * @Name: 分类id (1-手机 | 2-笔记本)
         * @Example: 1
         * @Description:
         */
        private Long category;
        /*
         * @Name: 商品名称
         * @Example: 小米9pro 5G版手机 钛银黑 12G+512G
         * @Description:
         */
        private String name;
    
        /*
         * @Name: 规格参数
         * @Example: [
            {
                "attrName": "颜色",
                "attrValues": ["黑色"]
            },
            {
                "attrName": "内存",
                "attrValues": ["64G", "128G"]
            }
         ]
         * @Description:
         */
        private List attrs = Lists.newArrayList();
    }

     

  3. 分页查询接口实现
        // 商品分页查询
        @Override
        public Page search(QueryVO query, int page, int size) {
            Pageable pageable = PageRequest.of(page-1, size);
            return productRepository.search(this.addFilters(query), pageable);
        }
    
        // 过滤条件
        private BoolQueryBuilder addFilters(QueryVO query) {
            BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();
            List attrGroups = query.getAttrs();
            if (attrGroups != null) {
                // 多属性过滤查询,如(内存、颜色、屏幕尺寸、版本等等)
                for (AggVO.AttrGroup attrGroup : attrGroups) {
                    BoolQueryBuilder attrBoolQuery = QueryBuilders.boolQuery();
                    attrBoolQuery.filter(QueryBuilders.matchQuery(Product.FieldName.ATTRS_NAME, attrGroup.getAttrName()));
                    attrBoolQuery.filter(QueryBuilders.termsQuery(Product.FieldName.ATTRS_VALUE, attrGroup.getAttrValues())); // 这里多值匹配用【termsQuery】注意区分【termQuery】
                    // 使用NestedQuery查询(嵌套对象的过滤查询)                
                    NestedQueryBuilder nestedQueryBuilder = QueryBuilders.nestedQuery(Product.FieldName.ATTRS, attrBoolQuery, ScoreMode.None);
                    boolQueryBuilder.filter(nestedQueryBuilder);
                }
            }
            if (query.getId() != null) {
                // 商品Id查询
                boolQueryBuilder.filter(QueryBuilders.matchQuery(Product.FieldName.ID, query.getId()).operator(Operator.AND));
            }
            if (!query.getBrands().isEmpty()) {
                // 品牌查询
                boolQueryBuilder.filter(QueryBuilders.termsQuery(Product.FieldName.BRAND, query.getBrands()));
            }
            if (query.getCategory() != null) {
                // 分类查询
                boolQueryBuilder.filter(QueryBuilders.matchQuery(Product.FieldName.CATEGORY, query.getCategory()).operator(Operator.AND));
            }
            if (StringUtils.isNotBlank(query.getName())) {
                // 商品名称查询(这里暂时没做分词处理)
                boolQueryBuilder.filter(QueryBuilders.matchQuery(Product.FieldName.NAME, query.getName()).operator(Operator.AND));
            }
            return boolQueryBuilder;
        }

    这里可以将filter查询替换成must查询,区别在于filter性能更好一些,must要进行打分评估,也就是说要进行_score,而filter则不会。

其次我们再实现筛选条件聚合的接口

通过TermsAggregationBuilder、NestedAggregationBuilder来实现字段和内置嵌套对象字段的分组统计查询,具体代码如下:

    // 筛选条件聚合查询接口
    @Override
    public AggVO agg(QueryVO query) {
        Pageable pageable = PageRequest.of(0, 10);
        //检索条件
        BoolQueryBuilder boolQueryBuilder = this.addFilters(query);
        final String BRAND_AGG = "brand_agg";
        final String CATEGORY_AGG = "category_agg";
        final String ATTR_AGG = "attr_agg";
        final String ATTR_NAME_AGG = ATTR_AGG + "_name";
        final String ATTR_VALUE_AGG = ATTR_AGG+"_value";
        //聚合条件

        // 品牌聚合(取命中最多的前500个品牌)
        TermsAggregationBuilder brandAggBuilder = AggregationBuilders.terms(BRAND_AGG).field(Product.FieldName.BRAND).size(500);
        // 分类聚合(取命中最多的前20个)
        TermsAggregationBuilder categoryAggBuilder = AggregationBuilders.terms(CATEGORY_AGG).field(Product.FieldName.CATEGORY).size(20);
        // 内置对象-规格聚合
        NestedAggregationBuilder attrAggBuilder = AggregationBuilders.nested(ATTR_AGG, Product.FieldName.ATTRS);
        // 先聚合规格名称
        TermsAggregationBuilder attrNameAggBuilder = AggregationBuilders.terms(ATTR_NAME_AGG).field(Product.FieldName.ATTRS_NAME);
        // 再将名称分组后聚合规格值(取命中最多的前7个规格,聚合分组规格后再在每个分组规格中取命中最多的前15个参数值)
        attrNameAggBuilder.subAggregation(AggregationBuilders.terms(ATTR_VALUE_AGG).field(Product.FieldName.ATTRS_VALUE).size(15)).size(7);
        attrAggBuilder.subAggregation(attrNameAggBuilder);
        //构建查询
        SearchQuery searchQuery = new NativeSearchQueryBuilder()
                .withQuery(boolQueryBuilder) // 过滤条件
                .addAggregation(brandAggBuilder) // 品牌聚合
                .addAggregation(categoryAggBuilder) // 分类聚合
                .addAggregation(attrAggBuilder) // 规格参数聚合
                .withPageable(pageable) // 分页
                .build();
        AggregatedPage search = (AggregatedPage)productRepository.search(searchQuery);
        AggVO aggVO = new AggVO();
        // 符合条件的商品总数量
        aggVO.setTotalNum(search.getTotalElements());

        // 品牌聚合结果
        Terms brandAgg = (Terms)search.getAggregation(BRAND_AGG);
        for (Terms.Bucket bucket : brandAgg.getBuckets()) {
            String brandName = bucket.getKeyAsString();
            aggVO.getBrands().add(brandName);
        }

        // 分类聚合结果
        Terms categoryAgg = (Terms)search.getAggregation(CATEGORY_AGG);
        for (Terms.Bucket bucket : categoryAgg.getBuckets()) {
            long categoryId = bucket.getKeyAsNumber().longValue();
            aggVO.getCategories().add(categoryId);
        }

        // 规格参数聚合结果
        InternalNested attrNested = (InternalNested)search.getAggregation(ATTR_AGG);
        Map aggregationMap = attrNested.getAggregations().asMap();
        Terms attrNameAgg = (Terms)aggregationMap.get(ATTR_NAME_AGG);
        for (Terms.Bucket bucket : attrNameAgg.getBuckets()) {
            String attrName = bucket.getKeyAsString();
            AggVO.AttrGroup attrGroup = new AggVO.AttrGroup();
            attrGroup.setAttrName(attrName);
            Terms attrValueAgg = (Terms)bucket.getAggregations().asMap().get(ATTR_VALUE_AGG);
            for (Terms.Bucket subBucket : attrValueAgg.getBuckets()) {
                String attrValue = subBucket.getKeyAsString();
                attrGroup.getAttrValues().add(attrValue);
            }
            aggVO.getAttrs().add(attrGroup);
        }
        search.getAggregation(CATEGORY_AGG);
        return aggVO;
    }

最后再附上源代码:https://gitee.com/wangsai1109/springboot-es.git

你可能感兴趣的:(elasticsearch,springboot)