ES底層原理
1、倒排索引(分段,segment)
Elasticsearch 使用一種稱為倒排索引的結構,它適用于快速的全文搜索。
有倒排索引,肯定會對應有正向索引:
- 正向索引(forward index)
- 反向索引(inverted index,實際就是倒排索引)
所謂的正向索引,就是搜索引擎會將待搜索的文件都對應一個文件ID,搜索時將這個ID和搜索關鍵字進行對應,形成K-V對,然后對關鍵字進行統計計數。

但是互聯網上收錄在搜索引擎中的文檔的數目是個天文數字,這樣的索引結構根本無法滿足實時返回排名結果的要求。所以,搜索引擎會將正向索引重新構建為倒排索引,即把文件ID對應到關鍵詞的映射轉換為關鍵詞到文件ID的映射(跟正向索引反過來了),每個關鍵詞都對應著一系列的文件,這些文件中都出現這個關鍵詞。

1.1、倒排索引示例
倒排索引的例子:一個倒排索引由文檔中所有不重復詞的列表構成,對于其中每個詞,有一個包含它的文檔列表。例如,假設我們有兩個文檔,每個文檔的 content 域包含如下內容:
- The quick brown fox jumped over the lazy dog
- Quick brown foxes leap over lazy dogs in summer
為了創建倒排索引,我們首先將每個文檔的 content 域拆分成單獨的詞(我們稱它為詞條或tokens ),創建一個包含所有不重復詞條的排序列表,然后列出每個詞條出現在哪個文檔。結果如下所示:

現在,如果我們想搜索 quick brown ,我們只需要查找包含每個詞條的文檔:

兩個文檔都匹配,但是第一個文檔比第二個匹配度更高。如果我們使用僅計算匹配詞條數量的簡單相似性算法,那么我們可以說,對于我們查詢的相關性來講,第一個文檔比第二個文檔更佳。
但是,我們目前的倒排索引有一些問題:
-
Quick和quick以獨立的詞條出現,然而用戶可能認為它們是相同的詞。 -
fox和foxes非常相似,就像dog和dogs;他們有相同的詞根。 -
jumped和leap,盡管沒有相同的詞根,但他們的意思很相近。他們是同義詞。
使用前面的索引搜索+Quick +fox不會得到任何匹配文檔。(記住,+前綴表明這個詞必須存在)。只有同時出現Quick和fox 的文檔才滿足這個查詢條件,但是第一個文檔包含quick fox ,第二個文檔包含Quick foxes 。我們的用戶可以合理的期望兩個文檔與查詢匹配。我們可以做的更好。
如果我們將詞條規范為標準模式,那么我們可以找到與用戶搜索的詞條不完全一致,但具有足夠相關性的文檔。例如:
- Quick可以小寫化為quick。
- foxes可以詞干提取變為詞根的格式為fox。類似的,dogs可以為提取為dog。
- jumped和leap是同義詞,可以索引為相同的單詞jump 。
現在索引看上去像這樣:

這還遠遠不夠。我們搜索+Quick +fox 仍然會失敗,因為在我們的索引中,已經沒有Quick了。但是,如果我們對搜索的字符串使用與content域相同的標準化規則,會變成查詢+quick +fox,這樣兩個文檔都會匹配!分詞和標準化的過程稱為分析,這非常重要。你只能搜索在索引中出現的詞條,所以索引文本和查詢字符串必須標準化為相同的格式。
1.2、不可改變的倒排索引
早期的全文檢索會為整個文檔集合建立一個很大的倒排索引并將其寫入到磁盤。 一旦新的索引就緒,舊的就會被其替換,這樣最近的變化便可以被檢索到。
倒排索引被寫入磁盤后是不可改變的:它永遠不會修改。不變性的好處如下:
- 不需要鎖。如果你從來不更新索引,你就不需要擔心多進程同時修改數據的問題。
- 一旦索引被讀入內核的文件系統緩存,便會留在哪里,由于其不變性,只要文件系統緩存中還有足夠的空間,那么大部分讀請求會直接請求內存,而不會命中磁盤,這提供了很大的性能提升。
- 其它緩存(像filter緩存),在索引的生命周期內始終有效,它們不需要在每次數據改變時被重建,因為數據不會變化。
- 寫入單個大的倒排索引允許數據被壓縮,可減少磁盤 IO 和需要被緩存到內存的索引的使用量。
壞處:
- 每次都要重新構建整個索引
1.3、如何動態更新倒排索引(段,segment)
一個不變的索引有上述好處,當然也有不好的地方,主要因為是它是不可變的,你不能修改它。所以如果你需要讓一個新的文檔可被搜索,你需要重建整個索引。這要么對一個索引所能包含的數據量造成了很大的限制,要么對索引可被更新的頻率造成了很大的限制。
如何在保留不變性的前提下實現倒排索引的更新?答案是:用更多的索引。通過增加新的補充索引來反映新近的修改,而不是直接重寫整個倒排索引。每一個倒排索引都會被輪流查詢到,從最早的開始查詢,然后再對結果進行合并。
Elasticsearch基于Lucene,這個java庫引入了按段搜索的概念。每一段本身都是一個倒排索引,但索引在 Lucene 中除表示所有段的集合外,還增加了提交點的概念:一個列出了所有已知段的文件。

