1. 程式人生 > >為什麼ElasticSearch比MySQL更適合全文索引

為什麼ElasticSearch比MySQL更適合全文索引

熟悉 MySQL 的同學一定都知道,MySQL 對於複雜條件查詢的支援並不好。MySQL 最多使用一個條件涉及的索引來過濾,然後剩餘的條件只能在遍歷行過程中進行記憶體過濾,對這個過程不瞭解的同學可以先行閱讀一下[《MySQL複雜where條件分析》](http://remcarpediem.net/article/89b25dd5/)。 上述這種處理複雜條件查詢的方式因為只能通過一個索引進行過濾,所以需要進行大量的 I/O 操作來讀取行資料,並消耗 CPU 進行記憶體過濾,導致查詢效能的下降。 而 ElasticSearch 因其特性,十分適合進行復雜條件查詢,是業界主流的複雜條件查詢場景解決方案,廣泛應用於訂單和日誌查詢等場景。 下面我們就一起來看一下,為什麼 ElasticSearch 適合進行復雜條件查詢。 #### ElasticSearch 簡介 Elasticsearch 是開源的實時分散式搜尋分析引擎,內部使用 Lucene 做索引與搜尋。它提供"準實時搜尋"能力,並且能動態叢集規模,彈性擴容。 ![](https://img2020.cnblogs.com/blog/1816118/202102/1816118-20210220214651082-1049722695.png) Elasticsearch 使用 Lucene 作為其全文搜尋引擎,用於處理純文字的資料,但 Lucene 只是一個庫,提供建立索引、執行搜尋等介面,但不包含分散式服務,這些正是 Elasticsearch 做的。 下面,我們來介紹一下 ElasticSearch 的相關概念。為了便於初學者理解,我們先將 ElasticSearch 中的概念和 MySQL 中的概念大致地進行對應。但是***二者在具體細節上還是有很多差異的,大家深入瞭解 ElasticSearch 就會將二者區分清楚***,不能強行對比等同。 ![](https://img2020.cnblogs.com/blog/1816118/202102/1816118-20210220214711594-2005716984.png) - ElasticSearch 中的索引 Index 類似於 MySQL 中的資料庫 Database; - ElasticSearch 中的型別 Type 類似於 MySQL 中的表 Table;需要注意,這個概念在 7.x 版本中被完全刪除,而且概念上和 Table 也有較大差異; - ElasticSearch 中的文件 Document 類似於 MySQL 中的資料行 Row,每個文件由多個欄位 Filed 組成,這個Filed 就類似於 MySQL 的 Column; - ElasticSearch 中的對映 Mapping 是對索引庫中的索引欄位及其資料型別進行定義,類似於關係型資料庫中的表結構 Schema; - ElasticSearch 使用自己的領域語言 Query DSL 來進行增刪改查,而 MySQL 使用 SQL 語言進行上訴操作。 ElasticSearch 還有一系列有關其分散式特性的概念,我們這裡就暫不介紹了,等後續學習到其分散式特性時在進行介紹。 #### 倒排索引 MySQL 有 B+ 樹索引,而 ElasticSearch 則是倒排索引 (Inverted Index),它通過倒排索引來實現比 MySQL 更快的過濾和複雜條件的查詢,此外,全文搜尋功能也是依賴倒排索引才能實現。下面,我們就具體來看一下何為倒排索引。 倒排索引按照維基百科的描述,是儲存文件內容到文件位置對映關係的資料庫索引結構。不過只看定義,我是有點迷惑,這不是和 MySQL 的非主鍵索引類似嘛,為什麼要叫它“倒排”呢?這個問題我目前也為搞清楚,可能要等到後續瞭解了其具體實現才能理解。 我們還是以書籍檢索為例,假設有以下資料,每一行就是一個 Document,每個 Document 由 id,ISBN 號,作者名稱和評分組成。 ![](https://img2020.cnblogs.com/blog/1816118/202102/1816118-20210220214736819-928756814.png) 給上述資料按照 ISBN 和 Author 建立的倒排索引如下所示。倒排索引是每個欄位分開建立的,相互獨立。有兩個專門的術語,分別是索引 Term 和倒排表 Posting List。欄位的值就是 Term,比如 N0007,而 Term 對應的文件 ID 的列表就是 Posting List,對應圖中紅色的部分。 ![](https://img2020.cnblogs.com/blog/1816118/202102/1816118-20210220214751123-2036174420.png) 一般 Term 都是按照順序排序的,比如 Author 名稱就是按照字母序進行了排序,排序之後,當我們搜尋某一個 Term 時,就不需要從頭遍歷,而是採用二分查詢。一系列排序後的 Term 就組成了索引表 Term Dictionary。 但是 Term Dictionary 往往很大,無法完整放入記憶體,這是為了更快的查詢,還需要再給它建立索引,也就是 Term Index 。 ElasticSearch 使用 Burst-Trie 結構來實現 Term Index,它是一種字首樹 Trie 的一種變種,它主要是將字尾進行了壓縮,降低了Trie的高度,從而獲取更好查詢效能。 Term Index 並不需要像 MySQL 的索引一樣,包含所有的 Term,而是包含的是這些 Term 的字首。它就類似於字典的查詢目錄,可以進行快速定位到 Term Dictionary 的某一位置,然後再從這個位置向後查詢。 綜上, Alice,Alf,Arlan,Bob,Tom 等詞的倒排索引如下所示。綠色部分是 Term Index,藍色部分是 Term Dictionary,紅色部分是 Posting List。 ![](https://img2020.cnblogs.com/blog/1816118/202102/1816118-20210220214810192-621202462.png) 一般來說,Term Index 都是全部快取在記憶體中,查詢時,先通過其快速定位到 Term Dictionary 對應的大致範圍,然後再進行磁碟讀取查詢對應的 Term,這樣就大大減少了磁碟 I/O 的次數。 #### 聯合索引查詢 瞭解了 ElasticSearch 的倒排索引後,我們再來看看其如何處理複雜的聯合索引查詢。比如上述書籍例子中,我們需要查詢評分等於2.2並且作者名稱叫 Tom的書籍。 理論上,我們只需要分別按照 Score 和 Author 欄位的倒排索引進行查詢,獲取響應的 Posting List,再將其做交集合並即可。 這裡又要吐槽一下 MySQL,它是不支援這個合併操作的,它只能按照一個欄位的索引進行查詢,然後根據另外一個欄位的條件做記憶體過濾。順便說一下,MySQL 的 join 功能也弱爆了,感興趣的同學可以瞭解一下[這篇文章](http://remcarpediem.net/article/cd0b2c21/) 而 ElasticSearch 則支援使用跳錶 Skip List和 Bitset 的方式將資料集進行合併。 - 使用 Skip List 結構,同時遍歷 Score 和 Author 查詢出來的 Posting List,利用其 Skip List 結構,相互跳躍對比,得出合集。 - 使用 Bitset 結構,對 Score 和 Author 查詢出來的 Posting List 的值計算出各自的 Bitset,然後進行 AND 操作。 #### 跳錶合併策略 ElasticSearch 在儲存 Posting List 資料時,就儲存了對應的多級跳錶結構響應的資料,這也體現了其空間換時間的基本思想。 這裡先介紹一下跳錶的基本概念,它其實是一種可以進行二分查詢的有序連結串列。跳錶在原有的有序連結串列上面增加了多級索引,通過索引來實現快速查詢。首先在最高階索引上查詢最後一個小於當前查詢元素的位置,然後再跳到次高階索引繼續查詢,直到跳到最底層為止,通過這種方式,加快了查詢的速度。 比如,按照 Score 查出來的 Posting List 為[2,3,4,5,7,9,10,11],按照 Author 查出來的結果為 [3,8,9,12,13],則二者的跳錶結構如下圖所示。 ![](https://img2020.cnblogs.com/blog/1816118/202102/1816118-20210220214835481-1844363387.png) 具體合併過程則是先選最短的 posting list,也就是 Author 的結果集,從其最小的一個 id 開始,將其作為當前最大值。然後依次剩餘 posting list 中查詢大於或等於該值的位置。 比如上述結果集中,先去 Score 結果集中查詢 3,找到後,就表明 3是二者的合集元素之一;然後再重新開啟一輪,選取 Author 結果集中 3 的下一個值 8 ,去 Score 結果集查詢 8,發現了大於等於 8 的最小的值是 9 ,所以不可能有共同的值 8,然後再去 Author 結果集查詢 9 ,發現其大於等於 9 的最小值是 12,所以再去 Score 結果集中查詢大於等於 12的值,發現並不存在;最終得出二者的合集就只有[3]。 在查詢過程中,每個 posting list 都可以根據當前 id 通過 skip list 快速跳過不符合的 id 值,加速整個合併取交集的過程。 ElasticSearch 對於較長的 posting list 也會使用 Frame Of Reference 進行壓縮編碼,減少了磁碟佔用,減少了索引尺寸。有關具體儲存結構的實現我們後續再進行細聊。 #### Bitset 合併策略 ElasticSearch除了使用 skipList 來進行資料磁碟讀取時的合併操作外,還會將一些查詢條件對應的結果集 posting list 進行記憶體快取,也就是所謂的 Filter Cache,為了後續再次複用。 為了減少記憶體快取所消耗的記憶體空間大小,ElasticSearch 沒有使用單純的陣列和 bitset 來儲存 posting list,而是使用要壓縮效率更高的 Roaring Bitmap。 我們可以先來講一下單純陣列或 bitset 資料結構為什麼並不使用。比如如下一道較為常見的面試題目: > 給定含有40億個不重複的位於[0, 2^32 - 1]區間內的整數的集合,如何快速判定某個數是否在該集合內? 如果我們要使用 unsigned long 陣列來儲存它的話,也就需要消耗 40億 * 32 位 = 160 Byte,大致是 16000 MB。 如果要使用點陣圖 Bitset 來儲存的話,即某個數位於原集合內,就將它對應的點陣圖內的位元置為1,否則保持為0。這樣只需要消耗 2 ^ 32 位 = 512 MB,***這可只有原來的 3.2 % 左右***。 但是,Bitset 也有其缺陷,也就是稀疏儲存的問題,比如上述集合並不是 40億,而是隻有2,3個,那麼 Bitset 中只有少數幾位是1,其他位都是 0,但是它仍然佔用了 512 MB。 而 RoaringBitmap 就是為了解決稀疏儲存的問題。下圖就是 RoaringBitmap 的基本原理示意圖。 ![](https://img2020.cnblogs.com/blog/1816118/202102/1816118-20210220214855144-1872868114.png) 首先,如上圖所示,計算出32位無符號整數和 65536 的除數和餘數。其含義表示,將32位無符號整數按照高16位分桶,即最多可能有2^16=65536個桶,術語懲治為 container。儲存資料時,按照資料的高16位找到 container(找不到就會新建一個),再將低16位放入container中。也就是說,一個 RoaringBitmap 就是很多container的集合。 然後 container 內具體的儲存結構要根據存入其內資料的基數來決定。 - 基數小於 2 ^ 12 次方即 4096時,使用unsigned short型別的有序陣列來儲存,最大消耗空間就是 8 KB。 - 基數大於 4096 時,則使用大小為 2 ^ 16 次方的普通 bitset 來儲存,固定消耗 8 KB。當然,有些時候也會對 bitset 進行行程長度編碼(RLE)壓縮,進一步減少空間佔用。 ElasticSearch 就是使用 Roaring Bitmap 來快取不同條件查詢出來的 posting list,然後再進行與操作計算出最終結果集。 #### 後記 至此,我們也算了解了 ElasticSearch 為什麼比 MySQL 更適合複雜條件查詢,但是有好就有弊,因為為了查詢做了這麼多的準備工作,ElasticSearch 的插入速度就會慢於 MySQL,而且**資料存入ES後並不是立馬就能檢索到**。 歡迎持續關注歷小冰,後續繼續為大家分享 MySQL、Redis 和 ElasticSearch 等資料庫相關的原理和實踐經驗。 ##### 參考文章 - http://www.nosqlnotes.com/technotes/searchengine/lucene-invertedindex/ - https://zhuanlan.zhihu.com/p/33671444 - https://www.cnblogs.com/forfuture1978/archive/2010/04/04/1704258.html - https://www.jianshu.com/p/818ac