4. Elasticsearch数据建模与映射设计
在 Elastic 机器学习数据平台中,特征工程的第一步往往不是写代码,而是决定数据在 Elasticsearch 里“长什么样”。映射(Mapping)定义了索引中每个字段的类型、分词方式、是否可聚合、是否参与排序,而这些属性会直接决定后续异常检测、分类、回归和推理任务能拿到什么形态的特征。换句话说,映射设计是特征质量的“上游闸门”:字段类型选错,特征可能被静默丢弃;嵌套关系处理不当,特征会丢失层级语义;父子文档设计不合理,聚合特征会变得昂贵甚至不可用。
字段类型:特征语义的第一道分水岭
Elasticsearch 的字段类型决定了值如何被索引、存储和检索。对机器学习特征而言,最关键的区分是:数值型(long、integer、double、float、scaled_float)可直接作为连续或离散特征;keyword 适合作为分类特征或聚合维度;text 经过分词后适合全文检索,但默认不能直接用于聚合和排序,也不适合作为分类特征;boolean 适合二值特征;date 是时间序列异常检测的基石;ip、geo_point 等则承载空间或网络特征。
一个常见误区是把本应是数值的字段写成 keyword。例如把 response_time_ms 写成字符串,虽然能检索,但无法参与 avg、max 等聚合,异常检测作业会直接报“字段不是数值类型”。反过来,把用户 ID 写成 long 虽然能聚合,但会失去精确匹配的语义,且在高基数场景下容易造成内存压力。因此,字段类型的选择要同时考虑“特征用途”和“查询/聚合成本”。
动态映射:便利与风险并存
动态映射(Dynamic Mapping)让 Elasticsearch 在写入文档时自动推断字段类型。默认规则是:JSON 字符串映射为 text 并附带 keyword 子字段,数字映射为 long 或 float,布尔映射为 boolean,对象映射为 object,数组则根据第一个非空元素推断。这在快速原型阶段非常方便,但对机器学习特征却是双刃剑。
风险主要有三:第一,类型漂移。如果第一批文档中 temperature 是整数 25,映射为 long;后续写入 25.5 时会被强制转换或直接拒绝,导致特征值失真。第二,字符串误判。像 "2024-01-01" 这样的日期字符串可能被推断为 date,也可能被推断为 text,取决于格式识别,结果不可控。第三,字段爆炸。动态映射会为每个新字段创建映射,如果文档中有大量动态键(如用户自定义标签),会导致 mapping 膨胀,拖慢集群并影响特征抽取性能。
dynamic: "strict",强制所有字段显式声明。这样可以在写入阶段就暴露类型问题,而不是等到训练作业失败才发现。显式映射:为特征“定契约”
显式映射(Explicit Mapping)是在创建索引时明确声明每个字段的类型和参数。对机器学习特征而言,显式映射的价值在于“契约稳定”:特征抽取作业可以依赖固定的字段类型,不必担心某天写入的数据改变了映射。下面是一个面向异常检测的示例映射,用于记录服务器指标:
PUT metrics-ml
{
"mappings": {
"dynamic": "strict",
"properties": {
"timestamp": { "type": "date" },
"host": { "type": "keyword" },
"cpu_percent": { "type": "double" },
"mem_used_mb": { "type": "long" },
"disk_io": { "type": "double" },
"status": { "type": "keyword" },
"tags": { "type": "keyword" }
}
}
}
这里 timestamp 是时间字段,异常检测作业会用它做时间分桶;host 和 status 是分类维度,可用于按主机分组;cpu_percent、mem_used_mb、disk_io 是数值特征,直接进入特征向量;tags 是关键词数组,可用于过滤或作为多值分类特征。注意 dynamic: "strict" 会让任何未声明字段的写入失败,从而保证映射不被意外污染。
多字段:一份数据,多种特征视角
多字段(Multi-fields)允许同一个字段以不同方式索引。最典型的是 text 主字段加 keyword 子字段:主字段用于全文检索,子字段用于聚合和排序。对机器学习而言,多字段的意义在于“同一份原始数据可以派生出不同用途的特征”。例如商品名称 product_name 可以同时有 text 用于语义搜索,keyword 用于精确分类,甚至再加一个 text 子字段使用不同分词器用于关键词提取。
"product_name": {
"type": "text",
"fields": {
"raw": { "type": "keyword" },
"ik": { "type": "text", "analyzer": "ik_max_word" }
}
}
在特征工程中,product_name.raw 可以作为分类特征输入,而 product_name 本身可以用于计算文本相似度或关键词权重。需要注意的是,多字段会增加索引体积和写入开销,因此只对真正需要多视角的字段使用。
嵌套与父子文档:层级关系的特征表达
当文档包含对象数组时,Elasticsearch 默认使用 object 类型,但 object 数组会“扁平化”内部字段,丢失元素之间的关联。例如订单文档中有多个商品,每个商品有 name 和 price,如果使用 object,查询“name 为 A 且 price 大于 100”可能匹配到不同商品组合,导致特征语义错误。nested 类型通过为每个数组元素建立独立文档来解决这个问题,保持元素内部字段的关联性。
对机器学习特征而言,nested 的价值在于可以精确计算“每个订单中高价商品的数量”这类特征,而不会跨商品混淆。但 nested 查询和聚合成本较高,且不能直接用于某些 ML 作业的字段抽取,通常需要先用 nested 聚合或脚本字段展开成扁平特征。
父子文档(Parent-Child)则用于表达一对多关系,且父子文档可以独立更新。例如“主机”是父文档,“指标采样”是子文档。父子文档适合需要频繁更新子文档而不重建父文档的场景,但在机器学习中,父子文档的聚合需要通过 has_child 或 has_parent 查询,特征抽取链路更复杂,通常不如 nested 直接。选择时需权衡更新频率与特征抽取复杂度。
实践建议:从特征需求反推映射
在实际项目中,建议按以下顺序设计映射:第一,列出所有机器学习作业需要的特征字段及其类型(数值、分类、时间、文本、层级);第二,为每个字段选择最合适的 Elasticsearch 类型,数值特征避免使用 keyword,分类特征优先 keyword;第三,对需要多视角的字段使用多字段;第四,对对象数组使用 nested,并评估聚合成本;第五,关闭动态映射或设置为 strict,用显式映射锁定契约。这样,特征抽取作业才能在稳定的映射基础上运行,减少因映射变化导致的训练失败或特征漂移。
_reindex 配合新映射重建索引。对于机器学习作业,建议在重建后重新运行特征抽取,并对比新旧特征分布,确认映射调整没有引入意外的特征偏移。