1. 程式人生 > >Elasticsearch深入理解(一)

Elasticsearch深入理解(一)

[x] Elasticsearch是一個實時分散式搜尋和分析引擎。

  • 維基百科使用Elasticsearch提供全文搜尋並高亮關鍵字,以及輸入實時搜尋(search-as-you-type)和搜尋糾錯(did-you-mean)等搜尋建議功能。
  • StackOverflow結合全文搜尋與地理位置查詢,以及more-like-this功能來找到相關的問題和答案。
  • Github使用Elasticsearch檢索1300億行的程式碼。

    現在大部分資料庫在提取可用知識方面顯得異常無能。的確,它們能夠通過時間戳或者精確匹配做過濾,但是它們能夠進行全文搜尋,處理同義詞和根據相關性給文件打分嗎?它們能根據同一份資料生成分析和聚合的結果嗎?最重要的是,它們在沒有大量工作程序(執行緒)的情況下能做到對資料的實時處理嗎?
    這就是Elasticsearch存在的理由:Elasticsearch鼓勵你瀏覽並利用你的資料,而不是讓它爛在資料庫裡,因為在資料庫裡實在太難查詢了。

[x] Elasticsearch也使用Java開發並使用Lucene作為其核心來實現所有索引和搜尋的功能,但是它的目的是通過簡單的RESTful API來隱藏Lucene的複雜性,從而讓全文搜尋變得簡單。
[x] 不過,Elasticsearch不僅僅是Lucene和全文搜尋,我們還能這樣去描述它:
* 分散式的實時檔案儲存,每個欄位都被索引並可被搜尋
* 分散式的實時分析搜尋引擎
* 可以擴充套件到上百臺伺服器,處理PB級結構化或非結構化資料

[ ] 叢集和節點:節點(node)是一個執行著的Elasticsearch例項。叢集(cluster)是一組具有相同cluster.name的節點集合,他們協同工作,共享資料並提供故障轉移和擴充套件功能,當然一個節點也可以組成一個叢集。

[ ] 與elasticsearch進行互動:
Elasticsearch為Java使用者提供了兩種內建客戶端:
節點客戶端(node client):節點客戶端以無資料節點(none data node)身份加入叢集,換言之,它自己不儲存任何資料,但是它知道資料在叢集中的具體位置,並且能夠直接轉發請求到對應的節點上。

傳輸客戶端(Transport client):

這個更輕量的傳輸客戶端能夠傳送請求到遠端叢集。它自己不加入叢集,只是簡單轉發請求給叢集中的節點。
兩個Java客戶端都通過9300埠與叢集互動,使用Elasticsearch傳輸協議(Elasticsearch Transport Protocol)。叢集中的節點之間也通過9300埠進行通訊。如果此埠未開放,你的節點將不能組成叢集。
spring.data.elasticsearch.cluster-name = cluster
elasticsearch.ip.nodes[0] = 10.118.29.158:9300
elasticsearch.ip.nodes[1] = 10.118.29.157:9300
elasticsearch.index = voicewords
elasticsearch.type = keywords

注意:Java客戶端所在的Elasticsearch版本必須與叢集中其他節點一致,否則,它們可能互相無法識別。

基於HTTP協議,以JSON為資料互動格式的RESTful API

其他所有程式語言都可以使用RESTful API,通過9200埠的與Elasticsearch進行通訊,你可以使用你喜歡的WEB客戶端,事實上,如你所見,你甚至可以通過curl命令與Elasticsearch通訊。
curl -X ‘://:/?’ -d ‘’

  • VERB HTTP方法:GET, POST, PUT, HEAD, DELETE
  • PROTOCOL http或者https協議(只有在Elasticsearch前面有https代理的時候可用)
  • HOST Elasticsearch叢集中的任何一個節點的主機名,如果是在本地的節點,那麼就叫localhost
  • PORT Elasticsearch HTTP服務所在的埠,預設為9200
  • PATH API路徑(例如_count將返回叢集中文件的數量),PATH可以包含多個元件,例如_cluster/stats或者_nodes/stats/jvm
  • QUERY_STRING 一些可選的查詢請求引數,例如?pretty引數將使請求返回更加美觀易讀的JSON資料
  • BODY 一個JSON格式的請求主體(如果請求需要的話)