按段搜索會以如下流程執行:
1)新文檔被收集到內存索引緩存。

2)不時地,緩存被提交。
- 一個新的段,一個追加的倒排索引,被寫入磁盤。
- 一個新的包含新段名字的提交點被寫入磁盤。
- 磁盤進行同步,所有在文件系統緩存中等待的寫入都刷新到磁盤,以確保它們被寫入物理文件
3)新的段被開啟,讓它包含的文檔可見以被搜索。
4)內存緩存被清空,等待接收新的文檔。

當一個查詢被觸發,所有已知的段按順序被查詢。詞項統計會對所有段的結果進行聚合,以保證每個詞和每個文檔的關聯都被準確計算,這種方式可以用相對較低的成本將新文檔添加到索引。
段是不可改變的,所以既不能從把文檔從舊的段中移除,也不能修改舊的段來進行反映文檔的更新。取而代之的是,每個提交點會包含一個.del 文件,文件中會列出這些被刪除文檔的段信息。
- 當一個文檔被“刪除”時,它實際上只是在 .del 文件中被標記刪除。一個被標記刪除的文檔仍然可以被查詢匹配到,但它會在最終結果被返回前從結果集中移除。
- 文檔更新也是類似的操作方式:當一個文檔被更新時,舊版本文檔被標記刪除,文檔的新版本被索引到一個新的段中。可能兩個版本的文檔都會被一個查詢匹配到,但被刪除的那個舊版本文檔在結果集返回前就已經被移除。
1.4、索引、分片、分段的關系
每個分片包含多個“分段”,其中分段是倒排索引。分段內的 doc 數量上限是2的31次方。默認每秒都會生成一個 segment 文件。在分片中搜索將依次搜索每個片段,然后將其結果合并到該分片的最終結果中。索引、分片、分段的關系如下圖:

2、文檔寫入原理
分別從集群角度和分片(shard)角度來介紹數據如何寫入。
2.1、集群角度(Primary -> Replica)
客戶端可以發送請求到集群中的任一節點。 每個節點都有能力處理任意請求。 每個節點都知道集群中任一文檔位置,所以可以直接將請求轉發到需要的節點上。

流程說明如下:
- 客戶端向NODE1 發送寫請求。檢查 Active 的 Shard 數。
- NODE1 使用文檔 ID 來確定文檔屬于的分片(圖例是:分片0),通過集群狀態中的信息獲知分片0的主分片位于 NODE3,因此請求被轉發到 NODE3 上。NODE3 上的主分片執行寫操作。
- 并發的向所有副本分片發起寫入請求,即將請求并行轉發到 NODE1 和 NODE2 的副分片上。
- 等待所有副本分片返回結果給主分片
- 主分片將最終的成功或者失敗后結果返回給協調節點,最后返回到 Client
一個文檔從開始更新或修改,到可被搜索的延遲主要就是主分片的延時 + 并行寫入副本的最大延時,如下圖:

Q&A:
- 為什么要檢查Active的Shard數?
ES中有一個參數,叫做wait_for_active_shards。這個參數的含義是,在每次寫入前,該shard至少具有的active副本數。假設我們有一個Index,其每個Shard有3個Replica,加上Primary則總共有4個副本。如果配置wait_for_active_shards為3,那么允許最多有一個Replica掛掉,如果有兩個Replica掛掉,則Active的副本數不足3,此時不允許寫入。
這個參數默認是1,即只要Primary在就可以寫入。如果配置大于1,可以起到一種保護的作用,保證寫入的數據具有更高的可靠性。但是這個參數只在寫入前檢查,并不保證數據一定在至少這些個副本上寫入成功,所以并不是嚴格保證了最少寫入了多少個副本。
- 寫入Primary完成后,為何要等待所有同步Replica響應(或連接失敗)后返回?
早期ES版本,Primary和Replica之間是允許異步復制的,即寫入Primary成功即可返回。但是這種模式下,如果Primary掛掉,就有丟數據的風險,而且從Replica讀數據也很難保證能讀到最新的數據。所以后來ES就取消異步模式了,改成Primary等同步Replica返回后再返回給客戶端。
- 如果某個Replica持續寫失敗,用戶是否會經常查到舊數據?
假如一個Replica持續寫入失敗,那么這個Replica上的數據可能落后Primary很多。Primary會將這個信息報告給Master,然后Master會在Meta中更新這個Index的InSyncAllocations配置,將這個Replica從中移除,移除后它就不再承擔讀請求。在Meta更新到各個Node之前,用戶可能還會讀到這個Replica的數據,但是更新了Meta之后就不會了。所以這個方案并不是非常的嚴格,考慮到ES本身就是一個近實時系統,數據寫入后需要refresh才可見,所以一般情況下,在短期內讀到舊數據應該也是可接受的。
2.2、分片角度
下面詳細介紹數據是如何寫入分片的。

在每一個Shard中,寫入流程分為兩部分,先寫入Lucene,再寫入TransLog。
- 寫入請求到達Shard(分片)后,先寫Lucene文件。此時索引還在內存 Buffer 緩存里面,接著去寫TransLog。
- 每隔1秒鐘執行一次 refresh 操作,將index-buffer(即內存)中文檔(document)生成的segment寫到文件緩存系統之中。(當文檔在文件系統緩存中時,就已經可以像其它文件一樣被打開和讀取了。)
- 每 30 分鐘或當 translog 達到一定大小(由index.translog.flush_threshold_size控制,默認512mb),ES會觸發一次 flush 操作,此時 ES 會先執行 refresh 操作將 buffer 中的數據生成 segment,然后調用 lucene 的 commit 過程將所有文件緩存系統中的 segment fsync 到磁盤。此時lucene中的數據就完成了持久化。
- 寫磁盤成功后,請求返回給用戶。
(當一個文檔寫入Lucene后是不能被立即查詢到的,Elasticsearch提供了一個refresh操作,會定時地為內存中新寫入的數據生成一個新的segment,此時被處理的文檔均可以被檢索到。refresh操作的時間間隔由refresh_interval參數控制,默認為1s。)
Q&A:
- 為什么es要先寫入
lucene,后寫入translog?
Lucene的內存寫入會有很復雜的邏輯,很容易失敗,比如分詞,字段長度超過限制等,比較重,為了避免TransLog中有大量無效記錄,為了減少寫入失敗回滾的復雜度和提高速度,所以就把寫Lucene放在了最前面。
2.2.1、refresh 操作
Memory Buffer的緩存區。Memory Buffer的性能非常高,客戶端發出寫入請求的時候是直接寫在Memory Buffer里的。Memory Buffer的空間閾值默認大小為堆內存的10%,時間閾值為1s。空間閾值和時間閾值只要達成任意一個,就會觸發 Refresh操作。默認1秒鐘刷新一次,所以說ES是近實時的搜索引擎,不是準實時。文檔的變化并不是立即對搜索可見,但會在1秒之內變為可見。相比于 Lucene 的提交操作,ES的refresh是相對輕量級的操作。先將緩存中文檔(document)生成的segment寫到文件緩存系統之中,這樣避免了比較損耗性能 io 操作,又可以使搜索可見。

