📣 极限科技诚招搜索运维工程师(Elasticsearch/Easysearch)- 全职/北京 👉 : 立即申请加入

适用版本: 6.8-8.11

1. 错误异常的基本描述 #

Hunspell 是 Elasticsearch 内置的拼写检查分析器(spell checker analyzer)所依赖的词典引擎。每个 Hunspell 词典目录必须包含两类核心文件:

  • .dic 文件:存放词根和对应词形变化的词典数据
  • .aff 文件:定义词缀规则(前缀/后缀)的配置文件

当 Elasticsearch 尝试加载某个 locale 对应的 Hunspell 词典时,如果在其字典目录下找不到任何 .aff 文件,就会抛出如下异常:

Missing affix file for hunspell dictionary [locale]

其中 [locale] 是你在 analysis 配置中指定的词典区域标识(如 en_USzh_CN 等)。

常见现象 #

  • 创建索引或更新索引 settings 时返回 400500 错误,索引无法创建或更新失败。
  • Elasticsearch 节点日志中出现 Missing affix file for hunspell dictionary 异常,并伴随完整的 Java 异常栈。
  • 使用 _analyze API 测试包含 hunspell 分词器的索引时,请求直接失败。
  • 如果问题出现在多个节点上,集群中可能只有部分节点成功加载了词典,导致查询行为不一致。

典型报错与异常栈 #

ElasticsearchException[Missing affix file for hunspell dictionary [en_US]]
    at org.elasticsearch.index.analysis.HunspellService.getDictionary(HunspellService.java:XXX)
    at org.elasticsearch.index.analysis.HunspellTokenFilterFactory.<init>(HunspellTokenFilterFactory.java:XXX)
    ...

2. 为什么会发生这个错误 #

Hunspell 词典的加载逻辑在 HunspellService 中实现。Elasticsearch 会在配置的 Hunspell 字典目录(默认为 $ES_HOME/config/hunspell/)下,根据 locale 名称查找子目录,然后在该子目录中扫描 .aff 文件。源码逻辑如下:

Path[] affixFiles = FileSystemUtils.files(dicDir, "*.aff");
if (affixFiles.length == 0) {
    throw new ElasticsearchException(String.format(Locale.ROOT,
        "Missing affix file for hunspell dictionary [%s]", locale));
}
if (affixFiles.length != 1) {
    throw new ElasticsearchException(String.format(Locale.ROOT,
        "Too many affix files exist for hunspell dictionary [%s]", locale));
}

常见原因包括:

  • 字典目录不完整:只拷贝了 .dic 文件,遗漏了 .aff 文件,或者 .aff 文件被误删。
  • locale 目录名称不匹配:配置中指定的 locale 与磁盘上实际的目录名不一致(如配置了 en_US 但目录名为 en-us)。
  • 多节点文件不一致:集群中某些节点的字典目录完整,某些节点缺失 .aff 文件,导致只有部分节点加载成功。
  • 文件权限问题.aff 文件存在,但 Elasticsearch 进程没有读取权限,导致扫描时无法识别该文件。
  • 压缩包解压不完整:从 Hunspell 词典仓库下载的压缩包解压时出错,.aff 文件未被正确解压出来。

3. 如何排查这个异常 #

建议按以下步骤逐一确认:

  1. 确认报错时间点与索引:从异常栈中找到触发加载的索引名称和 locale 值,确认问题范围。
  2. 检查 Hunspell 字典目录结构:登录 Elasticsearch 节点,查看 config/hunspell/ 目录下的子目录和文件。
  3. 验证文件完整性:确认对应 locale 目录下同时存在 .dic.aff 文件,且文件名符合 Hunspell 规范。
  4. 核对文件权限:确认 .aff 文件对运行 Elasticsearch 的用户可读。
  5. 检查多节点一致性:如果是集群环境,逐一检查各节点的字典目录是否一致。

排查示例 #

# 假设 locale 为 en_US,检查字典目录
ls -la $ES_HOME/config/hunspell/en_US/