舉例說明,為了計算叢集中的文件數量,我們可以這樣做:
curl -XGET ‘http://localhost:9200/_count?pretty’ -d ’
{
“query”: {
“match_all”: {}
}
}

[x] Elasticsearch是面向文件(document oriented)的,這意味著它可以儲存整個物件或文件(document)。然而它不僅僅是儲存,還會索引(index)每個文件的內容使之可以被搜尋。在Elasticsearch中,你可以對文件(而非成行成列的資料)進行索引、搜尋、排序、過濾。這種理解資料的方式與以往完全不同,這也是Elasticsearch能夠執行復雜的全文搜尋的原因之一。

ELasticsearch使用Javascript物件符號(JavaScript Object Notation),也就是JSON,作為文件序列化格式。JSON現在已經被大多語言所支援,而且已經成為NoSQL領域的標準格式。它簡潔、簡單且容易閱讀。

[ ] 我們首先要做的是儲存員工資料,每個文件代表一個員工。在Elasticsearch中儲存資料的行為就叫做索引(indexing),不過在索引之前,我們需要明確資料應該儲存在哪裡。
在Elasticsearch中,文件歸屬於一種型別(type),而這些型別存在於索引(index)中,我們可以畫一些簡單的對比圖來類比傳統
關係型資料庫:
Relational DB -> Databases -> Tables -> Rows -> Columns
Elasticsearch -> Indices -> Types -> Documents -> Fields

Elasticsearch叢集可以包含多個索引(indices)(資料庫),每一個索引可以包含多個型別(types)(表),每一個型別包含多個文件(documents)(行),然後每個文件包含多個欄位(Fields)(列)。
* 索引(名詞) 如上文所述,一個索引(index)就像是傳統關係資料庫中的資料庫,它是相關文件儲存的地方,index的複數是indices 或indexes。
* 索引(動詞) 「索引一個文件」表示把一個文件儲存到索引(名詞)裡,以便它可以被檢索或者查詢。這很像SQL中的INSERT關鍵字,差別是,如果文件已經存在,新的文件將覆蓋舊的文件。
* 倒排索引 傳統資料庫為特定列增加一個索引,例如B-Tree索引來加速檢索。Elasticsearch和Lucene使用一種叫做倒排索引(inverted index)的資料結構來達到相同目的。
文件中的所有欄位都會被索引(擁有一個倒排索引),只有這樣他們才是可被搜尋的。

所以為了建立員工目錄,我們將進行如下操作:
* 為每個員工的文件(document)建立索引,每個文件包含了相應員工的所有資訊。
* 每個文件的型別為employee。
* employee型別歸屬於索引megacorp。
* megacorp索引儲存在Elasticsearch叢集中。
插入員工id為1的員工資訊:
PUT /megacorp/employee/1
{
“first_name” : “John”,
“last_name” : “Smith”,
“age” : 25,
“about” : “I love to go rock climbing”,
“interests”: [ “sports”, “music” ]
}
我們看到path:/megacorp/employee/1包含三部分資訊:
名字
說明
megacorp
索引名
employee
型別名
1
這個員工的ID
查詢員工id為1的員工資訊:
GET /megacorp/employee/1
我們嘗試一個最簡單的搜尋全部員工的請求:
+
GET /megacorp/employee/_search

