第一阶段 02 · Mapping 与数据类型(text vs keyword 是重点)

02 · Mapping 与数据类型(text vs keyword 是重点)

阶段:第一阶段 / 核心概念

目标:把 PostgreSQL 的「列类型 / DDL」心智,迁移到 ES 的 mapping;

彻底搞懂从 SQL 转 ES 最容易踩的坑------textkeyword 的区别。


1. 概念

Mapping 就是 ES 索引的「表结构 / DDL」,定义每个字段的类型、是否分词、是否可聚合等。

和 PG 最大的不同:

  • PG 建表必须先写好列类型;ES 支持动态映射(dynamic mapping),写入一条 JSON 时会自动推断类型。
  • ES 的字符串有两种截然不同的类型
    • text:会分词 ,用于全文检索(match)。默认不能精确匹配、排序、聚合。
    • keyword不分词 ,整体作为一个词,用于精确匹配(term)、排序、聚合。

一句话记忆:text 用来「搜」,keyword 用来「精确匹配 / 分组 / 排序」。

常用数据类型对照

ES 类型 PostgreSQL 近似 用途
text text + 全文检索 tsvector 全文搜索,会分词
keyword varchar / 枚举 精确匹配、聚合、排序
long / integer / short / byte bigint / int / smallint 整数
double / float numeric / double precision 浮点数
scaled_float numeric(x,2)(金额) 定标浮点,适合金额
boolean boolean 布尔
date date / timestamp 日期时间
object jsonb(嵌套对象) 内嵌 JSON 对象
nested 一对多子表(近似) 数组内对象独立索引
ip inet IP 地址

2. PostgreSQL 对照

在 PG 里你会明确声明类型,并区分「精确列」和「模糊搜索列」:

sql 复制代码
CREATE TABLE salesdata_master_info (
                                       record_id      bigint PRIMARY KEY,
                                       invoice_number varchar(64),          -- 精确匹配 => ES keyword
                                       material_desc  text,                 -- 模糊搜索 => ES text
                                       net_amount     numeric(18,2),        -- 金额     => ES scaled_float
                                       is_active      boolean,              -- 布尔     => ES boolean
                                       invoice_dt     date                  -- 日期     => ES date
);

-- 精确匹配(对应 ES 的 term / keyword)
SELECT * FROM salesdata_master_info WHERE invoice_number = 'INV-2026-001';

-- 模糊全文(对应 ES 的 match / text)
SELECT * FROM salesdata_master_info WHERE material_desc ILIKE '%thinkpad%';

关键差异:

  • PG 里一列同时能 =ILIKE;ES 里 text 不能 =(精确),keyword 不能好好分词搜。
  • ES 用**多字段(multi-field)**同时满足两种需求:一个字段既建 text 又建 keyword

3. ES DSL(在 Kibana Dev Tools 里跑)

3.1 显式创建 mapping(相当于 CREATE TABLE)

复制代码
PUT salesdata_idx
{
  "mappings": {
    "properties": {
      "record_id":      { "type": "long" },
      "invoice_number": { "type": "keyword" },
      "material_desc":  { "type": "text" },
      "net_amount":     { "type": "scaled_float", "scaling_factor": 100 },
      "is_active":      { "type": "boolean" },
      "invoice_dt":     { "type": "date", "format": "yyyy-MM-dd" }
    }
  }
}

3.2 多字段(既能搜又能聚合)

复制代码
PUT salesdata_idx
{
  "mappings": {
    "properties": {
      "material_desc": {
        "type": "text",
        "fields": {
          "raw": { "type": "keyword" }
        }
      }
    }
  }
}

用法:全文搜用 material_desc,精确/聚合/排序用 material_desc.raw

复制代码
GET salesdata_idx/_search
{
  "query":  { "match": { "material_desc": "thinkpad x1" } },
  "aggs":   { "by_model": { "terms": { "field": "material_desc.raw" } } }
}

3.3 索引 / mapping 的「增删改查」(对照 DDL 操作)

把索引和 mapping 当成 PG 的表和列,四类操作一一对应:

操作 PostgreSQL ES DSL
建(Create) CREATE TABLE PUT salesdata_idx { ... }
查(Read) \d table / 查表结构 GET salesdata_idx / GET salesdata_idx/_mapping
改(Update) ALTER TABLE ADD COLUMN PUT salesdata_idx/_mapping { ... }(仅能加字段)
删(Delete) DROP TABLE DELETE salesdata_idx

建(Create) ------见 3.1 / 3.2。也可以在建索引时一起带上 settings(分片、副本、刷新间隔、分词器):

