springboot 2.3.1整合spring-data-elasticsearch4.0.1 (elsaticsearch 7.6.2) 包含父子文档(join)整合

我想大家都知道软件技术的更新迭代速度非常快,Spring技术栈的迭代步伐更是让我们这些Java程序猿有些望而却步,像Springboot、Springdata、Springcloud这些技术,我们这些做Java的,我想大家无论多忙也会抽时间去跟一下。无论热爱技术也好,为了工作也罢,就是那句古话,学无止境。

言归正传,因为最近要升级Elasticsearch, 之前延用的是6.x版本,众所周知,7.x较6.x版本改动还是有点大的,个人觉得Elasticsearch技术栈的学习资料较其他常用的技术栈要少一些,自己在研究整合时候参考较多的都是官网和JavaDoc, 强烈建议首先要系统的学习一下Elasticsearch, 起码能在kibana中对Elasticsearch操作自如,有助于在整合时了解API的含义,其实SpringData也就是把你在kibana中对Elasticsearch的JSON操作基于RestHighLevelClient再封装成了Template, Springboot的一贯风格。

关于Elasticsearch7.x 与 SpringData的技术我在这里就不介绍了,大家可以参考官网或其他资料,下面贴上官网地址

说明一下,整合以Demo代码案例驱动,主要针对于包含了父子文档,Java如何操纵Elasticsearch,自己对Elasticsearch 7.6.2操作进行了封装,如果单文档、基本的repository如果能解决你的问题,可以没有必用我封装的这一套。Elasticsearch 的数据建模设计原则是:能单文档解决,坚决不用关系文档,You know, for search.

1. 前置准备

  • 搭建ELK环境,Elasticsearch 版本为 7.6.2 ,并且安装ik中文分词器(其它中文分词器自便),Kibana版本为 7.6.2 与 Elasticsearch版本一致

2. 整合从pom开始

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.3.1.RELEASE</version>
        <relativePath/>
    </parent>
    <groupId>cn.apelx</groupId>
    <artifactId>elasticsearch</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>elasticsearch</name>
    <description>Demo project for Spring Boot</description>

    <properties>
        <java.version>1.8</java.version>
        <hutool-core.version>5.3.8</hutool-core.version>
        <fastjson.version>1.2.62</fastjson.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-elasticsearch</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-core</artifactId>
            <version>${hutool-core.version}</version>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>${fastjson.version}</version>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

3. application.properties

# 集群多个逗号隔开
spring.elasticsearch.rest.uris=http://localhost:9200
spring.elasticsearch.rest.connection-timeout=1
spring.elasticsearch.rest.read-timeout=30

这里我具体研究的是spring-data-elasticsearch 封装的 ElasticsearchRestTemplate API, 只需要如上配置,而单文档你用repository的话,如上配置也足矣

4. Mapping操作的封装

直接使用 Spring-Data-Elasticsearch 的 @Document@Field 注解,用repository.save(S entity)方法, SpringData 会根据你@Field注解的属性自动创建mapping。 而实际生产环境中,自动生成的mapping往往会有很多问题,达不到预期的业务需求,所以还是建议大家自己去定义mapping。可以先在Kibana中插入数据,查看自动生成的mapping, 自己对mapping json 做好业务相关调整后,再放入代码中。
因为mapping设置都是json, 基于Java的面向对象思想,我将mapping映射封装成了JavaBean, 先贴一段mapping

{
  "users" : {
    "mappings" : {
      "properties" : {
        "_class" : {
          "type" : "text",
          "fields" : {
            "keyword" : {
              "type" : "keyword",
              "ignore_above" : 256
            }
          },
          "analyzer" : "ik_max_word",
          "search_analyzer": "ik_max_word"
        },
        "age" : {
          "type" : "integer"
        },
        "birth" : {
          "type" : "date",
          "format" : "uuuu-MM-dd'T'HH:mm:ss"
        },
        "firstName" : {
          "type" : "text",
          "fields" : {
            "keyword" : {
              "type" : "keyword",
              "ignore_above" : 256
            }
          },
          "analyzer" : "ik_max_word",
          "search_analyzer": "ik_max_word"
        },
        "lastName" : {
          "type" : "text",
          "fields" : {
            "keyword" : {
              "type" : "keyword",
              "ignore_above" : 256
            }
          },
          "analyzer" : "ik_max_word",
          "search_analyzer": "ik_max_word"
        },
        "money" : {
          "type" : "double"
        },
        "pmsContent" : {
          "type" : "keyword"
        },
        "pmsId" : {
          "type" : "long"
        },
        "userGuid" : {
          "type" : "keyword"
        },
        "userId" : {
          "type" : "keyword"
        },
        "userPermissionRelation" : {
          "type" : "join",
          "eager_global_ordinals" : true,
          "relations" : {
            "users" : "permission"
          }
        }
      }
    }
  }
}

对比如上mapping, 给大家描述一下封装的JavaBean

PropertiesMapping
/**
 * Properties Mapping
 *
 * @author lx
 * @since 2020/7/17 17:32
 */
@Data
@NoArgsConstructor
public class PropertiesMapping {

    /**
     * 字段名称
     */
    private @NonNull String fieldName;

    /**
     * 字段映射(与关系映射互斥)
     */
    private FieldMapping fieldMapping;

    /**
     * 关系映射(与字段映射互斥)
     */
    private RelationshipMapping relationshipMapping;

    public PropertiesMapping(@NonNull String fieldName, FieldMapping fieldMapping) {
        this.fieldName = fieldName;
        this.fieldMapping = fieldMapping;
    }

    public PropertiesMapping(@NonNull String fieldName, RelationshipMapping relationshipMapping) {
        this.fieldName = fieldName;
        this.relationshipMapping = relationshipMapping;
    }
}

PropertiesMapping 类对应如上json中key为 properties 下的每一个字段的设置, fieldName 对应每个字段名称; fieldMapping 对应每个字段的具体mapping设置; relationshipMapping 对应关系文档的设置(join)

FieldMapping
/**
 * 字段映射
 *
 * @author lx
 * @since 2020/7/17 17:33
 */
@Data
@AllArgsConstructor
@NoArgsConstructor
public class FieldMapping {

    /**
     * 字段类型
     */
    private String type;

    /**
     * 是否索引字段
     */
    private Boolean index;

