ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Elasticsearch 8.17.1 Java工具类:封装连接、索引与文档操作

Elasticsearch 8.17.1 Java工具类:封装连接、索引与文档操作 1. 项目背景Elasticsearch 8.17.1 Java工具类到底要解决什么做后端久了就会发现Elasticsearch 客户端从来不是“引一个依赖就能跑”那么简单。我自己最早接触的还是在 7.x 时代用 High Level REST Client后来项目升级到 8.x一打开代码全是红兮兮的 deprecated 和删掉的类光是把建索引、写入、查询这些重复操作从旧客户端迁移到新客户端就花了一个下午。这次我整理了一套 Elasticsearch 8.17.1 JAVA工具类目的很直接把 Elasticsearch Java API Client 的连接创建、索引管理、文档读写、结果映射和异常处理全部收敛到一个工具类里让业务代码只关注“我要查什么”不关心“客户端怎么来的”。这个工具类适合谁来抄主要有三类人第一项目刚起步想在 Java 服务里接入 Elasticsearch 8.x 的开发者第二正在从 7.x 迁移到 8.x被一堆 Builder 写法逼疯的维护者第三团队里多个微服务都要连同一个 ES 集群想统一管理连接参数和操作入口的人。如果你只是偶尔在命令行里用 curl 调 ES那这个工具类属于过度设计但只要你写的是 Java 后端它就值得保留。不要小看一个工具类的价值。Elasticsearch 8.17.1 的服务端功能再强大落在 Java 侧最后还是十几个静态方法的事。封装不到位每个 Service 里都能看到new RestClientTransport这种初始化代码一旦集群地址变更或者证书要换你就要全局搜项目去改。封装到位之后配置外置、连接复用、错误提示统一后续接日志、接入商品检索、接报表统计都是同一个入口。这篇博文我就把完整的设计思路、代码实现、踩坑记录都拆开讲一遍希望能让你少走弯路。1.1 项目需求拆解工具类该管多少事既然叫 Elasticsearch 8.17.1 JAVA工具类就不能只是一个返回ElasticsearchClient的 getter。我整理需求时把它拆成四个层次第一是连接管理。要能从配置文件读取地址、用户名、密码、超时时间延迟到第一次使用才初始化客户端并且保证整个进程内只有一个ElasticsearchClient实例。第二是索引生命周期。要能检查索引是否存在、创建索引、删除索引。创建索引时最好支持简单的字段 mapping 配置而不是把所有 mapping 都硬编码在业务代码里。第三是文档操作。单条写入、批量写入、根据 id 更新、根据 id 删除这几个接口最常用。批量写入还要能处理部分成功的情况不能因为几条脏数据就让整个请求看起来失败。第四是查询封装。至少要提供简单的 match_all 查询和带条件的 bool 组合查询。查询结果要能直接映射成 POJO返回SearchResponseT让调用方决定怎么处理分页和排序。这四个层次覆盖了我在实际项目里 90% 以上的 ES 操作。其他像聚合、scroll、reindex 这些用法不需要塞进工具类里否则工具类会膨胀成一个大杂烩反而不容易维护。工具类只做高频、稳定的基础能力特殊场景让调用方直接拿底层的ElasticsearchClient去扩展这是我认为最合理的设计边界。1.2 为什么要选官方 Java API Client而不是继续用旧客户端Elasticsearch 官方从 7.x 中期就开始推新的 Java API Client工包名是co.elastic.clients:elasticsearch-java。很多人还在用老的 High Level REST Client觉得 API 顺手、资料多但在 8.0 版本里这个客户端已经被官方宣告寿终正寝所以新项目必须切换到新客户端。新客户端最大的变化是全面 Builder 化。以前写查询要么传 JSON 字符串要么构建一堆 Request 对象现在几乎所有请求都靠XxxRequest.Builder链式调用IDE 提示非常友好写错字段名在编译期就会报错。另外一个很重要的改进是结果映射不再依赖老旧的XContentParser而是直接支持 Jackson 和 JSON-P 两种映射器我们可以配置JacksonJsonpMapper把响应自动反序列化成自己的业务 POJO。版本选择上我这次用的是 8.17.1。Java 客户端版本最好和服务端版本保持一致至少不要差一个大版本。如果服务端是 8.17.xJava 客户端用 8.17.1 是最稳妥的避免出现某些新字段解析不了或者序列化兼容问题。2. 环境准备与依赖引入写代码之前先把依赖和环境讲清楚。Elasticsearch 8.17.1 对 Java 版本有明确要求官方推荐 Java 17 以上我自己在项目里用的是 Java 17。如果你还在 Java 8 上那确实不建议升级到 8.x 客户端因为很多 8.x 代码已经用到 var 和记录类等新特性强行降级会非常痛苦。2.1 Maven 依赖与版本对齐用 Maven 管理依赖最简单在pom.xml里加上下面这些坐标properties elasticsearch.version8.17.1/elasticsearch.version /properties dependencies dependency groupIdco.elastic.clients/groupId artifactIdelasticsearch-java/artifactId version${elasticsearch.version}/version /dependency dependency groupIdcom.fasterxml.jackson.core/groupId artifactIdjackson-databind/artifactId version2.17.2/version /dependency /dependencies如果用的是 Gradle对应写法是这样implementation co.elastic.clients:elasticsearch-java:8.17.1 implementation com.fasterxml.jackson.core:jackson-databind:2.17.2为什么一定要加jackson-databind因为新客户端默认情况下会去 classpath 里找JacksonJsonpMapper没有 Jackson 的时候它可能退回到 JSON-P 实现但这个实现不是你主动引入的很容易在运行时报JsonpMappingException。加上jackson-databind之后客户端会优先使用 Jackson 完成序列化和反序列化对咱们中国人习惯写的 POJO 最友好日期格式、字段别名都可以直接贴 Jackson 注解。2.2 配置文件把可变参数全部外置客户端地址、用户名、密码、超时时间这些都属于环境相关配置绝对不能硬编码在 Java 类里。我在src/main/resources下放了一个es.propertieses.hostshttp://localhost:9200,http://10.0.0.8:9200 es.usernameelastic es.passwordchangeit es.connectTimeout5000 es.socketTimeout60000 es.sslfalse多个节点用英文逗号分隔工具类会解析成 HttpHost 数组。生产环境我一般还会加es.index.prefix这样的公共前缀但为了代码演示简洁这里先只保留最核心的配置项。es.ssl这个开关很重要如果集群开了 HTTPS后续构建客户端时就要走 SSLContext。2.3 构建客户端实例懒加载单例Elasticsearch Java API Client 的实例是线程安全的官方也建议一个进程只创建一个客户端复用。我用静态内部类懒加载的方式实现单例既避免在类加载时就把连接参数初始化又不会引入额外依赖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.config.RequestConfig; import org.apache.http.impl.client.BasicCredentialsProvider; import org.elasticsearch.client.RestClient; import org.elasticsearch.client.RestClientBuilder; import java.io.IOException; import java.io.InputStream; import java.util.Arrays; import java.util.Properties; public final class EsClientUtil { private EsClientUtil() { } public static ElasticsearchClient client() { return Holder.CLIENT; } private static class Holder { private static final RestClient REST_CLIENT buildRestClient(); private static final ElasticsearchTransport TRANSPORT new RestClientTransport(REST_CLIENT, new JacksonJsonpMapper()); private static final ElasticsearchClient CLIENT new ElasticsearchClient(TRANSPORT); } private static RestClient buildRestClient() { Properties props loadProperties(); HttpHost[] hosts Arrays.stream(props.getProperty(es.hosts).split(,)) .map(HttpHost::create) .toArray(HttpHost[]::new); String username props.getProperty(es.username, ); String password props.getProperty(es.password, ); int connectTimeout Integer.parseInt(props.getProperty(es.connectTimeout, 5000)); int socketTimeout Integer.parseInt(props.getProperty(es.socketTimeout, 60000)); RestClientBuilder builder RestClient.builder(hosts); builder.setHttpClientConfigCallback(http - { if (username ! null !username.isBlank()) { BasicCredentialsProvider credentials new BasicCredentialsProvider(); credentials.setCredentials(AuthScope.ANY, new UsernamePasswordCredentials(username, password)); http.setDefaultCredentialsProvider(credentials); } http.setDefaultRequestConfig(RequestConfig.custom() .setConnectTimeout(connectTimeout) .setSocketTimeout(socketTimeout) .build()); return http; }); return builder.build(); } private static Properties loadProperties() { Properties props new Properties(); try (InputStream in EsClientUtil.class.getClassLoader() .getResourceAsStream(es.properties)) { if (in null) { throw new IllegalStateException(es.properties not found); } props.load(in); } catch (IOException e) { throw new IllegalStateException(load es.properties failed, e); } return props; } public static void close() { try { Holder.TRANSPORT.close(); } catch (IOException e) { throw new IllegalStateException(close es transport failed, e); } } }这段代码里有个容易被忽略的点RestClientTransport包装了低层RestClient关闭时只要关闭TRANSPORT即可它会连带关闭底层连接池。如果你只调用了REST_CLIENT.close()大概率会留下半开连接所以别把两个 close 都写上。3. 工具类核心方法拆解客户端初始化只是第一步真正干活的是后面这些操作封装。写工具类不是在每个方法里 new 一个 client而是把EsClientUtil.client()当成全局入口。下面我把四类高频操作逐一拆开每段代码都给出可以直接抄的版本。3.1 索引操作exists、create、delete项目里关于索引的常见需求是先判断索引是否存在不存在就创建最后可能还需要删掉测试索引。所以工具类里最基础的方法是existsIndex、createIndex、deleteIndex。import co.elastic.clients.elasticsearch.indices.CreateIndexResponse; import co.elastic.clients.elasticsearch.indices.DeleteIndexResponse; import co.elastic.clients.elasticsearch.indices.IndexSettings; import co.elastic.clients.elasticsearch._types.ElasticsearchException; import java.io.IOException; public static boolean existsIndex(String index) throws IOException { return EsClientUtil.client().indices().exists(e - e.index(index)).value(); } public static boolean createIndex(String index) throws IOException { if (existsIndex(index)) { return false; } try { CreateIndexResponse response EsClientUtil.client().indices() .create(c - c.index(index) .settings(s - s .numberOfShards(3) .numberOfReplicas(1))); return response.acknowledged(); } catch (ElasticsearchException e) { if (e.response() ! null e.response().status() 409) { return false; } throw e; } } public static boolean deleteIndex(String index) throws IOException { if (!existsIndex(index)) { return false; } DeleteIndexResponse response EsClientUtil.client().indices() .delete(d - d.index(index)); return response.acknowledged(); }这里有几个细节必须说清楚。exists返回的不是 boolean 而是BooleanResponse所以一定要.value()。createIndex方法里我先判断 exists 再创建但这并不能完全避免并发竞争两个实例同时创建同一个索引时后面的请求会收到 409 Resource Already Exists所以我额外捕获了ElasticsearchException通过response().status() 409识别这个场景保证方法在并发环境下不会线上炸一场。numberOfShards(3)我故意写成字符串因为 ES 8.x 的 API 允许字符串和整数两种形式。生产环境的分片数必须根据数据量和节点数提前规划写死一个工具类默认值只是兜底别期待它是万能答案。3.2 单条文档操作index、update、delete文档写入是使用频率最高的操作。工具类里indexDocument接收业务文档和可选的 id如果有 id 就是指定文档 id 写入没有就让 ES 自动生成。更新和删除都按 id 操作import co.elastic.clients.elasticsearch.core.IndexResponse; import co.elastic.clients.elasticsearch.core.UpdateResponse; import co.elastic.clients.elasticsearch.core.DeleteResponse; public static T IndexResponse indexDocument(String index, String id, T doc) throws IOException { return EsClientUtil.client().index(i - { i.index(index); if (id ! null !id.isBlank()) { i.id(id); } i.document(doc); return i; }); } public static T UpdateResponseT updateDocument(String index, String id, T doc) throws IOException { return EsClientUtil.client().update(u - u .index(index) .id(id) .doc(doc), Object.class); } public static DeleteResponse deleteDocument(String index, String id) throws IOException { return EsClientUtil.client().delete(d - d .index(index) .id(id)); }泛型方法在工具类里非常受欢迎因为你不需要为每个实体类都写一份重复代码。比如商品类是Product日志类是AccessLog直接调用同一个方法就能完成序列化。这里要注意的是UpdateResponseT的泛型其实表示的是 doc 类型但实际返回值里很少用到它所以我统一传Object.class。写入之后如果需要立刻搜索要注意 ES 的refresh策略。默认情况下写入的数据要等 1 秒才会被搜索到调用方如果强依赖“写后立即读”就会踩坑。所以我在indexDocument的注释里建议对实时性要求高的接口可以给 index 方法后加刷新参数或者在调用方等待一下。这一点我在后文踩坑实录里会再展开。3.3 批量写入必须检查每个 item 的错误批量写入是性能提升最明显的操作。一条条 index 的性能非常差一次 bulk 写几千条在大数据量下能快一个数量级。工具类里我封装了一个bulkIndeximport co.elastic.clients.elasticsearch.core.BulkRequest; import co.elastic.clients.elasticsearch.core.BulkResponse; import co.elastic.clients.elasticsearch.core.bulk.BulkResponseItem; public static T boolean bulkIndex(String index, ListT docs) throws IOException { if (docs null || docs.isEmpty()) { return true; } BulkRequest.Builder br new BulkRequest.Builder(); for (T doc : docs) { br.operations(op - op .index(idx - idx .index(index) .document(doc))); } BulkResponse response EsClientUtil.client().bulk(br.build()); if (response.errors()) { for (BulkResponseItem item : response.items()) { if (item.error() ! null) { System.err.println(bulk item error: item.error().reason()); } } return false; } return true; }这段代码的重点不是构造 bulk而是返回后的错误检查。response.errors()只能说明这批数据里有失败项但你仍然可能拿到 HTTP 200。真正重要的是遍历response.items()看每个 item 有没有error。实际业务中我见过有人只看response.errors()就放弃排查结果脏数据一直悄悄丢失。批量写入还有两个注意事项。第一数据量大时不要一个 List 全塞进去建议每 1000 条为一个批次提交避免单个 HTTP 请求体过大。第二如果写入失败想重试最好定位到失败 item 的 id 或者原始文档而不是把整个 bulk 重新发一遍否则会把成功的也重复写一遍。3.4 查询封装从 match_all 到 bool 组合查询查询封装是工具类里最容易被滥用的一部分。如果你想封装一个“万能查询器”那这个工具类会变成一把瑞士军刀最后没人敢用。我的策略是只封装两个入口一个无条件的searchAll一个暴露 Query.Builder 可以自由发挥的search。import co.elastic.clients.elasticsearch.core.SearchResponse; public static T SearchResponseT searchAll(String index, int from, int size, ClassT clazz) throws IOException { return EsClientUtil.client().search(s - s .index(index) .from(from) .size(size) .query(q - q.matchAll(m - m)), clazz); } public static T SearchResponseT search(String index, org.elasticsearch.client.RequestOptions options, java.util.function.Functionco.elastic.clients.elasticsearch._types.query_dsl.Query.Builder, co.elastic.clients.elasticsearch._types.query_dsl.Query.Builder queryBuilder, int from, int size, ClassT clazz) throws IOException { return EsClientUtil.client().search(s - s .index(index) .from(from) .size(size) .query(queryBuilder), clazz); }这里第二个方法的参数比较绕但这是 Java API Client 的标准写法。调用方可以传 lambda 进来比如SearchResponseProduct response EsClientUtil.search(product, q - q.bool(b - b .must(m - m.match(t - t.field(name).query(手机))) .filter(f - f.term(t - t.field(status).value(1)))), 0, 10, Product.class);这样做的好处是工具类不感知具体业务条件但调用方又不需要接触ElasticsearchClient和 transport。如果某些接口还需要排序、聚合完全可以让调用方在 lambda 里加.sort或者.aggregations。我的经验是查询这块工具类越薄越好把bool、term、match、range这些组合权交给业务代码远比封装一堆queryByKeyword、queryByRange方法更灵活。4. 实战案例从建索引到搜索一条龙光看方法列表可能还差点意思我拿一个商品检索的真实场景把整个调用过程串一遍。这个案例不是玩具代码是我在一个垂直电商项目中实际用过的结构只是字段做了一定精简。4.1 定义业务实体类ES 客户端用 Jackson 做映射所以实体类上直接加JsonProperty注解让字段名和 ES 文档字段完全对齐。import com.fasterxml.jackson.annotation.JsonProperty; import java.math.BigDecimal; import java.time.LocalDateTime; public class Product { JsonProperty(id) private Long id; JsonProperty(name) private String name; JsonProperty(price) private BigDecimal price; JsonProperty(category) private String category; JsonProperty(status) private Integer status; JsonProperty(createTime) private LocalDateTime createTime; // 必须有无参构造和 getter/setter public Product() { } // getter/setter 省略 }有一点很重要Jackson 在做反序列化时需要无参构造函数和 getter/setterfinal字段不适合这种模式。如果你喜欢 Lombok加Data和NoArgsConstructor即可。4.2 初始化索引这里我简化成先删除再创建方便本地反复测试。生产环境一般不会每天删索引但开发环境这么干效率非常高。EsClientUtil.deleteIndex(product); EsClientUtil.createIndex(product);如果要带 mapping可以在工具类里再扩展一个createIndex(String index, MapString, Property properties)方法。我习惯把常用 mapping 配置抽成静态方法比如商品索引的 mapping 直接在调用侧用一个Map拼出来。只要记住一点工具类不负责写死业务 mapping所有 mapping 参数由调用方传入这样才能被多个业务模块复用。4.3 批量插入测试数据构造一个商品列表然后直接调bulkIndexListProduct products new ArrayList(); for (int i 1; i 20; i) { Product p new Product(); p.setId(1000L i); p.setName(测试商品 i); p.setPrice(new BigDecimal(9.9).add(new BigDecimal(i))); p.setCategory(数码); p.setStatus(1); p.setCreateTime(LocalDateTime.now().minusDays(i)); products.add(p); } boolean ok EsClientUtil.bulkIndex(product, products); System.out.println(bulk result ok);批量插入后立刻搜索可能搜不到这一点我在前文提过。你在本地测试时如果发现没数据别慌等一秒再查或者手动设置 refresh。工具类里也可以加一个bulkIndexWithRefresh方法用Refresh.WaitFor参数控制但这样会牺牲写入性能所以默认方法不提供这个能力。4.4 组合查询和结果解析现在我要查“名字里带 测试商品 且状态正常、价格在 10 到 30 之间”的商品按创建时间倒序SearchResponseProduct response EsClientUtil.search( product, q - q.bool(b - b .must(m - m.match(t - t.field(name).query(测试商品))) .filter(f - f.term(t - t.field(status).value(1))) .filter(f - f.range(r - r.field(price).gte(JsonData.of(10)).lte(JsonData.of(30)))) ), 0, 10, Product.class); long total response.hits().total().value(); ListProduct list response.hits().hits().stream() .map(co.elastic.clients.elasticsearch.core.search.Hit::source) .collect(Collectors.toList());JsonData.of(10)这里要注意。range 查询里如果传入的是字符串数字ES 会尝试自动解析但在某些类型严格的情况下可能报number_format_exception所以更稳妥的做法是直接传JsonData.of(new BigDecimal(10))。response.hits().total().value()返回的是命中总数Hit::source会直接把 JSON 文档映射成Product对象。如果你还想拿高亮、排序值、score同样的Hit对象里都能取到。5. 踩坑实录与排查方法写工具类的过程中我踩过的坑比实现本身还多。整理几个高频问题每个都能在搜索引擎里找到大把提问但真正提到细节的很少。5.1 启动类加载就报 JsonpMappingException很多人在引入新客户端后项目一启动就报类似jakarta.json.spi.JsonProvider找不到的错。这个问题十有八九是因为没有引入 JSON 实现。官方 Java API Client 底层走了 JSON-P 标准而它默认的JakartaJsonpMapper需要一个 JSON-P Provider。解决办法有两个要么引入 Jackson 并让客户端自动用JacksonJsonpMapper要么单独引入org.eclipse.parsson:parsson依赖。我更推荐前者因为团队通常已经在用 Jackson格式统一省心。5.2 版本升级后旧代码大量报错从 7.x 的 High Level REST Client 迁移过来老的SearchSourceBuilder、BoolQueryBuilder全部失效。这是正常现象因为它们已经在新客户端里消失了。我建议迁移时不要把代码逐行翻译而是直接按新客户端的 Builder 结构重写查询逻辑。表面上看工作量更大实际上新客户端代码自解释性很强翻译出来的旧代码反而不伦不类。迁移过程中容易被忽略的是依赖冲突。项目里如果还残留着org.elasticsearch.client:elasticsearch-rest-high-level-client一定要从 pom 里删干净。旧客户端和新客户端同时出现在 classpath 里往往会出现方法签名冲突和版本回退。5.3 批量写入返回成功却丢数据这个问题我在 3.3 里说过一段但值得再强调一次。BulkResponse.errors()为 false 只说明没有 item 失败所有 item 都成功为 true 时也不代表请求整体失败。排查时一定要遍历response.items()。另外如果 bulk 里出现了 mapping 不兼容的字段比如给 keyword 字段传了数组ES 会标记该 item 为失败但其他 item 照常写入整个 HTTP 响应依然是 200。不少新手在这里被坑得很惨。5.4 深分页报 max_result_window 超限默认情况下 ES 只允许你查询 10000 条以内的数据。如果调用方传了from10000就会收到Result window is too large的异常。这个限制不是 bug是 ES 在保护自身的堆内存。工具类不需要去改这个限制反而应该约束调用方普通查询最多几千条超过这个量就考虑用search_after或者 scroll。把这条写在工具类的注释里能减少好几个让人熬夜的线上问题。5.5 只走 HTTP 却配了 HTTPS 的证书报错本地连测试集群时如果集群开了 HTTPS 但工具类没配信任证书请求会直接报 SSLHandshakeException。有两种处理方式第一种是在RestClientBuilder的setHttpClientConfigCallback里构建一个信任所有证书的SSLContext调试用可以生产不建议第二种是把服务端证书导入本地cacerts或者使用自定义 trust store。工具类里最好只保留一个开关比如配置es.ssltrue后启用第一种方式等正式环境再换成第二种。这个设计让我在开发环境省了大量时间。6. 工具类扩展思路与顺带答疑工具类写到这基本可以满足日常使用。但我觉得还应该聊聊设计范式因为最近有同学在搜“java中有没有工作日判定工具类的方法”虽然这是另一个问题但背后关于“工具类怎么设计”的思路完全一致。6.1 从 EsClientUtil 到通用工具类的设计范式我写EsClientUtil的经验可以抽成一套方法论静态方法、配置外置、延迟初始化、统一异常。凡是满足这四个条件的场景都值得做一个工具类。静态方法让调用方不需要 new 对象配置外置让环境差异只体现在 properties 文件延迟初始化避免类加载时就创建昂贵资源统一异常让错误信息在入口处就变得可读。比如工作日判定工具类本质上是“读取节假日配置 判断日期是否为工作日的静态方法”它同样应该把节假日列表放文件或数据库而不是写在代码里。6.2 工作日判定的一个小实现参考如果有人确实需要“java 中有没有工作日判定工具类的方法”我的答案是 Java 标准库没有直接支持因为工作日不仅包含周六周日还要考虑法定节假日和调休。一个最简单的实现是import java.time.DayOfWeek; import java.time.LocalDate; import java.time.format.DateTimeFormatter; import java.util.HashSet; import java.util.Set; public final class WorkdayUtil { private WorkdayUtil() { } private static final SetLocalDate HOLIDAYS new HashSet(); static { // 实际项目应从配置中心或数据库读取这里只是示例 HOLIDAYS.add(LocalDate.parse(2025-01-01, DateTimeFormatter.ISO_LOCAL_DATE)); } public static boolean isWorkday(LocalDate date) { if (HOLIDAYS.contains(date)) { return false; } DayOfWeek day date.getDayOfWeek(); return day ! DayOfWeek.SATURDAY day ! DayOfWeek.SUNDAY; } }这个简易版本只判断了固定节假日没有处理调休补班。生产系统里建议把节假日和调休表拆成两张配置或者直接接第三方节假日接口。但它已经能说明问题Java 中没有天生的“工作日”概念工具类的价值就在于把业务规则封装进静态方法里。6.3 后续还可以怎么扩展我的EsClientUtil目前只覆盖了基础读写后续有两个方向打算继续补。第一是接入 Spring Boot把ElasticsearchClient声明成一个Bean工具类只保留操作封装这样依赖注入更优雅第二是给查询封装增加search_after支持方便处理超过 10000 条的数据分页。如果你自己维护这个工具类我建议也按“先稳后全”的原则来把基础能力打磨好再接新需求。7. 最后再提醒几句说实话我后来看自己写的第一版EsClientUtil最值钱的就是那些异常分支和注释。工具类代码量不大但它是整个 ES 操作的门面把门面维护好后面接搜索、接日志、接报表都会舒服很多。你可以直接拿我的代码去改但记得把配置改成自己的集群地址把批量操作的批次大小和超时时间按实际情况调整别生搬硬套。最后再分享一个小技巧工具类的方法签名不要追求过度抽象。如果有人为了“灵活性”把所有参数都塞进一个MapString, Object那调用方根本不知道要传什么。宁可多写两个重载方法也要让方法签名一眼能看出调用意图。写工具类不是炫技是让下一个维护代码的人少挠一次头。
返回列表