面試小結之Elasticsearch篇


Elasticsearch-基礎介紹及索引原理分析

最近在參與一個基於Elasticsearch作為底層數據框架提供大數據量(億級)的實時統計查詢的方案設計工作,花了些時間學習Elasticsearch的基礎理論知識,整理了一下,希望能對Elasticsearch感興趣/想了解的同學有所幫助。 同時也希望有發現內容不正確或者有疑問的地方,望指明,一起探討,學習,進步。

介紹

Elasticsearch 是一個分布式可擴展的實時搜索和分析引擎,一個建立在全文搜索引擎 Apache Lucene(TM) 基礎上的搜索引擎.當然 Elasticsearch 並不僅僅是 Lucene 那么簡單,它不僅包括了全文搜索功能,還可以進行以下工作:

  • 分布式實時文件存儲,並將每一個字段都編入索引,使其可以被搜索。
  • 實時分析的分布式搜索引擎。
  • 可以擴展到上百台服務器,處理PB級別的結構化或非結構化數據。

基本概念

先說Elasticsearch的文件存儲,Elasticsearch是面向文檔型數據庫,一條數據在這里就是一個文檔,用JSON作為文檔序列化的格式,比如下面這條用戶數據:

{
    "name" : "John", "sex" : "Male", "age" : 25, "birthDate": "1990/05/01", "about" : "I love to go rock climbing", "interests": [ "sports", "music" ] } 

用Mysql這樣的數據庫存儲就會容易想到建立一張User表,有balabala的字段等,在Elasticsearch里這就是一個文檔,當然這個文檔會屬於一個User的類型,各種各樣的類型存在於一個索引當中。這里有一份簡易的將Elasticsearch和關系型數據術語對照表:

關系數據庫     ⇒ 數據庫 ⇒ 表    ⇒ 行    ⇒ 列(Columns)

Elasticsearch  ⇒ 索引(Index)   ⇒ 類型(type) ⇒ 文檔(Docments) ⇒ 字段(Fields) 

一個 Elasticsearch 集群可以包含多個索引(數據庫),也就是說其中包含了很多類型(表)。這些類型中包含了很多的文檔(行),然后每個文檔中又包含了很多的字段(列)。Elasticsearch的交互,可以使用Java API,也可以直接使用HTTP的Restful API方式,比如我們打算插入一條記錄,可以簡單發送一個HTTP的請求:

PUT /megacorp/employee/1 { "name" : "John", "sex" : "Male", "age" : 25, "about" : "I love to go rock climbing", "interests": [ "sports", "music" ] } 

更新,查詢也是類似這樣的操作,具體操作手冊可以參見Elasticsearch權威指南


索引

Elasticsearch最關鍵的就是提供強大的索引能力了,其實InfoQ的這篇時間序列數據庫的秘密(2)——索引寫的非常好,我這里也是圍繞這篇結合自己的理解進一步梳理下,也希望可以幫助大家更好的理解這篇文章。

Elasticsearch索引的精髓:

一切設計都是為了提高搜索的性能

另一層意思:為了提高搜索的性能,難免會犧牲某些其他方面,比如插入/更新,否則其他數據庫不用混了。前面看到往Elasticsearch里插入一條記錄,其實就是直接PUT一個json的對象,這個對象有多個fields,比如上面例子中的name, sex, age, about, interests,那么在插入這些數據到Elasticsearch的同時,Elasticsearch還默默1的為這些字段建立索引--倒排索引,因為Elasticsearch最核心功能是搜索。

Elasticsearch是如何做到快速索引的

InfoQ那篇文章里說Elasticsearch使用的倒排索引比關系型數據庫的B-Tree索引快,為什么呢?

什么是B-Tree索引?

上大學讀書時老師教過我們,二叉樹查找效率是logN,同時插入新的節點不必移動全部節點,所以用樹型結構存儲索引,能同時兼顧插入和查詢的性能。因此在這個基礎上,再結合磁盤的讀取特性(順序讀/隨機讀),傳統關系型數據庫采用了B-Tree/B+Tree這樣的數據結構:

Alt text

