ES商品搜索实战 Easy-Es搭配原生SearchSourceBuilder

1.背景

在使用Easy-Es发现有很多功能可能不是很好使用,或者说我自己不太会使用。
这里分享自己使用EE和原生API一起搭配使用的商品搜索功能,包含keyword指定分词器搜索,商品分类,商品品牌,价格区间,嵌套分组搜索,权重排序,高亮,以及品牌去重等功能。

2.直接上代码

商品实体类

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@IndexName(value = "product", aliasName = "product")
public class Product {

	@IndexId(type = IdType.CUSTOMIZE)
	private Long id;

	private Long brandId;

	private String brandLogo;

	private Long productCategoryId;

	private Long freightTemplateId;

	private Long productAttributeCategoryId;

	private String channelCode;

	private String channelProductCode;

	private String name;

	private String productType;

	private String pic;

	private String productCode;

	private Integer publishStatus;

	private Integer initStatus;

	private Integer verifyStatus;

	private Integer sort;

	private BigDecimal price;

	private BigDecimal costPrice;

	private BigDecimal crossedPrice;

	private BigDecimal profitMargin;

	private BigDecimal profitMarginPercent;

	private BigDecimal priceProfitMargin;

	private BigDecimal priceProfitMarginPercent;

	private Integer productRealSales;

	private Integer productVirtualSales;

	private String subTitle;

	private String description;

	private BigDecimal originalPrice;

	private String albumPics;

	private String detailTitle;

	private String detailDesc;

	private String detailHtml;

	private String detailMobileHtml;

	private String detailWechatHtml;

	private Integer sellType;

	private String keywords;

	private String note;
	
	private String brandName;

	private String channelProductCategoryPath;

	private String productCategoryName;

	private String productCategoryPath;

	private LocalDateTime joinTime;

	private Integer blacklist;

	private String blackReason;

	private Boolean delFlag;

	@IndexField(fieldType = FieldType.NESTED, nestedClass = SkuStock.class)
	private List<SkuStock> skuInfos;

	@IndexField(fieldType = FieldType.NESTED, nestedClass = ProductGroup.class)
	private List<ProductGroup> groupInfos;

	@IndexField(fieldType = FieldType.DATE, fieldData = true, dateFormat = "yyyy-MM-dd HH:mm:ss||yyyy-MM-dd||epoch_millis")
	private LocalDateTime createTime;

	@IndexField(fieldType = FieldType.DATE, fieldData = true, dateFormat = "yyyy-MM-dd HH:mm:ss||yyyy-MM-dd||epoch_millis")
	private LocalDateTime updateTime;
}

SKU实体类

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class SkuStock {

	private Long skuId;

	private Long productId;

	private String productCode;

	private String productType;

	private String skuCode;

	private String channelCode;

	private String channelSkuCode;

	private String skuName;

	private BigDecimal platformPrice;

	private BigDecimal platformSellPrice;

	private BigDecimal originalPrice;

	private BigDecimal crossedPrice;

	private Integer stock;

	private LocalDateTime warningTime;

	private Integer lowStock;

	private String pic;

	private Integer orderNum;

	private Integer backOrderNum;

	private Integer lockStock;

	private Integer publishStatus;

	private Integer warningStatus;

	private Integer initStatus;

	private String priceTypeCode;

	private String saleAttributes;

	private String marker;

	private String remark;

	private Integer blacklist;

	private String channelProductPoolCode;

	private Boolean skuDelFlag;

	private String productArea;

	private Integer lowestBuy;

	private String warrantDesc;

	private String capacity;

	private String weight;

	private Integer logisticsType;

	private BigDecimal taxRatePercentage;

	private Integer deliveryTime;

	private String taxCode;

	private String unit;

	private String wareInfo;

	private String albumPics;

	private String cubeParam;

	private Integer canInvoice;

	private Integer saleState;

	private Integer returnRuleType;

	private Integer noReasonToReturn;

	private String specificationAttributes;

	private String categoryAttributes;

	private Boolean extraDelFlag;
}

搜索的入参

@Data
public class EntProductSearchDTO implements Serializable {

    @ApiModelProperty(value = "商品名称")
    private String keyword;

    @ApiModelProperty(value = "商品价格")
    private BigDecimal priceMax;

    @ApiModelProperty(value = "商品价格")
    private BigDecimal priceMin;

    @ApiModelProperty(value = "品牌ID")
    private List<Long> brandIds;