你可以看到我們依然使用megacorp索引和employee型別,但是我們在結尾使用關鍵字_search來取代原來的文件ID。響應內容的hits陣列中包含了我們所有的三個文件。預設情況下搜尋會返回前10個結果。
{
“took”: 6,
“timed_out”: false,
“_shards”: { … },
“hits”: {
“total”: 3,
“max_score”: 1,
“hits”: [
{
“_index”: “megacorp”,
“_type”: “employee”,
“_id”: “3”,
“_score”: 1,
“_source”: {
“first_name”: “Douglas”,
“last_name”: “Fir”,
“age”: 35,
“about”: “I like to build cabinets”,
“interests”: [ “forestry” ]
}
},
{
“_index”: “megacorp”,
“_type”: “employee”,
“_id”: “1”,
“_score”: 1,
“_source”: {
“first_name”: “John”,
“last_name”: “Smith”,
“age”: 25,
“about”: “I love to go rock climbing”,
“interests”: [ “sports”, “music” ]
}
},
{
“_index”: “megacorp”,
“_type”: “employee”,
“_id”: “2”,
“_score”: 1,
“_source”: {
“first_name”: “Jane”,
“last_name”: “Smith”,
“age”: 32,
“about”: “I like to collect rock albums”,
“interests”: [ “music” ]
}
}
]
}
}
接下來,讓我們搜尋姓氏中包含“Smith”的員工。要做到這一點,我們將在命令列中使用輕量級的搜尋方法。這種方法常被稱作查詢字串(query string)搜尋,因為我們像傳遞URL引數一樣去傳遞查詢語句:
GET /megacorp/employee/_search?q=last_name:Smith
我們在請求中依舊使用_search關鍵字,然後將查詢語句傳遞給引數q=。這樣就可以得到所有姓氏為Smith的結果:
{

“hits”: {
“total”: 2,
“max_score”: 0.30685282,
“hits”: [
{

“_source”: {
“first_name”: “John”,
“last_name”: “Smith”,
“age”: 25,
“about”: “I love to go rock climbing”,
“interests”: [ “sports”, “music” ]
}
},
{

“_source”: {
“first_name”: “Jane”,
“last_name”: “Smith”,
“age”: 32,
“about”: “I like to collect rock albums”,
“interests”: [ “music” ]
}
}
]
}
}
使用DSL語句查詢

查詢字串搜尋便於通過命令列完成特定(ad hoc)的搜尋,但是它也有侷限性(參閱簡單搜尋章節)。Elasticsearch提供豐富且靈活的查詢語言叫做DSL查詢(Query DSL),它允許你構建更加複雜、強大的查詢。
DSL(Domain Specific Language特定領域語言)以JSON請求體的形式出現。我們可以這樣表示之前關於“Smith”的查詢:
GET /megacorp/employee/_search
{
“query” : {
“match” : {
“last_name” : “Smith”
}
}
}
這會返回與之前查詢相同的結果。你可以看到有些東西改變了,我們不再使用查詢字串(query string)做為引數,而是使用請求體代替。這個請求體使用JSON表示,其中使用了match語句(查詢型別之一,具體我們以後會學到)。

更復雜的搜尋

我們讓搜尋稍微再變的複雜一些。我們依舊想要找到姓氏為“Smith”的員工,但是我們只想得到年齡大於30歲的員工。我們的語句將新增過濾器(filter),它使得我們高效率的執行一個結構化搜尋:
GET /megacorp/employee/_search
{
“query” : {
“filtered” : {
“filter” : {
“range” : {
“age” : { “gt” : 30 } <1>
}
},
“query” : {
“match” : {
“last_name” : “smith” <2>
}
}
}
}
}
* <1> 這部分查詢屬於區間過濾器(range filter),它用於查詢所有年齡大於30歲的資料——gt為”greater than”的縮寫。
* <2> 這部分查詢與之前的match語句(query)一致。

全文搜尋

到目前為止搜尋都很簡單:搜尋特定的名字,通過年齡篩選。讓我們嘗試一種更高階的搜尋,全文搜尋——一種傳統資料庫很難實現的功能。
我們將會搜尋所有喜歡“rock climbing”的員工:
GET /megacorp/employee/_search
{
“query” : {
“match” : {
“about” : “rock climbing”
}
}
}
你可以看到我們使用了之前的match查詢,從about欄位中搜索”rock climbing”,我們得到了兩個匹配文件:
{

“hits”: {
“total”: 2,
“max_score”: 0.16273327,
“hits”: [
{

“_score”: 0.16273327, <1>
“_source”: {
“first_name”: “John”,
“last_name”: “Smith”,
“age”: 25,
“about”: “I love to go rock climbing”,
“interests”: [ “sports”, “music” ]
}
},
{

“_score”: 0.016878016, <2>
“_source”: {
“first_name”: “Jane”,
“last_name”: “Smith”,
“age”: 32,
“about”: “I like to collect rock albums”,
“interests”: [ “music” ]
}
}
]
}
}
* <1><2> 結果相關性評分。
預設情況下,Elasticsearch根據結果相關性評分來對結果集進行排序,所謂的「結果相關性評分」就是文件與查詢條件的匹配程度。很顯然,排名第一的John Smith的about欄位明確的寫到“rock climbing”。
但是為什麼Jane Smith也會出現在結果裡呢?原因是“rock”在她的abuot欄位中被提及了。因為只有“rock”被提及而“climbing”沒有,所以她的_score要低於John。
這個例子很好的解釋了Elasticsearch如何在各種文字欄位中進行全文搜尋,並且返回相關性最大的結果集。相關性(relevance)的概念在Elasticsearch中非常重要,而這個概念在傳統關係型資料庫中是不可想象的,因為傳統資料庫對記錄的查詢只有匹配或者不匹配。
短語搜尋