為了提高查詢的效率,減少磁盤尋道次數,將多個值作為一個數組通過連續區間存放,一次尋道讀取多個數據,同時也降低樹的高度。

什么是倒排索引?

Alt text

繼續上面的例子,假設有這么幾條數據(為了簡單,去掉about, interests這兩個field):

| ID | Name | Age | Sex | | -- |:------------:| -----:| -----:| | 1 | Kate | 24 | Female | 2 | John | 24 | Male | 3 | Bill | 29 | Male 

ID是Elasticsearch自建的文檔id,那么Elasticsearch建立的索引如下:

Name:

| Term | Posting List | | -- |:----:| | Kate | 1 | | John | 2 | | Bill | 3 | 

Age:

| Term | Posting List | | -- |:----:| | 24 | [1,2] | | 29 | 3 | 

Sex:

| Term | Posting List | | -- |:----:| | Female | 1 | | Male | [2,3] | 
Posting List

Elasticsearch分別為每個field都建立了一個倒排索引,Kate, John, 24, Female這些叫term,而[1,2]就是Posting List。Posting list就是一個int的數組,存儲了所有符合某個term的文檔id。

看到這里,不要認為就結束了,精彩的部分才剛開始...

通過posting list這種索引方式似乎可以很快進行查找,比如要找age=24的同學,愛回答問題的小明馬上就舉手回答:我知道,id是1,2的同學。但是,如果這里有上千萬的記錄呢?如果是想通過name來查找呢?

Term Dictionary

Elasticsearch為了能快速找到某個term,將所有的term排個序,二分法查找term,logN的查找效率,就像通過字典查找一樣,這就是Term Dictionary。現在再看起來,似乎和傳統數據庫通過B-Tree的方式類似啊,為什么說比B-Tree的查詢快呢?

Term Index

B-Tree通過減少磁盤尋道次數來提高查詢性能,Elasticsearch也是采用同樣的思路,直接通過內存查找term,不讀磁盤,但是如果term太多,term dictionary也會很大,放內存不現實,於是有了Term Index,就像字典里的索引頁一樣,A開頭的有哪些term,分別在哪頁,可以理解term index是一顆樹:

Alt text

這棵樹不會包含所有的term,它包含的是term的一些前綴。通過term index可以快速地定位到term dictionary的某個offset,然后從這個位置再往后順序查找。

Alt text

所以term index不需要存下所有的term,而僅僅是他們的一些前綴與Term Dictionary的block之間的映射關系,再結合FST(Finite State Transducers)的壓縮技術,可以使term index緩存到內存中。從term index查到對應的term dictionary的block位置之后,再去磁盤上找term,大大減少了磁盤隨機讀的次數。

這時候愛提問的小明又舉手了:"那個FST是神馬東東啊?"

一看就知道小明是一個上大學讀書的時候跟我一樣不認真聽課的孩子,數據結構老師一定講過什么是FST。但沒辦法,我也忘了,這里再補下課:

FSTs are finite-state machines that map a term (byte sequence) to an arbitrary output.

假設我們現在要將mop, moth, pop, star, stop and top(term index里的term前綴)映射到序號:0,1,2,3,4,5(term dictionary的block位置)。最簡單的做法就是定義個Map<string, integer="">,大家找到自己的位置對應入座就好了,但從內存占用少的角度想想,有沒有更優的辦法呢?答案就是:FST(理論依據在此,但我相信99%的人不會認真看完的)

Alt text

⭕️表示一種狀態

-->表示狀態的變化過程,上面的字母/數字表示狀態變化和權重

將單詞分成單個字母通過⭕️和-->表示出來,0權重不顯示。如果⭕️后面出現分支,就標記權重,最后整條路徑上的權重加起來就是這個單詞對應的序號。

FSTs are finite-state machines that map a term (byte sequence) to an arbitrary output.

FST以字節的方式存儲所有的term,這種壓縮方式可以有效的縮減存儲空間,使得term index足以放進內存,但這種方式也會導致查找時需要更多的CPU資源。

后面的更精彩,看累了的同學可以喝杯咖啡……


壓縮技巧

