电商项目——搜索过滤

本文详细阐述了商品搜索过滤功能的优化过程,包括商品分类、品牌筛选的动态展示,以及如何根据搜索结果聚合分类和品牌数据,同时扩展了返回结果以包含分类、品牌信息。还介绍了如何利用Elasticsearch进行分页、排序和复杂查询,并展示了FeignClient和Service的整合实现。
摘要由CSDN通过智能技术生成

搜索过滤

过滤功能分析

整个过滤部分有3块:

  • 顶部的导航,已经选择的过滤条件展示:
    • 商品分类面包屑,根据用户选择的商品分类变化
    • 其它已选择过滤参数
  • 过滤条件展示,又包含3部分
    • 商品分类展示
    • 品牌展示
    • 其它规格参数
  • 展开或收起的过滤条件的按钮

顶部导航要展示的内容跟用户选择的过滤条件有关。

  • 比如用户选择了某个商品分类,则面包屑中才会展示具体的分类
  • 比如用户选择了某个品牌,列表中才会有品牌信息。

所以,这部分需要依赖第二部分:过滤条件的展示和选择。

展开或收起的按钮是否显示,取决于过滤条件现在有多少,如果有很多,那么就没必要展示。

生成分类和品牌过滤

先来看分类和品牌。在我们的数据库中已经有所有的分类和品牌信息。用户搜索的条件会对商品进行过滤,而在搜索结果中,不一定包含所有的分类和品牌,直接展示出所有商品分类,让用户选择显然是不合适的。无论是分类信息,还是品牌信息,都应该从搜索的结果商品中进行聚合得到。

扩展返回的结果

原来,我们返回的结果是PageResult对象,里面只有total、totalPage、items3个属性。但是现在要对商品分类和品牌进行聚合,数据显然不够用,我们需要对返回的结果进行扩展,添加分类和品牌的数据。

分类:页面显示了分类名称,但背后肯定要保存id信息。所以至少要有id和name

品牌:页面展示的有logo,有文字,当然肯定有id,基本上是品牌的完整数据

新建一个类,继承PageResult,然后扩展两个新的属性:分类集合和品牌集合:

public class SearchResult extends PageResult<Goods>{

    private List<Category> categories;

    private List<Brand> brands;

    public SearchResult(Long total, Integer totalPage, List<Goods> items, List<Category> categories, List<Brand> brands) {
        super(total, totalPage, items);
        this.categories = categories;
        this.brands = brands;
    }
}

聚合商品分类和品牌

修改搜索的业务逻辑,对分类和品牌聚合。

因为索引库中只有id,所以根据id聚合,然后再根据id去查询完整数据。

所以,商品微服务需要提供一个接口:根据品牌id集合,批量查询品牌。

提供查询品牌接口

BrandApi

@RequestMapping("brand")
public interface BrandApi {

    @GetMapping("list")
    List<Brand> queryBrandByIds(@RequestParam("ids") List<Long> ids);
}

BrandController

/**
     * 根据多个id查询品牌
     * @param ids
     * @return
     */
@GetMapping("list")
public ResponseEntity<List<Brand>> queryBrandByIds(@RequestParam("ids") List<Long> ids){
    List<Brand> list = this.brandService.queryBrandByIds(ids);
    if(list == null){
        new ResponseEntity<>(HttpStatus.NOT_FOUND);
    }
    return ResponseEntity.ok(list);
}

BrandService

public List<Brand> queryBrandByIds(List<Long> ids) {
    return this.brandMapper.selectByIdList(ids);
}

BrandMapper

继承通用mapper的 SelectByIdListMapper即可

public interface BrandMapper extends Mapper<Brand>, SelectByIdListMapper<Brand,Long> {}

搜索功能改造

添加BrandClient

@FeignClient("item-service")
public interface BrandClient extends BrandApi {
}

修改SearchService:

@Service
public class SearchService {

    @Autowired
    private GoodsRepository goodsRepository;

    @Autowired
    private CategoryClient categoryClient;

    @Autowired
    private BrandClient brandClient;

