Elasticsearch(簡稱:ES)功能強大,其背後有很多預設值,或者預設操作。這些操作優劣並存,優勢在於我們可以迅速上手使用 ES,劣勢在於,其實這些預設值的背後涉及到很多底層原理,怎麼做更合適,只有數據使用者知道。用 ES 的話來說,你比 ES 更懂你的數據,但一些配置信息、限制信息,還是需... ...
Elasticsearch(簡稱:ES)功能強大,其背後有很多預設值,或者預設操作。這些操作優劣並存,優勢在於我們可以迅速上手使用 ES,劣勢在於,其實這些預設值的背後涉及到很多底層原理,怎麼做更合適,只有數據使用者知道。用 ES 的話來說,你比 ES 更懂你的數據,但一些配置信息、限制信息,還是需要在瞭解了 ES 的功能之後進行人工限制。
你是否遇到:在使用了一段時間 ES 之後,期望使用 ES 的其他功能,例如聚合、排序,但因為欄位類型受限,無奈只能進行reindex等一系列問題?
題主在遇到一些問題後,發現用 ES 很簡單,但是會用 ES 很難。這讓我下定決心一定好好瞭解 ES,也就出現了本文。
前言
ES(全稱 Elastic Search)是一款開源、近實時、高性能的分散式搜索引擎。在近 3 年的熱門搜索引擎類數據統計中,ES 都霸居榜首(數據來源:DBRaking),可見的其深受大家的喜愛。
隨著 ES 的功能越來越強大,其和資料庫的邊界也越來越小,除了進行全文檢索,ES 也支持聚合/排序。ES 底層基於Lucene開發,針對Lucene的局限性,ES 提供了 RESTful API 風格的介面、支持分散式、可水平擴展,同時它可以被多種編程語言調用。
ES 很多基礎概念以及底層實現其本質是 Lucene 的概念。
ps:本文所有的 dsl 查詢、結果展示均基於 ES v7.7
歷史背景
Lucene 的歷史背景
下圖這個人叫Doug Cutting,他是 Hadoop 語言和 Lucene 工具包的創始人。Doug Cutting 畢業於斯坦福大學,在 Xerox 積累了一定的工作經驗後,從 1997 年開始,利用業餘時間開發出了 Lucene。Lucene 面世於 1999 年,並於 2005 年成為 Apache 頂級開源項目。
Lucene的特點:
-
Lucene是基於 java 編寫的,開源的全文檢索引擎工具包。 -
Lucene具有高性能:在相同的硬體環境下,基於 Hadoop 的 webmap(Lucene 的第一個應用) 的反應速度是之前系統的 33 倍。
Lucene的局限性:
-
僅限於 java 開發。 -
類庫的介面學習成本高:本質上Lucene就是一個編程庫,可以按原始介面來調用,但是如果在應用程式中直接使用Lucene,需要覆蓋大量的集成框架工作。 -
原生並不支持水平擴展,若需實現海量數據的搜索引擎,需在此基礎上格外開發以支持分散式。
ES 的歷史背景
-
2004 年,Shay Banon 基於 Lucene 開發了 Compass,在考慮 Compass 的第三個版本時,他意識到有必要重寫 Compass 的大部分內容,以“創建一個可擴展的搜索解決方案”。因此,他創建了“一個從頭構建的分散式解決方案”,並使用了一個公共介面,即 HTTP 上的 JSON,它也適用於 Java 以外的編程語言。 -
2010 年,Shay Banon 在發佈了 Elasticsearch 的第一個版本。
ES 多個版本可能出現破壞性變更,例如,在 6.x,ES 不允許一個 Index 中出現多個Type。在 ES 的官網,每個版本都對應著一個使用文檔。
在使用 ES 之前,最好先瞭解 ES 的版本歷史。下麵列出一些比較重大的更新版本,可以在瞭解了基本概念之後再看。
-
初始版本 0.7.0 2010 年 5 月 14 日
-
Zen Discovery 自動發現模塊 - Groovy Client 支持 - 簡單的插件管理機制 - 更好支持 ICU 分詞器 -
1.0.0 2014 年 2 月 14 日
-
支持聚合分析 Aggregations -
CAT API 支持 -
Doc values 引入 -
支持聯盟查詢 -
斷路器支持 -
2.0.0 2015 年 10 月 28 日
-
query/filter 查詢合併,都合併到 query 中,根據不同的 context 執行不同的查詢 -
增加了 pipleline Aggregations 在 ES 中,有 Query 和 Filter 兩種 Context - Query Context :相關性算分 -
Filter Context :不需要算分(YES OR NO), 可以利用 Cache 獲得更好的性能 -
存儲壓縮可配置 -
Rivers 模塊被移除 -
Multicast 組播發現成為組件 -
5.0.0 2016 年 10 月 26 日
-
Lucene 6.x 的支持,磁碟空間少一半;索引時間少一半;查詢性能提升 25%;支持 IPV6。 -
Internal engine 級別移除了用於避免同一文檔併發更新的競爭鎖,帶來 15%-20%的性能提升 -
Shrink API ,它可將分片數進行收縮成它的因數,如之前你是 15 個分片,你可以收縮成 5 個或者 3 個又或者 1 個,那麼我們就可以想象成這樣一種場景,在寫入壓力非常大的收集階段,設置足夠多的索引,充分利用 shard 的並行寫能力,索引寫完之後收縮成更少的 shard,提高查詢性能 -
引入新的欄位類型 Text/Keyword 來替換 String -
提供了 Painless 腳本,代替 Groovy 腳本 -
新增 Sliced Scroll 類型,現在 Scroll 介面可以併發來進行數據遍歷了。每個 Scroll 請求,可以分成多個 Slice 請求,可以理解為切片,各 Slice 獨立並行,利用 Scroll 重建或者遍歷要快很多倍。- 限制索引請求大小,避免大量併發請求壓垮 ES -
限制單個請求的 shards 數量,預設 1000 個 -
6.0.0 2017 年 8 月 31 日
-
Index sorting,即索引階段的排序。 -
順序號的支持,每個 es 的操作都有一個順序編號(類似增量設計) -
無縫滾動升級 -
逐步廢棄 type,在 6.0 裡面,開始不支持一個 index 裡面存在多個 type -
Index-template inheritance,索引版本的繼承,目前索引模板是所有匹配的都會合併,這樣會造成索引模板有一些衝突問題, 6.0 將會只匹配一個,索引創建時也會進行驗證 - Load aware shard routing, 基於負載的請求路由,目前的搜索請求是全節點輪詢,那麼性能最慢的節點往往會造成整體的延遲增加,新的實現方式將基於隊列的耗費時間自動調節隊列長度,負載高的節點的隊列長度將減少,讓其他節點分攤更多的壓力,搜索和索引都將基於這種機制。- 已經關閉的索引將也支持 replica 的自動處理,確保數據可靠。 -
7.0.0 2019 年 4 月 10 日
-
集群連接變化:TransportClient 被廢棄 以至於,es7 的 java 代碼,只能使用 restclient -
重大改進-正式廢除單個索引下多 Type 的支持 -
es6 時,官方就提到了 es7 會刪除 type,並且 es6 時已經規定每一個 index 只能有一個 type。在 es7 中使用預設的_doc 作為 type,官方說在 8.x 版本會徹底移除 type。api 請求方式也發送變化,如獲得某索引的某 ID 的文檔:GET index/_doc/id 其中 index 和 id 為具體的值 -
Lucene9.0 - 引入了真正的記憶體斷路器,它可以更精準地檢測出無法處理的請求,並防止它們使單個節點不穩定 - Zen2 是 Elasticsearch 的全新集群協調層,提高了可靠性、性能和用戶體驗,變得更快、更安全,並更易於使用 - 新功能 - New Cluster coordination - Feature - Complete High Level REST Client - Script Score Query - 性能優化 - Weak-AND 演算法提高查詢性能 -
預設的 Primary Shared 數從 5 改為 1,避免 Over Sharding shard 也是一種資源,shard 過多會影響集群的穩定性。因為 shard 過多,元信息會變多,這些元信息會占用堆記憶體。shard 過多也會影響讀寫性能,因為每個讀寫請求都需要一個線程。所以如果 index 沒有很大的數據量,不需要設置很多 shard。 -
更快的前 k 個查詢 -
間隔查詢(Intervals queries) 某些搜索用例(例如,法律和專利搜索)引入了查找單詞或短語彼此相距一定距離的記錄的需要。Elasticsearch 7.0 中的間隔查詢引入了一種構建此類查詢的全新方式,與之前的方法(跨度查詢 span queries)相比,使用和定義更加簡單。與跨度查詢相比,間隔查詢對邊緣情況的適應性更強。
基礎概念介紹
下圖簡單概述了 index、type、document 之間的關係,type 在新版本中廢棄,所以畫圖時特殊標識了一下。
index
Index 翻譯過來是索引的意思。在 ES 里,索引有兩個含義:
-
名詞:一個索引相當於關係型資料庫中的一個表(在 6.x 以前,一個 index
可以被認為是一個資料庫) -
動詞:將一份 document
保存在一個index
里,這個過程也可以稱為索引。
type
在 6.x 之前, index
可以被理解為關係型資料庫中的【資料庫】,而 type
則可以被認為是【資料庫中的表】。使用 type
允許我們在一個 index
里存儲多種類型的數據,數據篩選時可以指定 type
。type
的存在從某種程度上可以減少 index
的數量,但是 type
存在以下限制:
-
不同 type 里的欄位需要保持一致。例如,一個 index
下的不同type
里有兩個名字相同的欄位,他們的類型(string, date 等等)和配置也必須相同。 -
只在某個 type
里存在的欄位,在其他沒有該欄位的 type 中也會消耗資源。 -
得分是由 index
內的統計數據來決定的。也就是說,一個 type 中的文檔會影響另一個 type 中的文檔的得分。
以上限制要求我們,只有同一個 index
的中的 type 都有類似的映射 (mapping) 時,才勉強適用 type
。否則,使用多個 type
可能比使用多個 index
消耗的資源更多。
這大概也是為什麼 ES 決定廢棄 type 這個概念,個人感覺 type 的存在,就像是一個語法糖,但是並未帶來太大的收益,反而增加了複雜度。
document
index 中的單條記錄稱為 document
(文檔),可以理解為表中的一行數據。多條 document
組成了一個 index
。
"hits" : {
"total" : {
"value" : 3563,
"relation" : "eq"
},
"max_score" : 1.0,
"hits" : [
{
"_index" : "test",
"_type" : "_doc",
"_id" : "3073",
"_score" : 1.0,
"_source" : {
...
}
}
]
上圖為 ES 一條文檔數據,其中:
-
_index
:文檔所屬索引名稱。 -
_type
:文檔所屬類型名(此處已預設為_doc)。 -
_id
:Doc 的主鍵。在寫入的時候,可以指定該 Doc 的 ID 值,如果不指定,則系統自動生成一個唯一的 UUID 值。 -
_score
:顧名思義,得分,也可稱之為相關性,在查詢是 ES 會 根據一些規則計算得分,並根據得分進行倒排。除此之外,ES 支持通過Function score query
在查詢時自定義 score 的計算規則。 -
_source
:文檔的原始 JSON 數據。
field
一個 document
會由一個或多個 field 組成,field 是 ES 中數據索引的最小定義單位,下麵僅列舉部分常用的類型。
⚠️ 在 ES 中,沒有數組類型,任何欄位都可以變成數組。
string
text
-
索引全文值的欄位,例如電子郵件正文或產品描述。 -
如果您需要索引結構化內容,例如電子郵件地址、主機名、狀態代碼或標簽,您可能應該使用 keyword
欄位。 -
出於不同目的,我們期望以不同方式索引同一欄位,這就是 multi-fields 。例如,可以將 string
欄位映射為用於全文搜索的text
欄位,並映射為用於排序或聚合的keyword
欄位:
PUT my_index
{
"mappings": {
"properties": {
"city": {
"type": "text",
"fields": {
"raw": {
"type": "keyword"
}
}
}
}
}
}
-
⚠️純 text
欄位預設無法進行排序或聚合 -
⚠️ 使用 text
欄位一定要使用合理的分詞器。
keyword
-
用於索引結構化內容的欄位,例如 ID、電子郵件地址、主機名、狀態代碼、郵政編碼或標簽。如果您需要索引全文內容,例如電子郵件正文或產品描述,你應該使用 text
欄位。 -
它們通常用於過濾(查找所有發佈狀態的博客文章)、排序和聚合。 keyword
欄位只能精確匹配。
numeric
long, integer, short, byte, double, float, half_float, scaled_float...
-
就整數類型( byte
、short
、integer
和long
)而言,應該選擇足以滿足用例的最小類型。 -
對於浮點類型,使用縮放因數將浮點數據存儲到整數中通常更有效,這就是 scaled_float
類型的實現。 -
下麵這個 case, scaling_factor
縮放因數設置為 100,對於所有的 API 來說, price 看起來都像是一個雙精度浮點數。但是對於 ES 內部,他其實是一個整數long
。
"price": {
"type": "scaled_float",
"scaling_factor": 100
}
-
如果 scaled_float
無法滿足精度要求,可以使用double
、float
、half_float
。 -
不是所有的欄位都適合存儲為 numberic
,numberic
類型更擅長range
類查詢,精確查詢可以嘗試使用keyword
。
mapping
mapping
是一個定義 document
結構的過程, mapping
中定義了一個文檔所包含的所有 field 信息。
定義欄位索引過多會導致爆炸的映射,這可能會導致記憶體不足錯誤和難以恢復的情況, mapping
提供了一些配置對 field
進行限制,下麵列舉幾個可能會比較常見的:
-
index.mapping.total_fields.limit 限制 field 的最大數量,預設值是 1000(field 和 object 內的所有欄位,都會加入計數)。 -
index.mapping.depth.limit 限制 object 的最大深度,預設值是 20。 -
index.mapping.field_name_length.limit 限制中欄位名的長度,預設是沒有限制。
dynamic mapping
在索引 document 時,ES 的動態 mapping
會將新增內容中不存在的欄位,自動的加入到映射關係中。ES 會自動檢測新增欄位的邏輯,並賦予其預設值。
-
One of the most important features of Elasticsearch is that it tries to get out of your way and let you start exploring your data as quickly as possible. -
You know more about your data than Elasticsearch can guess, so while dynamic mapping can be useful to get started, at some point you will want to specify your own explicit mappings.
截取了部分 ES 官方文檔中的話術,ES 認為一些自動化的操作會讓新手上手更容易。但是同時,又提出,你肯定比 ES 更瞭解你的數據,可能剛開始使用起來覺得比較方便,但是最好還是自己明確定義映射關係。
(