    /**
     * 格式化样式(data字段)
     */
    private String format;

    /**
     * 分词器
     */
    private String analyzer;

    /**
     * 搜索分词器
     */
    @JsonProperty(value = "search_analyzer")
    private String searchAnalyzer;

    /**
     * 字段长度限制
     */
    @JsonProperty(value = "ignore_above")
    private Integer ignoreAbove;

    /**
     * fields 子字段
     */
    private SubfieldMapping fields;

    public FieldMapping(String type) {
        this.type = type;
    }

    public FieldMapping(String type, Boolean index) {
        this.type = type;
        this.index = index;
    }

    public FieldMapping(String type, Boolean index, String format) {
        this.type = type;
        this.index = index;
        this.format = format;
    }

    public FieldMapping(String type, String analyzer, String searchAnalyzer) {
        this.type = type;
        this.analyzer = analyzer;
        this.searchAnalyzer = searchAnalyzer;
    }

    public FieldMapping(String type, Boolean index, String analyzer, String searchAnalyzer, SubfieldMapping fields) {
        this.type = type;
        this.index = index;
        this.analyzer = analyzer;
        this.searchAnalyzer = searchAnalyzer;
        this.fields = fields;
    }
}

FieldMapping类中具体对应mapping映射JavaDoc都有标注, SubfieldMapping 对应的是当前字段的子字段,例如你需要再当前text类型字段下加上一个keyword子字段不分词索引,可以加添加此属性

SubfieldMapping
/**
 * 子字段Mapping
 *
 * @author lx
 * @since 2020/7/17 17:33
 */
@Data
@AllArgsConstructor
@NoArgsConstructor
public class SubfieldMapping {
    /**
     * keyword 映射
     */
    private KeywordMapping keyword;
}

SubfieldMapping 类中我只封装了一个keyword, 对应你所需要添加的子字段映射。若一个子字段无法满足你的业务需求,可以添加多个子字段,根据业务需求基于此类再封装

KeywordMapping
/**
 * Keyword Mapping
 *
 * @author lx
 * @since 2020/7/17 17:39
 */
@Data
@AllArgsConstructor
@NoArgsConstructor
public class KeywordMapping {

    /**
     * 子字段类型
     */
    private String type;

    /**
     * 字段长度限制
     */
    @JsonProperty(value = "ignore_above")
    private Integer ignoreAbove;

    public KeywordMapping(String type) {
        this.type = type;
    }
}
RelationshipMapping
/**
 * 关系映射
 *
 * @author lx
 * @since 2020/7/20 22:50
 */
@Data
@AllArgsConstructor
@NoArgsConstructor
public class RelationshipMapping {

    /**
     * 类型; 推荐join
     */
    private String type;

    /**
     * 是否开启全局预加载,加快查询;
     * 此参数只支持text和keyword,keyword默认可用,而text需要设置fielddata属性
     */
    @JsonProperty(value = "eager_global_ordinals")
    private Boolean eagerGlobalOrdinals;

    /**
     * 关系描述("parentDocName": ["sonDocName" ...])
     */
    private JSONObject relations;
}

RelationshipMapping 类封装了mapping中关系的映射, relations 字段对应你的关系描述, 例如父文档名称为 user , 子文档名称为 permission , 那么relastions的key应该为 "user" , value为 Collections.singletonList("permission") , 如果父子文档不满足你的业务,还要涉及到子孙文档,那么推荐你分两个Index 查询多次,Elasticsearch 不是关系型数据库,它擅长的不是关系的处理

基于如上JSON,如下贴出如何通过JavaBean封装得到mapping
public List<PropertiesMapping> testGetMappingJavaBean() {
        List<PropertiesMapping> propertiesMappingList = new ArrayList<>();
        // -------------------父文档 User Mapping -------------------------------

        FieldMapping classFieldMapping = new FieldMapping("text", Boolean.TRUE, "ik_max_word", "ik_max_word",
                new SubfieldMapping(new KeywordMapping("keyword", 256)));
        PropertiesMapping classPropertiesMapping = new PropertiesMapping("_class", classFieldMapping);
        propertiesMappingList.add(classPropertiesMapping);

        FieldMapping userIdFieldMapping = new FieldMapping("keyword", Boolean.TRUE);
        PropertiesMapping userIdPropertiesMapping = new PropertiesMapping("userId", userIdFieldMapping);
        propertiesMappingList.add(userIdPropertiesMapping);

        FieldMapping ageFieldMapping = new FieldMapping("integer", Boolean.TRUE);
        PropertiesMapping agePropertiesMapping = new PropertiesMapping("age", ageFieldMapping);
        propertiesMappingList.add(agePropertiesMapping);

        FieldMapping birthFieldMapping = new FieldMapping("date", Boolean.TRUE, "uuuu-MM-dd'T'HH:mm:ss");
        PropertiesMapping birthPropertiesMapping = new PropertiesMapping("birth", birthFieldMapping);
        propertiesMappingList.add(birthPropertiesMapping);

        FieldMapping firstNameFieldMapping = new FieldMapping("text", Boolean.TRUE, "ik_max_word", "ik_max_word", new SubfieldMapping(new KeywordMapping("keyword", 256)));
        PropertiesMapping firstNamePropertiesMapping = new PropertiesMapping("firstName", firstNameFieldMapping);
        propertiesMappingList.add(firstNamePropertiesMapping);

        FieldMapping lastNameFieldMapping = new FieldMapping("text", Boolean.TRUE, "ik_max_word", "ik_max_word", new SubfieldMapping(new KeywordMapping("keyword", 256)));
        PropertiesMapping lastNamePropertiesMapping = new PropertiesMapping("lastName", lastNameFieldMapping);
        propertiesMappingList.add(lastNamePropertiesMapping);

        FieldMapping moneyFieldMapping = new FieldMapping("double", Boolean.TRUE);
        PropertiesMapping moneyPropertiesMapping = new PropertiesMapping("money", moneyFieldMapping);
        propertiesMappingList.add(moneyPropertiesMapping);

        FieldMapping userGuidFieldMapping = new FieldMapping("keyword", Boolean.TRUE);
        PropertiesMapping userGuidPropertiesMapping = new PropertiesMapping("userGuid", userGuidFieldMapping);
        propertiesMappingList.add(userGuidPropertiesMapping);

        // ---------------关联关系; permission---------------
        JSONObject relations = new JSONObject();
        relations.put("users", Collections.singletonList("permission"));
        RelationshipMapping relationshipMapping = new RelationshipMapping("join", Boolean.TRUE, relations);
        PropertiesMapping uprPropertiesMapping = new PropertiesMapping("userPermissionRelation", relationshipMapping);
        propertiesMappingList.add(uprPropertiesMapping);


        // -----------------子文档 Permission Mapping --------------------------
        FieldMapping pmsIdFieldMapping = new FieldMapping("long", Boolean.TRUE);
        PropertiesMapping pmsIdPropertiesMapping = new PropertiesMapping("pmsId", pmsIdFieldMapping);
        propertiesMappingList.add(pmsIdPropertiesMapping);

        FieldMapping pmsContentFieldMapping = new FieldMapping("keyword", Boolean.TRUE);
        PropertiesMapping pmsContentPropertiesMapping = new PropertiesMapping("pmsContent", pmsContentFieldMapping);
        propertiesMappingList.add(pmsContentPropertiesMapping);

        return propertiesMappingList;
    }

