4. 多字段与分词器设计

在 Elasticsearch 中,一个字段最终能否被正确检索,取决于两件事:字段是否被正确映射,以及文本是否被正确的分析器处理。多字段(fields)解决的是“同一份原始值,用不同方式被索引多次”的问题;分析器解决的是“文本如何被切成词项”的问题。两者结合,才能同时满足精确匹配、全文检索、排序聚合等不同需求。

一、多字段:一份数据,多种索引视角

多字段的核心思想是:主字段保留一种分析方式,通过 fields 派生出若干子字段,每个子字段使用不同的 analyzer 或 index 设置。例如一个商品名称,既希望支持分词后的模糊搜索,又希望支持不分词的精确匹配和排序:

PUT /products
{
  "mappings": {
    "properties": {
      "name": {
        "type": "text",
        "analyzer": "ik_max_word",
        "fields": {
          "keyword": { "type": "keyword", "ignore_above": 256 },
          "pinyin":  { "type": "text", "analyzer": "pinyin_analyzer" }
        }
      }
    }
  }
}

写入 name = "华为 Mate 60 Pro" 后,实际产生三个索引视角:name 走中文分词,用于全文检索;name.keyword 保留完整原文,用于精确匹配、排序和聚合;name.pinyin 走拼音分析,用于拼音搜索。查询时按目的选择字段:全文检索用 name,精确匹配用 name.keyword,拼音输入用 name.pinyin。需要注意,多字段会成倍增加索引体积,子字段不宜滥用,通常控制在 2~3 个以内。

二、分析器的三段结构

分析器(Analyzer)由三部分组成,按顺序执行:字符过滤器(Character Filter)→ 分词器(Tokenizer)→ 分词过滤器(Token Filter)。字符过滤器在分词前处理原始字符串,如去除 HTML 标签、替换 & 为 and;分词器把字符串切成词项,是唯一必需组件;分词过滤器对切出的词项做进一步加工,如小写化、去停用词、词干提取、加同义词。

分析器三段处理链:字符过滤 → 分词 → 分词过滤 原始文本 <b>Running</b> & Cats 字符过滤器 html_strip / mapping 去标签、替换字符 分词器 standard / ik / whitespace 切成词项 分词过滤器 lowercase / stop / stemmer synonym / asciifolding 示例:文本 "<b>Running</b> & Cats" 的处理结果 ① 字符过滤后: "Running & Cats" → "Running and Cats" ② 分词后: ["Running", "and", "Cats"] ③ 分词过滤后: ["run", "cat"](小写 + 去停用词 and + 词干提取) 最终倒排索引词项:run、cat —— 查询 "running" 或 "cats" 均可命中

三、内置分词器与选择依据

Elasticsearch 内置了多种分词器,选择时主要看语言和切分粒度:

分词器切分规则典型场景
standard按 Unicode 文本边界切分,去标点,默认小写英文及多数语言通用
whitespace仅按空格切分,不去标点需要保留标点的场景
keyword整段文本作为一个词项,不切分精确值、编码、标签
simple按非字母字符切分,转小写简单英文文本
ik_max_word中文最细粒度切分(需插件)中文全文检索,召回优先
ik_smart中文粗粒度切分(需插件)中文检索,精度优先

中文场景下,standard 会把“数据建模”切成单字,召回高但精度差;ik_max_word 会切成“数据”“建模”“数据建模”等多个词项,兼顾召回与精度。实际项目中,索引时常用 ik_max_word 提高召回,查询时用 ik_smart 提高精度,这是常见搭配。

四、自定义分析器

内置分析器无法满足需求时,通过 settings.analysis 自定义。自定义分析器需要显式声明 type: custom,并指定 tokenizer 和可选的 char_filter、filter:

PUT /articles
{
  "settings": {
    "analysis": {
      "char_filter": {
        "my_html": { "type": "html_strip" }
      },
      "filter": {
        "my_stop": { "type": "stop", "stopwords": ["的", "了", "和"] },
        "my_syn":  { "type": "synonym", "synonyms": ["es, elasticsearch", "电脑, 计算机"] },
        "my_stem": { "type": "stemmer", "language": "english" }
      },
      "analyzer": {
        "my_analyzer": {
          "type": "custom",
          "char_filter": ["my_html"],
          "tokenizer": "ik_max_word",
          "filter": ["lowercase", "my_stop", "my_syn", "my_stem"]
        }
      }
    }
  }
}

定义完成后,可用 _analyze 接口验证效果,这是调试分析器最直接的手段:

GET /articles/_analyze
{
  "analyzer": "my_analyzer",
  "text": "<p>Elasticsearch 和 电脑</p>"
}
// 返回词项:elasticsearch、计算机(es 被同义词替换,的/了/和 被去除)

五、词干提取与同义词处理

词干提取(Stemming)把词还原为词干,使 running、runs、ran 都能匹配到 run。英文常用 stemmer,中文因无形态变化,通常不做词干提取,而是依赖分词和同义词。词干提取会带来过度归并的风险,例如 universe 与 university 可能被错误归并,需通过测试验证。

同义词(Synonym)用于把不同说法映射到统一词项。同义词过滤器有两种展开方式:synonyms 在索引和查询时都展开,synonyms_path 从文件加载便于维护。同义词配置需注意方向性:"es, elasticsearch" 表示两者等价,而 "es => elasticsearch" 表示单向映射,只把 es 替换为 elasticsearch。同义词文件修改后需要关闭并重新打开索引(_close / _open)才能生效。

调试建议:分析器配置完成后,务必用 _analyze 分别验证索引分析器和查询分析器。常见问题是索引与查询使用了不同分析器,导致“明明有数据却搜不到”。多字段场景下,还要确认查询命中的是哪个子字段。
注意:修改已有字段的 analyzer 需要重建索引(reindex),因为倒排索引已经按旧分析器生成。生产环境应通过别名(alias)切换,先建新索引、reindex、再切别名,避免停机。

综合来看,多字段与分析器设计应遵循一条主线:先明确每个字段要支持哪些查询方式(全文、精确、排序、聚合),再为每种方式选择合适的分词器和子字段,最后用 _analyze 验证词项是否符合预期。这样设计出的映射,才能在检索精度、召回率和存储成本之间取得平衡。