目前我們可以在欄位中搜索單獨的一個詞,這挺好的,但是有時候你想要確切的匹配若干個單詞或者短語(phrases)。例如我們想要查詢同時包含”rock”和”climbing”(並且是相鄰的)的員工記錄。
要做到這個,我們只要將match查詢變更為match_phrase查詢即可:
GET /megacorp/employee/_search
{
“query” : {
“match_phrase” : {
“about” : “rock climbing”
}
}
}
毫無疑問,該查詢返回John Smith的文件:
{

“hits”: {
“total”: 1,
“max_score”: 0.23013961,
“hits”: [
{

“_score”: 0.23013961,
“_source”: {
“first_name”: “John”,
“last_name”: “Smith”,
“age”: 25,
“about”: “I love to go rock climbing”,
“interests”: [ “sports”, “music” ]
}
}
]
}
}

高亮我們的搜尋

很多應用喜歡從每個搜尋結果中高亮(highlight)匹配到的關鍵字,這樣使用者可以知道為什麼這些文件和查詢相匹配。在Elasticsearch中高亮片段是非常容易的。
讓我們在之前的語句上增加highlight引數:
GET /megacorp/employee/_search
{
“query” : {
“match_phrase” : {
“about” : “rock climbing”
}
},
“highlight”: {
“fields” : {
“about” : {}
}
}
}
當我們執行這個語句時,會命中與之前相同的結果,但是在返回結果中會有一個新的部分叫做highlight,這裡包含了來自about欄位中的文字,並且用來標識匹配到的單詞。
{

“hits”: {
“total”: 1,
“max_score”: 0.23013961,
“hits”: [
{

“_score”: 0.23013961,
“_source”: {
“first_name”: “John”,
“last_name”: “Smith”,
“age”: 25,
“about”: “I love to go rock climbing”,
“interests”: [ “sports”, “music” ]
},
“highlight”: {
“about”: [
“I love to go rock climbing” <1>
]
}
}
]
}
}
* <1> 原有文字中高亮的片段
你可以在高亮章節閱讀更多關於搜尋高亮的部分。

Elasticsearch致力於隱藏分散式系統的複雜性。以下這些操作都是在底層自動完成的:
* 將你的文件分割槽到不同的容器或者分片(shards)中,它們可以存在於一個或多個節點中。
* 將分片均勻的分配到各個節點,對索引和搜尋做負載均衡。
* 冗餘每一個分片,防止硬體故障造成的資料丟失。
* 將叢集中任意一個節點上的請求路由到相應資料所在的節點。
* 無論是增加節點,還是移除節點,分片都可以做到無縫的擴充套件和遷移。

叢集內部工作方式

你在使用Elasticsearch的時候可以長時間甚至永遠都不必擔心分片、複製和故障轉移——但是它會幫助你理解Elasticsearch內部的工作流程。
Elasticsearch用於構建高可用和可擴充套件的系統。擴充套件的方式可以是購買更好的伺服器(縱向擴充套件(vertical scale or scaling up))或者購買更多的伺服器(橫向擴充套件(horizontal scale or scaling out))
    對於大多數資料庫而言,橫向擴充套件意味著你的程式將做非常大的改動才能利用這些新新增的裝置。對比來說,Elasticsearch天生就是分散式的:它知道如何管理節點來提供高擴充套件和高可用。這意味著你的程式不需要關心這些。

空叢集

