以springboot集成easyes为例,进行说明

1.easyes介绍

  • Easy-Es 是一款基于 Elasticsearch 官方客户端(RestHighLevelClient)封装的 ORM 框架,其设计理念与 MyBatis-Plus 相似,旨在简化开发流程
  • 示例,用简洁的语法,解决传统DSL手动拼接json的问题,以及提高代码的可读性,可维护性高,支持自动创建索引,方便索引管理等
    EsWrappers.lambdaQuery(Entity.class) 
          .eq(Entity::getTitle, "程序员") 
          .between(Entity::getTime, start, end);
原始dsl举例:
{
  "query": {
    "bool": {
      "must": [
        { "match": { "title": "Elasticsearch" } },
        { "range": { "price": { "gte": 10 } } }
      ],
      "filter": [
        { "term": { "status": "active" } }
      ]
    }
  }
}
1.1语法介绍
基础 CRUD 语法
// 插入文档 
Entity entity = new Entity();
entity.setTitle(" 程序员");
entity.setContent("Java 开发指南");
entityMapper.insert(entity); 

// 根据ID查询 
Entity result = entityMapper.selectById("1"); 

// 批量更新 
List<Entity> entities = entityMapper.selectList(...); 
entities.forEach(e  -> e.setStatus(1)); 
entityMapper.updateBatchById(entities); 

// 删除文档 
entityMapper.deleteById("1"); 
条件构造器语法(LambdaEsQueryWrapper)
// 1. 等值查询 & 范围查询 
LambdaEsQueryWrapper<Entity> wrapper = new LambdaEsQueryWrapper<>();
wrapper.eq(Entity::getTitle,  "程序员")
       .between(Entity::getCreateTime, "2023-01-01", "2023-12-31");

// 2. 模糊查询 & 排序 
wrapper.like(Entity::getContent,  "开发")
       .orderByDesc(Entity::getReadCount);

// 3. 多条件组合(OR逻辑)
wrapper.and(w  -> w.eq(Entity::getCategory,  "技术")
                  .or()
                  .eq(Entity::getCategory, "编程"));
List<Entity> list = entityMapper.selectList(wrapper); 
高级功能语法
1. 高亮查询
LambdaEsQueryWrapper<Entity> wrapper = new LambdaEsQueryWrapper<>();
wrapper.match(Entity::getContent,  "Java")
       .highlight(Entity::getContent, "<em>", "</em>"); // 高亮标签定义 

List<Entity> results = entityMapper.selectList(wrapper); 
// 结果中高亮内容会自动填充到实体类的@HighLight注解字段 

2. 聚合统计
LambdaEsQueryWrapper<Entity> wrapper = new LambdaEsQueryWrapper<>();
wrapper.termsAggregation("category_agg",  Entity::getCategory) // 按分类聚合 
       .metricAvg("read_avg", Entity::getReadCount);          // 计算平均阅读量

SearchResponse response = entityMapper.search(wrapper); 
// 从response中解析聚合结果 

3. 地理位置查询(Geo)
// 查询距离某坐标3公里内的数据 
LambdaEsQueryWrapper<Entity> wrapper = new LambdaEsQueryWrapper<>();
wrapper.geoDistance(Entity::getLocation,  "40.12,116.50", 3, DistanceUnit.KILOMETERS);

// 支持多种坐标格式:
// 1. 字符串:"lat,lon"
// 2. 数组:new Double[]{116.50, 40.12}
// 3. GeoPoint对象:new GeoPoint(40.12, 116.50)
分页语法
// 1. 普通分页 
Page<Entity> page = new Page<>(1, 10); // 第1页,每页10条
Page<Entity> result = entityMapper.selectPage(page,  wrapper);

// 2. 优化分页(避免深分页性能问题)
Page<Entity> optimizePage = new Page<>(1, 10, true);
索引管理语法
// 自动创建索引(根据实体类注解)
@IndexName(value = "article_index", keepGlobalPrefix = true)
public class Entity {
    @IndexField(type = FieldType.KEYWORD)
    private String title;
    // 其他字段...
}

// 手动同步索引(数据迁移)
boolean success = entityMapper.synchronizedIndex("new_index_name"); 
嵌套查询语法
// 嵌套对象查询(假设Entity中有User类嵌套字段)
LambdaEsQueryWrapper<Entity> wrapper = new LambdaEsQueryWrapper<>();
wrapper.nested(e  -> e.eq(Entity::getUser,  User::getName, "张三")
                     .gt(Entity::getUser, User::getAge, 25));

