共计 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…