ElasticsearchNotes

第 06 章:Mapping 设计

zjc 于 2026-01-06 发布

这是《Elasticsearch 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开,代码块保留了原书可直接运行的版本。 Mapping 决定数据如何被解析、索引、搜索、排序和聚合。很多线上搜索问题的根因不是查询写错,而是 Mapping 设计不合理。

本章覆盖字段类型、动态映射、多字段、嵌套结构、日期数值、元字段、索引参数和 Mapping 治理。

6.1 Mapping 能控制什么

一个字段的 Mapping 通常决定:

  1. JSON 值如何解析;
  2. 是否生成倒排索引;
  3. 使用什么分词器;
  4. 是否保存 Doc Values;
  5. 是否参与评分;
  6. 是否可以聚合和排序;
  7. 日期和数值格式;
  8. 超长 keyword 的处理方式。

示例:

PUT /products
{
  "mappings": {
    "properties": {
      "title": {
        "type": "text",
        "analyzer": "ik_max_word"
      },
      "brand": {
        "type": "keyword",
        "ignore_above": 64
      },
      "price": {
        "type": "scaled_float",
        "scaling_factor": 100
      },
      "description": {
        "type": "text",
        "norms": false
      },
      "snapshot": {
        "type": "object",
        "enabled": false
      }
    }
  }
}

6.2 text 与 keyword

text

text 会被分词,适合全文搜索。

{
  "title": {
    "type": "text",
    "analyzer": "ik_max_word"
  }
}

写入“轻薄高性能笔记本电脑”后可能索引出:

轻薄
高性能
笔记本
电脑

适合标题、描述、正文、评论内容、错误消息,不适合状态、订单号、手机号、标签精确匹配和聚合字段。

keyword

keyword 不分词,适合精确匹配、排序和聚合。

{
  "order_no": {
    "type": "keyword",
    "ignore_above": 64
  }
}

适合枚举状态、ID 字符串、品牌、类目、标签、主机名、日志级别。

多字段

同一字段同时需要全文搜索和精确聚合时,使用 fields

{
  "title": {
    "type": "text",
    "analyzer": "ik_max_word",
    "fields": {
      "keyword": {
        "type": "keyword",
        "ignore_above": 128
      }
    }
  }
}

搜索:

{
  "match": { "title": "笔记本" }
}

聚合:

{
  "terms": { "field": "title.keyword" }
}

6.3 数值类型

类型 说明 适用
long 64 位整数 订单数、ID、计数
integer 32 位整数 状态、数量
short 16 位整数 少用
byte 8 位整数 少用
double 64 位浮点 精度要求不高的金额
float 32 位浮点 指标、评分
half_float 半精度浮点 低精度指标
scaled_float 带固定缩放因子 金额、价格
unsigned_long 无符号 64 位 特大整数

价格建议:

{
  "price": {
    "type": "scaled_float",
    "scaling_factor": 100
  }
}

scaling_factor=100 会把 99.99 存为整数 9999,排序更稳定,也更利于范围查询。

不要把所有数值都定义成 double。数值类型会影响磁盘、排序精度和范围查询效率。

6.4 日期类型

{
  "created_at": {
    "type": "date",
    "format": "strict_date_optional_time||epoch_millis"
  }
}

常见输入:

2026-08-25
2026-08-25T10:00:00Z
2026-08-25T18:00:00+08:00
1787642400000

查询:

{
  "range": {
    "created_at": {
      "gte": "now-7d",
      "lt": "now"
    }
  }
}

建议:

  1. 统一使用 UTC 或明确时区;
  2. 混合系统时显式声明 format;
  3. 日志索引名带日期,同时保留 @timestamp 字段;
  4. 不要把日期同时作为 text

6.5 布尔、IP 与范围

布尔:

{ "is_deleted": { "type": "boolean" } }

IP:

{ "client_ip": { "type": "ip" } }

IP 查询:

{
  "term": { "client_ip": "192.168.1.10" }
}

范围字段:

{
  "valid_period": {
    "type": "date_range",
    "format": "strict_date_optional_time"
  }
}

范围字段适合日程、价格区间、有效期等场景。

6.6 object 与 nested

普通对象:

{
  "seller": {
    "id": 100,
    "name": "Nova 旗舰店",
    "city": "Shanghai"
  }
}

Mapping:

{
  "seller": {
    "properties": {
      "id": { "type": "long" },
      "name": { "type": "keyword" },
      "city": { "type": "keyword" }
    }
  }
}

普通对象数组会被扁平化:

{
  "skus": [
    { "color": "red", "size": "S" },
    { "color": "blue", "size": "L" }
  ]
}

近似变成:

skus.color = [red, blue]
skus.size = [S, L]

查询 color=red AND size=L 会错误匹配,应使用 nested:

{
  "skus": {
    "type": "nested",
    "properties": {
      "color": { "type": "keyword" },
      "size": { "type": "keyword" },
      "price": { "type": "scaled_float", "scaling_factor": 100 }
    }
  }
}

查询:

{
  "nested": {
    "path": "skus",
    "query": {
      "bool": {
        "must": [
          { "term": { "skus.color": "red" } },
          { "term": { "skus.size": "S" } }
        ]
      }
    }
  }
}

nested 的成本:

  1. 每个 nested 对象是隐藏子文档;
  2. 数量大会增加索引体积;
  3. nested query 和聚合更复杂;
  4. 返回文档时需要重建完整结构。

如果数组对象只展示、不参与条件查询,可以设置:

