4. 索引建模与Mapping设计

索引建模是 Elasticsearch 工程化落地中最容易被低估、却最难返工的一环。Mapping 一旦写入并产生数据,字段类型基本不可修改,只能通过重建索引迁移。因此,建模阶段的核心任务不是“把数据塞进去”,而是提前回答三个问题:这个字段要支持什么样的查询?它需要被排序或聚合吗?它的写入量级和保留周期是多少?本节围绕字段类型、分词器、多字段、动态模板、索引生命周期与建模原则展开,并给出可直接参考的配置示例。

一、字段类型:先定查询语义,再定类型

Elasticsearch 的字段类型决定了倒排索引的构建方式与可用的查询能力。常见类型可分为四类:文本类(text、keyword)、数值与日期类(long、integer、double、scaled_float、date)、结构化类(boolean、ip、geo_point、nested、object)以及专用类(dense_vector、rank_feature、alias)。

最关键的区分是 text 与 keyword。text 会经过分词器切分成词项,适合全文检索,但默认不能直接用于排序和聚合;keyword 保留完整原值,适合精确匹配、排序、聚合与脚本字段。一个典型误区是把商品名称、日志消息这类需要全文检索的字段设为 keyword,结果只能做精确匹配;反过来把订单号、状态码设为 text,则聚合结果会被拆得支离破碎。

数值类型的选择同样影响存储与精度。金额字段若使用 double,浮点误差可能在聚合时累积;推荐使用 scaled_float 并指定 scaling_factor,例如金额以“分”为单位存储为 long,或使用 scaled_float 配合 scaling_factor: 100。日期字段应显式指定 format,避免不同来源的时间格式导致解析失败。

二、分词器:控制文本如何被切分

分词器由三部分组成:字符过滤器(character filter)、分词器(tokenizer)、词项过滤器(token filter)。中文场景下,标准分词器会把连续汉字切成单字,检索召回高但精度低;常用方案是 IK 分词器的 ik_max_word(写入时细粒度)与 ik_smart(查询时粗粒度)组合,或使用内置的 smartcn。

分词器需要在索引设置中定义,并在 Mapping 中引用。下面是一个可参考的示例:

PUT /product_v1
{
  "settings": {
    "analysis": {
      "analyzer": {
        "product_analyzer": {
          "type": "custom",
          "tokenizer": "ik_max_word",
          "filter": ["lowercase", "asciifolding"]
        }
      }
    }
  },
  "mappings": {
    "properties": {
      "title": {
        "type": "text",
        "analyzer": "product_analyzer",
        "search_analyzer": "ik_smart"
      },
      "brand": { "type": "keyword" },
      "price": { "type": "scaled_float", "scaling_factor": 100 },
      "created_at": { "type": "date", "format": "yyyy-MM-dd HH:mm:ss||epoch_millis" }
    }
  }
}

写入时用细粒度分词提高召回,查询时用粗粒度分词减少噪声,这是中文检索中常见的组合策略。需要注意的是,search_analyzer 只影响查询阶段,不影响已写入的倒排索引。

三、多字段:一个字段,多种用途

多字段(multi-fields)允许同一原始值以不同方式索引。最典型的用法是 text 主字段加 keyword 子字段:

"title": {
  "type": "text",
  "analyzer": "product_analyzer",
  "fields": {
    "raw": { "type": "keyword", "ignore_above": 256 }
  }
}

这样 title 用于全文检索,title.raw 用于排序、聚合和精确匹配。ignore_above 防止超长字符串进入 keyword 索引造成内存膨胀。多字段会带来额外的索引开销,应只对确实需要多种访问方式的字段使用。

四、动态模板:让新字段自动落入正确类型

当数据源字段不固定时,动态映射(dynamic mapping)可能把数字字符串识别成 text,或把日期识别成 text。动态模板(dynamic templates)通过匹配规则为新字段指定类型:

"dynamic_templates": [
  {
    "strings_as_keywords": {
      "match_mapping_type": "string",
      "match": "code_*",
      "mapping": { "type": "keyword" }
    }
  },
  {
    "longs_as_integers": {
      "match_mapping_type": "long",
      "mapping": { "type": "integer" }
    }
  }
]

动态模板按顺序匹配,先命中者生效。生产环境通常将 dynamic 设为 strict 或 false,避免未知字段污染 Mapping;strict 会在写入未知字段时直接报错,适合结构稳定的核心索引。

五、索引生命周期与建模原则

索引生命周期管理(ILM)把索引按时间划分为 hot、warm、cold、delete 四个阶段。日志类场景常见策略是:hot 阶段 7 天、warm 阶段 30 天并强制合并段、cold 阶段 90 天、之后删除。ILM 依赖 rollover 别名,写入指向别名而非具体索引,当索引达到 max_size 或 max_age 时自动滚动到新索引。

建模原则可以归纳为几条:按查询模式设计,而非按数据源结构照搬;能用 keyword 精确匹配的不要用 text;需要聚合的字段必须是 doc_values 可用的类型;避免过度 nested,nested 查询代价高,能用扁平结构加过滤解决的优先扁平化;控制单索引分片数,分片过多会放大集群元数据与查询开销,一般单分片 10~50GB 为宜。

建模阶段建议先用真实样本数据做一次“查询反推”:列出所有已知查询、排序、聚合需求,再逐字段确认类型与分词器,最后用 _analyze API 验证分词结果是否符合预期。
索引建模决策链:从字段语义到生命周期 字段类型 → 分词器 → 多字段 → 动态模板 → ILM 阶段 ① 字段语义判断 全文检索?精确匹配? 排序 / 聚合?范围查询? 是否需要高亮? ② 类型选择 text / keyword long / scaled_float / date boolean / ip / geo_point ③ 分词器配置 ik_max_word(写入) ik_smart(查询) lowercase / asciifolding ④ 多字段 title → text title.raw → keyword ignore_above: 256 ⑤ 动态模板 dynamic_templates match_mapping_type: string → keyword match: code_* → keyword;dynamic: strict 防污染 ⑥ 索引生命周期 ILM:hot → warm → cold → delete hot 写入 / 查询 7 天 · rollover warm 只读 / 合并段 30 天 · forcemerge cold 低频查询 90 天 · 可搜索快照 delete 到期删除 释放存储 建模原则:按查询设计 · keyword 优先精确匹配 · 聚合字段需 doc_values · 慎用 nested · 单分片 10~50GB

上图把建模过程串成一条决策链:先判断字段语义,再选类型与分词器,必要时用多字段兼顾检索与聚合,用动态模板兜住未知字段,最后用 ILM 管理索引的时间维度。实际落地时,建议把这条链固化为团队评审清单,在索引创建前逐项确认,避免上线后因 Mapping 不可变而被迫重建索引。

Mapping 中已存在字段的类型无法直接修改。若必须调整,只能新建索引、用 _reindex 迁移数据并切换别名。因此,任何涉及字段类型变更的需求,都应在建模评审阶段拦截。