如何将如上Mapping put 到 Elasticsearch中,调用下面封装的 ElasticsearchUtils 中的工具方法 putMapping 即可

5. 业务 Entity

案例实体为一对多,分别是用户 User 及权限 Permission ,一个User有多条Permission
父子关系的文档在Elasticsearch 中必须保存在一个Index中,而在Java中,往往一对多关系是多个Entity, 毕竟我们都有面向对象的思想,那么一些列的问题就来了,且细听我分说
首先贴上entity

User
/**
 * 用户实体类
 *
 * @author lx
 * @since 2020/7/17 13:55
 */
@Data
@NoArgsConstructor
@AllArgsConstructor
@Document(indexName = "users", replicas = 1, shards = 1, createIndex = true)
public class User implements Serializable {

    @Id
    private String userId;

    @Field(type = FieldType.Keyword)
    private String userGuid;

    @Field(type = FieldType.Text, analyzer = "ik_max_word", searchAnalyzer = "ik_max_word")
    private String firstName;

    @Field(type = FieldType.Text, analyzer = "ik_max_word", searchAnalyzer = "ik_max_word")
    private String lastName;

    @Field(type = FieldType.Integer)
    private Integer age;

    @Field(type = FieldType.Double)
    private Double money;

    /**
     * 1. Jackson日期时间序列化问题:
     * Cannot deserialize value of type `java.time.LocalDateTime` from String "2020-06-04 15:07:54": Failed to deserialize java.time.LocalDateTime: (java.time.format.DateTimeParseException) Text '2020-06-04 15:07:54' could not be parsed at index 10
     * 解决:@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
     * 2. 日期在ES存为long类型
     * 解决:需要加format = DateFormat.custom
     * 3. java.time.DateTimeException: Unable to obtain LocalDate from TemporalAccessor: {DayOfMonth=5, YearOfEra=2020, MonthOfYear=6},ISO of type java.time.format.Parsed
     * 解决:pattern = "uuuu-MM-dd HH:mm:ss" 即将yyyy改为uuuu,或8uuuu: pattern = "8uuuu-MM-dd HH:mm:ss"
     * 参考:https://www.elastic.co/guide/en/elasticsearch/reference/current/migrate-to-java-time.html#java-time-migration-incompatible-date-formats
     */
    @JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss")
    @Field(type = FieldType.Date, format = DateFormat.custom, pattern = "uuuu-MM-dd'T'HH:mm:ss")
    private LocalDateTime birth;


    private RelationModel userPermissionRelation;

}

这里有特别注意的三点

  1. @Document注解 since 4.0 已经过期了type属性
  2. @Id 注解标注的字段,无法再用@Filed注解去标注字段的Elasticsearch 类型,否则用repository save 的时候会报错
  3. Java中的字段难免会和日期打交道,但是Elasticsearch 对应的无法使用 java.util.Date, 它默认的序列化为long型的时间戳,不仅不符合我们的需求,并且反序列化时会报错,包括其他的时间类,都做了尝试,最后在官网和博客中找到了解决方案,JavaDoc中贴了资料地址,有兴趣可以自行研究

这里的 userPermissionRelation 是join关系映射的自定义名称, RelationModel 是我封装的保存父子文档关系join的映射,如下贴出

RelationModel
	/**
 * 关系Model
 *
 * @author lx
 * @since 2020/7/20 23:45
 */
@Data
@NoArgsConstructor
@AllArgsConstructor
public class RelationModel {

    /**
     * 关系名称
     */
    private @NonNull
    String name;

    /**
     * 父文档ID
     */
    private @Nullable
    String parent;

    public RelationModel(String name) {
        this.name = name;
    }
}

父文档relationModel.parent为空即可,子文档需填写父文档ID,为了添加Join关系与索引时指定routing

Permission
/**
 * 权限
 *
 * @author lx
 * @since 2020/7/21 10:17
 */
@Data
@NoArgsConstructor
@AllArgsConstructor
@Document(indexName = "users", replicas = 1, shards = 1, createIndex = true)
public class Permission implements Serializable {

    @Id
    private Long pmsId;

    @Field(type = FieldType.Keyword)
    private String pmsContent;

    private RelationModel userPermissionRelation;

}

注意:这里User 与 Permission 所对应的indexName 是相同的,即保存在同一索引中,否则就无父子文档的概念了,你会发现你搜索不到你想要的结果

