一篇掌握Elasticsearch7.10集群搭建到实战

  目录

一、背景

二、内容概要

三、基础概念

四、ES版本选择

Elasticsearch版本选择

ES客户端SDK版本选择

五、Elasticsearch多租户

多租户下的架构

 租户的识别和路由

六、ES 集群搭建

  集群部署架构

 单集群示例

 多集群示例

ES集群搭建配置

安装中文分词插件 IK

七、ES SDK及增删改查Demo

八、ES管理工具

安装集群管理工具- Kibana

es-header插件安装使用

九、ES集群安全策略

X-pack安全策略开启及集群配置

十、ES运维

数据备份与恢复

索引重建

索引设置别名

十一、ES常用操作命令

十二、ES 相关资料


一、背景

  鉴于公司接下来很多业务场景需要用到搜索引擎,之后负责进行调研, 比较早的时候用过solr,现在来看显得有点过时了,不过它依然还是有自己的优势比如查询效率、同步延迟等。各个云厂商也有搜索引擎但是作为小厂肯定是用不起,现在主流都是ES也只有这一个选择,不过ES也有不少坑,比如最少1s的查询延迟,假如你的业务不允许有1s的查询延迟,那你还是选择别的方案吧。

二、内容概要

  • elasticsearch版本选择对比,包括sdk的选择
  • elasticsearch多租户设计方案,逻辑隔离与物理隔
  • elasticsearch集群搭建,安全设置
  • elasticsearch 增删改查Demo及sdk的封装
  • kibana介绍与搭建
  • elasticsearch常见运维方法及操作命令

三、基础概念

Elasticsearch是一个开源的分布式搜索引擎,可以用来存储您的数据,然后进行快速的搜索。
Elasticsearch是面向文档的,与关系型数据库对比,在数据概念上有些区别:
   
一个业务数据在关系数据库中往往会建一个表,在ES 7.x++中是建一个索引 (indies),无论是表还是索引都会包含一些字段。
数据库中一条记录往往称为一条记录用row来表示,在es中被称为一个文档document。在es中添加一条数据就是添加一个文档。
数据库中一个属性往往会叫做列column,在es中被称为一个field。
数据库创建表有标准的SQL语句,ES也有自己的创建索引语法,例如以下为创建一个名为 test001的索引,数据分成5片,一份副本,共有三个字段
PUT test001
{
    "settings": {
        "number_of_shards": 5,
        "number_of_replicas": 1
    },
    "mappings": {
        "properties": {
            "id": {
                "type": "long"
            },
            "name": {
                "type": "text",
                "analyzer": "ik_max_word"
            },
            "age": {
                "type": "integer"               
            }
        }
    }
}

四、ES版本选择

:由于elasticsearch各版本差异较大,所以在调研及给出具体的方案前我们需要确定使用的es版本

通过大量资料搜索对比了对各个版本差异,最终选择7.10比较稳定的版本来使用。 es从5.x、6.x、7.x、8.x设计及sdk差异都比较大,5和6版本比较老了,与现在spring boot 2.x版本集成存在问题,8.x比较新才出一两年也有比较多的license限制,所以最终选择了7.10版本。

elasticsearch版本选择

  • 结论 :优先选择 7.10.0
  • 原因对比如下
类型\版本6.x7.x8.x建议
Licence

Apache 2.0

7.0 ~ 7.10 Apache 2.0
7.11++ SSPL
SSPL
建议选择更友好的Apache2.0版本,sspl对于在云上想要让ES as a Service,将会面临es厂商的限制
云厂商支持程度

腾讯、阿里云均支持,

华为不支持 

腾讯云最高版 7.10.x
阿里云7.10.x,7.16.x
华为云7.6.x, 7.10.x
均不支持
各云厂商也主要在推广7.x版本,稳定性及占用率更高,建议选择7.x中的7.10.0版本
发版时间

初版2016

2019年
2021年底
建议选择 7.x版本,经历将近4年,稳定性已经经过验证,6.x和8.x一个太老一个太新
特性差异

/

集群配置简化,master选举进行了优化,

能够避免集群脑裂问题;

索引创建已经去除了type,更加简化;

索引查询算法升级,查询性能有优化;

提供安全策略;

Kibana更轻量化,更易用;

ES API进行了升级方便后续升级使用;
更加安全,es默认开启了一些安全功能;
新的搜索API 特性,比如支持NLP等;
7.x基本也能满足目前需求,稳定性也更有保障

2.1 ~ 2.2版本对6.x支持

2.3 ~ 2.7版本对7.x支持
/
基于目前应用中使用的spring boot版本,只能选择 7.x版本

ES客户端SDK版本选择

  • 结论使用spring官方提供的spring-boot-es-starter
客户端适用版本优点缺点建议
TransportClient5.x
6.x
启动速度快,轻量级,可创建极多连接,与应用程序解耦;推荐使用原生的,ES本身就很简单,灵活性很高分发或查询数据速度较慢,不能获取指定节点数据,高版本已经废弃不建议使用
JestClient5.x
6.x
7.x
提供Restful API, 原生ES API不具备;若ES集群使用不同的ES版本,使用原生ES API会有问题,而Jest不会;更安全(可以在Http层添加安全处理);JestClient是ElasticSearch的Java HTTP Rest客户端; JestClient填补了 ElasticSearch缺少HttpRest接口客户端的空白; JestClient可以跨版本18年已经停止更新,7.x、8.x版本兼容性存疑不建议使用
RestClient
low-level-rest-client
5.0++基于Http Client 进行的简单封装,RestClient可以跨版本,支持到目前8.x所有版本。HttpClient和Jsoup都不直接支持发送DELETE方法带参数的请求(官方高版本已经放弃使用)。使用成本较高不推荐
high-level-rest-client7.2.0 - 7.16.x官方基于RestClient进行的封装,提供一系列API方便对ES的使用在7.17版本后官方又废弃了7部分版本推荐使用
New ElasticsearchClient7.17++为最新官方提供的版本较高版本es适用8.x官方推荐使用
spring-boot-es-starter3.0++spring官方封装的ES api,使用起来相对简单,也spring兼容性也能保障,教程也比较多。需要与使用的es版本进行匹配推荐使用

五、Elasticsearch多租户

  • es本身没有租户的能力,对es来说都是一个一个独立索引,这些索引默认都存储在一个es集群上,考虑后面ES as a Service,即作为PaaS能力来提供服务时,就不得不加入租户设计,这也是云时代iaas、paas、saas所面临的共同问题。通过调研市面上并无较多可参考的设计及直接可用的es多租户plugin,基于此需要自行设计支持。

  多租户下的架构

  • 多租户架构往往我们要提供逻辑隔离和物理隔离两种能力,根据需求并可以灵活定制某一租户是独享集群还是共享集群,当独享集群则是物理隔离,当共享集群时就是逻辑隔离,无论哪种方式都要做到部署架构能够低成本支持,应用层对集群无感;

 租户的识别和路由

  • 逻辑隔离代表着所有索引在同一个ES集群上,此时可以通过在索引上加一个tenant字段,通过字段隔离业务数据
  • 物理隔离则可以前置一个负载均衡,比如使用Nginx来完成往不同的ES集群分发请求,不同的集群有不同的地址,应用发起请求时根据请求中的参数进行路由
    • 考虑方便路由及管理,团队内可以定制租户及索引名统一命名规范,比如租户cod: tenantxx,文章索引名为article,完整索引名为:tenantxx_article

六、ES 集群搭建

 注意
  • 生产环境linux下Es 不能使用root启动,需要给es创建独立用户

  集群部署架构

下面示例是以三个机器实例来搭建的,es集群搭建机器实例个数也要是单数避免选举master或脑裂问题。 

 单集群示例

  • 集群每个节点都可以设置为可参与master选举及是否是数据节点
 

 多集群示例

集群访问规范

  出于安全及后续维护综合考虑,es集群对外提供访问地址最好是一个 域名,这样能避免ip到处暴露,也避免IP较高频率变化而导致系统经常升级。

ES集群搭建配置

   es官方下载地址,es和kibana尽量下载同一版本

  • elasticsearch各版本下载地址

        https://www.elastic.co/cn/downloads/past-releases#elasticsearch

  • kibana (es的可视化管理工具)

        https://www.elastic.co/cn/downloads/past-releases/#kibana

     启动前需要修改一些配置,先在一台上进行修改运行验证,待验证通过后,再copy到其它实例上
     集群搭建前必读(以下配置验证于es7.10版本,不同的版本差异比较大请认准版本)

1、先搭建单个节点

  先找任一个节点修改elasticsearch.yml,并添加以下配置(此时不要添加集群和证书配置)

path.data: /opt/data
path.logs: /opt/logs

#http访问端口,程序或kibana使用
http.port: 9200

xpack.security.enabled: true
xpack.security.transport.ssl.enabled: true

2、启动ES

  • 观察启动是否ok,若ok进行第三步
./bin/elasticsearch

3、设置安全账号信息(ES要启动状态)

  • 执行以下命令,给各账号设置密码, 整个集群只需要设置一次即可
  • 警告:设置账户密码切记要在单实例非集群模式时配置,不能添加任何集群的配置,否则会设置失败
    bin/elasticsearch-setup-passwords interactive

4、添加配置集群配置信息

  • 集群搭建需要开启安全,开启安全需要用到证书,证书生成参考【ES集群安全策略步骤进行生成】,生成后进行以下配置
  • elasticsearch.yml加入以下配置
#数据和日志存储路径
path.data: /opt/data
path.logs: /opt/logs
#数据备份和恢复使用,可以一到多个目录
path.repo: ["/opt/backup/es", "/opt/backup/es1"]

#http访问端口,程序或kibana使用
http.port: 9200

#集群名称
cluster.name: es001
#节点名,每个节点名不能重复
node.name: node1

#是否可以参与选举主节点
node.master: true
#是否是数据节点
node.data: true

#允许访问的ip,4个0的话则允许任何ip进行访问
network.host: 0.0.0.0


#es各节点通信端口
transport.tcp.port: 9300

#集群每个节点IP地址。
discovery.seed_hosts: ["xxx.xx.xx.xx:9300", "xx.xx.xx:9300", "xx.xx.xx:9300"]                  
#es7.x新增的配置,初始化一个新的集群时需要此配置来选举master
cluster.initial_master_nodes: ["node1", "node2", "node3"]  


#配置是否压缩tcp传输时的数据,默认为false,不压缩
transport.tcp.compress: true
# 是否支持跨域,es-header插件使用
http.cors.enabled: true
# *表示支持所有域名跨域访问
http.cors.allow-origin: "*"
http.cors.allow-headers: Authorization,X-Requested-With,Content-Type,Content-Length

#集群模式开启安全 https://www.elastic.co/guide/en/elasticsearch/reference/7.17/security-minimal-setup.html
xpack.security.enabled: true
xpack.security.transport.ssl.enabled: true
xpack.license.self_generated.type: basic
xpack.security.transport.ssl.verification_mode: certificate
xpack.security.transport.ssl.keystore.path: certs/elastic-certificates.p12
xpack.security.transport.ssl.truststore.path: certs/elastic-certificates.p12


#默认为1s,指定了节点互相ping的时间间隔。
discovery.zen.fd.ping_interval: 1s
#默认为30s,指定了节点发送ping信息后等待响应的时间,超过此时间则认为对方节点无响应。
discovery.zen.fd.ping_timeout: 30s
#ping失败后重试次数,超过此次数则认为对方节点已停止工作。
discovery.zen.fd.ping_retries: 3

5 jvm.options

        设置占用的内存,根据机器配置设置,es还是需要比较多的内存
-Xms6g     
-Xmx6g
 如果在linux下启动出现以错时需要修改linux系统配置
        max virtual memory areas vm.max_map_count [65530] is too low, increase to at
