Elasticsearch更新了分词器之后

想必大家用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_indexmy_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 auto6 并行分片,提速
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" }
}
  • 每天/每周跑一次,负载可控 失败可重试单批次
  • 从 ES 读原始 _source
  • 在外部集群用新分词器处理
  • 写入新 ES 索引

✅ 适合 TB 级以上、且已有大数据平台的公司。增加了中间件 复杂性up up++

方案 C:冷热分离 + 渐进替换
  • Hot 数据(最近 7 天):立即 Reindex
  • Warm/Cold 数据:延迟处理,或只在查询时 fallback 到旧索引
风险 应对措施
Reindex 占用大量 IO/CPU 限流(requests_per_second)、夜间执行
磁盘空间翻倍 提前扩容,或边 Reindex 边删旧数据(需谨慎)
双写不一致 用版本号(version_type: external)或时间戳去重
IK 词典更新导致结果突变 先在小流量索引验证效果

✅ 终极建议

  1. 不要等数据爆炸了才想换分词器 → 上线前用真实语料测试分词效果;

  2. 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 jqapt-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 天再删
相关推荐
xiaobaibai1532 小时前
决策引擎深度拆解:AdAgent 用 CoT+RL 实现营销自主化决策
大数据·人工智能
悟纤2 小时前
学习与专注音乐流派 (Study & Focus Music):AI 音乐创作终极指南 | Suno高级篇 | 第33篇
大数据·人工智能·深度学习·学习·suno·suno api
ESBK20252 小时前
第四届移动互联网、云计算与信息安全国际会议(MICCIS 2026)二轮征稿启动,诚邀全球学者共赴学术盛宴
大数据·网络·物联网·网络安全·云计算·密码学·信息与通信
Elastic 中国社区官方博客2 小时前
Elasticsearch:Workflows 介绍 - 9.3
大数据·数据库·人工智能·elasticsearch·ai·全文检索
B站_计算机毕业设计之家2 小时前
豆瓣电影推荐系统 | Python Django Echarts构建个性化影视推荐平台 大数据 毕业设计源码 (建议收藏)✅
大数据·python·机器学习·django·毕业设计·echarts·推荐算法
莽撞的大地瓜3 小时前
洞察,始于一目了然——让舆情数据自己“说话”
大数据·网络·数据分析
证榜样呀3 小时前
2026 中专大数据技术专业可考的证书有哪些,必看!
大数据·sql
星辰_mya3 小时前
Elasticsearch主分片数写入后不能改
大数据·elasticsearch·搜索引擎
班德先生3 小时前
深耕多赛道品牌全案策划,为科技与时尚注入商业表达力
大数据·人工智能·科技