SpringBoot 整合 Elasticsearch

发布于:2025-08-13 ⋅ 阅读:(17) ⋅ 点赞:(0)

Elasticsearch 几种常用的客户端介绍

1. Rest High Level Client (RestHLClient)

  • 简介
    这是 Elasticsearch 官方早期推出的高级 REST 客户端,基于底层低级 REST 客户端封装,支持同步和异步操作,封装了常用的 ES API。

  • 优点
    • 官方支持,文档丰富,生态完善
    • 封装了大部分 ES REST API,调用方便
    • 支持异步请求
    • Spring Data Elasticsearch 2.x - 4.x 版本都基于它
  • 缺点
    • 只能用到 ES 7.x,8.x 以后官方宣布弃用
    • API 不够现代化,使用 Builder 模式不够优雅
    • 不支持 Elasticsearch 8.x 的新特性(比如新的安全模型)
  • 适用场景
    • 仍在用 Elasticsearch 7.x 或之前版本
    • 需要快速集成且依赖成熟的生态

2. Spring Data Elasticsearch

  • 简介
    Spring 社区基于 Rest High Level Client 封装的 Spring Data 项目,提供 Repository 风格的数据访问,支持注解实体映射,方便 Spring Boot 集成。5.x版本及以后基于Elasticsearch-java,5.x版本以前基于RestHLClient。

  • 优点
    • 极大简化 Elasticsearch 的数据访问
    • 支持声明式 Repository,熟悉 Spring Data JPA 的开发者易上手
    • 自动处理 POJO 与 ES 映射的转换
    • 集成 Spring 生态,方便事务管理、依赖注入等
  • 缺点
    • 功能不及原生客户端灵活,复杂查询可能不方便
    • 更新节奏较慢,追赶 ES 新版本滞后
  • 适用场景
    • Spring Boot 项目
    • 业务逻辑不复杂,想快速做 CRUD 和简单查询

3. Elasticsearch Java API Client (elasticsearch-java)

  • 简介
    这是 Elasticsearch 官方从 8.0 版本开始推出的官方客户端,完全重写,使用更现代的 Builder 模式和 JSON-P 映射(默认 Jackson),设计更加现代和类型安全。

  • 优点
    • 官方最新客户端,持续更新和支持
    • API 设计现代,支持异步和响应式编程(配合 Reactor)
    • 支持 Elasticsearch 8.x 所有新特性,包括安全认证等
    • 基于 JSON-P,默认集成 Jackson,序列化效率更高
  • 缺点
    • 只支持 ES 8.x 及以上版本
    • 对新手来说,API 需要一定学习成本
    • 生态和社区相较 RestHLClient 还在逐步完善
  • 适用场景
    • 使用 ES 8.x 版本或准备升级
    • 希望用官方推荐的最新客户端

本文集成官方最新推出的新版客户端 elasticsearch-java

pom.xml 添加依赖

<!-- 官方 Elasticsearch Java API Client -->
<dependency>
    <groupId>co.elastic.clients</groupId>
    <artifactId>elasticsearch-java</artifactId>
    <version>9.1.1</version>
</dependency>

<!-- 需要包含 Elasticsearch Transport 依赖(HTTP 传输依赖) -->
<dependency>
    <groupId>org.elasticsearch.client</groupId>
    <artifactId>elasticsearch-rest-client</artifactId>
    <version>9.1.1</version>
</dependency>

<!-- json 时间类型处理 -->
<dependency>
    <groupId>com.fasterxml.jackson.datatype</groupId>
    <artifactId>jackson-datatype-jsr310</artifactId>
</dependency>

配置 elasticsearch Bean

elasticsearch9 已经默认开启了 SSL/TLS 证书,下面这个类是跳过证书验证的配置(测试环境使用)

package com.xxl.elasticsearch.config;

import co.elastic.clients.elasticsearch.ElasticsearchClient;
import co.elastic.clients.json.jackson.JacksonJsonpMapper;
import co.elastic.clients.transport.rest_client.RestClientTransport;
import org.apache.http.HttpHost;
import org.apache.http.auth.AuthScope;
import org.apache.http.auth.UsernamePasswordCredentials;
import org.apache.http.client.CredentialsProvider;
import org.apache.http.impl.client.BasicCredentialsProvider;
import org.elasticsearch.client.RestClient;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
import java.security.SecureRandom;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;

