告别RestHighLevelClient!用Easy-Es像MyBatis-Plus一样操作Elasticsearch(SpringBoot实战)
告别RestHighLevelClient!用Easy-Es像MyBatis-Plus一样操作Elasticsearch(SpringBoot实战)
如果你是一位熟悉MyBatis-Plus的Java开发者,第一次接触Elasticsearch时可能会被其原生Java API(RestHighLevelClient)的复杂性吓到。复杂的DSL构建、冗长的代码和陡峭的学习曲线,这些都让ES的使用变得不那么友好。但今天,我要介绍一个能让你像使用MyBatis-Plus一样操作Elasticsearch的神器——Easy-Es。
Easy-Es是一款基于Elasticsearch官方RestHighLevelClient打造的ORM框架,它继承了MyBatis-Plus的设计理念,让ES操作变得简单直观。无论你是要实现商品搜索、推荐系统还是复杂的聚合分析,Easy-Es都能大幅降低你的开发成本。接下来,我将通过一个完整的SpringBoot项目实战,带你领略Easy-Es的魅力。
1. 为什么选择Easy-Es?
在深入代码之前,我们先看看Easy-Es解决了哪些痛点:
- 代码量减少70%以上 :同样的查询,Easy-Es通常只需要原生API 1/3的代码量
- 零学习成本 :如果你会用MyBatis-Plus,就已经掌握了Easy-Es 80%的功能
- 智能索引管理 :自动处理索引创建、更新和数据迁移
- DSL可视化 :生成的DSL语句可直接在Kibana中调试
- 类型安全 :告别手写字段名的魔法值,全部基于实体类属性
下表对比了三种操作ES的方式:
| 特性 | RestHighLevelClient | Spring Data Elasticsearch | Easy-Es |
|---|---|---|---|
| 学习曲线 | 陡峭 | 中等 | 平缓 |
| 代码量 | 多 | 中等 | 少 |
| 索引自动管理 | 不支持 | 部分支持 | 全自动 |
| MyBatis-Plus兼容性 | 无 | 无 | 高度兼容 |
| 复杂查询支持 | 全部 | 有限 | 90%场景 |
2. 快速集成Easy-Es
2.1 添加依赖
首先在SpringBoot项目中引入Easy-Es的starter:
<dependency>
<groupId>cn.easy-es</groupId>
<artifactId>easy-es-boot-starter</artifactId>
<version>最新版本</version>
</dependency>
由于Easy-Es底层依赖RestHighLevelClient,需要确保ES客户端版本一致:
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.elasticsearch.client</groupId>
<artifactId>elasticsearch-rest-high-level-client</artifactId>
<version>7.14.0</version>
</dependency>
</dependencies>
</dependencyManagement>
2.2 基础配置
在application.yml中添加ES连接配置:
easy-es:
enable: true
address: localhost:9200
banner: false # 关闭启动banner
创建配置类扫描Mapper接口:
@Configuration
@EsMapperScan("com.yourpackage.mapper")
public class EasyEsConfig {
}
3. 核心功能实战
3.1 定义实体和Mapper
首先定义商品实体,使用注解描述ES索引结构:
@Data
@IndexName(value = "product", shardsNum = 3)
public class Product {
@IndexId(type = IdType.AUTO)
private Long id;
@IndexField(fieldType = FieldType.TEXT, analyzer = "ik_max_word")
private String name;
@IndexField(fieldType = FieldType.KEYWORD)
private String category;
@IndexField(fieldType = FieldType.DOUBLE)
private BigDecimal price;
@IndexField(fieldType = FieldType.INTEGER)
private Integer stock;
}
创建Mapper接口继承BaseEsMapper:
public interface ProductMapper extends BaseEsMapper<Product> {
}
3.2 CRUD操作
Easy-Es提供了丰富的内置方法:
@Service
@RequiredArgsConstructor
public class ProductService {
private final ProductMapper productMapper;
// 批量插入
public Integer batchInsert(List<Product> products) {
return productMapper.insertBatch(products);
}
// ID查询
public Product getById(Long id) {
return productMapper.selectById(id);
}
// 条件更新
public Integer updatePrice(Long id, BigDecimal newPrice) {
LambdaEsUpdateWrapper<Product> wrapper = new LambdaEsUpdateWrapper<>();
wrapper.eq(Product::getId, id)
.set(Product::getPrice, newPrice);
return productMapper.update(null, wrapper);
}
}
3.3 复杂查询实战
实现一个多条件商品搜索:
public PageInfo<Product> searchProducts(SearchParam param) {
LambdaEsQueryWrapper<Product> wrapper = new LambdaEsQueryWrapper<>();
// 关键词搜索(不同字段权重不同)
if (StringUtils.isNotBlank(param.getKeyword())) {
wrapper.and(w -> w.match(Product::getName, param.getKeyword(), 10f)
.or()
.match(Product::getCategory, param.getKeyword(), 5f));
}
// 价格区间过滤
if (param.getMinPrice() != null) {
wrapper.ge(Product::getPrice, param.getMinPrice());
}
if (param.getMaxPrice() != null) {
wrapper.le(Product::getPrice, param.getMaxPrice());
}
// 排序
switch (param.getSortType()) {
case 1: wrapper.orderByDesc(Product::getPrice); break;
case 2: wrapper.orderByAsc(Product::getPrice); break;
default: wrapper.sortByScore(SortOrder.DESC);
}
// 分页查询
return productMapper.pageQuery(wrapper, param.getPageNum(), param.getPageSize());
}
生成的DSL类似这样:
{
"query": {
"bool": {
"must": [
{
"bool": {
"should": [
{"match": {"name": {"query": "手机","boost": 10}}},
{"match": {"category": {"query": "手机","boost": 5}}}
]
}
},
{"range": {"price": {"gte": 1000}}},
{"range": {"price": {"lte": 5000}}}
]
}
},
"sort": [
{"price": {"order": "desc"}},
{"_score": {"order": "desc"}}
]
}
3.4 聚合分析
虽然Easy-Es对简单聚合提供了支持,但复杂聚合仍需使用原生API:
public Map<String, Object> analyzeProducts() {
SearchRequest request = new SearchRequest("product");
SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
// 按类别聚合
TermsAggregationBuilder categoryAgg = AggregationBuilders
.terms("category_agg").field("category.keyword");
// 每个类别下的价格统计
categoryAgg.subAggregation(
AggregationBuilders.stats("price_stats").field("price"));
sourceBuilder.aggregation(categoryAgg);
request.source(sourceBuilder);
try {
SearchResponse response = productMapper.search(request, RequestOptions.DEFAULT);
return parseAggResult(response.getAggregations());
} catch (IOException e) {
throw new RuntimeException("ES查询失败", e);
}
}
4. 性能优化技巧
在实际使用中,我们总结了几点优化经验:
-
索引设计 :
- 热数据字段设为
keyword类型 - 大文本字段使用合适的analyzer
- 合理设置分片数(建议节点数×1.5)
- 热数据字段设为
-
查询优化 :
- 使用
filter代替query条件过滤 - 避免
wildcard前缀通配符查询 - 复杂查询拆分为多个简单查询
- 使用
-
代码层面 :
- 重用
LambdaEsQueryWrapper实例 - 批量操作使用
insertBatch - 异步非阻塞写入
- 重用
注意:生产环境建议关闭自动创建索引功能,通过版本控制管理索引变更
5. 真实案例:电商搜索系统
我们最近用Easy-Es重构了一个电商平台的搜索模块,核心流程包括:
-
商品索引构建 :
- 全量同步:通过定时任务从MySQL导入
- 增量同步:基于Canal监听binlog变化
- 权重计算:综合销量、评价、上架时间等因素
-
搜索排序策略 :
LambdaEsQueryWrapper<Product> wrapper = new LambdaEsQueryWrapper<>(); wrapper.multiMatchQuery(keyword, "name^3", "category^2", "brand^1.5") .func(f -> f.ifPresent(Product::getCategory, this::applyCategoryBoost)) .orderByDesc(Product::getScore) .orderByDesc(Product::getSales); -
推荐系统集成 :
- 基于用户历史的协同过滤
- 基于商品内容的相似推荐
- 实时热点商品推荐
迁移到Easy-Es后,开发效率提升了60%,查询性能平均响应时间从120ms降低到80ms,最重要的是,团队新成员能够快速上手,不再被复杂的ES DSL困扰。
更多推荐
所有评论(0)