Elasticsearch里除了上面說到用FST壓縮term index外,對posting list也有壓縮技巧。 
小明喝完咖啡又舉手了:"posting list不是已經只存儲文檔id了嗎?還需要壓縮?"

嗯,我們再看回最開始的例子,如果Elasticsearch需要對同學的性別進行索引(這時傳統關系型數據庫已經哭暈在廁所……),會怎樣?如果有上千萬個同學,而世界上只有男/女這樣兩個性別,每個posting list都會有至少百萬個文檔id。 Elasticsearch是如何有效的對這些文檔id壓縮的呢?

Frame Of Reference

增量編碼壓縮,將大數變小數,按字節存儲

首先,Elasticsearch要求posting list是有序的(為了提高搜索的性能,再任性的要求也得滿足),這樣做的一個好處是方便壓縮,看下面這個圖例: Alt text

如果數學不是體育老師教的話,還是比較容易看出來這種壓縮技巧的。

原理就是通過增量,將原來的大數變成小數僅存儲增量值,再精打細算按bit排好隊,最后通過字節存儲,而不是大大咧咧的盡管是2也是用int(4個字節)來存儲。

Roaring bitmaps

說到Roaring bitmaps,就必須先從bitmap說起。Bitmap是一種數據結構,假設有某個posting list:

[1,3,4,7,10]

對應的bitmap就是:

[1,0,1,1,0,0,1,0,0,1]

非常直觀,用0/1表示某個值是否存在,比如10這個值就對應第10位,對應的bit值是1,這樣用一個字節就可以代表8個文檔id,舊版本(5.0之前)的Lucene就是用這樣的方式來壓縮的,但這樣的壓縮方式仍然不夠高效,如果有1億個文檔,那么需要12.5MB的存儲空間,這僅僅是對應一個索引字段(我們往往會有很多個索引字段)。於是有人想出了Roaring bitmaps這樣更高效的數據結構。

Bitmap的缺點是存儲空間隨着文檔個數線性增長,Roaring bitmaps需要打破這個魔咒就一定要用到某些指數特性:

將posting list按照65535為界限分塊,比如第一塊所包含的文檔id范圍在0~65535之間,第二塊的id范圍是65536~131071,以此類推。再用<商,余數>的組合表示每一組id,這樣每組里的id范圍都在0~65535內了,剩下的就好辦了,既然每組id不會變得無限大,那么我們就可以通過最有效的方式對這里的id存儲。

Alt text

細心的小明這時候又舉手了:"為什么是以65535為界限?"

程序員的世界里除了1024外,65535也是一個經典值,因為它=2^16-1,正好是用2個字節能表示的最大數,一個short的存儲單位,注意到上圖里的最后一行“If a block has more than 4096 values, encode as a bit set, and otherwise as a simple array using 2 bytes per value”,如果是大塊,用節省點用bitset存,小塊就豪爽點,2個字節我也不計較了,用一個short[]存着方便。

那為什么用4096來區分大塊還是小塊呢?

個人理解:都說程序員的世界是二進制的,4096*2bytes = 8192bytes < 1KB, 磁盤一次尋道可以順序把一個小塊的內容都讀出來,再大一位就超過1KB了,需要兩次讀。


聯合索引

上面說了半天都是單field索引,如果多個field索引的聯合查詢,倒排索引如何滿足快速查詢的要求呢?

  • 利用跳表(Skip list)的數據結構快速做“與”運算,或者
  • 利用上面提到的bitset按位“與”

先看看跳表的數據結構:

Alt text

將一個有序鏈表level0,挑出其中幾個元素到level1及level2,每個level越往上,選出來的指針元素越少,查找時依次從高level往低查找,比如55,先找到level2的31,再找到level1的47,最后找到55,一共3次查找,查找效率和2叉樹的效率相當,但也是用了一定的空間冗余來換取的。

假設有下面三個posting list需要聯合索引:

Alt text

如果使用跳表,對最短的posting list中的每個id,逐個在另外兩個posting list中查找看是否存在,最后得到交集的結果。

如果使用bitset,就很直觀了,直接按位與,得到的結果就是最后的交集。


總結和思考

Elasticsearch的索引思路:

將磁盤里的東西盡量搬進內存,減少磁盤隨機讀取次數(同時也利用磁盤順序讀特性),結合各種奇技淫巧的壓縮算法,用及其苛刻的態度使用內存。

所以,對於使用Elasticsearch進行索引時需要注意:

  • 不需要索引的字段,一定要明確定義出來,因為默認是自動建索引的
  • 同樣的道理,對於String類型的字段,不需要analysis的也需要明確定義出來,因為默認也是會analysis的
  • 選擇有規律的ID很重要,隨機性太大的ID(比如java的UUID)不利於查詢

關於最后一點,個人認為有多個因素:

其中一個(也許不是最重要的)因素: 上面看到的壓縮算法,都是對Posting list里的大量ID進行壓縮的,那如果ID是順序的,或者是有公共前綴等具有一定規律性的ID,壓縮比會比較高;

另外一個因素: 可能是最影響查詢性能的,應該是最后通過Posting list里的ID到磁盤中查找Document信息的那步,因為Elasticsearch是分Segment存儲的,根據ID這個大范圍的Term定位到Segment的效率直接影響了最后查詢的性能,如果ID是有規律的,可以快速跳過不包含該ID的Segment,從而減少不必要的磁盤讀次數,具體可以參考這篇如何選擇一個高效的全局ID方案(評論也很精彩)

 


Elasticsearch是如何實現Master選舉的?

  • Elasticsearch的選主是ZenDiscovery模塊負責的,主要包含Ping(節點之間通過這個RPC來發現彼此)和Unicast(單播模塊包含一個主機列表以控制哪些節點需要ping通)這兩部分;
  • 對所有可以成為master的節點(node.master: true)根據nodeId字典排序,每次選舉每個節點都把自己所知道節點排一次序,然后選出第一個(第0位)節點,暫且認為它是master節點。
  • 如果對某個節點的投票數達到一定的值(可以成為master節點數n/2+1)並且該節點自己也選舉自己,那這個節點就是master。否則重新選舉一直到滿足上述條件。
  • 補充:master節點的職責主要包括集群、節點和索引的管理,不負責文檔級別的管理;data節點可以關閉http功能

Elasticsearch中的節點(比如共20個),其中的10個選了一個master,另外10個選了另一個master,怎么辦?

  • 當集群master候選數量不小於3個時,可以通過設置最少投票通過數量(discovery.zen.minimum_master_nodes)超過所有候選節點一半以上來解決腦裂問題;
  • 當候選數量為兩個時,只能修改為唯一的一個master候選,其他作為data節點,避免腦裂問題。

客戶端在和集群連接時,如何選擇特定的節點執行請求的?

  • TransportClient利用transport模塊遠程連接一個elasticsearch集群。它並不加入到集群中,只是簡單的獲得一個或者多個初始化的transport地址,並以 輪詢 的方式與這些地址進行通信。

詳細描述一下Elasticsearch索引文檔的過程。

  • 協調節點默認使用文檔ID參與計算(也支持通過routing),以便為路由提供合適的分片。
shard = hash(document_id) % (num_of_primary_shards) 
  • 當分片所在的節點接收到來自協調節點的請求后,會將請求寫入到Memory Buffer,然后定時(默認是每隔1秒)寫入到Filesystem Cache,這個從Momery Buffer到Filesystem Cache的過程就叫做refresh;
  • 當然在某些情況下,存在Momery Buffer和Filesystem Cache的數據可能會丟失,ES是通過translog的機制來保證數據的可靠性的。其實現機制是接收到請求后,同時也會寫入到translog中,當Filesystem cache中的數據寫入到磁盤中時,才會清除掉,這個過程叫做flush;
  • 在flush過程中,內存中的緩沖將被清除,內容被寫入一個新段,段的fsync將創建一個新的提交點,並將內容刷新到磁盤,舊的translog將被刪除並開始一個新的translog。
  • flush觸發的時機是定時觸發(默認30分鍾)或者translog變得太大(默認為512M)時;

Elasticsearch索引文檔的過程