/**
 * @author 7号
 * @version 1.0
 * @since 2025-08-11 20:37
 */
@Configuration
public class ElasticsearchConfig {
    
    private static final String HOSTNAME = "localhost";
    private static final String PORT = "9200";
    private static final String SCHEME = "https";

    /**
     * 创建了一个 RestClientBuilder,用来构造 RestClient 实例
     * destroyMethod = "close":表示在 Spring 容器关闭时,会调用该 Bean 的 close() 方法进行资源释放。
     */
    @Bean(destroyMethod = "close")
    public RestClient restClient() throws Exception {
        final CredentialsProvider credentialsProvider = new BasicCredentialsProvider();
        credentialsProvider.setCredentials(AuthScope.ANY, new UsernamePasswordCredentials("es账号", "es密码"));

        // 创建绕过证书验证的 SSLContext
        SSLContext sslContext = SSLContext.getInstance("TLS");

        // TrustManager,跳过证书校验
        TrustManager[] trustAllCerts = new TrustManager[]{
                new X509TrustManager() {
                    @Override
                    public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
                    }

                    @Override
                    public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
                    }

                    @Override
                    public X509Certificate[] getAcceptedIssuers() {
                        return new X509Certificate[0];
                    }
                }
        };
        sslContext.init(null, trustAllCerts, new SecureRandom());

        return RestClient.builder(new HttpHost(HOSTNAME, Integer.parseInt(PORT), SCHEME))
                .setHttpClientConfigCallback(httpClientBuilder ->   //
                        httpClientBuilder   //
                                .setDefaultCredentialsProvider(credentialsProvider) //
                                // 设置自定义 SSLContext 并跳过 hostname 校验
                                .setSSLContext(sslContext)  //
                                .setSSLHostnameVerifier((hostname, session) -> true)
                )   //
                .setRequestConfigCallback(requestConfigBuilder ->
                        requestConfigBuilder    //
                                .setConnectTimeout(5000)    //
                                .setSocketTimeout(60000)    //
                                .setConnectionRequestTimeout(0)
                )
                .build();
    }


    @Bean
    public ElasticsearchClient elasticsearchClient(RestClient restClient) {
        // 创建 ObjectMapper 并注册 JavaTimeModule,支持 LocalDate/LocalDateTime 序列化
        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.registerModule(new JavaTimeModule());
        // 禁用序列化为时间戳(数组形式)
        objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
        // 用自定义 ObjectMapper 创建 JacksonJsonpMapper
        JacksonJsonpMapper jsonpMapper = new JacksonJsonpMapper(objectMapper);

        // RestClientTransport 是 elasticsearch-java 客户端用来封装 HTTP 传输的类,底层用的就是前面那个 RestClient(Apache HTTP 客户端)。
        RestClientTransport transport = new RestClientTransport(restClient, jsonpMapper);
        // 构造 ElasticsearchClient 对象。
        // ElasticsearchClient 是官方提供的高级客户端,负责调用 Elasticsearch API,处理请求和响应。
        // 这个客户端依赖 transport 来执行实际的 HTTP 通信。
        return new ElasticsearchClient(transport);
    }
}

如果不要跳过证书,就使用这个 bean 配置(生产环境使用,还未验证)

@Configuration
public class ElasticsearchConfig {

    /**
     * 创建了一个 RestClientBuilder,用来构造 RestClient 实例
     * destroyMethod = "close":表示在 Spring 容器关闭时,会调用该 Bean 的 close() 方法进行资源释放。
     */
    @Bean(destroyMethod = "close")
    public RestClient restClient() {
        final CredentialsProvider credentialsProvider = new BasicCredentialsProvider();
        credentialsProvider.setCredentials(AuthScope.ANY, new UsernamePasswordCredentials("es账号", "es密码"));

        return RestClient.builder(new HttpHost("localhost", 9200, "http"))  //
                .setHttpClientConfigCallback(httpClientBuilder ->       //
                        httpClientBuilder.setDefaultCredentialsProvider(credentialsProvider))   //
                .setRequestConfigCallback(requestConfigBuilder ->   //
                        requestConfigBuilder    //
                                .setConnectTimeout(5000)    //
                                .setSocketTimeout(60000)    //
                                .setConnectionRequestTimeout(0))    //
                .build();
    }