sudo vim /etc/sysctl.conf
 
#添加参数
vm.max_map_count = 262144

#重新加载配置
sysctl -p

6、配置集群其它节点

    重复1、2、4、5等步骤,然后启动其它节点ES,看是否成功。搭建第2个节点时可以直接把第一个节点的配置和证书全copy过来。
     注意:启动第二个节点后最好先验证节点是否已经加到集群里了,验证方法有以下两种:
    
  • 使用系统命令查看集群节点数量
curl -XGET -u elastic:elastic123 "http://127.0.0.1:9200/_cluster/health?pretty"
  • 创建一个索引看集群中每个节点索引数据
    • 1、可以在任间一个节点执行创建索引命令
      curl -XPUT -u elastic:elastic123 "http://127.0.0.1:9200/test-index"
    • 2、查看所有节点索引是否一致
      curl -XGET -u elastic:elastic123  "http://localhost:9200/_cat/indices?pretty"
     在每个节点都执行以上命令看所有节点是否返回一致,如果集群搭建正常,在任意一个节点创建索引后都会自动同步到其它节点,如果返回不一致则认为集群搭建的是有问题的;

    7、关闭索引自动创建

  • 待es集群搭建完启动成功后,执行以下命令可以关闭索引自动创建功能,当然也可以不关闭,不关闭的话es sdk保存数据时候如果索引不存在则会自动创建索引,尽量还是关闭由专门负责人统一来管理索引
    PUT _cluster/settings
    {
      "persistent": {
        "action.auto_create_index": "false" 
      }
    }
    PUT _cluster/settings { "persistent": { "action.auto_create_index": "false" } }
     注:如果后续装x-pak,可能需要修改 action.auto_create_index 值

    8、修改es集群索引默认分片数

PUT _template/template_http_request_record
{
    "index_patterns": [
        "*"
    ],
    "settings": {
        "number_of_shards": 5,
        "number_of_replicas": 1
    }
}
      index_patterns = * 代表对所有索引生效
      number_of_shards 是索引主分片数.
      number_of_replicas 是备份数量.
     注: 7.x版本默认是1分片1备份,在集群3个节点情况设置为5分片较合适

  

  • ES集群模式数据分布原理
一个ES集群至少有一个节点,一个节点就是一个 elasricsearch 进程,每个节点可以有多个索引,如果创建索引时设置为5个分片,一个副本,那么索引数据将会均匀划分到 5 个分片上 (primary shard,又称主分片),每个分片有一个副本(replica shard,又称复制分片)。为了保证数据的稳定性es会把某个分片及副本存储在不同的实例上。

集群索引分片策略
  • 合理设置分片意义

    • es是一个分式式的搜索引擎,ES中默认为每个索引创建5个分片,每个分片提供一个备份。如果分片过小,当索引数据量非常大的话,每个分片上的数据就会比较多,导致检索时候效率较低,反之分片过多各节点之间的数据同步会过多消耗集群资源、检索时多分片数据归并也会影响效率,所以需要有一些标准来设置合理的分片避免上述问题。

  • 分片设置官方建议

    1、分片过小会导致段过小,进而致使开销增加。您要尽量将分片的平均大小控制在至少几 GB 到几十 GB 之间。
    对时序型数据用例而言,分片大小通常介于 20GB 至 40GB 之间。
    
    2、由于单个分片的开销取决于段数量和段大小,所以通过 forcemerge 操作强制将
    较小的段合并为较大的段能够减少开销并改善查询性能。理想状况下,
    应当在索引内再无数据写入时完成此操作。请注意:这是一个极其耗费资源的操作,
    所以应该在非高峰时段进行。 
    
    3、每个节点上可以存储的分片数量与可用的堆内存大小成正比关系,但是 Elasticsearch并未
    强制规定固定限值。这里有一个很好的经验法则:确保对于节点上已配置的每个 GB,将分片数量
    保持在 20 以下。如果某个节点拥有 30GB 的堆内存,那其最多可有 600 个分片,但是在此限值范围内,
    您设置的分片数量越少,效果就越好。一般而言,这可以帮助集群保持良好的运行状态。
    (编者按:从 8.3 版开始,我们大幅减小了每个分片的堆使用量,
    因此对本博文中的经验法则也进行了相应更新。请按照以下提示了解 8.3+ 版本的 
    Elasticsearch。)
    • 其它网上使用经验

      • 每个分片的数据量不超过最大JVM堆空间设置,一般不超过32G。如果一个索引大概500G,那分片大概在16个左右比较合适。

      • 单个索引分片个数一般不超过节点数的3倍,推荐是1.5 ~ 3倍之间。假如一个集群3个节点,根据索引数据量大小分片数在5-9之间比较合适。

      • 主分片、副本和节点数,分配时也可以参考以下关系:节点数<= 主分片数 * (副本数 +1 )

  • 结论

    • 集群能承受的分片数

      单实例推荐最大分片数: (8G-2G-0.5G) * 20 = 110 个分片

      • 依目前3个4C8G实例规格举例来说明:

      • 单实例推荐最大分片数: (8G-2G-0.5G) * 20 = 110 个分片
      •  2G留给操作系统及其它软件内存使用,0.5G是留给ES本身做计算所需的内存资源。

        所以当前集群可以承载110 * 3 = 330 个分片,假如每个索引5个分片,整个集群可以容纳 330 / 5 = 66 个索引
        • 单个索引分片数

          • 依上面经验来计算3个节点的集群,索引分片为5-9个 比较合适

安装中文分词插件 IK

    ES 底层使用Lucene来构建的索引,Lucene历来对中文分词一直支持的不够好,所以需要使用更智能的中文分词。基于Lucene支持自定义分词的能力,ES也以Plugin方式提供了安装自定义分词的能力。目前使用最广泛的中文分词是IK,也提供了ES版本,直接安装使用即可。
  • IK分词git,上面有安装使用说明
    https://github.com/medcl/elasticsearch-analysis-ik/tree/v7.10.0
  • 安装命令-注意版本是否正确,一定要和es版本匹配
./bin/elasticsearch-plugin install https://github.com/medcl/elasticsearch-analysis-ik/releases/download/v7.10.0/elasticsearch-analysis-ik-7.10.0.zip
    测试分词效果,需要在kibana控制台测试
POST _analyze
{
  "analyzer": "ik_max_word",
  "text":"中国河南南阳"
}

七、ES SDK及增删改查

 完整代码可以从github上下载 

https://github.com/caizi12/my-elasticsearch-starter.git

代码说明

封装了易于使用的elasticsearch starter,使用时可以先把代码Deploy到私有仓库中,然后应用程序中依赖使用,如果没有私有仓库可以把代码copy到应用中使用。

Deploy到仓库后使用方式

1、应用添加依赖

<dependency>
  <groupId>com.my.es</groupId>
  <artifactId>elasticsearch-starter</artifactId>
  <version>1.0.0-SNAPSHOT</version>     
</dependency>

2、application.properties 添加es链接配置

#es链接地址
spring.elasticsearch.uris=http://localhost:9200

#es账号密码,根据实际填写
spring.elasticsearch.username=elastic
spring.elasticsearch.password=123456
#可省配置:连接es集群超时参数,默认毫秒
spring.elasticsearch.connection-timeout=300
spring.elasticsearch.read-timeout=300

3、Demo,(更多示例可以看单元测试部分)



@SpringBootTest
public class MyEsServiceTest {
    @Autowired
    private MyEsService myEsService;

    @Test
    public void delIndex() {
        boolean result = myEsService.deleteIndexIfExist(Student.class);
        Assert.assertTrue(result);
    }

    @Test
    public void delIndexDoc() {
        String result = myEsService.delIndexDoc("3007", Student.class);
        System.out.println("delIndexDoc:" + Student.class.getName());
    }


    @Test
    public void updateMapping() {
        boolean result = myEsService.updateIndexMapping(Student.class);
        Assert.assertTrue(result);
    }


    @Test
    public void updateIndexMapping() {
        boolean result = myEsService.updateIndexMapping(Shop.class);
        Assert.assertTrue(result);
    }

    @Test
    public void createIndex() {
        boolean exist = myEsService.existIndex(Student.class);
        boolean result = false;
        if (!exist) {
            result = myEsService.createIndexIfNotExist(Student.class);
        } else {
            System.out.println("index exist:" + Student.class.getName());
        }
        Assert.assertTrue(result);
    }


    @Test
    public void addIndexDoc() {
        Student student = new Student(1000, "张三", "测试索引添加", "哈哈", "三年二班刘", 10, new Date(), null);
        String documentId = myEsService.addIndexDoc(student);
        System.out.println("addIndexDoc result:" + documentId);
        Assert.assertNotNull(documentId);
    }
}

ES SDK封装代码示例

接口定义

package com.my.elasticsearch;

import java.util.List;

import javax.annotation.Nullable;

import com.my.elasticsearch.model.MyEsSearchRequest;
import org.elasticsearch.index.query.QueryBuilder;
import org.springframework.data.domain.Pageable;
import org.springframework.data.elasticsearch.core.IndexedObjectInformation;
import org.springframework.data.elasticsearch.core.SearchHits;
import org.springframework.data.elasticsearch.core.query.BulkOptions;
import org.springframework.data.elasticsearch.core.query.NativeSearchQueryBuilder;
import org.springframework.data.elasticsearch.core.query.UpdateResponse;

/**
 * es服务接口,该接口提供对es的增删改查操作
 *
 * @authro nantian
 * @date 2022-10-08 15:19
 */
public interface MyEsService {
    /**
     * 判断索引是否存在, 文档需标注@Document注解
     *
     * @param clazz
     * @return
     */
    boolean existIndex(Class<?> clazz);

    /**
     * 判断索引是否存在, 文档需标注@Document注解
     *
     * @param clazz
     * @param nonTenantMode 是否是租户模式,true表示非租户模式,即通用索引
     * @return
     */
    boolean existIndex(Class<?> clazz, boolean nonTenantMode);

    /**
     * 创建索引并设置mapping,setting信息
     * 文档需标注@Document注解、包含@Id注解,其它属性字段需要添加@Field注解
     *
     * @param clazz
     * @return
     */
    boolean createIndexIfNotExist(Class<?> clazz);

    /**
     * 创建索引并设置mapping,setting信息
     * 文档需标注@Document注解、包含@Id注解,其它属性字段需要添加@Field注解
     *
     * @param clazz
     * @param nonTenantMode 是否是租户模式,true表示非租户模式,即通用索引
     * @return
     */
    boolean createIndexIfNotExist(Class<?> clazz, boolean nonTenantMode);

    /**
     * 更新索引mapping信息,已存在的索引重复调用新加的字段会自动更新上去,老字段不会变化
     *
     * @param clazz
     * @return
     */
    boolean updateIndexMapping(Class<?> clazz);

    /**
     * 更新索引mapping信息,已存在的索引重复调用新加的字段会自动更新上去,老字段不会变化
     *
     * @param clazz
     * @param nonTenantMode 是否是租户模式,true表示非租户模式,即通用索引
     * @return
     */
    boolean updateIndexMapping(Class<?> clazz, boolean nonTenantMode);

    /**
     * 删除索引,业务应用中不建议用,如果有必要联系管理员在Kibana控台进行操作
     *
     * @param clazz
     * @return
     */
    boolean deleteIndexIfExist(Class<?> clazz);

    /**
     * 删除索引,业务应用中不建议用,如果有必要联系管理员在Kibana控台进行操作
     *
     * @param clazz
     * @param nonTenantMode 是否是租户模式,true表示非租户模式,即通用索引
     * @return
     */
    boolean deleteIndexIfExist(Class<?> clazz, boolean nonTenantMode);

    /**
     * 判断一个文档是否存在
     *
     * @param clazz
     * @param docId
     * @return
     */
    boolean existDocById(Class<?> clazz, String docId);