內存索引緩沖區中的文檔被寫入新段,新段首先寫入文件系統緩存(這個過程性能消耗很低),然后才刷新到磁盤(這個過程代價很高)。但是,在文件進入緩存后,它就已經對搜索可見。
-
手動刷新文檔(refresh)
Elasticsearch 文檔的變化并不是立即對搜索可見的,這種行為可能會對新用戶造成困惑:他們索引了一個文檔然后嘗試搜索它,但卻沒有搜到。這個問題的解決辦法是用refresh API執行一次手動刷新:/usersl_refresh
盡管刷新是比提交(寫入磁盤)輕量很多的操作,它還是會有性能開銷。當寫測試的時候,手動刷新很有用,但是不要在生產環境下每次索引一個文檔都去手動刷新。相反,你的應用需要意識到Elasticsearch 的近實時的性質,并接受它的不足。
甚至并不是所有的情況都需要每秒刷新,比如可能你正在使用Elasticsearch索引大量的日志文件,你可能想優化索引速度而不是近實時搜索,可以通過設置refresh_interval ,降低每個索引的刷新頻率,如下:
{
"settings": {
"refresh_interval": "30s"
}
}
refresh_interval 可以在既存索引上進行動態更新。在生產環境中,當你正在建立一個大的新索引時,可以先關閉自動刷新,待開始使用該索引時,再把它們調回來。
# 關閉自動刷新
PUT /users/_settings
{ "refresh_interval": -1 }
# 每一秒刷新
PUT /users/_settings
{ "refresh_interval": "1s" }
注意:實際情況需要結合自己的業務場景設置refresh頻率值。
2.2.2、flush 操作(寫入磁盤)
從filesystem cache(文件緩存)寫入磁盤的過程就是flush。
每30分鐘或當 translog 達到一定大小(由index.translog.flush_threshold_size控制,默認512mb),ES會觸發一次 flush 操作。在 flush 過程中,ES 會先執行 refresh 操作將 buffer 中的數據生成 segment,內存中的緩沖將被清除,內容被寫入一個新段,段的 fsync 將創建一個新的提交點,并將內容刷新到磁盤,舊的 translog 將被刪除并開始一個新的 translog,此時lucene中的數據就完成了持久化。
如果沒有用 fsync 把數據從文件系統緩存刷(flush)到硬盤,我們不能保證數據在斷電甚至是程序正常退出之后依然存在。為了保證Elasticsearch 的可靠性,需要確保數據變化被持久化到磁盤。在動態更新索引,我們說一次完整的提交會將段刷到磁盤,并寫入一個包含所有段列表的提交點(commit point),Elasticsearch 在啟動或重新打開一個索引的過程中使用這個提交點來判斷哪些段隸屬于當前分片。
你很少需要自己手動執行flush操作,通常情況下,自動刷新就足夠了。當Elasticsearch嘗試恢復或重新打開一個索引,它需要重放 translog 中所有的操作,所以如果日志越短,恢復越快,所以我們可以在重啟節點或關閉索引之前執行 flush。
2.2.3、merge 操作(合并分段)
由于自動刷新流程每秒會創建一個新的段 ,這樣會導致短時間內的段數量暴增。而段數量太多會帶來較大的麻煩,每一個段都會消耗文件句柄、內存和cpu運行周期,更重要的是,每個搜索請求都必須輪流檢查每個段,所以段越多,搜索也就越慢。Elasticsearch 會運行一個任務檢測當前磁盤中的segment,對符合條件的segment進行合并操作,小的段被合并到大的段,然后這些大的段再被合并到更大的段。
不僅如此,merge 過程也是舊的doc真正被刪除的時候,段合并的時候會將那些舊的已刪除文檔從文件系統中清除,被刪除的文檔(或被更新文檔的舊版本)不會被拷貝到新的大段中。
用戶還可以手動調用 _forcemerge API 來主動觸發merge,以減少集群的segment個數和清理已刪除或更新的文檔。
- 刷新(refresh)操作會創建新的段并將段打開以供搜索使用。
- 合并進程選擇一小部分大小相似的段,并且在后臺將它們合并到更大的段中,這并不會中斷索引和搜索。
- 新的段被打開用來搜索,老的段會被刪除