官方文档:
    /**
     * 场景一: 嵌套and的使用 
     */
    @Test
    public void testNestedAnd() {
        // 下面查询条件等价于MySQL中的 select * from document where star_num in (1, 2) and (title = 'work' or title = '干活')
        LambdaEsQueryWrapper<Document> wrapper = new LambdaEsQueryWrapper<>();
        wrapper.in(Document::getStarNum, 1, 2)
               .and(w -> w.eq(Document::getTitle, "work").or().eq(Document::getTitle, "干活"));
        List<Document> documents = documentMapper.selectList(wrapper);
    }

    /**
     * 场景二: 拼接and的使用 
     */
    @Test
    public void testAnd(){
        // 下面查询条件等价于MySQL中的 select * from document where title = '工作' and content like 'work'
        // 拼接and比较特殊,因为使用场景最多,所以条件与条件之间默认就是拼接and,所以可以直接省略,这点和MP是一样的
        LambdaEsQueryWrapper<Document> wrapper = new LambdaEsQueryWrapper<>();
        wrapper.eq(Document::getTitle, "干活")
               .match(Document::getContent, "work");
        List<Document> documents = documentMapper.selectList(wrapper);
    }

    /**
     * 场景二: 嵌套or的使用 
     */
    @Test
    public void testNestedOr() {
        // 下面查询条件等价于MySQL中的 select * from document where star_num = 1 or (title = '工作' and creator = 'hj')
        LambdaEsQueryWrapper<Document> wrapper = new LambdaEsQueryWrapper<>();
        wrapper.eq(Document::getStarNum, 1)
                .or(i -> i.eq(Document::getTitle, "工作").eq(Document::getCreator, "hj"));
        List<Document> documents = documentMapper.selectList(wrapper);
    }

    /**
     * 场景三: 拼接or的使用 
     */
    @Test
    public void testOr() {
        // 下面查询条件等价于MySQL中的 select * from document where title = '工作' or title = 'work'
        LambdaEsQueryWrapper<Document> wrapper = new LambdaEsQueryWrapper<>();
        wrapper.eq(Document::getTitle, "工作")
                .or()
                .eq(Document::getTitle, "work");
        List<Document> documents = documentMapper.selectList(wrapper);
    }

    /**
     * 场景四: 嵌套filter的使用 其实和场景一一样,只不过filter中的条件不计算得分,无法按得分排序,查询性能稍高
     */
    @Test
    public void testNestedFilter() {
        // 下面查询条件等价于MySQL中的 select * from document where star_num in (1, 2) and (title = '工作' or title = 'work')
        LambdaEsQueryWrapper<Document> wrapper = new LambdaEsQueryWrapper<>();
        wrapper.in(Document::getStarNum, 1, 2)
                .filter(w -> w.eq(Document::getTitle, "工作").or().eq(Document::getTitle, "work"));
        List<Document> documents = documentMapper.selectList(wrapper);
    }

    /**
     * 场景五: 拼接filter的使用 filter中的条件不计算得分,无法按得分排序,查询性能稍高
     */
    @Test
    public void testFilter() {
        // 下面查询条件等价于MySQL中的 select * from document where title = '工作'
        LambdaEsQueryWrapper<Document> wrapper = new LambdaEsQueryWrapper<>();
        wrapper.filter().eq(Document::getTitle, "工作");
        List<Document> documents = documentMapper.selectList(wrapper);
    }

    /**
     * 场景六: 嵌套mustNot的使用 
     */
    @Test
    public void testNestedNot() {
        // 下面查询条件等价于MySQL中的 select * from document where title = '工作' and (size != 18 and age != 18)
        LambdaEsQueryWrapper<Document> wrapper = new LambdaEsQueryWrapper<>();
        wrapper.eq(Document::getTitle, "工作")
               .not(i->i.eq(size,18).eq(age,18));
        List<Document> documents = documentMapper.selectList(wrapper);
    }

    /**
     * 场景六: 拼接not()的使用
     */
    @Test
    public void testNot() {
        // 下面查询条件等价于MySQL中的 select * from document where title = '工作' and  size != 18
        LambdaEsQueryWrapper<Document> wrapper = new LambdaEsQueryWrapper<>();
        wrapper.eq(Document::getTitle, "工作")
               .not()
               .eq(size,18);
        List<Document> documents = documentMapper.selectList(wrapper);
    }