# 期望看到类似输出:
# -rw-r--r--  en_US.aff
# -rw-r--r--  en_US.dic

# 如果只有 .dic 文件,则说明 .aff 文件缺失
# 如果目录不存在,则说明 locale 配置有误或字典未部署

4. 如何解决这个错误 #

方案一:补齐缺失的 .aff 文件 #

从可靠的 Hunspell 词典源(如 LibreOffice Hunspell 词典仓库)下载对应 locale 的完整词典包,将 .aff 文件放入正确的目录:

# 下载并解压词典包后,将文件放到对应目录
mkdir -p $ES_HOME/config/hunspell/en_US/
cp en_US.aff en_US.dic $ES_HOME/config/hunspell/en_US/

# 确认文件权限正确
chown elasticsearch:elasticsearch $ES_HOME/config/hunspell/en_US/*
chmod 644 $ES_HOME/config/hunspell/en_US/*

完成后重启节点,或在不重启的情况下通过更新索引 settings 触发重新加载。

方案二:统一各节点字典目录 #

在集群环境中,需确保所有节点具备相同的 Hunspell 字典文件。可以使用配置管理工具(如 Ansible、Chef、Puppet)或简单的同步脚本:

# 在主节点准备好字典目录后,同步到所有节点
rsync -avz $ES_HOME/config/hunspell/ node2:$ES_HOME/config/hunspell/
rsync -avz $ES_HOME/config/hunspell/ node3:$ES_HOME/config/hunspell/

方案三:校验 locale 配置 #

确认索引 settingshunspell 分析器的 locale 配置与磁盘目录名称完全一致:

{
  "settings": {
    "analysis": {
      "filter": {
        "my_hunspell": {
          "type": "hunspell",
          "locale": "en_US"
        }
      },
      "analyzer": {
        "my_analyzer": {
          "type": "custom",
          "tokenizer": "standard",
          "filter": ["my_hunspell"]
        }
      }
    }
  }
}

如果 locale 配置为 en_US,则磁盘路径必须是 $ES_HOME/config/hunspell/en_US/,目录名大小写需完全匹配。

5. 预防建议 #

  • 发布自定义词典时使用标准化流程:在部署脚本中加入 .dic.aff 文件的完整性校验步骤,确保两个文件同时存在且可读。
  • 多节点环境使用统一的字典分发机制:避免手动逐个节点拷贝文件,推荐使用配置管理工具或容器镜像预置字典文件。
  • 变更 analyzer 配置前先在测试环境验证:在测试集群中预先验证 Hunspell 词典加载是否成功,确认无误后再推送到生产环境。
  • 建立 Hunspell 词典的版本管理:将自定义词典文件纳入 Git 等版本控制系统,记录每次变更的内容和原因,便于回溯。
  • 监控节点启动日志:在节点启动后检查日志中是否有 Hunspell 相关异常,尽早发现问题。

6. 小结 #

Missing affix file for hunspell dictionary 表示 Hunspell 词典目录不完整,缺少必要的 .aff 词缀规则文件,导致 Elasticsearch 无法初始化该词典。修复重点是补齐 .aff 文件、统一集群各节点的字典文件分发,并确认 locale 配置与磁盘目录名称完全匹配。通过规范的词典管理流程和预发布验证,可以有效避免此类问题再次发生。

相关错误 #

附:日志上下文 #

boolean ignoreCase = nodeSettings.getAsBoolean("ignore_case", defaultIgnoreCase);
Path[] affixFiles = FileSystemUtils.files(dicDir, "*.aff");
if (affixFiles.length == 0) {
    throw new ElasticsearchException(String.format(Locale.ROOT,
        "Missing affix file for hunspell dictionary [%s]", locale));
}
if (affixFiles.length != 1) {
    throw new ElasticsearchException(String.format(Locale.ROOT,
        "Too many affix files exist for hunspell dictionary [%s]", locale));
}
InputStream affixStream = null;