    private static final Logger logger = LoggerFactory.getLogger(SearchService.class);

    public PageResult<Goods> search(SearchRequest request) {
        // 判断是否有搜索条件,如果没有,直接返回null。不允许搜索全部商品
        if (StringUtils.isBlank(request.getKey())) {
            return null;
        }

        // 1、构建查询条件
        NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder();
        // 通过sourceFilter设置返回的结果字段,我们只需要id、skus、subTitle
        queryBuilder.withSourceFilter(new FetchSourceFilter(
                new String[]{"id", "skus", "subTitle"}, null));

        // 1.1、基本查询
        queryBuilder.withQuery(QueryBuilders.matchQuery("all", request.getKey()));

        // 1.2.分页排序
        searchWithPageAndSort(queryBuilder,request);
        
        // 1.3、聚合
        String categoryAggName = "category"; // 商品分类聚合名称
        String brandAggName = "brand"; // 品牌聚合名称
        // 对商品分类进行聚合
        queryBuilder.addAggregation(AggregationBuilders.terms(categoryAggName).field("cid3"));
        // 对品牌进行聚合
        queryBuilder.addAggregation(AggregationBuilders.terms(brandAggName).field("brandId"));

        // 2、查询,获取结果
        AggregatedPage<Goods> pageInfo = (AggregatedPage<Goods>) this.goodsRepository.search(queryBuilder.build());

        // 3、解析查询结果
        // 3.1、分页信息
        Long total = pageInfo.getTotalElements();
        int totalPage = (total.intValue() + request.getSize() - 1) / request.getSize();
     	// 3.2、商品分类的聚合结果
        List<Category> categories = 
            getCategoryAggResult(pageInfo.getAggregation(categoryAggName));
        // 3.3、品牌的聚合结果
        List<Brand> brands = getBrandAggResult(pageInfo.getAggregation(brandAggName));

        // 返回结果
        return new SearchResult(total, totalPage, pageInfo.getContent(), categories, brands);
    }
    

    // 解析品牌聚合结果
    private List<Brand> getBrandAggResult(Aggregation aggregation) {
        try {
            LongTerms brandAgg = (LongTerms) aggregation;
            List<Long> bids = new ArrayList<>();
            for (LongTerms.Bucket bucket : brandAgg.getBuckets()) {
                bids.add(bucket.getKeyAsNumber().longValue());
            }
            // 根据id查询品牌
            return this.brandClient.queryBrandByIds(bids);
        } catch (Exception e){
            logger.error("品牌聚合出现异常:", e);
            return null;
        }
    }

    // 解析商品分类聚合结果
    private List<Category> getCategoryAggResult(Aggregation aggregation) {
        try{
            List<Category> categories = new ArrayList<>();
            LongTerms categoryAgg = (LongTerms) aggregation;
            List<Long> cids = new ArrayList<>();
            for (LongTerms.Bucket bucket : categoryAgg.getBuckets()) {
                cids.add(bucket.getKeyAsNumber().longValue());
            }
            // 根据id查询分类名称
            List<String> names = this.categoryClient.queryNameByIds(cids);

            for (int i = 0; i < names.size(); i++) {
                Category c = new Category();
                c.setId(cids.get(i));
                c.setName(names.get(i));
                categories.add(c);
            }
            return categories;
        } catch (Exception e){
            logger.error("分类聚合出现异常:", e);
            return null;
        }
    }

    // 构建基本查询条件
    private void searchWithPageAndSort(NativeSearchQueryBuilder queryBuilder, SearchRequest request) {
        // 准备分页参数
        int page = request.getPage();
        int size = request.getSize();

        // 1、分页
        queryBuilder.withPageable(PageRequest.of(page - 1, size));
        // 2、排序
        String sortBy = request.getSortBy();
        Boolean desc = request.getDescending();
        if (StringUtils.isNotBlank(sortBy)) {
            // 如果不为空,则进行排序
            queryBuilder.withSort(SortBuilders.fieldSort(sortBy).order(desc ? SortOrder.DESC : SortOrder.ASC));
        }
    }
}

