1. 引言
1.1 Elasticsearch 简介
Elasticsearch 是一个基于 Lucene 的分布式搜索和分析引擎,提供实时数据搜索、聚合分析、分布式存储等功能。其设计目标是实现高可用、可扩展、易维护,广泛应用于日志分析、电商搜索、实时监控等场景。Java 作为企业级开发的主流语言,与 Elasticsearch 的集成非常成熟,官方提供了完善的 Java 客户端库。
1.2 Java 客户端优势
- 兼容性强:支持主流 Java 版本(Java 8+),适配 Spring、Spring Boot 等框架。
- 功能完整:涵盖文档操作、复杂查询、聚合分析、集群管理等全功能。
- 性能高效:通过连接池、批量操作等优化,满足高并发场景需求。
2. 环境准备
2.1 安装 Elasticsearch
2.1.1 下载与启动
- 从 Elasticsearch 官网 下载对应版本(本文以 8.9.0 为例)。
- 解压后执行启动命令:
- Linux/macOS:
./bin/elasticsearch
- Windows:
.\bin\elasticsearch.bat
- Linux/macOS:
- 验证启动:访问
http://localhost:9200
,返回包含集群信息的 JSON 数据。
2.1.2 配置文件
config/elasticsearch.yml
可配置集群名称、节点名称、网络绑定地址、端口等。
cluster:
name: my-elasticsearch-cluster
node:
name: node-1
network:
host: 0.0.0.0
http:
port: 9200
2.2 配置 Java 开发环境
2.2.1 引入依赖(Maven)
Elasticsearch 提供两种 Java 客户端:
- Transport Client:基于 TCP 协议,已被弃用(6.x 版本后不再推荐,8.x 版本移除)。
- REST Client:基于 HTTP 协议,推荐使用 高级 REST 客户端(RestHighLevelClient)。
添加 Maven 依赖:
<dependencies>
<!-- Elasticsearch 客户端 -->
<groupId>org.elasticsearch.client</groupId>
<artifactId>elasticsearch-rest-high-level-client</artifactId>
<version>8.9.0</version>
<!-- Elasticsearch 核心库(需与服务端版本一致) -->
<groupId>org.elasticsearch</groupId>
<artifactId>elasticsearch</artifactId>
<version>8.9.0</version>
<!-- HTTP 客户端(OkHttp 或 Apache HttpClient) -->
<groupId>org.apache.httpcomponents.client5</groupId>
<artifactId>httpclient5</artifactId>
<version>5.3</version>
</dependencies>
2.2.2 开发工具
- IDE:IntelliJ IDEA / Eclipse
- JDK:Java 8+(推荐 Java 11+)
3. 核心概念与术语
3.1 基本概念
概念 | 描述 |
---|---|
集群 (Cluster) | 由一个或多个节点组成,共享相同的集群名称,协同工作处理数据。 |
节点 (Node) | 集群中的单个服务器实例,可作为数据节点(存储数据)或协调节点(路由请求)。 |
索引 (Index) | 逻辑上的数据集,类似关系型数据库中的 “数据库”,由多个分片组成。 |
文档 (Document) | 索引中的基本数据单元,以 JSON 格式存储,类似关系型数据库中的 “行”。 |
类型 (Type) | 旧版本用于区分文档类别(6.x 版本后弃用,默认使用 _doc 类型)。 |
映射 (Mapping) | 定义索引中文档的字段类型、分词器、存储方式等元数据,类似表结构。 |
分片 (Shard) | 索引的物理拆分单元,分为主分片(Primary Shard)和副本分片(Replica Shard),提高吞吐量和可用性。 |
3.2 数据模型
- 文档以 JSON 格式存储,每个文档有唯一 ID(自动生成或自定义)。
- 映射支持丰富的数据类型:字符串(text、keyword)、数值(integer、double)、日期(date)、数组、对象等。
4. Java 客户端初始化
4.1 创建 RestHighLevelClient 实例
import org.apache.http.HttpHost;
import org.elasticsearch.client.RestClient;
import org.elasticsearch.client.RestHighLevelClient;
public class ElasticsearchClientDemo {
private static RestHighLevelClient client;
static {
// 配置集群节点(支持单个或多个节点)
RestClient.builder(
new HttpHost("localhost", 9200, "http"),
new HttpHost("node2", 9200, "http")
)
// 配置连接池(可选,默认配置适用于大多数场景)
.setRequestConfigCallback(requestConfigBuilder ->
requestConfigBuilder.setConnectTimeout(5000) // 连接超时时间(ms)
.setSocketTimeout(30000) // 套接字超时时间(ms)
)
.setHttpClientConfigCallback(httpClientBuilder ->
httpClientBuilder.setMaxConnTotal(100) // 最大连接数
.setDefaultCookieStore(null)
);
client = new RestHighLevelClient(
RestClient.builder(new HttpHost("localhost", 9200, "http"))
);
}
public static void closeClient() throws IOException {
if (client != null) {
client.close();
}
}
}
4.2 SSL 加密连接(HTTPS)
若 Elasticsearch 启用 HTTPS 访问,需配置证书:
import org.apache.http.ssl.SSLContexts;
import org.elasticsearch.client.RestClient;
import org.elasticsearch.client.RestHighLevelClient;
import java.io.File;
import java.security.KeyStore;
public class HttpsClientDemo {
public static void main(String[] args) throws Exception {
// 加载 CA 证书
KeyStore trustStore = KeyStore.getInstance(KeyStore.getDefaultType());
trustStore.load(new File("path/to/ca.crt"), "password".toCharArray());
RestClient.builder(new HttpHost("localhost", 9243, "https"))
.setHttpClientConfigCallback(httpClientBuilder -> {
httpClientBuilder.setSSLContext(SSLContexts.custom()
.loadTrustMaterial(trustStore, null)
.build());
return httpClientBuilder;
});
RestHighLevelClient client = new RestHighLevelClient(
RestClient.builder(new HttpHost("localhost", 9243, "https"))
);
}
}
5. 数据操作:增删改查
5.1 索引文档(Create/Index)
5.1.1 自定义文档 ID
import org.elasticsearch.action.index.IndexRequest;
import org.elasticsearch.action.index.IndexResponse;
import org.elasticsearch.common.xcontent.XContentType;
public class IndexDocument {
public static void main(String[] args) throws IOException {
// 创建文档数据(JSON 格式)
String docJson = "{\n" +
" \"id\": 1001,\n" +
" \"name\": \"Elasticsearch 实战\",\n" +
" \"price\": 99.9,\n" +
" \"tags\": [\"搜索\", \"大数据\"]\n" +
"}";
// 构建 IndexRequest(指定索引名、文档类型(固定为 _doc)、文档 ID)
IndexRequest request = new IndexRequest("products", "_doc", "1001")
.source(docJson, XContentType.JSON);
// 执行索引操作
IndexResponse response = client.index(request, RequestOptions.DEFAULT);
// 解析响应
System.out.println("索引名: " + response.getIndex());
System.out.println("文档 ID: " + response.getId());
System.out.println("版本: " + response.getVersion());
System.out.println("结果: " + response.getResult()); // CREATED 或 UPDATED
}
}
5.1.2 自动生成文档 ID
IndexRequest request = new IndexRequest("products", "_doc")
.source(docJson, XContentType.JSON);
5.2 查询文档(Get)
import org.elasticsearch.action.get.GetRequest;
import org.elasticsearch.action.get.GetResponse;
public class GetDocument {
public static void main(String[] args) throws IOException {
GetRequest request = new GetRequest("products", "_doc", "1001");
// 可选:指定返回字段(避免返回全量数据)
request.fetchSourceContext(new FetchSourceContext(false))
.storedFields("_none_"); // 不返回任何字段,仅获取元数据
GetResponse response = client.get(request, RequestOptions.DEFAULT);
if (response.isExists()) {
String sourceAsString = response.getSourceAsString(); // JSON 字符串
Map<String, Object> sourceAsMap = response.getSourceAsMap(); // Map 结构
System.out.println("文档存在,内容:" + sourceAsString);
} else {
System.out.println("文档不存在");
}
}
}
5.3 更新文档(Update)
5.3.1 全量更新(替换文档)
直接使用 IndexRequest
,若文档 ID 已存在则覆盖。
5.3.2 部分更新(推荐)
import org.elasticsearch.action.update.UpdateRequest;
import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.common.xcontent.XContentFactory;
public class UpdateDocument {
public static void main(String[] args) throws IOException {
UpdateRequest request = new UpdateRequest("products", "_doc", "1001");
// 构建更新脚本(使用 JSON 或脚本语言,如 Painless)
XContentBuilder builder = XContentFactory.jsonBuilder();
builder.startObject()
.field("doc")
.startObject()
.field("price", 109.9)
.endObject()
.endObject();
request.doc(builder);
client.update(request, RequestOptions.DEFAULT);
}
}
5.4 删除文档(Delete)
import org.elasticsearch.action.delete.DeleteRequest;
import org.elasticsearch.action.delete.DeleteResponse;
public class DeleteDocument {
public static void main(String[] args) throws IOException {
DeleteRequest request = new DeleteRequest("products", "_doc", "1001");
DeleteResponse response = client.delete(request, RequestOptions.DEFAULT);
System.out.println("结果: " + response.getResult()); // DELETED 或 NOT_FOUND
}
}
6. 复杂查询:从简单到高级
6.1 查询构建器(Query Builders)
Elasticsearch 提供丰富的查询类型,通过 QueryBuilders
类构建。
6.1.1 术语查询(Term Query)
精确匹配单个字段(不分词):
import org.elasticsearch.index.query.TermQueryBuilder;
TermQueryBuilder query = QueryBuilders.termQuery("tags", "搜索");
6.1.2 布尔查询(Bool Query)
组合多个查询条件(must、filter、should、mustNot):
import org.elasticsearch.index.query.BoolQueryBuilder;
BoolQueryBuilder boolQuery = QueryBuilders.boolQuery()
.must(QueryBuilders.termQuery("category", "书籍")) // 必须匹配
.filter(QueryBuilders.rangeQuery("price").lte(200)) // 过滤(不计算相关性)
.should(QueryBuilders.termQuery("author", "张三")) // 可选匹配(提升分数)
.mustNot(QueryBuilders.termQuery("status", "下架")); // 必须不匹配
6.1.3 全文搜索(Match Query)
对文本字段进行分词后匹配:
import org.elasticsearch.index.query.MatchQueryBuilder;
MatchQueryBuilder matchQuery = QueryBuilders.matchQuery("description", "分布式搜索");
6.1.4 模糊查询(Fuzzy Query)
允许拼写错误的近似匹配:
import org.elasticsearch.index.query.FuzzyQueryBuilder;
FuzzyQueryBuilder fuzzyQuery = QueryBuilders.fuzzyQuery("name", "Elastcsearch")
.fuzziness(Fuzziness.AUTO); // 自动计算编辑距离
6.2 执行搜索请求
import org.elasticsearch.action.search.SearchRequest;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.search.SearchHit;
import org.elasticsearch.search.SearchHits;
import org.elasticsearch.search.builder.SearchSourceBuilder;
public class SearchDocuments {
public static void main(String[] args) throws IOException {
// 构建搜索请求
SearchRequest searchRequest = new SearchRequest("products");
SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
// 设置查询条件
sourceBuilder.query(QueryBuilders.termQuery("tags", "大数据"));
// 设置返回字段(排除不需要的字段)
sourceBuilder.fetchSource(new String[]{"name", "price"}, new String[]{"tags"});
// 设置分页(默认从第 0 页开始,每页 10 条)
sourceBuilder.from(0);
sourceBuilder.size(20);
// 设置排序
sourceBuilder.sort("price", SortOrder.DESC);
searchRequest.source(sourceBuilder);
// 执行搜索
SearchResponse searchResponse = client.search(searchRequest, RequestOptions.DEFAULT);
// 解析结果
SearchHits hits = searchResponse.getHits();
System.out.println("总命中数: " + hits.getTotalHits().value);
for (SearchHit hit : hits) {
String docId = hit.getId();
Map<String, Object> sourceAsMap = hit.getSourceAsMap();
System.out.println("文档 ID: " + docId + ", 内容: " + sourceAsMap);
}
}
}
6.3 聚合分析(Aggregation)
6.3.1 术语聚合(Terms Aggregation):分组统计
import org.elasticsearch.search.aggregations.AggregationBuilders;
import org.elasticsearch.search.aggregations.bucket.terms.Terms;
SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
sourceBuilder.aggregation(AggregationBuilders.terms("tag_agg")
.field("tags")
.size(10) // 最多返回 10 个桶
.order(Terms.Order.count(false)) // 降序排列
);
SearchResponse response = client.search(new SearchRequest("products").source(sourceBuilder), RequestOptions.DEFAULT);
Terms tagAgg = response.getAggregations().get("tag_agg");
for (Terms.Bucket bucket : tagAgg.getBuckets()) {
System.out.println("标签: " + bucket.getKeyAsString() + ", 数量: " + bucket.getDocCount());
}
6.3.2 数值聚合(Max/Avg/Min):统计价格最大值
sourceBuilder.aggregation(AggregationBuilders.max("max_price").field("price"));
Max maxPrice = response.getAggregations().get("max_price");
System.out.println("最高价格: " + maxPrice.getValue());
7. 高级功能与最佳实践
7.1 批量操作(Bulk API)
批量处理大量文档(显著提升性能):
import org.elasticsearch.action.bulk.BulkRequest;
import org.elasticsearch.action.bulk.BulkResponse;
public class BulkOperations {
public static void main(String[] args) throws IOException {
BulkRequest bulkRequest = new BulkRequest();
// 添加多个索引/删除请求
for (int i = 1002; i <= 1010; i++) {
String docJson = "{\"id\":" + i + ", \"name\":\"产品" + i + "\"}";
IndexRequest indexRequest = new IndexRequest("products", "_doc", String.valueOf(i))
.source(docJson, XContentType.JSON);
bulkRequest.add(indexRequest);
}
BulkResponse bulkResponse = client.bulk(bulkRequest, RequestOptions.DEFAULT);
// 处理错误(如有)
if (bulkResponse.hasFailures()) {
bulkResponse.getItems().forEach(item -> {
if (item.getIndex() != null && item.getIndex().isFailed()) {
System.out.println("索引失败: " + item.getIndex().getFailureMessage());
}
});
}
}
}
7.2 滚动查询(Scroll Query)
处理超过分页限制的大量数据(单次查询最多返回 10000 条,滚动查询可突破限制):
import org.elasticsearch.search.Scroll;
public class ScrollSearch {
public static void main(String[] args) throws IOException {
Scroll scroll = new Scroll(TimeValue.timeValueMinutes(1)); // 滚动保持时间
SearchRequest searchRequest = new SearchRequest("large_index");
SearchSourceBuilder sourceBuilder = new SearchSourceBuilder()
.query(QueryBuilders.matchAllQuery())
.size(1000); // 每页大小
searchRequest.source(sourceBuilder).scroll(scroll);
SearchResponse response = client.search(searchRequest, RequestOptions.DEFAULT);
String scrollId = response.getScrollId();
SearchHit[] hits = response.getHits().getHits();
while (hits != null && hits.length > 0) {
// 处理当前页数据
for (SearchHit hit : hits) {
// 业务逻辑
}
// 滚动获取下一页
SearchScrollRequest scrollRequest = new SearchScrollRequest(scrollId);
scrollRequest.scroll(scroll);
response = client.scroll(scrollRequest, RequestOptions.DEFAULT);
scrollId = response.getScrollId();
hits = response.getHits().getHits();
}
// 清除滚动上下文(释放资源)
ClearScrollRequest clearScrollRequest = new ClearScrollRequest();
clearScrollRequest.addScrollId(scrollId);
client.clearScroll(clearScrollRequest, RequestOptions.DEFAULT);
}
}
7.3 映射管理
7.3.1 创建索引时定义映射
import org.elasticsearch.client.indices.CreateIndexRequest;
import org.elasticsearch.client.indices.CreateIndexResponse;
import org.elasticsearch.common.xcontent.XContentFactory;
public class CreateIndexWithMapping {
public static void main(String[] args) throws IOException {
CreateIndexRequest request = new CreateIndexRequest("blogs");
// 定义映射
String mapping = "{\n" +
" \"mappings\": {\n" +
" \"properties\": {\n" +
" \"title\": {\"type\": \"text\", \"analyzer\": \"ik_max_word\"},\n" + // 中文分词器
" \"author\": {\"type\": \"keyword\"},\n" + // 精确匹配
" \"publish_date\": {\"type\": \"date\", \"format\": \"yyyy-MM-dd HH:mm:ss\"}\n" +
" }\n" +
" }\n" +
"}";
request.mapping(mapping, XContentType.JSON);
CreateIndexResponse response = client.indices().create(request, RequestOptions.DEFAULT);
if (response.isAcknowledged()) {
System.out.println("索引创建成功");
}
}
}
7.3.2 更新映射(仅支持新增字段,不能修改已有字段类型)
import org.elasticsearch.client.indices.PutMappingRequest;
PutMappingRequest request = new PutMappingRequest("blogs")
.type("_doc")
.source("{\"properties\": {\"new_field\": {\"type\": \"text\"}}}", XContentType.JSON);
client.indices().putMapping(request, RequestOptions.DEFAULT);
7.4 别名与模板
7.4.1 索引别名(Alias)
为索引创建别名,支持动态切换底层索引(如日志按天分索引时使用 logs-*
别名):
import org.elasticsearch.client.indices.AliasRequest;
import org.elasticsearch.client.indices.PutAliasRequest;
PutAliasRequest request = new PutAliasRequest("current_products")
.addAlias(new AliasRequest.Alias("current_products").index("products_2025"));
client.indices().putAlias(request, RequestOptions.DEFAULT);
7.4.2 模板(Template)
预定义索引映射和设置,用于自动创建索引时应用规则(如日志索引模板):
import org.elasticsearch.client.indices.PutIndexTemplateRequest;
String template = "{\n" +
" \"index_patterns\": [\"logs-*\"],\n" +
" \"mappings\": {\n" +
" \"properties\": {\n" +
" \"message\": {\"type\": \"text\", \"analyzer\": \"logstash_analyzer\"}\n" +
" }\n" +
" }\n" +
"}";
PutIndexTemplateRequest request = new PutIndexTemplateRequest("log_template")
.source(template, XContentType.JSON);
client.indices().putTemplate(request, RequestOptions.DEFAULT);
8. 集成 Spring Boot
8.1 添加依赖
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-elasticsearch</artifactId>
</dependency>
<!-- 排除旧版客户端,使用官方 REST 客户端 -->
<exclusion>
<groupId>org.elasticsearch.client</groupId>
<artifactId>elasticsearch-rest-client</artifactId>
</exclusion>
</dependencies>
8.2 配置文件(application.yml)
spring:
data:
elasticsearch:
cluster-name: my-elasticsearch-cluster
cluster-nodes: localhost:9300 # Transport Client 端口(旧版,8.x 不推荐)
rest:
uris: http://localhost:9200 # 推荐使用 REST 客户端配置
8.3 定义实体类
import org.springframework.data.elasticsearch.annotations.Document;
import org.springframework.data.elasticsearch.annotations.Field;
import org.springframework.data.elasticsearch.annotations.FieldType;
@Document(indexName = "products", type = "_doc", shards = 2, replicas = 1)
public class Product {
@Id
private String id;
@Field(type = FieldType.Text, analyzer = "ik_max_word")
private String name;
@Field(type = FieldType.Keyword)
private String category;
// Getter/Setter
}
8.4 定义 Repository
import org.springframework.data.elasticsearch.repository.ElasticsearchRepository;
public interface ProductRepository extends ElasticsearchRepository<Product, String> {
// 自定义查询方法(基于方法名解析)
List<Product> findByNameContaining(String keyword);
// 复杂查询使用 @Query 注解
@Query("{\"bool\": {\"must\": {\"term\": {\"category\": \"?0\"}}}}")
List<Product> findByCategory(String category);
}
8.5 服务层使用
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.stereotype.Service;
@Service
public class ProductService {
@Autowired
private ProductRepository repository;
public Product save(Product product) {
return repository.save(product);
}
public Page<Product> search(String keyword, int page, int size) {
return repository.findByNameContaining(keyword, PageRequest.of(page, size));
}
}
9. 性能优化
9.1 客户端配置优化
- 连接池调优:根据并发量设置
maxConnTotal
(建议 100-200)和maxConnPerRoute
(建议 50-100)。 - 超时控制:合理设置
connectTimeout
(连接超时)和socketTimeout
(响应超时),避免长耗时阻塞。
9.2 索引设计优化
- 映射优化:
- 对不需要搜索的字段设置
index: false
。 - 对精确匹配字段使用
keyword
类型,全文搜索字段使用text
并指定分词器。
- 对不需要搜索的字段设置
- 分片规划:
- 主分片数在创建索引时确定,建议单个分片大小控制在 10-50GB。
- 副本分片数根据集群节点数和可用性需求设置(如 1 个副本提高容灾能力)。
9.3 查询优化
- 避免深分页:使用
from + size
分页时,深度分页(如from=10000
)性能低下,建议改用滚动查询或搜索后处理。 - 合理使用过滤(Filter):对不计算相关性的查询(如范围查询)使用
filter
而非must
,利用缓存提升性能。 - 批量操作:使用
Bulk API
处理批量写入,减少网络开销。
9.4 集群调优
- 节点角色分离:将节点分为数据节点、协调节点、机器学习节点,避免混合部署影响性能。
- JVM 内存配置:设置
ES_JAVA_OPTS="-Xms8g -Xmx8g"
,内存大小不超过物理内存的 50%,且不超过 32GB(避免压缩指针失效)。
10. 错误处理与最佳实践
10.1 异常处理
Elasticsearch 客户端抛出 ElasticsearchException
及其子类,需捕获并处理:
try {
// 客户端操作
} catch (ElasticsearchStatusException e) { // 处理 HTTP 状态码异常(如 404、400)
int statusCode = e.statusCode();
String errorMessage = e.getMessage();
// 业务逻辑处理
} catch (IOException e) { // 处理网络连接异常
// 重试或记录日志
}
10.2 日志管理
配置客户端日志输出(如使用 Logback),记录关键操作和异常:
<logger name="org.elasticsearch.client" level="WARN"/>
<logger name="org.apache.http" level="INFO"/>
10.3 版本兼容性
- 确保客户端版本与 Elasticsearch 服务端版本完全一致(主版本号必须相同,次版本号建议兼容)。
- 避免跨大版本升级(如 7.x 到 8.x),需参考官方升级指南。
10.4 安全配置
- 认证授权:启用 Elasticsearch 的 X-Pack 安全模块,使用用户名密码或 API 密钥认证。
- HTTPS 加密:客户端与服务端通信使用 HTTPS,防止数据泄露。
11. 总结
本文系统介绍了 Java 集成 Elasticsearch 的核心技术,包括环境搭建、客户端初始化、数据操作、复杂查询、高级功能、Spring Boot 集成、性能优化和最佳实践。Elasticsearch 的 Java 客户端提供了强大且灵活的接口,能够满足从简单搜索到复杂数据分析的各种需求。在实际开发中,需根据业务场景合理设计索引结构,优化查询性能,并结合集群管理确保高可用性。
附录:常用工具与资源
-
Elasticsearch 官方文档:Java REST Client [7.17] | Elastic
-
Java 客户端 API 文档:Java High Level REST Client | Java REST Client [7.17] | Elastic
-
分词器工具:IK 分词器(中文)、Standard 分词器(英文)
-
集群监控:Elasticsearch Head 插件、Kibana 监控面板