
简介面向SpringBoot开发者与需要将Elasticsearch升级到7.x的技术人员这份PDF从版本兼容痛点切入说明Spring Boot 2.1.x内置的spring-boot-starter-data-elasticsearch仍停留在ES 2.X因此改用Spring-data-elasticsearch以适配7.2.0。文中对比transport与rest两种连接方式明确rest通过HTTP API访问、官方推荐且后续版本长期支持并给出完整实现引入elasticsearch、elasticsearch-rest-client、elasticsearch-rest-high-level-client三个Maven依赖在application.yml配置elasticsearch.ip地址再通过配置类创建RestHighLevelClient同时设置连接、Socket及连接请求超时时间为5分钟。资源为单个PDF文件压缩包仅58KB内容紧凑可直接阅读已有6769人学习浏览适合正在搭建ES客户端或排查版本兼容问题的读者可对照示例快速复制配置并理解RestHighLevelClient初始化细节。1. SpringBoot 整合 Elasticsearch 7.2.0解决什么问题值不值得做一个搜索接口数据库LIKE %关键词%数据量上了百万之后查询耗时从几十毫秒涨到两秒这是很多业务系统都会撞上的瓶颈。把关键词检索、日志分析、数据聚合这类需求交给 Elasticsearch是后端团队最常走的路线。而 SpringBoot 整合 Elasticsearch 7.2.0本质上就是用一套统一的方式让你的应用能连接 ES、建索引、写数据、再组合查询条件检索数据。7.2.0 这个版本号要单独拎出来说它是 7.x 早期一个很稳定的节点自带成熟的 RestHighLevelClient对 JDK 版本要求也不苛刻。很多现存项目就是把它当基础设施用的版本没有跟到 8.x不是因为懒而是因为够用且稳定。这套方案适合谁适合正在维护老项目、需要给 SpringBoot 服务接一个搜索引擎或者想避开 8.x/new Java Client 那套更复杂 API 的开发者。下面从环境、依赖、配置到查询把整条链路铺开代码可以直接抄。2. 版本选型7.2.0 为什么值得锁依赖怎么配才不打架2.1 ES 7.x 的分水岭type 移除与 RestHighLevelClient 成熟ES 6.x 之前一个索引里可以定义多个 type6.x 开始逐步废弃到 7.0 直接移除了多 type 的概念。这个改动对整合方是重大利好索引结构从「索引 类型 文档」简化为「索引 文档」Spring Data 层的映射逻辑也清爽了很多。如果你现在去搜旧资料看到IndexRequest里还带 type例如doc那多半是 6.x 时代的写法放到 7.2.0 会直接报错。7.2.0 另一个价值点是 RestHighLevelClient 的 API 形态已经成型。这个客户端走 HTTP 协议不需要持有 TransportClient 那套 TCP 端口和集群嗅探配置只关心9200这一个 HTTP 端口。从使用角度讲它对业务代码的侵入很小你构造一个请求对象、执行、拿响应剩下的连接管理、心跳探测、请求重试都由客户端处理。SpringBoot 项目里只需要一个配置类把它注册成 Bean后续注入即可。还有一点值得说ES 7.2.0 安装包里自带一个 JDK路径在安装目录的jdk文件夹下。这意味着你本地甚至不用单独配JAVA_HOME直接跑启动脚本就能起来一个单机实例。对开发联调来说非常省事这也是很多团队在本地把版本锁在 7.x 早期版本的原因。2.2 先把依赖配齐pom 与 SpringBoot 版本搭配常见的做法是引入elasticsearch-rest-high-level-client版本严格指定7.2.0。这里不要用 Spring Boot 的 dependencyManagement 去管理 ES 客户端版本因为 SpringBoot 的 BOM 锁定的是 Spring Data Elasticsearch 的配套版本和 ES 服务端不一定对齐。properties elasticsearch.version7.2.0/elasticsearch.version java.version1.8/java.version /properties dependencies !-- SpringBoot Web 基础 -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency !-- ES 7.2.0 官方高版本客户端 -- dependency groupIdorg.elasticsearch.client/groupId artifactIdelasticsearch-rest-high-level-client/artifactId version${elasticsearch.version}/version /dependency !-- 如果需要用 Spring Data 的 Repository 风格再加这个 -- !-- 注意它会按自己的兼容策略绑 ES 版本7.2.0 建议 SpringBoot 2.2.x ~ 2.4.x -- !-- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-data-elasticsearch/artifactId /dependency -- /dependencies这段配置里有几个细节。java.version保持 1.8 是完全没问题的7.2.0 官方支持 JDK 8这点比 7.10 和 8.x 都亲民。注释里我也想提醒spring-boot-starter-data-elasticsearch不是不能用但它引入的 Spring Data Elasticsearch 版本会决定底层用哪种客户端、兼容哪个 ES 版本版本匹配一旦错位就会出现诡异的序列化异常。我的经验是新项目如果核心诉求是「把搜索做好」直接用 RestHighLevelClient 最可控如果团队习惯 Repository 风格再考虑 starter但必须对照兼容矩阵确认版本。2.3 Windows 下启动 ES 7.2.0 的最小操作本地开发基本都在 Windows 上7.2.0 的启动方式和 Linux 一样只要你把安装包解压到纯英文路径。# Windows 命令行进入 ES 安装目录 cd D:\elasticsearch-7.2.0 # 启动前台运行日志直接打在当前窗口 bin\elasticsearch.bat # 也可以用 -d 参数后台启动 bin\elasticsearch.bat -d启动完成后打开浏览器访问http://localhost:9200看到一个包含you_know_for_search的 JSON 响应就说明服务起来了。我一般会在config/elasticsearch.yml里先确认两个参数cluster.name和http.port。默认http.port是 9200如果被占用会启动失败直接改端口或者关掉占用进程即可。还有一个在 Windows 下值得注意的点ES 7.x 默认绑定的主机是localhost如果你要用另一台机器连接才需要改network.host本地开发不用动。3. 客户端配置与索引建库从 yml 到 mapping 不踩坑3.1 yml 里 ES 地址怎么设置客户端配置不需要写死在 Java 代码里放配置文件更符合 SpringBoot 习惯。下面是application.yml里的常见写法spring: elasticsearch: rest: uris: http://localhost:9200 connection-timeout: 5s read-timeout: 10s # 自定义配置ES 在 SpringBoot 2.x 中常用自定义前缀 elasticsearch: host: localhost port: 9200 scheme: http username: elastic password: changeme注意这段 yml 里spring.elasticsearch.rest.uris是 Spring Boot 2.2 官方提供的配置项可以少写很多 Java 配置。但 7.2.0 对应的 SpringBoot 版本对这个属性的支持程度不同项目里更稳妥的方式是自己定义一个前缀比如elasticsearch.host然后用ConfigurationProperties或Value注入到配置类里不依赖 SpringBoot 版本对你 ES 版本的兼容判断这条路最稳。3.2 写一个配置类把 RestHighLevelClient 交给容器import org.apache.http.HttpHost; import org.elasticsearch.client.RestClient; import org.elasticsearch.client.RestHighLevelClient; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; Configuration public class ElasticsearchClientConfig { Value(${elasticsearch.host}) private String host; Value(${elasticsearch.port}) private Integer port; Value(${elasticsearch.scheme}) private String scheme; Bean(destroyMethod close) public RestHighLevelClient restHighLevelClient() { // 三参数的 HttpHost 分别对应 地址、端口、协议 return new RestHighLevelClient( RestClient.builder(new HttpHost(host, port, scheme)) .setRequestConfigCallback(requestConfigBuilder - requestConfigBuilder .setConnectTimeout(5000) .setSocketTimeout(60000)) .setMaxRetryTimeoutMillis(60000) ); } }这个配置类有两个关键点。第一destroyMethod close是必须的不声明的话Spring 容器关闭时不会主动释放连接池在频繁重启的应用里会积累 TIME_WAIT 连接。第二setMaxRetryTimeoutMillis控制请求失败后的最大重试时间建议不要设太大否则 ES 集群抖动时接口会长时间阻塞。3.3 索引创建与 mapping 设计keyword 和 text 别搞混索引是整个搜索系统的表结构ES 的 mapping 定义了字段类型和分析方式。新手最容易在这翻车把用户名字段设成 text结果精确过滤时查不到把描述字段设成 keyword结果全文搜索搜不出来。常见的取舍如下表业务字段类型说明idkeyword精确匹配、聚合、排序titletext keyword 子字段全文检索 精确过滤contenttext全文检索不参与聚合createTimedate时间范围查询statusinteger精确过滤tagskeyword数组多值精确匹配创建索引的代码要在应用启动时执行一次不能每次请求都去建。我一般是在ApplicationRunner里做一次幂等创建。import org.elasticsearch.action.admin.indices.create.CreateIndexRequest; import org.elasticsearch.action.admin.indices.get.GetIndexRequest; import org.elasticsearch.client.RequestOptions; import org.elasticsearch.client.RestHighLevelClient; import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.common.xcontent.XContentFactory; import org.springframework.boot.ApplicationArguments; import org.springframework.boot.ApplicationRunner; import org.springframework.stereotype.Component; Component public class IndexInitializer implements ApplicationRunner { private final RestHighLevelClient client; public IndexInitializer(RestHighLevelClient client) { this.client client; } Override public void run(ApplicationArguments args) throws Exception { String index article; // 先查索引是否存在存在就直接跳过避免重复创建报错 GetIndexRequest existsRequest new GetIndexRequest(index); boolean exists client.indices().exists(existsRequest, RequestOptions.DEFAULT); if (exists) { return; } CreateIndexRequest createRequest new CreateIndexRequest(index); // 分片 1副本 0单机开发环境不浪费资源 createRequest.settings().put(index.number_of_shards, 1); createRequest.settings().put(index.number_of_replicas, 0); XContentBuilder mapping XContentFactory.jsonBuilder() .startObject() .startObject(properties) .startObject(id) .field(type, keyword) .endObject() .startObject(title) .field(type, text) .startObject(fields) .startObject(raw) .field(type, keyword) .endObject() .endObject() .endObject() .startObject(content) .field(type, text) .field(analyzer, standard) .endObject() .startObject(createTime) .field(type, date) .field(format, yyyy-MM-dd HH:mm:ss||yyyy-MM-dd||epoch_millis) .endObject() .endObject() .endObject(); createRequest.mapping(mapping); client.indices().create(createRequest, RequestOptions.DEFAULT); } }这段代码里值得解释的是 title 字段的双重结构text类型负责全文搜索fields.raw子字段用keyword负责精确匹配和排序。如果你要对 title 做order byES 里是sort by title.raw直接用title排序会报「fielddata is disabled」的错误。format那段我写了三种时间格式epoch_millis是给毫秒时间戳用的这样写入端传什么格式都能解析算是少踩一个格式坑的经验。4. 数据写入与组合查询把业务数据同步进 ES 的最小闭环4.1 批量写入BulkRequest 比循环单条快多少写入 ES 最忌讳的是写个for循环一条条IndexRequest性能至少差一个数量级。正确姿势是攒一批再发BulkRequest。下面的代码从 MySQL 查出数据批量写入 ESimport org.elasticsearch.action.bulk.BulkRequest; import org.elasticsearch.action.bulk.BulkResponse; import org.elasticsearch.action.index.IndexRequest; import org.elasticsearch.client.RequestOptions; import org.elasticsearch.client.RestHighLevelClient; import org.elasticsearch.common.xcontent.XContentType; import java.util.List; import java.util.Map; public void bulkSync(ListMapString, Object rows) throws Exception { // 批量请求对象可以容纳多个子请求 BulkRequest bulkRequest new BulkRequest(); for (MapString, Object row : rows) { // id 用业务主键保证同一篇文档重复写入时是覆盖而不是新增 String docId String.valueOf(row.get(id)); IndexRequest indexRequest new IndexRequest(article) .id(docId) .source(row, XContentType.JSON); bulkRequest.add(indexRequest); } BulkResponse response client.bulk(bulkRequest, RequestOptions.DEFAULT); if (response.hasFailures()) { // 打印第一条失败原因方便定位 throw new RuntimeException(response.buildFailureMessage()); } }三个参数值得记一下。row必须是 Map 结构字段名和 mapping 里定义的要保持一致否则会写入失败或字段类型不匹配id用业务主键这是数据同步幂等的关键XContentType.JSON表示把 Map 序列化成 JSON 再发送。调用处建议每 5000 条调一次bulkSync单批太大内存压力高太小网络开销大5000 是我在普通配置机器上试出比较稳的区间。4.2 查询BoolQueryBuilder 组合条件与高亮ES 的查询 DSL 在 Java 里对应SearchSourceBuilder一组boolQuery能覆盖绝大多数业务检索场景。下面是一个带关键词、状态过滤、时间范围和高亮的完整查询代码import org.elasticsearch.action.search.SearchRequest; import org.elasticsearch.action.search.SearchResponse; import org.elasticsearch.client.RequestOptions; import org.elasticsearch.client.RestHighLevelClient; import org.elasticsearch.index.query.BoolQueryBuilder; import org.elasticsearch.index.query.QueryBuilders; import org.elasticsearch.search.builder.SearchSourceBuilder; import org.elasticsearch.search.fetch.subphase.highlight.HighlightBuilder; import org.elasticsearch.search.sort.SortOrder; public SearchResponse search(String keyword, Integer status, String startTime, String endTime, int page, int size) throws Exception { // 1. 布尔查询must 是必须满足的filter 不影响评分 BoolQueryBuilder boolQuery QueryBuilders.boolQuery(); if (keyword ! null !keyword.isEmpty()) { // 多字段匹配标题和正文权重分配 2.0 和 1.0 boolQuery.must(QueryBuilders.multiMatchQuery(keyword, title^2.0, content)); } if (status ! null) { boolQuery.filter(QueryBuilders.termQuery(status, status)); } if (startTime ! null endTime ! null) { boolQuery.filter(QueryBuilders.rangeQuery(createTime) .gte(startTime).lte(endTime)); } // 2. 高亮关键词用 em 标签包起来 HighlightBuilder highlightBuilder new HighlightBuilder(); highlightBuilder.field(title).field(content); highlightBuilder.preTags(em).postTags(/em); // 3. 组装请求 SearchSourceBuilder sourceBuilder new SearchSourceBuilder() .query(boolQuery) .from((page - 1) * size) .size(size) .sort(createTime, SortOrder.DESC) .highlighter(highlightBuilder); SearchRequest searchRequest new SearchRequest(article); searchRequest.source(sourceBuilder); return client.search(searchRequest, RequestOptions.DEFAULT); }这里最容易忽略的一个点sort(createTime, SortOrder.DESC)要求createTime是 date 类型并且 mapping 里没被设为enable: false。而高亮字段title必须是 text 类型keyword 字段是不能高亮的。还有 filter 和 must 的区别status 和时间范围用 filter因为它们不参与相关性评分ES 能走缓存性能更好关键词用 must因为它影响打分排序。4.3 分页与排序from/size 的边界在哪上面的代码用了from size分页这在小数据量下没问题但它有硬边界默认最多只能翻到第 10000 条。因为 ES 需要把每个分片上的前from size条全部取出来再归并排序翻页越深性能开销越大。如果业务查询深翻页场景多有两个替代方案// 方案一search_after适合实时滚动翻页 SearchSourceBuilder sourceBuilder new SearchSourceBuilder() .query(boolQuery) .size(size) .sort(_shard_doc) .searchAfter(new Object[]{lastSortValue}); // 注意searchAfter 必须配合排序使用且排序字段值要唯一否则翻页会丢数据// 方案二scroll适合数据导出不实时 SearchRequest searchRequest new SearchRequest(article); SearchSourceBuilder sourceBuilder new SearchSourceBuilder() .query(boolQuery) .size(5000); searchRequest.source(sourceBuilder); searchRequest.scroll(TimeValue.timeValueMinutes(1L));我的建议是用户前台搜索用from size限制最多翻 100 页后台管理系统的导出任务用 scroll实时增量同步用search_after。三种场景对应三种 API不要混用。5. 避坑整合 7.2.0 最常见的 5 个翻车现场5.1 现象启动报错NoNodeAvailableException连接被拒原因可能是三个第一ES 服务没启动第二9200 端口没开放第三也是最高频的——你从旧项目里拷贝了 TransportClient 的配置拿着 TCP 端口 9300 去连。这是 6.x 时代留下的「历史债务」9300 是节点间通信端口RestHighLevelClient 根本不用它。解决确认访问地址是http://你的IP:9200不是9300。本地用curl http://localhost:9200先验证通了再排查应用配置。另外不要写cluster.name到 RestHighLevelClient 配置里它只对 TransportClient 有意义写了也不会报错但会让后来接手的人误以为这是必需的。5.2 现象用spring-boot-starter-data-elasticsearch启动后执行save()或findAll()报类型转换异常ClassCastException或ElasticsearchStatusException。原因Spring Data Elasticsearch 的版本和 ES 服务端版本不匹配。每个 SpringBoot 版本都绑定了特定版本的 Spring Data ES而 Spring Data ES 内部对 ES 版本有硬性兼容要求。你的 SpringBoot 版本太新比如 2.6 配 spring-data-elasticsearch 4.3.x它默认发起的 REST 请求格式和 7.2.0 服务端不完全兼容就会在反序列化阶段出问题。解决先查 Spring Data Elasticsearch 官方兼容矩阵确认 SpringBoot 版本与 ES 7.2.0 的对应关系。最省心的还是绕开 starter直接用第 2 章里的rest-high-level-client手动写 CRUD。这不算绕路反而让你的业务代码不依赖 Spring Data 的实体映射少掉一堆注解层面的黑匣子。5.3 现象Windows 下双击elasticsearch.bat一闪而过日志文件里报java.lang.IllegalStateException: path.home is not configured或乱码错误。原因ES 安装路径包含中文或空格比如D:\软件\elasticsearch-7.2.0。ES 的启动脚本对路径分隔符和特殊字符很敏感路径一乱解析配置就失败。解决把 ES 解压到纯英文路径比如D:\dev\elasticsearch-7.2.0。顺手把config/jvm.options里-Xms1g和-Xmx1g调成512m本地开发机器内存不足时ES 会因为无法分配堆内存而静默退出。这一步是血泪经验ES 启动失败很少在控制台直接报清晰错误日志文件才是第一排查入口。5.4 现象中文关键词搜不到英文能搜到。原因ES 7.2.0 自带的标准分析器 standard 对中文是逐字切分或者整句当成一个 token——实际上是按 Unicode 字符切分。比如搜索「整合」时如果文档里是「SpringBoot 整合 ES」分词结果是被拆散的匹配不上。解决给公司业务索引装 IK 中文分词插件或者在写入数据时用 HanLP 提前分词存进另一个 keyword 字段。具体方案放在第 6 章讲但这里先给出定位思路先用_analyzeAPI 看看分词结果。curl -X POST http://localhost:9200/_analyze?pretty -H Content-Type: application/json -d {analyzer: standard, text: SpringBoot整合Elasticsearch}输出里如果看到每个汉字都是独立 token就说明中文分词没有生效查插件安装和索引 mapping 里的 analyzer 配置。5.5 现象SpringBoot 版本太高比如 2.7.x / 3.xpom 里引入elasticsearch-rest-high-level-client后互相冲突Cannot resolve或运行期NoClassDefFoundError。原因SpringBoot 3.x 基于 Java 17整个依赖体系对elasticsearch相关 jar 的坐标做了调整而且高版本 SpringBoot 默认的spring-boot-dependenciesBOM 里可能引入了 ES 8.x 的客户端 jar和你的 7.2.0 冲突。这也是「springboot版本太高」搜索热度高不下来的现实原因。解决不建议硬升。要么把项目整体迁到 SpringBoot 3 新 ES 客户端那是另一个量级的改造要么就在 2.x 里选一个合理版本2.3.x ~ 2.5.x 区间配 7.2.0。比起追版本业务能稳定跑才是优先级。6. 进阶玩法与验证方法聚合统计、中文分词与数据恢复快照6.1 聚合统计按类目计数、按天统计搜索之外ES 的聚合语法在报表场景也很好用。下面的代码统计每个status下的文档数并额外统计按天分布的文档数import org.elasticsearch.search.aggregations.AggregationBuilders; import org.elasticsearch.search.aggregations.BucketOrder; import org.elasticsearch.search.aggregations.bucket.terms.TermsAggregationBuilder; import org.elasticsearch.search.aggregations.bucket.histogram.DateHistogramAggregationBuilder; import org.elasticsearch.search.aggregations.bucket.histogram.DateHistogramInterval; public void aggExample() { SearchSourceBuilder sourceBuilder new SearchSourceBuilder(); sourceBuilder.size(0); // 只取聚合结果不取文档列表 // 按状态字段分组降序排列 TermsAggregationBuilder statusAgg AggregationBuilders .terms(status_count) .field(status) .order(BucketOrder.count(false)); // 按天分桶interval 指定时间间隔 DateHistogramAggregationBuilder dateAgg AggregationBuilders .dateHistogram(date_count) .field(createTime) .calendarInterval(DateHistogramInterval.DAY); sourceBuilder.aggregation(statusAgg); sourceBuilder.aggregation(dateAgg); // 执行后从 response.getAggregations() 中解析桶结果 }聚合里有个容易踩的坑field(status)前提是该字段在 mapping 中的类型是 keyword 或 integer且开启了doc_values: true。text 字段默认不开 doc_values直接对 text 做 terms 聚合会报Fielddata access is disabled。以前的解决方案是开启 fielddata非常浪费堆内存现在的方案是在 mapping 里为文本字段加 keyword 子字段聚合用子字段。6.2 中文分词HanLP 在 SpringBoot 中的轻量整合思路IK 分词插件需要在 ES 服务端安装插件包并重启节点这个流程在生产和测试环境都要走运维。如果不想装插件有一个轻量做法写入时用 HanLP 分词把分词结果放进一个独立字段查询时也用同样逻辑分词去匹配。import com.hankcs.hanlp.HanLP; import com.hankcs.hanlp.seg.common.Term; // 写入前处理 ListString tokenList new ArrayList(); ListTerm termList HanLP.segment(content); for (Term term : termList) { // 过滤标点符号和虚词保留名词、动词等实词 if (term.nature.toString().startsWith(n) || term.nature.toString().startsWith(v)) { tokenList.add(term.word); } } row.put(contentSeg, String.join( , tokenList));这个方案的优点是业务代码完全可控不依赖 ES 服务端安装任何插件缺点是索引体积变大而且查询需要和写入用同一套分词逻辑否则匹配不上。HanLP 在 SpringBoot 中整合就成了一个普通 Bean依赖里引入hanlp即可启动时自动加载模型不需要额外配置。数据量不大时这个方案完全够用我甚至觉得比部署 IK 插件更灵活。6.3 快照恢复把 ES 数据备份当成验收项整合完 ES第一件要验证的其实是灾难恢复能力而不是花哨的查询功能。ES 的快照 API 支持把索引备份到共享目录或对象存储恢复时一条命令完成。# 1. 在 elasticsearch.yml 中声明仓库目录 # path.repo: [D:/es_backup] # 然后重启 ES 使配置生效 # 2. 注册一个快照仓库 curl -X PUT http://localhost:9200/_snapshot/my_backup -H Content-Type: application/json -d {type: fs, settings: {location: D:/es_backup}} # 3. 为所有索引创建一个快照 curl -X PUT http://localhost:9200/_snapshot/my_backup/snapshot_20250101?wait_for_completiontrue # 4. 恢复指定索引 curl -X POST http://localhost:9200/_snapshot/my_backup/snapshot_20250101/_restore -H Content-Type: application/json -d {indices: article, rename_pattern: (.), rename_replacement: article_restored}恢复时建议使用rename_replacement还原成新索引名确认数据完整后再切换别名或改回原名。我见过有人直接覆盖原索引恢复失败导致数据双写的名称混淆是快照恢复里最常见的失误。把这个功能在项目初期就验证一遍比上线后数据丢了再研究「elasticsearch 恢复数据」要省心得多。最后一个习惯分享我现在做任何 ES 整合第一步先确认服务端版本第二步把客户端版本和服务端锁死第三步才碰业务代码。版本错位的事故我处理过太多次多数不是技术难题而是依赖管理上想当然。7.2.0 配合 SpringBoot 2.x 是一套经过大量项目验证的组合按这个思路走无论你是一周内要交付搜索功能的开发还是维护老项目想加搜索能力方向都不会偏。希望帮到你。本文还有配套的精品资源点击获取