1. 为什么选择High Level REST Client?
Elasticsearch作为当前最流行的分布式搜索和分析引擎,几乎成为了大数据处理的标配。而作为Java开发者,我们经常需要在应用中集成Elasticsearch的功能。这时候,High Level REST Client(以下简称HLRC)就成为了我们的得力助手。
你可能要问,为什么不用Transport Client?这个老前辈确实陪伴了我们很多年,但从Elasticsearch 7.0开始就被官方标记为废弃,8.0版本更是直接移除了它。HLRC作为接班人,不仅支持最新的REST API,还提供了更简洁的接口和更好的兼容性。
我最近在一个电商项目中使用了HLRC,感触颇深。比如商品搜索、日志分析、用户行为追踪等功能,HLRC都能轻松应对。它就像是一个贴心的翻译官,把我们熟悉的Java对象转换成Elasticsearch能理解的JSON,再把搜索结果转换回我们需要的Java对象。
2. 环境准备与基础配置
在开始编码之前,我们需要做好准备工作。首先确保你的项目中已经添加了Elasticsearch的依赖。这里我们使用Maven作为构建工具:
<!-- Elasticsearch High Level REST Client 依赖 -->
<dependency>
<groupId>org.elasticsearch.client</groupId>
<artifactId>elasticsearch-rest-high-level-client</artifactId>
<version>7.12.1</version>
</dependency>
注意版本号要与你的Elasticsearch服务端版本保持一致,否则可能会出现兼容性问题。我曾经就遇到过版本不一致导致的奇怪错误,排查了半天才发现是版本问题。
接下来,我们创建一个基础的客户端配置类:
import org.apache.http.HttpHost;
import org.elasticsearch.client.RestClient;
import org.elasticsearch.client.RestHighLevelClient;
public class ElasticsearchClientConfig {
// 创建并返回RestHighLevelClient实例
public static RestHighLevelClient createClient() {
// 配置Elasticsearch集群地址,可以配置多个节点实现负载均衡
RestHighLevelClient client = new RestHighLevelClient(
RestClient.builder(
new HttpHost("localhost", 9200, "http") // 默认端口9200
// 可以添加更多节点,如:
// , new HttpHost("localhost", 9201, "http")
)
);
return client;
}
// 关闭客户端的方法
public static void closeClient(RestHighLevelClient client) throws IOException {
if (client != null) {
client.close();
}
}
}
这个配置类做了两件事:创建客户端和关闭客户端。看似简单,但实际项目中,客户端的管理非常重要。记得一定要在使用完毕后关闭客户端,否则可能会导致资源泄露。
3. 索引操作实战
索引就像是Elasticsearch中的数据库,我们首先要学会如何创建和管理它们。让我们来看一个完整的索引操作示例:
import org.elasticsearch.action.admin.indices.create.CreateIndexRequest;
import org.elasticsearch.action.admin.indices.create.CreateIndexResponse;
import org.elasticsearch.action.admin.indices.delete.DeleteIndexRequest;
import org.elasticsearch.action.support.master.AcknowledgedResponse;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.xcontent.XContentType;
public class IndexOperations {
// 创建索引并设置映射
public static void createProductIndex() throws IOException {
// 获取客户端实例
RestHighLevelClient client = ElasticsearchClientConfig.createClient();
try {
// 创建索引请求
CreateIndexRequest request = new CreateIndexRequest("products");
// 索引配置:3个分片,2个副本
request.settings(Settings.builder()
.put("index.number_of_shards", 3)
.put("index.number_of_replicas", 2)
);
// 定义映射(相当于数据库表结构)
String mapping = "{\n" +
" \"properties\": {\n" +
" \"name\": {\"type\": \"text\", \"analyzer\": \"ik_max_word\"},\n" +
" \"price\": {\"type\": \"double\"},\n" +
" \"stock\": {\"type\": \"integer\"},\n" +
" \"category\": {\"type\": \"keyword\"},\n" +
" \"description\": {\"type\": \"text\", \"analyzer\": \"ik_smart\"},\n" +
" \"createTime\": {\"type\": \"date\", \"format\": \"yyyy-MM-dd HH:mm:ss||epoch_millis\"}\n" +
" }\n" +
"}";
// 添加映射到请求
request.mapping(mapping, XContentType.JSON);
// 执行创建索引操作
CreateIndexResponse response = client.indices().create(request, RequestOptions.DEFAULT);
// 检查是否创建成功
if (response.isAcknowledged()) {
System.out.println("索引创建成功");
} else {
System.out.println("索引创建失败");
}
} finally {
// 关闭客户端
ElasticsearchClientConfig.closeClient(client);
}
}
// 删除索引
public static void deleteIndex(String indexName) throws IOException {
RestHighLevelClient client = ElasticsearchClientConfig.createClient();
try {
DeleteIndexRequest request = new DeleteIndexRequest(indexName);
AcknowledgedResponse response = client.indices().delete(request, RequestOptions.DEFAULT);
if (response.isAcknowledged()) {
System.out.println("索引删除成功");
} else {
System.out.println("索引删除失败");
}
} finally {
ElasticsearchClientConfig.closeClient(client);
}
}
}
在这个示例中,我们创建了一个名为"products"的索引,定义了商品的各种字段类型。注意我们使用了IK分词器(需要在Elasticsearch中安装IK插件),这是中文搜索的利器。对于文本字段,我们根据需求选择了不同的分词器:ik_max_word用于名称(最细粒度拆分),ik_smart用于描述(智能拆分)。
4. 文档CRUD操作
有了索引,接下来就是文档操作了。文档相当于数据库中的记录,是Elasticsearch中的基本数据单元。让我们看看如何使用HLRC进行文档的增删改查:
import org.elasticsearch.action.delete.DeleteRequest;
import org.elasticsearch.action.delete.DeleteResponse;
import org.elasticsearch.action.get.GetRequest;
import org.elasticsearch.action.get.GetResponse;
import org.elasticsearch.action.index.IndexRequest;
import org.elasticsearch.action.index.IndexResponse;
import org.elasticsearch.action.update.UpdateRequest;
import org.elasticsearch.action.update.UpdateResponse;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.common.xcontent.XContentType;
public class DocumentCRUD {
// 添加文档
public static void addProduct() throws IOException {
RestHighLevelClient client = ElasticsearchClientConfig.createClient();
try {
// 准备商品数据
String json = "{\n" +
" \"name\": \"华为Mate40 Pro\",\n" +
" \"price\": 6999.00,\n" +
" \"stock\": 100,\n" +
" \"category\": \"手机\",\n" +
" \"description\": \"华为旗舰手机,搭载麒麟9000芯片\",\n" +
" \"createTime\": \"2023-05-10 10:00:00\"\n" +
"}";
// 创建索引请求
IndexRequest request = new IndexRequest("products")
.id("1") // 指定文档ID,如果不指定会自动生成
.source(json, XContentType.JSON);
// 执行请求
IndexResponse response = client.index(request, RequestOptions.DEFAULT);
System.out.println("文档ID: " + response.getId());
System.out.println("文档版本: " + response.getVersion());
} finally {
ElasticsearchClientConfig.closeClient(client);
}
}
// 获取文档
public static void getProduct(String id) throws IOException {
RestHighLevelClient client = ElasticsearchClientConfig.createClient();
try {
GetRequest request = new GetRequest("products", id);
GetResponse response = client.get(request, RequestOptions.DEFAULT);
if (response.isExists()) {
System.out.println("文档内容: " + response.getSourceAsString());
} else {
System.out.println("文档不存在");
}
} finally {
ElasticsearchClientConfig.closeClient(client);
}
}
// 更新文档
public static void updateProduct(String id) throws IOException {
RestHighLevelClient client = ElasticsearchClientConfig.createClient();
try {
// 准备更新内容
String json = "{\"price\": 6599.00, \"stock\": 80}";
UpdateRequest request = new UpdateRequest("products", id)
.doc(json, XContentType.JSON);
UpdateResponse response = client.update(request, RequestOptions.DEFAULT);
System.out.println("更新成功,新版本: " + response.getVersion());
} finally {
ElasticsearchClientConfig.closeClient(client);
}
}
// 删除文档
public static void deleteProduct(String id) throws IOException {
RestHighLevelClient client = ElasticsearchClientConfig.createClient();
try {
DeleteRequest request = new DeleteRequest("products", id);
DeleteResponse response = client.delete(request, RequestOptions.DEFAULT);
System.out.println("删除成功,文档ID: " + response.getId());
} finally {
ElasticsearchClientConfig.closeClient(client);
}
}
}
这些操作看起来简单,但有几个细节需要注意:
- 文档ID最好使用有意义的业务ID,而不是自动生成
- 更新操作是部分更新,只更新指定的字段
- Elasticsearch的文档是不可变的,更新实际上是删除旧文档+创建新文档
- 每个文档都有版本号,可以用来实现乐观锁
5. 高级搜索功能
Elasticsearch最强大的功能莫过于搜索了。让我们看看如何使用HLRC构建复杂的搜索请求:
import org.elasticsearch.action.search.SearchRequest;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.search.builder.SearchSourceBuilder;
import org.elasticsearch.search.sort.SortOrder;
public class ProductSearch {
// 多条件组合搜索
public static void searchProducts() throws IOException {
RestHighLevelClient client = ElasticsearchClientConfig.createClient();
try {
// 创建搜索请求
SearchRequest searchRequest = new SearchRequest("products");
// 构建搜索条件
SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
// 组合查询:名称包含"华为"且价格低于7000的商品
sourceBuilder.query(QueryBuilders.boolQuery()
.must(QueryBuilders.matchQuery("name", "华为"))
.must(QueryBuilders.rangeQuery("price").lte(7000))
);
// 添加排序:按价格降序
sourceBuilder.sort("price", SortOrder.DESC);
// 分页设置:第一页,每页10条
sourceBuilder.from(0);
sourceBuilder.size(10);
// 高亮显示
// HighlightBuilder highlightBuilder = new HighlightBuilder()
// .field("name")
// .preTags("<em>")
// .postTags("</em>");
// sourceBuilder.highlighter(highlightBuilder);
// 聚合分析:按类别分组统计
// sourceBuilder.aggregation(AggregationBuilders.terms("category_agg").field("category"));
// 将搜索条件添加到请求中
searchRequest.source(sourceBuilder);
// 执行搜索
SearchResponse response = client.search(searchRequest, RequestOptions.DEFAULT);
// 处理结果
response.getHits().forEach(hit -> {
System.out.println("命中文档: " + hit.getSourceAsString());
System.out.println("得分: " + hit.getScore());
});
// 处理聚合结果
// Terms categoryAgg = response.getAggregations().get("category_agg");
// categoryAgg.getBuckets().forEach(bucket -> {
// System.out.println("类别: " + bucket.getKeyAsString() + ", 数量: " + bucket.getDocCount());
// });
} finally {
ElasticsearchClientConfig.closeClient(client);
}
}
}
这个搜索示例展示了几个关键功能:
- 使用boolQuery组合多个查询条件
- 范围查询和匹配查询
- 排序和分页
- 高亮显示(注释部分)
- 聚合分析(注释部分)
在实际项目中,你可能还需要处理更复杂的场景,比如:
- 拼音搜索(需要安装拼音插件)
- 同义词扩展
- 搜索建议和自动补全
- 地理位置搜索
6. 批量操作与性能优化
当需要处理大量数据时,单条操作效率太低。Elasticsearch提供了批量操作API:
import org.elasticsearch.action.bulk.BulkRequest;
import org.elasticsearch.action.bulk.BulkResponse;
import org.elasticsearch.action.index.IndexRequest;
public class BulkOperations {
// 批量添加文档
public static void bulkAddProducts() throws IOException {
RestHighLevelClient client = ElasticsearchClientConfig.createClient();
try {
BulkRequest request = new BulkRequest();
// 添加多个文档到批量请求
request.add(new IndexRequest("products")
.id("101")
.source(XContentType.JSON,
"name", "iPhone 13",
"price", 5999.00,
"stock", 50,
"category", "手机",
"description", "苹果最新款手机",
"createTime", "2023-05-15 09:00:00"));
request.add(new IndexRequest("products")
.id("102")
.source(XContentType.JSON,
"name", "小米12 Pro",
"price", 4699.00,
"stock", 80,
"category", "手机",
"description", "小米旗舰手机",
"createTime", "2023-05-16 10:00:00"));
request.add(new IndexRequest("products")
.id("201")
.source(XContentType.JSON,
"name", "联想小新Pro16",
"price", 6499.00,
"stock", 30,
"category", "笔记本",
"description", "高性能轻薄本",
"createTime", "2023-05-17 11:00:00"));
// 执行批量操作
BulkResponse responses = client.bulk(request, RequestOptions.DEFAULT);
// 处理响应
if (responses.hasFailures()) {
System.out.println("批量操作部分失败: " + responses.buildFailureMessage());
} else {
System.out.println("批量操作全部成功");
}
} finally {
ElasticsearchClientConfig.closeClient(client);
}
}
}
批量操作可以显著提高性能,特别是在初始化数据或数据迁移时。但要注意:
- 批量大小要适中,通常1000-5000个文档一批比较合适
- 太大的批量会导致内存压力,太小则效率不高
- 可以多线程发送批量请求进一步提高吞吐量
7. 应用场景与技术选型
Elasticsearch + HLRC的组合适用于多种场景:
典型应用场景:
- 电商平台商品搜索(支持多条件筛选、排序、相关性评分)
- 内容管理系统全文检索(支持中文分词、高亮显示)
- 日志分析系统(结合Logstash和Kibana构成ELK栈)
- 用户行为分析(通过聚合分析用户行为模式)
- 地理位置搜索(如附近的人、附近的商家)
技术优势:
- 近实时搜索:文档索引后几乎立即可查
- 分布式架构:天然支持水平扩展
- 丰富的查询DSL:支持各种复杂查询需求
- 强大的聚合功能:支持多维数据分析
- 完善的Java API:HLRC提供了全面的操作接口
局限性:
- 不适合频繁更新的事务型场景
- 不适合作为主数据库使用(通常与关系型数据库配合)
- 资源消耗较大,特别是内存和磁盘IO
- 学习曲线较陡峭,特别是复杂查询和聚合
8. 注意事项与最佳实践
在使用HLRC时,有一些坑需要注意:
客户端管理:
- 客户端是线程安全的,应该重用而不是频繁创建销毁
- 使用完毕后必须关闭,否则会导致资源泄露
- 在生产环境中,建议使用连接池配置
错误处理:
- ElasticsearchClientException是大多数异常的基类
- 要处理ElasticsearchStatusException(如索引不存在)
- 重试策略对于临时性错误很有帮助
性能调优:
- 适当调整批量操作的大小
- 使用多线程提高吞吐量
- 考虑使用scroll API处理大量数据导出
版本兼容性:
- 客户端版本应与服务端版本一致
- 升级时要特别注意API的变化
安全配置:
- 生产环境一定要配置安全认证
- 考虑使用HTTPS而不是HTTP
- 限制客户端的访问权限
9. 总结与展望
通过本文的详细介绍,相信你已经掌握了使用High Level REST Client与Elasticsearch交互的核心技能。从基础的CRUD操作到复杂的搜索聚合,HLRC提供了全面的API支持Java开发者与Elasticsearch集群交互。
在实际项目中,Elasticsearch通常不是独立存在的,而是与Spring Boot、MyBatis等技术栈配合使用。比如,可以使用Spring Data Elasticsearch进一步简化操作,但它底层仍然是基于HLRC的。
未来,Elasticsearch可能会继续演进其Java客户端API,但REST客户端的地位应该会保持稳固。作为开发者,我们需要持续关注官方文档和社区动态,及时掌握最新的最佳实践。
评论