Elasticsearch 基礎入門詳文
作者:lynneyli,騰訊 IEG 運營開發工程師
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 的歷史背景
ElasticSearch 創始人 - Shay Banon
-
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 更瞭解你的數據,可能剛開始使用起來覺得比較方便,但是最好還是自己明確定義映射關係。
(🙄️ 個人認爲,這些自動操作是在用戶對 ES 沒有太多瞭解的情況下進行的,如果剛開始依賴了這些默認的操作,例如:新增字段使用了 ES 賦予的默認值,如果後續有分析、排序、聚合等操作可能會有一定限制)。
⚠️ 在 ES 中,刪除 / 變更 field 定義,需要進行 reindex
,所以在構建 mapping
結構時記得評估好字段的用途,以使用最合適的字段類型。
部分查詢關鍵字介紹
match&match_phrase
-
match
:用於執行全文查詢的標準查詢,包括**模糊匹配和短語或接近查詢。**重要參數:控制 Token 之間的布爾關係:operator:or/and -
match_phrase
:與 match 查詢類似但用於匹配確切的短語或單詞接近匹配。重要參數:Token 之間的位置距離:slop 參數,默認爲 0
GET /_analyze
{
"text": ["這是測試"],
"analyzer": "ik_smart"
}
//Result
{
"tokens" : [
{
"token" : "這是",
"start_offset" : 0,
"end_offset" : 2,
"type" : "CN_WORD",
"position" : 0
},
{
"token" : "測試",
"start_offset" : 2,
"end_offset" : 4,
"type" : "CN_WORD",
"position" : 1
}
]
}
//match+analyzer:ik_smart
//可以查詢到所有describe中包含【這是測試】、【這是】、【測試】的doc
GET /doraon_recommend_tab_test/_search
{
"query": {
"match": {
"describe":{
"query": "這是測試",
"analyzer": "ik_smart"
}
}
}
}
//match_phrase + analyzer:ik_smart + slop=0(默認)
//可以查詢到所有describe中包含【這是】+【測試】token間隔爲0的doc(說人話就是:模糊匹配【這是測試】)
GET /doraon_recommend_tab_test/_search
{
"_source": "describe",
"query": {
"match_phrase": {
"describe": "這是測試"
}
}
}
//match_phrase + analyzer:ik_smart + slop=1
//可以查詢到所有describe中包含【這是】+【測試】token間隔爲1的doc
//例如某個doc中describe爲【這是一個測試】,【這是一個測試】分詞後的token分別爲【這是】【一個】【測試】
//【這是】和【測試】之間間隔了1個token【一個】,所以可以被查詢到;同理【這是一個我的測試】查詢不到
GET /test/_search
{
"query": {
"match_phrase": {
"describe":{
"query": "這是測試",
"analyzer": "ik_smart",
"slop": 1
}
}
}
}
term
term
是進行精確查找的關鍵;在 Lucene 中,term 是中索引和搜索的最小單位。一個 field 會由一個或多個 term
組成, term
是由 field 經過 Analyzer(分詞)產生。Term Dictionary
即 term
詞典,是根據條件查找 term
的基本索引。
-
避免對
text
字段使用術語查詢。默認情況下,ES 會在分析過程中更改文本字段的值。這會使查找text
字段值的精確匹配變得困難。要搜索text
字段值,強烈建議改用match
查詢。 -
⚠️默認分詞情況下,無論是
term
還是match
,都無法判斷text
類型字段是否爲空字符串
以上兩點均是因爲 text
字段存儲的是分詞結果,如果字段值爲空,分詞結果將不會存儲 term
信息, keyword
字段存儲的是原始內容。
GET /test/_termvectors/123?fields=content
{
"_index" : "[your index]",
"_type" : "_doc",
"_id" : "123",
"_version" : 2,
"found" : true,
"took" : 0,
"term_vectors" : { }
}
GET /test/_termvectors/234?fields=card_pic
{
"_index" : "[your index]",
"_type" : "_doc",
"_id" : "234",
"_version" : 1,
"found" : true,
"took" : 0,
"term_vectors" : {
"card_pic" : {
"field_statistics" : {
"sum_doc_freq" : 183252,
"doc_count" : 183252,
"sum_ttf" : 183252
},
"terms" : {
"" : {
"term_freq" : 1,
"tokens" : [
{
"position" : 0,
"start_offset" : 0,
"end_offset" : 0
}
]
}
}
}
}
}
分析器 Analyzer
在上一篇文章中提到了,針對全文索引類型,一定要選擇合適的分析器,現在我們就來了解一下分析器~
Analyzer 主要是對輸入的文本類內容進行分析(通常是分詞),將分析結果以 term
的形式進行存儲。
Analyzer 由三個部分組成:Character Filters、Tokenizer、Token Filters
-
Character Filters
Character Filters 以 characters 流的方式接收原始數據,它可以支持 characters 的增、刪、改,通常內置的分析器都沒有設置默認的 Character Filters。ES 內置的 Character Filters:
-
HTML Strip Character Filter:支持剔除 html 標籤,解碼
-
Mapping Character Filter:支持根據定義的映射進行替換
-
Pattern Replace Character Filter:支持根據正則進行替換
-
Tokenizer
Tokenizer 接收一個字符流,分解成獨立的 tokens(通常就是指的分詞),並且輸出 tokens。例如, 一個 whitespace tokenizer(空格 tokenizer),以空格作爲分割詞對輸入內容進行分詞。例如:向 whitespace tokenizer 輸入 “Quick brown fox!”,將會輸出 “Quick”、 “brown”、“fox!” 3 個 token。
-
Token Filters
Token filters 接收 Tokenizer 輸出的 token 序列,它可以根據配置進行 token 的增、刪、改。例如:指定 synonyms 增加 token、指定 remove stopwords 進行 token 刪除,抑或是使用 lowercasing 進行小寫轉換。
ES 內置的分析器有 Standard Analyzer、Simple Analyzer、Whitespace Analyzer、Stop Analyzer、Keyword Analyzer、Pattern Analyzer、Language Analyzers、Fingerprint Analyzer,並且支持定製化。
這裏的內置分詞器看起來都比較簡單,這裏簡單介紹一下 Standard Analyzer、Keyword Analyzer,其他的分詞器大家感興趣可以自行查閱。
text 類型默認 analyzer:Standard Analyzer
Standard Analyzer 的組成部分:
-
TokenizerStandard Tokenizer:基於 Unicode 文本分割算法 -Unicode 標準附件# 29,支持使用
max_token_length
參數指定 token 長度,默認爲 255。 -
Token Filters
-
Stop Token Filter :默認沒有 stop token/words,需通過參數
stopwords
或stopwords_path
進行指定。
如果 text 類型沒有指定 Analyzer,Standard Analyzer,前面我們已經瞭解了 ES 分析器的結構,理解它的分析器應該不在話下。Unicode 文本分割算法依據的標準,給出了文本中詞組、單詞、句子的默認分割邊界。該附件在 notes 中提到,像類似中文這種複雜的語言,並沒有明確的分割邊界,簡而言之就是說,中文並不適用於這個標準。
通常我們的全文檢索使用場景都是針對中文的,所以我們在創建我們的映射關係時,一定要指定合適的分析器。
keyword 類型默認 analyzer:Keyword Analyzer
Keyword Analyzer 本質上就是一個 "noop" Analyzer,直接將輸入的內容作爲一整個 token。
第三方中文分詞器 ik
github 地址:https://github.com/medcl/elasticsearch-analysis-ik
IK Analyzer 是一個開源的,基於 java 語言開發的輕量級的中文分詞工具包。從 2006 年 12 月推出 1.0 版開始, IKAnalyzer 已經推出了 4 個大版本。最初,它是以開源項目 Luence 爲應用主體的,結合詞典分詞和文法分析算法的中文分詞組件。從 3.0 版本開始,IK 發展爲面向 Java 的公用分詞組件,獨立於 Lucene 項目,同時提供了對 Lucene 的默認優化實現。在 2012 版本中,IK 實現了簡單的分詞歧義排除算法,標誌着 IK 分詞器從單純的詞典分詞向模擬語義分詞衍化。
使用方式:
// mapping創建
PUT /[your index]
{
"mappings": {
"properties": {
"text_test":{
"type": "text",
"analyzer": "ik_smart"
}
}
}
}
// 新建document
POST /[your index]/_doc
{
"text_test":"我愛中國"
}
//查看term vector
GET /[your index]/_termvectors/ste3HYABZRKvoZUCe2oH?fields=text_test
//結果包含了 “我”“愛”“中國”
相似性得分 similarity
classic:基於 TF/IDF 實現,V7 已禁止使用,V8 徹底廢除(僅供瞭解)
TF/IDF 介紹文章:https://zhuanlan.zhihu.com/p/31197209
TF/IDF 使用逆文檔頻率作爲權重,降低常見詞彙帶來的相似性得分。從公式中可以看出,這個相似性算法僅與文檔詞頻相關,覆蓋不夠全面。例如:缺少文檔長度帶來的權重,當其他條件相同,“王者榮耀” 這個查詢關鍵字同時出現在短篇文檔和長篇文檔中時,短篇文檔的相似性其實更高。
在 ESV5 之前,ES 使用的是 Lucene 基於 TF/IDF 自實現的一套相關性得分算法,如下所示:
score(q,d) =
queryNorm(q)
· coord(q,d)
· ∑ (
tf(t in d)
· idf(t)²
· t.getBoost()
· norm(t,d)
) (t in q)
-
queryNorm:query normalization factor 查詢標準化因子,旨在讓不同查詢之間的相關性結果可以進行比較(實際上 ES 的 tips 中提到,並不推薦大家這樣做,不同查詢之間的決定性因素是不一樣的)
-
coord:coordination factor 協調因子,query 經過分析得到的 terms 在文章中命中的數量越多,coord 值越高。例如:查詢 “王者榮耀五週年”,terms:“王者”、“榮耀”、“五週年”,同時包含這幾個 term 的文檔 coord 值越高
-
tf:詞頻
-
idf:文檔逆頻率
-
boost:boost 翻譯過來是增長推動的意思,這裏可以理解爲一個支持可配的加權參數。
-
norm:文檔長度標準化,內容越長,值越小
Lucene 已經針對 TF/IDF 做了儘可能的優化,但是有一個問題仍然無法避免:
- 詞頻飽和度問題,如下圖所示,TF/IDF 算法的相似性得分會隨着詞頻不斷上升。在 Lucene 現有的算法中,如果一個詞出現的頻率過高,會直接忽略掉文檔長度帶來的權重影響。
另一條曲線是 BM25 算法相似性得分隨詞頻的關係,它的結果隨詞頻上升而趨於一個穩定值。
BM25:默認
BM25 介紹文章:https://en.wikipedia.org/wiki/Okapi_BM25 ,對 BM25 的實現細節我們在這裏不做過多闡述,主要了解一下 BM25 算法相較於之前的算法有哪些優點:
-
詞頻飽和不同於 TF/IDF,BM25 的實現基於一個重要發現:“詞頻和相關性之間的關係是非線性的”。當詞頻到達一定閾值後,對相關性得分的影響是相同的,此時應該由其他因素的權重決定得分高低,例如之前提到的文檔長度。
-
將文檔長度加入算法中相同條件下,短篇文檔的權重值會高於長篇文檔。
-
提供了可調整的參數
我們在查詢過程可以通過設置 "explain":true
查看相似性得分的具體情況
GET /[your index]/_search
{
"explain": true,
"query": {
"match": {
"describe": "測試"
}
}
}
//簡化版查詢結果
{
"_explanation": {
"value": 0.21110919,
"description": "weight(describe:測試 in 1) [PerFieldSimilarity], result of:",
"details": [
{
"value": 0.21110919,
"description": "score(freq=1.0), computed as boost * idf * tf from:",
"details": [
{
"value": 2.2,
"description": "boost",
"details": []
},
{
"value": 0.18232156,
"description": "idf, computed as log(1 + (N - n + 0.5) / (n + 0.5)) from:",
"details": [...]
},
{
"value": 0.5263158,
"description": "tf, computed as freq / (freq + k1 * (1 - b + b * dl / avgdl)) from:",
"details": [...]
}
]
}
]
}
}
boolean
boolean 相似性非常好理解,只能根據查詢條件是否匹配,其最終值其實就是 query boost 值。
query and filter context
-
filter Does this document match this query clause?
filter 只關心是 / 否,根據你過濾條件給你篩選出默認的文檔
-
query how well does this document match this query clause?
query 的關注點除了是否之外,還關注這些文檔的匹配度有多高
他們本質上的區別是是否參與相關性得分。在查詢過程中,官方建議可以根據實際使用情況配合使用 filter
和 query
。但是如果你的查詢並不關心相關性得分,僅關心查詢到的結果,其實兩者差別不大。
題主本來以爲使用 filter 可以節省計算相似性得分的耗時,但是使用 filter 同樣會進行相似性得分,只是通過特殊的方式將其 value 置爲了 0。
//only query
GET /[your index]/_search
{
"explain": true,
"query": {
"bool": {
"must": [
{"match": {"describe": "測試"}},
{"term": {"tab_id": 5}}
]
}
}
}
//簡化_explanation結果
{
"_explanation": {
"value": 1.2111092,
"description": "sum of:",
"details": [
{
"value": 0.21110919,
"description": "weight(describe:測試 in 1) [PerFieldSimilarity], result of:"
},
{
"value": 1,
"description": "tab_id:[5 TO 5]",
"details": []
}
]
}
}
//query+filter
GET /[your index]/_search
{
"explain": true,
"query": {
"bool": {
"filter": [
{"term": {"tab_id": "5"}}
],
"must": [
{"match": {"describe": "測試"}}
]
}
}
}
//簡化_explanation結果
{
"_explanation": {
"value": 0.21110919,
"description": "sum of:",
"details": [
{
"value": 0.21110919,
"description": "weight(describe:測試 in 1) [PerFieldSimilarity], result of:"
},
{
"value": 0,
"description": "match on required clause, product of:",
"details": [
{
"value": 0,
"description": "# clause",
"details": []
},
{
"value": 1,
"description": "tab_id:[5 TO 5]",
"details": []
}
]
}
]
}
}
排序 sort
在執行 ES 查詢時,默認的排序規則是根據相關性得分倒排,針對非全文索引字段,可以指定排序方式,使用也非常簡單。
//查詢時先根據tab_id降序排列,若tab_id相同,則根究status升序排列
GET /[your index]/_search
{
"sort": [
{"tab_id": {"order": "desc"}},
{"status": {"order": "asc"}}
]
}
好坑啊:缺失數值類字段的默認值並不是 0
事情的背景
題主使用的編程語言是 golang,通常使用 pb 定義結構體,生成對應的 go 代碼,默認情況下,結構體字段的 json tag 都會包含 omitempty
屬性,也就是忽略空值,如果數字類型的 value 爲 0,進行 json marshall 時,不會生成對應字段。
事情的經過
剛好題主通過以上方式進行文檔變更,所以實際上如果某個數值字段爲 0,它並沒有被存儲。
在題主的功能邏輯裏,剛好需要對某個數值字段做升序排列,驚奇地發現我認爲的字段值爲 0 的文檔,出現在了列表最末。
事情的調查結果
針對缺失數值類字段的默認值並不是 0,ES 默認會保證排序字段沒有 value 的文檔被放在最後,默認情況下:
-
降序排列,缺失字段默認值爲該字段類型的最小值
-
升序排列,缺失字段默認值爲該字段類型的最大值
好消息是,ES 爲我們提供了 missing
參數,我們可以指定缺失值填充,但是它太隱蔽了 😭,其默認值爲 _last
。
GET /[your index]/_search
{
"sort": [
{"num": {"order": "asc"}}
]
}
//簡化結果
{
"hits": [
{"sort": [1]},
{"sort": [9223372036854775807]},
{"sort": [9223372036854775807]}
]
}
GET /your_index/_search
{
"sort": [
{"num": {"order": "desc"}}
]
}
//簡化結果
{
"hits": [
{"sort": [1]},
{"sort": [-9223372036854775808]},
{"sort": [-9223372036854775808]}
]
}
// with missing
GET /[your index]/_search
{
"sort": [
{
"num": {
"order": "asc",
"missing": "0"
}
}
]
}
//簡化結果
{
"hits": [
{"sort": [0]},
{"sort": [0]},
{"sort": [1]}
]
}
使用技巧:用 function score 實現自定義排序
不知道大家是否遇到過類似的場景:期望查詢結果按照某個類型進行排序,或者查詢結果順序由多個字段的權重組合決定。
具體解決方案需要根據業務具體情況而定,這裏給出一種基於 ES 查詢的解決方案。ES 爲我們提供了 function score
,支持自定義相關性得分 score 的生成方式,部分參數介紹:
-
weight:權重值
-
boost:加權值
-
boost_mode:加權值計算方式(默認爲 multiple)
-
score_mode:得分計算方式(默認爲 multiple)
舉點實際的栗子,假設咱們有一個存放水果的 Index:
- 簡單一點的 case:查詢結果根據水果類型蘋果,梨優先 蘋果的優先級高於梨的優先級,梨的優先級高於其他水果的優先級。我們可以定義梨的權重爲 1,蘋果的權重爲 2
GET /fruit_test/_search
{
"explain": true,
"query": {
"function_score": {
"functions": [
{
"filter": {"term": {"type": "pear"}},
"weight": 1
},
{
"filter": {"term": {"type": "apple"}},
"weight": 2
}
],
"boost": 1,
"score_mode": "sum"
}
}
}
-
複雜一點的 case(別問我是怎麼想到的):
-
優先級一:根據水果是否有貨排序,有貨的排前面,無貨的過濾掉
-
優先級二:根據水果是否預售排序,非預售優先展示
-
優先級三:根據水果類型蘋果,梨優先展示
-
優先級四:根據水果顏色紅色,綠色優先展示
-
優先級五:根據價格升序排序 我們根據優先級順序定義每個條件的權重,指定自定義相關性得分規則後,在
sort
中指定先根據_score
降序排列,再根據價格升序排列。 -
優先級四:綠色權重 1 、紅色權重 2
-
優先級三:梨權重 3 、蘋果權重 4
-
優先級二:預售權重 7(優先級四 max + 優先級三 max = 6,優先級二的權重必須大於這個值)
-
優先級一:直接將無貨水果過濾
GET /fruit_test/_search
{
"query": {
"function_score": {
"query": {"range": {"stock": {"gt": 0}}
},
"functions": [
{
"filter": {"term": {"color": "green"}},
"weight": 1
},
{
"filter": {"term": {"color": "red"}},
"weight": 2
},
{
"filter": {"term": {"type": "pear"}},
"weight": 3
},
{
"filter": {"term": {"type": "apple"}},
"weight": 4
},
{
"filter": {"term": {"pre_sale": false}},
"weight": 7
}
],
"boost": 1,
"boost_mode": "sum",
"score_mode": "sum"
}
},
"sort": [
{"_score": {"order": "desc"}},
{"price_per_kg": {"order": "asc"}
}
]
}
聚合 aggs
聚合操作可以幫助我們將查詢數據按照指定的方式進行歸類。常見的聚合方式,諸如:max、min、avg、range、根據 term 聚合等等,這些都比較好理解,功能使用上也沒有太多疑惑,下面主要介紹題主在使用過程中遇到的坑點以及指標聚合嵌套查詢。
ES 還支持 pipline aggs,主要針對的對象不是文檔集,而是其他聚合的結果,感興趣的同學可以自行了解。
好坑啊:ES 默認的時間格式爲毫秒級時間
如果你有訴求,需要針對秒級時間戳進行時間聚合,例如:某銷售場景下,我們期望按小時 / 天 / 月 / 進行銷售單數統計。
那麼有以下兩種常見錯誤使用方式需要規避:
-
如果在創建
date
類型字段,但是沒有指定時間 format 格式,並且以秒級時間戳賦值(直接以年月日賦值沒有問題) 根據時間聚合將無法解析出正確的數據,時間會被解析爲 1970 年 -
如果直接使用
numberic
類型,例如integer
存儲時間戳 不管是秒級還是毫秒級,都無法被正確識別
正確的做法:創建 mapping,明確指定時間的格式爲秒級時間戳。
PUT /date_test/_mapping
{
"properties":{
"create_time":{
"type":"date",
"format" : "epoch_second"
}
}
}
//以年爲時間間隔 進行統計
GET /date_test/_search
{
"size": 0,
"aggs": {
"test": {
"date_histogram": {
"field": "create_time",
"format": "yyyy",
"interval": "year"
}
}
}
}
//從查詢結果可以看出來,實際計算時ES會幫我們把秒級時間戳轉成毫秒級時間戳
{
"aggregations" : {
"test" : {
"buckets" : [
{
"key_as_string" : "2018",
"key" : 1514764800000,
"doc_count" : 2
},
{
"key_as_string" : "2019",
"key" : 1546300800000,
"doc_count" : 0
},
{
"key_as_string" : "2020",
"key" : 1577836800000,
"doc_count" : 3
}
]
}
}
}
聚合嵌套查詢
上面介紹了根據時間聚合,還是以剛剛的例子來說,某銷售場景下,我們期望在根據時間統計銷售單數的同時,統計出時間區間內的銷售總金額。
GET /date_test/_search
{
"size": 0,
"aggs": {
"test": {
"date_histogram": {
"field": "create_time",
"format": "yyyy",
"interval": "year"
},
"aggs": {
"sum_profit": {
"sum": {
"field": "profit"
}
}
}
}
}
}
{
"aggregations" : {
"test" : {
"buckets" : [
{
"key_as_string" : "2018",
"key" : 1514764800000,
"doc_count" : 2,
"sum_profit" : {
"value" : 200.0
}
},
{
"key_as_string" : "2019",
"key" : 1546300800000,
"doc_count" : 0,
"sum_profit" : {
"value" : 0.0
}
},
{
"key_as_string" : "2020",
"key" : 1577836800000,
"doc_count" : 3,
"sum_profit" : {
"value" : 3000.0
}
}
]
}
}
}
使用技巧:自實現 distinct
ES 默認並不支持 distinct,可以嘗試使用 terms
聚合,解析結果中的 key
{
"aggregations" : {
"test" : {
"doc_count_error_upper_bound" : 0,
"sum_other_doc_count" : 0,
"buckets" : [
{"key" : "1","doc_count" : 2},
{"key" : "10","doc_count" : 2},
{"key" : "16","doc_count" : 2}
]
}
}
}
索引別名、索引生命週期策略、索引模版
-
Aliases 索引別名 索引別名,顧名思義,定義了別名之後,可以通過別名對 index 進行查詢
PUT /[your index]/_alias/[your alias name]
-
Index Lifecycle Policies 索引生命週期策略 索引生命週期策略支持我們根據天、存儲量級等信息去自動管理我們的索引。創建方式可以通過 RESTful API,也可以直接在 kibana 上創建,題主使用的是後者,可視化界面看起來比較清晰~ 支持配置滿足一定規則後索引自動變化:
-
自動滾動索引(hot)
-
保留索引僅供檢索(warm)
-
保留索引僅供檢索同時減少磁盤存儲(cold)
-
刪除索引
-
Template 索引模板 通過
index_patterns
參數設置索引名正則匹配規則,向一個不存在的索引 POST 數據,命中索引名規則後即會根據索引模版創建索引,不會進行動態映射。
ES 的一個比較常見的應用場景是存儲日誌流,自實現一套這樣的系統就可以結合上述 3 個功能。
參考
-
https://www.jianshu.com/p/1a737a3dde86
-
https://www.modb.pro/db/130339
-
https://www.cnblogs.com/qdhxhz/p/11448451.html
-
https://blog.csdn.net/tengxing007/article/details/100663530
-
https://www.elastic.co/guide/en/elasticsearch/reference/7.7/index.html
-
https://zhuanlan.zhihu.com/p/35469104
-
https://zhuanlan.zhihu.com/p/142641300
-
https://www.elastic.co/guide/en/elasticsearch/reference/7.7/index.html
-
https://blog.csdn.net/laoyang360/article/details/80468757
-
https://www.elastic.co/guide/en/elasticsearch/reference/7.0/query-filter-context.html
-
https://zhuanlan.zhihu.com/p/31197209
-
https://code.google.com/archive/p/ik-analyzer/
-
https://zhuanlan.zhihu.com/p/79202151
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/GG_zrQlaiP2nfPOxzx_j9w