    @ApiModelProperty(value = "商品分类ID")
    private List<Long> productCategoryIds;

    @ApiModelProperty(value = "分组ID")
    private List<String> groupingIdList;

    @ApiModelProperty(value = "商品类型 :real->实物商品;call->话费商品;coupon->卡券;recharge->直充商品")
    private String productType;

    @ApiModelProperty("发布状态 0-下架 1-上架 2三方下架")
    private List<Integer> publishStatus;

    @ApiModelProperty("分类地址 ,分割")
    private String productCategoryPath;

    /**
     * 页码
     */
    @ApiModelProperty("页码")
    private Integer page;

    /**
     * 每页记录数
     */
    @ApiModelProperty("每页记录数")
    private Integer size;

    @NotNull(message = "sortType cannot be null")
    @Min(value = 0, message = "sortType mix is 0")
    @Max(value = 3, message = "sortType max is 3")
    @ApiModelProperty(value = "排序方式:0->默认排序;1->价格大;2->价格小")
    private Integer sortType;

    public int getPage() {
        return page == null ? 1 : page;
    }

    public void setPage(int page) {
        this.page = page;
    }

    public int getSize() {
        return size == null ? 20 : size;
    }

    public void setSize(int size) {
        this.size = size;
    }

    /**
     * 返回偏移量
     *
     * @return
     */
    public int getOffset() {
        int offset = (getPage() - 1) * getSize();
        return Math.max(offset, 0);
    }

}

3.进入正题,搜索模块

组装查询条件LambdaEsQueryWrapper,包含查询条件,排序,分页,高亮,去重

LambdaEsQueryWrapper<Product> wrapper = new LambdaEsQueryWrapper<>();
        SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
        sourceBuilder.query(decorateBp(dto));

        //排序
        if (dto.getSortType() != null) {
            switch (dto.getSortType()) {
                case 1:
                    sourceBuilder.sort(SortBuilders.fieldSort(FieldUtils.val(Product::getPrice)).order(SortOrder.DESC));
                    break;
                case 2:
                    sourceBuilder.sort(SortBuilders.fieldSort(FieldUtils.val(Product::getPrice)).order(SortOrder.ASC));
                    break;
                case 3:
                    sourceBuilder.sort(SortBuilders.fieldSort(FieldUtils.val(Product::getSort)).order(SortOrder.DESC));
                    break;
                default:
                    sourceBuilder.sort("_score", SortOrder.DESC);
                    sourceBuilder.sort(SortBuilders.fieldSort(FieldUtils.val(Product::getSort)).order(SortOrder.DESC));
            }
        }
        //分页
        sourceBuilder.from(dto.getOffset());
        sourceBuilder.size(dto.getSize());

        //高亮
        if (StringUtils.isNotBlank(dto.getKeyword())) {
            HighlightBuilder highlightBuilder = new HighlightBuilder();
            highlightBuilder.field(FieldUtils.val(Product::getName));
            highlightBuilder.preTags("<b style='color:red'>");
            highlightBuilder.postTags("</b>");
            sourceBuilder.highlighter(highlightBuilder);
        }
        //品牌信息要去重
        if (brandCon) {
            sourceBuilder.collapse(new CollapseBuilder(FieldUtils.val(Product::getBrandId)));
        }
        wrapper.setSearchSourceBuilder(sourceBuilder);
        log.info("搜索条件{}", brandCon ? "品牌" : "商品");
        return wrapper;

