关于java:MyBatisPlus同款Elasticsearch-ORM框架用起来够优雅

5次阅读

共计 13550 个字符,预计需要花费 34 分钟才能阅读完成。

应用过 Spring Data 操作 ES 的小伙伴应该有所理解,它只能实现一些十分根本的数据管理工作,一旦遇到略微简单点的查问,根本都要依赖 ES 官网提供的 RestHighLevelClient,Spring Data 只是在其根底上进行了简略的封装。最近发现一款更优雅的 ES ORM 框架Easy-Es,应用它能像 MyBatis-Plus 一样操作 ES,明天就以 mall 我的项目中的商品搜寻性能为例,来聊聊它的应用!

SpringBoot 实战电商我的项目 mall(50k+star)地址:https://github.com/macrozheng/mall

Easy-Es 简介

Easy-Es(简称 EE)是一款基于 Elasticsearch(简称 ES)官网提供的 RestHighLevelClient 打造的 ORM 开发框架,在 RestHighLevelClient 的根底上,只做加强不做扭转,为简化开发、提高效率而生。EE 和 Mybatis-Plus(简称 MP)的用法十分类似,如果你之前应用过 MP 的话,应该能很快上手 EE。EE 的理念是:把简略、易用、不便留给用户,把简单留给框架。

EE 的次要个性如下:

  • 全自动索引托管:开发者无需关怀索引的创立、更新及数据迁徙等繁琐步骤,框架能主动实现。
  • 屏蔽语言差别:开发者只须要会 MySQL 的语法即可应用 ES。
  • 代码量极少:与间接应用官网提供的 RestHighLevelClient 相比,雷同的查问均匀能够节俭 3 - 5 倍的代码量。
  • 零魔法值:字段名称间接从实体中获取,无需手写。
  • 零额定学习老本: 开发者只有会国内最受欢迎的 Mybatis-Plus 用法,即可无缝迁徙至 EE。

MySQL 与 Easy-Es 语法比照

首先咱们来对 MySQL、Easy-Es 和 RestHighLevelClient 的语法做过比照,来疾速学习下 Easy-Es 的语法。

MySQL Easy-Es es-DSL/es java api
and and must
or or should
= eq term
!= ne boolQueryBuilder.mustNot(queryBuilder)
> gt QueryBuilders.rangeQuery(‘es field’).gt()
>= ge .rangeQuery(‘es field’).gte()
< lt .rangeQuery(‘es field’).lt()
<= le .rangeQuery(‘es field’).lte()
like ‘%field%’ like QueryBuilders.wildcardQuery(field,value)
not like ‘%field%’ notLike must not wildcardQuery(field,value)
like ‘%field’ likeLeft QueryBuilders.wildcardQuery(field,*value)
like ‘field%’ likeRight QueryBuilders.wildcardQuery(field,value*)
between between QueryBuilders.rangeQuery(‘es field’).from(xx).to(xx)
notBetween notBetween must not QueryBuilders.rangeQuery(‘es field’).from(xx).to(xx)
is null isNull must not QueryBuilders.existsQuery(field)
is notNull isNotNull QueryBuilders.existsQuery(field)
in in QueryBuilders.termsQuery(” xx es field”, xx)
not in notIn must not QueryBuilders.termsQuery(” xx es field”, xx)
group by groupBy AggregationBuilders.terms()
order by orderBy fieldSortBuilder.order(ASC/DESC)
min min AggregationBuilders.min
max max AggregationBuilders.max
avg avg AggregationBuilders.avg
sum sum AggregationBuilders.sum
order by xxx asc orderByAsc fieldSortBuilder.order(SortOrder.ASC)
order by xxx desc orderByDesc fieldSortBuilder.order(SortOrder.DESC)
match matchQuery
matchPhrase QueryBuilders.matchPhraseQuery
matchPrefix QueryBuilders.matchPhrasePrefixQuery
queryStringQuery QueryBuilders.queryStringQuery
select * matchAllQuery QueryBuilders.matchAllQuery()
highLight HighlightBuilder.Field

集成及配置

接下来把 Easy-Es 集成到我的项目中配置下就能够应用了。

  • 首先须要在 pom.xml 中增加 Easy-Es 的相干依赖;
<dependency>
    <groupId>cn.easy-es</groupId>
    <artifactId>easy-es-boot-starter</artifactId>
    <version>1.0.2</version>
