应用过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的语法。

MySQLEasy-Eses-DSL/es java api
andandmust
ororshould
=eqterm
!=neboolQueryBuilder.mustNot(queryBuilder)
>gtQueryBuilders.rangeQuery('es field').gt()
>=ge.rangeQuery('es field').gte()
<lt.rangeQuery('es field').lt()
<=le.rangeQuery('es field').lte()
like '%field%'likeQueryBuilders.wildcardQuery(field,value)
not like '%field%'notLikemust not wildcardQuery(field,value)
like '%field'likeLeftQueryBuilders.wildcardQuery(field,*value)
like 'field%'likeRightQueryBuilders.wildcardQuery(field,value*)
betweenbetweenQueryBuilders.rangeQuery('es field').from(xx).to(xx)
notBetweennotBetweenmust not QueryBuilders.rangeQuery('es field').from(xx).to(xx)
is nullisNullmust not QueryBuilders.existsQuery(field)
is notNullisNotNullQueryBuilders.existsQuery(field)
ininQueryBuilders.termsQuery(" xx es field", xx)
not innotInmust not QueryBuilders.termsQuery(" xx es field", xx)
group bygroupByAggregationBuilders.terms()
order byorderByfieldSortBuilder.order(ASC/DESC)
minminAggregationBuilders.min
maxmaxAggregationBuilders.max
avgavgAggregationBuilders.avg
sumsumAggregationBuilders.sum
order by xxx ascorderByAscfieldSortBuilder.order(SortOrder.ASC)
order by xxx descorderByDescfieldSortBuilder.order(SortOrder.DESC)
-matchmatchQuery
-matchPhraseQueryBuilders.matchPhraseQuery
-matchPrefixQueryBuilders.matchPhrasePrefixQuery
-queryStringQueryQueryBuilders.queryStringQuery
select *matchAllQueryQueryBuilders.matchAllQuery()
-highLightHighlightBuilder.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:正本数
@IndexIdES主键注解type:指定注解类型,CUSTOMIZE示意自定义
@IndexFieldES字段注解fieldType:字段在索引中的类型;analyzer:索引文档时用的分词器;nestedClass:嵌套类
@Score得分注解decimalPlaces:得分保留小数位,实体类中被作为ES查问得分返回的字段应用
  • EsProduct中嵌套类型EsProductAttributeValue的代码如下。
/** * 搜寻商品的属性信息 * Created by macro on 2018/6/27. */@Data@EqualsAndHashCodepublic 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. */@Servicepublic 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. */@Servicepublic 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. */@Servicepublic 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. */@Servicepublic 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. */@Servicepublic 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...