使用過Spring Data操作ES的小夥伴應該有所瞭解,它只能實現一些非常基本的數據管理工作,一旦遇到稍微複雜點的查詢,基本都要依賴ES官方提供的RestHighLevelClient,Spring Data只是在其基礎上進行了簡單的封裝。最近發現一款更優雅的ES ORM框架Easy-Es,使用它能像MyBatis-Plus一樣操作ES,今天就以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 |
| != | not | 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>org.dromara.easy-es</groupId>
<artifactId>easy-es-boot-starter</artifactId>
<version>3.0.0</version>
</dependency>
- 由於底層使用了ES官方提供的RestHighLevelClient,這裏ES的相關依賴版本需要統一下,這裏使用的ES客户端版本為
7.17.28,ES版本為7.17.3;
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.elasticsearch.client</groupId>
<artifactId>elasticsearch-rest-high-level-client</artifactId>
<version>7.17.28</version>
</dependency>
<dependency>
<groupId>org.elasticsearch</groupId>
<artifactId>elasticsearch</artifactId>
<version>7.17.28</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的話,需要和它的掃描路徑區分開來。
/**
* @auther macrozheng
* @description EasyEs配置類
* @date 2025/9/4
* @github https://github.com/macrozheng
*/
@Configuration
@EsMapperScan("com.macro.blog.easyes")
public class EasyEsConfig {
}
使用
Easy-Es集成和配置完成後,就可以開始使用了。這裏還是以mall項目的商品搜索功能為例,聊聊Easy-Es的使用 。
mall項目介紹
這裏還是簡單介紹下mall項目吧,mall項目是一套基於 SpringBoot + Vue + uni-app 實現的電商系統(Github標星60K),採用Docker容器化部署,後端支持多模塊和微服務架構。包括前台商城項目和後台管理系統,能支持完整的訂單流程!涵蓋商品、訂單、購物車、權限、優惠券、會員、支付等功能!
- Boot項目:https://github.com/macrozheng/mall
- Cloud項目:https://github.com/macrozheng/mall-swarm
- 教程網站:https://www.macrozheng.com
項目演示:
註解的使用
下面我們來學習下Easy-Es中註解的使用。
- 首先我們需要創建文檔對象
EsProduct,然後給類和字段添加上Easy-Es的註解;
/**
* @auther macrozheng
* @description 搜索商品的信息
* @date 2025/9/4
* @github https://github.com/macrozheng
*/
@Data
@EqualsAndHashCode
@IndexName(value = "es_product")
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, nestedOrObjectClass = EsProductAttributeValue.class)
private List<EsProductAttributeValue> attrValueList;
}
- EsProduct中的註解具體説明如下:
| 註解名稱 | 用途 | 參數 |
|---|---|---|
| @IndexName | 索引名註解 | value:指定索引名;shardsNum:分片數;replicasNum:副本數 |
| @IndexId | ES主鍵註解 | type:指定註解類型,CUSTOMIZE表示自定義 |
| @IndexField | ES字段註解 | fieldType:字段在索引中的類型;analyzer:索引文檔時用的分詞器;nestedClass:嵌套類 |
- EsProduct中嵌套類型EsProductAttributeValue的代碼如下。
/**
* @auther macrozheng
* @description 搜索商品的屬性信息
* @date 2025/9/4
* @github https://github.com/macrozheng
*/
@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;
/**
* @auther macrozheng
* @description 商品ES操作類
* @date 2025/9/4
* @github https://github.com/macrozheng
*/
public interface EsProductMapper extends BaseEsMapper<EsProduct> {
}
- 然後在Service實現類中直接使用EsProductMapper內置方法實現即可,是不是和MyBatis-Plus的用法一致?
/**
* @auther macrozheng
* @description 搜索商品管理Service實現類
* @date 2025/9/4
* @github https://github.com/macrozheng
*/
@Service
public class EsProductServiceImpl implements EsProductService {
private static final Logger LOGGER = LoggerFactory.getLogger(EsProductServiceImpl.class);
@Autowired
private EsProductMapper esProductMapper;
@Override
public int importAll() {
return esProductMapper.insertBatch(getAllEsProductList(null));
}
@Override
public void delete(Long id) {
esProductMapper.deleteById(id);
}
@Override
public EsProduct create(Long id) {
EsProduct result = null;
List<EsProduct> esProductList = 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的小夥伴應該很熟悉了;
/**
* @auther macrozheng
* @description 搜索商品管理Service實現類
* @date 2025/9/4
* @github https://github.com/macrozheng
*/
@Service
public class EsProductServiceImpl implements EsProductService {
@Autowired
private EsProductMapper esProductMapper;
@Override
public EsPageInfo<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:8088/swagger-ui.html
- 把DSL語句直接複製Kibana中即可執行查看結果了,這和我們手寫DSL語句沒什麼兩樣的。
綜合商品搜索
下面我們來實現一個複雜的商品搜索,涉及到過濾、不同字段匹配權重不同以及可以進行排序。
- 首先來説需求,按輸入的關鍵字搜索商品名稱(權重10)、副標題(權重5)和關鍵詞(權重2),可以按品牌和分類進行篩選,可以有5種排序方式,默認按相關度進行排序,看下接口文檔有助於理解;
- 這個功能之前使用Spring Data來實現非常複雜,使用Easy-Es來實現確實簡潔不少,下面是使用Easy-Es的實現方式;
/**
* @auther macrozheng
* @description 搜索商品管理Service實現類
* @date 2025/9/4
* @github https://github.com/macrozheng
*/
@Service
public class EsProductServiceImpl implements EsProductService {
@Autowired
private EsProductMapper esProductMapper;
@Override
public EsPageInfo<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);
}
}
//搜索
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來實現依舊是那麼簡潔!
/**
* @auther macrozheng
* @description 搜索商品管理Service實現類
* @date 2025/9/4
* @github https://github.com/macrozheng
*/
@Service
public class EsProductServiceImpl implements EsProductService {
@Autowired
private EsProductMapper esProductMapper;
@Override
public EsPageInfo<EsProduct> recommend(Long id, Integer pageNum, Integer pageSize) {
LambdaEsQueryWrapper<EsProduct> wrapper = new LambdaEsQueryWrapper<>();
List<EsProduct> esProductList = getAllEsProductList(id);
if (esProductList.size() > 0) {
EsProduct esProduct = esProductList.get(0);
String keyword = esProduct.getName();
Long brandId = esProduct.getBrandId();
Long productCategoryId = esProduct.getProductCategoryId();
//用於過濾掉相同的商品
wrapper.not().eq(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);
}
}
總結
今天將之前的使用Spring Data的商品搜索案例使用Easy-Es改寫了一下,確實使用Easy-Es更簡單,但是對於複雜的聚合搜索功能,兩者都需要使用原生的RestHighLevelClient用法來實現。使用Easy-Es來操作ES確實足夠優雅,它類似MyBatis-Plus的用法能大大降低我們的學習成本,快速完成開發工作!
參考資料
項目源碼地址
https://github.com/macrozheng/spring-examples/tree/master/spring-easyes