复制代码
PUT salesdata_idx
{
  "settings": {
    "index": {
      "number_of_replicas": 0,
      "refresh_interval": "60s",
      "max_result_window": 100000
    }
  },
  "mappings": {
    "properties": {
      "invoice_number": { "type": "keyword" }
    }
  }
}

查(Read)------查看索引整体定义 / 只看 mapping / 判断是否存在:

复制代码
GET salesdata_idx                 # 索引全部信息(settings + mappings + aliases)
GET salesdata_idx/_mapping        # 仅看字段结构(相当于 \d)
HEAD salesdata_idx                # 判断索引是否存在(200 存在 / 404 不存在)
GET _cat/indices/salesdata_idx?v  # 看文档数、大小、健康状态

改(Update) ------只能新增字段 (相当于 ALTER TABLE ADD COLUMN),不能改已有字段类型:

复制代码
PUT salesdata_idx/_mapping
{
  "properties": {
    "customer_group": { "type": "keyword" }
  }
}

部分 settings 是动态的,可在线修改(如副本数、刷新间隔):

复制代码
PUT salesdata_idx/_settings
{
  "index": { "number_of_replicas": 1, "refresh_interval": "30s" }
}

number_of_shards(分片数)是静态的,创建后不可改,只能 reindex 到新索引。

删(Delete) ------删除整个索引(相当于 DROP TABLE,数据一并删除,慎用):

复制代码
DELETE salesdata_idx

注意:mapping 只能新增字段,不能修改已存在字段的类型 。要改类型必须 reindex(见第 33 篇)。

生产上常用「新建索引 + reindex + 切别名」实现零停机改结构。


4. Spring Boot 实现

用官方 co.elastic.clients 客户端建索引,本质就是把第 3 节的 DSL 翻译成 Java。

先讲通用做法与取舍,最后再给一个可借鉴的工程参考。

4.1 用代码 Builder 逐字段创建(最直观的起点)

建索引前先判断是否存在,避免重复创建报错;settingsmappings 一起提交:

java 复制代码
@Slf4j
@Component
public class IndexAdmin {

    @Autowired
    private ElasticsearchClient client;

    public void createIndex(String index) throws IOException {
        boolean exists = client.indices().exists(e -> e.index(index)).value();
        if (exists) {
            return;
        }

        client.indices().create(c -> c
                .index(index)
                .settings(s -> s
                        .numberOfShards("3")
                        .numberOfReplicas("1")
                        .refreshInterval(t -> t.time("30s")))
                .mappings(m -> m
                        .properties("record_id",      p -> p.long_(l -> l))
                        .properties("invoice_number", p -> p.keyword(k -> k))
                        .properties("material_desc",  p -> p.text(t -> t
                                .fields("raw", f -> f.keyword(k -> k))))
                        .properties("net_amount",     p -> p.scaledFloat(s -> s.scalingFactor(100.0)))
                        .properties("is_active",      p -> p.boolean_(b -> b))
                        .properties("invoice_dt",     p -> p.date(d -> d.format("yyyy-MM-dd")))
                )
        );
    }
}

优点:类型安全、IDE 有补全。缺点:字段一多就很长,而且和 Kibana 里调试用的 JSON 对不上,改字段要动代码重编译。

4.2 用外置 JSON 文件建索引(推荐,可维护性更好)

我自己更倾向把整份 settings + mappings 放到一个 JSON 文件里,代码只负责「读文件 + 提交」。

这样 mapping 就是纯配置,和 Kibana Dev Tools 里调试的 JSON 一模一样。

src/main/resources/mappings/xxx_idx.json

json 复制代码
{
  "settings": {
    "index": {
      "number_of_shards": 3,
      "number_of_replicas": 1,
      "refresh_interval": "30s"
    }
  },
  "mappings": {
    "properties": {
      "record_id":      { "type": "long" },
      "invoice_number": { "type": "keyword" },
      "material_desc":  { "type": "text", "fields": { "raw": { "type": "keyword" } } },
      "net_amount":     { "type": "scaled_float", "scaling_factor": 100 },
      "invoice_dt":     { "type": "date", "format": "yyyy-MM-dd" }
    }
  }
}

Java 端用 .withJson(...) 把整份 JSON 一次提交(官方客户端原生支持):

java 复制代码
public boolean createFromJson(String index, String mappingFile) throws IOException {
    String json = new String(
            new ClassPathResource(mappingFile).getInputStream().readAllBytes(),
            StandardCharsets.UTF_8);

    CreateIndexRequest request = CreateIndexRequest.of(cr -> cr
            .index(index)
            .withJson(new StringReader(json)));   // 整份 JSON 一次提交

    return client.indices().create(request).acknowledged();
}