这里是搜索的条件组装,封装BoolQueryBuilder

 /**
     * 组装搜索条件
     *
     * @param dto 入参
     */
    private BoolQueryBuilder decorateBp(EntSearchDTO dto) {
        BoolQueryBuilder bp = QueryBuilders.boolQuery();
        //关键字查询
        if (StringUtils.isNotBlank(dto.getKeyword())) {
            bp.must(QueryBuilders.multiMatchQuery(dto.getKeyword(), FieldUtils.val(Product::getBrandName),
                    FieldUtils.val(Product::getName)).analyzer("ik_smart"));
        }

        //分类ID
        if (!CollectionUtils.isEmpty(dto.getProductCategoryIds())) {
            bp.filter(QueryBuilders.termQuery(FieldUtils.val(Product::getProductCategoryId), dto.getProductCategoryIds()));
        }

        //品牌ID
        if (!CollectionUtils.isEmpty(dto.getBrandIds())) {
            bp.filter(QueryBuilders.termsQuery(FieldUtils.val(Product::getBrandId), dto.getBrandIds()));
        }
        if (StringUtils.isNotBlank(dto.getProductType())) {
            bp.filter(QueryBuilders.termQuery(FieldUtils.val(Product::getProductType), dto.getProductType()));
        }
        //上下架
        if (!CollectionUtils.isEmpty(dto.getPublishStatus())) {
            bp.filter(QueryBuilders.termsQuery(FieldUtils.val(Product::getPublishStatus), dto.getPublishStatus()));
        }

        //价格
        if (dto.getPriceMax() != null) {
            bp.must(QueryBuilders.rangeQuery(FieldUtils.val(Product::getPrice)).lte(dto.getPriceMax()));
        }
        if (dto.getPriceMin() != null) {
            bp.must(QueryBuilders.rangeQuery(FieldUtils.val(Product::getPrice)).gte(dto.getPriceMin()));
        }
        if (StringUtils.isNotBlank(dto.getProductCategoryPath())) {
            bp.must(QueryBuilders.wildcardQuery(FieldUtils.val(Product::getProductCategoryPath), dto.getProductCategoryPath() + "*"));
        }

        //分组
        if (!CollectionUtils.isEmpty(dto.getGroupingIdList())) {
            //俺也不想这样写,后面在优化吧。
            String groupId = FieldUtils.val(Product::getGroupInfos) + "." + FieldUtils.val(ProductGroup::getGroupId);
            String delFlag = FieldUtils.val(Product::getGroupInfos) + "." + FieldUtils.val(ProductGroup::getDelFlag);

            //嵌套查询
            bp.must(QueryBuilders.nestedQuery(FieldUtils.val(Product::getGroupInfos), new TermsQueryBuilder(groupId, dto.getGroupingIdList()), ScoreMode.None));
            bp.must(QueryBuilders.nestedQuery(FieldUtils.val(Product::getGroupInfos), new TermsQueryBuilder(delFlag, false), ScoreMode.None));
        }
        bp.must(QueryBuilders.termQuery(FieldUtils.val(Product::getDelFlag), false));
        bp.must(QueryBuilders.termQuery(FieldUtils.val(Product::getInitStatus), CommonConstants.TWO));
        bp.mustNot(QueryBuilders.termQuery(FieldUtils.val(Product::getBrandName), ProductConstant.DEFUTE_BRAND_NAME_ZH));

        return bp;

    }

这里就是方法的入口,productMapper.selectList(wrapper)和Mybatis-Plus的写法基本上一摸一样,如果不懂可以去看一下EE官方使用方法
Easy-Es

public Page<EntProductSearchVO> searchProduct(EntProductSearchDTO dto) {
      
        if (dto == null) {
            return new Page<>();
        }
        //查询条件
        LambdaEsQueryWrapper<Product> wrapper = buildSearchCondition(dto, false);
        List<EntProductSearchVO> vos = new ArrayList<>();

        //是否需要查询数据 默认查询数据
        if (dto.getNeedData() == null || dto.getNeedData()) {
            List<Product> products = productMapper.selectList(wrapper);
            products.forEach(product -> {
                EntProductSearchVO vo = BeanConvertUtil.convert(product,EntProductSearchVO.class);
                vo.setPromotionPrice(product.getOriginalPrice());

                //查询最小的价格的sku
                SkuStock skuStock = product.getSkuInfos().stream().min(Comparator.comparing(SkuStock::getPlatformSellPrice)).orElse(new SkuStock());
                vo.setChannelPriceCode(skuStock.getChannelCode());
                vo.setChannelPriceSkuCode(skuStock.getChannelSkuCode());
                List<EntProductSearchVO.EntSkuVO> skuVOList = new ArrayList<>();
                product.getSkuInfos().forEach(sku->{
                    EntProductSearchVO.EntSkuVO skuVO = BeanConvertUtil.convert(sku,EntProductSearchVO.EntSkuVO.class);
                    skuVO.setPrice(sku.getPlatformSellPrice());
                    skuVO.setPromotionPrice(sku.getOriginalPrice());
                    skuVOList.add(skuVO);
                });
                vo.setSkuList(skuVOList);
                vos.add(vo);
            });
        }
        Long total = productMapper.selectCount(wrapper);

        return new Page<EntProductSearchVO>().setCurrent(dto.getOffset()).setSize(dto.getSize()).setRecords(vos).setTotal(total);
    }
  • 6
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值