目录
- [Java 客户端介绍](#Java 客户端介绍)
- 环境搭建与连接
- 索引操作
- [文档操作 CRUD](#文档操作 CRUD)
- 查询操作
- 聚合操作
- 批量操作
- [Spring Boot 集成](#Spring Boot 集成)
- 高级特性
- 最佳实践
一、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();
}
}
优势:
- 完全控制:可以调用任何 ES API(包括实验性API)
- 轻量级:没有额外的抽象层
- 调试友好:直接看到 HTTP 请求/响应
- 向后兼容:ES 版本升级影响小
劣势:
- 手动构建 JSON:容易出错,字符串拼接繁琐
- 手动解析响应:需要自己处理 JSON
- 无类型安全:编译时无法检查错误
- 代码冗长:需要更多代码实现相同功能
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();
}
}
优势:
- API 友好:面向对象,易于使用
- 自动序列化:无需手动构建 JSON
- 部分类型安全:IDE 自动提示
- 功能完整:覆盖常用 ES 操作
劣势:
- 维护模式:ES 7.15+ 已标记废弃
- API 覆盖不全:部分新特性可能不支持
- 性能开销:有额外的对象创建
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 的场景:
- 调用未封装的 API
java
RestHighLevelClient highLevelClient = new RestHighLevelClient(...);
// 获取内部的 RestClient
RestClient lowLevelClient = highLevelClient.getLowLevelClient();
// 调用高级客户端不支持的 API
Request request = new Request("GET", "/_cluster/health");
Response response = lowLevelClient.performRequest(request);
- 性能敏感场景
java
// 批量操作时,直接使用 RestClient 可能更高效
RestClient client = RestClient.builder(...).build();
Request bulkRequest = new Request("POST", "/_bulk");
bulkRequest.setJsonEntity(bulkBody);
client.performRequest(bulkRequest);
- 调试和测试
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));
}
}
}
9.2 Search After(推荐替代 Scroll)
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);
}
}