那么问题来了,在Elasticsearch中,一个Index只有一个_id字段,我在JavaBean中对一个Index分了多个Entity, 那么这个时候,再用spring-data-elasticsearch 的repository去操作我们的entity 就会有问题了,那么怎么解决呢?说一下我自己的解决思路与过程
  1. 首先第一步肯定是put mapping 到Elasticsearch 中,基于自己封装的JavaBean 与 ElascticsearchRestTemplate, 自己封装了工具类方法
  2. 第二步索引父/子文档入Elasctisearch。父文档没什么问题,如普通的单文档索引一样,子文档索引时需要拿到父文档ID,也就是要获取到我封装的 RelationModel .parent 字段,怎么办?原本我是定义了接口,侵入业务entity, 也就是你的entity必须实现我的接口,接口中必须实现两个方法,1是返回文档ID,2是返回父文档ID,但是这样的侵入有些不太合理,怎么解决呢?最后还是选择了反射,因为使用spring-data-elasticsearch, 你必须要用到@Dcument与@Id注解,那么我就反射获取@Id注解的字段,获取文档ID值,再反射获取 RelationModel .parent 父文档ID字段值,那么问题就迎刃而解了!
    有个小细节先再这里说一下,因为操作json推荐使用jackson的 ObjectMapper , 在我们写mapping入Elasticsearch的时候,JavaBean中为null的字段不能序列化,其二索引父子文档的时候,因为都在一个Index中,我们又分开了业务Entity, 所以索引入Elasticsearch时空字段也无需序列化,所以ObjectMapper要设置null值不参与序列化。还有个细节是ObjectMapper无法正确正/反序列化 LocalDateTime ,所以我们要自定义Jackson的LocalDateTime正/反序列化类,设置入ObjectMapper中
  3. 第三步,也是比较难的痛点,你分开了Entity, 那么你查询的时候出来的数据就会非预期了,因为现在User、Permission 是存在同一个名为 users 的索引中的,在Elasticsearch中你查一个索引, 索引下数据都会出来,但实际业务场景下,虽然User、Permission是存在一个Index中,但是我只想查询User或只想查询Permission怎么办呢? 一个macthAll()方法,会将你Index下所有的文档都返回了,那不是我想要的结果。刚开始我看到了官网的 ElasticsearchCustomConversions , 转换你的类,我希望在返回之后,如果查询的User,那么我在conversions中剔除掉Permission,反之亦然,但是一番尝试之后我发现,它只是把中间转换的部分交给你处理,查询出的还是Index下所有的文档,我尝试在 conversions 方法中返回null, 但人家不允许你返回null,会报错,跟了源码发现,人家是查询好了,不为null了,怎么转成Map, 你可以自定义,Map怎么转成实体类你可以自定义,但是你不能改人结果集的多少,此路不通。
    而后,我就想看能不能看看它是怎么查询的,能不能改它的查询策略,想法固然美好,跟了源码后,底层的 RestHighLevelClient ,想改人源码纯属空闲时间太多,应变一下最后想到了解决方法:
    封装一个查询方法,默认查询包含 existQuery("标注@Id的字段") ,也就是说你想查询User, 你给我User.class, 我会默认给你加上 existQuery(“userId”), 一次只会在一个Index中查询一个类型的文档,如果你想查询Permission, 那么我会加上 existQuery(“pmsId”), 这样就会限制你的查询返回单类型(父或子)文档。 那么如果你不想限制,就想一个findAll查出全部的文档,那么就比较简单了,别分开Entity, 一个Index下的业务类,放到一个Entity中,你就可以直接使用Spring-Data-Elasticsearch 封装的Repository了, save, findAll 等一系列方法,不够还能用spring-data那一套,写个方法名还不用写实现,看大家的业务场景需求,和个人选择吧

下面贴上思路实现的封装

6. 封装的工具类

ElasticsearchUtils
/**
 * Elasticsearch工具类
 *
 * @author lx
 * @since 2020/7/17 10:28
 */
@Component
public class ElasticsearchUtils {


    private ObjectMapper objectMapper;
    private ElasticsearchRestTemplate elasticsearchRestTemplate;
    private static final String PROPERTIES_KEY = "properties";