List<Map<String,Object>>

public class SearchResult extends PageResult<Goods>{

    private List<Category> categories;// 分类过滤条件

    private List<Brand> brands; // 品牌过滤条件

    private List<Map<String,String>> specs; // 规格参数过滤条件

    public SearchResult(Long total, Integer totalPage, List<Goods> items,
                        List<Category> categories, List<Brand> brands,
                        List<Map<String,String>> specs) {
        super(total, totalPage, items);
        this.categories = categories;
        this.brands = brands;
        this.specs = specs;
    }
}

判断是否需要聚合

首先,在聚合得到商品分类后,判断分类的个数,如果是1个则进行规格聚合:

// 判断商品分类数量,看是否需要对规格参数进行聚合
List<Map<String,Object>> specs=null;
if (categories.size()==1){
    specs=getSpecs(categories.get(0).getId(),query);
}

获取需要聚合的规格参数

然后,需要根据商品分类,查询所有可用于搜索的规格参数:

List<SpecParam> specParams = this.specificationClient.querySpecParams(null, id, true, null);

要注意的是,这里需要根据id查询规格,而规格参数接口需要从商品微服务提供

商品微服务:tt-item-interface中提供接口:

@RequestMapping("spec")
public interface SpecificationApi {

    @GetMapping("/params")
    List<SpecParam> querySpecParam(SpecParam specParam);
}
@FeignClient("item-service")
public interface SpecificationClient extends SpecificationApi {
}

聚合规格参数

因为规格参数保存时不做分词,因此其名称会自动带上一个.keyword后缀:

specParams.forEach(p->{
            String key = p.getName();
            queryBuilder.addAggregation(AggregationBuilders.terms(key).field("specs." + key + ".keyword"));
        });

最终的完整代码

@Service
public class IndexService {

    @Autowired
    private GoodsRepository goodsRepository;

    @Autowired
    private ElasticsearchTemplate esTemplate;

    @Autowired
    private CategoryClient categoryClient;

    @Autowired
    private BrandClient brandClient;

    @Autowired
    private GoodsClient goodsClient;

    @Autowired
    private SpecificationClient specificationClient;

    private static final Logger logger = LoggerFactory.getLogger(IndexService.class);

    private static final ObjectMapper mapper = new ObjectMapper();

    public PageResult<Goods> search(SearchRequest request) {
        // 判断是否有搜索条件,如果没有,直接返回null。不允许搜索全部商品
        if (StringUtils.isBlank(request.getKey())) {
            return null;
        }

        // 1、构建查询条件
        NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder();
        // 通过sourceFilter设置返回的结果字段,我们只需要id、skus、subTitle
        queryBuilder.withSourceFilter(new FetchSourceFilter(
            new String[]{"id", "skus", "subTitle"}, null));

        // 1.1、基本查询
        queryBuilder.withQuery(QueryBuilders.matchQuery("all", key).operator(Operator.AND));

        // 1.2.分页排序
        searchWithPageAndSort(queryBuilder, request);

        // 1.3、聚合
        String categoryAggName = "category"; // 商品分类聚合名称
        String brandAggName = "brand"; // 品牌聚合名称
        // 对商品分类进行聚合
        queryBuilder.addAggregation(AggregationBuilders.terms(categoryAggName).field("cid3"));
        // 对品牌进行聚合
        queryBuilder.addAggregation(AggregationBuilders.terms(brandAggName).field("brandId"));
        // 对规格参数聚合


        // 2、查询,获取结果
        AggregatedPage<Goods> pageInfo = (AggregatedPage<Goods>) this.goodsRepository.search(queryBuilder.build());

        // 3、解析查询结果
        // 3.1、分页信息
        Long total = pageInfo.getTotalElements();
        int totalPage = (total.intValue() + request.getSize() - 1) / request.getSize();
        // 3.2、商品分类的聚合结果
        List<Category> categories = getCategoryAggResult(pageInfo.getAggregation(categoryAggName));
        // 3.3、品牌的聚合结果
        List<Brand> brands = getBrandAggResult(pageInfo.getAggregation(brandAggName));

        // 判断商品分类数量,看是否需要对规格参数进行聚合
        List<Map<String, Object>> specs = null;
        if (categories.size() == 1) {
            // 如果分类只剩下一个,才进行规格参数过滤
            specs = getSpecs(categories.get(0).getId(), query);
        }
        // 返回结果
        return new SearchResult(total, totalPage, pageInfo.getContent(), categories, brands, specs);
    }

