简介围绕 Spring Boot 与 Elasticsearch 7.2.0 的整合实现这份 PDF 文档面向 Java 后端开发者帮助解决 Spring Boot 2.1.x 默认依赖仅支持 Elasticsearch 2.X而项目需要升级到 7.x 的版本兼容问题。文档首先分析 spring-boot-starter-data-elasticsearch 的版本限制说明弃用该依赖并改用 Spring-data-elasticsearch 的原因同时对比 transport 与 rest 两种连接方式明确官方推荐基于 HTTP 的 rest 方式并指出 transport 在 7.0 中已不建议使用、后续版本可能废弃。正文逐步给出 POM 中需要引入的 elasticsearch、elasticsearch-rest-client、elasticsearch-rest-high-level-client 三个 Maven 依赖在 application.yml 中通过 elasticsearch.ip 配置服务地址并提供基于 RestHighLevelClient 的完整客户端连接配置类代码包括连接超时、读取超时等参数设置。PDF 中的示例代码注释清晰可直接复制到项目中调整 IP 和超时时间后运行帮助快速完成整合验证避免在版本兼容与连接细节上反复踩坑。资源为单个 PDF 文件压缩包约 58KB内容聚焦适合作为项目集成时的参考手册。目前已有 6769 人学习下载有类似升级需求的开发者可以借助这份资料节省排查时间。1. 整合前先看清版本适配SpringBoot 与 Elasticsearch 7.2.0 的真实距离SpringBoot 整合 Elasticsearch 7.2.0 这件事最大的坑其实不在代码而在版本适配。Spring Boot 2.1.x 官方提供的 spring-boot-starter-data-elasticsearch 捆绑的 Elasticsearch 版本还停留在 2.x而 Elasticsearch 当时已经跑到了 7.2.x——连 transport 客户端都在 7.0 被官方点名不建议使用8.x 直接废弃。这意味着网上大部分 starter 写法在 7.2.0 上要么报 NoNodeAvailableException要么被一堆过时 API 卡住。这篇笔记适合使用 Elasticsearch 7.x、想在 Spring Boot 里用官方 REST 客户端做查询和高亮的开发者。下面把依赖、配置类、查询链路和坑位从头到尾过一遍代码可以直接抄。2. 依赖选择与版本锁定弃用 starter 之后三个 jar 该怎么配先说结论项目标题里写的「改为直接使用 Spring-data-elasticsearch」其实不太准确翻开实际代码你会发现真正的落点是elasticsearch-rest-high-level-client也就是 ES 官方的高层 REST 客户端。搞清楚这个概念后面所有依赖和配置才有意义。Spring Data 对 ES 的封装在 4.x 才对齐 7.x而在 Spring Boot 2.1.x 的时代starter 里捆绑的还是 2.x 的老客户端属于完全没法用的状态。所以这份工程走的是「弃用 starter、直连官方 client」的路线。2.1 transport 与 rest为什么 TCP 客户端在 7.x 被官方放弃Elasticsearch 的 Java 客户端历史上长期存在两种连接方式。transport client 通过 TCP 9300 端口访问集群它复用的是 ES 节点之间内部通信的那套协议所以对版本极其敏感——客户端是 7.2服务端升到 7.3 都可能出现兼容性告警跨大版本基本不能跑。而且 TCP 方式只支持 Java 语言等于把其他语言栈全部挡在门外。rest client 走 HTTP 9200 端口本质上是封装了 ES 的 REST API没有语言限制版本兼容性也宽松得多。ES 官方在 7.0 版本就把 transport client 标记为 deprecated并在 8.x 中彻底移除。这意味着 7.2.0 时代的新项目如果还抄 transport 的老代码就是往死路上走。rest 方式的另一个好处是排障直观任何请求本质上都是一个 HTTP 调用你可以用 curl 或者 Kibana 的 Dev Tools 先验证再落到代码里不用在黑匣子里猜问题。这个工程选择RestHighLevelClient正是基于这个背景。2.2 三个依赖的分工与版本统一官方 REST 客户端在 7.x 时期拆成了三个 artifact很多初次接触的人会困惑为什么要引三个 jar。实际上它们各管一段elasticsearch提供核心模型和查询构造器SearchSourceBuilder、QueryBuilders这些类都来自它elasticsearch-rest-client是底层 HTTP 连接组件负责连接池、请求执行和响应处理elasticsearch-rest-high-level-client在这两者之上封装出SearchRequest、SearchResponse这类高层 API。三个 jar 的版本必须完全一致否则运行时大概率出现序列化异常或者方法找不到。properties elasticsearch.version7.2.0/elasticsearch.version /properties dependencies dependency groupIdorg.elasticsearch/groupId artifactIdelasticsearch/artifactId version${elasticsearch.version}/version /dependency dependency groupIdorg.elasticsearch.client/groupId artifactIdelasticsearch-rest-client/artifactId version${elasticsearch.version}/version /dependency dependency groupIdorg.elasticsearch.client/groupId artifactIdelasticsearch-rest-high-level-client/artifactId version${elasticsearch.version}/version /dependency /dependencies用${elasticsearch.version}统一版本而不是各自写死数字是为了避免复制粘贴时改漏一个。这里有一个常见翻车点如果项目之前引入过spring-boot-starter-data-elasticsearch它的传递依赖会把elasticsearch拉回 2.x导致你明明配了 7.2.0 的依赖运行时却加载旧类。解决办法是在这个 starter 的 dependency 里加exclusions排除掉org.elasticsearch:elasticsearch和 transport 相关依赖。新版 Spring Boot 的 starter 已经不捆绑 7.x但 2.1.x 的老项目排查时一定要先看依赖树。2.3 Spring Boot 版本与 ES 版本的映射关系很多人被「springboot 版本太高」搞得头疼其实是因为 Spring Boot 官方 starter 和 ES 大版本之间存在明显的错位。整理一份常见映射关系方便对照Spring Boot 版本starter-data-elasticsearch 捆绑的 ES建议方案2.1.xES 2.x弃用 starter直接引官方 rest client2.2.x对齐 Spring Data ES 3.3.xES 6.x参考本篇方案2.3.x ~ 2.6.x对齐 Spring Data ES 4.xES 7.x可用 starter也可用官方 clientSpring Boot 3.x对齐 ES 8.x使用新 client代码结构不同Spring Data Elasticsearch 4.0 左右才开始对齐 ES 7.x也就是说 2.1.x 的 Boot 想用 Spring Data 的方式操作 7.2.0光升依赖是不够的可能要把整个 Spring Boot 版本拉高。对于老工程来说这种升级代价太大。所以更务实的做法就是像这份工程一样跳过 Spring Data 封装直接用官方高层客户端自己维护一个配置类。ES 官方对RestHighLevelClient的支持周期很长7.2 到 7.17 的核心 API 基本一致后面迁移也相对平滑。3. 客户端配置类超时参数与多节点解析的完整写法RestHighLevelClient 的初始化是整个整合的基石。它不像 starter 那样有自动配置兜底所有参数都要自己显式声明。这一章把配置文件和配置类拆开讲清楚尤其是多节点解析和超时参数这两块很多人在这里踩坑。3.1 application.yml逗号分隔一次注入多个节点elasticsearch: ip: 192.168.52.132:9200,192.168.52.133:9200这里的关键是 Spring 的Value注解支持把逗号分隔的字符串自动转换成String[]所以配置类里可以直接声明Value(${elasticsearch.ip}) String[] ipAddress。单节点环境下写192.168.52.132:9200即可多个节点用逗号分隔顺序任意。生产环境建议把地址集中放在 Nacos 或 Apollo 配置中心方便扩缩容时动态调整不用重启应用。另外有个细节值得注意Value注入数组在 Spring Boot 2.x 里工作正常但如果配置项不存在启动会直接报错。更稳妥的做法是定义一个ConfigurationProperties(prefix elasticsearch)的属性类把 ip 字段放进去配置缺失时有更友好的错误提示。不过对于一份演示工程来说Value写法最直观也最容易照着抄。3.2 RestHighLevelClient 配置类逐段拆解package com.dc.elastic.configuration; import org.apache.commons.lang3.StringUtils; import org.apache.http.HttpHost; import org.apache.http.client.config.RequestConfig; import org.elasticsearch.client.RestClient; import org.elasticsearch.client.RestClientBuilder; import org.elasticsearch.client.RestHighLevelClient; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import java.util.Arrays; import java.util.Objects; Configuration public class ElasticsearchRestClient { /** 超时时间设为 5 分钟 */ private static final int TIME_OUT 5 * 60 * 1000; private static final int ADDRESS_LENGTH 2; private static final String HTTP_SCHEME http; Value(${elasticsearch.ip}) String[] ipAddress; Bean public RestClientBuilder restClientBuilder() { HttpHost[] hosts Arrays.stream(ipAddress) .map(this::makeHttpHost) .filter(Objects::nonNull) .toArray(HttpHost[]::new); return RestClient.builder(hosts); } Bean(name highLevelClient) public RestHighLevelClient highLevelClient(Autowired RestClientBuilder restClientBuilder) { restClientBuilder.setRequestConfigCallback(new RestClientBuilder.RequestConfigCallback() { Override public RequestConfig.Builder customizeRequestConfig(RequestConfig.Builder requestConfigBuilder) { return requestConfigBuilder .setConnectTimeout(TIME_OUT) .setSocketTimeout(TIME_OUT); } }); return new RestHighLevelClient(restClientBuilder); } private HttpHost makeHttpHost(String s) { assert StringUtils.isNotEmpty(s); String[] address s.split(:); if (address.length ADDRESS_LENGTH) { String ip address[0]; int port Integer.parseInt(address[1]); return new HttpHost(ip, port, HTTP_SCHEME); } else { return null; } } }这段配置类做三件事解析ip:port字符串为HttpHost数组、构建RestClientBuilder、再定制超时参数后生成RestHighLevelClient。makeHttpHost按冒号切分地址长度必须是 2否则返回 null 并在后续被filter(Objects::nonNull)过滤掉——这样配置里混入脏数据时不会直接让启动崩溃最多是少一个节点。Bean(name highLevelClient)指定 bean 名称是避免容器里存在多个 ES 客户端时注入歧义Controller 里注入时最好也用Qualifier(highLevelClient)显式声明。原代码里有System.err.println(ipAddress)这种调试输出实际跑起来会打出一串数组哈希而不是地址内容要看真实值得用Arrays.toString(ipAddress)。这类调试语句在排查问题时容易误导删掉或者改成日志输出更合适。3.3 超时三兄弟connectTimeout、socketTimeout、connectionRequestTimeoutHTTP 客户端的超时参数有三个很多人只认 socketTimeout导致连接迟迟建不起来时请求干挂在那边。整理成表方便对照参数默认值作用建议connectTimeout1000ms与 ES 节点建立 TCP 连接的超时10s 以内足够socketTimeout30000ms等待响应数据的超时短查询 60s长查询 5 分钟connectionRequestTimeout默认为 -1从连接池获取连接的超时10s 左右原配置里只设置了setSocketTimeout(TIME_OUT)也就是 5 分钟这是为慢查询预留的宽裕值。如果接口基本都是秒级返回的小查询5 分钟反而会让调用方长时间挂起不如拆开设置connectTimeout 给 10 秒socketTimeout 给 60 秒个别大聚合查询再单独放宽。老版本RequestConfig.Builder里设置这三个参数的方法都在只是有些人容易把setSocketTimeout当成唯一选项来用。4. 搜索查询实战BoolQuery、分页与高亮从组装到解析配置类跑通后真正的业务逻辑集中在查询这块。ES 7.x 的 Java 客户端查询链路是SearchRequest指定索引.source(SearchSourceBuilder)挂载查询条件QueryBuilders构造各种查询最后highLevelClient.search()执行并解析SearchResponse。这一章把查询组装、动态条件、分页边界和高亮取值的完整链路过一遍。4.1 从 SearchRequest 到 SearchSourceBuilder 的组装顺序SearchRequest searchRequest new SearchRequest(vw_ods); SearchSourceBuilder searchSourceBuilder new SearchSourceBuilder(); searchSourceBuilder.size(pageSize); searchSourceBuilder.from((pageIndex - 1) * pageSize); BoolQueryBuilder boolBuilder QueryBuilders.boolQuery(); boolBuilder.must(QueryBuilders.matchQuery(clearacctname, keyword)); searchSourceBuilder.query(boolBuilder); searchRequest.source(searchSourceBuilder);SearchRequest构造时传入索引名7.x 里不需要再调types()指定类型因为 mapping type 在 7.0 已经移除这也是原代码里searchRequest.types(indexName)被注释掉的原因。SearchSourceBuilder是查询的核心载体分页参数、查询条件和高亮都挂在它上面最后通过searchRequest.source(...)绑定。这个顺序是固定的先建请求再建 source builder最后组装。如果先执行searchRequest.source()再修改searchSourceBuilder常见做法是重新绑定一次避免修改不生效。4.2 BoolQueryBuilder用 Map 动态构造 must 条件private void queryBuilder(Integer pageIndex, Integer pageSize, MapString, Object query, String indexName, SearchRequest searchRequest) { if (query ! null !query.keySet().isEmpty()) { SearchSourceBuilder searchSourceBuilder new SearchSourceBuilder(); if (pageIndex ! null pageSize ! null) { searchSourceBuilder.size(pageSize); pageIndex pageIndex 0 ? 1 : pageIndex; searchSourceBuilder.from((pageIndex - 1) * pageSize); } BoolQueryBuilder boolBuilder QueryBuilders.boolQuery(); query.keySet().forEach(key - { boolBuilder.must(QueryBuilders.matchQuery(key, query.get(key))); }); searchSourceBuilder.query(boolBuilder); HighlightBuilder highlightBuilder new HighlightBuilder(); HighlightBuilder.Field highlightField new HighlightBuilder.Field(clearacctname) .preTags(strong) .postTags(/strong); highlightField.highlighterType(unified); highlightBuilder.field(highlightField); searchSourceBuilder.highlighter(highlightBuilder); searchRequest.source(searchSourceBuilder); } }这段代码把查询条件放在MapString, Object里遍历 keySet 动态生成 must 子句。好处是业务方可以按需传字段不用为每个查询条件写一套固定代码。但有两个细节需要注意matchQuery是分词匹配对字符串字段会走 analyzer对数字字段等价于精确匹配所以同一个方法在不同字段上的表现不一样如果传入的 Map 为空if判断会直接跳过最终执行的是不带 query 的查询ES 默认返回 match_all 的全部结果。很多人在线上发现「明明传了空条件却查出全量数据」就是栽在这里建议空条件时显式抛参数异常或者改用QueryBuilders.matchAllQuery()。4.3 分页边界pageIndex 为 0 时 from 变负数原工程的分页代码隐藏着一个真实的 bug先判断pageIndex 0后把 pageIndex 赋值为 0再执行from((pageIndex - 1) * pageSize)。当 pageIndex 传入 0 时from 就变成了-pageSize请求直接报from is negative的 IllegalArgumentException。正确写法是在计算前把页码归一化到从 1 开始int safePageIndex (pageIndex null || pageIndex 0) ? 1 : pageIndex; searchSourceBuilder.size(pageSize); searchSourceBuilder.from((safePageIndex - 1) * pageSize);另外一个更隐蔽的限制是from size不能超过index.max_result_window默认是 10000。也就是说页码超过 2000 页每页 5 条时会收到Result window is too large的报错。这不是用setFrom能绕过的深分页场景需要改用search_after或 scroll。原工程的 pageSize 只有 5日常没事但如果之后调大 pageSize 或者页码很深这个坑一定会出现。4.4 高亮取值完整链路与 fragments 空数组问题SearchResponse response highLevelClient.search(searchRequest, RequestOptions.DEFAULT); for (SearchHit hit : response.getHits().getHits()) { MapString, Object map hit.getSourceAsMap(); map.put(id, hit.getId()); MapString, HighlightField highlightFields hit.getHighlightFields(); HighlightField highlight highlightFields.get(clearacctname); if (highlight ! null highlight.fragments() ! null highlight.fragments().length 0) { map.put(highlight, highlight.fragments()[0].string()); } }高亮响应解析有几个容易忽略的细节。hit.getHighlightFields()返回的 Map 里key 是字段名value 是HighlightField。如果高亮没有命中任何内容highlight本身可能就是 nullhighlight.fragments()返回的是Text[]同一个字段多值时会返回多个片段取fragments[0]之前必须判空。原代码里直接fragments[0].string()的写法在无高亮结果时必然抛空指针或数组越界。另外HighlightBuilder.Field默认requireFieldMatch为 true即只有查询字段与高亮字段完全一致时才返回高亮片段。如果查询的是clearacctname而高亮字段写成title高亮结果就是空的。原工程里恰好存在这个字段不一致的问题属于典型的复制粘贴翻车现场。5. 避坑排查版本冲突、深分页与高亮取空的 6 个现场这一章把实际跑这个工程时最容易遇到的报错和异常按「现象 → 原因 → 解决」列出来。每一条都来自真实环境不是教科书式的理论推演。5.1 NoNodeAvailableExceptionES 没起、端口不通、Windows 启动关窗口现象应用启动正常但第一次调用 search 就抛NoNodeAvailableException提示None of the configured nodes are available。原因RestHighLevelClient 在请求时发现所有配置的节点都无法连接。常见原因有三个ES 服务没启动9200 端口被防火墙拦截Windows 上启动 ES 后直接关掉了命令行窗口导致节点下线。很多人以为 ES 启动完成就万事大吉其实双击elasticsearch.bat启动的进程是挂在当前控制台下的CtrlC 或者关窗口会把进程一并带走。解决先确认节点存活curl http://192.168.52.132:9200能返回 JSON 才算通。Windows 环境建议用elasticsearch-service.bat install把 ES 注册成系统服务避免窗口误关。排查时顺手看下ipAddress配置是否被Value正确注入用调试日志打印Arrays.toString(ipAddress)确认数组内容而不是打印数组哈希值。5.2 NoClassDefFoundErrorstarter 传递依赖把版本拉回 2.x现象pom 里明明引了 7.2.0 的三个依赖运行时却报NoClassDefFoundError: org/elasticsearch/action/search/SearchRequest或者ClassNotFoundException。原因项目里同时存在spring-boot-starter-data-elasticsearch和官方 client 的依赖starter 的传递依赖把elasticsearch的版本拉回 2.x。两个大版本的类结构差异很大类加载器加载到旧版 jar自然找不到 7.x 才有的类。Maven 的依赖仲裁规则里声明顺序和深度都会影响最终生效的版本仅靠 properties 里写版本号压不住传递依赖。解决用mvn dependency:tree查看依赖树确认org.elasticsearch:elasticsearch实际解析到哪个版本。如果确认是 starter 引入的旧版在这个 starter 的exclusions里排除elasticsearch、elasticsearch-transport等传递依赖。更干净的做法是直接把 starter 依赖移除因为既然用了官方 clientspring-boot-starter-data-elasticsearch 的自动配置已经没有任何价值留着只会添乱。5.3 7.x 移除 typessearchRequest.types() 被注释的原因现象网上搜到的代码里有searchRequest.types(vw_ods)照抄后编译不过或者运行报错错误信息提示 types 相关 API 已移除。原因ES 7.0 起正式移除 mapping type 概念一个索引下不再支持多个 type。SearchRequest.types()在 6.x 是可行的到了 7.x 已经删掉。原工程里把这行代码注释掉正是因为在新版本下它已经不存在了。解决直接不写types()SearchRequest构造时传索引名即可。如果是从 6.x 迁移上来的老代码需要检查所有地方是否都删干净了包括创建索引时指定的 type 名称和 mapping 里的_type字段。7.x 之后索引的_doc这类命名也没有意义了。5.4 高亮取不到或直接空指针requireFieldMatch 与 null 判断现象查询能返回结果但hit.getHighlightFields().get(字段名)得到 null或者调用fragments()时空指针。原因两个独立问题叠加。第一HighlightBuilder.Field默认requireFieldMatchtrue查询字段与高亮字段不一致时ES 不会返回该字段的高亮片段。第二即便字段匹配如果该字段的高亮内容为空数组fragments[0]依然会炸。解决高亮字段与查询字段保持同名确认要跨字段高亮时设置requireFieldMatch(false)。取值前强制判空先判断highlight ! null再判断fragments().length 0最后才取[0]。我一般会写一个小工具方法封装高亮取值统一处理 null 和空数组避免每个业务方法里重复写判空逻辑。5.5 Result window is too large深分页撞上 10000 条上限现象页码大了之后控制台报Result window is too large, from size must be less than or equal to: [10000]。原因ES 默认index.max_result_window为 10000from size超过这个值会被直接拒绝。这不是报错信息里常见的「分页越界」而是 ES 防止深分页拖垮内存和磁盘的自我保护机制。from越大协调节点需要排序的数据就越多10000 这个默认值对大多数业务够用。解决浅分页场景可直接调大max_result_window比如PUT /vw_ods/_settings { index.max_result_window: 20000 }这相当于临时放宽限制。如果业务确实要翻几百页甚至上万页就该换search_after或者 scroll 方案了。search_after适合实时翻页scroll 适合批量导出。原工程的 from/size 写法在页数不深时没问题但上线前一定要评估最大页数别等线上告警了再补救。5.6 版本不一致导致的反序列化异常现象查询执行成功但解析SearchResponse时抛SerializationException提示无法反序列化某个类。原因三个依赖 jar 版本不一致比如elasticsearch是 7.2.0、rest-high-level-client是 7.3.0。高层客户端序列化响应时用了新版类旧版核心模型无法识别运行时直接炸。解决三个依赖版本必须完全一致统一用${elasticsearch.version}占位符管理。升级时只改 properties 里一处即可。另外注意服务端版本与客户端版本不必严格一致——ES 官方保证 7.x 范围内的小版本兼容但跨大版本7.x 客户端连 6.x 服务端同样会出问题。建索引和查询前先确认服务端版本再定客户端版本。6. 进阶把查询封装成通用方法再加一个 DSL 验证习惯前面的代码虽然是完整可跑的但 Controller 里堆着查询组装逻辑时间长了会失控。我一般会把搜索收敛成一个通用方法把索引名、条件 Map、分页参数、高亮开关都作为参数传进去这样业务方只需要维护查询条件不需要关心 SearchSourceBuilder 的内部组装。public ListMapString, Object search(String indexName, MapString, Object conditions, int pageIndex, int pageSize, boolean highlight) throws IOException { SearchRequest searchRequest new SearchRequest(indexName); SearchSourceBuilder sourceBuilder new SearchSourceBuilder(); int safePage Math.max(pageIndex, 1); sourceBuilder.from((safePage - 1) * pageSize); sourceBuilder.size(pageSize); if (conditions ! null !conditions.isEmpty()) { BoolQueryBuilder bool QueryBuilders.boolQuery(); conditions.forEach((field, value) - bool.must(QueryBuilders.matchQuery(field, value))); sourceBuilder.query(bool); } if (highlight) { HighlightBuilder hb new HighlightBuilder(); conditions.keySet().forEach(field - { HighlightBuilder.Field f new HighlightBuilder.Field(field) .preTags(strong).postTags(/strong); f.highlighterType(unified); hb.field(f); }); sourceBuilder.highlighter(hb); } searchRequest.source(sourceBuilder); SearchResponse response highLevelClient.search(searchRequest, RequestOptions.DEFAULT); ListMapString, Object result new ArrayList(); for (SearchHit hit : response.getHits().getHits()) { MapString, Object source hit.getSourceAsMap(); source.put(id, hit.getId()); if (highlight) { hit.getHighlightFields().forEach((field, hf) - { if (hf.fragments() ! null hf.fragments().length 0) { source.put(highlight_ field, hf.fragments()[0].string()); } }); } result.add(source); } return result; }这段封装把一个完整的查询流程压缩成一个方法最关键的是高亮条件直接从conditions的 keySet 取从源头避免了「查询字段和高亮字段不一致」的低级问题。字段需要独立配置高亮样式时再重载一个带SetString highlightFields的方法即可。另外养成了一个习惯每写一个 ES 查询先在 Kibana 的 Dev Tools 里把 query DSL 跑通再翻译成 Java 代码。比如先执行GET /vw_ods/_search验证 match 条件和高亮片段返回是否符合预期再照着 DSL 去改 SearchSourceBuilder。这个习惯帮我挡掉了至少一半的查询翻车——ES 的查询语义用 JSON 看最直观Java 链式调用反而容易掩盖真正的查询结构。项目上线后也是一样出问题先抓 Kibana 里的 slow log再回来看代码定位效率高得多。从那以后我每接一个 ES 查询需求都强制自己在 Dev Tools 里把 DSL 调通一遍再写客户端代码连高亮字段都先在响应里确认有值才落进 Java。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?