</dependency>
  • 因为底层应用了 ES 官网提供的 RestHighLevelClient,这里 ES 的相干依赖版本须要对立下,这里应用的 ES 客户端版本为7.14.0,ES 版本为7.17.3
<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.elasticsearch.client</groupId>
            <artifactId>elasticsearch-rest-high-level-client</artifactId>
            <version>7.14.0</version>
        </dependency>
        <dependency>
            <groupId>org.elasticsearch</groupId>
            <artifactId>elasticsearch</artifactId>
            <version>7.14.0</version>
        </dependency>
    </dependencies>
</dependencyManagement>
  • 再批改配置文件 application.yml 对 Easy-Es 进行配置。
easy-es:
  # 是否开启 EE 主动配置
  enable: true
  # ES 连贯地址 + 端口
  address: localhost:9200
  # 敞开自带 banner
  banner: false
  • 增加 Easy-Es 的 Java 配置,应用 @EsMapperScan 配置好 Easy-Es 的 Mapper 接口和文档对象门路,如果你应用了 MyBatis-Plus 的话,须要和它的扫描门路辨别开来。
/**
 * EasyEs 配置类
 * Created by macro on 2022/9/16.
 */
@Configuration
@EsMapperScan("com.macro.mall.tiny.easyes")
public class EasyEsConfig {}

应用

Easy-Es 集成和配置实现后,就能够开始应用了。这里还是以 mall 我的项目的商品搜寻性能为例,聊聊 Easy-Es 的应用,Spring Data 的实现形式能够参考 Elasticsearch 我的项目实战,商品搜寻功能设计与实现!。

注解的应用

上面咱们来学习下 Easy-Es 中注解的应用。

  • 首先咱们须要创立文档对象EsProduct,而后给类和字段增加上 Easy-Es 的注解;
/**
 * 搜寻商品的信息
 * Created by macro on 2018/6/19.
 */
@Data
@EqualsAndHashCode
@IndexName(value = "pms", shardsNum = 1, replicasNum = 0)
public class EsProduct implements Serializable {
    private static final long serialVersionUID = -1L;
    @IndexId(type = IdType.CUSTOMIZE)
    private Long id;
    @IndexField(fieldType = FieldType.KEYWORD)
    private String productSn;
    private Long brandId;
    @IndexField(fieldType = FieldType.KEYWORD)
    private String brandName;
    private Long productCategoryId;
    @IndexField(fieldType = FieldType.KEYWORD)
    private String productCategoryName;
    private String pic;
    @IndexField(fieldType = FieldType.TEXT, analyzer = "ik_max_word")
    private String name;
    @IndexField(fieldType = FieldType.TEXT, analyzer = "ik_max_word")
    private String subTitle;
    @IndexField(fieldType = FieldType.TEXT, analyzer = "ik_max_word")
    private String keywords;
    private BigDecimal price;
    private Integer sale;
    private Integer newStatus;
    private Integer recommandStatus;
    private Integer stock;
    private Integer promotionType;
    private Integer sort;
    @IndexField(fieldType = FieldType.NESTED, nestedClass = EsProductAttributeValue.class)
    private List<EsProductAttributeValue> attrValueList;
    @Score
    private Float score;
}
  • EsProduct 中的注解具体阐明如下:
注解名称 用处 参数
@IndexName 索引名注解 value:指定索引名;shardsNum:分片数;replicasNum:正本数
@IndexId ES 主键注解 type:指定注解类型,CUSTOMIZE 示意自定义
@IndexField ES 字段注解 fieldType:字段在索引中的类型;analyzer:索引文档时用的分词器;nestedClass:嵌套类
@Score 得分注解 decimalPlaces:得分保留小数位,实体类中被作为 ES 查问得分返回的字段应用
  • EsProduct 中嵌套类型 EsProductAttributeValue 的代码如下。
/**
 * 搜寻商品的属性信息
 * Created by macro on 2018/6/27.
 */
@Data
@EqualsAndHashCode
public class EsProductAttributeValue implements Serializable {
    private static final long serialVersionUID = 1L;
    @IndexField(fieldType = FieldType.LONG)
    private Long id;
    @IndexField(fieldType = FieldType.KEYWORD)
    private Long productAttributeId;
    // 属性值
    @IndexField(fieldType = FieldType.KEYWORD)
    private String value;
    // 属性参数:0-> 规格;1-> 参数
    @IndexField(fieldType = FieldType.INTEGER)
    private Integer type;
    // 属性名称
    @IndexField(fieldType=FieldType.KEYWORD)
    private String name;
}

商品信息保护