    // 聚合规格参数
    private List<Map<String, Object>> getSpecs(Long cid, QueryBuilder query) {
        try {
            // 根据分类查询规格
            List<SpecParam> params =
                this.specificationClient.querySpecParam(null, cid, true, null);

            // 创建集合,保存规格过滤条件
            List<Map<String, Object>> specs = new ArrayList<>();

            NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder();
            queryBuilder.withQuery(query);

            // 聚合规格参数
            params.forEach(p -> {
                String key = p.getName();
                queryBuilder.addAggregation(AggregationBuilders.terms(key).field("specs." + key + ".keyword"));

            });

            // 查询
            Map<String, Aggregation> aggs = this.esTemplate.query(queryBuilder.build(),
                                                                  SearchResponse::getAggregations).asMap();

            // 解析聚合结果
            params.forEach(param -> {
                Map<String, Object> spec = new HashMap<>();
                String key = param.getName();
                spec.put("k", key);
                StringTerms terms = (StringTerms) aggs.get(key);
                spec.put("options", terms.getBuckets().stream().map(StringTerms.Bucket::getKeyAsString));
                specs.add(spec);
            });

            return specs;
        }catch (Exception e){
            logger.error("规格聚合出现异常:", e);
            return null;
        }
    }

    // 解析品牌聚合结果
    private List<Brand> getBrandAggResult(Aggregation aggregation) {
        try {
            LongTerms brandAgg = (LongTerms) aggregation;
            List<Long> bids = new ArrayList<>();
            for (LongTerms.Bucket bucket : brandAgg.getBuckets()) {
                bids.add(bucket.getKeyAsNumber().longValue());
            }
            // 根据id查询品牌
            return this.brandClient.queryBrandByIds(bids);
        } catch (Exception e){
            logger.error("品牌聚合出现异常:", e);
            return null;
        }
    }

    // 解析商品分类聚合结果
    private List<Category> getCategoryAggResult(Aggregation aggregation) {
        try{
            List<Category> categories = new ArrayList<>();
            LongTerms categoryAgg = (LongTerms) aggregation;
            List<Long> cids = new ArrayList<>();
            for (LongTerms.Bucket bucket : categoryAgg.getBuckets()) {
                cids.add(bucket.getKeyAsNumber().longValue());
            }
            // 根据id查询分类名称
            List<String> names = this.categoryClient.queryNameByIds(cids);

            for (int i = 0; i < names.size(); i++) {
                Category c = new Category();
                c.setId(cids.get(i));
                c.setName(names.get(i));
                categories.add(c);
            }
            return categories;
        } catch (Exception e){
            logger.error("分类聚合出现异常:", e);
            return null;
        }
    }

    // 构建基本查询条件
    private void searchWithPageAndSort(NativeSearchQueryBuilder queryBuilder, SearchRequest request) {
        // 准备分页参数
        int page = request.getPage();
        int size = request.getSize();

        // 1、分页
        queryBuilder.withPageable(PageRequest.of(page - 1, size));
        // 2、排序
        String sortBy = request.getSortBy();
        Boolean desc = request.getDescending();
        if (StringUtils.isNotBlank(sortBy)) {
            // 如果不为空,则进行排序
            queryBuilder.withSort(SortBuilders.fieldSort(sortBy).order(desc ? SortOrder.DESC : SortOrder.ASC));
        }
    }
    // ...
}

后台添加过滤条件

拓展请求对象

在请求类:SearchRequest中添加属性,接收过滤属性。过滤属性都是键值对格式,但是key不确定,所以用一个map来接收即可。

public class SearchRequest {
    @Autowired
    private SearchConfig searchConfig;

