SpringBoot2.2.6整合Elasticsearch6.8.6开发详解
去年帮一个老项目做技术方案选型时我再一次把组合定在了 SpringBoot 2.2.6 Elasticsearch 6.8.6 上。很多人一听 6.x 就皱眉觉得版本太旧、没技术含量。但说句实在话6.8.6 是 6.x 生命周期里最稳的一个小版本SpringBoot 2.2.x 对它的适配也几乎到了“教科书级”的干净程度。如果你正准备写一个 SpringBoot Elasticsearch 6.8.6 的入门案例或者入职后发现公司搜索服务还跑在 6.8 上这篇内容能帮你把从环境搭建到第一个可运行案例的路一次走通顺便避掉我当年踩过的几个坑。这篇不是只贴代码我会把版本选择的理由、ES 6.8 里容易误解的概念、单机环境配置、完整 CRUD 和搜索示例、以及跑通之后必现的六个问题都讲清楚。适合刚接触 ES 的 SpringBoot 开发也适合给老项目做维护的人参考。1. 为什么我推荐“SpringBoot 2.2.6 Elasticsearch 6.8.6”这个组合而不是追新先说结论做入门案例版本稳定比版本新更重要。Elasticsearch 从 7.x 开始做了太多“激进式”调整比如彻底移除 type、TransportClient 直接退役、很多 DSL 行为变化。你拿 ES 7 的写法去查 6.8 的集群或者拿 6.8 的老经验去排查 7.x 的问题都会被版本差异坑一道。6.8.6 保留了 6.x 的完整特性又修复了早期版本的一堆安全问题恰好处于“旧特性还在、新趋势已现”的过渡期最适合用来理解 ES 的核心概念。更关键的是 Spring Data Elasticsearch 的版本对应关系。这不是你想用哪个就用哪个SpringBoot 的 starter 会把 Spring Data Elasticsearch 的版本一起管住而 Spring Data Elasticsearch 又只针对特定 ES 大版本做了兼容性测试。我整理了一份实际项目中用过的基础对应表SpringBoot 版本Spring Data Elasticsearch 版本ES 版本2.2.x3.2.x6.8.x2.3.x4.0.x7.x2.4.x4.1.x7.62.5.x - 2.6.x4.2.x - 4.4.x7.10用 SpringBoot 2.3.x 强行配 ES 6.8.6 是可以跑但 Spring Data ES 4.x 的底层 API 已经偏向 7.x部分 Repository 方法和查询语义有微妙差异。反过来用 SpringBoot 2.2.6 去配 ES 7.x则会直接遇到TransportClient无法连接的尴尬因为 Spring Data ES 3.2.x 默认走 9300 的 Transport 协议而 ES 7 已经不再推荐甚至移除了服务端对 TransportClient 的支持。所以最省心的组合就是SpringBoot 2.2.6.RELEASE spring-data-elasticsearch 3.2.x Elasticsearch 6.8.6。还有一个隐藏的坑是 JDK 版本。ES 6.8 官方支持 JDK 8 和 JDK 11但我个人体验是 JDK 8 最稳。SpringBoot 2.2.x 同样对 JDK 8 支持最完整JDK 11 跑 SpringBoot 2.2 也能跑但某些老版本的内嵌容器会出现日志或反射相关的兼容问题。所以我的建议很直接学习这套组合就用 JDK 8不要追求新 JDK省下来的时间足够你多跑一个搜索 demo。对老项目维护来说这一条尤其重要——别看生产环境 JDK 版本高搜服务集成模块大概率还是顶着 JDK 8 环境。2. 先把单机 ES 6.8.6 跑起来两条端口和三个配置文件是最快突破口ES 安装其实不复杂但很多入门者卡在“连不上”“启动报错”这些环境问题上。我们先把单机环境跑通再回头看 SpringBoot 对接。我习惯用 tar 包方式安装而不是系统的包管理工具因为 tar 包能精确定位版本不会因为源里的版本漂移导致环境不对。wget https://artifacts.elastic.co/downloads/elasticsearch/elasticsearch-6.8.6.tar.gz tar -zxf elasticsearch-6.8.6.tar.gz cd elasticsearch-6.8.6 bin/elasticsearch启动成功后用另一个终端验证curl http://127.0.0.1:9200/正常情况下你会看到一坨 JSON里面有三个值非常关键cluster_name、cluster_uuid、version.number。如果version.number不是6.8.6说明你下载的包不对。cluster_name是后面 SpringBoot 配置里必须对齐的字段默认是elasticsearch。这里我需要重点解释一下 ES 的两条端口这也是入门者最容易混乱的地方端口协议用途例子9200HTTP REST所有 REST API、curl、Kibana 访问curl http://127.0.0.1:9200/product/_search9300Transport 协议集群节点间通信、TransportClient、Spring Data ES 3.2.x 默认连接Java 程序里配置cluster-nodes: 127.0.0.1:9300这解释了为什么你curl 9200明明能通Java 却报连接错误。Spring Data Elasticsearch 3.2.x 走的不是 HTTP而是 9300 的 transport 协议所以你配置文件里的地址要写127.0.0.1:9300不能写 9200。这是这套组合最典型的入门拦路虎。单机开发环境还需要改三个地方第一个是config/jvm.options。默认分配的内存有时候不适合小机器我一般把堆内存调成 1g-Xms1g -Xmx1g语法注意jvm.options 里每行一个参数两边不要留空格。如果你的机器本身就 2G 内存可以再小一点改成 512m但 ES 官方并不推荐低于 256m。第二个是config/elasticsearch.yml。单机学习不需要改太多但建议把这两行打开或追加network.host: 127.0.0.1 http.port: 9200 transport.tcp.port: 9300network.host如果不写默认只监听本地回环如果写0.0.0.0可以由同网段其他机器访问但这在开发环境里很容易被同事扫到产生不必要的安全问题。第三个要理解的是config/logs和data目录。data目录存放索引分片数据logs是日志目录。ES 启动失败时先看logs/elasticsearch.log这个习惯要养成错误信息比任何猜想都准确。Linux 下还有一个常见坑ES 不允许以 root 用户运行。如果你在 root 用户下执行bin/elasticsearch大概率会报can not run elasticsearch as root。解决方法就是新建一个普通用户并把目录所属权切过去useradd es chown -R es:es /usr/local/elasticsearch su es cd /usr/local/elasticsearch bin/elasticsearch如果是在云主机上跑 ESmax_map_count不够也会启动失败报错信息里一般会提示vm.max_map_count [65530] is too low这时需要执行sysctl -w vm.max_map_count262144这个设置在重启后会失效需要写入/etc/sysctl.conf才持久化。总的思路是先看日志再查系统参数不要一上来就怀疑 SpringBoot 代码。Kibana 我建议入门阶段装一个同版本 6.8.6尤其是想看索引映射、调试 DSL 时它比 curl 舒服太多。中文搜索场景后面再加 IK 分词器这个我们后面细说。3. 工程依赖与配置文件三个最容易出事的约定环境跑起来后开始建 SpringBoot 工程。先说 pom.xml我直接给出完整可用的坐标parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version2.2.6.RELEASE/version relativePath/ /parent properties java.version1.8/java.version /properties dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-data-elasticsearch/artifactId /dependency /dependencies最需要注意的一点是不要手动给 spring-data-elasticsearch 指定版本号。SpringBoot 2.2.6 的 BOM 里已经锁定了 3.2.6.RELEASE这是和 ES 6.8.x 对应的版本。如果你在图省事时加了version把 Spring Data ES 升到 4.x接着就会遇到RestHighLevelClient没配置、ElasticsearchRestTemplate不存在等一系列连带问题。SpringBoot 全家桶的价值就在于版本一致性自己破坏一致性往往得不偿失。然后是application.ymlspring: data: elasticsearch: cluster-name: elasticsearch cluster-nodes: 127.0.0.1:9300 repositories: enabled: true server: port: 8080这三个配置值的含义要搞清楚cluster-name必须和 ES 里cluster_name完全一致。默认是elasticsearch但很多公司生产环境为了标识团队会改成别的名字。不一致的话程序启动时不会立刻报错但一旦调用 ES 接口就会出现NoNodeAvailableException。cluster-nodes是 ES 节点地址格式是 host:port注意端口必须是 9300而不是 9200。这个我们上一节已经说过了。repositories.enabled: true是打开 Spring Data ES 的 Repository 能力不打开的话接口扫描和自动创建索引的行为都不生效。三个隐藏约定里第三个特别容易被忽略Spring Data ES 3.2.x 的底层是 TransportClient不是 REST Client。所以它在启动时会向 9300 端口发起节点发现请求。如果你的程序报错里带NoNodeAvailableException先把cluster-nodes和cluster-name对着检查一遍。我当年就经历过一次“9200 能 curl、9300 一直在通但程序还是连不上”的问题最后发现是 cluster-name 不匹配ES 集群真实名称是es-app-prod配置文件里写的还是默认的elasticsearch。另外在 Spring Data ES 3.2.x 里索引的自动创建是在应用启动阶段完成的。只要实体上标了Document并开启了 repositories容器启动时就会尝试创建索引。如果没有创建权限启动会报错。开发环境直接给 ES 目录用chown授权即可不要一上来就研究“权限模型”。如果你在 SpringBoot 2.3.x 里用这套配置会发现spring.data.elasticsearch.cluster-nodes不一定生效因为从 4.0 开始配置项被整理到了spring.elasticsearch.rest.*命名空间。这正是版本连带关系的一个直观体现。所以还是那句话入门阶段锁死版本组合不要混搭。4. 第一个可运行的 CRUD 案例从实体映射到 HTTP 接口配置没问题后就可以写第一个案例了。我这次用一个很典型的商品搜索场景商品包含名称、分类、价格、描述。这样后面讲搜索时也有业务语境。先定义一个实体类并用注解把 Java 类和 ES 索引对应起来Document(indexName product, type _doc, shards 1, replicas 0) public class Product { Id private String id; private String name; private String category; private Double price; private String description; // 无参构造、getter/setter 省略建议使用 IDE 生成 }这里有几个点值得展开indexName对应 ES 索引名注意索引名必须小写。如果写成ProductES 6.8 会直接拒绝创建因为索引名只允许小写。这个报错很典型错误信息里会出现Invalid index name [Product], must be lowercase。type _doc是这个版本特有的写法。ES 6.x 规定一个索引只剩一个 type官方推荐统一用_doc。ES 7 以后连 type 概念都废弃了所以你在 6.8 里学会的_doc其实就是通向 7.x 思路的过渡桥梁。shards 1, replicas 0是开发环境配置。单机只有一个节点副本数写 0 避免出现 yellow 健康状态。生产环境至少 3 个副本分片这是后话。然后定义 Repository 接口。Spring Data 的套路和 JPA 很像public interface ProductRepository extends ElasticsearchRepositoryProduct, String { ListProduct findByName(String name); ListProduct findByCategory(String category); ListProduct findByPriceBetween(Double min, Double max); ListProduct findByNameAndCategory(String name, String category); }不用写任何实现类Spring Data 会在启动时自动生成代理对象。ElasticsearchRepository已经内置了一些方法save、saveAll、findById、findAll、deleteById、count等满足入门 CRUD 完全够用。接着写一个 Controller暴露 HTTP 接口RestController RequestMapping(/product) public class ProductController { private final ProductRepository productRepository; public ProductController(ProductRepository productRepository) { this.productRepository productRepository; } PostMapping(/save) public Product save(RequestBody Product product) { return productRepository.save(product); } GetMapping(/{id}) public Product findById(PathVariable String id) { return productRepository.findById(id).orElse(null); } GetMapping(/list) public ListProduct list() { ListProduct list new ArrayList(); productRepository.findAll().forEach(list::add); return list; } DeleteMapping(/{id}) public String delete(PathVariable String id) { productRepository.deleteById(id); return deleted; } }这个案例启动后可以用 curl 做一轮完整的自测curl -XPOST http://localhost:8080/product/save \ -H Content-Type: application/json \ -d {name:iPhone 15 Pro,category:手机,price:8999,description:6.1英寸 钛金属设计} curl http://localhost:8080/product/QtKJp3YBLgWxNnPE0jLh curl http://localhost:8080/product/list curl -XDELETE http://localhost:8080/product/QtKJp3YBLgWxNnPE0jLh这里有一个必须提前说明的差异findAll()在 ES 里不是 SQL 里直接返回全部记录。ES 搜索默认size是 10所以即使你插入了几十条数据findAll()也可能只返回前 10 条。这是 ES 的分页机制决定的不是数据丢了。如果需要拿更多要显式指定 Pageable。save方法也有个小细节入参实体的id可以手动指定也可以不传。不传时 ES 会自动生成一个 20 位随机 ID。我建议业务数据尽量手动指定业务主键比如商品 ID这样后续做增量同步、幂等写入都方便。自动生成的 ID 在日志排查时非常难对应到具体业务对象。这一段跑通后你已经完成了 ES 的写入、按 ID 查询、全量列表、删除四件事。简单是简单但这四个动作背后ES 已经完成了分词、倒排索引、路由计算、分片分发一整条链路。后面做搜索时你会越来越理解为什么 ES 做这个比 MySQL 的 like 查询快得多。5. 搜索不是 like命名查询、DSL 和分页排序的入门写法CRUD 只是热身搜索才是 ES 的看家本领。Spring Data ES 提供了两条路一种是方法名命名规则自动生成查询一种是用Query注解写完整的 JSON DSL。先看方法名命名查询。比如按名称搜商品在 Repository 里加上ListProduct findByName(String name);这个方法名会被拆成两部分关键字findBy和属性nameSpring Data 会生成一个match查询或term查询具体取决于字段类型和日期格式。这里很多新手会犯一个直觉错误以为findByName等同于 SQL 的%name%。实际上ES 会对name字段做分词默认标准分词器会把英文按空格和标点切词中文则整句作为一个个单字或连续 token 处理。所以findByName(iPhone 15 Pro)搜索的是分词后的匹配结果不是纯粹的模糊 query。组合查询也很自然ListProduct findByNameAndCategory(String name, String category); ListProduct findByCategoryAndPriceBetween(String category, Double min, Double max);Spring Data 会把多个条件组装成 bool 查询。如果你需要控制是must还是should方法命名就不够用了这时需要QueryQuery({\bool\:{\should\:[{\match\:{\name\:\?0\}},{\match\:{\category\:\?1\}}]}}) ListProduct searchByNameOrCategory(String name, String category);?0、?1是参数占位符按位置依次替换。这里的字符串是完整的 ES 查询 DSL不是 Lucene 语法。6.8.6 里这条 query 会被透传到 ES由 ES 引擎解析执行所以你先在 Kibana 的 Dev Tools 里调好 DSL再复制进Query调试效率最高。再看分页排序。ES 的分页跟 MySQL 的 LIMIT 是两套逻辑Spring Data 用Pageable统一封装GetMapping(/page) public PageProduct page( RequestParam(defaultValue 0) int page, RequestParam(defaultValue 10) int size) { Pageable pageable PageRequest.of(page, size, Sort.by(Sort.Direction.DESC, price)); return productRepository.findByCategory(手机, pageable); }特别注意排序这个坑。ES 6.8 里字符串字段默认会被映射成text和keyword两个子字段。text用于分词匹配keyword用于精确匹配和排序。如果你直接对name这个 text 字段排序会收到类似Fielddata is disabled on text fields by default.这个报错对新手特别不友好因为它不是语法错误而是 ES 出于性能考虑禁止给 text 字段做排序。正确做法是在 Repository 方法里按文档的字段路径写或者换一个 keyword 类型字段排序。实体里category通常更适合排序或者把name在查询时写成name.keyword。Spring Data ES 中排序写法可以这样Sort.by(Sort.Order.asc(category.keyword))为什么 ES 禁止 text 直接排序因为 text 字段经过分词后存储的是“词项集合”而不是原始串排序毫无意义而 keyword 字段保存原始字符串才能稳定排序。理解这一点后你就不会再为 fielddata 报错焦虑了。这里面还有一个和 MySQL 极具差异的规则ES 默认分页大小是 10也就是说不传 Pageable 时只返回 10 条。很多入门项目在findAll()时发现数据不齐还以为索引丢数据其实就是没理解这个默认值。ES 真的很像只给你看“第一页”而不是把全表倒给你。分页深度也是个值得留意的点。默认from size如果超过 10000ES 会直接报Result window is too large, from size must be less than or equal to: [10000]这跟 MySQL 的深分页问题本质一样ES 需要把每个分片的前 N 条全部拿回来再聚合排序N 越大开销越高。6.8 里解决这个问题的方法是search_after游标查询但 Spring Data ES 3.2.x 对它的封装不够优雅入门阶段先知道“有这个限制”就行了真正做后台翻页到上万条时再引 RestHighLevelClient 处理。搜索入门到这一步你已经能做单字段匹配、多条件组合、分页排序、以及用Query写 DSL。下一步要玩高亮、聚合、嵌套对象、自定义打分建议直接换成 RestHighLevelClient 手写 searchRequest灵活性完全是另一个级别。Spring Data 适合快速出活复杂查询还是直接 SDK 更顺手。6. 跑通之后一定要避开的六个坑同一个案例跑通后不同的人会在不同阶段遇到类似的问题。下面六个坑我全部亲手踩过按出现频率排序每一条都值得你收藏。第一个是NoNodeAvailableException。这个报错几乎 80% 是因为配置里cluster-name或cluster-nodes不对。排查链路我建议这样走先 curl ES 的 HTTP 接口curl http://127.0.0.1:9200/确认cluster_name是多少再查 9300 端口有没有监听Linux 上可以用ss -lntp | grep 9300最后确认 SpringBoot 配置里的cluster-nodes写的是不是127.0.0.1:9300。注意 Spring Data ES 3.2.x 对节点发现有一定缓存机制改完配置后重启应用不要只做热刷新。第二个是索引已存在但字段变更不生效。假设你第一次启动时实体里只有name后来加了price字段重新启动应用后ES 不会自动给老索引加字段。因为索引的 mapping 在创建时就定了。你会遇到查询新字段没有任何结果或者写入时报 mapper 错误。解决办法是删掉索引让它重建开发环境直接DELETE /product就行。但如果数据重要就要用 mapping 更新的 API 或者选重建索引的异步方案。结论在开发阶段实体结构变化频繁时宁可删索引也别指望热更新。第三个是删除文档后磁盘空间没减少。ES 删除操作只是把文档标记为删除真正释放空间要等 Lucene 的段合并。判断逻辑很简单deleteById后会看到文档查不到了但磁盘空间不变这是正常现象不用慌张。段合并由 ES 内部触发也可以主动调用 POST/product/_forcemerge强制合并。入门阶段不需要操作知道这个机制就够。第四个是 text 字段排序报错。这个上面讲过本质是 text 和 keyword 的分工不同。规避方式有两种实体里写Field(type FieldType.Keyword)把不需要分词的字段直接设为 keyword或者排序时使用name.keyword。我建议对“分类、状态码、商品编号”这类精确值字段直接标注成 keyword避免后续一堆和分词相关的意外。第五个是索引健康状态为 yellow。单机 ES 里最常见的原因是你建索引时写了replicas: 1但只有一个节点副本分片无法分配健康状态就降级。开发环境把实体注解里改成replicas 0即可。看到 green 状态不代表性能好只是说明所有分片都有可用副本单机学习场景 green 的意义不大别过度追求。第六个是 SpringBoot 升级造成的连带问题。如果你照着这个案例跑通后顺手把 SpringBoot 升到 2.7.x会发现spring.data.elasticsearch.cluster-nodes配置被废弃了甚至 Repository 默认底层都变了。这不是你的代码错了而是 Spring Data ES 的版本策略变了。老项目如果锁定 ES 6.8尽量不要动 SpringBoot 大版本否则连锁升级会让整个搜索模块重写一遍。这个教训我替换过很多次每次都要换掉一批 API。最后说点个人体会这套案例写完后我通常会把 Repository 里的复杂查询逐步迁移到 RestHighLevelClient 上。原因很简单Spring Data ES 适合标准 CRUD但一旦涉及高亮、聚合、脚本排序、search_after 这些高级玩法手写 SearchRequest 反而更清晰也脱离 SpringBoot 版本的绑架。ES 的查询 DSL 才是核心能力Spring Data 只是个便捷封装。另外案例跑通后的下一步我强烈建议给 ES 装上 IK 分词器并把商品名称的 analyzer 改成ik_max_word。中文搜索场景下没有 IK 和标准分词器的效果差距非常明显搜“手机壳”能不能匹配“手机”完全取决于分词策略。下载 IK 插件时注意版本必须严格对应 6.8.6插件目录放好后重启 ES再用_analyze接口验证分词效果就明白了。如果你拿这套组合做毕业设计或简历项目可以继续扩展用 Logstash 定时同步 MySQL 商品表到 ES再写一个带搜索高亮的前端页面基本就是一个功能完整的电商搜索模块了。有问题也欢迎在评论区交流各自的踩坑记录版本匹配这种事真的是多踩一次就多长一个记性。