上面咱们来实现几个简略的商品信息保护接口,包含商品信息的导入、创立和删除。

  • 首先咱们须要定义一个 Mapper,继承 BaseEsMapper;
/**
 * 商品 ES 操作类
 * Created by macro on 2018/6/19.
 */
public interface EsProductMapper extends BaseEsMapper<EsProduct> {}
  • 而后在 Service 实现类中间接应用 EsProductMapper 内置办法实现即可,是不是和 MyBatis-Plus 的用法统一?
/**
 * 搜寻商品治理 Service 实现类
 * Created by macro on 2018/6/19.
 */
@Service
public class EsProductServiceImpl implements EsProductService {
    @Autowired
    private EsProductDao productDao;
    @Autowired
    private EsProductMapper esProductMapper;
    @Override
    public int importAll() {List<EsProduct> esProductList = productDao.getAllEsProductList(null);
        return esProductMapper.insertBatch(esProductList);
    }

    @Override
    public void delete(Long id) {esProductMapper.deleteById(id);
    }

    @Override
    public EsProduct create(Long id) {
        EsProduct result = null;
        List<EsProduct> esProductList = productDao.getAllEsProductList(id);
        if (esProductList.size() > 0) {result = esProductList.get(0);
            esProductMapper.insert(result);
        }
        return result;
    }

    @Override
    public void delete(List<Long> ids) {if (!CollectionUtils.isEmpty(ids)) {esProductMapper.deleteBatchIds(ids);
        }
    }
}

简单商品搜寻

上面咱们来实现一个最简略的商品搜寻,分页搜寻商品名称、副标题、关键词中蕴含指定关键字的商品。

  • 通过 QueryWrapper 来结构查问条件,而后应用 Mapper 中的办法来进行查问,应用过 MyBatis-Plus 的小伙伴应该很相熟了;
/**
 * 搜寻商品治理 Service 实现类
 * Created by macro on 2018/6/19.
 */
@Service
public class EsProductServiceImpl implements EsProductService {
    @Autowired
    private EsProductMapper esProductMapper;
    @Override
    public PageInfo<EsProduct> search(String keyword, Integer pageNum, Integer pageSize) {LambdaEsQueryWrapper<EsProduct> wrapper = new LambdaEsQueryWrapper<>();
        if(StrUtil.isEmpty(keyword)){wrapper.matchAllQuery();
        }else{wrapper.multiMatchQuery(keyword,EsProduct::getName,EsProduct::getSubTitle,EsProduct::getKeywords);
        }
        return esProductMapper.pageQuery(wrapper, pageNum, pageSize);
    }
}
  • 应用 Swagger 拜访接口后,能够在控制台输入查看生成的 DSL 语句,拜访地址:http://localhost:8080/swagger…
  • 把 DSL 语句间接复制 Kibana 中即可执行查看后果了,这和咱们手写 DSL 语句没什么两样的。

综合商品搜寻

上面咱们来实现一个简单的商品搜寻,波及到过滤、不同字段匹配权重不同以及能够进行排序。

  • 首先来说需要,按输出的关键字搜寻商品名称(权重 10)、副标题(权重 5)和关键词(权重 2),能够按品牌和分类进行筛选,能够有 5 种排序形式,默认按相关度进行排序,看下接口文档有助于了解;
  • 这个性能之前应用 Spring Data 来实现非常复杂,应用 Easy-Es 来实现的确简洁不少,上面是应用 Easy-Es 的实现形式;
/**
 * 搜寻商品治理 Service 实现类
 * Created by macro on 2018/6/19.
 */