{
  "skus": {
    "type": "object",
    "enabled": false
  }
}

6.7 join 字段

ES 的 join 不是数据库 Join,而是父子文档关系。

PUT /company
{
  "mappings": {
    "properties": {
      "relation": {
        "type": "join",
        "relations": {
          "department": "employee"
        }
      }
    }
  }
}

父文档:

PUT /company/_doc/dept_1
{
  "name": "Search Platform",
  "relation": "department"
}

子文档:

PUT /company/_doc/emp_1?routing=dept_1
{
  "name": "Tom",
  "relation": {
    "name": "employee",
    "parent": "dept_1"
  }
}

查询:

GET /company/_search
{
  "query": {
    "has_child": {
      "type": "employee",
      "query": {
        "term": { "name.keyword": "Tom" }
      }
    }
  }
}

限制:

  1. 父子必须路由到同一分片;
  2. 查询成本高;
  3. 更新和重建复杂;
  4. 大多数场景建议反范式或同步期展开。

6.8 index、doc_values 与 store

index

{
  "remark": {
    "type": "keyword",
    "index": false
  }
}

index=false 不能被搜索,但仍会出现在 _source。适合原始报文、大对象快照、只用于展示的扩展字段。

doc_values

{
  "price": {
    "type": "double",
    "doc_values": false
  }
}

Doc Values 支持排序和聚合,默认开启。确定不排序、不聚合的字段可以关闭以节省磁盘。

store

{
  "title": {
    "type": "text",
    "store": true
  }
}

默认 _source 已保存原始文档,通常不需要 store=true。只有 _source 很大且只需要取少量字段时才考虑。

6.9 norms 与 term_vectors

norms 参与字段长度归一化和 BM25 评分:

{
  "description": {
    "type": "text",
    "norms": false
  }
}

关闭后可节省空间,但相关性计算会改变,适合纯召回或过滤型 text 字段。

term_vectors:

{
  "content": {
    "type": "text",
    "term_vector": "with_positions_offsets"
  }
}

term_vectors 支持更多高亮和分析能力,但会增加索引体积,应按需开启。

6.10 dynamic mapping

动态映射策略:

{
  "mappings": {
    "dynamic": "strict",
    "properties": {
      "title": { "type": "text" }
    }
  }
}
行为
true 自动添加字段
false 忽略新字段
runtime 新字段作为运行时字段
strict 写入未知字段直接拒绝

动态模板:

{
  "mappings": {
    "dynamic_templates": [
      {
        "strings_as_keywords": {
          "match_mapping_type": "string",
          "mapping": {
            "type": "keyword",
            "ignore_above": 128
          }
        }
      },
      {
        "ids_as_keyword": {
          "match": "*_id",
          "mapping": {
            "type": "keyword"
          }
        }
      },
      {
        "times_as_date": {
          "match": "*_at",
          "mapping": {
            "type": "date"
          }
        }
      }
    ]
  }
}

生产建议:

  1. 主业务索引使用 strict;
  2. 日志索引起始 Mapping 明确核心字段;
  3. 未知字段可使用 runtime 或 false;
  4. 限制字段总数;
  5. 定期检查字段增长。

6.11 runtime field

运行时字段在查询时计算,不占用索引存储。

PUT /orders/_mapping
{
  "runtime": {
    "amount_with_tax": {
      "type": "double",
      "script": {
        "source": "emit(doc['amount'].value * 1.13)"
      }
    }
  }
}

查询:

GET /orders/_search
{
  "query": {
    "range": {
      "amount_with_tax": {
        "gte": 100
      }
    }
  }
}

优点是不需要重建索引,适合临时字段;缺点是每次查询计算,数据量大时性能差。长期使用的字段应固化到 Mapping 并重新索引。

6.12 Mapping 修改规则

不能直接修改:

  1. 字段类型;
  2. 分词器;
  3. index 从 false 改 true;
  4. doc_values 从 false 改 true。

可以新增字段:

PUT /products/_mapping
{
  "properties": {
    "season": {
      "type": "keyword"
    }
  }
}

如果必须修改已有字段类型,需要创建新索引、Reindex、校验、切换别名。详见第 18 章。

6.13 元字段

字段 说明
_id 文档 ID
_source 原始 JSON
_routing 路由
_seq_no 序列号
_primary_term 主分片任期
_ignored ignore_above 忽略的字段

建议保留 _source。禁用后更新、高亮、重建索引都会受限。大字段可以使用 index=falseenabled=false,不要轻易关闭 _source

6.14 Mapping 设计流程

推荐步骤:

收集字段来源和业务含义
-> 标记查询类型:搜索/过滤/聚合/排序/展示
-> 选择字段类型
-> 设计多字段结构
-> 确定 dynamic 策略
-> 确定 object/nested
-> 确定日期和金额格式
-> 建立索引模板
-> 写入样本数据
-> 验证查询、排序、聚合

字段设计表模板:

字段 类型 搜索 过滤 聚合 排序 说明
title text + keyword keyword 可以 ik_max_word
brand keyword 品牌
price scaled_float 两位小数
status keyword 枚举
description text norms 可关

6.15 本章小结

6.16 思考题

  1. 为什么状态字段用 keyword 而不是 text?
  2. title 需要“搜索标题”和“按完整标题聚合”两种能力,应如何设计?
  3. 商品 SKU 的颜色和尺码组合查询为什么必须使用 nested?
  4. 禁用 _source 会影响哪些功能?
  5. Mapping 错误后为什么不能原地修改?标准处理流程是什么?