AI 生成声明:本文内容由 AI 生成或经 AI 辅助完成,请注意甄别。
背景
一套 3 节点的 Elasticsearch 7.17 集群跑在 Docker 里,节点分布在三台内网机器上,容器用 --net=host 起、数据目录挂载在宿主机。现在需要给 IK 分词器导入业务自定义词典,让"图计算引擎""向量数据库"这类专有名词不被切碎。
最初的直觉方案很朴素:改 IK 的 IKAnalyzer.cfg.xml,把词典文件填进 ext_dict,重启就完事。但容器是 docker stop && docker rm 重建式管理的,改在容器里的任何东西都会随容器删除而丢失------于是整个问题变成"如何把 IK 的插件与配置真正持久化到宿主机"。
真正做下来才发现,坑远不止这一层。
一、先搞清楚 7.x 的 IK 目录结构
这是整个过程中最关键的一条认知,也是后面所有问题的根源:
| 内容 | 位置 |
|---|---|
插件本体(jar 包、plugin-descriptor.properties) |
/usr/share/elasticsearch/plugins/analysis-ik/ |
配置文件(IKAnalyzer.cfg.xml、main.dic、stopword.dic) |
/usr/share/elasticsearch/config/analysis-ik/ |
在 IK 6.x 时代两者都在 plugins/analysis-ik/config/ 下,7.x 之后配置目录搬到了 ES 的 config/analysis-ik/。网上大量教程还是 6.x 的路径,照抄会直接撞墙。
所以持久化必须挂两个目录,缺一不可:
bash
-v /data/elasticsearch/plugins/analysis-ik:/usr/share/elasticsearch/plugins/analysis-ik \
-v /data/elasticsearch/config/analysis-ik:/usr/share/elasticsearch/config/analysis-ik \
只挂配置、不挂插件,容器一重建插件就丢,_cat/plugins 立刻变空------这个现象后面会反复出现。
二、两种自定义词典方案
方案 A:本地词典 ext_dict
把词典文件放进配置目录,在 IKAnalyzer.cfg.xml 的 <properties> 里声明:
xml
<entry key="ext_dict">custom.dic</entry>
多个词典用分号分隔。词典格式要求很硬:每行一个词,UTF-8 无 BOM ,可选 词#词性 标注词性。缺点是每次改词典都要重启节点,词典迭代频繁时体验很差。
方案 B:远程词典 remote_ext_dict(推荐)
把词典放到一个 HTTP 静态服务上,配置指向 URL:
xml
<entry key="remote_ext_dict">http://192.168.1.13:8080/custom.dic</entry>
IK 会按 Last-Modified 头判断文件有没有变,默认约 60 秒轮询一次,变了就重新加载。只有第一次改配置需要重启,之后改词典内容全程免重启。
远程词典服务用 python3 -m http.server 就够,再用 systemd 托管保证开机自启:
ini
[Unit]
Description=IK Dictionary HTTP Server
Wants=network-online.target
After=network-online.target
[Service]
User=deploy
Group=deploy
ExecStart=/usr/bin/python3 -m http.server 8080 --directory /data/ik-dict
Restart=always
RestartSec=5
WorkingDirectory=/data/ik-dict
[Install]
WantedBy=multi-user.target
bash
sudo systemctl daemon-reload
sudo systemctl enable --now ik-dict-server
curl -I http://192.168.1.13:8080/custom.dic
返回 200 且带 Last-Modified 才算正常------python3 的 http.server 自带这个头,换成某些简易服务端可能没有,那就热更新失效。
服务装在哪台机器都行,只要三个节点都能访问到 8080 端口。建议同时配两个 URL(用分号分隔)指向不同机器上的同一份词典,避免单点:
xml
<entry key="remote_ext_dict">http://192.168.1.13:8080/custom.dic;http://192.168.1.11:8080/custom.dic</entry>
三、踩坑实录
下面这些报错按发生顺序排列,基本覆盖了"docker 化 ES + 插件持久化"会遇到的所有典型问题。
坑 1:docker cp ... :/plugins/analysis-ik/config/. 报找不到文件
Error response from daemon: Could not find the file
/usr/share/elasticsearch/plugins/analysis-ik/config/. in container es-node-1
第一反应是 docker cp 对结尾 /. 的兼容问题,但实际原因是 7.x 的 config 根本不在 plugins/ 下。先确认目录结构,别靠猜:
bash
docker exec es-node-1 ls -la /usr/share/elasticsearch/plugins/analysis-ik/
docker exec es-node-1 ls -la /usr/share/elasticsearch/config/analysis-ik/
第一条只看到 jar 包、第二条才有 IKAnalyzer.cfg.xml,说明路径用错了,正确的复制源是 /usr/share/elasticsearch/config/analysis-ik/.。
坑 2:重建容器后插件消失,_cat/plugins 为空
name component version
空表。原因就是只挂了 config/analysis-ik,没挂 plugins/analysis-ik,插件 jar 躺在容器可写层里,docker rm 之后灰飞烟灭。
修复要先在运行中的容器里重装插件,再把整个插件目录复制出来持久化:
bash
docker exec -it es-node-1 /bin/bash -c \
"./bin/elasticsearch-plugin install https://get.infini.cloud/elasticsearch/analysis-ik/7.17.0"
sudo mkdir -p /data/elasticsearch/plugins
sudo docker cp es-node-1:/usr/share/elasticsearch/plugins/analysis-ik /data/elasticsearch/plugins/
sudo chown -R 1000:1000 /data/elasticsearch/plugins
官方源下载慢的话可以换成镜像站的 release zip(
elasticsearch-plugin install <zip-url>形式同样可用)。
坑 3:/data/elasticsearch 整目录 chown 把 ES 搞崩了
在目标机上为了能 scp 写入,顺手执行了:
bash
sudo chown -R deploy:deploy /data/elasticsearch
结果节点进入 Restarting 循环:
ElasticsearchException[failed to bind service];
nested: AccessDeniedException[.../data/nodes/0/indices/.../_state/.es_temp_file]
ES 容器内进程以 uid 1000 运行,data 目录属主被改成普通用户后写不进去,节点直接启动失败。数据目录千万不能跟着改属主:
bash
sudo chown -R 1000:1000 /data/elasticsearch/data
docker restart es-node-2
正确的目录权限分工是:
data、plugins:1000:1000,容器内进程要读写;- 需要给普通用户同步的目录:保持属主不变,只补读权限
sudo chmod -R a+rX。
坑 4:scp 报 Permission denied
scp: local opendir "/data/elasticsearch/config/analysis-ik": Permission denied
目录属主是 1000:1000、权限 700 左右,普通用户 deploy 读不了本地源目录。不用改属主,加读权限即可:
bash
sudo chmod -R a+rX /data/elasticsearch/config/analysis-ik
sudo chmod -R a+rX /data/elasticsearch/plugins
a+rX 里的 X 只对目录生效,不会给普通文件加执行位,是这里最合适的写法。
坑 5:scp 报 stat remote: No such file or directory
不是本地问题,是目标机上 /data/elasticsearch/ 还没建。先远程把目录建好并授权:
bash
ssh -t deploy@192.168.1.12 "sudo mkdir -p /data/elasticsearch/config && sudo chown -R deploy:deploy /data/elasticsearch"
坑 6:SSH 远程执行 sudo 报 a terminal is required
sudo: a terminal is required to read the password; either use the -S option ...
非交互式 SSH 没有分配终端,sudo 无法提示密码。加上 -t 强制分配伪终端即可:
bash
ssh -t deploy@192.168.1.12 "sudo mkdir -p /data/elasticsearch/config && sudo chown -R deploy:deploy /data/elasticsearch"
或者一劳永逸配置该用户的 sudo 免密(内网运维机可以接受):
bash
ssh -t deploy@192.168.1.12 "echo 'deploy ALL=(ALL) NOPASSWD: ALL' | sudo tee /etc/sudoers.d/deploy && sudo chmod 440 /etc/sudoers.d/deploy"
坑 7:三台节点插件不一致
_cat/plugins 只列出两台,第三台缺失。用 docker inspect 看挂载就能一眼定位:
bash
docker inspect es-node-3 --format '{{range .Mounts}}{{.Source}} -> {{.Destination}}{{println}}{{end}}'
结果少了 plugins/analysis-ik 那一行------重建时用的是旧命令。补上挂载重建即可。三台节点的插件与配置必须完全一致,否则同一段文本在不同节点分词结果不同,检索会出现莫名其妙的不一致。
坑 8:集群 red、大量分片未分配
滚动重启 + 权限事故之后,unassigned_shards 一度涨到 63,status: red。处理顺序是:
bash
# 1. 先看能不能直接重试(节点重启导致的未分配,通常这步就够)
curl -s -m 10 -u "elastic:${ES_PWD}" -X POST "http://localhost:9200/_cluster/reroute?retry_failed=true?pretty"
# 2. 重试无效,查具体原因
curl -s -m 10 -u "elastic:${ES_PWD}" "http://localhost:9200/_cluster/allocation/explain?pretty"
# 3. 同时看磁盘水位
curl -s -m 10 -u "elastic:${ES_PWD}" "http://localhost:9200/_cat/allocation?v"
reason 字段的常见取值:NODE_LEFT / CLUSTER_RECOVERED 属暂时状态,重试后自动恢复;ALLOCATION_FAILED 要看详细报错;如果看到 DISK_WATERMARK 则是磁盘水位,只能先清盘或临时调高阈值(恢复后务必调回):
bash
curl -s -m 10 -u "elastic:${ES_PWD}" -X PUT "http://localhost:9200/_cluster/settings" \
-H 'Content-Type: application/json' -d'
{
"persistent": {
"cluster.routing.allocation.disk.watermark.low": "92%",
"cluster.routing.allocation.disk.watermark.high": "95%",
"cluster.routing.allocation.disk.watermark.flood_stage": "98%"
}
}'
另外,curl 卡住不代表集群挂了------16g 堆的节点重启加 60 多个分片恢复,1~3 分钟内连不上是正常的。排查时一律加 -m 10 超时,避免在终端里干等。
四、怎么验证自定义词典真的生效了
按三层查,从配置到效果逐层排除:
第一层:配置是否指向词典
bash
cat /data/elasticsearch/config/analysis-ik/IKAnalyzer.cfg.xml
三台的 remote_ext_dict 内容应完全一致。
第二层:词典服务是否可用
bash
systemctl status ik-dict-server
curl -I http://192.168.1.13:8080/custom.dic
第三层:分词效果
bash
curl -s -m 10 -u "elastic:${ES_PWD}" "http://localhost:9200/_analyze?pretty" \
-H 'Content-Type: application/json' -d'
{
"analyzer": "ik_max_word",
"text": "知识图谱推理"
}'
判断标准:tokens 里出现完整的"知识图谱推理"即生效;被切成"知识/图谱/推理"则没加载。再拿一个词典里没有的词做对照,两个结果一对比就清楚。
两个容易忽略的点:
- 分析器必须是
ik_max_word或ik_smart,用standard测永远不会命中自定义词; - 刚改完词典要等约 60 秒轮询周期,立刻测还是旧结果。
节点日志里也能看到加载记录,连不上词典服务时会打 download error:
bash
docker logs es-node-1 2>&1 | grep -iE "dict|ik" | head -20
五、日常维护
加词:一条命令,一分钟全集群生效。
bash
echo "新词条" | sudo tee -a /data/ik-dict/custom.dic
批量加词用 heredoc 更省事。带词性写 词条#nz。
删词 :这是 IK 的已知行为------热更新只增不减,已加载进内存的词不会因为从词典里删掉而消失。想真正删掉,要么重启节点全量加载,要么把词典文件改名(让 IK 重新拉取),推荐后者:
bash
sudo mv /data/ik-dict/custom.dic /data/ik-dict/custom.dic.old
# 重新写入一份不含待删词的新 custom.dic
改词性同理,只对新增生效。
备份:词典虽然是纯文本,但丢了就是线上分词立刻跑偏,建议定期留一份:
bash
sudo cp /data/ik-dict/custom.dic /data/ik-dict/backup/custom.dic.$(date +%Y%m%d)
词典体积:IK 启动时把词典全量读进每个节点的内存,几万词以上会明显拖慢启动并占用堆,别把整个行业词库一股脑塞进去。
本地 + 远程同时配置 :ext_dict 保证节点启动时基础词一定可用(哪怕词典服务暂时挂了),remote_ext_dict 负责日常热更新 ------ 两者并存是最稳的组合。
六、最后的检查清单
给容器化 ES 配 IK 自定义词典,收尾时确认这几条:
- 插件目录、配置目录两个都挂载,缺一会导致重建即失联;
- 节点采用
1000:1000属主,同步用chmod a+rX而不是改属主,数据目录尤其不能动; - 三台节点的插件版本、
IKAnalyzer.cfg.xml内容完全一致; - 词典文件 UTF-8 无 BOM,每行一词;
- 远程词典服务有
Last-Modified,三节点网络可达 8080; - 用
_analyze+ik_max_word验证,而不是只看文件在不在; - 重建节点逐台进行,每台起来并加回集群后再动下一台,避免分片恢复压力叠加。
整套配好之后,日常维护确实只剩下"一条 echo + 等 60 秒",但前期把目录、权限、挂载这三件事理顺,才是真正省时间的地方。
注:文中 IP、主机名、账号、密码、集群名、索引与路径等均已做匿名化/占位符处理,示例账号密码一律用
${ES_PWD}环境变量代替。