谷粒商城商品检索服务笔记整理

博主 49 2023-11-21

对应b站视频 视频

视频使用的API为ES7.x的API,本人使用ES8.x 链式风格JAVA API
折腾了挺久的,基本弄明白链式API的用法了,实际上这里就是根据业务写出es DSL语句,再把DSL语句翻译成Java 语言的写法。

参考资料:

  1. https://blog.csdn.net/pyd1040201698/article/details/108354264
  2. https://www.elastic.co/guide/en/elasticsearch/client/java-api-client/current/java-client-javadoc.html
  3. https://blog.csdn.net/Oaklkm/article/details/130992152

给出DSL语句


//GET glmall_product/_search
GET /glmall_product/_search
{
  "query": {
    "bool": {
      "must": [
        {
          "match": {
            "skuTitle": "小米"
          }
        }
      ],
      "filter": [
        {
          "term": {
            "catalogId": "225"
          }
        },
        {
          "terms": {
            "brandId": [
              "11"
            ]
          }
        },
        {
          "nested": {
            "path": "attrs",
            "query": {
              "bool": {
                "must": [
                  {
                    "term": {
                      "attrs.attrId": {
                        "value": "9"
                      }
                    }
                  },
                  {
                    "terms": {
                      "attrs.attrValue": [
                        "红色",
                        "以官网信息为准"
                      ]
                    }
                  }
                ]
              }
            }
          }
        },
        {
          "nested": {
            "path": "attrs",
            "query": {
              "bool": {
                "must": [
                  {
                    "term": {
                      "attrs.attrId": {
                        "value": "23"
                      }
                    }
                  },
                  {
                    "terms": {
                      "attrs.attrValue": [
                        "3"
                      ]
                    }
                  }
                ]
              }
            }
          }
        },
        {
          "nested": {
            "path": "attrs",
            "query": {
              "bool": {
                "must": [
                  {
                    "term": {
                      "attrs.attrId": {
                        "value": "8"
                      }
                    }
                  },
                  {
                    "terms": {
                      "attrs.attrValue": [
                        "是"
                      ]
                    }
                  }
                ]
              }
            }
          }
        },
        {
          "term": {
            "hasStock": {
              "value": "true"
            }
          }
        },
        {
          "range": {
            "skuPrice": {
              "gte": 0,
              "lte": 6000
            }
          }
        }
      ]
    }
  },
  "sort": [
    {
      "skuPrice": {
        "order": "desc"
      }
    }
  ],
  "from": 0,
  "size": 2,
  "highlight": {
    "fields": {
      "skuTitle": {}
    },
    "pre_tags": "<b style='color: red'>",
    "post_tags": "</b>"
  },
  "aggs": {
    "brand_agg": {
      "terms": {
        "field": "brandId",
        "size": 10
      },
      "aggs": {
        "brand_name_agg": {
          "terms": {
            "field": "brandName",
            "size": 10
          }
        },
        "brand_img_agg": {
          "terms": {
            "field": "brandImg",
            "size": 10
          }
        }
      }
    },
    "catalog_agg": {
      "terms": {
        "field": "catalogId",
        "size": 10
      },
      "aggs": {
        "catalog_name_agg": {
          "terms": {
            "field": "catalogName",
            "size": 10
          }
        }
      }
    },
    "attr_agg": {
      "nested": {
        "path": "attrs"
      },
      "aggs": {
        "attr_id_agg": {
          "terms": {
            "field": "attrs.attrId",
            "size": 10
          },
          "aggs": {
            "attr_name_agg": {
              "terms": {
                "field": "attrs.attrName",
                "size": 10
              }
            },
            "attr_value_agg": {
              "terms": {
                "field": "attrs.attrValue",
                "size": 10
              }
            }
          }
        }
      }
    }
  }
}

Controller层

@Controller
public class ListController {
    @Resource
    MallESService mallESService;

    @GetMapping(value = {"/list.html"})
    private String search(SearchParam searchParam, Model model) {
        SearchResponseVo result = mallESService.search(searchParam);
        model.addAttribute("result", result);
        return "list";
    }
}

Service层

public interface MallESService {

    /**
     * @param searchParam 检索的参数
     * @return 检索出来的结果
     */
    SearchResponseVo search(SearchParam searchParam);
}

@Service("mallESService")
public class MallESServiceImpl implements MallESService {

    @Resource
    ElasticsearchClient client;

