去年帮一个老项目做技术方案选型时,我再一次把组合定在了 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.x | 3.2.x | 6.8.x |
| 2.3.x | 4.0.x | 7.x |
| 2.4.x | 4.1.x | 7.6+ |
| 2.5.x - 2.6.x | 4.2.x - 4.4.x | 7.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 的两条端口,这也是入门者最容易混乱的地方:
| 端口 | 协议 | 用途 | 例子 |
|---|---|---|---|
| 9200 | HTTP REST | 所有 REST API、curl、Kibana 访问 | curl http://127.0.0.1:9200/product/_search |
| 9300 | Transport 协议 | 集群节点间通信、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如果是在云主机上跑 ES,max_map_count不够也会启动失败,报错信息里一般会提示vm.max_map_count [65530] is too low,这时需要执行:
sysctl -w vm.max_map_count=262144这个设置在重启后会失效,需要写入/etc/sysctl.conf才持久化。总的思路是:先看日志,再查系统参数,不要一上来就怀疑 SpringBoot 代码。
Kibana 我建议入门阶段装一个同版本 6.8.6,尤其是想看索引映射、调试 DSL 时,它比 curl 舒服太多。中文搜索场景后面再加 IK 分词器,这个我们后面细说。
3. 工程依赖与配置文件:三个最容易出事的约定
环境跑起来后,开始建 SpringBoot 工程。先说 pom.xml,我直接给出完整可用的坐标:
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.2.6.RELEASE</version> <relativePath/> </parent> <properties> <java.version>1.8</java.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-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.yml:
spring: 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 索引名,注意索引名必须小写。如果写成Product,ES 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 ElasticsearchRepository<Product, String> { List<Product> findByName(String name); List<Product> findByCategory(String category); List<Product> findByPriceBetween(Double min, Double max); List<Product> 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 List<Product> list() { List<Product> 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 里加上:
List<Product> findByName(String name);这个方法名会被拆成两部分:关键字findBy和属性name,Spring Data 会生成一个match查询或term查询,具体取决于字段类型和日期格式。这里很多新手会犯一个直觉错误:以为findByName等同于 SQL 的%name%。实际上,ES 会对name字段做分词,默认标准分词器会把英文按空格和标点切词,中文则整句作为一个个单字或连续 token 处理。所以findByName("iPhone 15 Pro")搜索的是分词后的匹配结果,不是纯粹的模糊 query。
组合查询也很自然:
List<Product> findByNameAndCategory(String name, String category); List<Product> findByCategoryAndPriceBetween(String category, Double min, Double max);Spring Data 会把多个条件组装成 bool 查询。如果你需要控制是must还是should,方法命名就不够用了,这时需要@Query:
@Query("{\"bool\":{\"should\":[{\"match\":{\"name\":\"?0\"}},{\"match\":{\"category\":\"?1\"}}]}}") List<Product> 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 Page<Product> 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如果超过 10000,ES 会直接报:
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,再写一个带搜索高亮的前端页面,基本就是一个功能完整的电商搜索模块了。有问题也欢迎在评论区交流各自的踩坑记录,版本匹配这种事,真的是多踩一次就多长一个记性。