    /**
     * 判断一个文档是否存在
     *
     * @param clazz
     * @param docId
     * @param nonTenantMode 是否是租户模式,true表示非租户模式,即通用索引
     * @return
     */
    boolean existDocById(Class<?> clazz, String docId, boolean nonTenantMode);

    /**
     * 添加一个数据到索引中,推荐使用@addIndexDoc(T model)
     *
     * @param indexName 索引名
     * @param model     索引数据,注解@Id的字段值不允许为空
     * @return 文档ID
     */
    <T> String addIndexDoc(String indexName, T model);

    /**
     * 添加一个数据到索引中,推荐使用@addIndexDoc(T model)
     *
     * @param indexName     索引名
     * @param model         索引数据,注解@Id的字段值不允许为空
     * @param nonTenantMode 是否是租户模式,true表示非租户模式,即通用索引
     * @return 文档ID
     */
    <T> String addIndexDoc(String indexName, T model, boolean nonTenantMode);

    /**
     * 添加一个数据到索引中
     * 会自动获取类上的@Document(indexName)属性当索引名
     *
     * @param model 文档数据,注解@Id的字段值不允许为空
     * @return
     */
    <T> String addIndexDoc(T model);

    /**
     * 添加一个数据到索引中
     * 会自动获取类上的@Document(indexName)属性当索引名
     *
     * @param model         文档数据,注解@Id的字段值不允许为空
     * @param nonTenantMode 是否是租户模式,true表示非租户模式,即通用索引
     * @return
     */
    <T> String addIndexDoc(T model, boolean nonTenantMode);

    /**
     * 添加一个数据到索引中,指定数据版本号
     *
     * @param model   es文档; 文档需标注@Document注解、包含@Id注解字段, 且@Id注解标注的文档ID字段值不能为空
     * @param version 数据版本号
     * @return
     */
    <T> String saveIndexDoc(T model, Long version);

    /**
     * 添加一个数据到索引中,指定数据版本号
     *
     * @param model         es文档; 文档需标注@Document注解、包含@Id注解字段, 且@Id注解标注的文档ID字段值不能为空
     * @param version       数据版本号
     * @param nonTenantMode 是否是租户模式,true表示非租户模式,即通用索引
     * @return
     */
    <T> String saveIndexDoc(T model, Long version, boolean nonTenantMode);

    /**
     * 添加一个数据到索引中
     * 会自动获取类上的@Document(indexName)属性当索引名
     * 指定数据版本号
     *
     * @param indexName 索引名称
     * @param model     es文档; 文档需标注@Document注解、包含@Id注解字段, 且@Id注解标注的文档ID字段值不能为空
     * @param version   数据版本号
     * @return
     */
    <T> String saveIndexDoc(String indexName, T model, Long version);

    /**
     * 添加一个数据到索引中
     * 会自动获取类上的@Document(indexName)属性当索引名
     * 指定数据版本号
     *
     * @param indexName     索引名称
     * @param model         es文档; 文档需标注@Document注解、包含@Id注解字段, 且@Id注解标注的文档ID字段值不能为空
     * @param version       数据版本号
     * @param nonTenantMode 是否是租户模式,true表示非租户模式,即通用索引
     * @return
     */
    <T> String saveIndexDoc(String indexName, T model, Long version, boolean nonTenantMode);

    /**
     * 批量添加索引,推荐使用@bulkAddIndexDoc(Class<?> clazz, List<T> docList)
     *
     * @param indexName
     * @param docList
     * @return
     */
    <T> List<IndexedObjectInformation> bulkAddIndexDoc(String indexName, List<T> docList);

    /**
     * 批量添加索引,推荐使用@bulkAddIndexDoc(Class<?> clazz, List<T> docList)
     *
     * @param indexName
     * @param docList
     * @param nonTenantMode 是否是租户模式,true表示非租户模式,即通用索引
     * @return
     */
    <T> List<IndexedObjectInformation> bulkAddIndexDoc(String indexName, List<T> docList, boolean nonTenantMode);

    /**
     * 批量添加索引
     *
     * @param indexName 索引名称
     * @param docList   es文档集合; 文档需标注@Document注解、包含@Id、@Version注解字段, 且@Id注解标注的文档ID字段值不能为空、@Version注解标注的文档数据版本字段值不能为空
     * @return
     */
    <T> List<IndexedObjectInformation> bulkSaveIndexDoc(String indexName, List<T> docList);

    /**
     * 批量添加索引
     *
     * @param indexName     索引名称
     * @param docList       es文档集合; 文档需标注@Document注解、包含@Id、@Version注解字段, 且@Id注解标注的文档ID字段值不能为空、@Version注解标注的文档数据版本字段值不能为空
     * @param nonTenantMode 是否是租户模式,true表示非租户模式,即通用索引
     * @return
     */
    <T> List<IndexedObjectInformation> bulkSaveIndexDoc(String indexName, List<T> docList, boolean nonTenantMode);

    /**
     * 批量添加索引,会自动获取类上的 @Document(indexName)属性当索引名
     *
     * @param clazz
     * @param docList
     * @return
     */
    <T> List<IndexedObjectInformation> bulkAddIndexDoc(Class<?> clazz, List<T> docList);

    /**
     * 批量添加索引,会自动获取类上的 @Document(indexName)属性当索引名
     *
     * @param clazz
     * @param docList
     * @param nonTenantMode 是否是租户模式,true表示非租户模式,即通用索引
     * @return
     */
    <T> List<IndexedObjectInformation> bulkAddIndexDoc(Class<?> clazz, List<T> docList, boolean nonTenantMode);

    /**
     * 批量添加索引
     *
     * @param clazz   会自动获取类上的 @Document(indexName)属性当索引名
     * @param docList es文档集合; 文档需标注@Document注解、包含@Id、@Version注解字段, 且@Id注解标注的文档ID字段值不能为空、@Version注解标注的文档数据版本字段值不能为空
     * @return
     */
    <T> List<IndexedObjectInformation> bulkSaveIndexDoc(Class<?> clazz, List<T> docList);

    /**
     * 批量添加索引
     *
     * @param clazz         会自动获取类上的 @Document(indexName)属性当索引名
     * @param docList       es文档集合; 文档需标注@Document注解、包含@Id、@Version注解字段, 且@Id注解标注的文档ID字段值不能为空、@Version注解标注的文档数据版本字段值不能为空
     * @param nonTenantMode 是否是租户模式,true表示非租户模式,即通用索引
     * @return
     */
    <T> List<IndexedObjectInformation> bulkSaveIndexDoc(Class<?> clazz, List<T> docList, boolean nonTenantMode);

    /**
     * 更新文档,会自动获取类上的@Document(indexName)属性当索引名
     *
     * @param model 注解@Id的字段值不允许为空
     * @return
     */
    <T> UpdateResponse.Result updateDoc(T model);

    /**
     * 更新文档,会自动获取类上的@Document(indexName)属性当索引名
     *
     * @param model         注解@Id的字段值不允许为空
     * @param nonTenantMode 是否是租户模式,true表示非租户模式,即通用索引
     * @return
     */
    <T> UpdateResponse.Result updateDoc(T model, boolean nonTenantMode);

    /**
     * 批量更新文档,会自动获取类上的@Document(indexName)属性当索引名
     *
     * @param clazz
     * @param <T>   注解@Id的字段值不允许为空
     * @return
     */
    <T> List<IndexedObjectInformation> bulkUpdateDoc(Class<?> clazz, List<T> modelList);

    /**
     * 批量更新文档
     *
     * @param clazz
     * @param <T>         注解@Id的字段值不允许为空
     * @param bulkOptions
     * @return
     */
    <T> List<IndexedObjectInformation> bulkUpdateDoc(Class<?> clazz, List<T> modelList, BulkOptions bulkOptions);

    /**
     * 根据ID删除一个索引文档
     *
     * @param id
     * @param clazz
     * @return
     */
    String delIndexDoc(String id, Class<?> clazz);

    /**
     * 根据ID删除一个索引文档
     *
     * @param id
     * @param clazz
     * @param nonTenantMode 是否是租户模式,true表示非租户模式,即通用索引
     * @return
     */
    String delIndexDoc(String id, Class<?> clazz, boolean nonTenantMode);

    /**
     * 批量删除索引
     *
     * @param clazz
     * @param ids
     * @return
     */
    List<String> bulkDelIndexDoc(Class<?> clazz, List<String> ids);

    /**
     * 批量删除索引
     *
     * @param clazz
     * @param ids
     * @param nonTenantMode 是否是租户模式,true表示非租户模式,即通用索引
     * @return
     */
    List<String> bulkDelIndexDoc(Class<?> clazz, List<String> ids, boolean nonTenantMode);

    /**
     * 删除一个索引文档,会自动从类上获取注解为@Id属性的value当作ID
     *
     * @param model
     * @param <T>
     * @return
     */
    <T> String delIndexDoc(T model);

    /**
     * 删除一个索引文档,会自动从类上获取注解为@Id属性的value当作ID
     *
     * @param model
     * @param nonTenantMode 是否是租户模式,true表示非租户模式,即通用索引
     * @return
     */
    <T> String delIndexDoc(T model, boolean nonTenantMode);

    /**
     * @param docId
     * @param tClass
     * @param <T>
     * @return
     */
    <T> T findById(String docId, Class<T> tClass);

    /**
     * @param docId
     * @param clazz
     * @param nonTenantMode 是否是租户模式,true表示非租户模式,即通用索引
     * @param <T>
     * @return
     */
    <T> T findById(String docId, Class<T> clazz, boolean nonTenantMode);

    /**
     * 根据ID批量查询
     *
     *  使用id查询数据实时性更好
     *
     * @param indexName
     * @param clazz
     * @param docIdList
     * @param nonTenantMode
     * @param <T>
     * @return
     */
     <T> List<T> findByIds(String indexName, Class<T> clazz, List<String> docIdList, boolean nonTenantMode) ;
     <T> List<T> findByIds(Class<T> clazz, List<String> docIdList) ;
     <T> List<T> findByIds(Class<T> clazz, List<String> docIdList,boolean nonTenantMode) ;


    /**
     * 更丰富灵活的索引查询,开放spring-boot-es-starter原生NativeSearchQueryBuilder
     *
     * @param clazz        自动获取类上的@Document(indexName)属性当索引名
     * @param queryBuilder
     * @return
     */
    <T> SearchHits<T> search(Class<T> clazz, NativeSearchQueryBuilder queryBuilder);

    /**
     * 更丰富灵活的索引查询,开放spring-boot-es-starter原生NativeSearchQueryBuilder
     *
     * @param clazz         自动获取类上的@Document(indexName)属性当索引名
     * @param queryBuilder
     * @param nonTenantMode 是否是租户模式,true表示非租户模式,即通用索引
     * @return
     */
    <T> SearchHits<T> search(Class<T> clazz, NativeSearchQueryBuilder queryBuilder, boolean nonTenantMode);

    /**
     * 封装查询对象,简化NativeSearchQueryBuilder构造过程
     *
     * @param clazz   自动获取类上的@Document(indexName)属性当索引名
     * @param request
     * @return
     */
    <T> SearchHits<T> search(Class<T> clazz, MyEsSearchRequest request);

    /**
     * 封装查询对象,简化NativeSearchQueryBuilder构造过程
     *
     * @param clazz         自动获取类上的@Document(indexName)属性当索引名
     * @param request
     * @param nonTenantMode 是否是租户模式,true表示非租户模式,即通用索引
     * @return
     */
    <T> SearchHits<T> search(Class<T> clazz, MyEsSearchRequest request, boolean nonTenantMode);

    /**
     * 精确查询类场景推荐使用,es不会计算文档相关性分值,性能更好
     *
     * @param clazz         自动获取类上的@Document(indexName)属性当索引名
     * @param filterBuilder
     * @param pageable
     * @return
     */
    <T> SearchHits<T> searchByFilter(Class<T> clazz, QueryBuilder filterBuilder, @Nullable Pageable pageable);