    @Override
    public SearchResponseVo search(SearchParam searchParam) {
        Function<SearchRequest.Builder, ObjectBuilder<SearchRequest>> searchFunction = buildSearchFunction(searchParam);
        SearchResponse<SkuEsTo> searchResponse = null;
        try {
            System.out.println("dsl function");
            System.out.println(searchFunction);
            searchResponse = client.search(searchFunction, SkuEsTo.class);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
        SearchResponseVo searchResponseVo = buildSearchResult(searchResponse, searchParam);

        return searchResponseVo;
    }

    /**
     * 将es的响应结果封装成我们的SearchResponseVo的格式
     *
     * @param searchResponse
     * @param searchParam
     * @return
     */
    private SearchResponseVo buildSearchResult(SearchResponse<SkuEsTo> searchResponse, SearchParam searchParam) {
        SearchResponseVo searchResponseVo = new SearchResponseVo();

        // 1. 返回查询到的所有商品
        List<Hit<SkuEsTo>> hits = searchResponse.hits().hits();
        List<SkuEsTo> products = hits.stream().map(Hit::source).collect(Collectors.toList());
        searchResponseVo.setProducts(products);

        // 命中的总记录数量
        assert searchResponse.hits().total() != null;
        long totalHit = searchResponse.hits().total().value();
        // 总共页码数量计算 page = (totalCount + pageSize - 1) / pageSize
        searchResponseVo.setTotalPages( (totalHit+EsConstant.PRODUCT_PAGESIZE-1) / EsConstant.PRODUCT_PAGESIZE);
        searchResponseVo.setPageNum(Long.valueOf(searchParam.getPageNo()));

        searchResponseVo.setTotal(totalHit);


        // 2. 当前商品的所有属性值
        Map<String, Aggregate> aggregations = searchResponse.aggregations();
        Aggregate attrAgg = aggregations.get("attr_agg");
        List<SearchResponseVo.AttrVO> arrtVoList = attrAgg.nested().aggregations().get("attr_id_agg").lterms().buckets().array().stream().map(innerBucket -> {
            SearchResponseVo.AttrVO attrVO = new SearchResponseVo.AttrVO();
            Map<String, Aggregate> bucketAgg = innerBucket.aggregations();
            String attrName = bucketAgg.get("attr_name_agg").sterms().buckets().array().get(0).key().stringValue();

            Aggregate attrValueAgg = bucketAgg.get("attr_value_agg");
            List<StringTermsBucket> bucketList = attrValueAgg.sterms().buckets().array();

            String attrValue = bucketList.get(0).key().stringValue();
            attrVO.setAttrId(innerBucket.key());
            attrVO.setAttrName(attrName);
            assert attrValue != null;
            attrVO.setAttrValue(Arrays.asList(attrValue.split(";")));
            return attrVO;
        }).collect(Collectors.toList());
        searchResponseVo.setAttrs(arrtVoList);

        // 3. 设置品牌信息
        Aggregate brandAgg = aggregations.get("brand_agg");
        List<SearchResponseVo.BrandVO> brandVOList = brandAgg.lterms().buckets().array().stream().map(longTermsBucket -> {
            long brandId = longTermsBucket.key();
            SearchResponseVo.BrandVO brandVO = new SearchResponseVo.BrandVO();
            Aggregate brandNameAgg = longTermsBucket.aggregations().get("brand_name_agg");
            String brandName = brandNameAgg.sterms().buckets().array().get(0).key().stringValue();
            Aggregate brandImgAgg = longTermsBucket.aggregations().get("brand_img_agg");
            String imgName = brandImgAgg.sterms().buckets().array().get(0).key().stringValue();
            brandVO.setBrandName(brandName);
            brandVO.setBrandId(brandId);
            brandVO.setBrandImg(imgName);
            return brandVO;
        }).collect(Collectors.toList());
        searchResponseVo.setBrands(brandVOList);

        // 4. 设置分类信息
        Aggregate catalogAgg = aggregations.get("catalog_agg");
        List<SearchResponseVo.CatelogVO> catelogVOList = catalogAgg.lterms().buckets().array().stream().map(longTermsBucket -> {
            long catalogId = longTermsBucket.key();
            Aggregate catalogNameAgg = longTermsBucket.aggregations().get("catalog_name_agg");
            String catalogName = catalogNameAgg.sterms().buckets().array().get(0).key().stringValue();

            SearchResponseVo.CatelogVO catelogVO = new SearchResponseVo.CatelogVO();
            catelogVO.setCatelogName(catalogName);
            catelogVO.setCatelogId(catalogId);
            return catelogVO;
        }).collect(Collectors.toList());
        searchResponseVo.setCatelogs(catelogVOList);


        return searchResponseVo;
    }

    /**
     * 将检索的参数转化为es请求的查询、聚合条件
     *
     * @param searchParam
     * @return
     */
    private Function<SearchRequest.Builder, ObjectBuilder<SearchRequest>> buildSearchFunction(SearchParam searchParam) {

        return builder -> {
            // 查询功能 模糊匹配,过滤(属性 分类 品牌 价格区间 库存)
            Query byKeyWord;
            if (!StringUtils.isNullOrEmpty(searchParam.getKeyword())) {
                byKeyWord = MatchQuery.of(m -> m.field("skuTitle").query(searchParam.getKeyword()))._toQuery();
            } else {
                byKeyWord = null;
            }

            //

            SearchRequest.Builder requestBuilder = builder.index(EsConstant.PRODUCT_INDEX)
                    .query(q ->
                            q.bool(b -> {
                                // 一、 构造查询条件 布尔查询
                                        if (byKeyWord != null) b.must(Collections.singletonList(byKeyWord));
                                        if (searchParam.getCatelog3Id() != null) {
                                            // 1. 三级分类id
                                            b.filter(
                                                    TermQuery.of(t ->
                                                            t.field("catalogId") // es中的分类id
                                                                    .value(searchParam.getCatelog3Id()))._toQuery()
                                            );
                                        }
                                        if (searchParam.getBrandId() != null && searchParam.getBrandId().size() > 0) {
                                            // 2. 按照品牌id匹配
                                            b.filter(TermsQuery.of(ts ->
                                                    ts.field("brandId") // es中的品牌id
                                                            .terms(new TermsQueryField.Builder()
                                                                    .value(searchParam.getBrandId().stream().map(FieldValue::of).collect(Collectors.toList())) // 将Long类型的品牌id转化为FieldValue类型
                                                                    .build()))._toQuery()
                                            );
                                        }
                                        if (searchParam.getStock() != null && searchParam.getStock().equals(1)) {
                                            // 3. 按照有无库存查询。 为1查询有库存的;如果为0就把有库存和没库存的都查询出来,不拼接本条语句
                                            b.filter(TermQuery.of(t -> t.field("hasStock").value(true))
                                                    ._toQuery());
                                        }

                                        // 4. 按照价格区间查询 价格区间:price=0_400 或者 price=_200 或者 price=200_
                                        if (!StringUtils.isNullOrEmpty(searchParam.getSkuPrice())) {
                                            String skuPrice = searchParam.getSkuPrice().trim();
                                            b.filter(RangeQuery.of(
                                                            r -> {
                                                                r.field("skuPrice");
                                                                String[] priceSpilt = skuPrice.split("_");
                                                                if (priceSpilt.length == 2){
                                                                    r.gte(JsonData.of(priceSpilt[0])).lte(JsonData.of(priceSpilt[1]));
                                                                }else if (priceSpilt.length == 1){
                                                                    // _500 means <=500
                                                                    if (skuPrice.startsWith("_")) r.lte(JsonData.of(priceSpilt[0]));
                                                                    // 100_ means >=100
                                                                    else if (skuPrice.endsWith("_")) r.gte(JsonData.of(priceSpilt[0]));
                                                                    else throw new RuntimeException("检查请求参数skuPrice的格式");
                                                                }else throw new RuntimeException("检查请求参数skuPrice的格式");
                                                                return r;
                                                            } )
                                                    ._toQuery());
                                        }

                                        // 5. 按照指定属性的条件进行查询 attrs=1_3G:4G
                                        if(searchParam.getAttrs() != null && searchParam.getAttrs().size() > 0){
                                            List<String> attrs = searchParam.getAttrs();

                                            attrs.forEach(attr->{
                                                List<Query> attrQuery = new ArrayList<>();
                                                String[] idString = attr.split("_");
                                                long attrId = Long.parseLong(idString[0]);
                                                List<String> attrValue = Arrays.asList(idString[1].split(":"));
                                                attrQuery.add(TermQuery.of(termQuery -> termQuery.field("attrs.attrId").value(attrId))._toQuery());
                                                attrQuery.add( TermsQuery.of(termsQuery->termsQuery.field("attrs.attrValue").terms(new TermsQueryField.Builder()
                                                        .value(attrValue.stream().map(FieldValue::of).collect(Collectors.toList())).build()))._toQuery() );
                                                // 对于每一个要查询的属性都要生成一个filter
                                                b.filter(NestedQuery.of(
                                                                n -> n.path("attrs")
                                                                        .query(attr_q ->
                                                                                attr_q.bool(attr_b -> attr_b.must(attrQuery))))._toQuery());
                                            });
                                        }

                                        return b;
                                    }
                            ));
            // 二、 构造排序条件
            /**
             * sort = saleCount_asc/desc
             * sort = skuPrice_asc/desc
             * sort = hotScore_asc/desc
             */
            if (!StringUtils.isNullOrEmpty(searchParam.getSort())){
                String[] split = searchParam.getSort().split("_");
                assert split.length == 2;
                String field = split[0];
                String rule = split[1];
                SortOrder sortOrder = Objects.equals(rule, "asc") ? SortOrder.Asc : SortOrder.Desc;
                builder.sort(f->f.field(o->o.field(field).order(sortOrder)));
            }

            // 三、构造分页条件
            int pageNo = searchParam.getPageNo()==null ? 1 : searchParam.getPageNo();
//            int pageSize = searchParam.getPageSize()==null ? EsConstant.PRODUCT_PAGESIZE : searchParam.getPageSize();
            int pageSize = EsConstant.PRODUCT_PAGESIZE; // 暂时先固定为常量
            // (页码-1)* 页大小 = from
            builder.from((pageNo-1)* pageSize );
            builder.size(pageSize);

            // 四、高亮查询条件
            if (!StringUtils.isNullOrEmpty(searchParam.getKeyword())){
                // 高亮查询的keyword
                builder.highlight(h->h.fields("skuTitle", new HighlightField.Builder().build()).preTags("<b style='color: red'>").postTags("</b>"));
            }

            // 五、聚合分析
            // 5.1 brand_agg
            builder.aggregations("brand_agg", aggregation -> aggregation
                            .terms(termsAggregation->termsAggregation.field("brandId").size(50))
                            .aggregations("brand_name_agg", innerAggregation->innerAggregation.terms(terms->terms.field("brandName").size(1)))
                            .aggregations("brand_img_agg", innerAggregation->innerAggregation.terms(terms->terms.field("brandImg").size(1))));

            // 5.2 catalog_agg
            builder.aggregations("catalog_agg", aggregation -> aggregation
                    .terms(termsAggregation->termsAggregation.field("catalogId").size(30))
                    .aggregations("catalog_name_agg", innerAggregation->innerAggregation.terms(terms->terms.field("catalogName").size(1))));

            // 5.3 attr_agg
            builder.aggregations("attr_agg", aggregation -> aggregation
                    .nested(nest-> nest.path("attrs"))
                    .aggregations("attr_id_agg", attrIdAggretation -> attrIdAggretation
                            .terms(term->term.field("attrs.attrId").size(50))
                            .aggregations("attr_name_agg", attrNameAggregation -> attrNameAggregation.terms(term->term.field("attrs.attrName").size(1)))
                            .aggregations("attr_value_agg", attrValueAggregation -> attrValueAggregation.terms(term->term.field("attrs.attrValue").size(50)))
                    ));
            return requestBuilder;
        };
    }
}

请求参数VO

@Data
public class SearchParam {

    /**
     * 检索关键字
     */
    String keyword;

    /**
     * 品牌id,可以多选
     */
    List<Long> brandId;

    /**
     * 分类id
     */
    Long catelog3Id;

    /**
     * sort = saleCount_asc/desc
     * sort = skuPrice_asc/desc
     * sort = hotScore_asc/desc
     */
    String sort;

    /**
     * 价格区间:price=0_400 或者 price=_200 或者 price=200_
     */
    String skuPrice;

    /**
     * 是否有库存: stock=0/1;1只显示有货,0或者不传都会显示
     */
    Integer stock;


    /**
     *  可以传入多个; attrs=1_3G:4G;1号属性值为3G或者4G
     */
    List<String> attrs;

    /**
     * 页码
     */
    Integer pageNo;

    /**
     * 页面大小
     */
    Integer pageSize;
}