補充:關於Lucene的Segement:

  • Lucene索引是由多個段組成,段本身是一個功能齊全的倒排索引。
  • 段是不可變的,允許Lucene將新的文檔增量地添加到索引中,而不用從頭重建索引。
  • 對於每一個搜索請求而言,索引中的所有段都會被搜索,並且每個段會消耗CPU的時鍾周、文件句柄和內存。這意味着段的數量越多,搜索性能會越低。
  • 為了解決這個問題,Elasticsearch會合並小段到一個較大的段,提交新的合並段到磁盤,並刪除那些舊的小段。

詳細描述一下Elasticsearch更新和刪除文檔的過程。

  • 刪除和更新也都是寫操作,但是Elasticsearch中的文檔是不可變的,因此不能被刪除或者改動以展示其變更;
  • 磁盤上的每個段都有一個相應的.del文件。當刪除請求發送后,文檔並沒有真的被刪除,而是在.del文件中被標記為刪除。該文檔依然能匹配查詢,但是會在結果中被過濾掉。當段合並時,在.del文件中被標記為刪除的文檔將不會被寫入新段。
  • 在新的文檔被創建時,Elasticsearch會為該文檔指定一個版本號,當執行更新時,舊版本的文檔在.del文件中被標記為刪除,新版本的文檔被索引到一個新段。舊版本的文檔依然能匹配查詢,但是會在結果中被過濾掉。

詳細描述一下Elasticsearch搜索的過程。

  • 搜索被執行成一個兩階段過程,我們稱之為 Query Then Fetch;
  • 在初始查詢階段時,查詢會廣播到索引中每一個分片拷貝(主分片或者副本分片)。 每個分片在本地執行搜索並構建一個匹配文檔的大小為 from + size 的優先隊列。PS:在搜索的時候是會查詢Filesystem Cache的,但是有部分數據還在Memory Buffer,所以搜索是近實時的。
  • 每個分片返回各自優先隊列中 所有文檔的 ID 和排序值 給協調節點,它合並這些值到自己的優先隊列中來產生一個全局排序后的結果列表。
  • 接下來就是 取回階段,協調節點辨別出哪些文檔需要被取回並向相關的分片提交多個 GET 請求。每個分片加載並 豐富 文檔,如果有需要的話,接着返回文檔給協調節點。一旦所有的文檔都被取回了,協調節點返回結果給客戶端。
  • 補充:Query Then Fetch的搜索類型在文檔相關性打分的時候參考的是本分片的數據,這樣在文檔數量較少的時候可能不夠准確,DFS Query Then Fetch增加了一個預查詢的處理,詢問Term和Document frequency,這個評分更准確,但是性能會變差。

Elasticsearch執行搜索的過程

在Elasticsearch中,是怎么根據一個詞找到對應的倒排索引的?

SEE:

Elasticsearch在部署時,對Linux的設置有哪些優化方法?

  • 64 GB 內存的機器是非常理想的, 但是32 GB 和16 GB 機器也是很常見的。少於8 GB 會適得其反。
  • 如果你要在更快的 CPUs 和更多的核心之間選擇,選擇更多的核心更好。多個內核提供的額外並發遠勝過稍微快一點點的時鍾頻率。
  • 如果你負擔得起 SSD,它將遠遠超出任何旋轉介質。 基於 SSD 的節點,查詢和索引性能都有提升。如果你負擔得起,SSD 是一個好的選擇。
  • 即使數據中心們近在咫尺,也要避免集群跨越多個數據中心。絕對要避免集群跨越大的地理距離。
  • 請確保運行你應用程序的 JVM 和服務器的 JVM 是完全一樣的。 在 Elasticsearch 的幾個地方,使用 Java 的本地序列化。
  • 通過設置gateway.recover_after_nodes、gateway.expected_nodes、gateway.recover_after_time可以在集群重啟的時候避免過多的分片交換,這可能會讓數據恢復從數個小時縮短為幾秒鍾。
  • Elasticsearch 默認被配置為使用單播發現,以防止節點無意中加入集群。只有在同一台機器上運行的節點才會自動組成集群。最好使用單播代替組播。
  • 不要隨意修改垃圾回收器(CMS)和各個線程池的大小。
  • 把你的內存的(少於)一半給 Lucene(但不要超過 32 GB!),通過ES_HEAP_SIZE 環境變量設置。
  • 內存交換到磁盤對服務器性能來說是致命的。如果內存交換到磁盤上,一個 100 微秒的操作可能變成 10 毫秒。 再想想那么多 10 微秒的操作時延累加起來。 不難看出 swapping 對於性能是多么可怕。
  • Lucene 使用了大量的文件。同時,Elasticsearch 在節點和 HTTP 客戶端之間進行通信也使用了大量的套接字。 所有這一切都需要足夠的文件描述符。你應該增加你的文件描述符,設置一個很大的值,如 64,000。