other
增加:
// 插入一条记录,默认插入至当前mapper对应的索引
Integer insert(T entity);
// 插入一条记录 可指定具体插入的路由
Integer insert(String routing, T entity);
// 父子类型 插入一条记录 可指定路由, 父id
Integer insert(String routing, String parentId, T entity);
// 插入数据 可指定具体插入的索引,多个用逗号隔开
Integer insert(T entity, String... indexNames);
// 插入数据,可指定路由及多索引插入
Integer insert(String routing, T entity, String... indexNames);
// 父子类型 插入数据,可指定路由,父id及多索引插入
Integer insert(String routing, String parentId, T entity, String... indexNames);

// 批量插入多条记录
Integer insertBatch(Collection<T> entityList)
// 批量插入 可指定路由
Integer insertBatch(String routing, Collection<T> entityList);
// 父子类型 批量插入 可指定路由, 父id
Integer insertBatch(String routing, String parentId, Collection<T> entityList);

// 批量插入多条记录 可指定具体插入的索引,多个用逗号隔开 
Integer insertBatch(Collection<T> entityList, String... indexNames);
// 批量插入 可指定路由及多索引
Integer insertBatch(String routing, Collection<T> entityList, String... indexNames);
// 父子类型 批量插入 可指定路由,父id及多索引
Integer insertBatch(String routing, String parentId, Collection<T> entityList, String... indexNames);

删除:
// 根据 ID 删除
Integer deleteById(Serializable id);
// 根据 ID 删除 可指定路由
Integer deleteById(String routing, Serializable id);
// 根据 ID 删除 可指定具体的索引,多个用逗号隔开 
Integer deleteById(Serializable id, String... indexNames);
// 根据 ID 删除 可指定路由及多索引
Integer deleteById(String routing, Serializable id, String... indexNames);

// 根据 entity 条件,删除记录
Integer delete(LambdaEsQueryWrapper<T> wrapper);

// 删除(根据ID 批量删除)
Integer deleteBatchIds(Collection<? extends Serializable> idList);
// 删除(根据ID 批量删除)可指定路由
Integer deleteBatchIds(String routing, Collection<? extends Serializable> idList);
// 删除(根据ID 批量删除)可指定具体的索引,多个用逗号隔开 
Integer deleteBatchIds(Collection<? extends Serializable> idList, String... indexNames);
// 删除(根据ID 批量删除) 可指定路由及多索引
Integer deleteBatchIds(String routing, Collection<? extends Serializable> idList, String... indexNames);

修改:
//根据 ID 更新
Integer updateById(T entity);
// 根据 ID 更新 可指定路由
Integer updateById(String routing, T entity);
// 根据 ID 更新 可指定具体的索引,多个用逗号隔开 
Integer updateById(T entity, String... indexNames);
// 根据 ID 更新 可指定路由和多索引
Integer updateById(String routing, T entity, String... indexNames);

// 根据ID 批量更新
Integer updateBatchByIds(Collection<T> entityList);
// 根据ID 批量更新 可指定路由
Integer updateBatchByIds(String routing, Collection<T> entityList);

//根据 ID 批量更新 可指定具体的索引,多个用逗号隔开 
Integer updateBatchByIds(Collection<T> entityList, String... indexNames);
// 根据ID 批量更新 可指定路由及多索引
Integer updateBatchByIds(String routing, Collection<T> entityList, String... indexNames);

// 根据动态条件 更新记录
Integer update(T entity, LambdaEsUpdateWrapper<T> updateWrapper);

查询:
// 获取总数
Long selectCount(LambdaEsQueryWrapper<T> wrapper);
// 获取总数 distinct为是否去重 若为ture则必须在wrapper中指定去重字段
Long selectCount(Wrapper<T> wrapper, boolean distinct);

// 根据 ID 查询 
T selectById(Serializable id);
// 根据 ID 查询 可指定路由
T selectById(String routing, Serializable id);

// 根据 ID 查询 可指定具体的索引,多个用逗号隔开 
T selectById(Serializable id, String... indexNames);
// 根据 ID 查询 可指定路由及多索引
T selectById(String routing, Serializable id, String... indexNames);

// 查询(根据ID 批量查询)
List<T> selectBatchIds(Collection<? extends Serializable> idList);
// 查询(根据ID 批量查询) 可指定路由
List<T> selectBatchIds(String routing, Collection<? extends Serializable> idList);