    /**
     * 精确查询类场景推荐使用,es不会计算文档相关性分值,性能更好
     *
     * @param clazz         自动获取类上的@Document(indexName)属性当索引名
     * @param filterBuilder
     * @param pageable
     * @param nonTenantMode 是否是租户模式,true表示非租户模式,即通用索引
     * @return
     */
    <T> SearchHits<T> searchByFilter(Class<T> clazz, QueryBuilder filterBuilder, @Nullable Pageable pageable, boolean nonTenantMode);

    /**
     * 标题或文章内容检索类场景推荐使用,es会计算文档相关性,并按相关性自动排序
     *
     * @param clazz
     * @param queryBuilder
     * @param pageable
     * @return
     */
    <T> SearchHits<T> search(Class<T> clazz, QueryBuilder queryBuilder, @Nullable Pageable pageable);

    /**
     * 标题或文章内容检索类场景推荐使用,es会计算文档相关性,并按相关性自动排序
     *
     * @param clazz
     * @param queryBuilder
     * @param pageable
     * @param nonTenantMode 是否是租户模式,true表示非租户模式,即通用索引
     * @return
     */
    <T> SearchHits<T> search(Class<T> clazz, QueryBuilder queryBuilder, @Nullable Pageable pageable, boolean nonTenantMode);

    /**
     * 索引数据查询
     *
     * @param clazz         索引类
     * @param queryBuilder
     * @param filterBuilder
     * @return
     */
    <T> SearchHits<T> search(Class<T> clazz, QueryBuilder queryBuilder, QueryBuilder filterBuilder, @Nullable Pageable pageable);

    /**
     * 索引数据查询
     *
     * @param clazz         索引类
     * @param queryBuilder
     * @param filterBuilder
     * @param nonTenantMode 是否是租户模式,true表示非租户模式,即通用索引
     * @return
     */
    <T> SearchHits<T> search(Class<T> clazz, QueryBuilder queryBuilder, QueryBuilder filterBuilder, @Nullable Pageable pageable, boolean nonTenantMode);

}

实现

package com.my.elasticsearch.impl;

import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;

import com.my.elasticsearch.MyEsService;
import com.my.elasticsearch.cache.EsIndexNameCache;
import com.my.elasticsearch.util.EsReflectUtils;
import com.my.elasticsearch.util.EsTenantUtil;
import com.my.elasticsearch.model.MyEsSearchRequest;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.commons.lang3.StringUtils;
import org.elasticsearch.index.query.BoolQueryBuilder;
import org.elasticsearch.index.query.QueryBuilder;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.search.aggregations.AbstractAggregationBuilder;
import org.springframework.data.domain.Pageable;
import org.springframework.data.elasticsearch.core.ElasticsearchRestTemplate;
import org.springframework.data.elasticsearch.core.IndexedObjectInformation;
import org.springframework.data.elasticsearch.core.MultiGetItem;
import org.springframework.data.elasticsearch.core.MyRestIndexTemplate;
import org.springframework.data.elasticsearch.core.SearchHits;
import org.springframework.data.elasticsearch.core.document.Document;
import org.springframework.data.elasticsearch.core.index.Settings;
import org.springframework.data.elasticsearch.core.mapping.IndexCoordinates;
import org.springframework.data.elasticsearch.core.query.BulkOptions;
import org.springframework.data.elasticsearch.core.query.FetchSourceFilter;
import org.springframework.data.elasticsearch.core.query.IndexQuery;
import org.springframework.data.elasticsearch.core.query.IndexQueryBuilder;
import org.springframework.data.elasticsearch.core.query.NativeSearchQueryBuilder;
import org.springframework.data.elasticsearch.core.query.StringQuery;
import org.springframework.data.elasticsearch.core.query.UpdateQuery;
import org.springframework.data.elasticsearch.core.query.UpdateResponse;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;

/**
 * es 公共组件服务实现
 *
 *  该实现提供了租户也非租户模式,租户模式会给索引名前自动加租户code前缀,如果不需要可以进行修改
 *
 * @authro nantian
 * @date 2022-10-08 15:19
 */
public class MyEsServiceImpl implements MyEsService {
    private static ObjectMapper objectMapper;
    private ElasticsearchRestTemplate elasticsearchRestTemplate;
    private static final String PROPERTIES_KEY = "properties";

    public MyEsServiceImpl(ElasticsearchRestTemplate elasticsearchRestTemplate) {
        this.elasticsearchRestTemplate = elasticsearchRestTemplate;
    }