2.2.4、translog文件
當一個文檔寫入Lucence后是存儲在內存中的,即使執行了refresh操作仍然是在文件系統緩存中,如果此時服務器宕機,那么這部分數據將會丟失。為此ES增加了translog, 當進行文檔寫操作時會先將文檔寫入Lucene,然后寫入一份到translog,寫入translog后會落盤的,這樣就可以防止服務器宕機后數據的丟失。重新啟動ES時,Elasticsearch 會將所有未刷新的操作從 Translog 重播到 Lucene 索引,以使其恢復到重新啟動前的狀態。translog 的目的是保證操作不會丟失,在文件被 fsync 到磁盤前,被寫入的文件在重啟之后就會丟失。
translog 提供所有還沒有被刷到磁盤的操作的一個持久化紀錄。當Elasticsearch啟動的時候,它會從磁盤中使用最后一個提交點去恢復己知的段,并且會重放 translog 中所有在最后一次提交后發生的變更操作。
translog 也被用來提供實時CRUD。當你試著通過ID查詢、更新、刪除一個文檔,它會在嘗試從相應的段中檢索之前,首先檢查 translog任何最近的變更,這意味著它總是能夠實時地獲取到文檔的最新版本。
參考:https://blog.csdn.net/Mrerlou/article/details/129124784
3、分析器(字符過濾器、分詞器、Token 過濾器)
分析包含下面的過程:
- 將一塊文本分成適合于倒排索引的獨立的詞條。
- 將這些詞條統一化為標準格式以提高它們的“可搜索性”,或者recall。
分析器執行上面的工作。分析器實際上是將三個功能封裝到了一個包里:
- 字符過濾器:首先,字符串按順序通過每個字符過濾器,他們的任務是在分詞前整理字符串。一個字符過濾器可以用來去掉 HTML,或者將 & 轉化成 and。
- 分詞器:其次,字符串被分詞器分為單個的詞條。一個簡單的分詞器遇到空格和標點的時候,可能會將文本拆分成詞條。
- Token 過濾器:最后,詞條按順序通過每個 token 過濾器 。這個過程可能會改變詞條(例如,小寫化Quick ),刪除詞條(例如, 像 a, and, the 等無用詞),或者增加詞條(例如,像jump和leap這種同義詞)
3.1、ES的內置分析器
Elasticsearch 附帶了可以直接使用的預包裝的分析器。
下面會列出最重要的分析器,為了證明它們的差異,我們看看每個分析器會從下面的字符串得到哪些詞條:
"Set the shape to semi-transparent by calling set_trans(5)"
3.1.1、標準分析器(默認)
標準分析器是Elasticsearch 默認使用的分析器,它是分析各種語言文本最常用的選擇。它根據Unicode 聯盟定義的單詞邊界劃分文本,刪除絕大部分標點,最后,將詞條小寫。上面字符串將產生以下詞條:
set, the, shape, to, semi, transparent, by, calling, set_trans, 5
3.1.2、簡單分析器
簡單分析器在任何不是字母的地方分隔文本,將詞條小寫。它會產生:
set, the, shape, to, semi, transparent, by, calling, set, trans
3.1.3、空格分析器
空格分析器在空格的地方劃分文本。它會產生:
Set, the, shape, to, semi-transparent, by, calling, set_trans(5)
3.1.4、語言分析器
特定語言分析器可用于很多語言,它們可以考慮指定語言的特點。例如,英語分析器附帶了一組英語無用詞(常用單詞,例如and或者the ,它們對相關性沒有多少影響),它們會被刪除。由于理解英語語法的規則,這個分詞器可以提取英語單詞的詞干。
上面字符串使用英語分詞器會產生下面的詞條:
set, shape, semi, transpar, call, set_tran, 5
注意看transparent、calling和 set_trans已經變為詞根格式。
3.2、分析器使用場景
當我們索引一個文檔,它的全文域被分析成詞條以用來創建倒排索引。但是,當我們在全文域搜索的時候,我們需要將查詢字符串通過相同的分析過程,以保證我們搜索的詞條格式與索引中的詞條格式一致。
- 當你查詢一個全文域時,會對查詢字符串應用相同的分析器,以產生正確的搜索詞條列表。
- 當你查詢一個精確值域時,不會分析查詢字符串,而是搜索你指定的精確值。
3.3、演示分析器的效果
有些時候很難理解分詞的過程和實際被存儲到索引中的詞條,為了理解發生了什么,你可以使用analyze API來看文本是如何被分析的,在消息體里,指定分析器和要分析的文本。
#GET http://localhost:9200/_analyze { "analyzer": "standard", "text": "Text to analyze" }
結果如下,每個元素代表一個單獨的詞條:

