告别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. 性能优化技巧

在实际使用中,我们总结了几点优化经验:

  1. 索引设计

    • 热数据字段设为 keyword 类型
    • 大文本字段使用合适的analyzer
    • 合理设置分片数(建议节点数×1.5)
  2. 查询优化

    • 使用 filter 代替 query 条件过滤
    • 避免 wildcard 前缀通配符查询
    • 复杂查询拆分为多个简单查询
  3. 代码层面

    • 重用 LambdaEsQueryWrapper 实例
    • 批量操作使用 insertBatch
    • 异步非阻塞写入

注意:生产环境建议关闭自动创建索引功能,通过版本控制管理索引变更

5. 真实案例:电商搜索系统

我们最近用Easy-Es重构了一个电商平台的搜索模块,核心流程包括:

  1. 商品索引构建

    • 全量同步:通过定时任务从MySQL导入
    • 增量同步:基于Canal监听binlog变化
    • 权重计算:综合销量、评价、上架时间等因素
  2. 搜索排序策略

    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);
    
  3. 推荐系统集成

    • 基于用户历史的协同过滤
    • 基于商品内容的相似推荐
    • 实时热点商品推荐

迁移到Easy-Es后,开发效率提升了60%,查询性能平均响应时间从120ms降低到80ms,最重要的是,团队新成员能够快速上手,不再被复杂的ES DSL困扰。

更多推荐