    @Bean
    public ElasticsearchClient elasticsearchClient(RestClient restClient) {
        // RestClientTransport 是 elasticsearch-java 客户端用来封装 HTTP 传输的类,底层用的就是前面那个 RestClient(Apache HTTP 客户端)。
        RestClientTransport transport = new RestClientTransport(restClient, new JacksonJsonpMapper());
        // 构造 ElasticsearchClient 对象。
        // ElasticsearchClient 是官方提供的高级客户端,负责调用 Elasticsearch API,处理请求和响应。
        // 这个客户端依赖 transport 来执行实际的 HTTP 通信。
        return new ElasticsearchClient(transport);
    }
}

SpringBoot 使用 Elasticsearch API

实体类        // 注意 Mysql 的实体是Mysql 的,es 的实体是es的,不要用同一个实体。使用之前先在 Elasticsearch 中创建好索引

import lombok.Data;

@Data
public class ProductDocument {

    private String id;

    private String name;

    private String description;

}

Service 层

@Service
public class ProductDocumentService {

    @Autowired
    private ElasticsearchClient client;

    // 添加一条数据
    public String indexProduct(ProductDocument product) throws IOException {
        IndexResponse response = client.index(i -> i
                .index("products")      // 索引名
                .id(product.getId())
                .document(product)
        );
        return response.id();
    }
}

Controller 层

@RestController
@RequestMapping("/products")
public class ProductDocumentController {

    @Autowired
    private ProductDocumentService service;

    @PostMapping
    public String addProduct(@RequestBody ProductDocument product) throws IOException {
        return service.indexProduct(product);
    }
}

结果,调用成功


添加数据

添加数据,索引不存在时,自动创建索引。推荐提前在 es 服务中建好索引,因为自动映射类型不够精准。

    @Autowired
    private ElasticsearchClient client;

    // 添加一条数据
    public String indexProduct(ProductDocument product) throws IOException {
        IndexResponse response = client.index(i -> i
                .index("products")      // 索引名
                .id(product.getId())
                .document(product)
        );
        return response.id();
    }

查询数据

通过 id 查询

    @Autowired
    private ElasticsearchClient client;

    /**
     * 根据 id 查询数据
     */
    public ProductDocument getProductById(String id) throws IOException {
        GetResponse<ProductDocument> response = client.get(g -> g
                        .index("products")
                        .id(id),
                ProductDocument.class
        );

        if (response.found()) {
            return response.source();
        } else {
            return null;  // 或抛异常,文档不存在
        }
    }

多条件,组合查询

    @Autowired
    private ElasticsearchClient client;

    public List<ProductDocument> searchByNameAndPriceRange(String nameValue, Double minPrice, Double maxPrice) throws IOException {
        SearchResponse<ProductDocument> response = client.search(s -> s
                        .index("products")
                        .query(q -> q
                                .bool(b -> b
                                        .must(m -> m
                                                .match(t -> t
                                                        .field("name")  // 查询的字段
                                                        .query(nameValue)   // 查询的值
                                                )
                                        ).filter(f -> f
                                                .range(r -> r
                                                        .number(nr -> nr
                                                                .field("price")    // 查询的字段
                                                                .gte(minPrice)    // 大于等于
                                                                .lte(maxPrice)    // 小于等于
                                                        )
                                                )
                                        )
                                )
                        ),
                ProductDocument.class
        );

        return response.hits().hits().stream()
                .map(hit -> hit.source())
                .collect(Collectors.toList());
    }

更新数据

更新部分字段,对象的方式,没传的字段不会被删除,也不会丢失,依然存在原文档里

    @Autowired
    private ElasticsearchClient client;

    public String updateDocumentFields(ProductDocument product) throws IOException {
        UpdateResponse<ProductDocument> response = client.update(u -> u
                        .index("products")
                        .id(product.getId())
                        .doc(product),
                ProductDocument.class);
        return response.id();
    }

删除数据

根据 id 删除数据

    @Autowired
    private ElasticsearchClient client;

    public String deleteDocumentById(String id) throws IOException {
        DeleteResponse response = client.delete(d -> d
                .index("products")
                .id(id)
        );
        return response.id();
    }


网站公告

今日签到

点亮在社区的每一天
去签到