
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 更了解你的資料,可能剛開始使用起來覺得比較方便,但是最好還是自己明確定義映射關系,
(??? 個人認為,這些自動操作是在用戶對 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 -
Lower Case Token Filter -
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)2
· 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
作者:lynneyli
本文來自博客園,作者:古道輕風,轉載請注明原文鏈接:https://www.cnblogs.com/88223100/p/Basic-Introduction-to-Elasticsearch.html
轉載請註明出處,本文鏈接:https://www.uj5u.com/shujuku/542118.html
標籤:其他
上一篇:ClickHouse(11)ClickHouse合并樹MergeTree家族表引擎之SummingMergeTree詳細決議