補充:索引階段性能提升方法

  • 使用批量請求並調整其大小:每次批量數據 5–15 MB 大是個不錯的起始點。
  • 存儲:使用 SSD
  • 段和合並:Elasticsearch 默認值是 20 MB/s,對機械磁盤應該是個不錯的設置。如果你用的是 SSD,可以考慮提高到 100–200 MB/s。如果你在做批量導入,完全不在意搜索,你可以徹底關掉合並限流。另外還可以增加 index.translog.flush_threshold_size 設置,從默認的 512 MB 到更大一些的值,比如 1 GB,這可以在一次清空觸發的時候在事務日志里積累出更大的段。
  • 如果你的搜索結果不需要近實時的准確度,考慮把每個索引的index.refresh_interval 改到30s。
  • 如果你在做大批量導入,考慮通過設置index.number_of_replicas: 0 關閉副本。

對於GC方面,在使用Elasticsearch時要注意什么?

  • SEE:https://elasticsearch.cn/article/32
  • 倒排詞典的索引需要常駐內存,無法GC,需要監控data node上segment memory增長趨勢。
  • 各類緩存,field cache, filter cache, indexing cache, bulk queue等等,要設置合理的大小,並且要應該根據最壞的情況來看heap是否夠用,也就是各類緩存全部占滿的時候,還有heap空間可以分配給其他任務嗎?避免采用clear cache等“自欺欺人”的方式來釋放內存。
  • 避免返回大量結果集的搜索與聚合。確實需要大量拉取數據的場景,可以采用scan & scroll api來實現。
  • cluster stats駐留內存並無法水平擴展,超大規模集群可以考慮分拆成多個集群通過tribe node連接。
  • 想知道heap夠不夠,必須結合實際應用場景,並對集群的heap使用情況做持續的監控。

Elasticsearch對於大數據量(上億量級)的聚合如何實現?

  • Elasticsearch 提供的首個近似聚合是cardinality 度量。它提供一個字段的基數,即該字段的distinct或者unique值的數目。它是基於HLL算法的。HLL 會先對我們的輸入作哈希運算,然后根據哈希運算的結果中的 bits 做概率估算從而得到基數。其特點是:可配置的精度,用來控制內存的使用(更精確 = 更多內存);小的數據集精度是非常高的;我們可以通過配置參數,來設置去重需要的固定內存使用量。無論數千還是數十億的唯一值,內存使用量只與你配置的精確度相關。

在並發情況下,Elasticsearch如果保證讀寫一致?

  • 可以通過版本號使用樂觀並發控制,以確保新版本不會被舊版本覆蓋,由應用層來處理具體的沖突;
  • 另外對於寫操作,一致性級別支持quorum/one/all,默認為quorum,即只有當大多數分片可用時才允許寫操作。但即使大多數可用,也可能存在因為網絡等原因導致寫入副本失敗,這樣該副本被認為故障,分片將會在一個不同的節點上重建。
  • 對於讀操作,可以設置replication為sync(默認),這使得操作在主分片和副本分片都完成后才會返回;如果設置replication為async時,也可以通過設置搜索請求參數_preference為primary來查詢主分片,確保文檔是最新版本。

如何監控Elasticsearch集群狀態?

  • Marvel 讓你可以很簡單的通過 Kibana 監控 Elasticsearch。你可以實時查看你的集群健康狀態和性能,也可以分析過去的集群、索引和節點指標。

介紹下你們電商搜索的整體技術架構。

