SpringBoot集成Easy-Es实战:从零构建高效搜索引擎

张开发
2026/5/24 7:43:07 15 分钟阅读
SpringBoot集成Easy-Es实战:从零构建高效搜索引擎
1. Easy-Es简介与SpringBoot集成优势Elasticsearch作为一款强大的分布式搜索引擎在企业级应用中越来越普及。但直接使用原生ES客户端API开发时代码往往显得冗长且难以维护。Easy-Es简称EE应运而生它是一款基于Elasticsearch官方RestHighLevelClient打造的ORM框架完美解决了这个问题。我在实际项目中使用Easy-Es已经两年多最大的感受就是开发效率的提升。以前需要写几十行的查询代码现在用EE只需要几行就能搞定。特别是对于从MyBatis-Plus转过来的开发者EE的语法设计会让你感到非常亲切。Easy-Es的核心优势零学习成本如果你熟悉MyBatis-PlusEE的API设计几乎与之完全一致功能强大支持索引自动托管、智能字段映射、多种查询方式等性能优异基于官方客户端开发没有额外的性能损耗扩展灵活既保留了原生API的所有能力又提供了更便捷的操作方式2. 环境准备与基础配置2.1 依赖引入首先创建一个新的SpringBoot项目在pom.xml中添加必要依赖dependencies !-- SpringBoot基础依赖 -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId exclusions exclusion groupIdorg.elasticsearch.client/groupId artifactIdelasticsearch-rest-high-level-client/artifactId /exclusion /exclusions /dependency !-- Elasticsearch官方客户端 -- dependency groupIdorg.elasticsearch.client/groupId artifactIdelasticsearch-rest-high-level-client/artifactId version7.14.0/version /dependency !-- Easy-Es核心依赖 -- dependency groupIdorg.dromara.easy-es/groupId artifactIdeasy-es-boot-starter/artifactId version2.0.0-beta7/version /dependency /dependencies这里有个小坑需要注意SpringBoot自带的ES客户端版本可能与你的ES服务端版本不一致建议显式指定版本号。我在实际项目中就遇到过版本不兼容导致的各种奇怪问题。2.2 配置文件设置在application.yml中添加基础配置easy-es: address: 127.0.0.1:9200 # ES服务地址 schema: http # 协议类型 banner: true # 是否打印banner global-config: process-index-mode: smoothly # 索引处理模式如果ES设置了用户名密码还需要添加认证信息easy-es: username: your_username password: your_password2.3 启动类配置在SpringBoot启动类上添加EsMapperScan注解指定Mapper接口的扫描路径EsMapperScan(com.example.mapper.es) SpringBootApplication public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } }这里有个实际开发中的经验如果你的项目同时使用了MyBatis-Plus和Easy-Es一定要把两者的Mapper放在不同的包下否则会导致扫描冲突。3. 索引操作实战3.1 实体类与索引映射定义商品文档实体类Data IndexName(item_index) // 指定索引名称 public class ItemDoc { IndexId(type IdType.CUSTOMIZE) // 自定义ID private Long id; IndexField(fieldType FieldType.TEXT, analyzer Analyzer.IK_SMART, searchAnalyzer Analyzer.IK_MAX_WORD) private String name; IndexField(fieldType FieldType.INTEGER) private Integer price; IndexField(fieldType FieldType.KEYWORD) private String category; IndexField(fieldType FieldType.DATE, dateFormat yyyy-MM-dd HH:mm:ss) private LocalDateTime createTime; }几个关键注解说明IndexName相当于MySQL的TableName指定索引名称IndexId标记主键字段IndexField定义字段在ES中的类型和分析器3.2 Mapper接口定义创建Mapper接口继承BaseEsMapperpublic interface ItemDocMapper extends BaseEsMapperItemDoc { // 可以自定义方法 }这个设计模式与MyBatis-Plus完全一致对于熟悉MP的开发者来说几乎没有学习成本。3.3 索引CRUD操作创建索引SpringBootTest class IndexTests { Autowired private ItemDocMapper itemDocMapper; Test void testCreateIndex() { boolean success itemDocMapper.createIndex(); System.out.println(创建索引结果 success); } }创建索引时会自动根据实体类的注解配置生成mapping非常方便。我在实际项目中发现如果索引已存在再次创建会报错所以通常需要先判断索引是否存在。索引存在性检查Test void testIndexExists() { String indexName ItemDoc.class.getSimpleName().toLowerCase(); boolean exists itemDocMapper.existsIndex(indexName); System.out.println(索引是否存在 exists); }删除索引Test void testDeleteIndex() { String indexName item_index; boolean success itemDocMapper.deleteIndex(indexName); System.out.println(删除索引结果 success); }更新索引ES的索引一旦创建字段类型就不能修改但可以新增字段Test void testUpdateIndex() { LambdaEsIndexWrapperItemDoc wrapper new LambdaEsIndexWrapper(); wrapper.indexName(item_index) .mapping(ItemDoc::getStock, FieldType.INTEGER); boolean success itemDocMapper.updateIndex(wrapper); System.out.println(更新索引结果 success); }4. 文档CRUD操作4.1 新增文档单条插入Test void testInsert() { ItemDoc item new ItemDoc(); item.setId(1L); item.setName(华为Mate40 Pro); item.setPrice(699900); item.setCategory(手机); item.setCreateTime(LocalDateTime.now()); int affectRows itemDocMapper.insert(item); System.out.println(影响行数 affectRows); }批量插入Test void testBatchInsert() { ListItemDoc items new ArrayList(); // 添加多个ItemDoc对象 int affectRows itemDocMapper.insertBatch(items); System.out.println(批量插入影响行数 affectRows); }批量插入时有个性能优化点建议每批次控制在1000-5000条过大的批次反而会降低性能。我在实际测试中发现每批2000条左右时吞吐量最佳。4.2 查询文档主键查询Test void testSelectById() { ItemDoc item itemDocMapper.selectById(1L); System.out.println(item); }条件查询使用LambdaEsQueryWrapper构建查询条件Test void testSelectByCondition() { LambdaEsQueryWrapperItemDoc wrapper new LambdaEsQueryWrapper(); wrapper.eq(ItemDoc::getCategory, 手机) .between(ItemDoc::getPrice, 100000, 500000) .orderByDesc(ItemDoc::getPrice); ListItemDoc items itemDocMapper.selectList(wrapper); items.forEach(System.out::println); }4.3 更新文档根据ID更新Test void testUpdateById() { ItemDoc item new ItemDoc(); item.setId(1L); item.setPrice(599900); int affectRows itemDocMapper.updateById(item); System.out.println(影响行数 affectRows); }条件更新Test void testUpdateByCondition() { // 更新条件 LambdaEsQueryWrapperItemDoc queryWrapper new LambdaEsQueryWrapper(); queryWrapper.eq(ItemDoc::getCategory, 手机); // 更新内容 LambdaEsUpdateWrapperItemDoc updateWrapper new LambdaEsUpdateWrapper(); updateWrapper.set(ItemDoc::getPrice, 599900); int affectRows itemDocMapper.update(queryWrapper, updateWrapper); System.out.println(影响行数 affectRows); }4.4 删除文档根据ID删除Test void testDeleteById() { int affectRows itemDocMapper.deleteById(1L); System.out.println(影响行数 affectRows); }条件删除Test void testDeleteByCondition() { LambdaEsQueryWrapperItemDoc wrapper new LambdaEsQueryWrapper(); wrapper.lt(ItemDoc::getPrice, 100000); int affectRows itemDocMapper.delete(wrapper); System.out.println(影响行数 affectRows); }5. 高级查询功能5.1 复杂条件查询Easy-Es支持丰富的查询条件构建Test void testComplexQuery() { LambdaEsQueryWrapperItemDoc wrapper new LambdaEsQueryWrapper(); wrapper.match(ItemDoc::getName, 华为) // 匹配查询 .gt(ItemDoc::getPrice, 500000) // 大于 .le(ItemDoc::getPrice, 1000000) // 小于等于 .likeLeft(ItemDoc::getCategory, 机) // 左模糊 .orderByAsc(ItemDoc::getPrice) // 排序 .limit(10); // 分页 ListItemDoc items itemDocMapper.selectList(wrapper); items.forEach(System.out::println); }5.2 聚合查询支持各种聚合操作Test void testAggregation() { LambdaEsQueryWrapperItemDoc wrapper new LambdaEsQueryWrapper(); wrapper.eq(ItemDoc::getCategory, 手机) .groupBy(ItemDoc::getBrand) // 按品牌分组 .sum(ItemDoc::getPrice, total_price); // 计算总价 SearchResponse response itemDocMapper.search(wrapper); // 处理聚合结果 }5.3 高亮查询实现搜索结果高亮显示Test void testHighlight() { LambdaEsQueryWrapperItemDoc wrapper new LambdaEsQueryWrapper(); wrapper.match(ItemDoc::getName, 华为) .highlight(ItemDoc::getName, strong, /strong); SearchResponse response itemDocMapper.search(wrapper); // 处理高亮结果 }6. 实战经验与性能优化6.1 数据同步策略MySQL与ES的数据同步是实际项目中的常见需求推荐几种方案定时任务同步简单但实时性差Binlog监听实时性好但实现复杂双写实现简单但要处理一致性问题我比较推荐的是基于Canal的Binlog监听方案虽然实现复杂些但对业务代码无侵入实时性也能保证。6.2 查询性能优化合理使用分页避免深分页推荐使用searchAfter控制返回字段只查询需要的字段善用filter不计算得分的查询使用filter索引设计根据查询模式设计合理的分片和副本数6.3 常见问题解决问题1查询超时解决方案调整socket-timeout参数easy-es: socket-timeout: 60000 # 单位毫秒问题2字段类型不匹配解决方案检查IndexField注解的fieldType配置问题3分页结果不准确解决方案确保enable-track-total-hitstrue7. 扩展功能与最佳实践7.1 索引别名管理使用别名可以实现无缝的索引切换Test void testAlias() { // 添加别名 itemDocMapper.addAlias(item_index, item_alias); // 查询时可以使用别名 LambdaEsQueryWrapperItemDoc wrapper new LambdaEsQueryWrapper(); wrapper.setIndexName(item_alias) .eq(ItemDoc::getCategory, 手机); ListItemDoc items itemDocMapper.selectList(wrapper); }7.2 父子文档与嵌套类型对于复杂关系数据可以使用父子文档或嵌套类型Data IndexName(blog_index) public class BlogDoc { IndexId private String id; IndexField(fieldType FieldType.TEXT) private String title; IndexField(fieldType FieldType.NESTED) private ListComment comments; } Data public static class Comment { IndexField(fieldType FieldType.TEXT) private String content; IndexField(fieldType FieldType.DATE) private Date createTime; }7.3 自定义Repository对于复杂查询可以自定义Repositorypublic interface CustomItemRepository { ListItemDoc searchByCustomCondition(CustomCondition condition); } public class CustomItemRepositoryImpl implements CustomItemRepository { Autowired private ItemDocMapper itemDocMapper; Override public ListItemDoc searchByCustomCondition(CustomCondition condition) { // 实现自定义查询逻辑 } }8. 项目实战建议索引设计根据查询模式设计合理的分片数和副本数字段类型Text类型用于全文检索Keyword类型用于精确匹配分析器选择中文场景推荐使用IK分析器版本控制建议使用别名管理索引便于回滚监控告警配置ES集群监控及时发现性能问题我在电商项目中实践发现将商品核心信息名称、价格等与商品详情大文本描述分开存储查询性能可以提升30%以上。同时对于不参与搜索的字段设置为indexfalse可以显著减少索引大小。

更多文章