Stories

Detail Return Return

橫空出世!MyBatis-Plus 同款 ES ORM 框架,用起來夠優雅! - Stories Detail

使用過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容器化部署,後端支持多模塊和微服務架構。包括前台商城項目和後台管理系統,能支持完整的訂單流程!涵蓋商品、訂單、購物車、權限、優惠券、會員、支付等功能!

項目演示:

註解的使用

下面我們來學習下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);
    }
}

  • 把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://www.easy-es.cn/

項目源碼地址

https://github.com/macrozheng/spring-examples/tree/master/spring-easyes

user avatar ciel717 Avatar cafebabe Avatar aipaobudehoutao Avatar huifeideniao Avatar chenjiabing666 Avatar mengxiang_592395ab95632 Avatar _61e9689d548cc Avatar zhuyundataflux Avatar abai_681266b7f0de8 Avatar yansudeshanyang Avatar
Favorites 10 users favorite the story!
Favorites

Add a new Comments

Some HTML is okay.