蓋房子之前要先畫藍圖。這個道理大家都懂,但很多人用 Elasticsearch 的時候選擇跳過這一步——直接丟資料進去,讓 Dynamic Mapping 自己猜型別。

一開始很順。直到某天你寫入一筆小數,系統跟你說:「不行,這個欄位是整數。」

第一筆資料進來的那一刻,型別就鎖死了。之後想改?只能砍掉重建。沒有 ALTER TABLE。


Dynamic Mapping:方便的糖衣,危險的炸彈

Elasticsearch 的 Dynamic Mapping 會在你第一次寫入資料時自動推斷欄位型別。你丟 "hello",它猜 text + keyword;你丟 123,它猜 long;你丟 "2026-04-29",它猜 date。

聽起來很聰明。問題是,它只看第一筆。

五個一定會爆的地雷

第一筆決定命運。你的第一筆訂單 price: 100 → 欄位被設為 long。第二筆 price: 99.5 → 直接報 mapper_parsing_exception。long 欄位塞不下 float。事後想改?砍 Index 重建。

字串自動雙倍空間。Dynamic Mapping 對每個字串欄位同時建 text(分詞搜尋)+ keyword 子欄位(精確比對)。你明明只需要精確比對的 status 欄位,它也幫你建了一份分詞索引。空間直接翻倍。

Mapping Explosion。使用者上傳結構不固定的 JSON,每筆都多一個新欄位名。Dynamic Mapping 照單全收,欄位數暴增到上限(預設 1,000),然後 IllegalArgumentException。某團隊讓使用者自由上傳,最後搞出 50,000 個欄位,Cluster State 膨脹到 2 GB,Master Node 直接 OOM。

日期格式鎖定。第一筆用 ISO 格式 "2026-04-29T10:00:00Z",日期格式就鎖定了。第二筆用 epoch_millis 1745836800000,格式不匹配,寫入失敗。

object 跟 nested 搞混。Dynamic Mapping 把巢狀物件推斷為 object——扁平化儲存。items: [{name: "小籠包", qty: 2}, {name: "炒飯", qty: 1}] 變成 items.name: ["小籠包", "炒飯"] + items.qty: [2, 1]。關聯性消失了。搜「name=小籠包 AND qty=1」會誤命中。

四種控制模式

Dynamic Mapping 不是只有開跟關。dynamic 參數有四個值:

true(預設):自動偵測建 Mapping。方便但危險。

runtime:新欄位存為 runtime field,不佔索引空間。查的時候動態計算,適合探索階段。

false:新欄位存入 _source 但不索引。資料在,但搜不到。

strict:未定義的欄位寫入直接報錯。正式環境永遠用這個。

1
2
3
4
5
6
7
8
9
10
PUT /strict_index
{
"mappings": {
"dynamic": "strict",
"properties": {
"name": { "type": "text" },
"price": { "type": "float" }
}
}
}

手動 Mapping:每個欄位想清楚

跟 Dynamic Mapping 相反的做法是——自己定義每個欄位的型別。麻煩一點,但不會被第一筆資料綁架。

型別選擇速查

text:全文搜尋用。會過 Analyzer 切成 token 存入倒排索引。不能排序、不能聚合(token 沒有原始值語義)。需要排序就搭配 .keyword 子欄位。

keyword:精確比對用。完全不分詞,原封不動存入。status、category、email、ID、tags 這類欄位用這個。ignore_above: 256 是好習慣,避免超長字串浪費空間。

數值型別:integer、long、float、double、half_float、scaled_float。金額用 scaled_float(例如 scaling_factor: 100,用整數存「分」),不要用 float——浮點精度問題是永恆的痛。

date:支援多種格式,用 || 分隔。ES 內部統一存為 epoch_millis(long),不管你用什麼格式寫入。這就是為什麼 date 可以做 range 查詢和排序。

1
2
3
4
"order_time": {
"type": "date",
"format": "yyyy-MM-dd HH:mm:ss||yyyy-MM-dd||epoch_millis"
}

object vs nested:這是最容易搞混的。object 把巢狀結構扁平化,效能好但關聯性消失。nested 為每個巢狀物件建獨立的 Lucene document,保留關聯性但耗資源。如果你需要「商品 A 的數量是 2」這種跨欄位關聯查詢,必須用 nested。

geo_point:地理座標。注意陣列格式是 [lon, lat] 不是 [lat, lon]——每個踩過這坑的人都會記得。

省空間的進階參數

不需搜尋的欄位設 index: false(存 _source 但不索引),不需排序聚合的設 doc_values: false,不需計分的設 norms: false。每個參數都是空間和效能的取捨。


Index Template:一次定義,自動套用

每次建 Index 都要手動寫一大堆 Mapping?太累了。