字段說明:
- token是實際存儲到索引中的詞條。
- start_ offset 和 end_ offset指明字符在原始字符串中的位置。
- position 指明詞條在原始文本中出現的位置。
3.4、指定分析器
當Elasticsearch在你的文檔中檢測到一個新的字符串域,它會自動設置其為一個全文字符串域,使用標準分析器對它進行分析。你不希望總是這樣,可能你想使用一個不同的分析器,適用于你的數據使用的語言。有時候你想要一個字符串域就是一個字符串域,不使用分析,直接索引你傳入的精確值,例如用戶 ID 或者一個內部的狀態域或標簽。要做到這一點,我們必須手動指定這些域的映射。
3.5、IK分詞器
ES 的默認分詞器無法識別中文中測試、 單詞這樣的詞匯,而是簡單的將每個字拆完分為一個詞。
下面通過 Postman 發送 GET 請求查詢默認分詞效果
# GET http://localhost:9200/_analyze { "text":"測試單詞" }
結果如下:

這樣的結果顯然不符合我們的使用要求,所以我們需要下載 ES 對應版本的中文分詞器。
3.5.1、中文分詞器
IK 中文分詞器下載網址:https://github.com/infinilabs/analysis-ik/releases/tag/v7.8.0,將解壓后的后的文件夾放入 ES 根目錄下的 plugins 目錄下,重啟 ES 即可使用。


加入新的查詢參數"analyzer":“ik_max_word”:
# GET http://localhost:9200/_analyze { "text":"測試單詞", "analyzer":"ik_max_word" }
- ik_max_word:會將文本做最大(即最細)粒度的拆分。
- ik_smart:會將文本做最小(即最粗)粒度的拆分。
結果如下:

ES 中也可以進行擴展詞匯,首先默認查詢如下:
#GET http://localhost:9200/_analyze { "text":"弗雷爾卓德", "analyzer":"ik_max_word" }
結果如下:

上面僅僅可以得到每個字的分詞結果,我們需要做的就是使分詞器識別到弗雷爾卓德也是一個詞語。
- 首先進入 ES 根目錄中的 plugins 文件夾下的 ik 文件夾,進入 config 目錄,創建 custom.dic文件,直接寫入“弗雷爾卓德”。
-
- 然后打開 IKAnalyzer.cfg.xml 文件,將新建的 custom.dic 配置其中。
-
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd"> <properties> <comment>IK Analyzer 擴展配置</comment>
<!--用戶可以在這里配置自己的擴展字典 --> <entry key="ext_dict">custom.dic</entry>
<!--用戶可以在這里配置自己的擴展停止詞字典--> <entry key="ext_stopwords"></entry> <!--用戶可以在這里配置遠程擴展字典 --> <!-- <entry key="remote_ext_dict">words_location</entry> --> <!--用戶可以在這里配置遠程擴展停止詞字典--> <!-- <entry key="remote_ext_stopwords">words_location</entry> --> </properties>
-
- 重啟 ES 服務器 。
擴展后發起相同接口,結果如下:

3.6、自定義分析器
雖然Elasticsearch帶有一些現成的分析器,然而在分析器上Elasticsearch真正的強大之處在于,你可以通過在一個適合你的特定數據的設置之中組合字符過濾器、分詞器、詞匯單元過濾器來創建自定義的分析器。一個分析器就是在一個包里面組合了三種函數的一個包裝器,三種函數按照順序被執行:
- 字符過濾器
字符過濾器用來整理一個尚未被分詞的字符串。例如,如果我們的文本是HTML格式的,它會包含像<p>或者<div>這樣的HTML標簽,這些標簽是我們不想索引的。我們可以使用html清除字符過濾器來移除掉所有的HTML標簽,并且像把Á轉換為相對應的Unicode字符á 這樣,轉換HTML實體。一個分析器可能有0個或者多個字符過濾器。
- 分詞器
一個分析器必須有一個唯一的分詞器。分詞器把字符串分解成單個詞條或者詞匯單元。標準分析器里使用的標準分詞器把一個字符串根據單詞邊界分解成單個詞條,并且移除掉大部分的標點符號,然而還有其他不同行為的分詞器存在。
例如,關鍵詞分詞器完整地輸出接收到的同樣的字符串,并不做任何分詞。空格分詞器只根據空格分割文本。正則分詞器根據匹配正則表達式來分割文本。
- 詞單元過濾器
經過分詞,作為結果的詞單元流會按照指定的順序通過指定的詞單元過濾器。詞單元過濾器可以修改、添加或者移除詞單元。我們已經提到過lowercase和stop詞過濾器,但是在Elasticsearch 里面還有很多可供選擇的詞單元過濾器。詞干過濾器把單詞遏制為詞干。ascii_folding過濾器移除變音符,把一個像"très”這樣的詞轉換為“tres”。
ngram和 edge_ngram詞單元過濾器可以產生適合用于部分匹配或者自動補全的詞單元。
3.6.1、自定義分析器例子
下面演示如何創建自定義的分析器:
#PUT http://localhost:9200/my_index { "settings": { "analysis": { "char_filter": { "&_to_and": { "type": "mapping", "mappings": [ "&=> and " ] } }, "filter": { "my_stopwords": { "type": "stop", "stopwords": [ "the", "a" ] } }, "analyzer": { "my_analyzer": { "type": "custom", "char_filter": [ "html_strip", "&_to_and" ], "tokenizer": "standard", "filter": [ "lowercase", "my_stopwords" ] } } } } }
索引被創建以后,使用 analyze API 來 測試這個新的分析器:
# GET http://127.0.0.1:9200/my_index/_analyze { "text":"The quick & brown fox", "analyzer": "my_analyzer" }
結果如下:

4、處理并發沖突
4.1、文檔沖突介紹
當我們使用index API更新文檔,可以一次性讀取原始文檔做我們的修改,然后重新索引整個文檔。最早的索引請求將獲勝,如果其他人同時更改這個文檔,他們的更改將丟失。
很多時候這是沒有問題的,很多場景下,我們都是將主數據(關系型數據庫)復制到Elasticsearch中并使其可被搜索,也許兩個人同時更改相同的文檔的幾率很小,或者對于我們的業務來說偶爾丟失更改并不是很嚴重的問題,但有時丟失了一個變更就是非常嚴重的。比如我們使用Elasticsearch 存儲我們網上商城商品庫存的數量,每次我們賣一個商品的時候,我們在 Elasticsearch 中將庫存數量減少。有一天,管理層決定做一次促銷,一秒賣好幾個商品,假設有兩個web程序并行運行,每一個都同時處理所有商品的銷售數據。

web_1 對stock_count所做的更改會丟失,因為 web_2不知道它的 stock_count 的拷貝實際已經過期,結果就會出現賣出的商品數超過實際庫存。
變更越頻繁,讀數據和更新數據的間隙越長,也就越可能丟失變更。在數據庫領域中,有兩種方法通常被用來確保并發更新時變更不會丟失:
- 悲觀并發控制:這種方法被關系型數據庫廣泛使用,它假定有變更沖突可能發生,因此阻塞訪問資源以防止沖突。一個典型的例子是讀取一行數據之前先將其鎖住,確保只有放置鎖的線程能夠對這行數據進行修改。
- 樂觀并發控制:Elasticsearch 中使用的這種方法假定沖突是不可能發生的,并且不會阻塞正在嘗試的操作。然而,如果源數據在讀寫當中被修改,更新將會失敗。應用程序接下來將決定該如何解決沖突。例如,可以重試更新、使用新的數據、或者將相關情況報告給用戶。
(正常更新文檔其實并不需要指定版本號,但在并發時就可能會發生更新請求順序混亂的問題,下面一系列措施在更新時指定版本號就是為了解決這些問題)
4.2、樂觀并發控制
Elasticsearch是分布式的。當文檔創建、更新或刪除時,新版本的文檔必須復制到集群中的其他節點。Elasticsearch也是異步和并發的,這意味著將有很多復制請求可能會被并行發送,并且到達目的地時也許順序是亂的。Elasticsearch需要一種方法確保文檔的舊版本不會覆蓋新的版本。
當我們之前討論 index , GET和DELETE請求時,我們指出每個文檔都有一個_version(版本號),當文檔被修改時版本號遞增。Elasticsearch使用這個version號來確保變更以正確順序得到執行。如果舊版本的文檔在新版本之后到達,它可以被簡單的忽略。
我們可以利用 version 號來確保應用中相互沖突的變更不會導致數據丟失。我們也可以通過指定想要修改文檔的 version 號來達到這個目的,如果該版本不是當前版本號,我們的請求將會失敗。
(老的版本ES使用version,但是新版本不支持了,會報下面的錯誤,提示我們用if_seq _no和if _primary_term)
創建文檔:
#PUT http://127.0.0.1:9200/shopping/_create/1007
結果如下:

更新文檔數據如下:
#POST http://127.0.0.1:9200/shopping/_update/1007 { "doc":{ "title":"華為手機" } }
結果如下:

