简介:这份资源是一份围绕 Spring Boot 整合 Elasticsearch 7.2.0 的 PDF 技术笔记,面向需要绕过旧版 spring-boot-starter-data-elasticsearch 限制的 Java 后端开发者。它针对 Spring Boot 2.1.x 默认只支持 ES 2.X 的问题,改用 Spring Data Elasticsearch 与 RestHighLevelClient 作为连接方案,并对比 transport 和 rest 两种方式的适用场景:前者走 TCP 且仅限 Java,后者走 HTTP API 且无语言限制,官方也建议使用 rest,避免后续升级到 Elasticsearch 8.X 时面临废弃接口。内容包含依赖坐标、application.yml 中 IP 配置、客户端配置类完整代码,以及五分钟超时设置,可帮助开发者快速搭建可运行的客户端环境;代码结构清晰,能减少因 starter 版本错配引起的启动异常。文档还简要说明为什么 Spring Boot 2.1.x 不能直接用官方 starter,给出版本演进背景,能够帮助理解实际项目中常见的依赖冲突来源。资源包共 1 个 PDF 文件,约 58KB,轻量易读,对排查问题尤其有用。目前已有 6769 人学习下载,适合正在集成 Elasticsearch 的 Spring Boot 开发者参考。
1. SpringBoot 整合 Elasticsearch 7.2.0:别急着写 CRUD,先过版本关
做过 SpringBoot 后台检索功能的人应该都有这种经历:产品说“加个搜索”,你往 pom 里塞一个spring-boot-starter-data-elasticsearch,然后照着几年前的博客写@Document实体,结果项目要么启动时疯狂报连接错误,要么查询结果和你本地 curl 的返回对不上。这些问题十有八九不是代码写错了,而是 SpringBoot、Spring Data Elasticsearch、Elasticsearch 7.2.0 三者版本错配。Elasticsearch 7.2 是个分水岭——它已经正式废弃了 transport 客户端,9300 端口默认不开放,网上大量老教程在这一步就把人带进沟里。这篇实战笔记面向正在做 SpringBoot 业务后台、想接入全文检索和聚合统计的团队,我会按自己实际落地路径,从依赖选型、客户端配置、数据访问到排错一次讲清。
2. 版本兼容与依赖配置:SpringBoot 版本太高,ES 7.2.0 也扛不住
2.1 先看懂 spring-data-elasticsearch 的版本矩阵
很多人不知道spring-boot-starter-data-elasticsearch不是一个独立的东西,它只是 Spring Boot 对 Spring Data Elasticsearch 的自动装配封装。真正决定你能不能连上 ES 7.2.0 的,是底层 Spring Data Elasticsearch 的版本。这个版本不是你自己随便定的,它被spring-boot-dependencies这个 BOM 统一管理。也就是说,你的 SpringBoot 版本一换,Spring Data Elasticsearch 的默认版本就跟着变,而它对应能兼容的 ES 版本也完全不同。
我整理了一份实际项目中验证过的对应关系,建议直接收藏:
| SpringBoot 版本 | 默认 Spring Data Elasticsearch 版本 | 对应兼容 ES 版本 |
|---|---|---|
| 2.2.x | 3.2.x | ES 7.2 ~ 7.6 |
| 2.3.x | 4.0.x | ES 7.6+ |
| 2.4.x | 4.1.x | ES 7.9+ |
| 2.5.x | 4.2.x | ES 7.10+ |
这里能看出两个关键信息。第一,如果你的 SpringBoot 是 2.2.x,默认依赖就是 3.2.x,正好匹配 ES 7.2.0,这也是最省事的一条路。第二,网上很多教程让你把 SpringBoot 升到 2.4 甚至 2.7,再按 ES 7.2.0 去配置,那就是纯给自己挖坑——Spring Data 4.1.x 内部对 ES 的 REST 接口调用方式已经变了,连 7.2 的服务端反而会出现请求路径不匹配的玄学问题。
2.2 最小依赖配置:显式锁版本,别让 Boot 替你决定
基于上面的版本矩阵,我在项目里最稳的做法是双保险:SpringBoot 用 2.2.x,再在pom.xml的properties里显式锁住 Spring Data Elasticsearch 版本。为什么要显式锁?因为即便 SpringBoot 2.3、2.4 的 BOM 已经升级,你还是想用 ES 7.2.0 的集群,这时候你唯一能做的就是强制降级 Spring Data 版本,让它回到 3.2.x。
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.2.5.RELEASE</version> </parent> <properties> <java.version>1.8</java.version> <spring-data-elasticsearch.version>3.2.6</spring-data-elasticsearch.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-elasticsearch</artifactId> </dependency> </dependencies>这里的核心逻辑是:spring-boot-starter-data-elasticsearch本身没有version标签,它依赖父级 BOM 来管理版本。而你手动在properties里声明spring-data-elasticsearch.version,优先级高于 BOM 的默认值,这样无论 SpringBoot 父依赖怎么变,Spring Data Elasticsearch 都会被钉在你指定的版本上。
注意:锁版本时不要低于 3.2.x。比如 3.1.x 对应的 REST 客户端 API 和 7.2 存在细微差异,ES 返回的
_source解析结构对不上,同样会报序列化异常。
另外提一个 gradle 项目里常见的坑:如果你用spring-boot-gradle-plugin搭建 SpringBoot 项目,锁版本的方式略有不同,需要在build.gradle里直接声明ext['spring-data-elasticsearch.version'] = '3.2.6',而不是写在dependencies块的 version 里。Gradle 对 BOM 属性覆盖的优先级和 Maven 不完全一样,直接给依赖写死版本是最省心的。
2.3 为什么不能用 TransportClient:9300 端口是第一个坑
ES 7.2.0 的另一个历史包袱是 TransportClient。ES 从 7.0 开始就把 TransportClient 标记为废弃,到 8.0 直接移除。你可以理解为 ES 团队希望所有外部系统都走 HTTP 协议的 REST 接口,而不是 TCP 层的私有协议。很多老项目还在用transportClient配置方式,连的是 9300 端口——这个端口在 ES 7.2 默认仍然监听,但主要用于节点间通信,不再对 Java 客户端开放同样的服务逻辑。
常见的翻车现场是这样的:SpringBoot 项目启动后日志刷出Unable to connect to localhost:9300,然后整片报错。原因很简单——spring.data.elasticsearch.cluster-nodes=localhost:9300是老教程里的写法,而 Spring Data Elasticsearch 3.2.x 已经彻底转向 REST 客户端,它只认 9200 端口。正确的配置应该写在application.yml里:
spring: elasticsearch: rest: uris: http://localhost:9200 connection-timeout: 5s read-timeout: 30s这里uris是 Spring Boot 2.2 之后的标准写法,可以配多个节点,逗号分隔。connection-timeout建议设 5 秒以下,ES 集群如果负载高,连接超时设太短反而会在启动时误报。read-timeout则要按你的查询复杂度来,聚合查询多就拉到 30 秒以上。
3. 手动整合 RestHighLevelClient:从配置 Bean 到第一次写入
3.1 配置类与连接参数说明
如果你不想受 Spring Data 那套注解和命名方法约束,最直观的方式是直接用 RestHighLevelClient——这是 ES 7.2 官方推荐的 Java 客户端。它本质上就是一个对 9200 端口 REST API 的封装,所有操作最终都会转换成 HTTP 请求发送给 ES。手动配置的好处是你能完全掌控连接池、超时和请求行为,不会吃到 Spring Data 自动配置里那些隐式规则。
我一般会写一个独立的配置类来声明这个 Bean:
import org.apache.http.HttpHost; import org.elasticsearch.client.RestClient; import org.elasticsearch.client.RestClientBuilder; import org.elasticsearch.client.RestHighLevelClient; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class EsClientConfig { @Bean public RestHighLevelClient restHighLevelClient() { RestClientBuilder builder = RestClient.builder( new HttpHost("localhost", 9200, "http") ); builder.setRequestConfigCallback(requestConfigBuilder -> requestConfigBuilder .setConnectTimeout(5000) .setSocketTimeout(30000) .setConnectionRequestTimeout(10000)); builder.setHttpClientConfigCallback(httpClientBuilder -> httpClientBuilder .setMaxConnTotal(100) .setMaxConnPerRoute(20)); return new RestHighLevelClient(builder); } }这段代码里有几个参数值得说明。setConnectTimeout是建立 TCP 连接的超时时间,5 秒对本地环境和内网环境都够用,但如果 ES 在异地机房,建议放宽到 10 秒。setSocketTimeout是两次数据包之间的等待时间,不是总请求时长——聚合大索引时经常单请求就要跑几十秒,这个值设 30 秒是底线。setMaxConnTotal和setMaxConnPerRoute是 HTTP 连接池的上限,100 和 20 的配比适合大多数单集群场景,如果同时查询两个 ES 集群,记得调大。
注意:RestHighLevelClient 在 SpringBoot 2.2 里不能用
@ConfigurationProperties自动绑定的方式创建,必须手动 new。到 SpringBoot 2.7 之后才有更优雅的自动配置,但那个版本已经不支持 ES 7.2.0 了。
3.2 创建索引与写入数据:mapping 里 text 和 keyword 分开设
有了客户端,第一步是建索引。ES 7.2 中索引对应关系型数据库里的“表”,但结构灵活得多。强烈建议在建索引时显式声明 mapping,而不是让 ES 根据第一次写入的数据自动推断。自动推断会把所有字符串都映射成text类型并附加keyword子字段,这会导致后续 term 查询出现各种诡异结果——这个问题后面单独讲。
import org.elasticsearch.action.admin.indices.create.CreateIndexRequest; import org.elasticsearch.action.index.IndexRequest; import org.elasticsearch.action.index.IndexResponse; import org.elasticsearch.client.RequestOptions; import org.elasticsearch.common.xcontent.XContentType; // 创建索引并指定 mapping CreateIndexRequest request = new CreateIndexRequest("product_index"); request.mapping("{\n" + " \"properties\": {\n" + " \"title\": {\n" + " \"type\": \"text\",\n" + " \"analyzer\": \"ik_max_word\"\n" + " },\n" + " \"brand\": {\n" + " \"type\": \"keyword\"\n" + " },\n" + " \"price\": {\n" + " \"type\": \"double\"\n" + " },\n" + " \"createTime\": {\n" + " \"type\": \"date\",\n" + " \"format\": \"yyyy-MM-dd HH:mm:ss\"\n" + " }\n" + " }\n" + "}", XContentType.JSON); restHighLevelClient.indices().create(request, RequestOptions.DEFAULT);这里把title设成text并指定ik_max_word分词器——这是中文搜索的标配,如果你没装 IK 插件,可以先改用 ES 自带的standard分词器,否则会报找不到分词器的错误。brand设成keyword是个关键决策:品牌名这类字段不需要分词,用 keyword 才能做精确匹配和聚合统计。
写入数据的代码同样直接:
import com.fasterxml.jackson.databind.ObjectMapper; ObjectMapper objectMapper = new ObjectMapper(); Product product = new Product(); product.setId("1001"); product.setTitle("SpringBoot 实战教程"); product.setBrand("人民邮电出版社"); product.setPrice(89.0); IndexRequest indexRequest = new IndexRequest("product_index"); indexRequest.id(product.getId()); indexRequest.source(objectMapper.writeValueAsString(product), XContentType.JSON); IndexResponse indexResponse = restHighLevelClient.index(indexRequest, RequestOptions.DEFAULT);注意IndexRequest指定了id,这样可以保证同一 id 的文档写入是幂等的——重复执行不会产生重复数据,只会覆盖。如果业务上要求每次插入都生成新文档,可以不设 id,ES 会自动生成 base64 编码的_id。序列化我习惯用 Jackson 的ObjectMapper,速度和稳定性都够,你也可以换成 Fastjson2,但没有本质差别。
3.3 三种常见检索写法:match、term、range 的实际场景
数据写进去了,接下来是查询。ES 的查询 DSL 核心是match和term的区别:match会先对查询词做分词,再按分词结果去倒排索引里匹配,适合搜索框这种语义模糊的场景;term不做分词,拿整个查询词去精确匹配,适合品牌、状态码这类字段。
import org.elasticsearch.action.search.SearchRequest; import org.elasticsearch.action.search.SearchResponse; import org.elasticsearch.index.query.QueryBuilders; import org.elasticsearch.search.builder.SearchSourceBuilder; SearchRequest searchRequest = new SearchRequest("product_index"); SearchSourceBuilder sourceBuilder = new SearchSourceBuilder(); // 场景一:全文检索,title 中包含“实战”即可,分词后匹配 sourceBuilder.query(QueryBuilders.matchQuery("title", "实战")); // 场景二:精确匹配品牌 sourceBuilder.query(QueryBuilders.termQuery("brand", "人民邮电出版社")); // 场景三:价格区间过滤,配合 bool 查询 sourceBuilder.query(QueryBuilders.boolQuery() .must(QueryBuilders.matchQuery("title", "SpringBoot")) .filter(QueryBuilders.rangeQuery("price").gte(50).lte(100))); searchRequest.source(sourceBuilder); SearchResponse searchResponse = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);boolQuery是 ES 里最常用的复合查询容器。must表示必须匹配,影响相关度分数;filter表示必须匹配但不参与打分。如果你只是做过滤,比如按价格筛选,用filter会让查询性能更好,因为 ES 会缓存 filter 的结果集。
查询结果解析时注意:SearchResponse里每个命中文档的_source字段是一段 JSON,你需要自己用objectMapper.readValue()转回Product对象。这也是手动使用 RestHighLevelClient 和 Spring Data 的最大区别——没有实体映射自动转换,所有解析都得手工写。数据量小无所谓,一旦字段多了,你会怀念 Spring Data 的自动映射。
4. 用 Spring Data Elasticsearch 打通 Repository:实体注解与服务层
4.1 @Document 与 @Field:先定字段类型,再定查询方式
手动客户端适合做查询验证和运维脚本,但业务代码里大量 CRUD 还靠它写,工作量太大了。这时候可以用 Spring Data Elasticsearch 的 Repository 层——它把实体映射、增删改查、分页排序都封装好了。实体类需要加上一组注解:
import org.springframework.data.annotation.Id; import org.springframework.data.elasticsearch.annotations.Document; import org.springframework.data.elasticsearch.annotations.Field; import org.springframework.data.elasticsearch.annotations.FieldType; @Document(indexName = "product_index", shards = 3, replicas = 1) public class Product { @Id private String id; @Field(type = FieldType.Text, analyzer = "ik_max_word") private String title; @Field(type = FieldType.Keyword) private String brand; @Field(type = FieldType.Double) private Double price; @Field(type = FieldType.Date, format = DateFormat.custom, pattern = "yyyy-MM-dd HH:mm:ss") private String createTime; }这里@Document注解的indexName对应索引名,shards和replicas是索引创建时的分片数和副本数。分片数量一旦创建就无法修改,所以初始设置要谨慎——数据量在百 GB 以内,3 个分片是安全值;副本数后面随时能调。@Field里的analyzer只对text类型生效,keyword类型设置分词器会被忽略。createTime建议直接存 String 并指定pattern,避免日期序列化时区问题——这是我在实际项目中用血泪换来的教训。
4.2 Repository 接口:命名方法查询能省一大半代码
实体定义好后,写一个接口继承ElasticsearchRepository,Spring Data 会按方法名自动生成查询实现,不用写任何 SQL 或 DSL。
import org.springframework.data.elasticsearch.repository.ElasticsearchRepository; import org.springframework.stereotype.Repository; import java.util.List; @Repository public interface ProductRepository extends ElasticsearchRepository<Product, String> { List<Product> findByTitle(String title); List<Product> findByBrandAndPriceBetween(String brand, Double min, Double max); List<Product> findByTitleContaining(String keyword); }findByTitle底层的查询语义是term精确匹配,不是分词匹配。如果你希望在搜索框里做分词检索,要用findByTitleContaining或findByTitleMatch这类方法——但注意Containing在 ES 里实际对应的是match_phrase,它要求分词后的词序完全一致,和纯match还是有一点差别。这块建议在实际业务里多试几种命名方式,看看 DSL 日志,不要想当然。ElasticsearchRepository自带save、findById、delete、search等方法,覆盖日常 80% 的 CRUD 场景毫无压力。
注意:
ElasticsearchRepository的分页查询有两个重载版本,一个接收Pageable,一个不接收。不传Pageable时,ES 默认只返回前 10 条——和手动查询一个德行。
4.3 ElasticsearchRestTemplate:覆盖 Repository 搞不定的聚合与多条件场景
Repository 命名方法在简单查询上是真的爽,可一旦涉及聚合、多条件动态拼接、嵌套查询,它就力不从心了。这时候要用ElasticsearchRestTemplate——它接受一个NativeSearchQuery,让你直接写原生的 QueryBuilders,同时保留 Spring Data 的实体映射能力。
import org.springframework.data.elasticsearch.core.ElasticsearchRestTemplate; import org.springframework.data.elasticsearch.core.query.NativeSearchQueryBuilder; import org.springframework.data.elasticsearch.core.SearchHits; import org.elasticsearch.index.query.QueryBuilders; import org.elasticsearch.search.aggregations.AggregationBuilders; NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder(); // 动态条件拼接 if (StringUtils.hasText(keyword)) { queryBuilder.withQuery(QueryBuilders.matchQuery("title", keyword)); } // 品牌分组聚合,统计每个品牌下的商品数量 queryBuilder.addAggregation( AggregationBuilders.terms("group_by_brand").field("brand") ); NativeSearchQuery query = queryBuilder.build(); SearchHits<Product> searchHits = elasticsearchRestTemplate.search(query, Product.class);注意search方法返回的是SearchHits<Product>,这里泛型直接指定Product,Spring Data 会自动帮你把_source反序列化成实体对象——这就是比 RestHighLevelClient 舒服的地方。.getContent()拿到List<SearchHit<Product>>,每个 hit 再.getContent()得到真正的商品对象。聚合结果在searchHits.getAggregations()里,按桶名取出来解析即可。
聚合查询在 ES 里最怕内存溢出。terms聚合默认只返回前 10 个桶,如果你需要更多,在AggregationBuilders.terms("group_by_brand").size(100)里指定。但 size 设太大也有风险——ES 是把所有桶先堆在内存里再取前 N 个,桶数量超过百万级时会直接 OOM。业务上建议聚合结果控制在一万桶以内。
5. 避坑常见问题排查:ES 7.2 整合最容易踩的 5 个坑
5.1 启动报错:Unable to connect to localhost:9300
现象:SpringBoot 项目一启动,日志直接抛异常,提示无法连接localhost:9300,后面跟一堆ElasticsearchStatusException。
原因:项目里application.yml沿用了老配置spring.data.elasticsearch.cluster-nodes: localhost:9300。Spring Data Elasticsearch 3.2.x 已经基于 RestHighLevelClient 实现,走 HTTP 协议,9300 端口是 transport 协议专用,根本不会响应 REST 请求。
解决:把配置改成spring.elasticsearch.rest.uris: http://localhost:9200,同时确认 ES 进程的http.port没有被改过。如果要排查到底是哪个配置项在起作用,可以在启动类上临时加--debug参数,SpringBoot 会把所有自动配置的匹配过程打出来,看ElasticsearchRestClientAutoConfiguration是否生效。
5.2 查询结果永远只有 10 条
现象:索引里明明插了几百条文档,Repository 的findAll()却只返回 10 条,翻页也翻不动。
原因:ES 的searchAPI 默认size就是 10。Spring Data Elasticsearch 的findAll()未传Pageable时,走的是SearchRequest默认值,不会帮你自动设置 size。这是很多新手第一次用 Repository 时的固定翻车点。
解决:所有列表查询都显式传Pageable,比如PageRequest.of(page, pageSize)。如果你确实需要一次性拉出全部数据,searchAll方法无法做到,要手动用 RestHighLevelClient 配合 Scroll API,或者用SearchSourceBuilder.size(10000)——但 ES 默认index.max_result_window是 10000,超过这个数还得靠滚动查询。
5.3 term 查询查不出来,match 却可以
现象:Repository 里findByBrand("人民邮电出版社")返回空结果,但同样的词在 Kibana 里用match查询却能搜到。
原因:term查询不做任何分词处理,它拿整个查询词去倒排索引里找精确匹配。如果字段在 mapping 里被映射成了text类型,写入时会经过分词器拆成多个词条,例如“人民邮电出版社”可能被拆成“人民”“邮电”“出版社”,“人民邮电出版社”这个完整词条在索引里根本不存在,所以 term 查不到。
解决:对需要精确匹配的字段(品牌、状态码、订单号),在 mapping 里显式声明为keyword类型。如果不方便改 mapping,另一种临时方案是用term查询配合keyword子字段:ES 7.x 对 text 字段自动生成了<字段名>.keyword子字段,QueryBuilders.termQuery("brand.keyword", "人民邮电出版社")也能达到效果,但这解决不了已有错误 mapping 的存量数据,只能在新索引上做正确映射。
5.4 mapping 字段类型改不了,reindex 也不认
现象:线上已经跑了半年,突然发现price字段在写入时被自动推断成long,现在要改成double,直接调用修改 mapping 的 API 报错。
原因:ES 的 mapping 一旦创建,字段类型不能修改,只能新增字段。这是 ES 设计上的硬限制,不像 MySQL 改列类型那样方便。原因是倒排索引和 doc values 已经按原有类型完成了编码,强行改类型会导致索引损坏。
解决:正确做法是创建一个新索引,定义好新 mapping,然后用reindex把数据搬过去,再把业务读写切换到新索引上。reindex 在 ES 7.2 里走 REST API:
POST /_reindex { "source": {"index": "product_index"}, "dest": {"index": "product_index_v2"} }注意product_index_v2必须提前建好,reindex 不会自动创建目标索引的 mapping。线上操作建议先跑一次?wait_for_completion=false,拿到 task id 后用GET /_tasks/{taskId}查询进度,避免长时间阻塞连接。
5.5 老代码用 /index/type/_search 路径,ES 7.2 直接 404
现象:迁移到 ES 7.2.0 之后,原来按GET /product_index/product/_search写的查询全部返回 404,日志里报no handler found for uri [/product_index/product/_search]。
原因:ES 7.0 正式移除了 mapping 里的type概念,REST API 路径不再允许出现/{type}这一段。ES 6.x 里product_index下面还可以叫product,7.x 统一改成_doc这个固定类型名,但绕过它的最简写法是直接省略类型段。
解决:手动使用 RestHighLevelClient 时,彻底不要写.setType()相关代码。查看老代码时,凡是出现IndexRequest、SearchRequest、DeleteRequest里带 type 参数的,全部删除。如果团队里有从 ES 2.x 时代走过来的老手,这个习惯非常容易保留,代码 review 时要重点盯。
6. 进阶:批量写入与深分页,压测期必会的两个技巧
整合做通了,下一步就是面对真实数据量。ES 单条写入的性能其实很一般,因为每写一条就是一次 HTTP 往返。生产环境里必须用_bulkAPI 做批量写入,Spring Data Repository 没有直接封装,得回到 RestHighLevelClient 写:
import org.elasticsearch.action.bulk.BulkRequest; import org.elasticsearch.action.bulk.BulkResponse; import org.elasticsearch.action.index.IndexRequest; BulkRequest bulkRequest = new BulkRequest(); for (Product product : productList) { bulkRequest.add(new IndexRequest("product_index") .id(product.getId()) .source(objectMapper.writeValueAsString(product), XContentType.JSON)); } BulkResponse bulkResponse = restHighLevelClient.bulk(bulkRequest, RequestOptions.DEFAULT);一个 BulkRequest 官方建议 5MB~15MB 左右,或大约 1000~2000 条文档。太大容易把节点内存打满,太小又浪费吞吐。如果压测时发现rejected_execution_exception,说明 bulk 请求过大冲击了线程池,把批次调小即可。
深分页是另一个高频问题。ES 默认from+size只能翻 10000 条,这是max_result_window的硬限制。业务上做后台列表按页码翻还好,做导出、跑批就会撞上这个天花板。7.2 版本里没有 PIT(Point In Time)机制,实用方案是search_after:
SearchSourceBuilder sourceBuilder = new SearchSourceBuilder(); sourceBuilder.query(QueryBuilders.matchAllQuery()); sourceBuilder.sort("createTime", SortOrder.ASC); sourceBuilder.size(1000); // 携带上一页最后一条记录的 sort 值继续翻 sourceBuilder.searchAfter(new Object[] {"2025-06-01 12:00:00"});search_after的原理是:把上一页最后一条文档的排序值记下来,下次查询从它后面继续,天然规避了深分页性能衰减。缺点是无法跳页——用户要求直接跳到 5000 页时它无能为力。不过做导出、扫全量这类后台任务,它能稳定跑完全量数据,不会像 Scroll 那样把 ES 堆内存吃光。
这套整合方案跑下来,我最大的感受是:ES 7.2.0 的坑不深,但很密,版本错配、mapping 设计、分页边界这三个问题,网上连翻车的姿势都高度相似。建议你在项目启动前先把版本矩阵贴到团队 wiki 里,省得后来的人一遍遍在 9300 端口上浪费时间。希望帮到你。
本文还有配套的精品资源,点击获取