    static {
        //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 index
     * @return
     */
    private String convertTenantIndex(String index) {
        return EsTenantUtil.getTenantIndex(index);
    }

    /**
     * 构建操作es的索引类
     *
     * @param index
     * @param nonTenantMode 是否是租户模式,true表示非租户模式,即通用索引
     * @return
     */
    private IndexCoordinates buildIndexCoordinates(String index, boolean nonTenantMode) {
        if (!nonTenantMode) {
            index = convertTenantIndex(index);
        }
        return IndexCoordinates.of(index);
    }

    private IndexCoordinates buildIndexCoordinates(Class<?> clazz) {
        return buildIndexCoordinates(clazz, false);
    }

    private IndexCoordinates buildIndexCoordinates(Class<?> clazz, boolean nonTenantMode) {
        if (!nonTenantMode) {
            return IndexCoordinates.of(convertTenantIndex(getEsIndexName(clazz)));
        }
        return IndexCoordinates.of(getEsIndexName(clazz));
    }

    /**
     * 根据类@Document(indexName)属性获取索引名
     *
     * @param clazz
     * @return
     */
    private String getEsIndexName(Class<?> clazz) {
        return EsIndexNameCache.get(clazz);
    }

    /**
     * 判断索引是否存在
     *
     * @param indexName     索引名称
     * @param nonTenantMode 是否是租户模式,true表示非租户模式,即通用索引
     * @return
     */
    public boolean existIndex(String indexName, boolean nonTenantMode) {
        if (StringUtils.isNotEmpty(indexName)) {
            return elasticsearchRestTemplate.indexOps(buildIndexCoordinates(indexName, nonTenantMode)).exists();
        }
        return Boolean.FALSE;
    }

    /**
     * 判断索引是否存在
     *
     * @param clazz
     * @return
     */
    @Override
    public boolean existIndex(Class<?> clazz) {
        return existIndex(clazz, false);
    }

    @Override
    public boolean existIndex(Class<?> clazz, boolean nonTenantMode) {
        if (clazz != null) {
            return existIndex(getEsIndexName(clazz), nonTenantMode);
        }
        return Boolean.FALSE;
    }

    /**
     * 索引不存在时创建索引[无分片及mapping信息,暂不开放使用]
     *
     * @param indexName 索引名称
     * @return 是否创建成功
     */
    private boolean createIndexIfNotExist(String indexName) {
        return createIndexIfNotExist(indexName, false);
    }

    /**
     * 索引不存在时创建索引[无分片及mapping信息,暂不开放使用]
     *
     * @param indexName     索引名称
     * @param nonTenantMode 是否是租户模式,true表示非租户模式,即通用索引
     * @return 是否创建成功
     */
    private boolean createIndexIfNotExist(String indexName, boolean nonTenantMode) {
        if (!existIndex(indexName, nonTenantMode)) {
            return elasticsearchRestTemplate.indexOps(buildIndexCoordinates(indexName, nonTenantMode)).create();
        }
        return Boolean.FALSE;
    }

    /**
     * 索引不存在时创建索引并设置分片及mapping信息
     *
     * @param clazz 索引类信息
     * @return 是否创建成功
     */
    @Override
    public boolean createIndexIfNotExist(Class<?> clazz) {
        return createIndexIfNotExist(clazz, false);
    }

    @Override
    public boolean createIndexIfNotExist(Class<?> clazz, boolean nonTenantMode) {
        boolean result = existIndex(clazz, nonTenantMode);
        if (!result) {
            //不能直接用createWithMapping,会漏掉租户信息,改写索引类上租户实现比较复杂
            //elasticsearchRestTemplate.indexOps(clazz).createWithMapping();
            MyRestIndexTemplate esRestIndexTemplate = new MyRestIndexTemplate(elasticsearchRestTemplate, clazz);
            Document document = esRestIndexTemplate.createMapping();
            Settings settings = esRestIndexTemplate.createSettings();

            return esRestIndexTemplate.doCreate(buildIndexCoordinates(clazz, nonTenantMode), settings, document);
        }
        return Boolean.FALSE;
    }

    /**
     * 更新索引字段,会自动获取类上的索引注解信息进行更新索引mapping,已存在的字段不会更新
     *
     * @param clazz
     * @return
     */
    @Override
    public boolean updateIndexMapping(Class<?> clazz) {
        return updateIndexMapping(clazz, false);
    }

    @Override
    public boolean updateIndexMapping(Class<?> clazz, boolean nonTenantMode) {
        boolean result = existIndex(clazz, nonTenantMode);
        if (result) {
            MyRestIndexTemplate esRestIndexTemplate = new MyRestIndexTemplate(elasticsearchRestTemplate, clazz);
            Document document = esRestIndexTemplate.createMapping();
            return esRestIndexTemplate.doPutMapping(buildIndexCoordinates(clazz, nonTenantMode), document);
        }
        return Boolean.FALSE;
    }

    /**
     * 索引存在删除索引
     *
     * @param indexName 索引名称
     * @return 是否删除成功
     */
    public boolean deleteIndexIfExist(String indexName) {
        return deleteIndexIfExist(indexName, false);
    }

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

    /**
     * 索引存在删除索引
     *
     * @param clazz 索引名称
     * @return 是否删除成功
     */
    @Override
    public boolean deleteIndexIfExist(Class<?> clazz) {
        if (existIndex(clazz)) {
            return deleteIndexIfExist(getEsIndexName(clazz));
        }
        return Boolean.FALSE;
    }

    @Override
    public boolean deleteIndexIfExist(Class<?> clazz, boolean nonTenantMode) {
        if (existIndex(clazz, nonTenantMode)) {
            return deleteIndexIfExist(getEsIndexName(clazz), nonTenantMode);
        }
        return Boolean.FALSE;
    }

    /**
     * 新增索引文档,根据类上的@Document获取索引名
     *
     * @param model elasticsearch文档; 文档需标注@Document注解、包含@Id注解字段, 且@Id注解标注的文档ID字段值不能为空
     * @return 文档ID
     */
    @Override
    public <T> String addIndexDoc(T model) {
        return addIndexDoc(getEsIndexName(model.getClass()), model);
    }

    @Override
    public <T> String addIndexDoc(T model, boolean nonTenantMode) {
        return addIndexDoc(getEsIndexName(model.getClass()), model, nonTenantMode);
    }

    /**
     * 新增文档,指定索引名
     *
     * @param indexName 索引名称
     * @param model     es文档; 文档需标注@Document注解、包含@Id注解字段, 且@Id注解标注的文档ID字段值不能为空
     * @return 文档ID
     */
    @Override
    public <T> String addIndexDoc(String indexName, T model) {
        return addIndexDoc(indexName, model, false);
    }

    @Override
    public <T> String addIndexDoc(String indexName, T model, boolean nonTenantMode) {
        Assert.notNull(indexName, "addIndexDoc elasticsearch indexName is null");
        Assert.notNull(model, "addIndexDoc document is null");
        return elasticsearchRestTemplate.index(
                new IndexQueryBuilder().withId(getDocumentIdValue(model)).withObject(model).build(),
                buildIndexCoordinates(indexName, nonTenantMode));
    }

    /**
     * 保存文档,指定数据版本号
     *
     * @param model   es文档; 文档需标注@Document注解、包含@Id注解字段, 且@Id注解标注的文档ID字段值不能为空
     * @param version 数据版本号
     * @param <T>
     * @return
     */
    @Override
    public <T> String saveIndexDoc(T model, Long version) {
        return saveIndexDoc(model, version, false);
    }

    @Override
    public <T> String saveIndexDoc(T model, Long version, boolean nonTenantMode) {
        return saveIndexDoc(getEsIndexName(model.getClass()), model, version, nonTenantMode);
    }

    /**
     * 保存文档,指定索引名和数据版本号
     *
     * @param indexName 索引名称
     * @param model     es文档; 文档需标注@Document注解、包含@Id注解字段, 且@Id注解标注的文档ID字段值不能为空
     * @param version   数据版本号
     * @return 文档ID
     */
    @Override
    public <T> String saveIndexDoc(String indexName, T model, Long version) {
        return saveIndexDoc(indexName, model, version, false);
    }

    /**
     * 保存文档,指定索引名和数据版本号
     *
     * @param indexName     索引名称
     * @param model         es文档; 文档需标注@Document注解、包含@Id注解字段, 且@Id注解标注的文档ID字段值不能为空
     * @param version       数据版本号
     * @param nonTenantMode 是否是租户模式,true表示非租户模式,即通用索引
     * @return 文档ID
     */
    @Override
    public <T> String saveIndexDoc(String indexName, T model, Long version, boolean nonTenantMode) {
        Assert.notNull(indexName, "addIndexDoc elasticsearch indexName is null");
        Assert.notNull(model, "addIndexDoc document is null");
        return elasticsearchRestTemplate.index(
                new IndexQueryBuilder().withId(getDocumentIdValue(model)).withVersion(version).withObject(model).build(),
                buildIndexCoordinates(indexName, nonTenantMode));
    }

    @Override
    public <T> List<IndexedObjectInformation> bulkAddIndexDoc(Class<?> clazz, List<T> docList) {
        return bulkAddIndexDoc(getEsIndexName(clazz), docList);
    }

    @Override
    public <T> List<IndexedObjectInformation> bulkAddIndexDoc(Class<?> clazz, List<T> docList, boolean nonTenantMode) {
        return bulkAddIndexDoc(getEsIndexName(clazz), docList, nonTenantMode);
    }

    @Override
    public <T> List<IndexedObjectInformation> bulkSaveIndexDoc(Class<?> clazz, List<T> docList) {
        return bulkSaveIndexDoc(clazz, docList, false);
    }

    @Override
    public <T> List<IndexedObjectInformation> bulkSaveIndexDoc(Class<?> clazz, List<T> docList, boolean nonTenantMode) {
        return bulkSaveIndexDoc(getEsIndexName(clazz), docList, nonTenantMode);
    }

    /**
     * 批量新增文档
     *
     * @param indexName 索引名称
     * @param docList   es文档集合; 文档需标注@Document注解、包含@Id注解字段, 且@Id注解标注的文档ID字段值不能为空
     * @return 文档ID
     */
    public <T> List<IndexedObjectInformation> bulkAddIndexDoc(String indexName, List<T> docList) {
        return bulkAddIndexDoc(indexName, docList, false);
    }

    /**
     * 批量新增文档
     *
     * @param indexName     索引名称
     * @param docList       es文档集合; 文档需标注@Document注解、包含@Id注解字段, 且@Id注解标注的文档ID字段值不能为空
     * @param nonTenantMode 是否是租户模式,true表示非租户模式,即通用索引
     * @return 文档ID
     */
    @Override
    public <T> List<IndexedObjectInformation> bulkAddIndexDoc(String indexName, List<T> docList, boolean nonTenantMode) {
        Assert.notNull(indexName, "bulkAddIndexDoc elasticsearch indexName is null");
        Assert.notNull(docList, "bulkAddIndexDoc document is null");

        List<IndexQuery> indexQueries = new ArrayList<>();
        docList.forEach(doc ->
                indexQueries.add(new IndexQueryBuilder().withId(getDocumentIdValue(doc)).withObject(doc).build()));

        return elasticsearchRestTemplate.bulkIndex(indexQueries, buildIndexCoordinates(indexName, nonTenantMode));
    }

    /**
     * 批量新增文档
     *
     * @param indexName 索引名称
     * @param docList   es文档集合; 文档需标注@Document注解、包含@Id、@Version注解字段, 且@Id注解标注的文档ID字段值不能为空、@Version注解标注的文档数据版本字段值不能为空
     * @param <T>
     * @return
     */
    @Override
    public <T> List<IndexedObjectInformation> bulkSaveIndexDoc(String indexName, List<T> docList) {
        return bulkSaveIndexDoc(indexName, docList, false);
    }

    /**
     * 批量新增文档
     *
     * @param indexName     索引名称
     * @param docList       es文档集合; 文档需标注@Document注解、包含@Id、@Version注解字段, 且@Id注解标注的文档ID字段值不能为空、@Version注解标注的文档数据版本字段值不能为空
     * @param nonTenantMode 是否是租户模式,true表示非租户模式,即通用索引
     * @return
     */
    @Override
    public <T> List<IndexedObjectInformation> bulkSaveIndexDoc(String indexName, List<T> docList, boolean nonTenantMode) {
        Assert.notNull(indexName, "bulkAddIndexDoc elasticsearch indexName is null");
        Assert.notNull(docList, "bulkAddIndexDoc document is null");

        // 验证是否传version值
        docList.forEach(doc -> getDocumentVersionValue(doc));

        List<IndexQuery> indexQueries = new ArrayList<>();
        docList.forEach(doc ->
                indexQueries.add(new IndexQueryBuilder().withId(getDocumentIdValue(doc)).withVersion(getDocumentVersionValue(doc)).withObject(doc).build()));

        return elasticsearchRestTemplate.bulkIndex(indexQueries, buildIndexCoordinates(indexName, nonTenantMode));
    }

    /**
     * 根据ID查询文档
     *
     * @param indexName     索引名称
     * @param docId         文档ID
     * @param clazz         映射类Class
     * @param nonTenantMode 是否是租户模式,true表示非租户模式,即通用索引
     * @param <T>
     * @return Elasticsearch 文档
     */
    public <T> T findById(String indexName, String docId, Class<T> clazz, boolean nonTenantMode) {
        if (StringUtils.isNotEmpty(docId) && clazz != null) {
            return elasticsearchRestTemplate.get(docId, clazz, buildIndexCoordinates(indexName, nonTenantMode));
        }
        return null;
    }

    public <T> T findById(String docId, Class<T> clazz) {
        return findById(docId, clazz, false);
    }

    @Override
    public <T> T findById(String docId, Class<T> clazz, boolean nonTenantMode) {
        return findById(getEsIndexName(clazz), docId, clazz, nonTenantMode);
    }


    /**
     * 根据多个ID查询文档
     *
     * @param indexName     索引名称
     * @param docIdList      文档ID
     * @param clazz         映射类Class
     * @param nonTenantMode 是否是租户模式,true表示非租户模式,即通用索引
     * @param <T>
     * @return Elasticsearch 文档
     */
    @Override
    public <T> List<T> findByIds(String indexName, Class<T> clazz, List<String> docIdList, boolean nonTenantMode) {
        if (CollectionUtils.isEmpty(docIdList) || clazz == null || indexName == null) {
            return null;
        }
        StringQuery query = StringQuery.builder("stringQuery").withIds(docIdList).build();
        List<MultiGetItem<T>> result = elasticsearchRestTemplate.multiGet(query, clazz, buildIndexCoordinates(indexName, nonTenantMode));
        if(CollectionUtils.isEmpty(result)){
            return null;
        }

        List list = result.stream().map(o->o.getItem()).filter(item->item != null).collect(Collectors.toList());
        return list;
    }

    @Override
    public <T> List<T> findByIds(Class<T> clazz,List<String> docIdList) {
        return findByIds( clazz, docIdList,false);
    }

    @Override
    public <T> List<T> findByIds(Class<T> clazz, List<String> docIdList,boolean nonTenantMode) {
        return findByIds(getEsIndexName(clazz),  clazz,docIdList, nonTenantMode);
    }

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

    public boolean existDocById(Class<?> clazz, String docId) {
        return existDocById(clazz, docId, false);
    }

    @Override
    public boolean existDocById(Class<?> clazz, String docId, boolean nonTenantMode) {
        return existDocById(getEsIndexName(clazz), docId, nonTenantMode);
    }

    public <T> UpdateResponse.Result updateDoc(T elasticsearchModel) {
        return updateDoc(elasticsearchModel, false);
    }

    @Override
    public <T> UpdateResponse.Result updateDoc(T elasticsearchModel, boolean nonTenantMode) {
        String indexName = getEsIndexName(elasticsearchModel.getClass());
        return updateDoc(indexName, elasticsearchModel, nonTenantMode);
    }

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

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

    /**
     * 更新文档
     *
     * @param indexName          索引名称
     * @param elasticsearchModel elasticsearch文档; 文档需标注@Document注解、包含@Id注解字段, 且@Id标注的文档ID值不能为空
     * @param objectMapper       objectMapper
     * @param nonTenantMode      是否是租户模式,false表示非租户模式,即通用索引
     * @return UpdateResponse.Result
     */
    private <T> UpdateResponse.Result updateDoc(String indexName, T elasticsearchModel, ObjectMapper objectMapper, boolean nonTenantMode) {
        Assert.notNull(indexName, "bulkUpdateDoc clazz is null");
        Assert.notNull(elasticsearchModel, "bulkUpdateDoc modelList is null");
        try {
            String id = getDocumentIdValue(elasticsearchModel);
            Assert.isTrue(existDocById(indexName, id, nonTenantMode), "elasticsearch document is not exist.");

            objectMapper = objectMapper == null ? this.objectMapper : objectMapper;
            String json = objectMapper.writeValueAsString(elasticsearchModel);
            UpdateQuery updateQuery = UpdateQuery.builder(id).withDocument(Document.parse(json)).build();

            return elasticsearchRestTemplate.update(updateQuery, buildIndexCoordinates(indexName, nonTenantMode)).getResult();
        } catch (JsonProcessingException e) {
            e.printStackTrace();
            throw new RuntimeException(e);
        }
    }

    public <T> List<IndexedObjectInformation> bulkUpdateDoc(Class<?> clazz, List<T> modelList) {
        return bulkUpdateDoc(clazz, modelList, null);
    }

    public <T> List<IndexedObjectInformation> bulkUpdateDoc(Class<?> clazz, List<T> modelList, BulkOptions bulkOptions) {
        return bulkUpdateDoc(clazz, modelList, bulkOptions, objectMapper);
    }


    /**
     * 批量更新文档
     *
     * @param clazz        索引名称
     * @param modelList    elasticsearch文档; 文档需标注@Document注解、包含@Id注解字段, 且@Id标注的文档ID值不能为空
     * @param objectMapper objectMapper
     * @return UpdateResponse.Result
     */
    private <T> List<IndexedObjectInformation> bulkUpdateDoc(Class<?> clazz, List<T> modelList, BulkOptions bulkOptions,
                                                             ObjectMapper objectMapper) {
        Assert.notNull(clazz, "bulkUpdateDoc clazz is null");
        Assert.notNull(clazz, "bulkUpdateDoc modelList is null");

        try {
            List<UpdateQuery> queries = new ArrayList(modelList.size());
            UpdateQuery updateQuery = null;
            String id = null;
            for (T model : modelList) {
                id = getDocumentIdValue(model);
                Assert.notNull(id, clazz.getName() + " instance document id is null");
                String json = objectMapper.writeValueAsString(model);
                updateQuery = UpdateQuery.builder(getDocumentIdValue(model)).withDocument(Document.parse(json)).build();
                queries.add(updateQuery);
            }
            bulkOptions = bulkOptions == null ? BulkOptions.defaultOptions() : bulkOptions;
            return elasticsearchRestTemplate.doBulkOperation(queries, bulkOptions, buildIndexCoordinates(clazz));
        } catch (Exception e) {
            e.printStackTrace();
            throw new RuntimeException(e);
        }
    }

    private <T> String getDocumentIdValue(T elasticsearchModel) {
        return EsReflectUtils.getDocumentIdValue(elasticsearchModel);
    }

    private <T> Long getDocumentVersionValue(T elasticsearchModel) {
        return EsReflectUtils.getDocumentVersionValue(elasticsearchModel);
    }

    /**
     * 查询文档
     *
     * @param clazz         映射文档类 文档需标注@Document注解、包含@Id注解字段
     * @param queryBuilder  非结构化数据 QueryBuilder; queryBuilder与filterBuilder必须二者存在其一
     * @param filterBuilder 过滤查询
     * @param <T>
     * @return
     */
    public <T> SearchHits<T> search(Class<T> clazz, QueryBuilder queryBuilder, QueryBuilder filterBuilder,
                                    Pageable pageable) {
        MyEsSearchRequest request = new MyEsSearchRequest(queryBuilder, filterBuilder, pageable);
        return search(clazz, request);
    }

    @Override
    public <T> SearchHits<T> search(Class<T> clazz, QueryBuilder queryBuilder, QueryBuilder filterBuilder,
                                    Pageable pageable, boolean nonTenantMode) {
        MyEsSearchRequest request = new MyEsSearchRequest(queryBuilder, filterBuilder, pageable);
        return search(clazz, request, nonTenantMode);
    }

    public <T> SearchHits<T> searchByFilter(Class<T> clazz, QueryBuilder filterBuilder, Pageable pageable) {
        MyEsSearchRequest request = new MyEsSearchRequest(null, filterBuilder, pageable);
        return search(clazz, request);
    }

    @Override
    public <T> SearchHits<T> searchByFilter(Class<T> clazz, QueryBuilder filterBuilder, @javax.annotation.Nullable Pageable pageable, boolean nonTenantMode) {
        MyEsSearchRequest request = new MyEsSearchRequest(null, filterBuilder, pageable);
        return search(clazz, request, nonTenantMode);
    }

    public <T> SearchHits<T> search(Class<T> clazz, QueryBuilder queryBuilder, Pageable pageable) {
        MyEsSearchRequest request = new MyEsSearchRequest(queryBuilder, null, pageable);
        return search(clazz, request);
    }

    @Override
    public <T> SearchHits<T> search(Class<T> clazz, QueryBuilder queryBuilder, @javax.annotation.Nullable Pageable pageable, boolean nonTenantMode) {
        MyEsSearchRequest request = new MyEsSearchRequest(queryBuilder, null, pageable);
        return search(clazz, request, nonTenantMode);
    }

    public <T> SearchHits<T> search(Class<T> clazz, MyEsSearchRequest request) {
        return search(clazz, request, false);
    }

    @Override
    public <T> SearchHits<T> search(Class<T> clazz, MyEsSearchRequest request, boolean nonTenantMode) {
        return search(getEsIndexName(clazz), clazz, request.getQueryBuilder(), request.getFilterBuilder(),
                request.getAggregationBuilder(), request.getPageable(), request.getQueryFields(), nonTenantMode);
    }

    public <T> SearchHits<T> search(Class<T> clazz, NativeSearchQueryBuilder queryBuilder) {
        return search(clazz, queryBuilder, false);
    }

    @Override
    public <T> SearchHits<T> search(Class<T> clazz, NativeSearchQueryBuilder queryBuilder, boolean nonTenantMode) {
        return elasticsearchRestTemplate.search(queryBuilder.build(), clazz, buildIndexCoordinates(clazz, nonTenantMode));
    }

    /**
     * 查询文档
     *
     * <p>
     * 查询的文档必须包含映射@Document的@Id字段
     * </p>
     *
     * @param indexName                  索引名称
     * @param clazz                      映射文档类 文档需标注@Document注解、包含@Id注解字段
     * @param queryBuilder               非结构化数据 QueryBuilder; queryBuilder与filterBuilder必须二者存在其一
     * @param filterBuilder              结构化数据 QueryBuilder; filterBuilder与queryBuilder必须二者存在其一
     * @param abstractAggregationBuilder 聚合查询Builder
     * @param pageable                   分页/排序; 分页从0开始
     * @param fields                     包含字段
     * @param nonTenantMode              是否是租户模式,false表示非租户模式,即通用索引
     * @return
     */
    private <T> SearchHits<T> search(String indexName, Class<T> clazz, @Nullable QueryBuilder queryBuilder,
                                     @Nullable QueryBuilder filterBuilder,
                                     @Nullable AbstractAggregationBuilder abstractAggregationBuilder,
                                     @Nullable Pageable pageable, @Nullable String[] fields, boolean nonTenantMode) {
        if (StringUtils.isNotBlank(indexName)) {
            // 查询的文档必须包含映射@Document的@Id字段(
            BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery().must(
                    QueryBuilders.existsQuery(EsReflectUtils.getDocumentIdFieldName(clazz)));
            if (queryBuilder != null) {
                boolQueryBuilder.must(queryBuilder);
            }
            NativeSearchQueryBuilder nativeSearchQueryBuilder = new NativeSearchQueryBuilder().withQuery(
                    boolQueryBuilder);
            if (filterBuilder != null) {
                nativeSearchQueryBuilder.withFilter(filterBuilder);
            }
            if (abstractAggregationBuilder != null) {
                nativeSearchQueryBuilder.withAggregations(abstractAggregationBuilder);
            }
            if (pageable != null) {
                nativeSearchQueryBuilder.withPageable(pageable);
            }
            if (fields != null && fields.length > 0) {
                nativeSearchQueryBuilder.withSourceFilter(new FetchSourceFilter(fields, null));
                //nativeSearchQueryBuilder.withFields(fields);
            }
//            nativeSearchQueryBuilder.withSorts(SortBuilders.fieldSort("id").order(SortOrder.ASC));
            return search(clazz, nativeSearchQueryBuilder, nonTenantMode);
        }
        return null;
    }

    @Override
    public String delIndexDoc(String id, Class<?> clazz) {
        return delIndexDoc(id, clazz, false);
    }

    @Override
    public String delIndexDoc(String id, Class<?> clazz, boolean nonTenantMode) {
        return elasticsearchRestTemplate.delete(id, buildIndexCoordinates(clazz, nonTenantMode));
    }

    @Override
    public <T> String delIndexDoc(T model) {
        return delIndexDoc(model, false);
    }

    @Override
    public <T> String delIndexDoc(T model, boolean nonTenantMode) {
        return delIndexDoc(EsReflectUtils.getDocumentIdValue(model), model.getClass(), nonTenantMode);
    }

    /**
     * 根据ID批量删除
     * 官方未提供根据id批量删除的,暂时就以循环删除的方式来操作,若有大批量操作存在性能问题考虑转为query delete方式
     *
     * @param clazz
     * @param ids
     * @return 返回每个ID删除后的返回结果
     */
    @Override
    public List<String> bulkDelIndexDoc(Class<?> clazz, List<String> ids) {
        return bulkDelIndexDoc(clazz, ids, false);
    }

    /**
     * 根据ID批量删除
     * 官方未提供根据id批量删除的,暂时就以循环删除的方式来操作,若有大批量操作存在性能问题考虑转为query delete方式
     *
     * @param clazz
     * @param ids
     * @param nonTenantMode 是否是租户模式,true表示非租户模式,即通用索引
     * @return 返回每个ID删除后的返回结果
     */
    @Override
    public List<String> bulkDelIndexDoc(Class<?> clazz, List<String> ids, boolean nonTenantMode) {
        if (clazz == null || CollectionUtils.isEmpty(ids)) {
            return null;
        }
        List delResutList = new ArrayList();
        for (String id : ids) {
            delResutList.add(elasticsearchRestTemplate.delete(id, buildIndexCoordinates(clazz, nonTenantMode)));
        }
        return delResutList;
    }

}

