02 · Mapping 与数据类型(text vs keyword 是重点)
阶段:第一阶段 / 核心概念
目标:把 PostgreSQL 的「列类型 / DDL」心智,迁移到 ES 的 mapping;
彻底搞懂从 SQL 转 ES 最容易踩的坑------
text与keyword的区别。
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 逐字段创建(最直观的起点)
建索引前先判断是否存在,避免重复创建报错;settings 和 mappings 一起提交:
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 调好,再落文件。
- ✅ 配置与代码解耦 :
settings、mappings、自定义 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. 坑与最佳实践
text不能精确匹配 :对text字段用term查不到你想要的结果,因为它被分词了。
需要精确匹配 / 排序 / 聚合,请用keyword(或多字段的.raw)。- 动态映射的陷阱 :不显式建 mapping 时,字符串默认被建成
text + keyword多字段,
数值/日期靠推断------"2026-07-01"可能被识别成text而非date。生产环境建议显式建 mapping。 - 金额别用
float:浮点有精度问题,用scaled_float(配scaling_factor)或long(存分)。 - mapping 不可改类型:字段类型定错只能 reindex 重建,成本高,创建前想清楚。
keyword有长度成本 :超长文本设为keyword会占用大量内存做聚合/排序,
可用ignore_above限制(超过则不索引该字段值)。- 字符串默认怎么选 :如果这个字段只用来过滤、聚合、排序(不做全文搜索),
直接用keyword最省心;需要全文搜索才用text,两者都要就用多字段。
(很多以「精确查询/报表聚合」为主的系统,绝大多数字段都是keyword,可作参考。)
下一篇
03-分词与相关性入门.md:讲 analyzer 分词过程、_score 打分(BM25),
理解「为什么 match 能模糊命中、结果还带相关性排序」。