    @PostConstruct
    public void init(){
        JavaTimeModule timeModule = new JavaTimeModule();
        timeModule.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer());
        timeModule.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer());
        // 设置NULL值不参与序列化
        objectMapper = new ObjectMapper().setSerializationInclusion(JsonInclude.Include.NON_NULL).registerModule(timeModule);
    }


    /**
     * 判断索引是否存在
     *
     * @param indexName 索引名称
     * @return 是否存在索引
     */
    public boolean existIndex(String indexName) {
        if (StringUtils.isNotEmpty(indexName)) {
            return elasticsearchRestTemplate.indexOps(IndexCoordinates.of(indexName)).exists();
        }
        return Boolean.FALSE;
    }


    /**
     * 索引不存在时创建索引
     *
     * @param indexName 索引名称
     * @return 是否创建成功
     */
    public boolean createIndexIfNotExist(String indexName) {
        if (!existIndex(indexName)) {
            return elasticsearchRestTemplate.indexOps(IndexCoordinates.of(indexName)).create();
        }
        return Boolean.FALSE;
    }

    /**
     * 索引存在删除索引
     *
     * @param indexName 索引名称
     * @return 是否删除成功
     */
    public boolean deleteIndexIfExist(String indexName) {
        if (existIndex(indexName)) {
            return elasticsearchRestTemplate.indexOps(IndexCoordinates.of(indexName)).delete();
        }
        return Boolean.FALSE;
    }

    /**
     * 设置索引Mapping
     *
     * @param indexName             索引名称
     * @param propertiesMappingList properties字段映射封装类集合
     * @return 是否设置Mapping成功
     */
    public boolean putMapping(String indexName, List<PropertiesMapping> propertiesMappingList) {
        if (StringUtils.isNotEmpty(indexName)) {
            createIndexIfNotExist(indexName);
            return elasticsearchRestTemplate.indexOps(IndexCoordinates.of(indexName)).putMapping(Document.parse(getJsonMapping(propertiesMappingList)));
        }
        return Boolean.FALSE;
    }


    /**
     * 获取Mapping映射JSON字符串
     *
     * @param propertiesMappingList properties封装类集合
     * @return Mapping映射JSON字符串
     */
    private String getJsonMapping(List<PropertiesMapping> propertiesMappingList) {
        JSONObject fieldsJson = new JSONObject();
        if (propertiesMappingList != null && !propertiesMappingList.isEmpty()) {
            propertiesMappingList.forEach(propertiesMapping ->
                    // 关系映射优先于字段映射
                    fieldsJson.put(propertiesMapping.getFieldName(), propertiesMapping.getRelationshipMapping() != null ?
                            propertiesMapping.getRelationshipMapping() : propertiesMapping.getFieldMapping()));
        }
        JSONObject propertiesJson = new JSONObject();
        propertiesJson.put(PROPERTIES_KEY, fieldsJson);
        try {
            return objectMapper.writeValueAsString(propertiesJson);
        } catch (JsonProcessingException e) {
            e.printStackTrace();
            return propertiesJson.toJSONString();
        }
    }

    /**
     * 新增文档
     *
     * @param indexName          索引名称
     * @param elasticsearchModel elasticsearch文档; 文档需标注@Document注解、包含@Id注解字段, 且@Id注解标注的文档ID字段值不能为空
     * @return 文档ID
     */
    public <T> String indexDoc(String indexName, T elasticsearchModel) {
        if (existIndex(indexName)) {
            return elasticsearchRestTemplate.index(new IndexQueryBuilder().withId(getDocumentIdValue(elasticsearchModel))
                    .withObject(elasticsearchModel).build(), IndexCoordinates.of(indexName));
        }
        return null;
    }

    /**
     * 批量新增文档
     *
     * @param indexName 索引名称
     * @param docList   elasticsearch文档集合; 文档需标注@Document注解、包含@Id注解字段, 且@Id注解标注的文档ID字段值不能为空
     * @return 文档ID
     */
    public <T> List<String> bulkIndexDoc(String indexName, List<T> docList) {
        if (existIndex(indexName) && docList != null && !docList.isEmpty()) {
            List<IndexQuery> indexQueries = new ArrayList<>();
            docList.forEach(doc ->
                    indexQueries.add(new IndexQueryBuilder().withId(getDocumentIdValue(doc)).withObject(doc).build()));
            return elasticsearchRestTemplate.bulkIndex(indexQueries, IndexCoordinates.of(indexName));
        }
        return null;
    }

    /**
     * 批量索引子文档
     * 数量由外部控制; 单次bulk操作控制50条
     *
     * @param indexName       索引名称
     * @param subDocList      elasticsearch子文档集合;
     *                        子文档需标注@Document注解、包含@Id注解字段, 且@Id注解标注的文档ID字段值不能为空
     *                        子文档需包含RelationModel.class字段、且字段RelationModel.parent值不为null
     * @param subRelationName 子文档关系名; 匹配subDocList中RelationModel.name值
     * @return 索引文档ID集合
     */
    public <T> List<String> bulkIndexSubDoc(String indexName, List<T> subDocList, String subRelationName) {
        if (existIndex(indexName) && subDocList != null && !subDocList.isEmpty()) {
            Map<String, List<T>> groupMap = groupByParentId(subDocList, subRelationName);
            List<String> result = new ArrayList<>();
            // 根据父文档ID分组循环
            groupMap.forEach((parentId, subDocGroupList) -> {
                List<IndexQuery> queries = new ArrayList<>();
                subDocGroupList.forEach(subGroupDoc ->
                        queries.add(new IndexQueryBuilder()
                                .withId(getDocumentIdValue(subGroupDoc))
                                .withObject(subGroupDoc)
                                .build()));
                result.addAll(elasticsearchRestTemplate.bulkIndex(queries, BulkOptions.builder().withRoutingId(parentId).build(), IndexCoordinates.of(indexName)));
            });
            return result;
        }
        return null;
    }

    /**
     * 索引子文档
     *
     * @param indexName             索引名称
     * @param elasticsearchSubModel Elasticsearch子文档;
     *                              子文档需标注@Document注解、包含@Id注解字段, 且@Id注解标注的文档ID字段值不能为空
     *                              子文档需包含RelationModel.class字段、且字段RelationModel.parent值不为null
     * @param subRelationName       子文档关系名; 匹配elasticsearchSubModel中RelationModel.name值
     * @return 子文档ID
     */
    public <T> String indexSubDoc(String indexName, T elasticsearchSubModel, String subRelationName) {
        if (existIndex(indexName) && elasticsearchSubModel != null) {
            List<IndexQuery> queries = Collections.singletonList(new IndexQueryBuilder().withId(getDocumentIdValue(elasticsearchSubModel))
                    .withObject(elasticsearchSubModel).build());
            List<String> result = elasticsearchRestTemplate.bulkIndex(queries, BulkOptions.builder()
                    .withRoutingId(getDocumentParentIdValue(elasticsearchSubModel, subRelationName)).build(), IndexCoordinates.of(indexName));
            return result != null && !result.isEmpty() ? result.get(0) : null;
        }
        return null;
    }

    /**
     * 根据父文档ID分组子文档
     *
     * @param subDocList      elasticsearch 子文档集合;
     *                        子文档需标注@Document注解、包含@Id注解字段, 且@Id注解标注的文档ID字段值不能为空
     *                        子文档需包含RelationModel.class字段、且RelationModel.parent值不为null
     * @param subRelationName 子文档关系名; 匹配subDocList中RelationModel.name值
     * @return key: 父文档ID; value: 父文档下所有子文档集合
     */
    private <T> Map<String, List<T>> groupByParentId(List<T> subDocList, String subRelationName) {
        Map<String, List<T>> result = new HashMap<>();
        if (subDocList != null && !subDocList.isEmpty()) {
            subDocList.forEach(subDoc -> {
                // 不存在key创建空ArrayList并push key-value; 存在key返回其value
                result.computeIfAbsent(getDocumentParentIdValue(subDoc, subRelationName), k -> new ArrayList<>()).add(subDoc);
            });
        }
        return result;
    }

    /**
     * 根据ID查询文档
     *
     * @param indexName 索引名称
     * @param docId     文档ID
     * @param tClass    映射类Class
     * @param <T>
     * @return Elasticsearch 文档
     */
    public <T> T findById(String indexName, String docId, Class<T> tClass) {
        if (existIndex(indexName) && StringUtils.isNotEmpty(docId) && tClass != null) {
            return elasticsearchRestTemplate.get(docId, tClass, IndexCoordinates.of(indexName));
        }
        return null;
    }

    /**
     * 根据ID判断文档是否存在
     *
     * @param indexName 索引名称
     * @param docId     文档ID
     * @return 存在与否
     */
    public boolean existDocById(String indexName, String docId) {
        if (existIndex(indexName) && StringUtils.isNotEmpty(docId)) {
            return elasticsearchRestTemplate.exists(docId, IndexCoordinates.of(indexName));
        }
        return Boolean.FALSE;
    }


    /**
     * 更新文档
     *
     * @param indexName          索引名称
     * @param elasticsearchModel elasticsearch文档; 文档需标注@Document注解、包含@Id注解字段, 且@Id标注的文档ID值不能为空
     * @return UpdateResponse.Result
     * @throws JsonProcessingException JsonProcessingException
     */
    public <T> UpdateResponse.Result updateDoc(String indexName, T elasticsearchModel) throws JsonProcessingException {
        return updateDoc(indexName, elasticsearchModel, this.objectMapper);
    }

    /**
     * 更新文档
     *
     * @param indexName          索引名称
     * @param elasticsearchModel elasticsearch文档; 文档需标注@Document注解、包含@Id注解字段, 且@Id标注的文档ID值不能为空
     * @param objectMapper       objectMapper
     * @return UpdateResponse.Result
     * @throws JsonProcessingException JsonProcessingException
     */
    public <T> UpdateResponse.Result updateDoc(String indexName, T elasticsearchModel, ObjectMapper objectMapper) throws JsonProcessingException {
        if (StringUtils.isNotEmpty(indexName) && elasticsearchModel != null) {
            Assert.isTrue(existDocById(indexName, getDocumentIdValue(elasticsearchModel)), "elasticsearch document miss.");
            objectMapper = objectMapper == null ? this.objectMapper : objectMapper;
            String json = objectMapper.writeValueAsString(elasticsearchModel);
            UpdateQuery updateQuery = UpdateQuery.builder(getDocumentIdValue(elasticsearchModel)).withDocument(Document.parse(json)).build();
            return elasticsearchRestTemplate.update(updateQuery, IndexCoordinates.of(indexName)).getResult();
        }
        return UpdateResponse.Result.NOOP;
    }

    /**
     * 查询文档
     *
     * @param indexName    索引名称
     * @param tClass       映射文档类 文档需标注@Document注解、包含@Id注解字段
     * @param queryBuilder 非结构化数据 QueryBuilder; queryBuilder与filterBuilder必须二者存在其一
     * @param <T>
     * @return
     */
    public <T> SearchHits<T> search(String indexName, Class<T> tClass, QueryBuilder queryBuilder) {
        return search(indexName, tClass, queryBuilder, null, null, null, null);
    }

    /**
     * 查询文档
     *
     * @param indexName     索引名称
     * @param tClass        映射文档类 文档需标注@Document注解、包含@Id注解字段
     * @param queryBuilder  非结构化数据 QueryBuilder; queryBuilder与filterBuilder必须二者存在其一
     * @param filterBuilder 结构化数据 QueryBuilder; filterBuilder与queryBuilder必须二者存在其一
     * @param <T>
     * @return
     */
    public <T> SearchHits<T> search(String indexName, Class<T> tClass, QueryBuilder queryBuilder, QueryBuilder filterBuilder) {
        return search(indexName, tClass, queryBuilder, filterBuilder, null, null, null);
    }

    /**
     * 查询文档
     *
     * @param indexName    索引名称
     * @param tClass       映射文档类 文档需标注@Document注解、包含@Id注解字段
     * @param queryBuilder 非结构化数据 QueryBuilder
     * @param pageable     FilterQueryBuilder
     * @param <T>
     * @return
     */
    public <T> SearchHits<T> search(String indexName, Class<T> tClass, QueryBuilder queryBuilder, Pageable pageable) {
        return search(indexName, tClass, queryBuilder, null, null, pageable, null);
    }


    /**
     * 查询文档
     * 查询QueryBuilder默认包含existQuery(tClass.@Id标注的字段)
     * <p>
     * 查询的文档必须包含映射@Document的@Id字段(父子文档关系索引中, 因父子文档保存在一个索引中,
     * 而JavaBean对应父子文档是分开的,业务查询的时候不希望查询到无关业务属性的文档数据映射出来,
     * 故通过包含单个父/子文档的@Id字段避免无关数据问题)
     * </p>
     *
     * @param indexName                  索引名称
     * @param tClass                     映射文档类 文档需标注@Document注解、包含@Id注解字段
     * @param queryBuilder               非结构化数据 QueryBuilder; queryBuilder与filterBuilder必须二者存在其一
     * @param filterBuilder              结构化数据 QueryBuilder; filterBuilder与queryBuilder必须二者存在其一
     * @param abstractAggregationBuilder 聚合查询Builder
     * @param pageable                   分页/排序; 分页从0开始
     * @param fields                     包含字段
     * @param <T>
     * @return
     */
    public <T> SearchHits<T> search(String indexName, Class<T> tClass, @Nullable QueryBuilder queryBuilder,
                                    @Nullable QueryBuilder filterBuilder, @Nullable AbstractAggregationBuilder abstractAggregationBuilder,
                                    @Nullable Pageable pageable, @Nullable String[] fields) {
        if (existIndex(indexName)) {
            // 查询的文档必须包含映射@Document的@Id字段(父子文档关系索引中, 因父子文档保存在一个索引中,
            // 而JavaBean对应父子文档是分开的,业务查询的时候不希望查询到无关业务属性的文档数据映射出来,故通过包含单个父/子文档的@Id字段避免无关数据问题)
            BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery().must(QueryBuilders.existsQuery(getDocumentIdFieldName(tClass)));
            if (queryBuilder != null) {
                boolQueryBuilder.must(queryBuilder);
            }
            NativeSearchQueryBuilder nativeSearchQueryBuilder = new NativeSearchQueryBuilder().withQuery(boolQueryBuilder);
            if (filterBuilder != null) {
                nativeSearchQueryBuilder.withFilter(filterBuilder);
            }
            if (abstractAggregationBuilder != null) {
                nativeSearchQueryBuilder.addAggregation(abstractAggregationBuilder);
                nativeSearchQueryBuilder.withSourceFilter(new FetchSourceFilterBuilder().build());
            }
            if (pageable != null) {
                nativeSearchQueryBuilder.withPageable(pageable);
            }
            if (fields != null && fields.length > 0) {
                nativeSearchQueryBuilder.withFields(fields);
            }
            return elasticsearchRestTemplate.search(nativeSearchQueryBuilder.build(), tClass, IndexCoordinates.of(indexName));
        }
        return null;
    }

    /**
     * 聚合查询
     * 查询QueryBuilder默认包含existQuery(tClass.@Id标注的字段)
     * 若涉及到父子文档,需要在一个index的聚合中包含父子文档时,请使用 {@link ElasticsearchUtils#aggSearch(String, Class, AbstractAggregationBuilder)}
     *
     * @param indexName                  索引名称
     * @param tClass                     映射文档类 文档需标注@Document注解、包含@Id注解字段
     * @param abstractAggregationBuilder 聚合查询Builder
     * @param <T>
     * @return
     */
    public <T> SearchHits<T> aggLimitSearch(String indexName, Class<T> tClass, AbstractAggregationBuilder abstractAggregationBuilder) {
        return search(indexName, tClass, null, null, abstractAggregationBuilder, null, null);
    }

    /**
     * 聚合查询
     * 无父子文档限制, 无查询,仅做Index的聚合
     *
     * @param indexName                  索引名称
     * @param tClass                     映射文档类
     * @param abstractAggregationBuilder 聚合查询Builder
     * @param <T>
     * @return
     */
    public <T> SearchHits<T> aggSearch(String indexName, Class<T> tClass, AbstractAggregationBuilder abstractAggregationBuilder) {
        if (existIndex(indexName) && abstractAggregationBuilder != null) {
            NativeSearchQueryBuilder nativeSearchQueryBuilder = new NativeSearchQueryBuilder().addAggregation(abstractAggregationBuilder)
                    .withSourceFilter(new FetchSourceFilterBuilder().build());
            return elasticsearchRestTemplate.search(nativeSearchQueryBuilder.build(), tClass, IndexCoordinates.of(indexName));
        }
        return null;
    }

    /**
     * 校验JavaBean是否实现了@Document注解
     *
     * @param elasticsearchModel elasticsearch bean
     * @param <T>
     */
    private <T> void validDocument(T elasticsearchModel) {
        Assert.notNull(elasticsearchModel, elasticsearchModel.getClass().getSimpleName() + " must not be null.");
        validDocument(elasticsearchModel.getClass());
    }

    /**
     * 校验JavaBean是否实现了@Document注解
     *
     * @param tClass elasticsearch bean class
     * @param <T>
     */
    private <T> void validDocument(Class<T> tClass) {
        Assert.notNull(tClass, tClass.getSimpleName() + " must not be null.");
        org.springframework.data.elasticsearch.annotations.Document document = tClass
                .getAnnotation(org.springframework.data.elasticsearch.annotations.Document.class);
        Assert.notNull(document, tClass.getSimpleName() + " must have @"
                + org.springframework.data.elasticsearch.annotations.Document.class.getName() + " annotation.");
    }

    /**
     * 获取elasticsearch bean 标注@Id注解的文档Id值
     * 校验 elasticsearch bean 是否实现了@Document注解
     * 获取标注了@Id注解的字段(存在多个取first)
     *
     * @param elasticsearchModel
     * @param <T>
     * @return 文档Id值; not null
     */
    @NonNull
    private <T> String getDocumentIdValue(T elasticsearchModel) {
        validDocument(elasticsearchModel);
        List<Field> fields = ReflectUtils.getClassFieldsByAnnotation(elasticsearchModel.getClass(), Id.class);
        // notEmpty 已校验notNull, 但是编译器无法检测NPE; 添加此句抑制编译器
        Assert.notNull(fields, elasticsearchModel.getClass().getSimpleName()
                + " no fields marked with @" + Id.class.getName() + " annotation.");
        Assert.notEmpty(fields, elasticsearchModel.getClass().getSimpleName()
                + " no fields marked with @" + Id.class.getName() + " annotation.");
        Object fieldValue = ReflectUtils.getFieldValue(elasticsearchModel, fields.get(0));
        Assert.isTrue(fieldValue != null && StringUtils.isNotEmpty(fieldValue.toString()),
                elasticsearchModel.getClass().getSimpleName() + " @Id value must not be null.");
        return String.valueOf(fieldValue);
    }

    /**
     * 获取elasticsearch bean属性为`RelationModel.class`字段的parent父文档ID字段值
     * 校验 elasticsearch bean 是否实现了@Document注解
     * 获取属性为`RelationModel.class`的字段, 多个根据指定的子文档类型名称获取其对应
     * 获取 RelationModel.parent 父文档ID字段值返回
     *
     * @param elasticsearchModel
     * @param subRelationName    子文档关系名
     * @param <T>
     * @return 父文档ID
     */
    @NonNull
    private <T> String getDocumentParentIdValue(T elasticsearchModel, String subRelationName) {
        validDocument(elasticsearchModel);
        Assert.isTrue(StringUtils.isNotEmpty(subRelationName), "parameter `subRelationName` must not be null");
        List<Field> fields = ReflectUtils.getClassFieldsByType(elasticsearchModel.getClass(), RelationModel.class);
        // notEmpty 已校验notNull, 但是编译器无法检测NPE; 添加此句抑制编译器
        Assert.notNull(fields, elasticsearchModel.getClass().getSimpleName() + " must has " + RelationModel.class.getName() + " fields.");
        Assert.notEmpty(fields, elasticsearchModel.getClass().getSimpleName() + " must has " + RelationModel.class.getName() + " fields.");
        for (Field field : fields) {
            Method getMethod = ReflectUtils.getMethodByName(elasticsearchModel.getClass(), StringUtils.upperFirstAndAddPre(field.getName(), "get"));
            Assert.notNull(getMethod, elasticsearchModel.getClass().getSimpleName() + " must has " + field.getName() + " field getter method.");
            try {
                RelationModel relationModel = (RelationModel) getMethod.invoke(elasticsearchModel);
                Assert.notNull(relationModel, elasticsearchModel.getClass().getSimpleName() + "." + field.getName() + " field value must not be null.");
                if (relationModel.getName().equals(subRelationName)) {
                    Assert.isTrue(StringUtils.isNotEmpty(relationModel.getParent()), elasticsearchModel.getClass().getSimpleName() + "." + field.getName() + ".parent value must not be null.");
                    return relationModel.getParent();
                }
            } catch (IllegalAccessException | InvocationTargetException e) {
                e.printStackTrace();
            }
        }
        throw new IllegalArgumentException(elasticsearchModel.getClass().getSimpleName() + " has no sub relation model filed with name eq '" + subRelationName + "'");
    }


    /**
     * 获取elasticsearch bean 标注@Id注解的文档Id字段名称
     * 校验 elasticsearch bean 是否实现了@Document注解
     * 获取标注了@Id注解的字段(存在多个取first)
     *
     * @param tClass
     * @param <T>
     * @return 文档Id字段名称 not null
     */
    @NonNull
    private <T> String getDocumentIdFieldName(Class<T> tClass) {
        validDocument(tClass);
        List<Field> fields = ReflectUtils.getClassFieldsByAnnotation(tClass, Id.class);
        // notEmpty 已校验notNull, 但是编译器无法检测NPE; 添加此句抑制编译器
        Assert.notNull(fields, tClass.getSimpleName() + " no fields marked with @" + Id.class.getName() + " annotation.");
        Assert.notEmpty(fields, tClass.getSimpleName() + " no fields marked with @" + Id.class.getName() + " annotation.");
        return fields.get(0).getName();
    }


    @Autowired
    public void setElasticsearchRestTemplate(ElasticsearchRestTemplate elasticsearchRestTemplate) {
        this.elasticsearchRestTemplate = elasticsearchRestTemplate;
    }
}
ElasticsearchConvertUtils
/**
 * Elasticsearch 数据转换工具类
 *
 * @author lx
 * @since 2020/7/24 15:02
 */
