倒排索引、正排索引,以及ElasticSearch對倒排索引的優化

正排索引與倒排索引

首先,我們需要這兩種索引方式是要幹啥?其實任何一種索引模式,都對應的是不同的信息存儲方式。這樣不同的存儲方式,主要是爲了不同的查詢要求而定的。正排索引和倒排索引就是如此,正排易維護,但搜索代價很大(耗時間);倒排搜索快,但建立倒排索引時間久、文檔庫的每次更新都意味着倒排索引的重建,故維護較麻煩。但由於倒排索引的建立可以放在線下,所以這一般來說不是太大的問題。

正排索引

正排索引就是最普通的索引排序方式。正排索引也是採取key-value pair的方式對數據進行保存,key是doc-id,value則可以存儲多種內容,如doc的分詞詞表、doc所在網頁的屬性信息等。由此可見,正排索引可以隨意添加數據,但如果你要查詢某個單詞在哪些文檔中出現,那麼你就不得不將全部文檔都遍歷一遍,若文檔庫極大,則時間消耗是不可接受的。

倒排索引

倒排索引是LuceneElasticSearch用來做全文檢索的標配。倒排索引類似將正排索引反過來,以全部文檔中出現的所有words建立一個term dictionary ,然後對於term dictionary 中的每個詞,它後面都會跟隨一個鏈表,該鏈表就是 倒排表倒排表內存儲着如下信息:

  • 該詞出現的doc-id
  • 該詞在某doc中的出現次數和出現位置

如此一來,倒排索引就可以在用戶輸入查詢query時,將query分詞成一個個token,然後將一個個token帶到term dictionary 去“查字典”,然後獲得出現該詞的doc-id集合,最終在這若干個token對應的若干doc-id集合上做交集,得到最符合用戶query查詢的結果。

由此可見,倒排索引大大加快了查詢速度,然而一旦有新的文檔加入文檔庫,你就要重新做一次"入庫操作",即建立倒排索引的操作。因爲此時各個token在不同文檔中的出現次數、以及每個token對應的 倒排表 都可能發生變化。

倒排索引的進階——倒排索引如何進一步加速?

從上面的介紹可知,倒排索引的建立這麼麻煩,其主要目的就是加速。所以除了索引建立策略上,我們在數據結構和查找策略方面也要最大程度節省時間。那麼倒排索引還做了哪些速度性能優化呢?

term dictionary 基礎上再建 term index

在這裏插入圖片描述
從左到右,分別是term-index、term-dictionary、posting list(倒排表)

我們可以將整個ElasticSearch的查找過程當做“查字典”過程。對於大型ES庫而言,當你的文檔數目很多的時候,你的目錄必然也會很長(畢竟字典裏的term變多了嘛)。所以,此時你就只能將term dictionary 放在磁盤上。即便我們對term dictionary 中term的順序進行有規則排序,從而在查詢時可以執行 二分查找,我們也還是會在搜索時多次訪問磁盤,從而因磁盤IO限制降低了查詢速度。
爲了解決這個問題,ES/Lucene引入了 term index,其本質就是個字典樹。至於字典樹是個啥(https://zh.wikipedia.org/wiki/Trie)。字典樹記錄了term的前綴信息,例如以下words組成的字典:

sand, sad, say, said, seed, search, sorry, sore, shabby, sherry, shadowsock, score, scholar…

我們可以建立字典樹,分別記錄由前綴 sasesoshsc 組成的詞在 term dictionary 中開始的位置,從而一下子大幅降低我們查詞典時的搜索空間、減少訪問磁盤的次數(其實新華字典不也是這麼幹的麼~)。與此同時,我們還可以將字典樹term index 存在內存上。所以說,ElasticSearch的優化,有時候就是字典樹term index 佔內存大小和搜索速度這兩方面的權衡——字典樹越大,你可以定位到的前綴信息越多(比如從只定位 sa -> sadsansai…),你在磁盤的term dictionary中做二分查找的次數肯定也會更少,速度就越快;但內存空間是有限的,你不可能無限擴張你的字典樹。

倒排表求交集的加速

設想一個用戶輸入這樣一個query:Boston Celtics Calendar ,即波士頓凱爾特人隊賽程表。我們將query分成三個term -> Boston , Celtics , Calendar . 我們對這三個詞到倒排索引中進行查詢,最終得到三個倒排表。假設倒排表是這樣的(極簡版,實際上倒排表內容會更多)

Boston -> [12, 13, 24, 28, 40, 42, 46, 51, 56, 59]
Celtics -> [2, 12, 13, 40, 43, 44, 46, 58, 59]
Calendar -> [0, 1, 12, 15, 20, 40, 46, 51, 55, 59, 60, 62, 65]

以上交集結果:[12, 40, 46, 59]

首先,對於此類有序數組,做交集有個很強大的數據結構——跳錶(skip list)。這個數據結構很簡單也很精巧,在這兒不再詳述。
其次,假設全部文檔真的都是這種integer格式的id,那麼對內存的考驗極大。一個int類型至少需要2 byte,50M個doc-id就是100M的內存佔用,所以說顯然在doc數量較大的情況下integer類型的doc-id有其侷限性。

那麼如果不用 integer+跳錶 的方式來做交集,還可以用啥方法呢?使用Bitmap的方法:
對於一個doc-id list [1,2,5,7],我們可以轉爲[1, 1, 0, 0, 1, 0, 1, 0],另一個list[1,2,8] -> [1, 1, 0, 0, 0, 0, 0, 1],顯而易見,這其實就是做了個類似“one-hot”的操作,將doc-id對應的位置置爲0,若存在於倒排表裏,就設爲1。這樣做更節省內存佔用,因爲每個doc-id是非0即1的,只佔1 bit,即1/8 byte. 50M的doc-id,這樣只需要 50M*1/8 byte = 6.25M的內存佔用,是不是節省了一大筆開銷?而且位運算速度可是計算機做的最快的運算方式!
但有個小問題——bitmap方法對應的每個倒排表實際上內存佔用都是一致的,都是長度爲全部doc-id這麼長的list。在面對doc-id數量較少的求交集運算時,這樣做就顯得有點冗餘。舉個例子,假設Boston, Celtics , Calendar 分別對應doc-id的集合爲[1,2]、[1,3]、[1,4],但doc-id一共有50M篇,那麼雖然參與交集運算的每個list最多隻有2 * 2 byte = 4 byte,但我還是得對每個倒排表開6.25M的內存空間,這是不是有點殺雞用牛刀呢?所以,依據doc-id總量的不同,到底是 integer+跳錶 還是 bitmap 也是可權衡的。
這就是ElasticSearch/Lucene在交集運算時的優化。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章