舊版本使用的防止沖突更新方法:
#POST http://127.0.0.1:9200/shopping/_update/1007?version=2 { "doc":{ "title":"華為手機2" } }
在新版 ES 中可能會報錯,結果如下:

新版本使用的防止沖突更新方法:
#POST http://127.0.0.1:9200/shopping/_update/1007?if_seq_no=43&if_primary_term=13 { "doc":{ "title":"華為手機2" } }
結果如下:

4.3、外部系統版本控制(指定編號作為文檔最新版本號)
一個常見的設置是使用其它數據庫作為主要的數據存儲,使用Elasticsearch做數據檢索,這意味著主數據庫的所有更改發生時都需要被復制到 Elasticsearch,如果多個進程負責這一數據同步,你可能遇到類似于上述描述的并發問題。
如果你的主數據庫已經有了版本號,或一個能作為版本號的字段值比如用 timestamp 作為版本號,那么你就可以在 Elasticsearch 中通過 version_type=extermal 參數來使用指定的外部版本號(如 timestamp)作為文檔的最新版本號。注意,版本號必須是大于零的整數,且小于9.2E+18,一個Java中 long類型的正值。
外部版本號的處理方式和我們之前討論的內部版本號的處理方式有些不同,Elasticsearch 不是檢查當前 _version 和請求中指定的版本號是否相同,而是檢查當前_version是否小于指定的版本號。如果請求成功,請求中指定的外部的版本號將作為文檔的新_version進行存儲。
#POST http://127.0.0.1:9200/shopping/_doc/1007?version=999&version_type=external { "title":"華為手機3" }
結果如下:

5、文檔更新、刪除原理
刪除和更新也都是寫操作,但是 Elasticsearch 中的文檔是不可變的,因此不能被刪除或者改動以展示其變更;
磁盤上的每個段都有一個相應的.del 文件。當刪除請求發送后,文檔并沒有真的被刪除,而是在.del文件中被標記為刪除。該文檔依然能匹配查詢,但是會在結果中被過濾掉。當段合并時,在.del 文件中被標記為刪除的文檔將不會被寫入新段。
在新的文檔被創建時,Elasticsearch 會為該文檔指定一個版本號,當執行更新時,舊版本的文檔在.del文件中被標記為刪除,新版本的文檔被索引到一個新段。舊版本的文檔依然能匹配查詢,但是會在結果中被過濾掉。
6、搜索的原理
搜索過程分為兩階段,我們稱之為 Query Then Fetch。
- 在初始查詢階段時,查詢會廣播到索引中每一個分片拷貝(主分片或者副本分片)。 每個分片在本地執行搜索并構建一個匹配文檔且大小為 from + size 的優先隊列。PS:在搜索的時候除了磁盤也會查詢Filesystem Cache 的,但是有部分數據還在 Memory Buffer,所以搜索是近實時的。每個分片返回各自優先隊列中所有文檔的 ID 和排序值給協調節點,它合并這些值到自己的優先隊列中來產生一個全局排序后的結果列表。
- 接下來就是取回階段,協調節點辨別出哪些文檔需要被取回并向相關的分片提交多個 GET 請求。每個分片加載并豐富文檔,如果有需要的話,接著返回文檔給協調節點。一旦所有的文檔都被取回了,協調節點返回結果給客戶端。
Query Then Fetch 的搜索類型在文檔相關性打分的時候參考的是本分片的數據,這樣在文檔數量較少的時候可能不夠準確,DFS Query Then Fetch 增加了一個預查詢的處理,詢問 Term 和 Document frequency,這個評分更準確,但是性能會變差。
7、在并發情況下,Elasticsearch 如果保證讀寫一致
可以通過版本號使用樂觀并發控制,以確保新版本不會被舊版本覆蓋,由應用層來處理具體的沖突;
另外對于寫操作,一致性級別支持 quorum/one/all,默認為 quorum,即只有當大多數分片可用時才允許寫操作。但即使大多數可用,也可能存在因為網絡等原因導致寫入副本失敗,這樣該副本被認為故障,分片將會在一個不同的節點上重建。
對于讀操作,可以設置 replication 為 sync(默認),這使得操作在主分片和副本分片都完成后才會返回;如果設置 replication 為 async 時,也可以通過設置搜索請求參數_preference 為 primary 來查詢主分片,確保文檔是最新版本。


浙公網安備 33010602011771號