如果我們啟動一個單獨的節點,它還沒有資料和索引,這個叢集看起來:
一個節點(node)就是一個Elasticsearch例項,而一個叢集(cluster)由一個或多個節點組成,它們具有相同的cluster.name,它們協同工作,分享資料和負載。當加入新的節點或者刪除一個節點時,叢集就會感知到並平衡資料。
叢集中一個節點會被選舉為主節點(master),它將臨時管理叢集級別的一些變更,例如新建或刪除索引、增加或移除節點等。主節點不參與文件級別的變更或搜尋,這意味著在流量增長的時候,該主節點不會成為叢集的瓶頸。任何節點都可以成為主節點。我們例子中的叢集只有一個節點,所以它會充當主節點的角色。
+
做為使用者,我們能夠與叢集中的任何節點通訊,包括主節點。每一個節點都知道文件存在於哪個節點上,它們可以轉發請求到相應的節點上。我們訪問的節點負責收集各節點返回的資料,最後一起返回給客戶端。這一切都由Elasticsearch處理。
叢集健康

在Elasticsearch叢集中可以監控統計很多資訊,但是隻有一個是最重要的:叢集健康(cluster health)。叢集健康有三種狀態:green、yellow或red。
GET /_cluster/health
{
“cluster_name”: “elasticsearch”,
“status”: “green”, <1>
“timed_out”: false,
“number_of_nodes”: 1,
“number_of_data_nodes”: 1,
“active_primary_shards”: 0,
“active_shards”: 0,
“relocating_shards”: 0,
“initializing_shards”: 0,
“unassigned_shards”: 0
}
* <1> status 是我們最感興趣的欄位
status欄位提供一個綜合的指標來表示叢集的的服務狀況。三種顏色各自的含義:
顏色
意義
green
所有主要分片和複製分片都可用
yellow
所有主要分片可用,但不是所有複製分片都可用
red
不是所有的主要分片都可用
新增索引

為了將資料新增到Elasticsearch,我們需要索引(index)——一個儲存關聯資料的地方。實際上,索引只是一個用來指向一個或多個分片(shards)的“邏輯名稱空間(logical namespace)”.

一個分片(shard)是一個最小級別“工作單元(worker unit)”,它只是儲存了索引中所有資料的一部分。在接下來的《深入分片》一章,我們將詳細說明分片的工作原理,但是現在我們只要知道分片就是一個Lucene例項,並且它本身就是一個完整的搜尋引擎。我們的文件儲存在分片中,並且在分片中被索引,但是我們的應用程式不會直接與它們通訊,取而代之的是,直接與索引通訊。
+

分片是Elasticsearch在叢集中分發資料的關鍵。把分片想象成資料的容器。文件儲存在分片中,然後分片分配到你叢集中的節點上。當你的叢集擴容或縮小,Elasticsearch將會自動在你的節點間遷移分片,以使叢集保持平衡。

分片可以是主分片(primary shard)或者是複製分片(replica shard)。你索引中的每個文件屬於一個單獨的主分片,所以主分片的數量決定了索引最多能儲存多少資料。

理論上主分片能儲存的資料大小是沒有限制的,限制取決於你實際的使用情況。分片的最大容量完全取決於你的使用狀況:硬體儲存的大小、文件的大小和複雜度、如何索引和查詢你的文件,以及你期望的響應時間。
複製分片只是主分片的一個副本,它可以防止硬體故障導致的資料丟失,同時可以提供讀請求,比如搜尋或者從別的shard取回文件。

當索引建立完成的時候,主分片的數量就固定了,但是複製分片的數量可以隨時調整。

讓我們在叢集中唯一一個空節點上建立一個叫做blogs的索引。預設情況下,一個索引被分配5個主分片,但是為了演示的目的,我們只分配3個主分片和一個複製分片(每個主分片都有一個複製分片):

PUT /blogs
{
“settings” : {
“number_of_shards” : 3,
“number_of_replicas” : 1
}
}
附帶索引的單一節點叢集:
我們的叢集現在看起來就像上圖——三個主分片都被分配到Node 1。如果我們現在檢查叢集健康(cluster-health),我們將見到以下資訊:
{
“cluster_name”: “elasticsearch”,
“status”: “yellow”, <1>
“timed_out”: false,
“number_of_nodes”: 1,
“number_of_data_nodes”: 1,
“active_primary_shards”: 3,
“active_shards”: 3,
“relocating_shards”: 0,
“initializing_shards”: 0,
“unassigned_shards”: 3 <2>
}
* <1> 叢集的狀態現在是 yellow
* <2> 我們的三個複製分片還沒有被分配到節點上
叢集的健康狀態yellow表示所有的主分片(primary shards)啟動並且正常運行了——叢集已經可以正常處理任何請求——但是複製分片(replica shards)還沒有全部可用。事實上所有的三個複製分片現在都是unassigned狀態——它們還未被分配給節點。在同一個節點上儲存相同的資料副本是沒有必要的,如果這個節點故障了,那所有的資料副本也會丟失。
增加故障轉移

