资讯动态

Elasticsearch 7.6 移除 type 实战:数据迁移与 reindex 全攻略

发布时间:2026/10/16 21:29:45 来源:尧图企业网站定制
1. 先搞明白type到底是个什么东西为什么被“杀”掉了1.1 一个老掉牙的概念坑了多少人如果你维护过早期的 Elasticsearch 版本一定对PUT /blog/article/1这种索引路径不陌生。blog是索引名article是 type1是文档 ID。当年的设计是想让一个索引blog里同时装下article、comment、author这几类数据通过 type 把不同类型的文档分开把索引类比成数据库、type 类比成数据表概念上很好理解。但实际操作过就知道这个类比从根上就有问题。ES 的索引和数据库表不一样一个索引在物理上对应一个 Lucene 目录分片数、副本数、刷新频率都是按整个索引配置的。type 只是索引内部一个逻辑分组所有 type 的文档仍然活在同一个分片、同一个 Lucene 索引里底层没有做物理隔离。这就导致几个很尴尬的问题一个索引里某个 type 写爆了整个索引的写入、查询、缓存都会受影响不同的 type 用了同名字段但类型不一致mapping 直接冲突映射扁平化之后本来属于不同 type 的字段全都堆在一个宽表里字段数量爆炸查询时还有额外的过滤成本。我记得有一次帮某公司排查一个搜索慢的问题那个索引里有七八个 type最大的 type 几千万文档最小的才几千条但所有的数据都混在同一组分片上。搜索小 type 时还是要扫描整个索引的所有分片白白扛了不该扛的压力。这种事情出现几次之后你就会理解为什么官方后来对 type 越来越不留情面。1.2 官方为什么要移除 typeES 官方对 type 的态度是逐步收紧的5.x 开始强调单 type 最佳实践6.x 阶段一个索引已经只允许保留一个 type7.0 正式宣布移除 type8.x 则彻底清理了相关参数。整个演进背后最核心的考量是“物理存储和逻辑分组的错位”。Lucene 层的字段都是扁平化的ES 虽然允许不同 type 有不同的 mapping但底层还是同一个倒排索引。这意味着article里的title字段和comment里的title字段如果类型不一样必须想办法处理冲突最后往往只能共用一个字段定义设计者连分开约束字段的自由都没有。再往下看同一分片内所有 type 的文档共享倒排文件、共享 doc_values、共享缓存所谓“隔离”只是查询时用_type做了一个额外的过滤条件成本一点不少收益却几乎没有。官方在 7.x 的破坏性变更里写得很直白type 的隔离实现不彻底与其保留一个会误导人的概念不如引导大家用更现代的方式来组织数据。一个索引对应一类文档数据结构不同就应该拆索引如果需要保留类型维度就用普通字段存一个doc_type之类的标识。这个思路后来变成了社区的主流做法我自己在 7.6 项目里也完全是按这个路子走的。1.3 7.6 里的“type 遗影”和兼容层很多人望文生义觉得 7.6 里 type 已经完全消失了实际没那么绝对。7.6 版本正处在“兼容期”新建索引的时候API 默认不允许再传 type 层级但如果你在处理 6.x 时代创建的旧索引日常读写路径还是可以用传统的GET /index/type/id方式访问响应的_source里也会保留_type字段。这是因为跨大版本升级需要给用户留一个缓冲直接删干净会让很多人没法平滑升上来。这个阶段里最关键的参数就是include_type_name。创建一个新索引时如果请求里还带着 type 层级必须显式加?include_type_nametrue才能被接受不加就默认当成_doc处理。查询、mapping 展示也是同理想要看老索引里的 type 信息得在 API 里带上这个参数否则响应里看不到 type 节点。换句话说7.6 的现状是新代码一律走_doc或不带 type老数据还能喘一口气。千万别把“能跑”当成“不需要改”等哪天集群升级到 8.x这些兼容接口一旦消失旧客户端和旧脚本会立刻报错。所以赶在 7.6 做改造时机刚刚好。2. 迁移前先盘家底现状梳理与迁移路径选型2.1 第一步盘点存量索引的 type 使用情况正式动手之前一定要把集群里的 type 使用情况摸清楚这一步越细后面越省事。我一般先拉一份全索引清单确认哪些索引还在用多 type哪些其实早就只剩一个_doc了。用 Kibana 的 Dev Tools 也行直接敲 REST 命令更直观# 查看所有索引及文档量 GET _cat/indices?vsindex # 查看完整 mapping重点看 type 层级 GET _all/_mapping?include_type_nametrue返回的 mapping 里你会看到类似这样的结构{ blog: { mappings: { article: { properties: { title: { type: text } } }, comment: { properties: { content: { type: text } } } } } }看到这种结构说明blog索引里有两个 type。这时候我习惯顺手统计一下每个 type 的文档量可以用_count逐个查也可以按_type字段做一次聚合GET blog/_search { size: 0, aggs: { by_type: { terms: { field: _type } } } }这一步能直接看出每个 type 的体量差距方便后续评估重放数据和切流的时间成本。别只盯着 mapping 看还得把读写入口的 URL 模式、依赖这个索引的业务方清单一起列出来后面改代码时才不会漏。2.2 三条典型迁移路径对比按我自己做过的几次迁移主流方案大概是三条路各有各的适用场景。路径 A保留原索引名把多 type 合并成一个索引数据里增加一个自定义字段比如doc_type来标识原类型。好处是业务侧改动最小之前按blog索引查的数据还可以继续查代价是原本不同类型的字段必须合并到一套 mapping 里同名类型冲突要处理而且索引内部仍然混合存放多种数据本质上只是“治标”把 type 从元数据挪到了普通字段。路径 B按 type 把数据拆分到独立索引比如blog_article、blog_comment再通过别名或路由规则统一入口。好处是数据彻底隔离mapping 各自独立之后扩容、调参、冷热分离都能按索引来是最符合官方思路的长期方案代价是读写代码都要改原本一条语句跨 type 聚合的场景要做两次查询再合并。路径 C什么也不改升到 7.6 后继续用老 url 访问旧索引。这个我只建议当过渡不适合当长期方案。客户端里的 type 参数迟早要清掉而且 8.x 以后这些接口基本不可用最终还是要还债。把这三条路的代价放到一张表里看更直观对比维度路径A 合并索引自定义字段路径B 按type拆分索引路径C 继续用旧type数据层改动需要reindex并把type写入字段需要reindex并拆到多个索引不需要mapping复杂度高字段冲突要协调低各索引独立保持现状查询代价单索引查询灵活但字段冗余多跨类型查询要合并结果兼容查询但后续不可用应用代码改动中等较大最小长期维护一般好差2.3 我的选型建议选哪条路核心要看原索引里的 type 之间到底像不像。如果两个 type 的字段高度重合比如一个LogInfo和LogError本质上都是日志数据结构和查询模式都差不多那路径 A 就很合适。自定义一个level字段区分来源查询时按字段过滤比分开维护两个索引反而舒服。如果 type 之间八竿子打不着比如一个user和一个order字段差异极大还各有各的写入高峰那路径 B 是唯一正确的答案。硬要合成一个索引只会互相拖累到时候大小分片配比都不知道该偏向谁。还有一个容易忽略的场景如果你的索引其实已经只剩一个 type而且名字恰好就是_doc那恭喜你根本不需要任何数据改造只需要把客户端里的 type 参数清掉就行。很多团队折腾半天最后发现项目里早就不用多 type 了只是 Java 客户端里那段老旧的构建代码还在传 type属于自己有库存没盘点清楚。3. 数据迁移实操reindex、mapping改造与别名切换3.1 mapping 合并改造范例先看一个实际会遇到的旧 mapping 长什么样。某内容项目原来在content索引下放了article和comment两个 typearticle有title和authorcomment有content和author关键问题在于author这个字段在两个 type 里类型不一致article.author在 6.x 里是keyword因为要存作者 IDcomment.author是text因为存的是作者昵称这种情况下如果直接合并 mapping新索引里author只能保留一个类型另一个必须改名。我的处理方式是给comment的昵称字段改名为author_namearticle的 ID 字段保留为author。看起来多了一步改名动作其实是在替业务方梳理数据模型反而把以前同名不同义的历史债还清了。新索引的 mapping 大致是这样的PUT content_v1 { mappings: { properties: { doc_type: { type: keyword }, title: { type: text, fields: { keyword: { type: keyword } } }, author: { type: keyword }, author_name: { type: text }, content: { type: text } } } }注意我把原来的 type 层级直接去掉了mapping 顶层就是properties。这就是 7.6 的标准写法不需要再嵌套一层 type 名。3.2 用 reindex 把 type 写进自定义字段mapping 定好之后正式开始搬数据。最顺手的工具还是_reindex它能把一个索引的数据搬到另一个索引并通过 painless 脚本在搬的过程中修改文档内容。我们这步要做的事很简单把原来_type的值写到_source.doc_type里。POST _reindex { source: { index: content }, dest: { index: content_v1 }, script: { lang: painless, source: ctx._source.doc_type ctx._type; } }有两点经验要提前说一是不要贪省事直接全库一把梭。文档量大、分片多的情况下建议加上slices参数比如slices: 5让 reindex 并行跑速度能快好几倍但对集群的压力也会同步放大建议在业务低峰期操作。二是 reindex 过程中如果源索引还有写入reindex 快照到的数据可能不完整最后要用_doc号做增量补录或者干脆先把写入切到新索引再搬历史数据。我通常的做法是先停掉对旧索引的写入或者让业务方先把写入切到新索引再用 reindex 搬存量数据的一致性最有保障。3.3 用别名和索引模板平滑过渡数据搬完并不代表改造结束还要解决“老代码怎么无缝切到新索引”的问题。最常用的手段是索引别名。假设原来业务方读的是content索引我们新索引叫content_v1那就先建一个别名POST _aliases { actions: [ { add: { index: content_v1, alias: content } }, { remove_index: { index: content } } ] }这里content_v1成为真实索引content变成别名业务方的查询语句不用改一行数据已经从老的content切换到了新索引。如果因为合规或回滚要求老索引不能立刻删可以把remove_index这步省掉先只加别名确认稳定后再清理。同时建议顺手把索引模板配置好。尤其是索引命名里带日期的场景比如logs-2024.01.01提前写好一个全局模板PUT _template/logs_template { index_patterns: [logs-*], settings: { number_of_shards: 3, number_of_replicas: 1 }, mappings: { properties: { doc_type: { type: keyword } } } }以后每天新建的logs-*索引都会自动带上doc_type字段不用每次手动补 mapping。这个细节很不起眼但少了它等第二天日志索引一生成你才会发现新数据根本没写进自定义类型字段又是一轮排查。4. 应用层改造REST API 与客户端代码避坑记录4.1 REST API 的变化点清单数据层迁移做完应用层的代码如果不跟着改照样会有不少请求报错。REST API 层面的变化点主要集中在 URL 写法上项目里凡是写死 type 的地方都要替换。老写法普遍是这样的# 写入 PUT /content/article/1 # 查询 GET /content/article/_search # 删除 DELETE /content/article/17.6 之后的标准写法换成# 写入 PUT /content/_doc/1 # 查询 GET /content/_search # 删除 DELETE /content/_doc/1如果是升级前创建的老索引访问时可以带旧 type 名比如content/article/1但我强烈建议新代码一律用_doc。因为一旦后续再升级到 8.x老索引的老路径也会被移除到时候还得再改一轮。7.6 这个阶段就是“一次性改到位”的最后窗口别给自己留二次整改的作业。4.2 客户端代码改动的几个典型位置以 Java High Level REST Client 7.6 为例。旧代码里如果构建了IndexRequest常见写法是把 type 作为参数传进去新版本里IndexRequest已经完全不接受 type 参数直接把文档按_doc处理。// 旧写法 IndexRequest request new IndexRequest(content, article, 1); request.source(map, XContentType.JSON); // 新写法 IndexRequest request new IndexRequest(content); request.id(1); request.source(map, XContentType.JSON);Python 的elasticsearch-py也是同样逻辑index方法的doc_type参数在 7.x 后已经弃用不要继续传。Go、Ruby、PHP 的客户端类似建议项目里全局搜一下article、comment这些 type 名把硬编码全部替换成_doc或直接去掉。搜索请求也一样。旧代码如果是SearchRequest(content).types(article)新版直接删掉.types(...)如果原来习惯用TypeQueryBuilder这一类构建器也已经移除统一改用普通的QueryBuilder。这些 API 的删减都有兼容期但 IDE 里会直接标红编译期就能发现反而是好事。4.3 API 报错与排查方向改造期间最常见的报错有几种提前认识了能省不少排查时间。第一种是创建索引时报Type is completely unsupported说明你的请求里带了 type 层级但没开启兼容参数。如果是新索引把 URL 里的 type 段删掉即可特殊情况下确实需要兼容老 API才能在创建时显式加?include_type_nametrue但我只建议在临时脚本里这么干。第二种是_reindex时报Type [article] is not supported说明你尝试往一个新索引里写入带 type 的数据。这种情况多半是 reindex 的dest索引之前用老格式创建过或者有一些老旧脚本文档 ID 直接拼了 type。要把目标索引先删掉用新格式重新创建或者把写入请求里的 type 去掉。第三种是升级后原来的多 type 查询报No mapping found for [article]这类问题通常发生在老索引还留着旧 type但客户端已经按新格式访问。仔细看日志里用的 URL 是旧路径还是新路径两边对齐就好。5. 验证与回滚上线前必做的检查清单5.1 数据完整性校验reindex 跑完别急着切流量先做一轮数据完整性校验。最基础的是对比文档数GET content/_count GET content_v1/_count两个数量对不上就说明 reindex 漏了数据优先排查是不是源索引在搬的过程中还有写入。除了总数还要按自定义字段分组抽查确认doc_type被正确写入GET content_v1/_search { size: 0, aggs: { by_doc_type: { terms: { field: doc_type } } } }如果聚合结果里出现了空值或未知值说明有一部分旧文档没有执行脚本常见的坑是 painless 脚本在部分文档上抛了异常reindex 默认不是原子操作异常文档会被略过。真遇到这种情况针对空值文档单独做一次轻量 reindex 就行。5.2 功能回归与性能观察数据没问题接着做功能回归。我习惯列一张清单覆盖最常见的读写场景单文档查询、按 ID 删除、全文检索、聚合统计、分页翻页。每一条都用新旧地址各跑一遍确认结果一致。特别要关注带doc_type过滤的查询是否生效字段类型有没有选对否则写了等于白写。性能方面重点看 reindex 期间和切流后的对比。用_cat/indices?vsindex观察store.size、docs.count的变化再用_cat/thread_pool?v看write和search线程池的占用情况。如果新索引查询明显慢优先检查 mapping看是不是keyword被误配成了text或者该加keyword子字段的地方漏了。慢日志也要开启一下PUT content_v1/_settings { index.search.slowlog.threshold.query.info: 1s, index.indexing.slowlog.threshold.index.info: 1s }有了慢日志上线后就算用户不反馈服务端也能自己发现潜在的性能劣化。5.3 留好回滚策略改造这种操作永远要留退路。我的习惯是老索引在迁移期间一律不物理删除只改别名保留至少一周。如果上线后发现业务方某个功能对旧 type 路径还有硬依赖只需要把别名指回老索引就能快速回滚不需要动数据。如果老索引数据量太大、磁盘放不下那就用快照备份PUT _snapshot/backup_repo/content_backup { indices: content, ignore_unavailable: true, include_global_state: false }这样就算真的误删了数据也有完整的恢复途径。回滚这种事宁可多准备一点用不上也别在出问题时抓瞎。6. 常见问题速查迁移路上的那些“神坑”把我在多个项目里踩过的坑整理成一张速查表按频率排序遇到问题直接对号入座问题现象触发场景根本原因解决办法创建索引报 type unsupported新索引写入路径带类型请求里带了 type 层级去掉 type改用_docreindex 报 type not supported迁移脚本带 type 段目标索引或请求仍然旧格式重建目标索引请求去掉 type查询结果里_type丢失访问 7.6 的新索引新索引默认无自定义 type 元数据改用自定义doc_type字段mapping 里多个 type 字段类型冲突合并索引同名不同义的历史脏数据统一类型或改名同一索引两个 type 数据互相“污染”搜索小 type 很慢物理存储未隔离拆索引走路径 B聚合doc_type出现空值reindex 后校验painless 脚本异常跳过单独补录异常文档客户端编译报types()方法不存在Java client 升级7.x 移除了 types 方法删除 or 用_doc兜底老代码访问老索引路径 404升级后访问旧 type URL兼容层对新建索引失效改用_doc路径更新文档时_type被覆盖reindex 结束后用户更新写入脚本未处理写入时通过 doc_type 字段区分别名切换后未生效仍读到旧数据缓存或别名删除遗漏确认 alias 和索引名称唯一映射这张表里最容易被忽视的是第一条和第六条。很多团队在数据迁移时很上心但建新索引的代码里还留着旧时代的拼写习惯结果第一波日志就疯狂报错另一类则是 reindex 脚本里没写doc_type看起来搬完了实际等于白搬查询时根本无法区分数据类型。另外提一句迁移完不要马上删掉旧索引就以为万事大吉。建议在新索引运行至少一个完整业务周期比如 7 天或一个报表周期后再清理期间通过doc_type定期做新旧数据对比等确认完全没有差异再动删除按钮。毕竟数据迁移这种事回滚容易数据重建难。最后再分享一个实操中的小技巧迁移期间可以把include_type_name参数临时加到排查请求里比如GET content/_mapping?include_type_nametrue它能看到老索引的 mapping 残留结合_cat/indices就能判断哪些索引已经改造完成、哪些还在走旧模式。等全部业务切完再全局搜一遍代码仓库里的type硬编码确保没有漏网之鱼。我自己在几轮迁移里都是这套流程走下来基本能保证切换过程平滑、数据不丢、应用无感。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