2022年3月18日
Elasticsearch 入門:從 RDBMS 概念對照到 Query DSL 與 Aggregation 實戰
這篇是從實際專案(房仲平台核心系統重構、
加盟房仲平台 等)用 Elasticsearch 支
撐物件搜尋的經驗整理出來的入門教學,不求涵蓋所有官方文件的細節,而是把剛接觸
Elasticsearch 時最容易卡住的幾個概念,用關聯式資料庫(RDBMS)的角度對照講清
楚。完整範例查詢與 docker-compose 環境放在
HappyElasticSearch。
先建立心智模型:Elasticsearch 對應 RDBMS 的什麼
| RDBMS | Elasticsearch |
|---|---|
| Table | Index |
| Row | Document |
| Column | Field |
| Schema | Mapping |
| SQL | DSL(Query DSL,用 JSON 表示查詢) |
Mapping:定義欄位型別
Mapping 定義 Document 裡每個欄位的型別,等同於在 RDBMS 定義 Table 的欄位型 別。
Dynamic Mapping(動態映射)
Elasticsearch 預設是動態映射:只要新增 Document 進 Index,就會依資料值自動 推斷欄位型別,不需要事先定義。
要注意的地方:字串欄位經過 Dynamic Mapping 後,預設會同時產生 text 與
keyword 兩個子欄位——text 用於全文檢索(會被斷詞),keyword 用於精確比
對與排序、聚合(不斷詞)。這代表同一個字串值會被存兩份。如果某個欄位你確
定只會拿來做精確比對(例如關鍵字標籤),讓它預設同時保留 text 反而浪費儲存
空間,應該用 Dynamic Mapping Template 或 Explicit Mapping 把它限定成只有
keyword。
Explicit Mapping(明確映射)
想完全自己控制型別時,可以在建立 Index 時就宣告 Mapping:
PUT /indexName
{
"mappings": {
"properties": {
"name": { "type": "text" },
"account": { "type": "text" },
"password": { "type": "keyword" }
}
}
}
也可以先建 Index 再補上映射;但既有欄位的型別一旦寫入資料就無法直接改,只能 重建 Index 或用 Reindex API 遷移。
查詢結果的欄位怎麼看(Response 結構)
一次查詢的回應大致長這樣,理解這幾個欄位對讀查詢結果很關鍵:
hits.total.value:符合查詢條件的資料總數hits.total.relation:eq表示 total 是精確值(沒有被截斷估算)hits.max_score:整次查詢中最高的匹配分數hits.hits[]._index:資料所在的 Indexhits.hits[]._id:Document 的 IDhits.hits[]._score:這筆資料的匹配分數,分數越高代表跟查詢條件越相關hits.hits[]._source:實際的資料內容
Query DSL:兩種查詢情境
Elasticsearch 的查詢分成兩種 Context,效能特性完全不同:
- Query Context:會計算匹配分數(
_score),分數越高排序越前面;子查詢 結果與分數會被快取 - Filter Context:只回答「符合或不符合」,不計算分數,處理速度更 快,也不快取有相關性算分的查詢
⚠️ 效能建議:不需要排序相關性的條件(例如「狀態是否為上架中」「日期是否在 某個範圍」)優先用
filter而不是must;也盡量避免用Script、Wildcard,這兩種查詢方式很容易吃滿效能。
Boolean Query(複合查詢)
如同 RDBMS 的多條件組合查詢:
GET /kibana_sample_data_ecommerce/_search
{
"query": {
"bool": {
"must": [
{ "term": { "order_date": { "value": "2022-02-03" } } },
{ "term": { "day_of_week": { "value": "Thursday" } } }
],
"should": [
{ "term": { "customer_id": { "value": 26 } } },
{ "term": { "customer_id": { "value": 27 } } }
]
}
}
}
對應的 SQL 概念大致是:
SELECT * FROM TABLE
WHERE order_date = '2022-02-03'
AND day_of_week = 'Thursday'
AND (customer_id = 26 OR customer_id = 27)
四種子句的角色:
| 子句 | 作用 | 對應邏輯 |
|---|---|---|
must | 必須符合,會計算分數 | 相當於 AND |
filter | 必須符合,不計算分數,速度較快 | 相當於 AND,建議優先使用 |
should | 符合會加分,非必要條件 | 相當於 OR |
must_not | 必須不符合 | 相當於 NOT |
全文檢索(Full Text Queries)
match:標準的全文查詢,支援 operator(AND/OR)與 fuzziness
(模糊比對,容忍拼字誤差):
GET /kibana_sample_data_ecommerce/_search
{
"query": {
"match": {
"customer_id": {
"query": 27,
"operator": "and",
"fuzziness": "AUTO"
}
}
}
}
multi_match:同一個查詢值同時比對多個欄位,很適合「搜尋框」這種不知道
使用者要查哪個欄位的場景。
match_all:查詢全部資料,務必搭配 size/from 分頁使用,避免一次把
整個 Index 撈出來。
欄位等級查詢(Term-level Queries)
term / terms:精確比對單一或多個值(不做全文分詞),適合查
keyword、數字、日期這類欄位。
range:範圍查詢:
GET /kibana_sample_data_ecommerce/_search
{
"query": {
"range": {
"order_date": { "gte": "2022-01-01", "lte": "2022-01-31" }
}
}
}
對應 SQL:WHERE order_date BETWEEN '2022-01-01' AND '2022-01-31'。
只要欄位,不要全部資料
用 _source 的 includes/excludes 控制回傳欄位,減少不必要的網路傳輸:
GET /kibana_sample_data_ecommerce/_search
{
"size": 20,
"from": 0,
"query": {},
"_source": {
"excludes": ["order_id", "order_date"],
"includes": ["*"]
}
}
Aggregation:ES 版的 GROUP BY 與統計函式
Aggregation 提供類似 RDBMS GROUP BY、COUNT、AVG 的能力,分成三種:
Bucket Aggregation:分桶(相當於 GROUP BY)
依欄位值把 Document 分到不同的桶:
GET /IndexName/_search
{
"size": 0,
"aggs": {
"day": {
"terms": { "field": "day_of_week", "size": 10 }
}
}
}
回傳結果會依 day_of_week 分組,每組附上該值出現的次數(doc_count),例如
"key": "Thursday", "doc_count": 775。這裡的 "size": 0 代表不要回傳原
始查詢命中的資料,只要聚合結果——查詢結果本身用不到時,這樣可以省掉大量不
必要的資料傳輸。
Metric Aggregation:數學運算
min/max/avg/sum/cardinality,或是一次拿到全部統計值的
stats:
GET /IndexName/_search
{
"size": 0,
"aggs": {
"productsBaseAvg": { "avg": { "field": "products.base_price" } },
"productsBaseMax": { "max": { "field": "products.base_price" } },
"productsBaseMin": { "min": { "field": "products.base_price" } },
"productsBaseStats": { "stats": { "field": "products.base_price" } }
}
}
stats 一次回傳 count/min/max/avg/sum,不用分開寫四個聚合。
Pipeline Aggregation:對聚合結果再聚合(巢狀分組)
在一個 Bucket Aggregation 裡面再放一個 Bucket Aggregation,達到多層分組,
相當於 RDBMS 多欄位的 GROUP BY:
GET /IndexName/_search
{
"size": 0,
"aggs": {
"county": {
"terms": { "field": "county.keyword" },
"aggs": {
"city": { "terms": { "field": "district.keyword" } }
}
}
}
}
結果會先依 county(縣市)分組,每個縣市底下再依 district(行政區)分
組——像是「台北市 → 大安區、內湖區」「新北市 → 板橋區、新莊區」這樣的階層統
計。
本機環境
docker-compose up
會啟動對應版本的 Elasticsearch 與 Kibana,直接在 Kibana 的 Dev Tools 貼上面 的查詢範例就能跑。
延伸:在 ASP.NET Core 中使用
倉庫裡也附了一個分層(Application/Service/Repository)的 ASP.NET Core 範 例專案,示範怎麼把上面的查詢邏輯包裝進一般後端服務的架構裡,而不是散落在各處 直接寫死 DSL 字串。
總結
- 把 Mapping/Document/Index 對應回 Schema/Row/Table,能少走很多彎路
- 字串欄位預設會存兩份(
text+keyword),確定只需要精確比對時就該用 Explicit Mapping 限定成keyword - 不需要算分數的條件優先用
filter,效能比must好 - 只要聚合結果、不需要原始資料時,
"size": 0能省下大量不必要的傳輸