 增删改查demo

package com.my.es.test;

import java.util.Date;
import java.util.List;


import com.my.elasticsearch.MyEsService;
import com.my.es.test.model.Shop;
import com.my.es.test.model.Student;
import com.my.elasticsearch.model.MyEsSearchRequest;
import com.fasterxml.jackson.core.JsonProcessingException;
import net.minidev.json.JSONObject;
import org.assertj.core.util.Lists;
import org.elasticsearch.index.query.BoolQueryBuilder;
import org.elasticsearch.index.query.MatchQueryBuilder;
import org.elasticsearch.index.query.QueryBuilder;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.index.query.RangeQueryBuilder;
import org.junit.Assert;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;
import org.springframework.data.domain.Sort.Direction;
import org.springframework.data.elasticsearch.core.ElasticsearchRestTemplate;
import org.springframework.data.elasticsearch.core.IndexedObjectInformation;
import org.springframework.data.elasticsearch.core.SearchHits;
import org.springframework.data.elasticsearch.core.query.UpdateResponse;
import org.springframework.data.elasticsearch.core.query.UpdateResponse.Result;

/**
 * es demo
 *
 * @authro nantian
 * @date 2022-10-08 19:33
 */
@SpringBootTest
public class MyEsServiceTest {
    @Autowired
    private MyEsService myEsService;

    @Autowired
    private ElasticsearchRestTemplate elasticsearchRestTemplate;

    @Test
    public void delIndex() {
        boolean result = myEsService.deleteIndexIfExist(Student.class);
        Assert.assertTrue(result);
    }

    @Test
    public void delIndexDoc() {
        String result = myEsService.delIndexDoc("3007", Student.class);
        System.out.println("delIndexDoc:" + Student.class.getName());
    }


    @Test
    public void updateMapping() {
        boolean result = myEsService.updateIndexMapping(Student.class);
        Assert.assertTrue(result);
    }


    @Test
    public void updateIndexMapping() {
        boolean result = myEsService.updateIndexMapping(Shop.class);
        Assert.assertTrue(result);
    }

    @Test
    public void createIndex() {
        boolean exist = myEsService.existIndex(Student.class);
        boolean result = false;
        if (!exist) {
            result = myEsService.createIndexIfNotExist(Student.class);
        } else {
            System.out.println("index exist:" + Student.class.getName());
        }
        Assert.assertTrue(result);
    }



    @Test
    public void createIndex3() {
        boolean result = myEsService.createIndexIfNotExist(Shop.class);
        System.out.println("index exist:" + Shop.class.getName());
        Assert.assertTrue(result);
    }

    @Test
    public void addIndexDoc() {
        Student student = new Student(1000, "张三", "测试索引添加", "哈哈", "三年二班刘", 10, new Date(), null);
        String documentId = myEsService.addIndexDoc(student);
        System.out.println("addIndexDoc result:" + documentId);
        Assert.assertNotNull(documentId);
    }

    @Test
    public void saveIndexDocWithVersion() {
        Student student = new Student(1009, "张三1001", "测试索引添加1", "哈哈", "三年二班刘11", 10, new Date(), null);
        Student existOne = myEsService.findById(student.getId() + "", Student.class);
        Long _version = existOne != null ? existOne.getVersion() + 1 : null;
        String documentId = myEsService.saveIndexDoc(student, _version);
        System.out.println("addIndexDoc result:" + documentId);
        Assert.assertNotNull(documentId);
    }

    @Test
    public void existIndex() {
        boolean result1 = myEsService.existIndex(Student.class);
        boolean result2 = myEsService.existIndex(Student.class, true);
        System.out.println(result1 + "------" + result2);
    }

