為什么需要ES
回憶時光
許多年前,一個剛結婚的名叫 Shay Banon 的失業開發者,跟著他的妻子去了倫敦,他的妻子在那里學習廚師。 在尋找一個賺錢的工作的時候,為了給他的妻子做一個食譜搜索引擎,他開始使用 Lucene 的一個早期版本。
直接使用 Lucene 是很難的,因此 Shay 開始做一個抽象層,Java 開發者使用它可以很簡單的給他們的程序添加搜索功能。 他發布了他的第一個開源項目 Compass。
后來 Shay 獲得了一份工作,主要是高性能,分布式環境下的內存數據網格。這個對于高性能,實時,分布式搜索引擎的需求尤為突出, 他決定重寫 Compass,把它變為一個獨立的服務并取名 Elasticsearch。
第一個公開版本在2010年2月發布,從此以后,Elasticsearch 已經成為了 Github 上最活躍的項目之一,他擁有超過300名 contributors(目前736名 contributors )。 一家公司已經開始圍繞 Elasticsearch 提供商業服務,并開發新的特性,但是,Elasticsearch 將永遠開源并對所有人可>用。據說,Shay 的妻子還在等著她的食譜搜索引擎…
學習了基礎的搜索引擎原理 我進而想到大量的數據都是存放在哪里的呢? 為了快速查詢數據結構必然發生變化,是不是需要有特殊存儲方式呢? 嘗試思考如下幾種方式來實現.
Sql
對于關系型數據,我們通常采用以下或類似架構去解決查詢瓶頸和寫入瓶頸(拆表):
1.通過主從備份解決數據安全性問題;
2.通過數據庫代理中間件心跳監測,解決單點故障問題;
3.通過代理中間件將查詢語句分發到各個slave節點進行查詢,并匯總結果
NoSql
對于Nosql數據庫,以mongodb為例,其它原理類似:
1.通過副本備份保證數據安全性;
2.通過節點競選機制解決單點問題;
3.先從配置庫檢索分片信息,然后將請求分發到各個節點,最后由路由節點合并匯總結果
內存
但把大量數據存在內存中并不靠譜,原因是內存成本太高了.尤其是對于搜索引擎來說需要存大量的數據.內存存儲的解決方案,例如redis還是主要用來做高數緩存的,以此來做數據庫就不合適了.
為解決以上問題,從源頭著手分析,通常會從以下方式來尋找方法:
1.存儲數據時按有序存儲;
2.將數據和索引分離;
3.壓縮數據;而這就引出了Elasticsearch。
ES基礎
ES是elaticsearch簡寫, Elasticsearch是一個開源的高擴展的分布式全文檢索引擎,它可以近乎實時的存儲、檢索數據;本身擴展性很好,可以擴展到上百臺服務器,處理PB級別的數據。
Elasticsearch也使用Java開發并使用Lucene作為其核心來實現所有索引和搜索的功能,但是它的目的是通過簡單的RESTful API來隱藏Lucene的復雜性,從而讓全文搜索變得簡單。
Lucene & elaticsearch
簡化Lucene 使用
對于Lucene可以看: Lucene 入門
這里提到了Lucene 是什么呢? 它就是我們想找的那個特殊的存儲結構,但是想要使用它,你必須使用Java來作為開發語言并將其直接集成到你的應用中,更糟糕的是,Lucene非常復雜,你需要深入了解檢索的相關知識來理解它是如何工作的。
而ES類似于netty框架幫我們封裝了復雜的Nio實現 他使用Java開發并使用Lucene作為其核心來實現所有索引和搜索的功能,他提供了簡單的RESTful API來隱藏Lucene的復雜性,從而讓全文搜索變得簡單。
ES做了什么
1.檢索相關數據;
2.返回統計結果;
3.速度要快。
優點:
- 分布式實時文件存儲,可將每一個字段存入索引,使其可以被檢索到。
- 實時分析的分布式搜索引擎。
分布式:索引分拆成多個分片,每個分片可有零個或多個副本。集群中的每個數據節點都可承載一個或多個分片,并且協調和處理各種操作;
負載再平衡和路由在大多數情況下自動完成。- 可以擴展到上百臺服務器,處理PB級別的結構化或非結構化數據。也可以運行在單臺PC上(已測試)
- 支持插件機制,分詞插件、同步插件、Hadoop插件、可視化插件等。
原理
首先和我們熟悉的mysql對比一下:
- 關系型數據庫中的數據庫(DataBase),等價于ES中的索引(Index)
- 一個數據庫下面有N張表(Table),等價于1個索引Index下面有N多類型(Type)
- 一個數據庫表(Table)下的數據由多行(ROW)多列(column,屬性)組成,等價于1個Type由多個文檔(Document)和多Field組成。
- 在一個關系型數據庫里面,schema定義了表、每個表的字段,還有表和字段之間的關系。 與之對應的,在ES中:Mapping定義索引下的Type的字段處理規則,即索引如何建立、索引類型、是否保存原始索引JSON文檔、是否壓縮原始JSON文檔、是否需要分詞處理、如何進行分詞處理等。
- 在數據庫中的增insert、刪delete、改update、查search操作等價于ES中的增PUT/POST、刪Delete、改_update、查GET.
進一步理解
Elasticsearch是面向文檔型數據庫,一條數據在這里就是一個文檔,用JSON作為文檔序列化的格式
{
"name" : "John",
"sex" : "Male",
"age" : 25,
"birthDate": "1990/05/01",
"about" : "I love to go rock climbing",
"interests": [ "sports", "music" ]
}
而如果是mysql呢?
用MySQL這樣的數據庫存儲就會容易想到建立一張User表,有balabala的字段等,在Elasticsearch里這就是一個文檔,當然這個文檔會屬于一個User的類型,各種各樣的類型存在于一個索引當中。這里有一份簡易的將Elasticsearch和關系型數據術語對照表:
關系數據庫 ? 數據庫 ? 表 ? 行 ? 列(Columns)
Elasticsearch ? 索引 ? 類型 ? 文檔 ? 字段(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" ]
}
其有一下幾個核心點:
Cluster:集群。
ES可以作為一個獨立的單個搜索服務器。不過,為了處理大型數據集,實現容錯和高可用性,ES可以運行在許多互相合作的服務器上。這些服務器的集合稱為集群。
Node:節點。
形成集群的每個服務器稱為節點。
Shard:分片。
當有大量的文檔時,由于內存的限制、磁盤處理能力不足、無法足夠快的響應客戶端的請求等,一個節點可能不夠。這種情況下,數據可以分為較小的分片。每個分片放到不同的服務器上。
當你查詢的索引分布在多個分片上時,ES會把查詢發送給每個相關的分片,并將結果組合在一起,而應用程序并不知道分片的存在。即:這個過程對用戶來說是透明的。
Replia:副本。
為提高查詢吞吐量或實現高可用性,可以使用分片副本。
副本是一個分片的精確復制,每個分片可以有零個或多個副本。ES中可以有許多相同的分片,其中之一被選擇更改索引操作,這種特殊的分片稱為主分片。
當主分片丟失時,如:該分片所在的數據不可用時,集群將副本提升為新的主分片。
全文檢索。
全文檢索就是對一篇文章進行索引,可以根據關鍵字搜索,類似于mysql里的like語句。
全文索引就是把內容根據詞的意義進行分詞,然后分別創建索引,例如”你們的激情是因為什么事情來的” 可能會被分詞成:“你們“,”激情“,“什么事情“,”來“ 等token,這樣當你搜索“你們” 或者 “激情” 都會把這句搜出來。
數據結構
主要就是其索引的建立思路
將磁盤里的東西盡量搬進內存,減少磁盤隨機讀取次數(同時也利用磁盤順序讀特性),結合各種奇技淫巧的壓縮算法,用及其苛刻的態度使用內存。
并且其一切設計都是為了提高搜索的性能 其余的部分肯定會有犧牲,比如插入/更新
倒排索引
這就要引出其核心數據結構倒排索引了 對于其的簡單理解可以先看一下之前的博客#搜索引擎入門 這里結合ES詳細介紹一下:
繼續上面的例子,假設有這么幾條數據(為了簡單,去掉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是一顆樹:
這棵樹不會包含所有的term,它包含的是term的一些前綴。通過term index可以快速地定位到term dictionary的某個offset,然后從這個位置再往后順序查找。
所以term index不需要存下所有的term,而僅僅是他們的一些前綴與Term Dictionary的block之間的映射關系,再結合FST(Finite State Transducers)的壓縮技術,可以使term index緩存到內存中。從term index查到對應的term dictionary的block位置之后,再去磁盤上找term,大大減少了磁盤隨機讀的次數。
而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
O表示一種狀態
->表示狀態的變化過程,上面的字母/數字表示狀態變化和權重
將單詞分成單個字母通過O和–>表示出來,0權重不顯示。如果O后面出現分支,就標記權重,最后整條路徑上的權重加起來就是這個單詞對應的序號。
FST以字節的方式存儲所有的term,這種壓縮方式可以有效的縮減存儲空間,使得term index足以放進內存,但這種方式也會導致查找時需要更多的CPU資源。
壓縮
我們學習mysql的時候肯定知道 如果某個數據列包含許多重復的內容,為它建立索引就沒有太大的實際效果。
但對于搜索來說總不能不讓用戶搜索這種字段吧,例如男/女這樣的. ES對于這種索引就會進行壓縮.
壓縮方法
ES為了壓縮索引真的是不擇手段。在壓縮過程中使用如下技巧,它會按依次檢測以下壓縮模式:
- 如果所有的數值各不相同(或缺失),設置一個標記并記錄這些值
- 如果這些值小于 256,將使用一個簡單的編碼表
- 如果這些值大于 256,檢測是否存在一個最大公約數
- 如果沒有存在最大公約數,從最小的數值開始,統一計算偏移量(增量)進行編碼
Frame Of Reference
增量編碼壓縮,將大數變小數,按字節存儲
首先,Elasticsearch要求posting list是有序的(為了提高搜索的性能,再任性的要求也得滿足),這樣做的一個好處是方便壓縮,看下面這個圖例:
如果數學不是體育老師教的話,還是比較容易看出來這種壓縮技巧的。
原理就是通過增量,將原來的大數變成小數僅存儲增量值,再精打細算按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存儲。
為什么是以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來區分采用數組還是bitmap的閥值呢?
這個是從內存大小考慮的,當block塊里元素超過4096后,用bitmap更剩空間: 采用bitmap需要的空間是恒定的: 65536/8 = 8192bytes 而如果采用short[],所需的空間是: 2*N(N為數組元素個數) N=4096剛好是邊界:
你也許會想 "好吧,貌似對數字很好,不知道字符串怎么樣?" 通過借助順序表(ordinal table),String 類型也是類似進行編碼的。String 類型是去重之后存放到順序表的,通過分配一個
ID
,然后通過數字類型的ID
構建Doc Values
。這樣String類型和數值類型可以達到同樣的壓縮效果。而順序表本身也有很多壓縮技巧,比如固定長度、變長或是前綴字符編碼等等。
聯合索引
上面說了半天都是單field索引,如果多個field索引的聯合查詢,倒排索引如何滿足快速查詢的要求呢?
- 利用跳表(Skip list)的數據結構快速做“與”運算,或者
- 利用上面提到的bitset按位“與”
先看看跳表的數據結構:
將一個有序鏈表level0,挑出其中幾個元素到level1及level2,每個level越往上,選出來的指針元素越少,查找時依次從高level往低查找,比如55,先找到level2的31,再找到level1的47,最后找到55,一共3次查找,查找效率和2叉樹的效率相當,但也是用了一定的空間冗余來換取的。
假設有下面三個posting list需要聯合索引:
如果使用跳表,對最短的posting list中的每個id,逐個在另外兩個posting list中查找看是否存在,最后得到交集的結果。
如果使用bitset,就很直觀了,直接按位與,得到的結果就是最后的交集。
最后
注意點
- 不需要索引的字段,一定要明確定義出來,因為默認是自動建索引的
- 同樣的道理,對于String類型的字段,不需要analysis的也需要明確定義出來,因為默認也是會analysis的
- 選擇有規律的ID很重要,隨機性太大的ID(比如java的UUID)不利于查詢
詳細使用
Elasticsearch權威指南1
Elasticsearch權威指南2
ES對外接口
JAVA API接口
常見的增、刪、改、查操作實現:
RESTful API接口
論壇
1)國外:https://discuss.elastic.co/
2)國內:http://elasticsearch.cn/