    private String sortBy;
    private Boolean descending;
    private String key;// 搜索条件
    private Integer page;// 当前页
    private Map<String,String> filter;
//    private static final Integer DEFAULT_SIZE = 20;// 每页大小,不从页面接收,而是固定大小

    private static final Integer DEFAULT_PAGE = 1;// 默认页

    public Integer getPage() {
        if(page == null){
            return DEFAULT_PAGE;
        }
        // 获取页码时做一些校验,不能小于1
        return Math.max(DEFAULT_PAGE, page);
    }

}

添加过滤条件

 queryBuilder.withSourceFilter(new FetchSourceFilter(new String[]{"id","skus","subTitle"}, null));
        QueryBuilder query = buildBasicQueryWithFilter(request);
        queryBuilder.withQuery(query);
        // 2.2、基本查询
//        queryBuilder.withQuery(QueryBuilders.matchQuery("all", key));
private QueryBuilder buildBasicQueryWithFilter(SearchRequest request) {
    BoolQueryBuilder queryBuilder = QueryBuilders.boolQuery();
    // 基本查询条件
    queryBuilder.must(QueryBuilders.matchQuery("all", request.getKey()).operator(Operator.AND));
    // 过滤条件构建器
    BoolQueryBuilder filterQueryBuilder = QueryBuilders.boolQuery();
    // 整理过滤条件
    Map<String, String> filter = request.getFilter();
    for (Map.Entry<String, String> entry : filter.entrySet()) {
        String key = entry.getKey();
        String value = entry.getValue();
        // 商品分类和品牌要特殊处理
        if (key != "cid3" && key != "brandId") {
            key = "specs." + key + ".keyword";
        }
        // 字符串类型,进行term查询
        filterQueryBuilder.must(QueryBuilders.termQuery(key, value));
    }
    // 添加过滤条件
    queryBuilder.filter(filterQueryBuilder);
    return queryBuilder;
}
### 电商项目性能测试中的场景划分方法 #### 明确业务需求和目标 为了有效地进行性能测试,必须深入了解被测对象——即电商项目本身。这不仅涉及理解其所属行业以及所提供的服务种类,还包括全面掌握平台上的每一项功能特性[^1]。 #### 核心业务识别 基于上述认识,下一步是确定哪些具体的业务活动应当成为重点考察对象。对于电商平台而言,通常会考虑如下几个方面作为主要关注点: - **商品浏览与查询** - 用户通过不同分类、标签等方式查找感兴趣的商品。 - **加入购物车操作** - 测试当大量并发请求尝试向购物车内添加相同或不同类型的产品时系统的响应情况。 - **结算支付流程** - 模拟多个客户同时完成订单创建直至付款确认全过程的压力状况。 - **促销活动参与** - 特别是在大型节假日或其他特殊时期内推出的限时折扣等活动期间的表现评估。 以上每种情形都可能构成独立的测试案例集的一部分[^2]。 #### 划分具体测试场景 一旦明确了核心业务之后,则可以进一步细分为更精确的具体测试场景。例如,在“商品浏览与查询”的大类下还可以继续拆解成若干子类别,像按价格区间筛选、品牌过滤等功能;同样,“加入购物车操作”也可以扩展至包含批量加购、跨店铺混合购买等多种模式下的表现验证。此外,还需考虑到异常情况下(如库存不足)系统应具备怎样的处理机制以保障用户体验不受影响[^3]。 ```python def define_test_scenarios(core_businesses, abnormal_situations): scenarios = [] for business in core_businesses: normal_cases = generate_normal_cases(business) edge_cases = handle_edge_cases(abnormal_situations[business]) combined_tests = combine(normal_cases, edge_cases) scenarios.extend(combined_tests) return scenarios core_businesses = ["product_search", "add_to_cart", "checkout_payment"] abnormal_situations = { "product_search": ["out_of_stock_items"], "add_to_cart": ["invalid_quantity_input"], "checkout_payment": ["failed_transaction"] } scenarios = define_test_scenarios(core_businesses, abnormal_situations) print(scenarios) ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值