为什么我更推荐 4.2

  • 与 Kibana 调试零翻译:JSON 直接粘到 Dev Tools 调好,再落文件。
  • 配置与代码解耦settingsmappings、自定义 analyzer 集中在一个文件,一处维护。
  • 改字段就是改 JSON:新增字段加一行即可,不用改 Java、不用重编译逻辑。
  • 天然可评审:mapping 变更以文本 diff 进 Git,Code Review 一目了然。

4.3 两种方式怎么选

维度 JSON 文件(4.2) 代码 Builder(4.1)
字段是否固定 固定/半固定,最合适 运行时动态拼字段更合适
与 Kibana 调试一致性 完全一致,可直接复制 需在 Java 与 JSON 间翻译
改字段成本 改 JSON 一行 改代码 + 重编译
可读性/评审 好,文本 diff 一般,藏在代码里

结论:字段固定就用 4.2 JSON 文件方式;只有字段需要程序动态生成时才用 4.1 Builder。

4.4 工程参考(本项目的做法,可借鉴)

上面 4.2 的思路,本项目正好也是这么落地的,可作为一个真实参考:

  • mapping 文件放在 resources/mappings/(如 salesdata_idx.json),里面除了 mappings
    还配了 settings.analysis 的自定义分词器(用逗号 pattern 分词,把 a,b,c 切成多个词)。
  • 建索引的 Handler 在构造时把 JSON 读进内存,再用 CreateIndexRequest.of(... .withJson(...)) 提交。
  • 业务上绝大多数字段都用 keyword(因为主要做精确过滤/聚合,而非全文搜索)。

借鉴点:mapping 外置成 JSON + .withJson() 提交 ,是一种简单又好维护的工程实践;

是否需要自定义 analyzer、字段该用 keyword 还是 text,仍要按你自己的查询需求决定,不要照搬。


5. 坑与最佳实践

  1. text 不能精确匹配 :对 text 字段用 term 查不到你想要的结果,因为它被分词了。
    需要精确匹配 / 排序 / 聚合,请用 keyword(或多字段的 .raw)。
  2. 动态映射的陷阱 :不显式建 mapping 时,字符串默认被建成 text + keyword 多字段,
    数值/日期靠推断------"2026-07-01" 可能被识别成 text 而非 date。生产环境建议显式建 mapping
  3. 金额别用 float :浮点有精度问题,用 scaled_float(配 scaling_factor)或 long(存分)。
  4. mapping 不可改类型:字段类型定错只能 reindex 重建,成本高,创建前想清楚。
  5. keyword 有长度成本 :超长文本设为 keyword 会占用大量内存做聚合/排序,
    可用 ignore_above 限制(超过则不索引该字段值)。
  6. 字符串默认怎么选 :如果这个字段只用来过滤、聚合、排序(不做全文搜索),
    直接用 keyword 最省心;需要全文搜索才用 text,两者都要就用多字段。
    (很多以「精确查询/报表聚合」为主的系统,绝大多数字段都是 keyword,可作参考。)

下一篇

03-分词与相关性入门.md:讲 analyzer 分词过程、_score 打分(BM25),

理解「为什么 match 能模糊命中、结果还带相关性排序」。

相关推荐
小大宇1 小时前
python蓝图、拦截器、异常处理、redis队列、线程池、控制台及日志文件输出
python
Zane19941 小时前
JMM 与 happens-before:一次搞懂 Java 内存模型
java·后端
用户298698530141 小时前
在线将 Word 文档转换为 TXT 格式:快速免费的方法
人工智能·后端
喝茶与编码2 小时前
真实业务场景:高并发 Upsert 死锁频发?InnoDB 锁机制与重试机制深度解析
数据库·python
shepherd1112 小时前
别再把 MCP 当成大模型的“手脚”:LLM 并不会直接调用 MCP
后端·ai编程·mcp
SimonKing2 小时前
别再写 setter 了!MapStruct Plus vs MapStruct,谁才是 Bean 转换的真神?
java·后端·程序员
名字还没想好☜2 小时前
Go for-range 循环变量陷阱:goroutine 里全打印同一个值(Go 1.22 前后差异)
开发语言·后端·golang·go
AI多Agent协作实战派2 小时前
AI多Agent协作系统实战(二十五):Spring Boot静态资源同步:为什么你改了代码但线上还是旧的?
后端
码栈研说2 小时前
Go 语言大白话入门 12 - JSON:工作里最常见的数据格式
后端·程序员