// 查询(根据ID 批量查询)可指定具体的索引,多个用逗号隔开 
List<T> selectBatchIds(Collection<? extends Serializable> idList, String... indexNames);
// 查询(根据ID 批量查询) 可指定路由及多索引
List<T> selectBatchIds(String routing, Collection<? extends Serializable> idList, String... indexNames);

// 根据动态查询条件,查询一条记录 若存在多条记录 会报错
T selectOne(LambdaEsQueryWrapper<T> wrapper);
// 根据动态查询条件,查询全部记录
List<T> selectList(LambdaEsQueryWrapper<T> wrapper);

2.测试项目版本

jdk 17
springBoot 3.2.4 --为避免兼容问题,尽量和es的version对应
elasticSerch 7.6.2 和ik版本一致  
easyes 2.0.0

3.代码文件

- pom依赖
<dependencies>  
    <dependency>  
        <groupId>org.elasticsearch.client</groupId>  
        <artifactId>elasticsearch-rest-high-level-client</artifactId>  
        <version>7.6.2</version>  
    </dependency>  
    <dependency>  
        <groupId>org.elasticsearch</groupId>  
        <artifactId>elasticsearch</artifactId>  
        <version>7.6.2</version>  
    </dependency>  

    <dependency>  
        <groupId>org.dromara.easy-es</groupId>  
        <artifactId>easy-es-boot-starter</artifactId>  
        <version>2.0.0</version>  
    </dependency>
</dependencies>
- yml
server:  
  port: 8080  

spring:  
  profiles:  
    active: dev  
  main:  
    allow-circular-references: true  
  datasource:  
    driver-class-name: com.mysql.cj.jdbc.Driver  
    #连接数据库的用户名  
    url: jdbc:mysql://localhost:3306/document?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&useSSL=false&allowPublicKeyRetrieval=true&rewriteBatchedStatements=true  
    username: root  
    password: 123456  

easy-es:  
  enable: true #默认为true,若为false则认为不启用本框架  
  address : localhost:9201 # es的连接地址,必须含端口 若为集群,则可以用逗号隔开 例如:127.0.0.1:9200,127.0.0.2:9200  

logging:  
  level:  
    tracer: trace # 设置日志级别为trace,开发时可开启以打印ES全部请求信息及DSL语句
- 启动类
@SpringBootApplication  
@EsMapperScan("com.example.easyes.mapper")  
public class EasyesApplication {  

    public static void main(String[] args) {  
        SpringApplication.run(EasyesApplication.class, args);  
    }  

}
- 实体类(项目用了lombok插件)
@Data  
@Builder  
@AllArgsConstructor  
@NoArgsConstructor  
@Getter  
@Setter  
@IndexName(value = "document")  
public class Document {  
    /**  
     * es对应主键id标识,自定义es中的id为我提供的id  
     */    @TableId  
    @IndexId(type = IdType.CUSTOMIZE)  
    private String id;  
    /**  
     * 文档标题,分析:IK_MAX_WORD,查找:IK_SMART  
     */    @IndexField(fieldType = FieldType.TEXT,analyzer = Analyzer.IK_MAX_WORD,searchAnalyzer = Analyzer.IK_SMART)  
    private String title;  
    /**  
     * 文档内容,分析:IK_MAX_WORD,查找:IK_SMART  
     */    @IndexField(fieldType = FieldType.TEXT,analyzer = Analyzer.IK_MAX_WORD,searchAnalyzer = Analyzer.IK_SMART)  
    private String content;  

    //@DateTimeFormat(pattern = "yyyy-MM-dd")  
    private LocalDateTime createTime;  

}
- Mapper(继承BaseEsMapper)
public interface DocumentMapper extends BaseEsMapper<Document> {  
}

4.CRUD

![[TestUserEeController.java]]

- 创建索引
@GetMapping("/createIndex")  
public Boolean createIndex() {  
    return documentMapper.createIndex();  
}
- 插入数据
@PostMapping("/insertBatch")  
public Integer insertBatch(@RequestParam Integer count) {  
    return insertDocuments(count);  
}  

/**  
 * 批量插入 1000 条数据到 Elasticsearch  
 */public int insertDocuments(int count) {  
    List<Document> documents = generateDocuments(count);  
    documentMapper.insertBatch(documents);  
    System.out.println("成功插入 1000 条数据到 Elasticsearch");  
    return count;  
}  