@Service
public class EsProductServiceImpl implements EsProductService {
    @Autowired
    private EsProductMapper esProductMapper;
    @Override
    public PageInfo<EsProduct> search(String keyword, Long brandId, Long productCategoryId, Integer pageNum, Integer pageSize,Integer sort) {LambdaEsQueryWrapper<EsProduct> wrapper = new LambdaEsQueryWrapper<>();
        // 过滤
        if (brandId != null || productCategoryId != null) {if (brandId != null) {wrapper.eq(EsProduct::getBrandId,brandId);
            }
            if (productCategoryId != null) {wrapper.eq(EsProduct::getProductCategoryId,productCategoryId).enableMust2Filter(true);
            }
        }
        // 搜寻
        if (StrUtil.isEmpty(keyword)) {wrapper.matchAllQuery();
        } else {wrapper.and(i -> i.match(EsProduct::getName, keyword, 10f)
                    .or().match(EsProduct::getSubTitle, keyword, 5f)
                    .or().match(EsProduct::getKeywords, keyword, 2f));
        }
        // 排序
        if(sort==1){
            // 按新品从新到旧
            wrapper.orderByDesc(EsProduct::getId);
        }else if(sort==2){
            // 按销量从高到低
            wrapper.orderByDesc(EsProduct::getSale);
        }else if(sort==3){
            // 按价格从低到高
            wrapper.orderByAsc(EsProduct::getPrice);
        }else if(sort==4){
            // 按价格从高到低
            wrapper.orderByDesc(EsProduct::getPrice);
        }else{
            // 按相关度
            wrapper.sortByScore(SortOrder.DESC);
        }
        return esProductMapper.pageQuery(wrapper, pageNum, pageSize);
    }
}
  • 再比照下之前应用 Spring Data 的实现形式,没有 QueryWrapper 来结构条件,还要硬编码字段名称,的确优雅了不少!

相干商品举荐

当咱们查看相干商品的时候,个别底部会有一些商品举荐,这里简略来实现下。

  • 首先来说下需要,能够依据指定商品的 ID 来查找相干商品,看下接口文档有助于了解;
  • 这里咱们的实现原理是这样的:首先依据 ID 获取指定商品信息,而后以指定商品的名称、品牌和分类来搜寻商品,并且要过滤掉以后商品,调整搜寻条件中的权重以获取最好的匹配度;
  • 应用 Easy-Es 来实现仍旧是那么简洁!
/**
 * 搜寻商品治理 Service 实现类
 * Created by macro on 2018/6/19.
 */
@Service
public class EsProductServiceImpl implements EsProductService {
    @Autowired
    private EsProductMapper esProductMapper;
    @Override
    public PageInfo<EsProduct> recommend(Long id, Integer pageNum, Integer pageSize) {LambdaEsQueryWrapper<EsProduct> wrapper = new LambdaEsQueryWrapper<>();
        List<EsProduct> esProductList = productDao.getAllEsProductList(id);
        if (esProductList.size() > 0) {EsProduct esProduct = esProductList.get(0);
            String keyword = esProduct.getName();
            Long brandId = esProduct.getBrandId();
            Long productCategoryId = esProduct.getProductCategoryId();
            // 用于过滤掉雷同的商品
            wrapper.ne(EsProduct::getId,id);
            // 依据商品题目、品牌、分类进行搜寻
            wrapper.and(i -> i.match(EsProduct::getName, keyword, 8f)
                    .or().match(EsProduct::getSubTitle, keyword, 2f)
                    .or().match(EsProduct::getKeywords, keyword, 2f)
                    .or().match(EsProduct::getBrandId, brandId, 5f)
                    .or().match(EsProduct::getProductCategoryId, productCategoryId, 3f));
            return esProductMapper.pageQuery(wrapper, pageNum, pageSize);
        }
        return esProductMapper.pageQuery(wrapper, pageNum, pageSize);
    }
}

聚合搜寻商品相干信息

在搜寻商品时,常常会有一个筛选界面来帮忙咱们找到想要的商品,这里咱们来简略实现下。

  • 首先来说下需要,能够依据搜寻关键字获取到与关键字匹配商品相干的分类、品牌以及属性,上面这张图有助于了解;
  • 这里咱们能够应用 ES 的聚合来实现,搜寻出相干商品,聚合出商品的品牌、商品的分类以及商品的属性,只有呈现次数最多的前十个即可;
  • 因为 Easy-Es 目前只用 groupBy 实现了简略的聚合,对于咱们这种有嵌套对象的聚合无奈反对,所以须要应用 RestHighLevelClient 来实现,如果你对照之前的 Spring Data 实现形式的话,能够发现用法差不多,看样子 Spring Data 只是做了简略的封装而已。
/**
 * 搜寻商品治理 Service 实现类
 * Created by macro on 2018/6/19.
 */