@Component
public class ElasticsearchConvertUtils {

    /**
     * searchHits 转换 List<T> 数据
     *
     * @param searchHits searchHits查询结果集
     * @param <T>
     * @return
     */
    public static <T> List<T> searchHitsConvertDataList(SearchHits<T> searchHits) {
        if (searchHits != null && searchHits.hasSearchHits()) {
            List<T> response = new ArrayList<>();
            searchHits.forEach(searchHit ->
                    response.add(searchHit.getContent()));
            return response;
        }
        return null;
    }

    /**
     * searchHits 转换 List<? extend Terms.Bucket> 数据
     *
     * @param searchHits          searchHits查询结果集
     * @param termAggregationName term聚合名称
     * @param <T>
     * @return
     */
    public static <T> List<? extends Terms.Bucket> searchHitsConvertTermsBuckets(SearchHits<T> searchHits, String termAggregationName) {
        if (searchHits != null && searchHits.hasAggregations() && StringUtils.isNotEmpty(termAggregationName)) {
            Aggregations aggregations = searchHits.getAggregations();
            if (aggregations != null) {
                ParsedTerms parsedTerms = aggregations.get(termAggregationName);
                if (parsedTerms != null) {
                    return parsedTerms.getBuckets();
                }
            }
        }
        return null;
    }

