Elasticsearch(一)架构及一般性应用
?
首先,當我們對記錄進行修改時,es會把數據同時寫到內存緩存區和translog中。而這個時候數據是不能被搜索到的,只有數據形成了segmentFile,才會被搜索到。默認情況下,es每隔一秒鐘執行一次refresh,可以通過參數index.refresh_interval來修改這個刷新間隔或者搜索時加上?refresh=wait_for強制刷新,但是會造成刷新頻次過高會造成性能下降,表示如果1秒內有請求立即更新并可見,執行refresh主要做三件事:
1、所有在內存緩沖區中的文檔被寫入到一個新的segment中,但是沒有調用fsync,因此內存中的數據可能丟失;
2、segment被打開使得里面的文檔能夠被搜索到;
3、清空內存緩沖區;
translog的相當于事務日志,記錄著所有對Elasticsearch的操作記錄,也是對Elasticsearch的一種備份。因為并不是寫到segment就表示數據落到磁盤了,實際上segment是存儲在系統緩存(page cache)中的,只有達到一個周期或者數據量達到一定值,才會flush到磁盤上。這個時候如果系統內存中的segment丟失,是可以通過translog來恢復的。這個flush過程主要做了三件事:
1、往磁盤里寫入commit point信息。
2、文件系統中的segment,fsync到磁盤。
3、清空translog文件。
translog可以保證緩存中的segment的恢復,但translog也不是實時也磁盤的,也就是說,內存中的translog丟了的話,也會有丟失數據的可能。所以translog也要進行flush。translog的flush主要有三個條件:
1、可以設置是否在某些操作之后進行強制flush,比如索引的刪除或批量請求之后。
2、translog大小超過512mb或者超過三十分鐘會強制對segment進行flush,隨后會強制對translog進行flush,這種情況緩存中的translog在flush之后會被清空。
3、默認5s,會強制對translog進行flush。最小值可配置100ms。
6.3版本顯示保留translog文件的最長持續時間。默認為12h。
參考官網:Translog | Elasticsearch Guide [6.3] | Elastic
refresh,flush 和fsync的區別
1.refresh是將緩沖隊列buffer里數據刷入文件緩沖系統生成索引文件segement,該segement數據才能被查詢到,保證查詢屬性可見
2.flush是將索引文件segement數據持久化到硬盤(觸發機制是translog文件超過512mb或者30分鐘強強制刷新segement)
3.fsyncd是將translog 持久化到硬盤(每5秒執行一次) 寫入translog其實也是在內存中。translog 和segement持久化到硬盤是兩回事。
持久化的translog文件中存的是所有索引成segement的數據但還未持久化到硬盤的內容,一旦segement持久化到硬盤translog會清空。
參考:https://elasticsearch.cn/question/3847
索引存儲方式
Elasticsearch是一個建立在全文搜索引擎庫Apache Lucene 基礎上的分布式搜索引擎,Lucene最早的版本是2000年發布的,距今已經18年,是當今最先進,最高效的全功能開源搜索引擎框架。
Lucene
Lucene中包含了四種基本數據類型,分別是:
- Index:索引,由很多的Document組成。
- Document:由很多的Field組成,是Index和Search的最小單位。
- Field:由很多的term組成,包括field_name和field_value。
- Term:由很多的字節組成,可以分詞,分詞之后每個詞即為一個term。term是索引的最小單元。
上述四種類型在Elasticsearch中同樣存在,意思也一樣。
Lucene中存儲的索引主要分為三種類型:
- Invert Index,即倒排索引。通過term可以快速查找到包含該term的doc_id。如果Field配置分詞,則分詞后的每個term都會進入倒排索引,如果Field不指定分詞,那該Field的value值則會作為一個term進入倒排。(這里需要注意的是term的長度是有限制的,如果對一個Field不采取分詞,那么不建議該Field存儲過長的值。關于term超長處理)
- DocValues,即正排索引。采用的是類似數據庫的列式存儲。對于一些特殊需求的字段可以選擇這種索引方式。
- Store,即原文。存儲整個完整Document的原始信息。
倒排索引是lucene的核心索引類型,采用鏈表的數據結構,倒排索引中的key就是一個term,value就是以doc_id形成的鏈表結構。
Term ? ? ?Doc_1 ? ? ?Doc_2
-------------------------
Quick ? ? ?| ? ? ? ? ? ? ? | ?X
The ? ? ? ? | ? X ? ? ? ? ?|
brown ? ? | ? X ? ? ? ? ?| ?X
dog ? ? ? ? | ? X ? ? ? ? ?|
dogs ? ? ? | ? ? ? ? ? ? ? ?| ?X
fox ? ? ? ? ?| ? X ? ? ? ? ?|
foxes ? ? ?| ? ? ? ? ? ? ? ?| ?X
in ? ? ? ? ? ?| ? ? ? ? ? ? ? ?| ?X
jumped ?| ? X ? ? ? ? ?| ? ?
lazy ? ? ? ?| ? X ? ? ? ? ?| ?X
leap ? ? ? ?| ? ? ? ? ? ? ? ?| ?X
over ? ? ? ?| ? X ? ? ? ? ?| ?X
quick ? ? ?| ? X ? ? ? ? ?|
summer ?| ? ? ? ? ? ? ? | ?X
the ? ? ? ? ?| ? X ? ? ? ? ?|
------------------------
現在,如果我們想搜索 quick brown ,我們只需要查找包含每個詞條的文檔:
Term ? ? ?Doc_1 ? ? ?Doc_2
-------------------------
brown ? | ? X ? ? ? ? ?| ?X
quick ? ?| ? X ? ? ? ? ? |
------------------------
Total ? ? | ? 2 ? ? ? ? ? | ?1
這里分別匹配到了doc1和doc2,但是doc1匹配度要高于doc2。
倒排索引中的value有四種存儲類型:
- DOCS:只存儲doc_id。
- DOCS_AND_FREQS:存儲doc_id和詞頻(Term Freq)。
- DOCS_AND_FREQS_AND_POSITIONS:存儲doc_id、詞頻(Term Freq)和位置。
- DOCS_AND_FREQS_AND_POSITIONS_AND_OFFSETS:存儲doc_id、詞頻(Term Freq)、位置和偏移(offset)。
DocValues
正排索引類似關系型數據庫的存儲模式,作用是通過doc_id和field_name可以快速定位到指定doc的特定字段值。DocValues的key是doc_id+field_name,value是field_value。
ES默認會對所有字段進行正排索引,但并不是所有字段都需要DocValues。所以合理配置DocValues可以節省存儲空間。DocValues的使用場景一般是:需要針對某個Field排序、聚合、過濾和script。
Store
存儲的是Document的完整信息,包括所有field_name和field_value。Store的key是doc_id,value是field_name+field_value。對于上訴中需要聚合和排序的Field并沒有開啟DocValues的情況,依然可以實現排序和聚合,會從Store中獲取要排序聚合的字段值。
Elasticsearch在Lucene基礎上的改變
Lucene本身不支持分布式,Elasticsearch通過_routing實現分布式的架構。我們可以通過_routing來實現不同doc分布在不同的Shard上,我們可以自己定義也可以系統自動分配。對于指定_routing的插入和查詢,性能上會更好。
Lucene中沒有主鍵索引,并且id是在Segment中唯一。那么Elasticsearch是如何實現doc_id唯一?Elasticsearch中有個系統字段_id,來決定doc唯一。_id是在用戶可見層度上的id值,實際上Elasticsearch內部會把_id存儲成_uid(_uid =index_type + '#' + _id)。_uid只會存儲倒排和原文,目的就是為了通過id可以快速索引到doc。這里需要注意的是,在Elasticsearch6.x版本以后,一個index只支持一個type,這也就意味著_id和_uid概念幾乎相同。以后的版本中,Elasticsearch會取消type。
Elasticsearch通過_version字段來保證文檔的一致性。更多關于文檔的一致性和鎖機制的參考:ElasticSearch干貨(一):鎖機制
Elasticsearch通過_source字段來存儲doc原文。這個字段非常重要。Lucene的update是覆蓋,是不支持針對doc中特定字段進行修改的。但Elasticsearch支持對特定字段的修改,就是基于_source字段實現的。關于Elasticsearch的update詳細內容,參考:ElasticSearch干貨(二):index、create、update區別
Elasticsearch通過_field_names字段來判斷doc中是否存在某個字段。_field_names的存儲形式為倒排,可以快速判斷出是否包含某個field_name。
Lucene中Segment一旦創建不可修改。那么Elasticsearch如何實現實時修改并索引數據的呢?詳細參考:ElasticSearch原理(三):寫入流程
關于原理和索引先介紹到這里,主要這里還是聚焦與如何實現幾個關鍵需求?
ES延時問題
默認情況下,es每隔一秒鐘執行一次refresh,可以通過參數index.refresh_interval來修改這個刷新間隔或者搜索時加上?refresh=wait_for強制刷新,但是會造成刷新頻次過高會造成性能下降,表示如果1秒內有請求立即更新并可見。另外由于沒有生成segment,也就是說不能通過索引來獲取,但是可以通過直接get by id來獲取單條記錄。
搜索(同時實現精確查詢和模糊查詢和組合查詢)
ES的搜索是分2個階段進行的,即Query階段和Fetch階段。? Query階段比較輕量級,通過查詢倒排索引,獲取滿足查詢結果的文檔ID列表。? 而Fetch階段比較重,需要將每個shard的結果取回,在協調結點進行全局排序。? 通過From+size這種方式分批獲取數據的時候,隨著from加大,需要全局排序并丟棄的結果數量隨之上升,性能越來越差。
1、精確查詢
在elasticsearch 中輸入查詢條件,一般會匹配到很多結果,是因為analyzer的存在:
Each element in the result represents a single term:
{"tokens": [{"token": "text","start_offset": 0,"end_offset": 4,"type": "<ALPHANUM>","position": 1},{"token": "to","start_offset": 5,"end_offset": 7,"type": "<ALPHANUM>","position": 2},{"token": "analyze","start_offset": 8,"end_offset": 15,"type": "<ALPHANUM>","position": 3}] }必須要將字段設置為not_analyzed?才可以,如下:
PUT /my_store { "mappings" : { "products" : { "properties" : { "productID" : { "type" : "string", "index" : "not_analyzed" } } } } }
2、es組合多個條件進行查詢
1、must、should
GET /test_index/_search
{
"query": {
"bool": {
"must": { "match": { "name": "tom" }},
"should": [
{ "match": { "hired": true }},
{ "bool": {
"must": { "match": { "personality": "good" }},
"must_not": { "match": { "rude": true }}
}}
],
"minimum_should_match": 1
}
}
}
在es中,使用組合條件查詢是其作為搜索引擎檢索數據的一個強大之處,在前幾篇中,簡單演示了es的查詢語法,但基本的增刪改查功能并不能很好的滿足復雜的查詢場景,比如說我們期望像mysql那樣做到拼接復雜的條件進行查詢該如何做呢?es中有一種語法叫bool,通過在bool里面拼接es特定的語法可以做到大部分場景下復雜條件的拼接查詢,也叫復合查詢
首先簡單介紹es中常用的組合查詢用到的關鍵詞,
filter:過濾,不參與打分
must:如果有多個條件,這些條件都必須滿足 and與
should:如果有多個條件,滿足一個或多個即可 or或
must_not:和must相反,必須都不滿足條件才可以匹配到 !非
發生 描述
must
該條款(查詢)必須出現在匹配的文件,并將有助于得分。
filter
子句(查詢)必須出現在匹配的文檔中。然而不像 must查詢的分數將被忽略。Filter子句在過濾器上下文中執行,這意味著評分被忽略,子句被考慮用于高速緩存。
should
子句(查詢)應該出現在匹配的文檔中。如果 bool查詢位于查詢上下文中并且具有mustor filter子句,則bool即使沒有should查詢匹配,文檔也將匹配該查詢 。在這種情況下,這些條款僅用于影響分數。如果bool查詢是過濾器上下文 或者兩者都不存在,must或者filter至少有一個should查詢必須與文檔相匹配才能與bool查詢匹配。這種行為可以通過設置minimum_should_match參數來顯式控制 。
must_not
子句(查詢)不能出現在匹配的文檔中。子句在過濾器上下文中執行,意味著評分被忽略,子句被考慮用于高速緩存。因為計分被忽略,0所有文件的分數被返回。
3、模糊查詢
前綴查詢:匹配包含具有指定前綴的項(not analyzed)的字段的文檔。前綴查詢對應?Lucene?的?PrefixQuery?。
案例 GET /_search { "query": {"prefix" : { "user" : { "value" : "ki", "boost" : 2.0 } }} }正則表達式查詢:egexp?(正則表達式)查詢允許您使用正則表達式進行項查詢。有關支持的正則表達式語言的詳細信息,請參閱正則表達式語法。第一個句子中的 “項查詢” 意味著?Elasticsearch?會將正則表達式應用于由該字段生成的項,而不是字段的原始文本。注意:?regexp?(正則表達式)查詢的性能很大程度上取決于所選的正則表達式。匹配一切像?“.*”?,是非常慢的,使用回顧正則表達式也是如此。如果可能,您應該嘗試在正則表達式開始之前使用長前綴。通配符匹配器?“.*?+”?將主要降低性能。
案例 GET /_search {"query": {"regexp":{"name.first":{"value":"s.*y","boost":1.2}}} } ‘通配符查詢:匹配與通配符表達式具有匹配字段的文檔(not analyzed)。支持的通配符是 “*”,它匹配任何字符序列(包括空字符);還有 “?”,它匹配任何單個字符。請注意,此查詢可能很慢,因為它需要迭代多個項。為了防止極慢的通配符查詢,通配符項不應以通配符 “*” 或 “?” 開頭。通配符查詢對應?Lucene?的?WildcardQuery?。
案例 GET /_search {"query": {"wildcard" : { "user" : { "value" : "ki*y", "boost" : 2.0 } }} }###模糊查詢數據量越大效率越低,當查詢內容較多,數據量較大時建議將該字段設置成text進行分詞,然后通過match進行匹配。
排序與相關性
默認情況下,返回的結果是按照 相關性 進行排序的——最相關的文檔排在最前。 在本章的后面部分,我們會解釋 相關性 意味著什么以及它是如何計算的, 不過讓我們首先看看 sort 參數以及如何使用它。
1、排序
為了按照相關性來排序,需要將相關性表示為一個數值。在 Elasticsearch 中, 相關性得分 由一個浮點數進行表示,并在搜索結果中通過 _score 參數返回, 默認排序是 _score 降序。
有時,相關性評分對你來說并沒有意義。例如,下面的查詢返回所有 user_id 字段包含 1 的結果:
GET /_search {"query" : {"bool" : {"filter" : {"term" : {"user_id" : 1}}}} }里沒有一個有意義的分數:因為我們使用的是 filter (過濾),這表明我們只希望獲取匹配 user_id: 1 的文檔,并沒有試圖確定這些文檔的相關性。 實際上文檔將按照隨機順序返回,并且每個文檔都會評為零分。
1.1、按照字段的值排序
在這個案例中,通過時間來對 tweets 進行排序是有意義的,最新的 tweets 排在最前。 我們可以使用 sort 參數進行實現:
GET /_search {"query" : {"bool" : {"filter" : { "term" : { "user_id" : 1 }}}},"sort": { "date": { "order": "desc" }} }1.2、多級排序
假定我們想要結合使用 date 和 _score 進行查詢,并且匹配的結果首先按照日期排序,然后按照相關性排序:
GET /_search {"query" : {"bool" : {"must": { "match": { "tweet": "manage text search" }},"filter" : { "term" : { "user_id" : 2 }}}},"sort": [{ "date": { "order": "desc" }},{ "_score": { "order": "desc" }}] }排序條件的順序是很重要的。結果首先按第一個條件排序,僅當結果集的第一個 sort 值完全相同時才會按照第二個條件進行排序,以此類推。
多級排序并不一定包含 _score 。你可以根據一些不同的字段進行排序, 如地理距離或是腳本計算的特定值。
1.3、字段多值的排序
一種情形是字段有多個值的排序, 需要記住這些值并沒有固有的順序;一個多值的字段僅僅是多個值的包裝,這時應該選擇哪個進行排序呢?
對于數字或日期,你可以將多值字段減為單值,這可以通過使用 min 、 max 、 avg 或是 sum 排序模式 。 例如你可以按照每個 date 字段中的最早日期進行排序,通過以下方法:
"sort": {"dates": {"order": "asc","mode": "min"} }- 更多詳情清參考此文;
如何在elasticsearch里面使用分頁功能
from + size 淺分頁
"淺"分頁可以理解為簡單意義上的分頁。它的原理很簡單,就是查詢前20條數據,然后截斷前10條,只返回10-20的數據。這樣其實白白浪費了前10條的查詢。
GET test_dev/_search {"query": {"bool": {"filter": [{"term": {"age": 28}}]}},"size": 10,"from": 20,"sort": [{"timestamp": {"order": "desc"},"_id": {"order": "desc"}}] }其中,from定義了目標數據的偏移值,size定義當前返回的數目。默認from為0,size為10,即所有的查詢默認僅僅返回前10條數據。
在這里有必要了解一下from/size的原理:
因為es是基于分片的,假設有5個分片,from=100,size=10。則會根據排序規則從5個分片中各取回100條數據數據,然后匯總成500條數據后選擇最后面的10條數據。
做過測試,越往后的分頁,執行的效率越低??傮w上會隨著from的增加,消耗時間也會增加。而且數據量越大,就越明顯!
es默認的from+size的分頁方式返回的結果數據集不能超過1萬點,超過之后返回的數據越多性能就越低;
這是因為es要計算相似度排名,需要排序整個整個結果集,假設我們有一個index它有5個shard,現在要讀取1000到1010之間的這10條數據,es內部會在每個shard上讀取1010條數據,然后返回給計算節點,這里有朋友可能問為啥不是10條數據而是1010條呢?這是因為某個shard上的10條數據,可能還沒有另一個shard上top10之后的數據相似度高,所以必須全部返回,然后在計算節點上,重新對5050條數據進行全局排序,最后在選取top 10出來,這里面排序是非常耗時的,所以這個數量其實是指數級增長的,到后面分頁數量越多性能就越下降的厲害,而且大量的數據排序會占用jvm的內存,很有可能就OOM了,這也是為什么es默認不允許讀取超過1萬條數據的原因。?
scroll 深分頁
from+size查詢在10000-50000條數據(1000到5000頁)以內的時候還是可以的,但是如果數據過多的話,就會出現深分頁問題。
為了解決上面的問題,elasticsearch提出了一個scroll滾動的方式。
scroll 類似于sql中的cursor,使用scroll,每次只能獲取一頁的內容,然后會返回一個scroll_id。根據返回的這個scroll_id可以不斷地獲取下一頁的內容,所以scroll并不適用于有跳頁的情景。
然后我們可以通過數據返回的_scroll_id讀取下一頁內容,每次請求將會讀取下10條數據,直到數據讀取完畢或者scroll_id保留時間截止:
GET _search/scroll {"scroll_id": "DnF1ZXJ5VGhlbkZldGNoBQAAAAAAAJZ9Fnk1d......","scroll": "5m" }注意:請求的接口不再使用索引名了,而是 _search/scroll,其中GET和POST方法都可以使用。
scroll刪除
根據官方文檔的說法,scroll的搜索上下文會在scroll的保留時間截止后自動清除,但是我們知道scroll是非常消耗資源的,所以一個建議就是當不需要了scroll數據的時候,盡可能快的把scroll_id顯式刪除掉。
清除指定的scroll_id:
DELETE _search/scroll/DnF1ZXJ5VGhlbkZldGNo.....清除所有的scroll:
DELETE _search/scroll/_allsearch_after 深分頁
scroll 的方式,官方的建議不用于實時的請求(一般用于數據導出),因為每一個 scroll_id 不僅會占用大量的資源,而且會生成歷史快照,對于數據的變更不會反映到快照上。
search_after 分頁的方式是根據上一頁的最后一條數據來確定下一頁的位置,同時在分頁請求的過程中,如果有索引數據的增刪改查,這些變更也會實時的反映到游標上。但是需要注意,因為每一頁的數據依賴于上一頁最后一條數據,所以無法跳頁請求。
為了找到每一頁最后一條數據,每個文檔必須有一個全局唯一值,官方推薦使用 _uid 作為全局唯一值,其實使用業務層的 id 也可以。
GET test_dev/_search {"query": {"bool": {"filter": [{"term": {"age": 28}}]}},"size": 20,"from": 0,"sort": [{"timestamp": {"order": "desc"},"_id": {"order": "desc"}}] }使用sort返回的值搜索下一頁:
GET test_dev/_search {"query": {"bool": {"filter": [{"term": {"age": 28}}]}},"size": 10,"from": 0,"search_after": [1541495312521,"d0xH6GYBBtbwbQSP0j1A"],"sort": [{"timestamp": {"order": "desc"},"_id": {"order": "desc"}}] } </div>只是整合記錄少有原創
總結
以上是生活随笔為你收集整理的Elasticsearch(一)架构及一般性应用的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 网关和BFF是如何演进出来的?
- 下一篇: 微服务架构下的安全认证与鉴权