Index Template 讓你定義一個模板,之後所有名字符合 pattern 的 Index 自動套用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
PUT _index_template/logs_template
{
"index_patterns": ["logs-*"],
"priority": 100,
"template": {
"settings": {
"number_of_shards": 3,
"number_of_replicas": 1,
"refresh_interval": "30s"
},
"mappings": {
"dynamic": "strict",
"properties": {
"@timestamp": { "type": "date" },
"level": { "type": "keyword" },
"message": { "type": "text" },
"service": { "type": "keyword" },
"trace_id": { "type": "keyword" },
"duration_ms": { "type": "integer" }
}
},
"aliases": {
"logs_current": {}
}
}
}

之後建任何 logs- 開頭的 Index,Mapping 和 Settings 自動到位:

1
2
PUT /logs-nginx-2026.05.04
// 自動套用!不需要手動寫 Mapping

兩件事要注意。第一,一定要設 priority。多個 Template 的 pattern 都匹配、priority 又相同,ES 會報錯。第二,index_patterns 記得加萬用字元。寫成 ["logs"] 而不是 ["logs-*"],只有名字剛好叫 logs 的 Index 才會套用。


Component Template:積木式的模組化設計

如果你有 10 種 Index 都需要「相同的基底欄位 + 不同的專用欄位」,Index Template 會讓你複製貼上到崩潰。

Component Template 把設定拆成積木——通用 Settings 一塊、通用 Mappings 一塊、專用 Mappings 一塊——然後在 Index Template 裡用 composed_of 組裝。

1
2
3
4
5
6
[base_settings]    [base_mappings]    [logs_mappings]
│ │ │
└──────────┬───────┘──────────────────┘

[logs_template]
composed_of: [base_settings, base_mappings, logs_mappings]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// 積木一:通用 Settings
PUT _component_template/base_settings
{
"template": {
"settings": {
"number_of_replicas": 1,
"refresh_interval": "5s"
}
}
}

// 積木二:通用 Mappings
PUT _component_template/base_mappings
{
"template": {
"mappings": {
"properties": {
"@timestamp": { "type": "date" },
"created_at": { "type": "date", "format": "yyyy-MM-dd HH:mm:ss||epoch_millis" }
}
}
}
}

// 積木三:Logs 專用
PUT _component_template/logs_mappings
{
"template": {
"mappings": {
"dynamic": "strict",
"properties": {
"level": { "type": "keyword" },
"message": { "type": "text" },
"service": { "type": "keyword" }
}
}
}
}

組裝:

1
2
3
4
5
6
PUT _index_template/logs_template
{
"index_patterns": ["logs-*"],
"priority": 200,
"composed_of": ["base_settings", "base_mappings", "logs_mappings"]
}

另一套 Orders Index 只要換第三塊積木,通用的兩塊共用。DRY 原則在 Elasticsearch 也適用。

想確認某個 Index 會套用哪些設定?用模擬 API:

1
POST _index_template/_simulate_index/logs-test-2026

改了 Template 不影響舊 Index

這是最容易忘記的一件事:Template 只在建立 Index 時套用

你改了 Component Template 加了新欄位,已經存在的 Index 不會自動更新。要讓舊 Index 跟上,得手動 PUT _mapping 補欄位,或直接 Reindex。

Template 是藍圖,不是遙控器。藍圖改了,已經蓋好的房子不會自己長出新的房間。


八條實戰準則

**1. 正式環境永遠 dynamic: strict**。開發環境可以 true,但 merge 前必須補齊 Explicit Mapping。

2. 字串欄位先想清楚。需要全文搜尋用 text,需要精確比對用 keyword。不確定就先 keyword——省空間,之後要加 text 還可以加。

**3. 金額用 scaled_float**。scaling_factor: 100,用整數存「分」。float 的精度問題不值得冒險。

4. 陣列物件要跨欄位關聯就用 nested。不需要就用 object。nested 每個物件底層是一個 Lucene doc,一筆訂單 100 個品項 = 101 個 doc,效能差距很大。

5. 日期支援多格式format|| 分隔,前端後端格式不一致的時候不會炸。

6. 用 Component Template 做模組化。通用欄位(timestamp、created_at)抽成共用組件。

**7. 不需搜尋的欄位設 index: false**。備註、電話這種存著但不需要搜的欄位,不索引就不佔空間。

8. Template 一定要設 priority。不同 priority 的 Template 匹配同一個 Index 時,高的贏。沒設 priority 會出不可預測的行為。


Mapping 的本質是決策——在資料寫入之前,你就得想清楚每個欄位「是什麼」跟「要怎麼用」。Dynamic Mapping 把這個決策延後了,讓第一筆資料替你決定。問題是,第一筆資料不知道你的業務邏輯。

Template 和 Component Template 則是把決策系統化——定義一次,之後所有符合 pattern 的 Index 自動繼承。改一塊積木,所有新 Index 同步更新。

先決策、再寫入。先藍圖、再蓋房。資料工程跟蓋房子一樣,最貴的不是水泥,是改設計。

參考來源:Elasticsearch 官方文件 — Mapping
參考來源:Elasticsearch 官方文件 — Index Templates