在單一節點上執行意味著有單點故障的風險——沒有資料備份。幸運的是,要防止單點故障,我們唯一需要做的就是啟動另一個節點。
啟動第二個節點

為了測試在增加第二個節點後發生了什麼,你可以使用與第一個節點相同的方式啟動第二個節點(《執行Elasticsearch》一章),而且命令列在同一個目錄——一個節點可以啟動多個Elasticsearch例項。
只要第二個節點與第一個節點有相同的cluster.name(請看./config/elasticsearch.yml檔案),它就能自動發現並加入第一個節點所在的叢集。如果沒有,檢查日誌找出哪裡出了問題。這可能是網路廣播被禁用,或者防火牆阻止了節點通訊。
如果我們啟動了第二個節點:
雙節點叢集——所有的主分片和複製分片都已分配:
第二個節點已經加入叢集,三個複製分片(replica shards)也已經被分配了——分別對應三個主分片,這意味著在丟失任意一個節點的情況下依舊可以保證資料的完整性。
文件的索引將首先被儲存在主分片中,然後併發複製到對應的複製節點上。這可以確保我們的資料在主節點和複製節點上都可以被檢索。
cluster-health現在的狀態是green,這意味著所有的6個分片(三個主分片和三個複製分片)都已可用:
{
“cluster_name”: “elasticsearch”,
“status”: “green”, <1>
“timed_out”: false,
“number_of_nodes”: 2,
“number_of_data_nodes”: 2,
“active_primary_shards”: 3,
“active_shards”: 6,
“relocating_shards”: 0,
“initializing_shards”: 0,
“unassigned_shards”: 0
}
* <1> 叢集的狀態是green.
我們的叢集不僅是功能完備的,而且是高可用的。

橫向擴充套件

隨著應用需求的增長,我們該如何擴充套件?如果我們啟動第三個節點,我們的叢集會重新組織自己,就像包含3個節點的叢集——分片已經被重新分配以平衡負載:
Node3包含了分別來自Node 1和Node 2的一個分片,這樣每個節點就有兩個分片,和之前相比少了一個,這意味著每個節點上的分片將獲得更多的硬體資源(CPU、RAM、I/O)。
分片本身就是一個完整的搜尋引擎,它可以使用單一節點的所有資源。我們擁有6個分片(3個主分片和三個複製分片),最多可以擴充套件到6個節點,每個節點上有一個分片,每個分片可以100%使用這個節點的資源。

繼續擴充套件

如果我們要擴充套件到6個以上的節點,要怎麼做?
主分片的數量在建立索引時已經確定。實際上,這個數量定義了能儲存到索引裡資料的最大數量(實際的數量取決於你的資料、硬體和應用場景)。然而,主分片或者複製分片都可以處理讀請求——搜尋或文件檢索,所以資料的冗餘越多,我們能處理的搜尋吞吐量就越大。
複製分片的數量可以在執行中的叢集中動態地變更,這允許我們可以根據需求擴大或者縮小規模。讓我們把複製分片的數量從原來的1增加到2:
PUT /blogs/_settings
{
“number_of_replicas” : 2
}
https://raw.githubusercontent.com/looly/elasticsearch-definitive-guide-cn/master/images/elas_0205.png
圖:增加number_of_replicas到2:
從圖中可以看出,blogs索引現在有9個分片:3個主分片和6個複製分片。這意味著我們能夠擴充套件到9個節點,再次變成每個節點一個分片。這樣使我們的搜尋效能相比原始的三節點叢集增加三倍。
當然,在同樣數量的節點上增加更多的複製分片並不能提高效能,因為這樣做的話平均每個分片的所佔有的硬體資源就減少了(譯者注:大部分請求都聚集到了分片少的節點,導致一個節點吞吐量太大,反而降低效能),你需要增加硬體來提高吞吐量。
不過這些額外的複製節點使我們有更多的冗餘:通過以上對節點的設定,我們能夠承受兩個節點故障而不丟失資料。