面试展示:网上商城项目搜索微服务模块展示

    技术2025-10-26  10

    一、建立商品索引库

    首先要分析商品的参数和页面上需要展示的内容来分析商品索引库字段,搜索出来的大图上展示的是每个spu部分信息以及每个spu下的各个sku部分信息,接着开始分析分词字段的内容,用户搜索的时候输入的是商品的标题、分类或是品牌,所以分词字段由商品的这三个属性拼接而成,选用的是ik_max_word分词器,接着设计用于过滤的字段,一些不适合作为索引的字段设置为index = false,再加入一些商品三级分类字段,价格字段便于页面的展示,最后调用创建索引库的api方法。 @Data @Document(indexName = "goods", type = "docs", shards = 1, replicas = 0) public class Goods implements Serializable { @Id private Long id; //spuId @Field(type = FieldType.Text, analyzer = "ik_max_word") private String all; //所有需要被搜索的信息,包含标题,分类,品牌 //过滤条件 private Long brandId; //品牌id private Map<String,Object> specs; //可搜索的规格参数,key是参数名,值是参数值,es可以放对象 //不适合索引的字段 @Field(type = FieldType.Keyword, index = false) private String skus; //sku信息的json结构 @Field(type = FieldType.Keyword, index = false) private String subTitle; //卖点 //便于页面的迅速展示 private Long cid1; // 1级分类id private Long cid2; // 2级分类id private Long cid3; // 3级分类id private Date createTime; //创建时间 private Set<Long> price; //价格 } 下面很重要的一步就是往索引库中添加添加数据,需要远程调用商品微服务中的方法获取数据,获取到数据之后将Spu对象传入buildGoods方法中,每100个商品对象作为一个集合调用loadData方法,最后在loadData方法中调用存索引集合的api方法。 public Goods buildGoods(Spu spu){ //构建下面要完成的TODO //分类 List<Category> categoryList = categoryClient.queryCategoryListByIds(Arrays.asList( spu.getCid1(),spu.getCid2(),spu.getCid3() )); if (CollectionUtils.isEmpty(categoryList)){ throw new LyException(ExceptionEnum.CATEGORY_NOT_FOUND); } List<String> names = categoryList.stream().map(Category::getName).collect(Collectors.toList()); //品牌 Brand brand = brandClient.queryBrandById(spu.getBrandId()); if (brand == null){ throw new LyException(ExceptionEnum.BRAND_NOT_FOUND); } //搜索字段 String all = spu.getTitle() + StringUtils.join(names," ") + brand.getName(); //价格需要查询sku List<Sku> skuList = goodsClient.querySkusBySpuId(spu.getId()); if (CollectionUtils.isEmpty(skuList)){ throw new LyException(ExceptionEnum.GOODS_NOT_FOUND); } //对sku进行处理,不需要sku的所有数据 List<Map<String,Object>> skus = new ArrayList<>(); //价格集合 Set<Long> priceList = new HashSet<>(); for (Sku sku:skuList){ Map<String,Object> map = new HashMap<>(); map.put("id",sku.getId()); map.put("title",sku.getTitle()); map.put("price",sku.getPrice()); map.put("image",StringUtils.substringBefore(sku.getImages(),",")); skus.add(map); priceList.add(sku.getPrice()); } //查询规格参数 List<SpecParam> params = specificationClient.queryParamList(null, spu.getCid3(), true); if (CollectionUtils.isEmpty(params)){ throw new LyException(ExceptionEnum.SPEC_PARAM_NOT_FOUND); } //查询商品详情 SpuDetail spuDetail = goodsClient.querySpuDetailBySpuId(spu.getId()); if (spuDetail == null){ throw new LyException(ExceptionEnum.GOODS_NOT_FOUND); } //通用规格参数 Map<Long,String> genericSpec = JsonUtils.parseMap(spuDetail.getGenericSpec(),Long.class,String.class); //特有规格参数k list Map<Long,List<String>> specialSpec = JsonUtils.nativeRead(spuDetail.getSpecialSpec(), new TypeReference<Map<Long, List<String>>>() {}); //规格参数, k v Map<String,Object> specs = new HashMap<>(); for (SpecParam param:params){ //规格名称 String key = param.getName(); //规格的值 Object value = ""; //是否是通用规格 if (param.getGeneric()){ value = genericSpec.get(param.getId()); //是否是数值类型 if (param.getNumeric() && StringUtils.isNotBlank(param.getSegments())){ value = chooseSegment(value.toString(),param); } }else { value = specialSpec.get(param.getId()); } //存入规格map specs.put(key,value); } //构建goods对象 Goods goods = new Goods(); goods.setAll(all); goods.setBrandId(spu.getBrandId()); goods.setCid1(spu.getCid1()); goods.setCid2(spu.getCid2()); goods.setCid3(spu.getCid3()); goods.setCreateTime(spu.getCreateTime()); goods.setId(spu.getId()); goods.setSubTitle(spu.getSubTitle()); //TO//DO: all search , price list, sku json,specs for search goods.setAll(all); goods.setPrice(priceList); goods.setSkus(JSON.toJSONString(skus)); goods.setSpecs(specs); return goods; } public void loadData(){ //查询spu int page = 1; int rows = 100; int size = 0; do { PageResult<Spu> result = goodsClient.querySpuByPage( page,rows,true,null); List<Spu> spuList = result.getItems(); if (CollectionUtils.isEmpty(spuList)){ break; } //构建goods库下的doc类型 List<Goods> goodsList = spuList.stream().map( searchService:: buildGoods).collect(Collectors.toList()); //存入索引库 goodsRepository.saveAll(goodsList); //翻页 page++; size = spuList.size(); }while (size == 100); }

    二、查询商品索引库

    首先指定要检索的字段,过滤掉一些不用检索的字段将搜索的结果进行分页处理既有搜索条件又有过滤条件时要用BoolQuery的must方法来整合,对应的是buildBasicQuery方法最后对结果按分类和品牌进行聚合,规定商品分类存在而且数量是 1, 则可以聚合规格参数,因为分类不同的话它们之间的规格参数显然是不同的 /** * 搜索 * @param request 校验已经在pojo完成 * @return */ public PageResult<Goods> search(SearchRequest request) { Integer page = request.getPage() - 1; //elasticsearch第一页默认是 0 Integer size = request.getSize(); //创建查询构建器 NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder(); //结果过滤 queryBuilder.withSourceFilter( new FetchSourceFilter( new String[]{"id","subTitle","skus"}, null)); //分页 queryBuilder.withPageable(PageRequest.of(page,size)); //搜索条件,还有过滤,使用boolquery整合 QueryBuilder basicQuery = buildBasicQuery(request); queryBuilder.withQuery(basicQuery); //聚合分类和品牌 queryBuilder.addAggregation(AggregationBuilders.terms("category_agg").field("cid3")); queryBuilder.addAggregation(AggregationBuilders.terms("brand_agg").field("brandId")); //查询 AggregatedPage<Goods> result = template.queryForPage(queryBuilder.build(),Goods.class); if (result == null){ throw new LyException(ExceptionEnum.GOODS_NOT_FOUND); } //解析结果 //解析分页结果 Long total = result.getTotalElements(); Integer totalPage = result.getTotalPages(); List<Goods> goodsList = result.getContent(); //解析聚合结果 Aggregations aggs = result.getAggregations(); List<Category> categories = parseCategoryAgg(aggs.get("category_agg")); List<Brand> brands = parseBrandAgg(aggs.get("brand_agg")); //规格参数聚合 List<Map<String, Object>> specs = null; if (categories != null && categories.size() == 1){ //规定商品分类存在而且数量是 1, 则可以聚合规格参数 specs = buildSpecificaitonAgg(categories.get(0).getId(),basicQuery); } return new SearchResult(total,totalPage,goodsList,categories,brands,specs); } private QueryBuilder buildBasicQuery(SearchRequest request) { //创建bool查询 BoolQueryBuilder queryBuilder = QueryBuilders.boolQuery(); try { //用户输入无效的搜索词时,会先使用match方法检索商品索引库里的文档,使其直接返回null,减少操作 NativeSearchQueryBuilder nativeSearchQueryBuilder = new NativeSearchQueryBuilder(); nativeSearchQueryBuilder.withSourceFilter( new FetchSourceFilter( new String[]{"id","subTitle","skus"}, null)); nativeSearchQueryBuilder.withQuery(QueryBuilders.matchQuery("all", request.getKey())); nativeSearchQueryBuilder.withIndices("goods").withTypes("docs"); NativeSearchQuery searchQuery = nativeSearchQueryBuilder.build(); long count = template.count(searchQuery); if (count == 0){ return null; } }catch (Exception e){ return null; } //查询条件 queryBuilder.must(QueryBuilders.matchQuery("all", request.getKey())); //过滤条件 Map<String, String> map = request.getFilter(); for (Map.Entry<String,String> entry : map.entrySet()){ String key = entry.getKey(); //处理key if ("cid3".equals(key) || "brandId".equals(key)){ //分类和品牌无需处理 }else { key = "specs."+key+".keyword"; } String value = entry.getValue(); queryBuilder.filter(QueryBuilders.termQuery(key,value)); } return queryBuilder; }

    三、展示搜索结果

    用户输入手机这一大类之后

    用户选中小米品牌的手机和后置摄像头像素之后

    用户输入商品索引库中没有的关键词后

    四、遇到的问题和解决途径

    规格参数索引对象下的属性在聚合时忘记加keyword导致异常 queryBuilder.addAggregation(AggregationBuilders.terms(name).field(“specs.”+name+".keyword")); 是通过 通过观察下面的报错后解决的 java.lang.IllegalArgumentException: Fielddata is disabled on text fields by default. Set fielddata=true on [specs.操作系统] in order to load fielddata in memory by uninverting the inverted index. Note that this can however use significant memory. Alternatively use a keyword field instead. 没有用户搜索时输入商品索引库没有的内容没有处理,导致页面混乱 try { //用户输入无效的搜索词时,会先使用match方法检索商品索引库里的文档,使其直接返回null,减少操作 NativeSearchQueryBuilder nativeSearchQueryBuilder = new NativeSearchQueryBuilder(); nativeSearchQueryBuilder.withSourceFilter( new FetchSourceFilter( new String[]{"id","subTitle","skus"}, null)); nativeSearchQueryBuilder.withQuery(QueryBuilders.matchQuery("all", request.getKey())); nativeSearchQueryBuilder.withIndices("goods").withTypes("docs"); NativeSearchQuery searchQuery = nativeSearchQueryBuilder.build(); long count = template.count(searchQuery); if (count == 0){ return null; } }catch (Exception e){ return null; } 页面上的和vue有关的一些代码请教前端组的学姐后解决了,因为设计问题一些向后端发送的数据也通过字符串的拼接解决了
    Processed: 0.009, SQL: 9