    @Test
    public void saveIndexDocWithNonTenantModel() {
        Student student = new Student(1001, "张三1001", "测试索引添加1", "哈哈", "三年二班刘11", 10, new Date(), null);
        boolean nonTenantModel = true;
        if (nonTenantModel) {
            if (!myEsService.existIndex(Student.class, nonTenantModel)) {
                myEsService.createIndexIfNotExist(Student.class, nonTenantModel);
            }

        }
        Student existOne = myEsService.findById(student.getId() + "", Student.class, nonTenantModel);
        Long _version = existOne != null ? existOne.getVersion() + 1 : null;
        String documentId = myEsService.saveIndexDoc(student, _version, nonTenantModel);
        System.out.println("addIndexDoc result:" + documentId);
        Assert.assertNotNull(documentId);
    }


    @Test
    public void bulkAddIndexDoc2() {
        Student student1 = new Student(1000, "zs0", "测试索引添加0", "哈哈33ss", "三年二班刘先生中国", 10, new Date(), null);
        Student student2 = new Student(1001, "zs", "测试索引添加1", "哈哈dd", "五年二班周先生美国", 20, new Date(), null);
        Student student3 = new Student(1002, "zs", "测试索引添加2", "哈哈aa", "10年二班刘女士中国", 0, new Date(), null);
        Student student4 = new Student(1003, "zs1003", "测试索引添加3", "哈哈aadd", "八年二班张女士北京", 50, new Date(), null);
        Student student5 = new Student(1004, "zs1004", "测试索引添加4", "哈哈bbaa", "三年二班刘重生北京", 60, new Date(), null);
        Student student6 = new Student(1006, "zs1006", "测试索引添加4", "哈哈bbaa", "三年二班刘重生北京", 60, new Date(), null);
        Student student7 = new Student(1007, "zs1007", "测试索引添加4", "哈哈bbaa", "三年二班刘重生北京", 60, new Date(), null);
        List list = Lists.newArrayList(student1, student2, student3, student4, student5, student6, student7);
        List<IndexedObjectInformation> result = myEsService.bulkAddIndexDoc(Student.class, list);
        System.out.println("bulkAddIndexDoc result:" + JSONObject.toJSONString(result));
        Assert.assertNotNull(result.size() > 0);
    }

    @Test
    public void bulkSaveIndexDoc() {
        Student student1 = new Student(1020, "zs0", "测试索引添加0", "哈哈33ss", "三年二班刘先生中国", 11, new Date(), null);
        Student student2 = new Student(1021, "zs", "测试索引添加1", "哈哈dd", "五年二班周先生美国", 12, new Date(), null);
        Student student3 = new Student(1022, "zs", "测试索引添加2", "哈哈aa", "10年二班刘女士中国", 13, new Date(), null);
        List<Student> list = Lists.newArrayList(student1, student2, student3);

        for (Student student : list) {
            Student existOne = myEsService.findById(student.getId() + "", Student.class);
            Long _version = existOne != null ? existOne.getVersion() + 1 : 1;
            student.setVersion(_version);
        }

        List<IndexedObjectInformation> result = myEsService.bulkSaveIndexDoc(Student.class, list);
        System.out.println("bulkAddIndexDoc result:" + JSONObject.toJSONString(result));
        Assert.assertNotNull(result.size() > 0);
    }

    @Test
    public void getByIdStudent() {
        Student student = myEsService.findById("1000", Student.class);
        System.out.println(JSONObject.toJSONString(student));
    }

    @Test
    public void updateDoc() throws JsonProcessingException {
        Student student = new Student();
        student.setId(1000);
        student.setAge(30);
        student.setText("lisi");
        UpdateResponse.Result result = myEsService.updateDoc(student);
        System.out.println("update result:" + JSONObject.toJSONString(result));
        Student student2 = myEsService.findById("1000", Student.class);
        System.out.println(JSONObject.toJSONString(student2));
        Assert.assertTrue(Result.UPDATED == result);
    }

    @Test
    public void searchAll() {
        SearchHits<Student> hits = myEsService.search(Student.class, QueryBuilders.matchAllQuery(), null);
        System.out.println(JSONObject.toJSONString(hits));
    }

    @Test
    public void searchBySingleField() {
        QueryBuilder queryBuilder = QueryBuilders.matchQuery("name", "zs0");
        SearchHits<Student> hits = myEsService.search(Student.class, queryBuilder, null);
        System.out.println(JSONObject.toJSONString(hits));
    }

    @Test
    public void searchByFilter() {
        MyEsSearchRequest request = new MyEsSearchRequest();
        request.setQueryFields(new String[]{"name", "id", "_version"});

        //1
        QueryBuilder queryBuilder = QueryBuilders.multiMatchQuery("zs", "name", "text");
        request.setQueryBuilder(queryBuilder);

        //2
        MatchQueryBuilder queryBuilder1 = QueryBuilders.matchQuery("name", "zs");
        RangeQueryBuilder queryBuilder2 = QueryBuilders.rangeQuery("age").gte("10").lte("60");
        MatchQueryBuilder fuzzyQueryBuilder = QueryBuilders.matchQuery("desc", "哈哈");

        BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();
        boolQueryBuilder.should(queryBuilder1);
        boolQueryBuilder.should(queryBuilder2);
        boolQueryBuilder.should(fuzzyQueryBuilder);

        BoolQueryBuilder filterQueryBuilder = QueryBuilders.boolQuery();
        filterQueryBuilder.should(QueryBuilders.matchQuery("id", "1000"));
        request.setFilterBuilder(filterQueryBuilder);

        //3 分页及排序
        request.setQueryBuilder(boolQueryBuilder);
        Sort sort = Sort.by(Direction.DESC, "age");
        PageRequest pageRequest = PageRequest.of(0, 10, sort);
        request.setPageable(pageRequest);

        SearchHits<Student> hits = myEsService.search(Student.class, request);
        System.out.println(JSONObject.toJSONString(hits));
    }
}

八、ES管理工具

推荐使用官方的kibana,功能更全面    

安装集群管理工具- Kibana

Kibanan安装比较简单,es集群搭建成功后,修改config/kibana.yml 加入以下配置,启动即可

#设置为中文
i18n.locale: "zh-CN"
#允许其它IP可以访问
server.host: "0.0.0.0"
elasticsearch.username: "kibana_system"
elasticsearch.password: "elastic123"
#es集群地址,填写真实的节点地址
elasticsearch.hosts: ["http://xxx.xx.xx.xx:9200","http://xxx.xx.xx.xx:9200","http://xxx.xx.xx.xx:9200"]
 启动命令
 
./bin/kibana

es-header插件安装使用

    es-header 插件是一个方便查看es集群、索引管理的一个工具,非官方提供,使用方式一般是安装chrome插件或下载安装header包进行本地安装(需要安装node),具体安装方式参考上面的文档, 以下是es-header图示:

  • 说明
    # 是否支持跨域,es-header插件使用
    http.cors.enabled: true
    # *表示支持所有域名跨域访问
    http.cors.allow-origin: "*"
    http.cors.allow-headers: Authorization,X-Requested-With,Content-Type,Content-Length
     http://localhost:9200/?auth_user=elastic&auth_password=elastic123
    • 开启跨域访问,使用es-header需要在es的配置文件中添加以下配置开启跨域访问
    • 添加口令访问,如果es集群开启了口令访问,es-header使用时需要在url后添加口令,示例如下:

九、ES集群安全策略

  ES 7以下默认几乎没有任何安全策略,如果IP、端口被暴露,在可访问的情况下任何用户都可以对索引进行管理,以及数据的查询、删除等,基于此需要考虑设置一定的安全策略, 目前常用几种方式如下 :
类别
优点
缺点
建议
nginx
对外屏蔽了ES集群的真实IP和端口,配置也较简单
只能做一些网络访问安全上面的防护,不能对索引及字段进行精确控制
选择X-pack
经验证在免费情况下X-Pack能满足基本诉求,对应用中使用影响也比较小
Search Guard
开源免费,基于RBAC权限模型设计,能够细粒度进行管控
配置复杂,需要安装证书及业务应用代码改造
X-Pack
官方提供,基于RBAC权限模型设计,能够细粒度进行管控,与client api及es集群兼容性较好
配置稍复杂,基础部分功能免费

X-pack安全策略开启及集群配置

  第一步:在ES的根目录生成CA证书
bin/elasticsearch-certutil ca
   步骤执行完会在elasticsearch根目录生成:elastic-stack-ca.p12
   注意
     1、提示输入输出文件,直接回车默认即可
     2、提示输入密码, 务必输入 ,第二步要用到,如果都是空密码后面启动es时会出password incorrect错误
  第二步: 使用第一步生成的证书,产生p12密钥
bin/elasticsearch-certutil cert --ca elastic-stack-ca.p12
  步骤执行完会在elasticsearch根目录生成: elastic-certificates.p12
   注意:这一步共有三个交互
  • Enter password for CA (elastic-stack-ca.p12)
   输入第一步中设置的的密码:
  • Please enter the desired output file [elastic-certificates.p12]:
   不用输入文件,直接回车默认即可
  • Enter password for elastic-certificates.p12 :
   不要输入密码,直接回车,否则还会出现es启动时报 password incorrect错误
  
  第三步: 配置证书
     1、将以上两个文件拷贝到config/certs目录,没有则新创建
     2、配置elasticsearch.yml
xpack.security.enabled: true xpack.license.self_generated.type: basic xpack.security.transport.ssl.enabled: true xpack.security.transport.ssl.verification_mode: certificate xpack.security.transport.ssl.keystore.path: certs/elastic-certificates.p12 xpack.security.transport.ssl.truststore.path: certs/elastic-certificates.p12
     注意
  • 第一步第二步整个集群只须执行一次,第一个节点配置完后把证书copy到其它节点共用即可
  • elasticsearch.yml 同样集群中每个es节点都需要配置
  第四步:重启ES
    重启ES看是否能正常启动,是否有ssl相关的错误信息,若无则一般说明配置没有问题
    重启成功再访问ES时则会提示输入用户名密码,此时说明es安全已经启用,此时由于没有设置密码是无法登录的,接下来按第5步进行操作
     注意
     先配置单个节点,重启ES看是否正常,如果正常其它节点再进行同样的操作
  

十、ES运维

 数据备份与恢复

   es官方提供了数据备份和恢复的方式,基本思路是用官方提供的API创建数据备份文件,然后再用恢复数据的API进行恢复,es默认情况下都是对整个集群的所有索引进行备份,也可进行定制只备份部分索引,恢复时也需要根据情况来恢复指定的索引数据,es均提供有相应的定制能力。
   官方文档
   https://www.elastic.co/guide/en/elasticsearch/reference/7.6/snapshots-register-repository.html
第一步:配置备份与恢复目录
   es规范要求必须先在 elasticsearch.yml中配置备份恢复目录,后续才可使用该目录进行备份和恢复,当然每个es节点都要进行配置。
     配置如下:
path.repo: ["/data/backups", "/data2/backups"]
  目录可以写一到多个,一般写一个即可,配置加上后需要重启es集群
  第二步:创建一个备份/快照
   创建备份的三种方式
  • 1、kibana上的dev_tools工具
    • 该工具就是手动在kibana,填写备份与恢复API、参数等进行数据的备份与恢复,能够灵活的定制一些备份和恢复策略,也可以借助此工具熟悉es数据备份和恢复的各种操作命令,但不能自动进行备份
  • 2、kibana提供的备份与恢复管理工具 [ 建议使用 ]
  •      提供了快照查看、备份目录管理、创建备份快照的策略(如:多长时间创建一次快照)、快照恢复等功能,基本能够满足日常的使用,但存在问题是kibana这个产品上很多数据也是存在ES集群中,如果ES集群出现问题比如数据丢失Kibana也可能无法使用,此时我们就需要使用curl等方式发送http请求来恢复数据
    • 工具路径,ip与port自行替换 http://127.0.0.1:5601/app/management/data/snapshot_restore/snapshots
  