整體技術架構

介紹一下你們的個性化搜索方案?

SEE 基於word2vec和Elasticsearch實現個性化搜索

是否了解字典樹?

  • 常用字典數據結構如下所示:

常用字典數據結構

  • Trie的核心思想是空間換時間,利用字符串的公共前綴來降低查詢時間的開銷以達到提高效率的目的。它有3個基本性質:
根節點不包含字符,除根節點外每一個節點都只包含一個字符。
從根節點到某一節點,路徑上經過的字符連接起來,為該節點對應的字符串。
每個節點的所有子節點包含的字符都不相同。

字典樹

  • 可以看到,trie樹每一層的節點數是26^i級別的。所以為了節省空間,我們還可以用動態鏈表,或者用數組來模擬動態。而空間的花費,不會超過單詞數×單詞長度。
  • 實現:對每個結點開一個字母集大小的數組,每個結點掛一個鏈表,使用左兒子右兄弟表示法記錄這棵樹;
  • 對於中文的字典樹,每個節點的子節點用一個哈希表存儲,這樣就不用浪費太大的空間,而且查詢速度上可以保留哈希的復雜度O(1)。

拼寫糾錯是如何實現的?

  • 拼寫糾錯是基於編輯距離來實現;編輯距離是一種標准的方法,它用來表示經過插入、刪除和替換操作從一個字符串轉換到另外一個字符串的最小操作步數;
  • 編輯距離的計算過程:比如要計算batyu和beauty的編輯距離,先創建一個7×8的表(batyu長度為5,coffee長度為6,各加2),接着,在如下位置填入黑色數字。其他格的計算過程是取以下三個值的最小值:
如果最上方的字符等於最左方的字符,則為左上方的數字。否則為左上方的數字+1。(對於3,3來說為0)
左方數字+1(對於3,3格來說為2)
上方數字+1(對於3,3格來說為2)

最終取右下角的值即為編輯距離的值3。

編輯距離

  • 對於拼寫糾錯,我們考慮構造一個度量空間(Metric Space),該空間內任何關系滿足以下三條基本條件:
d(x,y) = 0 -- 假如x與y的距離為0,則x=y d(x,y) = d(y,x) -- x到y的距離等同於y到x的距離 d(x,y) + d(y,z) >= d(x,z) -- 三角不等式 
  • 根據三角不等式,則滿足與query距離在n范圍內的另一個字符轉B,其與A的距離最大為d+n,最小為d-n。
  • BK樹的構造就過程如下:每個節點有任意個子節點,每條邊有個值表示編輯距離。所有子節點到父節點的邊上標注n表示編輯距離恰好為n。比如,我們有棵樹父節點是”book”和兩個子節點”cake”和”books”,”book”到”books”的邊標號1,”book”到”cake”的邊上標號4。從字典里構造好樹后,無論何時你想插入新單詞時,計算該單詞與根節點的編輯距離,並且查找數值為d(neweord, root)的邊。遞歸得與各子節點進行比較,直到沒有子節點,你就可以創建新的子節點並將新單詞保存在那。比如,插入”boo”到剛才上述例子的樹中,我們先檢查根節點,查找d(“book”, “boo”) = 1的邊,然后檢查標號為1的邊的子節點,得到單詞”books”。我們再計算距離d(“books”, “boo”)=2,則將新單詞插在”books”之后,邊標號為2。
  • 查詢相似詞如下:計算單詞與根節點的編輯距離d,然后遞歸查找每個子節點標號為d-n到d+n(包含)的邊。假如被檢查的節點與搜索單詞的距離d小於n,則返回該節點並繼續查詢。比如輸入cape且最大容忍距離為1,則先計算和根的編輯距離d(“book”, “cape”)=4,然后接着找和根節點之間編輯距離為3到5的,這個就找到了cake這個節點,計算d(“cake”, “cape”)=1,滿足條件所以返回cake,然后再找和cake節點編輯距離是0到2的,分別找到cape和cart節點,這樣就得到cape這個滿足條件的結果。

BK樹

 

 

參考:Elasticsearch-基礎介紹及索引原理分析

 
 
 
 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM