Elasticsearch Java 开发完全指南

目录

  1. [Java 客户端介绍](#Java 客户端介绍)
  2. 环境搭建与连接
  3. 索引操作
  4. [文档操作 CRUD](#文档操作 CRUD)
  5. 查询操作
  6. 聚合操作
  7. 批量操作
  8. [Spring Boot 集成](#Spring Boot 集成)
  9. 高级特性
  10. 最佳实践

一、Java 客户端介绍

1.1 客户端演进历史

复制代码
Elasticsearch Java 客户端发展:

┌─────────────────────────────────────────────────────┐
│ TransportClient (已废弃)                             │
│ ├─ ES 7.x 标记为废弃                                 │
│ ├─ ES 8.x 完全移除                                  │
│ └─ 不推荐使用                                        │
├─────────────────────────────────────────────────────┤
│ RestClient (Low-level REST Client) 🔧               │
│ ├─ 底层 HTTP 客户端                                 │
│ ├─ 所有客户端的基础                                  │
│ ├─ 处理连接池、重试、节点发现                         │
│ └─ 直接使用较少,通常作为高级客户端的基础              │
├─────────────────────────────────────────────────────┤
│ RestHighLevelClient (维护模式)                       │
│ ├─ 基于 RestClient 构建                             │
│ ├─ ES 7.x 推荐使用                                  │
│ ├─ ES 7.15+ 标记为废弃                              │
│ ├─ 功能完整,社区使用广泛                            │
│ └─ 适合现有项目                                      │
├─────────────────────────────────────────────────────┤
│ Java API Client (推荐) ⭐                            │
│ ├─ 同样基于 RestClient 构建                          │
│ ├─ ES 7.15+ 新客户端                                │
│ ├─ ES 8.x 官方推荐                                  │
│ ├─ 类型安全、流式 API                               │
│ └─ 适合新项目                                        │
└─────────────────────────────────────────────────────┘

1.2 客户端层次架构

复制代码
┌──────────────────────────────────────────────────┐
│         应用层 - 你的业务代码                      │
└──────────────────────────────────────────────────┘
                    ▲
                    │ 调用
                    │
    ┌───────────────┴────────────────┐
    │                                │
┌───▼────────────────────┐  ┌───────▼──────────────┐
│ RestHighLevelClient    │  │ Java API Client      │
│ (高级客户端 - ES 7.x)   │  │ (新客户端 - ES 8.x)   │
│                        │  │                      │
│ ├─ IndexRequest       │  │ ├─ CreateRequest     │
│ ├─ SearchRequest      │  │ ├─ SearchRequest     │
│ └─ ...                │  │ └─ ...               │
└────────────────────────┘  └──────────────────────┘
           │                          │
           │ 基于                     │ 基于
           └──────────┬───────────────┘
                      ▼
        ┌──────────────────────────────┐
        │    RestClient (低级客户端)     │
        │ (Low-level REST Client)      │
        │                              │
        │ ├─ HTTP 连接管理              │
        │ ├─ 请求/响应处理              │
        │ ├─ 节点发现与负载均衡          │
        │ ├─ 自动重试                  │
        │ └─ 连接池管理                │
        └──────────────────────────────┘
                      │
                      ▼
        ┌──────────────────────────────┐
        │   Apache HttpClient          │
        │   (底层 HTTP 库)              │
        └──────────────────────────────┘
                      │
                      ▼
        ┌──────────────────────────────┐
        │   Elasticsearch Server       │
        │   (HTTP API: 9200)           │
        └──────────────────────────────┘

1.3 RestClient vs RestHighLevelClient 详解

1.3.1 核心区别对比
特性 RestClient (Low-level) RestHighLevelClient (High-level)
抽象级别 低级 HTTP 客户端 高级 API 客户端
API 风格 原始 HTTP 请求 Java 对象 API
使用方式 手动构建 JSON Builder 模式
请求构建 Request + endpoint IndexRequest, SearchRequest
响应处理 原始 JSON 字符串 解析为 Java 对象
类型安全 ❌ 无 ⚠️ 部分
学习曲线 较高(需要了解 ES API) 中等
灵活性 ✅ 极高(支持所有 API) ⚠️ 受限于已实现的 API
直接使用场景 特殊需求、调试 日常开发
推荐度 作为基础组件 ES 7.x 推荐
1.3.2 RestClient(低级客户端)详解

定义

java 复制代码
// RestClient 是最底层的 HTTP 客户端
import org.elasticsearch.client.RestClient;

特点

  • 直接发送 HTTP 请求到 Elasticsearch
  • 返回原始的 HTTP 响应
  • 需要手动构建请求 JSON
  • 需要手动解析响应 JSON

典型用法示例

java 复制代码
import org.apache.http.HttpHost;
import org.elasticsearch.client.Request;
import org.elasticsearch.client.Response;
import org.elasticsearch.client.RestClient;

public class RestClientExample {

    public static void main(String[] args) throws Exception {
        // 创建低级客户端
        RestClient restClient = RestClient.builder(
            new HttpHost("localhost", 9200, "http")
        ).build();

        // 示例1:创建索引
        Request createIndexRequest = new Request("PUT", "/products");
        createIndexRequest.setJsonEntity(
            "{\n" +
            "  \"mappings\": {\n" +
            "    \"properties\": {\n" +
            "      \"name\": { \"type\": \"text\" },\n" +
            "      \"price\": { \"type\": \"long\" }\n" +
            "    }\n" +
            "  }\n" +
            "}"
        );
        Response createIndexResponse = restClient.performRequest(createIndexRequest);
        System.out.println("创建索引: " + createIndexResponse.getStatusLine());

        // 示例2:索引文档(手动构建 JSON)
        Request indexRequest = new Request("POST", "/products/_doc/1");
        indexRequest.setJsonEntity(
            "{\n" +
            "  \"name\": \"iPhone 15\",\n" +
            "  \"price\": 7999\n" +
            "}"
        );
        Response indexResponse = restClient.performRequest(indexRequest);
        System.out.println("索引文档: " + indexResponse.getStatusLine());

        // 示例3:搜索文档
        Request searchRequest = new Request("GET", "/products/_search");
        searchRequest.setJsonEntity(
            "{\n" +
            "  \"query\": {\n" +
            "    \"match\": { \"name\": \"iPhone\" }\n" +
            "  }\n" +
            "}"
        );
        Response searchResponse = restClient.performRequest(searchRequest);

        // 手动解析 JSON 响应
        String responseBody = EntityUtils.toString(searchResponse.getEntity());
        System.out.println("搜索结果: " + responseBody);

        // 关闭客户端
        restClient.close();
    }
}

优势

  1. 完全控制:可以调用任何 ES API(包括实验性API)
  2. 轻量级:没有额外的抽象层
  3. 调试友好:直接看到 HTTP 请求/响应
  4. 向后兼容:ES 版本升级影响小

劣势

  1. 手动构建 JSON:容易出错,字符串拼接繁琐
  2. 手动解析响应:需要自己处理 JSON
  3. 无类型安全:编译时无法检查错误
  4. 代码冗长:需要更多代码实现相同功能
1.3.3 RestHighLevelClient(高级客户端)详解

定义

java 复制代码
// RestHighLevelClient 基于 RestClient 构建
import org.elasticsearch.client.RestHighLevelClient;

内部结构

java 复制代码
public class RestHighLevelClient implements Closeable {
    private final RestClient client;  // 内部使用 RestClient

    public RestHighLevelClient(RestClient restClient) {
        this.client = restClient;
    }
}

特点

  • 封装了 RestClient
  • 提供面向对象的 API
  • 自动序列化/反序列化
  • Builder 模式构建请求

典型用法示例

java 复制代码
import org.apache.http.HttpHost;
import org.elasticsearch.action.index.IndexRequest;
import org.elasticsearch.action.index.IndexResponse;
import org.elasticsearch.action.search.SearchRequest;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestClient;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.search.builder.SearchSourceBuilder;

public class RestHighLevelClientExample {

    public static void main(String[] args) throws Exception {
        // 创建高级客户端(内部使用 RestClient)
        RestHighLevelClient client = new RestHighLevelClient(
            RestClient.builder(
                new HttpHost("localhost", 9200, "http")
            )
        );

        // 示例1:索引文档(使用 Builder 模式)
        IndexRequest indexRequest = new IndexRequest("products")
            .id("1")
            .source(
                "name", "iPhone 15",
                "price", 7999
            );
        IndexResponse indexResponse = client.index(indexRequest, RequestOptions.DEFAULT);
        System.out.println("索引文档: " + indexResponse.getResult());

        // 示例2:搜索文档(使用查询构建器)
        SearchRequest searchRequest = new SearchRequest("products");
        SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
        searchSourceBuilder.query(QueryBuilders.matchQuery("name", "iPhone"));
        searchRequest.source(searchSourceBuilder);

        SearchResponse searchResponse = client.search(searchRequest, RequestOptions.DEFAULT);
        System.out.println("命中数: " + searchResponse.getHits().getTotalHits().value);

        // 关闭客户端
        client.close();
    }
}

优势

  1. API 友好:面向对象,易于使用
  2. 自动序列化:无需手动构建 JSON
  3. 部分类型安全:IDE 自动提示
  4. 功能完整:覆盖常用 ES 操作

劣势

  1. 维护模式:ES 7.15+ 已标记废弃
  2. API 覆盖不全:部分新特性可能不支持
  3. 性能开销:有额外的对象创建
1.3.4 两者的关系与协作

关键理解:RestHighLevelClient 是 RestClient 的包装器

java 复制代码
// RestHighLevelClient 的内部实现(简化)
public class RestHighLevelClient {
    private final RestClient client;  // 持有 RestClient 实例

    public IndexResponse index(IndexRequest request, RequestOptions options) {
        // 1. 将 IndexRequest 转换为 HTTP 请求
        Request httpRequest = convertToHttpRequest(request);

        // 2. 使用 RestClient 发送请求
        Response response = client.performRequest(httpRequest);

        // 3. 解析响应并返回 IndexResponse
        return parseResponse(response);
    }
}

直接使用 RestClient 的场景

  1. 调用未封装的 API
java 复制代码
RestHighLevelClient highLevelClient = new RestHighLevelClient(...);

// 获取内部的 RestClient
RestClient lowLevelClient = highLevelClient.getLowLevelClient();

// 调用高级客户端不支持的 API
Request request = new Request("GET", "/_cluster/health");
Response response = lowLevelClient.performRequest(request);
  1. 性能敏感场景
java 复制代码
// 批量操作时,直接使用 RestClient 可能更高效
RestClient client = RestClient.builder(...).build();
Request bulkRequest = new Request("POST", "/_bulk");
bulkRequest.setJsonEntity(bulkBody);
client.performRequest(bulkRequest);
  1. 调试和测试
java 复制代码
// 使用 RestClient 可以直接看到原始请求
Request debugRequest = new Request("GET", "/products/_mapping");
Response response = restClient.performRequest(debugRequest);
String rawResponse = EntityUtils.toString(response.getEntity());
System.out.println(rawResponse);

1.4 客户端对比

特性 RestHighLevelClient Java API Client
类型安全 部分 ✅ 完全
流式 API
Builder 模式
JSON 支持 手动构建 自动序列化
维护状态 维护模式 活跃开发
学习曲线 中等 较低
推荐度 ES 7.x ES 8.x

1.5 选择建议

复制代码
直接使用 RestClient (Low-level):
✓ 需要调用实验性或未封装的 ES API
✓ 性能极致优化场景
✓ 需要完全控制 HTTP 请求/响应
✓ 调试和测试 ES API
✗ 日常开发(推荐使用高级客户端)

选择 RestHighLevelClient:
✓ 使用 ES 7.x
✓ 现有项目迁移成本高
✓ 社区资料丰富
✓ 功能完整,覆盖常用场景

选择 Java API Client:
✓ 使用 ES 8.x
✓ 新项目开发
✓ 需要类型安全
✓ 追求现代化 API
✓ 官方推荐的未来方向

关系总结

复制代码
RestClient          → 基础层,所有客户端依赖它
  ├─ RestHighLevelClient  → ES 7.x 推荐(已在 7.15+ 废弃)
  └─ Java API Client      → ES 8.x 推荐(未来方向)

二、环境搭建与连接

2.1 Maven 依赖

方式一:RestHighLevelClient(ES 7.x)

xml 复制代码
<dependencies>
    <!-- Elasticsearch RestHighLevelClient -->
    <dependency>
        <groupId>org.elasticsearch.client</groupId>
        <artifactId>elasticsearch-rest-high-level-client</artifactId>
        <version>7.17.15</version>
    </dependency>

    <!-- Elasticsearch Core -->
    <dependency>
        <groupId>org.elasticsearch</groupId>
        <artifactId>elasticsearch</artifactId>
        <version>7.17.15</version>
    </dependency>

    <!-- JSON 处理 -->
    <dependency>
        <groupId>com.fasterxml.jackson.core</groupId>
        <artifactId>jackson-databind</artifactId>
        <version>2.15.2</version>
    </dependency>

    <!-- Lombok(可选,简化代码) -->
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <version>1.18.30</version>
        <scope>provided</scope>
    </dependency>
</dependencies>

方式二:Java API Client(ES 8.x 推荐)

xml 复制代码
<dependencies>
    <!-- Elasticsearch Java API Client -->
    <dependency>
        <groupId>co.elastic.clients</groupId>
        <artifactId>elasticsearch-java</artifactId>
        <version>8.11.1</version>
    </dependency>

    <!-- JSON 处理库(必需) -->
    <dependency>
        <groupId>com.fasterxml.jackson.core</groupId>
        <artifactId>jackson-databind</artifactId>
        <version>2.15.2</version>
    </dependency>

    <!-- 依赖的 HTTP 客户端 -->
    <dependency>
        <groupId>org.elasticsearch.client</groupId>
        <artifactId>elasticsearch-rest-client</artifactId>
        <version>8.11.1</version>
    </dependency>
</dependencies>

2.2 创建客户端连接

方式一:RestHighLevelClient

java 复制代码
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.elasticsearch.client.RestHighLevelClient;

public class ElasticsearchConfig {

    /**
     * 创建简单连接(无认证)
     */
    public static RestHighLevelClient createSimpleClient() {
        return new RestHighLevelClient(
            RestClient.builder(
                new HttpHost("localhost", 9200, "http")
            )
        );
    }

    /**
     * 创建连接(带认证)
     */
    public static RestHighLevelClient createClientWithAuth() {
        // 配置认证信息
        CredentialsProvider credentialsProvider = new BasicCredentialsProvider();
        credentialsProvider.setCredentials(
            AuthScope.ANY,
            new UsernamePasswordCredentials("elastic", "password")
        );

        return new RestHighLevelClient(
            RestClient.builder(
                new HttpHost("localhost", 9200, "http")
            ).setHttpClientConfigCallback(httpClientBuilder ->
                httpClientBuilder.setDefaultCredentialsProvider(credentialsProvider)
            )
        );
    }

    /**
     * 创建集群连接(多节点)
     */
    public static RestHighLevelClient createClusterClient() {
        return new RestHighLevelClient(
            RestClient.builder(
                new HttpHost("node1", 9200, "http"),
                new HttpHost("node2", 9200, "http"),
                new HttpHost("node3", 9200, "http")
            )
        );
    }

    /**
     * 关闭连接
     */
    public static void closeClient(RestHighLevelClient client) {
        try {
            if (client != null) {
                client.close();
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

方式二:Java API Client

java 复制代码
import co.elastic.clients.elasticsearch.ElasticsearchClient;
import co.elastic.clients.json.jackson.JacksonJsonpMapper;
import co.elastic.clients.transport.ElasticsearchTransport;
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;

public class ElasticsearchClientConfig {

    /**
     * 创建 Elasticsearch 客户端(推荐方式)
     */
    public static ElasticsearchClient createClient() {
        // 创建 Low-level REST 客户端
        RestClient restClient = RestClient.builder(
            new HttpHost("localhost", 9200, "http")
        ).build();

        // 创建传输层(使用 Jackson 进行 JSON 序列化)
        ElasticsearchTransport transport = new RestClientTransport(
            restClient,
            new JacksonJsonpMapper()
        );

        // 创建 API 客户端
        return new ElasticsearchClient(transport);
    }

    /**
     * 创建带认证的客户端
     */
    public static ElasticsearchClient createClientWithAuth(
            String host, int port, String username, String password) {

        CredentialsProvider credentialsProvider = new BasicCredentialsProvider();
        credentialsProvider.setCredentials(
            AuthScope.ANY,
            new UsernamePasswordCredentials(username, password)
        );

        RestClient restClient = RestClient.builder(
            new HttpHost(host, port, "http")
        ).setHttpClientConfigCallback(httpClientBuilder ->
            httpClientBuilder.setDefaultCredentialsProvider(credentialsProvider)
        ).build();

        ElasticsearchTransport transport = new RestClientTransport(
            restClient,
            new JacksonJsonpMapper()
        );

        return new ElasticsearchClient(transport);
    }

    /**
     * 关闭客户端
     */
    public static void closeClient(ElasticsearchClient client) {
        try {
            if (client != null && client._transport() != null) {
                client._transport().close();
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

2.3 测试连接

java 复制代码
import co.elastic.clients.elasticsearch.ElasticsearchClient;
import co.elastic.clients.elasticsearch.core.InfoResponse;

public class ConnectionTest {

    public static void main(String[] args) {
        ElasticsearchClient client = ElasticsearchClientConfig.createClient();

        try {
            // 获取集群信息
            InfoResponse info = client.info();

            System.out.println("集群名称: " + info.clusterName());
            System.out.println("节点名称: " + info.name());
            System.out.println("版本号: " + info.version().number());
            System.out.println("连接成功!");

        } catch (Exception e) {
            System.err.println("连接失败: " + e.getMessage());
            e.printStackTrace();
        } finally {
            ElasticsearchClientConfig.closeClient(client);
        }
    }
}

三、索引操作

3.1 定义实体类

java 复制代码
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

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

/**
 * 商品实体类
 */
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Product {

    private String id;

    private String title;

    private String description;

    private Double price;

    private String brand;

    private String category;

    private List<String> tags;

    private Integer stock;

    @JsonProperty("created_at")
    private Date createdAt;

    @JsonProperty("updated_at")
    private Date updatedAt;
}

3.2 创建索引

使用 Java API Client(推荐):

java 复制代码
import co.elastic.clients.elasticsearch.ElasticsearchClient;
import co.elastic.clients.elasticsearch.indices.CreateIndexRequest;
import co.elastic.clients.elasticsearch.indices.CreateIndexResponse;
import co.elastic.clients.elasticsearch.indices.IndexSettings;

public class IndexOperations {

    /**
     * 创建索引(简单方式)
     */
    public static void createSimpleIndex(ElasticsearchClient client) throws Exception {
        CreateIndexResponse response = client.indices().create(c -> c
            .index("products")
        );

        System.out.println("索引创建: " + response.acknowledged());
    }

    /**
     * 创建索引(带设置和映射)
     */
    public static void createIndexWithMapping(ElasticsearchClient client) throws Exception {
        CreateIndexResponse response = client.indices().create(c -> c
            .index("products")
            .settings(s -> s
                .numberOfShards("3")
                .numberOfReplicas("1")
                .refreshInterval(t -> t.time("5s"))
                .analysis(a -> a
                    .analyzer("ik_analyzer", an -> an
                        .custom(ca -> ca
                            .tokenizer("ik_max_word")
                        )
                    )
                )
            )
            .mappings(m -> m
                .properties("id", p -> p.keyword(k -> k))
                .properties("title", p -> p.text(t -> t
                    .analyzer("ik_max_word")
                    .searchAnalyzer("ik_smart")
                    .fields("keyword", f -> f.keyword(k -> k))
                ))
                .properties("description", p -> p.text(t -> t
                    .analyzer("ik_max_word")
                ))
                .properties("price", p -> p.double_(d -> d))
                .properties("brand", p -> p.keyword(k -> k))
                .properties("category", p -> p.keyword(k -> k))
                .properties("tags", p -> p.keyword(k -> k))
                .properties("stock", p -> p.integer(i -> i))
                .properties("created_at", p -> p.date(d -> d
                    .format("yyyy-MM-dd HH:mm:ss||epoch_millis")
                ))
                .properties("updated_at", p -> p.date(d -> d
                    .format("yyyy-MM-dd HH:mm:ss||epoch_millis")
                ))
            )
        );

        System.out.println("索引创建成功: " + response.acknowledged());
    }

    /**
     * 使用 JSON 字符串创建索引
     */
    public static void createIndexFromJson(ElasticsearchClient client) throws Exception {
        String settings = """
            {
              "settings": {
                "number_of_shards": 3,
                "number_of_replicas": 1,
                "analysis": {
                  "analyzer": {
                    "ik_analyzer": {
                      "type": "custom",
                      "tokenizer": "ik_max_word"
                    }
                  }
                }
              },
              "mappings": {
                "properties": {
                  "title": {
                    "type": "text",
                    "analyzer": "ik_max_word",
                    "search_analyzer": "ik_smart",
                    "fields": {
                      "keyword": {
                        "type": "keyword"
                      }
                    }
                  },
                  "price": {
                    "type": "double"
                  }
                }
              }
            }
            """;

        // 使用原始 JSON(需要 Jackson 或其他 JSON 库)
        // 略过具体实现
    }
}

3.3 检查索引是否存在

java 复制代码
public static boolean indexExists(ElasticsearchClient client, String indexName)
        throws Exception {
    return client.indices().exists(e -> e.index(indexName)).value();
}

3.4 获取索引信息

java 复制代码
import co.elastic.clients.elasticsearch.indices.GetIndexResponse;

public static void getIndexInfo(ElasticsearchClient client, String indexName)
        throws Exception {
    GetIndexResponse response = client.indices().get(g -> g.index(indexName));

    response.result().forEach((name, indexState) -> {
        System.out.println("索引名称: " + name);
        System.out.println("分片数: " + indexState.settings().index().numberOfShards());
        System.out.println("副本数: " + indexState.settings().index().numberOfReplicas());
    });
}

3.5 删除索引

java 复制代码
import co.elastic.clients.elasticsearch.indices.DeleteIndexResponse;

public static void deleteIndex(ElasticsearchClient client, String indexName)
        throws Exception {
    DeleteIndexResponse response = client.indices().delete(d -> d.index(indexName));
    System.out.println("索引删除: " + response.acknowledged());
}

3.6 更新索引设置

java 复制代码
public static void updateIndexSettings(ElasticsearchClient client, String indexName)
        throws Exception {
    client.indices().putSettings(s -> s
        .index(indexName)
        .settings(set -> set
            .numberOfReplicas("2")
            .refreshInterval(t -> t.time("10s"))
        )
    );
    System.out.println("索引设置更新成功");
}

四、文档操作 CRUD

4.1 创建文档(Index)

java 复制代码
import co.elastic.clients.elasticsearch.core.IndexRequest;
import co.elastic.clients.elasticsearch.core.IndexResponse;

public class DocumentOperations {

    /**
     * 索引单个文档(自动生成 ID)
     */
    public static void indexDocument(ElasticsearchClient client) throws Exception {
        Product product = new Product();
        product.setTitle("iPhone 15 Pro");
        product.setDescription("最新款 iPhone,搭载 A17 Pro 芯片");
        product.setPrice(7999.0);
        product.setBrand("Apple");
        product.setCategory("手机");
        product.setTags(Arrays.asList("智能手机", "5G", "iOS"));
        product.setStock(100);
        product.setCreatedAt(new Date());
        product.setUpdatedAt(new Date());

        IndexResponse response = client.index(i -> i
            .index("products")
            .document(product)
        );

        System.out.println("文档ID: " + response.id());
        System.out.println("结果: " + response.result());
        System.out.println("版本: " + response.version());
    }

    /**
     * 索引文档(指定 ID)
     */
    public static void indexDocumentWithId(ElasticsearchClient client, Product product)
            throws Exception {
        IndexResponse response = client.index(i -> i
            .index("products")
            .id(product.getId())
            .document(product)
        );

        System.out.println("文档已索引: " + response.id());
    }

    /**
     * 使用 Map 索引文档
     */
    public static void indexDocumentFromMap(ElasticsearchClient client) throws Exception {
        Map<String, Object> doc = new HashMap<>();
        doc.put("title", "小米14 Pro");
        doc.put("price", 4999.0);
        doc.put("brand", "Xiaomi");

        IndexResponse response = client.index(i -> i
            .index("products")
            .id("xiaomi_14_pro")
            .document(doc)
        );

        System.out.println("文档索引: " + response.result());
    }
}

4.2 获取文档(Get)

java 复制代码
import co.elastic.clients.elasticsearch.core.GetResponse;

/**
 * 获取文档
 */
public static Product getDocument(ElasticsearchClient client, String id)
        throws Exception {
    GetResponse<Product> response = client.get(g -> g
        .index("products")
        .id(id),
        Product.class
    );

    if (response.found()) {
        Product product = response.source();
        System.out.println("文档: " + product);
        return product;
    } else {
        System.out.println("文档不存在");
        return null;
    }
}

/**
 * 检查文档是否存在
 */
public static boolean documentExists(ElasticsearchClient client, String id)
        throws Exception {
    return client.exists(e -> e
        .index("products")
        .id(id)
    ).value();
}

4.3 更新文档(Update)

java 复制代码
import co.elastic.clients.elasticsearch.core.UpdateRequest;
import co.elastic.clients.elasticsearch.core.UpdateResponse;

/**
 * 部分更新文档
 */
public static void updateDocument(ElasticsearchClient client, String id)
        throws Exception {
    // 方式1: 使用 Map
    Map<String, Object> updates = new HashMap<>();
    updates.put("price", 7499.0);
    updates.put("stock", 80);

    UpdateResponse<Product> response = client.update(u -> u
        .index("products")
        .id(id)
        .doc(updates),
        Product.class
    );

    System.out.println("更新结果: " + response.result());
    System.out.println("版本: " + response.version());
}

/**
 * 使用脚本更新
 */
public static void updateDocumentWithScript(ElasticsearchClient client, String id)
        throws Exception {
    client.update(u -> u
        .index("products")
        .id(id)
        .script(s -> s
            .inline(i -> i
                .source("ctx._source.stock -= params.count")
                .params("count", JsonData.of(5))
            )
        ),
        Product.class
    );

    System.out.println("库存已减少");
}

/**
 * Upsert(存在则更新,不存在则插入)
 */
public static void upsertDocument(ElasticsearchClient client, String id)
        throws Exception {
    Product product = new Product();
    product.setTitle("华为 Mate 60 Pro");
    product.setPrice(6999.0);

    Map<String, Object> updates = new HashMap<>();
    updates.put("price", 7299.0);

    client.update(u -> u
        .index("products")
        .id(id)
        .doc(updates)
        .upsert(product),  // 文档不存在时插入
        Product.class
    );
}

4.4 删除文档(Delete)

java 复制代码
import co.elastic.clients.elasticsearch.core.DeleteResponse;

/**
 * 删除文档
 */
public static void deleteDocument(ElasticsearchClient client, String id)
        throws Exception {
    DeleteResponse response = client.delete(d -> d
        .index("products")
        .id(id)
    );

    System.out.println("删除结果: " + response.result());
}

/**
 * 按查询删除(Delete By Query)
 */
public static void deleteByQuery(ElasticsearchClient client) throws Exception {
    client.deleteByQuery(d -> d
        .index("products")
        .query(q -> q
            .range(r -> r
                .field("price")
                .lt(JsonData.of(100))
            )
        )
    );

    System.out.println("低价商品已删除");
}

五、查询操作

5.1 基础查询

Match All 查询(查询所有):

java 复制代码
import co.elastic.clients.elasticsearch.core.SearchResponse;
import co.elastic.clients.elasticsearch.core.search.Hit;

public class QueryOperations {

    /**
     * 查询所有文档
     */
    public static void matchAllQuery(ElasticsearchClient client) throws Exception {
        SearchResponse<Product> response = client.search(s -> s
            .index("products")
            .query(q -> q
                .matchAll(m -> m)
            ),
            Product.class
        );

        System.out.println("总数: " + response.hits().total().value());

        for (Hit<Product> hit : response.hits().hits()) {
            Product product = hit.source();
            System.out.println("ID: " + hit.id() + ", 标题: " + product.getTitle());
        }
    }

    /**
     * 分页查询
     */
    public static void paginationQuery(ElasticsearchClient client, int page, int size)
            throws Exception {
        int from = (page - 1) * size;

        SearchResponse<Product> response = client.search(s -> s
            .index("products")
            .query(q -> q.matchAll(m -> m))
            .from(from)
            .size(size)
            .sort(so -> so
                .field(f -> f.field("created_at").order(SortOrder.Desc))
            ),
            Product.class
        );

        System.out.println("第 " + page + " 页,每页 " + size + " 条");
        response.hits().hits().forEach(hit -> {
            System.out.println(hit.source().getTitle());
        });
    }
}

5.2 全文搜索

Match 查询:

java 复制代码
/**
 * Match 查询(全文搜索)
 */
public static void matchQuery(ElasticsearchClient client, String keyword)
        throws Exception {
    SearchResponse<Product> response = client.search(s -> s
        .index("products")
        .query(q -> q
            .match(m -> m
                .field("title")
                .query(keyword)
            )
        ),
        Product.class
    );

    response.hits().hits().forEach(hit -> {
        System.out.println("得分: " + hit.score() + ", 标题: " + hit.source().getTitle());
    });
}

/**
 * Multi Match 查询(多字段搜索)
 */
public static void multiMatchQuery(ElasticsearchClient client, String keyword)
        throws Exception {
    SearchResponse<Product> response = client.search(s -> s
        .index("products")
        .query(q -> q
            .multiMatch(m -> m
                .fields("title^3", "description^2", "brand")  // 权重提升
                .query(keyword)
                .fuzziness("AUTO")  // 模糊匹配
            )
        ),
        Product.class
    );

    response.hits().hits().forEach(hit -> {
        System.out.println(hit.source().getTitle());
    });
}

5.3 精确匹配

Term 查询:

java 复制代码
/**
 * Term 查询(精确匹配)
 */
public static void termQuery(ElasticsearchClient client, String brand)
        throws Exception {
    SearchResponse<Product> response = client.search(s -> s
        .index("products")
        .query(q -> q
            .term(t -> t
                .field("brand")
                .value(brand)
            )
        ),
        Product.class
    );

    System.out.println("品牌 " + brand + " 的商品:");
    response.hits().hits().forEach(hit -> {
        System.out.println(hit.source().getTitle());
    });
}

/**
 * Terms 查询(多个精确值)
 */
public static void termsQuery(ElasticsearchClient client, List<String> brands)
        throws Exception {
    SearchResponse<Product> response = client.search(s -> s
        .index("products")
        .query(q -> q
            .terms(t -> t
                .field("brand")
                .terms(ts -> ts.value(
                    brands.stream()
                        .map(FieldValue::of)
                        .collect(Collectors.toList())
                ))
            )
        ),
        Product.class
    );

    System.out.println("多品牌商品:");
    response.hits().hits().forEach(hit -> {
        System.out.println(hit.source().getTitle());
    });
}

5.4 范围查询

java 复制代码
/**
 * Range 查询(范围查询)
 */
public static void rangeQuery(ElasticsearchClient client, double minPrice, double maxPrice)
        throws Exception {
    SearchResponse<Product> response = client.search(s -> s
        .index("products")
        .query(q -> q
            .range(r -> r
                .field("price")
                .gte(JsonData.of(minPrice))
                .lte(JsonData.of(maxPrice))
            )
        ),
        Product.class
    );

    System.out.println("价格在 " + minPrice + " ~ " + maxPrice + " 的商品:");
    response.hits().hits().forEach(hit -> {
        Product p = hit.source();
        System.out.println(p.getTitle() + " - ¥" + p.getPrice());
    });
}

/**
 * 日期范围查询
 */
public static void dateRangeQuery(ElasticsearchClient client) throws Exception {
    SearchResponse<Product> response = client.search(s -> s
        .index("products")
        .query(q -> q
            .range(r -> r
                .field("created_at")
                .gte(JsonData.of("now-7d/d"))  // 最近7天
                .lte(JsonData.of("now"))
            )
        ),
        Product.class
    );

    System.out.println("最近7天的商品:");
    response.hits().hits().forEach(hit -> {
        System.out.println(hit.source().getTitle());
    });
}

5.5 Bool 组合查询

java 复制代码
/**
 * Bool 查询(组合查询)
 */
public static void boolQuery(ElasticsearchClient client) throws Exception {
    SearchResponse<Product> response = client.search(s -> s
        .index("products")
        .query(q -> q
            .bool(b -> b
                .must(m -> m  // 必须匹配
                    .match(ma -> ma
                        .field("title")
                        .query("手机")
                    )
                )
                .filter(f -> f  // 过滤条件(不参与打分)
                    .term(t -> t
                        .field("brand")
                        .value("Apple")
                    )
                )
                .filter(f -> f
                    .range(r -> r
                        .field("price")
                        .gte(JsonData.of(5000))
                        .lte(JsonData.of(10000))
                    )
                )
                .should(sh -> sh  // 应该匹配(提高得分)
                    .term(t -> t
                        .field("tags")
                        .value("5G")
                    )
                )
                .mustNot(mn -> mn  // 必须不匹配
                    .term(t -> t
                        .field("stock")
                        .value(0)
                    )
                )
                .minimumShouldMatch("1")  // should 至少匹配1个
            )
        ),
        Product.class
    );

    System.out.println("符合条件的商品:");
    response.hits().hits().forEach(hit -> {
        Product p = hit.source();
        System.out.println(p.getTitle() + " - ¥" + p.getPrice());
    });
}

5.6 高亮显示

java 复制代码
import co.elastic.clients.elasticsearch.core.search.Highlight;
import co.elastic.clients.elasticsearch.core.search.HighlightField;

/**
 * 高亮查询
 */
public static void highlightQuery(ElasticsearchClient client, String keyword)
        throws Exception {
    SearchResponse<Product> response = client.search(s -> s
        .index("products")
        .query(q -> q
            .match(m -> m
                .field("title")
                .query(keyword)
            )
        )
        .highlight(h -> h
            .fields("title", f -> f
                .preTags("<em>")
                .postTags("</em>")
            )
        ),
        Product.class
    );

    response.hits().hits().forEach(hit -> {
        System.out.println("原标题: " + hit.source().getTitle());

        if (hit.highlight().containsKey("title")) {
            List<String> highlights = hit.highlight().get("title");
            System.out.println("高亮: " + highlights.get(0));
        }
    });
}

5.7 搜索建议(Suggester)

java 复制代码
/**
 * 搜索建议
 */
public static void suggesterQuery(ElasticsearchClient client, String prefix)
        throws Exception {
    SearchResponse<Product> response = client.search(s -> s
        .index("products")
        .suggest(su -> su
            .suggesters("title_suggest", sug -> sug
                .prefix(prefix)
                .completion(c -> c
                    .field("title.suggest")  // 需要是 completion 类型
                    .size(5)
                    .skipDuplicates(true)
                )
            )
        ),
        Product.class
    );

    // 处理建议结果
    // response.suggest().get("title_suggest")...
}

六、聚合操作

6.1 指标聚合(Metrics Aggregations)

java 复制代码
import co.elastic.clients.elasticsearch._types.aggregations.*;

public class AggregationOperations {

    /**
     * 统计聚合(Stats)
     */
    public static void statsAggregation(ElasticsearchClient client) throws Exception {
        SearchResponse<Product> response = client.search(s -> s
            .index("products")
            .size(0)  // 不返回文档,只返回聚合结果
            .aggregations("price_stats", a -> a
                .stats(st -> st.field("price"))
            ),
            Product.class
        );

        StatsAggregate stats = response.aggregations()
            .get("price_stats")
            .stats();

        System.out.println("价格统计:");
        System.out.println("  数量: " + stats.count());
        System.out.println("  最小值: " + stats.min());
        System.out.println("  最大值: " + stats.max());
        System.out.println("  平均值: " + stats.avg());
        System.out.println("  总和: " + stats.sum());
    }

    /**
     * 平均值聚合
     */
    public static void avgAggregation(ElasticsearchClient client) throws Exception {
        SearchResponse<Product> response = client.search(s -> s
            .index("products")
            .size(0)
            .aggregations("avg_price", a -> a
                .avg(av -> av.field("price"))
            ),
            Product.class
        );

        double avgPrice = response.aggregations()
            .get("avg_price")
            .avg()
            .value();

        System.out.println("平均价格: ¥" + avgPrice);
    }

    /**
     * 最大/最小值聚合
     */
    public static void minMaxAggregation(ElasticsearchClient client) throws Exception {
        SearchResponse<Product> response = client.search(s -> s
            .index("products")
            .size(0)
            .aggregations("min_price", a -> a
                .min(m -> m.field("price"))
            )
            .aggregations("max_price", a -> a
                .max(m -> m.field("price"))
            ),
            Product.class
        );

        double minPrice = response.aggregations().get("min_price").min().value();
        double maxPrice = response.aggregations().get("max_price").max().value();

        System.out.println("最低价: ¥" + minPrice);
        System.out.println("最高价: ¥" + maxPrice);
    }
}

6.2 桶聚合(Bucket Aggregations)

java 复制代码
/**
 * Terms 聚合(分组统计)
 */
public static void termsAggregation(ElasticsearchClient client) throws Exception {
    SearchResponse<Product> response = client.search(s -> s
        .index("products")
        .size(0)
        .aggregations("group_by_brand", a -> a
            .terms(t -> t
                .field("brand")
                .size(10)  // 返回前10个桶
                .order(NamedValue.of("_count", SortOrder.Desc))  // 按文档数降序
            )
        ),
        Product.class
    );

    StringTermsAggregate brands = response.aggregations()
        .get("group_by_brand")
        .sterms();

    System.out.println("各品牌商品数:");
    brands.buckets().array().forEach(bucket -> {
        System.out.println("  " + bucket.key().stringValue() + ": " + bucket.docCount());
    });
}

/**
 * Range 聚合(范围分组)
 */
public static void rangeAggregation(ElasticsearchClient client) throws Exception {
    SearchResponse<Product> response = client.search(s -> s
        .index("products")
        .size(0)
        .aggregations("price_ranges", a -> a
            .range(r -> r
                .field("price")
                .ranges(
                    ra -> ra.to(String.valueOf(2000)).key("低价"),
                    ra -> ra.from(String.valueOf(2000)).to(String.valueOf(5000)).key("中价"),
                    ra -> ra.from(String.valueOf(5000)).key("高价")
                )
            )
        ),
        Product.class
    );

    RangeAggregate ranges = response.aggregations()
        .get("price_ranges")
        .range();

    System.out.println("价格区间分布:");
    ranges.buckets().array().forEach(bucket -> {
        System.out.println("  " + bucket.key() + ": " + bucket.docCount());
    });
}

/**
 * Histogram 聚合(直方图)
 */
public static void histogramAggregation(ElasticsearchClient client) throws Exception {
    SearchResponse<Product> response = client.search(s -> s
        .index("products")
        .size(0)
        .aggregations("price_histogram", a -> a
            .histogram(h -> h
                .field("price")
                .interval(1000.0)  // 每1000元一个区间
            )
        ),
        Product.class
    );

    HistogramAggregate histogram = response.aggregations()
        .get("price_histogram")
        .histogram();

    System.out.println("价格分布直方图:");
    histogram.buckets().array().forEach(bucket -> {
        System.out.println("  " + bucket.key() + "-" +
            (bucket.key() + 1000) + ": " + bucket.docCount());
    });
}

/**
 * Date Histogram 聚合(时间直方图)
 */
public static void dateHistogramAggregation(ElasticsearchClient client) throws Exception {
    SearchResponse<Product> response = client.search(s -> s
        .index("products")
        .size(0)
        .aggregations("daily_sales", a -> a
            .dateHistogram(dh -> dh
                .field("created_at")
                .calendarInterval(CalendarInterval.Day)
                .format("yyyy-MM-dd")
                .minDocCount(0)  // 包含没有文档的时间桶
            )
        ),
        Product.class
    );

    DateHistogramAggregate daily = response.aggregations()
        .get("daily_sales")
        .dateHistogram();

    System.out.println("每日新增商品:");
    daily.buckets().array().forEach(bucket -> {
        System.out.println("  " + bucket.keyAsString() + ": " + bucket.docCount());
    });
}

6.3 嵌套聚合

java 复制代码
/**
 * 嵌套聚合(Terms + Stats)
 */
public static void nestedAggregation(ElasticsearchClient client) throws Exception {
    SearchResponse<Product> response = client.search(s -> s
        .index("products")
        .size(0)
        .aggregations("brands", a -> a
            .terms(t -> t
                .field("brand")
                .size(10)
            )
            .aggregations("avg_price", sub -> sub  // 子聚合
                .avg(av -> av.field("price"))
            )
            .aggregations("max_price", sub -> sub
                .max(m -> m.field("price"))
            )
        ),
        Product.class
    );

    StringTermsAggregate brands = response.aggregations()
        .get("brands")
        .sterms();

    System.out.println("各品牌价格统计:");
    brands.buckets().array().forEach(bucket -> {
        String brand = bucket.key().stringValue();
        long count = bucket.docCount();
        double avgPrice = bucket.aggregations().get("avg_price").avg().value();
        double maxPrice = bucket.aggregations().get("max_price").max().value();

        System.out.println(String.format("  %s: 数量=%d, 平均价=%.2f, 最高价=%.2f",
            brand, count, avgPrice, maxPrice));
    });
}

/**
 * 多层嵌套聚合
 */
public static void multiLevelAggregation(ElasticsearchClient client) throws Exception {
    SearchResponse<Product> response = client.search(s -> s
        .index("products")
        .size(0)
        .aggregations("by_category", a -> a
            .terms(t -> t.field("category"))
            .aggregations("by_brand", sub -> sub
                .terms(t -> t.field("brand"))
                .aggregations("total_price", subsub -> subsub
                    .sum(su -> su.field("price"))
                )
            )
        ),
        Product.class
    );

    // 解析多层聚合结果
    StringTermsAggregate categories = response.aggregations()
        .get("by_category")
        .sterms();

    categories.buckets().array().forEach(categoryBucket -> {
        System.out.println("分类: " + categoryBucket.key().stringValue());

        StringTermsAggregate brands = categoryBucket.aggregations()
            .get("by_brand")
            .sterms();

        brands.buckets().array().forEach(brandBucket -> {
            double totalPrice = brandBucket.aggregations()
                .get("total_price")
                .sum()
                .value();

            System.out.println("  品牌: " + brandBucket.key().stringValue() +
                ", 总价值: " + totalPrice);
        });
    });
}

七、批量操作

7.1 Bulk API

java 复制代码
import co.elastic.clients.elasticsearch.core.BulkRequest;
import co.elastic.clients.elasticsearch.core.BulkResponse;
import co.elastic.clients.elasticsearch.core.bulk.BulkOperation;
import co.elastic.clients.elasticsearch.core.bulk.BulkResponseItem;

public class BulkOperations {

    /**
     * 批量索引文档
     */
    public static void bulkIndex(ElasticsearchClient client, List<Product> products)
            throws Exception {
        BulkRequest.Builder br = new BulkRequest.Builder();

        for (Product product : products) {
            br.operations(op -> op
                .index(idx -> idx
                    .index("products")
                    .id(product.getId())
                    .document(product)
                )
            );
        }

        BulkResponse response = client.bulk(br.build());

        if (response.errors()) {
            System.err.println("批量操作有错误:");
            for (BulkResponseItem item : response.items()) {
                if (item.error() != null) {
                    System.err.println("  ID: " + item.id() + ", 错误: " +
                        item.error().reason());
                }
            }
        } else {
            System.out.println("批量索引成功: " + response.items().size() + " 条");
        }
    }

    /**
     * 批量混合操作(Index + Update + Delete)
     */
    public static void bulkMixed(ElasticsearchClient client) throws Exception {
        BulkRequest.Builder br = new BulkRequest.Builder();

        // 索引操作
        Product newProduct = new Product();
        newProduct.setId("prod_001");
        newProduct.setTitle("新商品");
        newProduct.setPrice(999.0);

        br.operations(op -> op
            .index(idx -> idx
                .index("products")
                .id(newProduct.getId())
                .document(newProduct)
            )
        );

        // 更新操作
        Map<String, Object> updates = new HashMap<>();
        updates.put("price", 1299.0);

        br.operations(op -> op
            .update(u -> u
                .index("products")
                .id("prod_002")
                .document(updates)
            )
        );

        // 删除操作
        br.operations(op -> op
            .delete(d -> d
                .index("products")
                .id("prod_003")
            )
        );

        BulkResponse response = client.bulk(br.build());
        System.out.println("批量操作完成,耗时: " + response.took() + "ms");
    }

    /**
     * 使用 BulkProcessor(自动批量)
     * 注意:Java API Client 8.x 没有内置 BulkProcessor,需要自己实现
     */
    public static void bulkProcessorExample(ElasticsearchClient client) {
        // 自定义实现 BulkProcessor 逻辑
        // 可以使用 CompletableFuture 和定时器实现自动刷新
    }
}

7.2 批量查询(Multi Get)

java 复制代码
import co.elastic.clients.elasticsearch.core.MgetResponse;
import co.elastic.clients.elasticsearch.core.mget.MultiGetResponseItem;

/**
 * 批量获取文档
 */
public static void multiGet(ElasticsearchClient client, List<String> ids)
        throws Exception {
    MgetResponse<Product> response = client.mget(m -> m
        .index("products")
        .ids(ids),
        Product.class
    );

    for (MultiGetResponseItem<Product> item : response.docs()) {
        if (item.isResult()) {
            Product product = item.result().source();
            System.out.println("ID: " + item.result().id() + ", 标题: " +
                product.getTitle());
        } else {
            System.out.println("文档未找到: " + item.failure().id());
        }
    }
}

八、Spring Boot 集成

8.1 添加依赖

xml 复制代码
<dependencies>
    <!-- Spring Boot Starter -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

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

    <!-- Jackson for JSON -->
    <dependency>
        <groupId>com.fasterxml.jackson.core</groupId>
        <artifactId>jackson-databind</artifactId>
    </dependency>

    <!-- Apache HTTP Client -->
    <dependency>
        <groupId>org.elasticsearch.client</groupId>
        <artifactId>elasticsearch-rest-client</artifactId>
        <version>8.11.1</version>
    </dependency>

    <!-- Lombok -->
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>
</dependencies>

8.2 配置文件

application.yml:

yaml 复制代码
elasticsearch:
  host: localhost
  port: 9200
  username: elastic
  password: changeme
  connect-timeout: 5000
  socket-timeout: 60000

8.3 配置类

java 复制代码
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.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class ElasticsearchConfig {

    @Value("${elasticsearch.host}")
    private String host;

    @Value("${elasticsearch.port}")
    private int port;

    @Value("${elasticsearch.username}")
    private String username;

    @Value("${elasticsearch.password}")
    private String password;

    @Bean
    public ElasticsearchClient elasticsearchClient() {
        // 创建认证提供者
        CredentialsProvider credentialsProvider = new BasicCredentialsProvider();
        credentialsProvider.setCredentials(
            AuthScope.ANY,
            new UsernamePasswordCredentials(username, password)
        );

        // 创建 RestClient
        RestClient restClient = RestClient.builder(
            new HttpHost(host, port, "http")
        ).setHttpClientConfigCallback(httpClientBuilder ->
            httpClientBuilder.setDefaultCredentialsProvider(credentialsProvider)
        ).build();

        // 创建 Transport
        RestClientTransport transport = new RestClientTransport(
            restClient,
            new JacksonJsonpMapper()
        );

        // 创建 API 客户端
        return new ElasticsearchClient(transport);
    }
}

8.4 Service 层

java 复制代码
import co.elastic.clients.elasticsearch.ElasticsearchClient;
import co.elastic.clients.elasticsearch.core.*;
import co.elastic.clients.elasticsearch.core.search.Hit;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.List;

@Service
@RequiredArgsConstructor
public class ProductService {

    private final ElasticsearchClient client;

    private static final String INDEX_NAME = "products";

    /**
     * 保存商品
     */
    public String saveProduct(Product product) {
        try {
            IndexResponse response = client.index(i -> i
                .index(INDEX_NAME)
                .id(product.getId())
                .document(product)
            );
            return response.id();
        } catch (Exception e) {
            throw new RuntimeException("保存商品失败", e);
        }
    }

    /**
     * 根据 ID 获取商品
     */
    public Product getProduct(String id) {
        try {
            GetResponse<Product> response = client.get(g -> g
                .index(INDEX_NAME)
                .id(id),
                Product.class
            );

            return response.found() ? response.source() : null;
        } catch (Exception e) {
            throw new RuntimeException("获取商品失败", e);
        }
    }

    /**
     * 搜索商品
     */
    public List<Product> searchProducts(String keyword, int page, int size) {
        try {
            int from = (page - 1) * size;

            SearchResponse<Product> response = client.search(s -> s
                .index(INDEX_NAME)
                .query(q -> q
                    .multiMatch(m -> m
                        .fields("title^3", "description")
                        .query(keyword)
                    )
                )
                .from(from)
                .size(size),
                Product.class
            );

            List<Product> products = new ArrayList<>();
            for (Hit<Product> hit : response.hits().hits()) {
                products.add(hit.source());
            }

            return products;
        } catch (Exception e) {
            throw new RuntimeException("搜索商品失败", e);
        }
    }

    /**
     * 删除商品
     */
    public void deleteProduct(String id) {
        try {
            client.delete(d -> d
                .index(INDEX_NAME)
                .id(id)
            );
        } catch (Exception e) {
            throw new RuntimeException("删除商品失败", e);
        }
    }

    /**
     * 批量保存商品
     */
    public void bulkSaveProducts(List<Product> products) {
        try {
            BulkRequest.Builder br = new BulkRequest.Builder();

            for (Product product : products) {
                br.operations(op -> op
                    .index(idx -> idx
                        .index(INDEX_NAME)
                        .id(product.getId())
                        .document(product)
                    )
                );
            }

            BulkResponse response = client.bulk(br.build());

            if (response.errors()) {
                throw new RuntimeException("批量保存失败");
            }
        } catch (Exception e) {
            throw new RuntimeException("批量保存商品失败", e);
        }
    }
}

8.5 Controller 层

java 复制代码
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@RestController
@RequestMapping("/api/products")
@RequiredArgsConstructor
public class ProductController {

    private final ProductService productService;

    /**
     * 创建商品
     */
    @PostMapping
    public String createProduct(@RequestBody Product product) {
        return productService.saveProduct(product);
    }

    /**
     * 获取商品
     */
    @GetMapping("/{id}")
    public Product getProduct(@PathVariable String id) {
        return productService.getProduct(id);
    }

    /**
     * 搜索商品
     */
    @GetMapping("/search")
    public List<Product> searchProducts(
            @RequestParam String keyword,
            @RequestParam(defaultValue = "1") int page,
            @RequestParam(defaultValue = "10") int size) {
        return productService.searchProducts(keyword, page, size);
    }

    /**
     * 删除商品
     */
    @DeleteMapping("/{id}")
    public void deleteProduct(@PathVariable String id) {
        productService.deleteProduct(id);
    }

    /**
     * 批量创建商品
     */
    @PostMapping("/bulk")
    public void bulkCreateProducts(@RequestBody List<Product> products) {
        productService.bulkSaveProducts(products);
    }
}

九、高级特性

9.1 滚动查询(Scroll)

java 复制代码
import co.elastic.clients.elasticsearch.core.ScrollResponse;
import co.elastic.clients.elasticsearch.core.search.Hit;

/**
 * 滚动查询(适合大量数据导出)
 */
public static void scrollQuery(ElasticsearchClient client) throws Exception {
    String scrollId = null;
    List<Product> allProducts = new ArrayList<>();

    try {
        // 初始化 scroll
        SearchResponse<Product> response = client.search(s -> s
            .index("products")
            .scroll(t -> t.time("1m"))  // 保持上下文1分钟
            .size(1000)  // 每批1000条
            .query(q -> q.matchAll(m -> m)),
            Product.class
        );

        scrollId = response.scrollId();

        // 第一批数据
        response.hits().hits().forEach(hit -> allProducts.add(hit.source()));

        // 继续滚动
        while (response.hits().hits().size() > 0) {
            final String currentScrollId = scrollId;

            ScrollResponse<Product> scrollResponse = client.scroll(s -> s
                .scrollId(currentScrollId)
                .scroll(t -> t.time("1m")),
                Product.class
            );

            scrollId = scrollResponse.scrollId();
            scrollResponse.hits().hits().forEach(hit -> allProducts.add(hit.source()));

            if (scrollResponse.hits().hits().isEmpty()) {
                break;
            }
        }

        System.out.println("总共获取: " + allProducts.size() + " 条数据");

    } finally {
        // 清除 scroll 上下文
        if (scrollId != null) {
            client.clearScroll(c -> c.scrollId(scrollId));
        }
    }
}
java 复制代码
/**
 * Search After(深度分页)
 */
public static void searchAfter(ElasticsearchClient client) throws Exception {
    List<Product> allProducts = new ArrayList<>();
    List<String> searchAfter = null;

    while (true) {
        final List<String> currentSearchAfter = searchAfter;

        SearchResponse<Product> response = client.search(s -> {
            s.index("products")
             .size(1000)
             .sort(so -> so.field(f -> f.field("created_at")))
             .sort(so -> so.field(f -> f.field("_id")))
             .query(q -> q.matchAll(m -> m));

            if (currentSearchAfter != null) {
                s.searchAfter(currentSearchAfter);
            }

            return s;
        }, Product.class);

        if (response.hits().hits().isEmpty()) {
            break;
        }

        response.hits().hits().forEach(hit -> {
            allProducts.add(hit.source());
        });

        // 获取最后一个文档的 sort 值
        Hit<Product> lastHit = response.hits().hits().get(
            response.hits().hits().size() - 1
        );
        searchAfter = lastHit.sort();
    }

    System.out.println("总共获取: " + allProducts.size() + " 条数据");
}

9.3 Reindex(重建索引)

java 复制代码
/**
 * 重建索引
 */
public static void reindex(ElasticsearchClient client) throws Exception {
    client.reindex(r -> r
        .source(s -> s.index("old_products"))
        .dest(d -> d.index("new_products"))
        .waitForCompletion(true)
    );

    System.out.println("重建索引完成");
}

9.4 别名操作

java 复制代码
/**
 * 创建索引别名
 */
public static void createAlias(ElasticsearchClient client) throws Exception {
    client.indices().updateAliases(u -> u
        .actions(a -> a
            .add(ad -> ad
                .index("products_v1")
                .alias("products")
            )
        )
    );
}

/**
 * 别名切换(零停机升级)
 */
public static void switchAlias(ElasticsearchClient client) throws Exception {
    client.indices().updateAliases(u -> u
        .actions(a -> a
            .remove(r -> r
                .index("products_v1")
                .alias("products")
            )
        )
        .actions(a -> a
            .add(ad -> ad
                .index("products_v2")
                .alias("products")
            )
        )
    );
}

十、最佳实践

10.1 连接池管理

java 复制代码
@Configuration
public class ElasticsearchConfig {

    @Bean
    public ElasticsearchClient elasticsearchClient() {
        RestClient restClient = RestClient.builder(
            new HttpHost("localhost", 9200)
        )
        .setRequestConfigCallback(requestConfigBuilder ->
            requestConfigBuilder
                .setConnectTimeout(5000)       // 连接超时
                .setSocketTimeout(60000)       // 读取超时
        )
        .setHttpClientConfigCallback(httpClientBuilder ->
            httpClientBuilder
                .setMaxConnTotal(100)          // 最大连接数
                .setMaxConnPerRoute(50)        // 每个路由最大连接数
        )
        .build();

        RestClientTransport transport = new RestClientTransport(
            restClient,
            new JacksonJsonpMapper()
        );

        return new ElasticsearchClient(transport);
    }

    @PreDestroy
    public void destroy() throws Exception {
        // Spring 容器销毁时自动关闭
    }
}

10.2 异常处理

java 复制代码
@Service
public class ProductService {

    public Product getProduct(String id) {
        try {
            GetResponse<Product> response = client.get(g -> g
                .index("products")
                .id(id),
                Product.class
            );

            if (!response.found()) {
                throw new ProductNotFoundException("商品不存在: " + id);
            }

            return response.source();

        } catch (ElasticsearchException e) {
            if (e.status() == 404) {
                throw new ProductNotFoundException("索引不存在");
            }
            throw new ElasticsearchOperationException("ES 操作失败", e);
        } catch (IOException e) {
            throw new ElasticsearchConnectionException("ES 连接失败", e);
        }
    }
}

10.3 性能优化

java 复制代码
/**
 * 批量操作优化
 */
public void bulkInsertOptimized(List<Product> products) {
    int batchSize = 1000;  // 每批1000条
    List<List<Product>> batches = partition(products, batchSize);

    for (List<Product> batch : batches) {
        BulkRequest.Builder br = new BulkRequest.Builder();

        for (Product product : batch) {
            br.operations(op -> op
                .index(idx -> idx
                    .index("products")
                    .id(product.getId())
                    .document(product)
                )
            );
        }

        try {
            BulkResponse response = client.bulk(br.build());

            if (response.errors()) {
                // 处理部分失败的情况
                handleBulkErrors(response);
            }
        } catch (Exception e) {
            // 记录日志,继续下一批
            log.error("批量插入失败", e);
        }
    }
}

/**
 * 使用 Routing 提高查询性能
 */
public void indexWithRouting(Product product) throws Exception {
    client.index(i -> i
        .index("products")
        .id(product.getId())
        .routing(product.getBrand())  // 按品牌路由
        .document(product)
    );
}

public List<Product> searchWithRouting(String brand, String keyword) throws Exception {
    SearchResponse<Product> response = client.search(s -> s
        .index("products")
        .routing(brand)  // 只查询特定分片
        .query(q -> q
            .bool(b -> b
                .filter(f -> f.term(t -> t.field("brand").value(brand)))
                .must(m -> m.match(ma -> ma.field("title").query(keyword)))
            )
        ),
        Product.class
    );

    // ... 处理结果
}

10.4 日志与监控

java 复制代码
import lombok.extern.slf4j.Slf4j;

@Slf4j
@Service
public class ProductService {

    public Product getProduct(String id) {
        long startTime = System.currentTimeMillis();

        try {
            GetResponse<Product> response = client.get(g -> g
                .index("products")
                .id(id),
                Product.class
            );

            long elapsed = System.currentTimeMillis() - startTime;
            log.info("获取商品成功, ID: {}, 耗时: {}ms", id, elapsed);

            return response.source();

        } catch (Exception e) {
            long elapsed = System.currentTimeMillis() - startTime;
            log.error("获取商品失败, ID: {}, 耗时: {}ms", id, elapsed, e);
            throw new RuntimeException("获取商品失败", e);
        }
    }
}

10.5 单元测试

java 复制代码
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
class ProductServiceTest {

    @Autowired
    private ProductService productService;

    @Test
    void testSaveProduct() {
        Product product = new Product();
        product.setId("test_001");
        product.setTitle("测试商品");
        product.setPrice(99.0);

        String id = productService.saveProduct(product);
        assertNotNull(id);
        assertEquals("test_001", id);
    }

    @Test
    void testSearchProducts() {
        List<Product> products = productService.searchProducts("手机", 1, 10);
        assertNotNull(products);
        assertTrue(products.size() > 0);
    }
}

相关推荐
321茄子41 分钟前
MySQL 事务隔离性及锁
java·数据库·mysql
杀死那个蝈坦44 分钟前
UV 统计(独立访客统计)
java·jvm·spring·kafka·tomcat·maven
语落心生44 分钟前
流式数据湖Paimon探秘之旅 (七) 读取流程全解析
大数据
语落心生44 分钟前
流式数据湖Paimon探秘之旅 (二) 存储模型与文件组织
大数据
带刺的坐椅1 小时前
Solon AI 开发学习7 - chat - 四种消息类型及提示语增强
java·ai·llm·solon
n***78681 小时前
PostgreSQL 中进行数据导入和导出
大数据·数据库·postgresql
济宁雪人1 小时前
Java安全基础——序列化/反序列化
java·开发语言
1***Q7841 小时前
后端在微服务中的服务路由
java·数据库·微服务
语落心生1 小时前
流式数据湖Paimon探秘之旅 (四) FileStore存储引擎核心
大数据