  • 3、使用curl发送 http请求 方式 【 终极方式
         当kibanan不可用时可以采用此方式,算是个保留的救命手段,此方式在备份与恢复使用的API及请求参数与第1种方式一致,只是使用了不同的工具来发送请求而已。
        这种方式在发送请求时需要填写集群地址及集群的访问口令,例如查看一个快照状态:
    curl -u elastic:elastic123 -X POST 'http://ip:9200/_snapshot/jingkai_backup/snapshot/_status'
  快照创建操作命令
 以下api适用于第1、3两种方式
   (1) 创建一个备份仓库
  创建备份前需要先创建一个仓库,所有的备份都需要放在仓库中,一个es集群可以有多个备份仓库
PUT /_snapshot/backup1 { "type": "fs", "settings": { "location": "/data/backups", "compress": true, "max_snapshot_bytes_per_sec": "50mb", "max_restore_bytes_per_sec": "50mb" } }
  • 其它命令
    (1)查看所有仓库: GET /_snapshot
  • 属性说明
    • _snapshot 命令是指创建一个备份快照,后面的路径【backup1】可以认为是仓库的名字
    • type=fs: 代表是文件系统来存储备份,其它方式暂没有找到
    • location: 当前备份存放的目录
    • compress: 是否开启压缩
    • max_snapshot_bytes_per_sec:备份时最大写入数据速度,可以根据实际硬件配置进行填写
    • max_restore_bytes_per_sec:恢复时最大读取速度
(2)创建一个备份/快照
json属性参数也可不填
PUT /_snapshot/backup1/snapshot_20221013?wait_for_completion=true { "indices": "index_1,index_2", "ignore_unavailable": true, "include_global_state": false }
  • wait_for_completion=true 设置为true时,操作界面会等待快照备份的结果,当索引比较大备份时间很长也有可能看不到结果,此时可以设置为false然后用以下命令查看备份进度
    GET /_snapshot/backup1/snapshot_20221013/_status
    "indices": "index_1,index_2" : 可以使用该参数只对部分索引进行备份
  • ignore_unavailable=true: 忽略不可用的索引
  • include_global_state =false 可以防止将群集全局状态存储为快照的一部分。默认情况下,如果参与快照的一个或多个索引没有所有可用的主分片,则整个快照将失败。可以通过设置为partial来更改此行为true。
(3)查看某个快照
  • GET /_snapshot/backup1/snapshot_20221013
  • GET /_snapshot/backup1/snapshot_20221013/_status
(4)终止创建/删除快照
   当备份时执行则可以终止备份,当备份结束后执行可以删除快照
  • DELETE /_snapshot/backup1/snapshot_20221013
第三步:数据恢复
es集群上有比较多的索引数据,除了业务索引还有es和Kibana等一些配套工具生成的系统内置索引,备份时如果没有指定只对部分索引进行备份,那默认是全部备份,当整个集群出现问题也可以使用此进行恢复。但在实际场景中可能只会对某部分出现问题的索引进行恢复,或者是想重命名及重建索引,这时候就需要在恢复时设置相应的参数。
注意:集群中正在提供服务的索引【状态为open】,是无法直接进行恢复的,强行恢复会提示『because an open index with same name already exists in the cluster』,此时需要先把索引关闭,但关闭索引就会影响线上业务功能,所以恢复索引时需要慎重操作。如果集群已经不可用了,可以直接使用。如果不能停机恢复可能需要借助索引别名及索引重建方式来恢复索引。
   使用Kibana工具恢复
    具体步骤就不进行赘述了,按界面上提供的功能进行操作即可
  使用dev_tools、curl方式进行恢复
    (1) 执行恢复
POST /_snapshot/backup1/snapshot_20221013/_restore { "indices": "index_1,index_2", "ignore_unavailable": true, "include_global_state": true, "rename_pattern": "index_(.+)", "rename_replacement": "restored_index_$1", "include_aliases": false }
  • _restore : 为恢复快照命令
  • indices: 要恢复的索引,支持使用索引别名, 支持通配符表达式, 多个索引名称用逗号拼接,如果不指定 indices , 则表示恢复所有索引.
  • ignore_unavailable 为true则恢复时忽略不可用的索引
  • include_global_state:设置为false,则不把快照中的状态恢复到当前集群中
  • rename_pattern: 可省参数,重命名索引的匹配规则,使用此参数可以让A索引恢复到B索引上,有点类似重建或重命名的意思,无此参数则直接还原到当前索引【当前索引必须是close状态】,此参数与rename_replacement须一起使用,一般使用正则匹配
  • rename_replacement:可省参数,重命名恢复的索引,须与rename_pattern结合使用,【$1】为rename_pattern中匹配到的数据,restored_index_则为恢复的索引添加了一个新的前缀名
  • include_aliases: false 防止别名与关联索引一起还原
  (2) 终止恢复-慎用
     注意:es并未提供单独的停止恢复命令,而是通过删除快照操作来达到终止恢复,所以执行此命令有可能导致快照数据也会被删除
DELETE /_snapshot/backup1/snapshot_20221013

索引重建

索引重建不是对当前索引进行重建,而是把一个索引重建到一个新的索引上,新的索引mapping,setting可以单独设置,在某些场景比如一开始索引设置的是5个分片,当集群节点增加后我们可以创建一个新的索引把分片设置更大一点,然后使用以下命令把数据重建到新索引上,然后应用访问新索引
POST _reindex { "source": { "index": "app1_pay_order_index" }, "dest": { "index": "app1_pay_order_index_new" } }
:索引重建结合索引别名功能,可以实现不停机索引切换

索引设置别名

索引设置别名后,我们可以使用别名对索引进行操作,与原名一样,别名在数据恢复或者重建场景非常有用,程序中建议使用别名进行访问索引

添加&删除别名

remove 删除索引别名add 给索引添加别名
POST /_aliases { "actions": [ #删除别名 { "remove": { "index": "app1_pay_order", "alias": "app1_pay_order2" } }, #添加别名 { "add": { "index": "app1_pay_order_new", "alias": "app1_pay_order2" }} ] }
查询所有索引别名
GET _alias

查询指定索引别名

GET app1_pay_order/_alias

十一、ES常用操作命令

  • es启动
    • 命令:./bin/elasticsearch
    • 访问地址:http://localhost:9200/ 默认9200端口
  • Kibana 启动
  • 查看集群状态
      检查集群运行情况: GET -> localhost:9200/_cat/health?v
      查看集群节点列表: GET -> localhost:9200/_cat/nodes
      查看所有索引:       GET -> localhost:9200/_cat/indices?v
  • 索引操作API
    • es集群所的操作都可以http方式进行操作,当然也可以使用kibana上的开发工具操作,kibana操作更简单些。
    • //1.查询查看分片状态-Authorization方式(postman通过账密获取token)
      curl -XGET ‘http://127.0.0.1:9200/_cluster/allocation/explain?pretty’ --header ‘Authorization’: Basic ZWxhc3RpYzphcDIwcE9QUzIw’
      
      //2.查询查看分片状态-账密方式
      curl -XGET -u elastic "http://127.0.0.1:9200/_cluster/allocation/explain?pretty" -H ‘Content-Type:application/json’
      
      //3.查询集群状态命令
      curl -XGET -u elastic:elastic123 "http://127.0.0.1:9200/_cluster/health?pretty"
      
      //4.查询Es全局状态
      curl -XGET -u elastic:elastic123 "http://127.0.0.1:9200/_cluster/stats?pretty"
      
      //5.查询集群设置
      curl -XGET -u elastic:elastic123 "http://127.0.0.1:9200/_cluster/settings?pretty"
      
      //6.查询集群文档总数
      curl -XGET -u elastic:elastic123 "http://127.0.0.1:9200/_cat/count?v"
      
      //7.查看当前集群索引分片信息
      curl -XGET -u elastic:elastic123 "http://127.0.0.1:9200/_cat/shards?v"
      
      //8.查看集群实例存储详细信息
      curl -XGET -u elastic "http://127.0.0.1:9200/_cat/allocation?v"
      
      //9.查看当前集群的所有实例
      curl -XGET -u elastic "http://127.0.0.1:9200/_cat/nodes?v"
      
      //10.查看当前集群等待任务
      curl -XGET -u elastic "http://127.0.0.1:9200/_cat/pending_tasks?v"
      
      //11.查看集群查询线程池任务
      curl -XGET -u elastic "http://127.0.0.1:9200/_cat/thread_pool/search?v"
      
      //12.查看集群写入线程池任务
      curl -XGET -u elastic "http://127.0.0.1:9200/_cat/thread_pool/bulk?v"
      
      //13.清理ES所有缓存
      curl -XPOST "http://127.0.0.1:9200/_cache/clear"
      
      //14.查询索引信息
      curl -XGET -u : ‘https://127.0.0.1:9200/licence_info_test?pretty’
      
      //15.关闭索引
      curl -XGET -u : ‘https://127.0.0.1:9200/my_index/_close?pretty’
      
      //16.打开索引
      curl -XGET -u : ‘https://127.0.0.1:9200/my_index/_open?pretty’
       

      kibana工具

十二、ES 相关资料

  • gitHub地址
https://github.com/elastic/elasticsearch/

 官方文档 

https://www.elastic.co/cn/downloads/past-releases/#kibana
  • wiki
    • Elasticsearch 集群协调迎来新时代
      • https://www.elastic.co/cn/blog/a-new-era-for-cluster-coordination-in-elasticsearch
    • Elasticsearch 集群内应该设置多少个分片?
      • https://www.elastic.co/cn/blog/how-many-shards-should-i-have-in-my-elasticsearch-cluster
    • Es spring-boot-starter wiki
      • https://docs.spring.io/spring-data/elasticsearch/docs/4.3.7/reference/html/#reference
  • 17
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论
Elasticsearch7.9.0是一个开源的分布式搜索和分析引擎,用于构建高效、可扩展的实时搜索解决方案。要搭建Elasticsearch7.9.0集群,需要按照以下步骤进行操作: 1. 下载和安装Elasticsearch7.9.0:从官方网站上下载Elasticsearch7.9.0压缩包,并解压到合适的目录下。 2. 配置Elasticsearch集群参数:在每个节点上的elasticsearch.yml配置文件中,设置集群名称、节点名称、绑定IP地址等参数。确保每个节点的配置文件相同,以便节点可以识别彼此。 3. 修改JVM配置:根据服务器的硬件配置和需求,修改jvm.options文件中的内存分配参数,以确保Elasticsearch能够充分利用可用的系统资源。 4. 启动Elasticsearch节点:在每个节点的终端或命令行中,切换到Elasticsearch的安装目录,并运行./bin/elasticsearch命令来启动节点。确保每个节点都能正常启动。 5. 集群发现和节点自动加入:在elasticsearch.yml配置文件中,配置集群发现机制,如使用单播或多播,以及设置初始主节点。这将使得新的节点能够自动连接到现有的Elasticsearch集群。 6. 验证集群状态:使用curl或其他HTTP客户端发送请求到任意一个节点的IP地址和端口号,查看集群的状态信息。确保所有的节点都连接到集群,并且状态正常。 7. 索引和搜索数据:使用Elasticsearch的REST API或Java客户端,可以索引和搜索数据。通过创建索引、定义映射、增删改查操作可以实现灵活和高效的搜索和分析功能。 8. 监控集群健康和性能:使用Elasticsearch提供的监控工具或第三方插件,可以实时监控集群的健康状态、性能指标和查询性能。这有助于及时发现和解决潜在的问题。 总结:以上是搭建Elasticsearch7.9.0集群的基本步骤。搭建集群后,可以实现数据的高可用性、可扩展性和分布式计算,为企业提供全文搜索、日志分析等功能。通过合理的集群配置、优化和监控,可以提高集群的性能和稳定性。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

南天一梦N

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值