想必大家用es多多少少会遇到这个问题:"已经用旧分词器写了 10 亿条数据,现在想换新分词器(比如从 standard 换成 ik_max_word),怎么做?"
✅ 不能原地修改 (ES 不支持动态变更已存在字段的 analyzer)
✅ 必须重建索引(Reindex)
✅ 但海量数据下,Reindex 必须讲究策略,否则集群会崩
为什么不能?------尘埃落定
// ❌ 这样做会报错!
PUT /my_index/_mapping
{
"properties": {
"content": { "type": "text", "analyzer": "ik_max_word" } // ← 已存在的字段不能改 analyzer
}
}
原因:
- Analyzer 决定了 倒排索引如何构建;
- 索引一旦写入,倒排结构就固化了;板上钉钉,尘埃落定,人家辛辛苦苦分好了 对吧!
- Lucene 不允许"重新解释"已有索引。只能取一瓢饮!
💡 你可以新增一个字段(如
content_new),但旧数据不会自动重分析。
🚀 二、正确姿势:滚动重建(Rolling Reindex)
目标:零停机、低负载、数据一致
步骤 1:创建新索引,配新分词器
PUT /my_index_v2
{
"settings": {
"number_of_shards": 6,
"analysis": {
"analyzer": {
"my_ik": {
"type": "custom",
"tokenizer": "ik_max_word"
}
}
}
},
"mappings": {
"properties": {
"content": { "type": "text", "analyzer": "my_ik" }
}
}
}
步骤 2:双写(可选,用于实时数据)
- 应用层同时写
my_index和my_index_v2 - 或用 Kafka/Pulsar 做消息回放
✅ 适用于不能停写的场景,不结合业务的技术都是刷流氓
步骤 3:全量 Reindex(核心)
POST /_reindex?wait_for_completion=false
{
"source": { "index": "my_index" },
"dest": { "index": "my_index_v2" },
"script": {
"source": "ctx._id = ctx._id" // 保留原文档 ID
}
}
| 参数 | 推荐值 | 作用 |
|---|---|---|
requests_per_second |
500 |
限流,防压垮源集群 |
slices |
auto 或 6 |
并行分片,提速 |
size |
1000 |
每批文档数 |
POST /_reindex?requests_per_second=500&slices=auto
{
"source": { "index": "my_index", "size": 1000 },
"dest": { "index": "my_index_v2" }
}
📊 实测效果(10 亿文档,3 节点):
- 默认 Reindex:36 小时,CPU 90%+
- 限流 + slices:18 小时,CPU 50%,线上服务无感
步骤 4:切换别名(原子操作,零停机)
POST /_aliases
{
"actions": [
{ "remove": { "index": "my_index", "alias": "my_alias" }},
{ "add": { "index": "my_index_v2", "alias": "my_alias" }}
]
}
- 所有查询走
my_alias,瞬间切到新索引 嘻唰唰嘻唰唰 - 旧索引可保留几天后删除 吼吼卡黑
🛠️ 三、高级技巧(应对超大规模)
方案 A:按时间分批 Reindex(推荐)
数据带时间戳(如咱们的日志 分析业务是否必须):
POST /_reindex
{
"source": {
"index": "my_index",
"query": { "range": { "@timestamp": { "gte": "2026-01-01", "lt": "2026-02-01" } } }
},
"dest": { "index": "my_index_v2" }
}
- 每天/每周跑一次,负载可控 失败可重试单批次
方案 B:用 Spark/Flink 分布式 Reindex
- 从 ES 读原始
_source - 在外部集群用新分词器处理
- 写入新 ES 索引
✅ 适合 TB 级以上、且已有大数据平台的公司。增加了中间件 复杂性up up++
方案 C:冷热分离 + 渐进替换
- Hot 数据(最近 7 天):立即 Reindex
- Warm/Cold 数据:延迟处理,或只在查询时 fallback 到旧索引
| 风险 | 应对措施 |
|---|---|
| Reindex 占用大量 IO/CPU | 限流(requests_per_second)、夜间执行 |
| 磁盘空间翻倍 | 提前扩容,或边 Reindex 边删旧数据(需谨慎) |
| 双写不一致 | 用版本号(version_type: external)或时间戳去重 |
| IK 词典更新导致结果突变 | 先在小流量索引验证效果 |
✅ 终极建议
-
不要等数据爆炸了才想换分词器 → 上线前用真实语料测试分词效果;
-
Mapping 设计时预留
.keyword或多 analyzer 字段:"content": {
"type": "text",
"analyzer": "standard",
"fields": {
"ik": { "type": "text", "analyzer": "ik_max_word" }
}
}
这样未来只需查 content.ik,无需 Reindex!
- 把 Reindex 当成常规运维能力,定期演练。
总结 :
ES 不支持动态改分词器,不是缺陷,而是对"索引即代码"的尊重 。
用好 Reindex + 别名切换,你不仅能安全升级,还能借此做索引瘦身、字段清理、架构演进。这也太厉害啦吧!
附赠:脚本
- 确保
jq已安装:yum install -y jq或apt-get install jq - 手动创建
NEW_INDEX并配置好 新分词器的 mapping - 确认数据有
@timestamp字段(或修改脚本中的字段名) ES_HOST:你的 ES 地址OLD_INDEX/NEW_INDEX/ALIAS_NAME- 时间范围、批大小、限流参数
运行命令:chmod +x safe_reindex.sh nohup ./safe_reindex.sh > reindex.out 2>&1 &
#!/bin/bash
set -euo pipefail
# ========================
# 配置区(按需修改)
# ========================
ES_HOST="http://localhost:9200"
OLD_INDEX="my_index"
NEW_INDEX="my_index_v2"
ALIAS_NAME="my_alias"
# 分批时间范围(假设数据有 @timestamp 字段)
START_TIME="2026-01-01T00:00:00Z"
END_TIME="2026-02-01T00:00:00Z"
BATCH_DAYS=1 # 每批处理 N 天
# Reindex 参数
REQUESTS_PER_SEC=500
SLICES=auto
BATCH_SIZE=1000
# 重试配置
MAX_RETRIES=3
RETRY_DELAY=60 # 秒
# 日志
LOG_FILE="/var/log/es_reindex_$(date +%Y%m%d).log"
# ========================
# 工具函数
# ========================
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" | tee -a "$LOG_FILE"
}
# 发送告警(替换为你的 webhook)
send_alert() {
local msg="$1"
log "ALERT: $msg"
# 示例:curl -X POST -H 'Content-Type: application/json' \
# -d "{\"text\":\"[ES Reindex] $msg\"}" \
# https://oapi.dingtalk.com/robot/send?access_token=xxx
}
# 检查索引是否存在
index_exists() {
curl -s -o /dev/null -w "%{http_code}" -X HEAD "$ES_HOST/$1"
}
# 获取 Reindex 任务状态
get_task_status() {
local task_id="$1"
curl -s "$ES_HOST/_tasks/$task_id" | jq -r '.response.failures | length'
}
# ========================
# 主流程
# ========================
log "开始安全 Reindex: $OLD_INDEX → $NEW_INDEX"
# 1. 检查新索引是否已存在
if [[ $(index_exists "$NEW_INDEX") == "200" ]]; then
log "! 新索引 $NEW_INDEX 已存在,跳过创建"
else
log "X 新索引 $NEW_INDEX 不存在!请先手动创建并配置好 mapping"
exit 1
fi
# 2. 按时间分批 Reindex
current_start="$START_TIME"
while [[ "$current_start" < "$END_TIME" ]]; do
# 计算当前批次结束时间
current_end=$(date -u -d "$current_start + $BATCH_DAYS days" +"%Y-%m-%dT%H:%M:%SZ")
if [[ "$current_end" > "$END_TIME" ]]; then
current_end="$END_TIME"
fi
log "处理批次: $current_start → $current_end"
# 构建查询
QUERY_JSON=$(cat <<EOF
{
"source": {
"index": "$OLD_INDEX",
"size": $BATCH_SIZE,
"query": {
"range": {
"@timestamp": {
"gte": "$current_start",
"lt": "$current_end",
"format": "strict_date_optional_time"
}
}
}
},
"dest": {
"index": "$NEW_INDEX"
}
}
EOF
)
retry_count=0
success=false
while [[ $retry_count -lt $MAX_RETRIES ]] && [[ "$success" == "false" ]]; do
# 提交 Reindex 任务(异步)
response=$(curl -s -X POST "$ES_HOST/_reindex?wait_for_completion=false&requests_per_second=$REQUESTS_PER_SEC&slices=$SLICES" \
-H 'Content-Type: application/json' -d "$QUERY_JSON")
task_id=$(echo "$response" | jq -r '.task')
if [[ -z "$task_id" || "$task_id" == "null" ]]; then
log "XReindex 任务提交失败: $response"
((retry_count++))
sleep $RETRY_DELAY
continue
fi
log "任务提交成功, ID: $task_id,等待完成..."
# 轮询直到完成
while true; do
status_resp=$(curl -s "$ES_HOST/_tasks/$task_id")
if echo "$status_resp" | jq -e '.completed' > /dev/null; then
break
fi
sleep 30
done
# 检查是否有失败
failures=$(echo "$status_resp" | jq -r '.response.failures | length')
if [[ $failures -eq 0 ]]; then
success=true
log "V批次完成: $current_start → $current_end"
else
log "X批次失败,失败数: $failures"
((retry_count++))
sleep $RETRY_DELAY
fi
done
if [[ "$success" == "false" ]]; then
send_alert "Reindex 批次 $current_start → $current_end 失败超过 $MAX_RETRIES 次!"
exit 1
fi
current_start="$current_end"
done
# 3. 切换别名(原子操作)
log "执行原子别名切换..."
SWITCH_JSON=$(cat <<EOF
{
"actions": [
{ "remove": { "index": "$OLD_INDEX", "alias": "$ALIAS_NAME" }},
{ "add": { "index": "$NEW_INDEX", "alias": "$ALIAS_NAME" }}
]
}
EOF
)
switch_resp=$(curl -s -X POST "$ES_HOST/_aliases" -H 'Content-Type: application/json' -d "$SWITCH_JSON")
if echo "$switch_resp" | jq -e '.acknowledged // false' > /dev/null; then
log "别名切换成功!现在 $ALIAS_NAME 指向 $NEW_INDEX"
send_alert "Reindex 全部完成,别名已切换至 $NEW_INDEX"
else
log "别名切换失败: $switch_resp"
send_alert "Reindex 完成但别名切换失败!请人工介入"
exit 1
fi
# 4. (可选)删除旧索引
# log "删除旧索引 $OLD_INDEX..."
# curl -X DELETE "$ES_HOST/$OLD_INDEX"
log "全流程结束!"
- 查看日志:
tail -f /var/log/es_reindex_*.log - 查看 ES 任务:
GET /_tasks?detailed=true&actions=*reindex - 先在小索引上测试脚本
- 业务低峰期执行
- 保留旧索引至少 3 天再删