/**  
 * 生成指定数量的 Document 数据  
 *  
 * @param count 数据数量  
 * @return 数据列表  
 */  
public List<Document> generateDocuments(int count) {  
    List<Document> documents = new ArrayList<>();  
    Random random = new Random();  
    // 当前时间为基准  
    LocalDateTime baseTime = LocalDateTime.now();  

    for (int i = 1; i <= count; i++) {  
        // 创建时间在基准时间基础上随机增加若干秒  
        LocalDateTime createTime = baseTime.plusSeconds(random.nextInt(86400)); // 随机一天内的秒数  
        Document document = Document.builder()  
                .id(String.valueOf(i)) // 模拟自增 ID                .title("Document Title " + i)  
                .content("This is the content of document " + i)  
               .createTime(createTime)  
               .build();  

        documents.add(document);  
    }  

    return documents;  
}
- 修改
@PutMapping("/updateById/{id}")  
public Integer updateById(@PathVariable Integer id,@RequestParam String title,@RequestParam String content) {  
    return EsWrappers.lambdaChainUpdate(documentMapper)  
            .eq(Document::getId, id)  
            .set(Document::getTitle, title)  
            .set(Document::getContent, content)  
            .update();  
}
- 查询(id模糊查询,链式,时间)
/**  
 * description: 根据title模糊查询(普通写法)  
 * @return: java.util.List<com.bluefoxyu.easyes.sample.domain.Document>  
 */  
@GetMapping("/searchByTitle1")  
public List<Document> searchByTitle1() {  
    // 3.查询出所有标题为修改的文档列表  
    LambdaEsQueryWrapper<Document> wrapper = new LambdaEsQueryWrapper<>();  
    wrapper.like(Document::getTitle, "修改");  
    return documentMapper.selectList(wrapper);  
}  

/**  
 * description: 根据title模糊查询(链式写法)  
 * @return: java.util.List<com.bluefoxyu.easyes.sample.domain.Document>  
 */  
@GetMapping("/searchByTitle2")  
public List<Document> searchByTitle2() {  
    // 3.查询出所有标题为修改的文档列表  
    return EsWrappers.lambdaChainQuery(documentMapper)  
            .like(Document::getTitle, "修改")  
            .list();  
}  

/**  
 * <p>  
 * description: 根据 createTime 查询文档列表(链式写法)  
 * </p>  
 *  
 * @param startTime 开始时间  
 * @param endTime 结束时间  
 * @return: java.util.List<com.bluefoxyu.easyes.sample.domain.Document>  
 */  
@GetMapping("/searchByCreateTime")  
public List<Document> searchByCreateTime(@RequestParam LocalDateTime startTime, @RequestParam LocalDateTime endTime) {  

    // 需要进行格式匹配  
    DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");  

    // 查询出 createTime 在 startTime 和 endTime 之间的文档列表  
    return EsWrappers.lambdaChainQuery(documentMapper)  
            .ge(Document::getCreateTime, startTime.format(formatter)) // 大于或等于开始时间  
            .le(Document::getCreateTime, endTime.format(formatter))   // 小于或等于结束时间  
            .list();  
}
删除
/**  
 * 全部删除  
 */  
@DeleteMapping("/deleteAll")  
public Integer deleteAll() {  
    // 创建删除条件:match_all 表示匹配所有文档  
    LambdaEsQueryWrapper<Document> deleteWrapper = new LambdaEsQueryWrapper<>();  
    deleteWrapper.matchAllQuery(); // 匹配所有文档  
    return documentMapper.delete(deleteWrapper);  
}

5.其他相关链接

- 混合查询:
https://www.easy-es.cn/pages/5743eb/
- 字段过滤:
https://www.easy-es.cn/pages/bbee1a/
- 嵌套查询(must,should,filter,must_not )
https://www.easy-es.cn/pages/17ea0a/#es%E5%9B%9B%E5%A4%A7%E5%B5%8C%E5%A5%97%E6%9F%A5%E8%AF%A2
- 官方文档(执行sql,条件构造器)
https://www.easy-es.cn/pages/cb6b26/#_3-%E6%96%B9%E6%B3%95%E5%B7%AE%E5%BC%82
- docker安装es,ik分词器轻移步:
https://blog.csdn.net/m0_74173363/article/details/141718077