@Service
public class EsProductServiceImpl implements EsProductService {
    @Autowired
    private EsProductMapper esProductMapper;
    @Override
    public EsProductRelatedInfo searchRelatedInfo(String keyword) {SearchRequest searchRequest = new SearchRequest();
        searchRequest.indices("pms_*");
        SearchSourceBuilder builder = new SearchSourceBuilder();
        // 搜寻条件
        if (StrUtil.isEmpty(keyword)) {builder.query(QueryBuilders.matchAllQuery());
        } else {builder.query(QueryBuilders.multiMatchQuery(keyword, "name", "subTitle", "keywords"));
        }
        // 聚合搜寻品牌名称
        builder.aggregation(AggregationBuilders.terms("brandNames").field("brandName"));
        // 汇合搜寻分类名称
        builder.aggregation(AggregationBuilders.terms("productCategoryNames").field("productCategoryName"));
        // 聚合搜寻商品属性,去除 type= 1 的属性
        AbstractAggregationBuilder<NestedAggregationBuilder> aggregationBuilder = AggregationBuilders.nested("allAttrValues", "attrValueList")
                .subAggregation(AggregationBuilders.filter("productAttrs", QueryBuilders.termQuery("attrValueList.type", 1))
                        .subAggregation(AggregationBuilders.terms("attrIds")
                                .field("attrValueList.productAttributeId")
                                .subAggregation(AggregationBuilders.terms("attrValues")
                                        .field("attrValueList.value"))
                                .subAggregation(AggregationBuilders.terms("attrNames")
                                        .field("attrValueList.name"))));
        builder.aggregation(aggregationBuilder);
        searchRequest.source(builder);
        try {SearchResponse searchResponse = esProductMapper.search(searchRequest, RequestOptions.DEFAULT);
            return convertProductRelatedInfo(searchResponse);
        } catch (IOException e) {e.printStackTrace();
        }
        return null;
    }

    /**
     * 将返回后果转换为对象
     */
    private EsProductRelatedInfo convertProductRelatedInfo(SearchResponse response) {EsProductRelatedInfo productRelatedInfo = new EsProductRelatedInfo();
        Map<String, Aggregation> aggregationMap = response.getAggregations().asMap();
        // 设置品牌
        Aggregation brandNames = aggregationMap.get("brandNames");
        List<String> brandNameList = new ArrayList<>();
        for(int i = 0; i<((Terms) brandNames).getBuckets().size(); i++){brandNameList.add(((Terms) brandNames).getBuckets().get(i).getKeyAsString());
        }
        productRelatedInfo.setBrandNames(brandNameList);
        // 设置分类
        Aggregation productCategoryNames = aggregationMap.get("productCategoryNames");
        List<String> productCategoryNameList = new ArrayList<>();
        for(int i=0;i<((Terms) productCategoryNames).getBuckets().size();i++){productCategoryNameList.add(((Terms) productCategoryNames).getBuckets().get(i).getKeyAsString());
        }
        productRelatedInfo.setProductCategoryNames(productCategoryNameList);
        // 设置参数
        Aggregation productAttrs = aggregationMap.get("allAttrValues");
        List<? extends Terms.Bucket> attrIds = ((ParsedStringTerms) ((ParsedFilter) ((ParsedNested) productAttrs).getAggregations().get("productAttrs")).getAggregations().get("attrIds")).getBuckets();
        List<EsProductRelatedInfo.ProductAttr> attrList = new ArrayList<>();
        for (Terms.Bucket attrId : attrIds) {EsProductRelatedInfo.ProductAttr attr = new EsProductRelatedInfo.ProductAttr();
            attr.setAttrId(Long.parseLong((String) attrId.getKey()));
            List<String> attrValueList = new ArrayList<>();
            List<? extends Terms.Bucket> attrValues = ((ParsedStringTerms) attrId.getAggregations().get("attrValues")).getBuckets();
            List<? extends Terms.Bucket> attrNames = ((ParsedStringTerms) attrId.getAggregations().get("attrNames")).getBuckets();
            for (Terms.Bucket attrValue : attrValues) {attrValueList.add(attrValue.getKeyAsString());
            }
            attr.setAttrValues(attrValueList);
            if(!CollectionUtils.isEmpty(attrNames)){String attrName = attrNames.get(0).getKeyAsString();
                attr.setAttrName(attrName);
            }
            attrList.add(attr);
        }
        productRelatedInfo.setProductAttrs(attrList);
        return productRelatedInfo;
    }
}

总结

明天将之前的应用 Spring Data 的商品搜寻案例应用 Easy-Es 改写了一下,的确应用 Easy-Es 更简略,然而对于简单的聚合搜寻性能,两者都须要应用原生的 RestHighLevelClient 用法来实现。应用 Easy-Es 来操作 ES 的确足够优雅,它相似 MyBatis-Plus 的用法能大大降低咱们的学习老本,疾速实现开发工作!

参考资料

官网文档:https://www.easy-es.cn/

我的项目源码地址

https://github.com/macrozheng…

正文完
 0