    /**
     * {@link Terms.Bucket} 转换获取子聚合 {@link Range.Bucket}
     *
     * @param bucket              {@link Terms.Bucket} 桶
     * @param rangAggregationName range聚合名称
     * @return
     */
    public static List<? extends Range.Bucket> termsBucketConvertRangeBucket(Terms.Bucket bucket, String rangAggregationName) {
        if (bucket != null && StringUtils.isNotEmpty(rangAggregationName)) {
            Aggregations aggregations = bucket.getAggregations();
            if (aggregations != null) {
                ParsedRange parsedRange = aggregations.get(rangAggregationName);
                if (parsedRange != null) {
                    return parsedRange.getBuckets();
                }
            }
        }
        return null;
    }

}
ReflectUtils
/**
 * 反射工具类
 *
 * @author lx
 * @since 2020/7/21 10:56
 */
public class ReflectUtils extends cn.hutool.core.util.ReflectUtil {

    /**
     * 根据指定的注解获取标注了注解的字段
     *
     * @param targetClass     目标对象Class
     * @param annotationClass 注解Class
     * @return
     */
    public static List<Field> getClassFieldsByAnnotation(Class<?> targetClass, Class<? extends Annotation> annotationClass) {
        if (Objects.nonNull(targetClass) && Objects.nonNull(annotationClass)) {
            Field[] fields = getFields(targetClass);
            if (Objects.nonNull(fields) && fields.length > 0) {
                List<Field> response = new ArrayList<>();
                for (Field field : fields) {
                    Annotation annotation = field.getAnnotation(annotationClass);
                    if (Objects.nonNull(annotation)) {
                        response.add(field);
                    }
                }
                return response.isEmpty() ? null : response;
            }
        }
        return null;
    }


    /**
     * 根据指定Type获取字段
     *
     * @param targetClass 目标对象Class
     * @param type        类型
     * @return
     */
    public static List<Field> getClassFieldsByType(Class<?> targetClass, Type type) {
        if (Objects.nonNull(targetClass) && Objects.nonNull(type)) {
            Field[] fields = getFields(targetClass);
            if (Objects.nonNull(fields) && fields.length > 0) {
                List<Field> response = new ArrayList<>();
                for (Field field : fields) {
                    if (field.getType().equals(type)) {
                        response.add(field);
                    }
                }
                return response.isEmpty() ? null : response;
            }
        }
        return null;
    }
}
StringUtils
/**
 * 字符串工具类
 *
 * @author lx
 * @since 2020/7/17 11:20
 */
public class StringUtils extends StrUtil {
}
LocalDateTimeSerializer
/**
 * LocalDateTime Jackson 序列化
 *
 * @author lx
 * @since 2020/7/23 8:48
 */
public class LocalDateTimeSerializer extends JsonSerializer<LocalDateTime> {
    @Override
    public void serialize(LocalDateTime localDateTime, JsonGenerator jsonGenerator, SerializerProvider serializers) throws IOException {
        jsonGenerator.writeString(localDateTime.format(DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss")));
    }
}
LocalDateTimeDeserializer
/**
 * LocalDateTime Jackson 反序列化
 *
 * @author lx
 * @since 2020/7/23 8:50
 */
public class LocalDateTimeDeserializer extends JsonDeserializer<LocalDateTime> {
  
    @Override
    public LocalDateTime deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException, JsonProcessingException {
        return LocalDateTime.parse(jsonParser.getValueAsString(), DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss"));
    }
}
工具类实现的细节我想我就不再赘述了,工具类的使用与测试内容也比较多,大家可以自己在github上宕下来,有我测试的一些方法,下面贴出github地址,有什么疑问欢迎交流

Github URL


原文: 小七_Ape
作者:spring-data-elasticsearch 4.0.1 整合 springboot 2.3.1 elsaticsearch 7.6.2 包含父子文档(join)整合_es 7.6 父子文档 bulk-CSDN博客