1. 程式人生 > 其它 >ElasticSearch詳解

ElasticSearch詳解

簡介

Elasticsearch(簡稱ES)是一個分散式、可擴充套件、實時的搜尋與資料分析引擎。ES不僅僅只是全文搜尋,還支援結構化搜尋、資料分析、複雜的語言處理、地理位置和物件間關聯關係等。

ES的底層依賴Lucene,Lucene可以說是當下最先進、高效能、全功能的搜尋引擎庫。但是Lucene僅僅只是一個庫。為了充分發揮其功能,你需要使用Java並將Lucene直接整合到應用程式中。更糟糕的是,您可能需要獲得資訊檢索學位才能瞭解其工作原理,因為Lucene非常複雜——《ElasticSearch官方權威指南》。

鑑於Lucene如此強大卻難以上手的特點,誕生了ES。ES也是使用Java編寫的,它的內部使用Lucene做索引與搜尋,它的目的是隱藏Lucene的複雜性,取而代之的提供一套簡單一致的RESTful API。

總體來說,ES具有如下特點:

  • 一個分散式的實時文件儲存引擎,每個欄位都可以被索引與搜尋

  • 一個分散式實時分析搜尋引擎,支援各種查詢和聚合操作

  • 能勝任上百個服務節點的擴充套件,並可以支援PB級別的結構化或者非結構化資料

基本概念

接近實時(NRT)

Elasticsearch是一個接近實時的搜尋平臺。這意味著,從索引一個文件直到這個文件能夠被搜尋到有一個輕微的延遲(通常是1秒)

叢集(cluster)

一個叢集就是由一個或多個節點組織在一起,它們共同持有整個的資料,並一起提供索引和搜尋功能。一個叢集由一個唯一的名字標識,這個名字預設就是 “elasticsearch”。這個名字是重要的,因為一個節點只能通過指定某個叢集的名字,來加入這個叢集。在產品環境中顯式地設定這個名字是一個好習慣,但是使用預設值來進行測試/開發也是不錯的。

節點(node)

一個節點是你叢集中的一個伺服器,作為叢集的一部分,它儲存你的資料,參與叢集的索引和搜尋功能。和叢集類似,一個節點也是由一個名字來標識的,預設情況 下,這個名字是一個隨機的漫威漫畫角色的名字,這個名字會在啟動的時候賦予節點。這個名字對於管理工作來說挺重要的,因為在這個管理過程中,你會去確定網路中的哪些伺服器對應於Elasticsearch叢集中的哪些節點。

一個節點可以通過配置叢集名稱的方式來加入一個指定的叢集。預設情況下,每個節點都會被安排加入到一個叫做“elasticsearch”的叢集中,這意 味著,如果你在你的網路中啟動了若干個節點,並假定它們能夠相互發現彼此,它們將會自動地形成並加入到一個叫做“elasticsearch”的叢集中。

在一個叢集裡,只要你想,可以擁有任意多個節點。而且,如果當前你的網路中沒有執行任何Elasticsearch節點,這時啟動一個節點,會預設建立並加入一個叫做“elasticsearch”的叢集。

  • Master 節點:每個 ES 節點啟動之前都會有個預設配置 node.master:true ,也就是說每個節點都有可能成為 Master 節點,這些節點被稱作 Master-eligible nodes ,就是合格的有資格成為 Master 節點的節點。
    當然 Master 只能有一個,所以會通過選舉的方法對這啟動的節點選舉,被選中的節點才會成為 Master 節點。
    Master 節點主要是負責維護叢集的狀態,像所有節點的資訊,所有的索引和它相關的 Mapping 關係,配置資訊,分片的路由等。
    既然 Master 節點維護了這麼重要的資訊,玩意它掛了怎麼辦?
    掛了的話,將會對其他的有資格成為 Master 節點的節點重新選舉出另一個 Master 節點,因此這就說明了其他 Master-eligible nodes 也會儲存叢集資訊,但是隻有 Master 節點有許可權能夠修改,試想如果其他節點也能修改的話,這將會導致資料不一致的問題。

  • Data Node 節點:資料節點,這個節點主要負責資料的儲存,在資料擴充套件上起到了至關重要的作用。也就是說讀寫資料都會找到相應的 Data Node 節點。

  • Coordinating Node 節點:協調節點主要負責協調客戶端的請求,將接收到的請求分發給合適的節點,並把結果彙集到一起。比如客戶端請求查詢某個索引的資料,協調節點將會把請求分發給儲存相關的資料的 DataNode 節點,找到相應的分片,並把查詢到的結果都彙集返回。並且每個節點都預設起到了 Coordinating Node 的職責。

  • Ingest Node節點: Ingest node 專門對索引的文件做預處理,發生在對真實文件建立索引之前。在建立索引對文件預處理之前,先定義一個管道(pipeline),管道里指定了一系列的處理器。每個處理器能夠把文件按照某種特定的方式轉換。比如在管道里定義一個從某個文件中移除欄位的處理器,緊接著一個重新命名欄位的處理器。叢集的狀態也會被儲存到配置的管道內。定義一個管道,簡單的在索引或者bulk request(一種批量請求方法)操作上定義 pipeline 引數,這樣 ingest node 就會知道哪個管道在使用。這個節點在使用過程中用的也不多,所以大概瞭解一下就行。

說明:

  • 一個節點可以充當一個或多個角色,預設三個角色都有。
  • 協調節點:一個節點只作為接收請求、轉發請求到其他節點、彙總各個節點返回資料等功能的節點。就叫協調節點。

分片(Shard)

當有大量的文件時,由於記憶體的限制、磁碟處理能力不足、無法足夠快的響應客戶端的請求等,一個節點可能不夠。

這種情況下,資料可以分為較小的分片。每個分片放到不同的伺服器上。
當你查詢的索引分佈在多個分片上時,ES會把查詢傳送給每個相關的分片,並將結果組合在一起,而應用程式並不知道分片的存在。即:這個過程對使用者來說是透明的。

說明:

  • 建立索引的時候就確定好主分片的數量,除非重索引。
  • 分片對應的儲存實體是索引
  • 一個分片就是一個 Lucene 例項

路由

Elasticsearch 如何知道一個文件應該存放到哪個分片中呢?當我們建立文件時,它如何決定這個文件應當被儲存在分片 1 還是分片 2 中呢?

首先這肯定不會是隨機的,否則將來要獲取文件的時候我們就不知道從何處尋找了。實際上,這個過程是根據下面這個公式決定的:

shard = hash(routing) % number_of_primary_shards

routing 是一個可變值,唯一不可重複,預設是文件的 _id ,也可以設定成一個自定義的值。 routing 通過 hash 函式生成一個數字,然後這個數字再除以 number_of_primary_shards (主分片的數量)後得到餘數 。這個分佈在 0 到 number_of_primary_shards-1 之間的餘數,就是我們所尋求的文件所在分片的位置。

這就解釋了為什麼我們要在建立索引的時候就確定好主分片的數量 並且永遠不會改變這個數量:因為如果數量變化了,那麼所有之前路由的值都會無效,文件也再也找不到了。
所有的文件 API( get 、 index 、 delete 、 bulk 、 update 以及 mget )都接受一個叫做 routing 的路由引數 ,通過這個引數我們可以自定義文件到分片的對映。一個自定義的路由引數可以用來確保所有相關的文件——例如所有屬於同一個使用者的文件——都被儲存到同一個分片中。

Replia:副本

在建立某個索引之前,需要指定分配這個索引多少個分片?多少個副本?副本就這這個分片的備胎,當分片掛掉了,它的副本就會隨時準備上位,因此副本也是個分片只不過不負責主要功能。

不僅僅如此,ES 如何能夠提高資料吞吐量呢?增加副本個數就是個不錯的選擇,比如說讀寫分離,讀資料的時候從副本上讀,寫資料的時候只用主分片去寫。需要注意的是,主分片的個數實在建立索引之前要確定,建立完索引之後,是不能夠進行修改的,除非重新建索引。因此在建索引之前,一定要合理的配置分片個數,副本個數的話後期是可以改動的。

為提高查詢吞吐量或實現高可用性,可以使用分片副本。
副本是一個分片的精確複製,每個分片可以有零個或多個副本。ES中可以有許多相同的分片,其中之一被選擇更改索引操作,這種特殊的分片稱為主分片。
當主分片丟失時,如:該分片所在的資料不可用時,叢集將副本提升為新的主分片。
Elasticsearch 禁止同一個分片的主分片和副本分片在同一個節點上,所以如果是一個節點的叢集是不能有副本的。

它在節點失敗的情況下提供高可用性。由於這個原因,需要注意的是,副本分片永遠不會分配到與主分片相同的節點上。

如何設定副本

啟動 2 個 ES 節點,配置分片個數為 3,副本個數為 1(每個分片有一個副本)。如下圖,藍色的代表主分片,綠色的是副本,仔細一點不難發現,分片與其副本不在同一個節點內。這是非常合理的,因為副本本來就是主分片的備胎,當主分片節點掛了,另外一個節點的副本將會充當主分片,如果它們在同一個節點內,副本將發揮不到作用。

水平擴充套件原理

單個節點的容量是有限的,如果後期兩個節點的容量不能夠支援三個分片,那麼另外啟動一個節點就可以了,ES 會自動的重新規劃分片,如下圖:可以看到 A3 節點已經被自動的分配到 Node3 節點裡面了,另外副本 B1 從 Node2 移動到 Node3 節點,B3 分片從 Node1 節點被分配到 Node2 節點。這裡想一下,如果再啟動一個節點呢?是的,再啟動一個節點將不會對主分片起到任何作用,因為主分片不可以修改,只有三個,但是副本可以修改,能夠起到擴容的作用。

索引(index)

一個索引就是一個擁有幾分相似特徵的文件的集合。比如說,你可以有一個客戶資料的索引,另一個產品目錄的索引,還有一個訂單資料的索引。一個索引由一個名字來標識(必須全部是小寫字母的),並且當我們要對對應於這個索引中的文件進行索引、搜尋、更新和刪除的時候,都要使用到這個名字。

在一個叢集中,如果你想,可以定義任意多的索引。

Elastic 會索引所有欄位,經過處理後寫入一個反向索引(Inverted Index)。查詢資料的時候,直接查詢該索引。

所以,Elastic 資料管理的頂層單位就叫做 Index(索引)。它是單個數據庫的同義詞。每個 Index (即資料庫)的名字必須是小寫。

下面的命令可以檢視當前節點的所有 Index。

curl -X GET 'http://localhost:9200/_cat/indices?v'

新建和刪除 Index

新建 Index,可以直接向 Elastic 伺服器發出 PUT 請求。下面的例子是新建一個名叫weather的 Index。

curl -X PUT 'localhost:9200/weather'

伺服器返回一個 JSON 物件,裡面的acknowledged欄位表示操作成功。

{
  "acknowledged":true,
  "shards_acknowledged":true
}

然後,我們發出 DELETE 請求,刪除這個 Index。

curl -X DELETE 'localhost:9200/weather'

型別(type)

在一個索引中,你可以定義一種或多種型別。一個型別是你的索引的一個邏輯上的分類/分割槽,其語義完全由你來定。通常,會為具有一組共同欄位的文件定義一個型別。比如說,我們假設你運營一個部落格平臺並且將你所有的資料儲存到一個索引中。在這個索引中,你可以為使用者資料定義一個型別,為部落格資料定義另一個型別,當然,也可以為評論資料定義另一個型別。

Document

Index 裡面單條的記錄稱為 Document(文件)。許多條 Document 構成了一個 Index。

一個文件是一個可被索引的基礎資訊單元。比如,你可以擁有某一個客戶的文件,某一個產品的一個文件,當然,也可以擁有某個訂單的一個文件。文件以 JSON(Javascript Object Notation)格式來表示,而JSON是一個到處存在的網際網路資料互動格式。

在一個index/type裡面,只要你想,你可以儲存任意多的文件。注意,儘管一個文件,物理上存在於一個索引之中,文件必須被索引/賦予一個索引的type。

Document 使用 JSON 格式表示,下面是一個例子。

{
  "user": "張三",
  "title": "工程師",
  "desc": "資料庫管理"
}

同一個 Index 裡面的 Document,不要求有相同的結構(scheme),但是最好保持相同,這樣有利於提高搜尋效率。

Type(7.0廢棄)

Document 可以分組,比如weather這個 Index 裡面,可以按城市分組(北京和上海),也可以按氣候分組(晴天和雨天)。這種分組就叫做 Type,它是虛擬的邏輯分組,用來過濾 Document。

不同的 Type 應該有相似的結構(schema),舉例來說,id欄位不能在這個組是字串,在另一個組是數值。這是與關係型資料庫的表的一個區別。性質完全不同的資料(比如productslogs)應該存成兩個 Index,而不是一個 Index 裡面的兩個 Type(雖然可以做到)。

下面的命令可以列出每個 Index 所包含的 Type。

curl 'localhost:9200/_mapping?pretty=true'

根據規劃,Elastic 6.x 版只允許每個 Index 包含一個 Type,7.x 版將會徹底移除 Type。

索引生命週期

在實戰開發的生產環境中,索引的動態模板設定、索引Mapping設定、索引分片數/副本數設定、索引建立、開啟、關閉、刪除的全生命週期的管理必須高度關注,做好提前知識儲備,否則,會在開發後期出現由於資料激增暴露架構設計不合理問題,甚至引發分片/節點資料丟失、叢集宕機等嚴重問題。

索引生命週期管理的重要性?

索引管理決定Elasticsearch魯棒性、高可用性。
索引管理和搜尋、插入效能也密切相關。
實際場景例子:100節點的叢集中某一個節點資料丟失後,GET /_cat/nodes?v 介面的返回時延時延非常大,接近5-8s。搜尋、聚合的效能更不必說。
原因:節點丟失後,ES會自動複製分片到新的節點中去,但是該丟失節點的shard非常大(幾百個GB甚至上TB),叢集當時的寫入壓力也非常大。這麼大量級的資料拷貝和實時寫入,最終導致延時會非常大。

高可用的索引管理初探

索引生命週期管理的核心就是定義索引的早期階段,前面考慮充分了,後面的架構才會高效、穩定。

實際Elasticsearch5.X之後的版本已經推出:新增了一個Rollover API。Rollover API解決的是以日期作為索引名稱的索引大小不均衡的問題。
medcl介紹如下:Rollover API對於日誌類的資料非常有用,一般我們按天來對索引進行分割(資料量更大還能進一步拆分),沒有Rollover之前,需要在程式裡設定一個自動生成索引的模板,相比於模板,Rollover API是更為簡潔的方式。

ILM

ILM:索引生命週期管理,即Manage the index lifecycle。使用ILM應確保叢集中的所有節點執行的是同一個版本,不然無法保證他們會按預期工作。

ES從6.7版本推出了索引生命週期管理(Index Lifecycle Management ,簡稱ILM)機制,能幫我們自動管理一個索引策略(Policy)下索引叢集的生命週期。索引策略將一個索引的生命週期定義為四個階段:

  • Hot:索引可寫入,也可查詢。
  • Warm:索引不可寫入,但可查詢。
  • Cold:索引不可寫入,但很少被查詢,查詢的慢點也可接受。
  • Delete:索引可被安全的刪除。

rollover

當索引滿足一定條件之後,將不再寫入資料,而是自動建立一個新的索引,所有的資料將寫入新索引。

使用滾動索引能夠:

  1. 優化活躍索引,在高效能hot節點上提升高接收速率。
  2. 優化warm節點搜尋效能。
  3. 將舊的、訪問頻率低的資料轉移到成本低的cold節點上。
  4. 通過刪除整個索引,根據索引保留策略刪除資料。

官方推薦使用data stream資料流來管理時間序列資料。每個資料流都需要一個索引模板,其中包括:

  1. 資料流的名稱或萬用字元(*)模式。
  2. 資料流時間戳欄位。該欄位必須對映為datedate_nanos資料型別。並且包含在索引到該資料流的每個文件中。
  3. 當建立每一個索引時將應用索引模板的對映和設定。

資料流專為追加資料而設計,其中資料流名稱可用作操作(讀取、寫入、翻轉、收縮等)目標。如果需要更新資料,可以使用索引別名來管理時間序列資料。

自動 rollover

ILM會根據你的配置:索引大小文件數量所在階段 ,當滿足這些條件時,自動實現rollover

RollOver的適用場景

這個特性對於存放日誌資料的場景、索引非常大、索引實時匯入資料的場景是極為友好的。
你也可以先在索引模板裡面設定索引的setting、mapping等引數, 然後設定好_rollover 規則,剩下的es會自動幫你處理。

索引生命週期管理

索引生命週期策略更新

  1. 生命週期策略被應用到索引上時,索引會獲取當前策略的最新版本號。如果更新了當前策略,版本號會發生衝突,ILM就能檢測出當前索引正在使用上一個版本的策略,需要將索引策略更新到最新版本。
  2. 如果將不同的策略應用到已經被管理的索引上時,索引還是使用先前管理策略中的快取定義來完成當前階段。直到進入下一個階段,索引才會應用新的管理策略。

索引生命週期操作

  1. allocate:將分片移動到具有不同效能特徵的節點上,並減少副本的數量。
  2. delete:永久移除索引。
  3. force merge:減少索引段的數量並清除已刪除的文件。使索引為只讀
  4. freeze:凍結索引以最大程度減少其記憶體的佔用量。
  5. read only:阻止對索引的寫操作。
  6. rollover:刪除索引作為過渡別名的寫索引,然後開始索引到新索引。
  7. set priority:降低索引在生命週期中的優先順序,以確保首先恢復熱索引。
  8. shrink:通過將索引縮小為新索引來減少主分片的數量。
  9. unfollow:將關注者索引轉換為常規索引。在進行滾動或收縮操作之前自動執行。
  10. wait for snapshot:刪除索引之前,確保快照存在。

配置生命週期策略

要讓ILM管理索引,必須要在index.lifecycle.name索引設定中指定有效的策略。

要為滾動索引建立生命週期策略,你要建立該策略並把它加入到索引模板中。

建立生命週期策略

可以通過Kibana管理頁面設定,也可以通過API設定。

PUT _ilm/policy/my_policy
{
    "policy": {
        "phases": {
            "hot": {
                "actions": {
                    "rollover": {
                        "max_size": "25GB"
                    }
                }
            },
            {
                "delete": {
                    "min_age": "30d",
                    "actions": {
                        "delete": {}
                    }
                }
            }
        }
    }
}

將生命週期策略應用到索引模板中

可以通過Kibana管理頁面設定,也可以通過API設定。

PUT _index_template/my_template
{
    "index_patterns": ["test-*"],
    "template": {
        "settings": {
            "number_of_shards": 1,
            "number_of_replicas": 1,
            "index.lifecycle.name": "my_policy",
            "index.lifecycle.rollover_alias": "test-alias"
        }
    }
}

建立一個初始被管理的索引

如果要給滾動索引設定策略,需要手動建立第一個被該策略管理的索引,並指定為可寫索引。

索引的名稱必須跟索引模板裡定義的模式相匹配,並且以數字結尾。

PUT test-000001
{
    "aliases": {
        "test-alias": {
            "is_write_index": true
        }
    }
}

手動應用生命週期策略

你可以在建立索引的時候指定一個策略,也可以直接將策略應用到一個已經存在的索引上通過Kibana管理或者更新設定的API。一旦你應用了策略,ILM立即會開始管理該索引。

PUT test-index
{
    "settings": {
        "number_of_shards": 1,
        "number_of_replicas": 1,
        "index.lifecycle.name": "my_police"
    }
}

將策略應用於多個索引

PUT mylogs-pre-ilm*/_settings
{
    "index": {
        "lifecycle": {
            "name": "mylogs_policy_existing"
        }
    }
}

解決生命週期策略執行報錯

檢視錯誤:

GET /my-index-000001/_ilm/explain   

重新執行報錯的一步:

POST /my-index-000001/_ilm/retry

開啟和終止索引生命週期管理

檢視ILM狀態:

GET _ilm/status

# 返回結果
{
    "operation_mode": "RUNNING"
}

終止ILM

POST _ilm/stop

# 返回結果
{
    "operation_mode": "STOPPING"
}

{
    "operation_mode": "STOPPED"
}

開啟ILM

POST _ilm/start

跳過rollover

設定index.lifecycle.indexing_completetrue

舉個例子,如果你要改變一系列新索引的名稱,並保留之前根據你配置的策略產生的索引資料,你可以:

  1. 為新的索引模式建立一個模板,並使用之前相同的策略。
  2. 根據新的模板建立一個初始索引。
  3. 使用索引別名API將別名的write索引更改為bootstrapped索引。
  4. 設定舊索引的index.lifecycle.indexing_complete的值為true

Rollover的不足和改進

Rollover API大大簡化了基於時間的索引的管理。但是,仍然需要以一種重複的方式呼叫_rollover API介面,可以手動呼叫,也可以通過基於crontab的工具(如director)呼叫。
但是,如果翻轉過程是隱式的並在內部進行管理,則會簡單得多。其思想是在建立索引時(或在索引模板中相等地)在別名中指定滾動條件。

PUT /<logs-{now/d}-1>
{
    "mappings": {...},
    "aliases" : {
        "logs-search" : {},
        "logs-write" : {
            "rollover" : {
                "conditions": {
                    "max_age":   "1d",
                    "max_docs":  100000
                }
            }
        }
    }
}

github提出的改進建議如下:
https://github.com/elastic/elasticsearch/issues/26092

中文分詞設定

首先,安裝中文分詞外掛。這裡使用的是 ik,也可以考慮其他外掛(比如 smartcn)。

./bin/elasticsearch-plugin install https://github.com/medcl/elasticsearch-analysis-ik/releases/download/v6.3.0/elasticsearch-analysis-ik-6.3.0.zip

注意:替換6.3.0為自己的 elasticsearch 版本

重新啟動 Elastic,就會自動載入這個新安裝的外掛。

示例

1.建立索引

curl -XPUT http://localhost:9200/index

2.建立對映

curl -XPOST http://localhost:9200/index/_mapping -H 'Content-Type:application/json' -d'
{
        "properties": {
            "content": {
                "type": "text",
                "analyzer": "ik_max_word",
                "search_analyzer": "ik_smart"
            }
        }

}'

3.索引一些文件

curl -XPOST http://localhost:9200/index/_create/1 -H 'Content-Type:application/json' -d'
{"content":"美國留給伊拉克的是個爛攤子嗎"}
'
curl -XPOST http://localhost:9200/index/_create/2 -H 'Content-Type:application/json' -d'
{"content":"公安部:各地校車將享最高路權"}
'
curl -XPOST http://localhost:9200/index/_create/3 -H 'Content-Type:application/json' -d'
{"content":"中韓漁警衝突調查:韓警平均每天扣1艘中國漁船"}
'
curl -XPOST http://localhost:9200/index/_create/4 -H 'Content-Type:application/json' -d'
{"content":"中國駐洛杉磯領事館遭亞裔男子槍擊 嫌犯已自首"}
'

4.查詢高亮

curl -XPOST http://localhost:9200/index/_search  -H 'Content-Type:application/json' -d'
{
    "query" : { "match" : { "content" : "中國" }},
    "highlight" : {
        "pre_tags" : ["<tag1>", "<tag2>"],
        "post_tags" : ["</tag1>", "</tag2>"],
        "fields" : {
            "content" : {}
        }
    }
}
'

結果

{
    "took": 14,
    "timed_out": false,
    "_shards": {
        "total": 5,
        "successful": 5,
        "failed": 0
    },
    "hits": {
        "total": 2,
        "max_score": 2,
        "hits": [
            {
                "_index": "index",
                "_type": "fulltext",
                "_id": "4",
                "_score": 2,
                "_source": {
                    "content": "中國駐洛杉磯領事館遭亞裔男子槍擊 嫌犯已自首"
                },
                "highlight": {
                    "content": [
                        "<tag1>中國</tag1>駐洛杉磯領事館遭亞裔男子槍擊 嫌犯已自首 "
                    ]
                }
            },
            {
                "_index": "index",
                "_type": "fulltext",
                "_id": "3",
                "_score": 2,
                "_source": {
                    "content": "中韓漁警衝突調查:韓警平均每天扣1艘中國漁船"
                },
                "highlight": {
                    "content": [
                        "均每天扣1艘<tag1>中國</tag1>漁船 "
                    ]
                }
            }
        ]
    }
}

字典配置

IKAnalyzer.cfg.xml 可以放在 {conf}/analysis-ik/config/IKAnalyzer.cfg.xml 或者 {plugins}/elasticsearch-analysis-ik-*/config/IKAnalyzer.cfg.xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd">
<properties>
	<comment>IK Analyzer 擴充套件配置</comment>
	<!--使用者可以在這裡配置自己的擴充套件字典 -->
	<entry key="ext_dict">custom/mydict.dic;custom/single_word_low_freq.dic</entry>
	 <!--使用者可以在這裡配置自己的擴充套件停止詞字典-->
	<entry key="ext_stopwords">custom/ext_stopword.dic</entry>
 	<!--使用者可以在這裡配置遠端擴充套件字典 -->
	<entry key="remote_ext_dict">location</entry>
 	<!--使用者可以在這裡配置遠端擴充套件停止詞字典-->
	<entry key="remote_ext_stopwords">http://xxx.com/xxx.dic</entry>
</properties>

熱更新IK分詞使用方法

當前外掛支援熱 IK 分詞,通過新聞更新應在 IK 配置檔案中出現如下的配置

 	<!--使用者可以在這裡配置遠端擴充套件字典--> 
	< entry  key = " remote_ext_dict " >location</ entry >
 	 <!--使用者可以在這裡遠端擴充套件停止詞字典--> 
	< entry  key = " remote_ext_stopwords " >location</ entry >

location是指一個網址,例如http://yoursite.com/getCustomDict,該需要滿足以下幾點幾點完成分詞熱更新。

  1. 該http請求需要返回一個標題,一個Last-Modified,是ETag,這兩個單獨的字串型別,只要有一個變化,就可以去抓取新的分詞,這是一個更新詞庫。
  2. 該http請求返回的內容格式是一行一個分詞,換行符用\n分類。

滿足上面兩點要求就可以實現熱更新分詞了,不需要重啟ES例項。

可以將需自動更新的熱詞編碼一個 UTF-8 編碼的 .txt 檔案裡,寫入 nginx 或其他簡易 http 伺服器下,當 .txt 檔案修改時,http 伺服器會在客戶端請求該檔案時自動返回相應的 Last-Modified 和 ETag。可以另外做一個工具來從業務系統提取相關詞彙,並更新 .txt 檔案。

然後,新建一個 Index,指定需要分詞的欄位。這一步根據資料結構而異,下面的命令只針對本文。基本上,凡是需要搜尋的中文欄位,都要單獨設定一下。

curl -X PUT 'localhost:9200/accounts' -d '
{
  "mappings": {
    "person": {
      "properties": {
        "user": {
          "type": "text",
          "analyzer": "ik_max_word",
          "search_analyzer": "ik_max_word"
        },
        "title": {
          "type": "text",
          "analyzer": "ik_max_word",
          "search_analyzer": "ik_max_word"
        },
        "desc": {
          "type": "text",
          "analyzer": "ik_max_word",
          "search_analyzer": "ik_max_word"
        }
      }
    }
  }
}'

上面程式碼中,首先新建一個名稱為accounts的 Index,裡面有一個名稱為person的 Type。person有三個欄位: user,title,desc

這三個欄位都是中文,而且型別都是文字(text),所以需要指定中文分詞器,不能使用預設的英文分詞器。

Elastic 的分詞器稱為 analyzer。我們對每個欄位指定分詞器。

"user": {
  "type": "text",
  "analyzer": "ik_max_word",
  "search_analyzer": "ik_max_word"
}

上面程式碼中,analyzer是欄位文字的分詞器,search_analyzer是搜尋詞的分詞器。ik_max_word分詞器是外掛ik提供的,可以對文字進行最大數量的分詞。

常見問題

1.自定義詞典為什麼沒有生效?

請確保你的擴充套件詞典的文字格式為 UTF8 編碼

2.如何手動安裝?

git clone https://github.com/medcl/elasticsearch-analysis-ik
cd elasticsearch-analysis-ik
git checkout tags/{version}
mvn clean
mvn compile
mvn package

拷貝和解壓release下的檔案: #{project_path}/elasticsearch-analysis-ik/target/releases/elasticsearch-analysis-ik-*.zip 到你的 elasticsearch 外掛目錄, 如: plugins/ik 重啟elasticsearch

3.分詞測試失敗 請在某個索引下呼叫analyze介面測試,而不是直接呼叫analyze介面 如:

curl -XGET "http://localhost:9200/your_index/_analyze" -H 'Content-Type: application/json' -d'
{
   "text":"中華人民共和國MN","tokenizer": "my_ik"
}'
  1. ik_max_word 和 ik_smart 什麼區別?

ik_max_word: 會將文字做最細粒度的拆分,比如會將“中華人民共和國國歌”拆分為“中華人民共和國,中華人民,中華,華人,人民共和國,人民,人,民,共和國,共和,和,國國,國歌”,會窮盡各種可能的組合,適合 Term Query;

ik_smart: 會做最粗粒度的拆分,比如會將“中華人民共和國國歌”拆分為“中華人民共和國,國歌”,適合 Phrase 查詢。

官方文件:https://github.com/medcl/elasticsearch-analysis-ik/

常用API

curl -XGET 'localhost:9200/_cat/health?v&pretty'    #叢集健康
curl -XGET 'localhost:9200/_cat/nodes?v&pretty'		#檢視所有節點
curl -XGET 'localhost:9200/_cat/indices?v&pretty'	#檢視所有索引
curl -XPUT 'localhost:9200/customer?pretty&pretty'    #建立一個名為 “customer” 的索引

# 索引一個簡單的 customer 文件到 customer 索引中,“external” 型別,與一個為 1 的 ID
curl -XPUT 'localhost:9200/customer/external/1?pretty&pretty' -d'
{
  "name": "John Doe"
}'

curl -XGET 'localhost:9200/customer/external/1?pretty&pretty'  #檢索我們剛剛索引的文件
curl -XDELETE 'localhost:9200/customer?pretty&pretty'   #刪除剛建立的索引

#如果仔細研究上面的命令,可以清楚的看到,如何訪問 Elasticsearch 中的資料的 pattern(模式)。該 pattern(模式)可以概括如下 :
<REST Verb> /<Index>/<Type>/<ID>

#更新文件
#更新我們先前的文件(ID 為 1),通過修改 name 欄位的值為 “Jane Doe” : 
curl -XPOST 'localhost:9200/customer/external/1/_update?pretty&pretty' -d'
{
  "doc": { "name": "Jane Doe" }
}'
#更新我們先前的文件(ID 為 1),通過修改 name 欄位的值為 “Jane Doe”,並且同時新增 age 欄位 : 
curl -XPOST 'localhost:9200/customer/external/1/_update?pretty&pretty' -d'
{
  "doc": { "name": "Jane Doe", "age": 20 }
}'
#更新也是通過使用簡單的簡本來執行。這個例子演示了使用簡本來將 age 加 5 : 
curl -XPOST 'localhost:9200/customer/external/1/_update?pretty&pretty' -d'
{
  "script" : "ctx._source.age += 5"
}'
#在上面的例子中,ctx._source 代表當前將被更新的源文件。

搜尋 API

現在讓我們從一些簡單的搜尋開始。這裡兩個執行搜尋的基本方法 : 一個是通過使用 REST request URI 傳送搜尋引數,另一個是通過使用 REST request body 來發送它們。請求體的方法可以讓您更具有表現力,並且可以在一個更可讀的 JSON 格式中定義您的搜尋。我們會嘗試使用一個 REST request URI 的示例,但是在本教程的其它部分,我們將只使用 REST request body 的方法。

搜尋的 REST API_search 的尾部開始。這個示例返回了 bank 索引中的所有文件 :

curl -XGET 'localhost:9200/bank/_search?q=*&sort=account_number:asc&pretty'

首先讓我們切開搜尋的呼叫。我們在 bank 索引中執行搜尋(_search 尾部),然後 q=引數命令Elasticsearch去匹配索引中所有的文件。pretty 引數,再一次告訴 Elasticsearch 去返回列印漂亮的 JSON** 結果。

響應如下(部分):

{
  "took" : 63,
  "timed_out" : false,
  "_shards" : {
    "total" : 5,
    "successful" : 5,
    "failed" : 0
  },
  "hits" : {
    "total" : 1000,
    "max_score" : null,
    "hits" : [ {
      "_index" : "bank",
      "_type" : "account",
      "_id" : "0",
      "sort": [0],
      "_score" : null,
      "_source" : {"account_number":0,"balance":16623,"firstname":"Bradshaw","lastname":"Mckenzie","age":29,"gender":"F","address":"244 Columbus Place","employer":"Euron","email":"[email protected]","city":"Hobucken","state":"CO"}
    }, {
      "_index" : "bank",
      "_type" : "account",
      "_id" : "1",
      "sort": [1],
      "_score" : null,
      "_source" : {"account_number":1,"balance":39225,"firstname":"Amber","lastname":"Duke","age":32,"gender":"M","address":"880 Holmes Lane","employer":"Pyrami","email":"[email protected]","city":"Brogan","state":"IL"}
    }, ...
    ]
  }
}

在響應中,我們可以看到以下幾個部分 :

  • took - Elasticsearch 執行搜尋的時間(毫秒)
  • time_out - 告訴我們搜尋是否超時
  • _shards - 告訴我們多少個分片被搜尋了,以及統計了成功/失敗的搜尋分片
  • hits - 搜尋結果
  • hits.total - 搜尋結果
  • hits.hits - 實際的搜尋結果陣列(預設為前 10 的文件)
  • sort - 結果的排序 key(鍵)(沒有則按 score 排序)
  • scoremax_score -現在暫時忽略這些欄位

這裡是上面相同的搜尋,使用了 REST request body 方法 :

curl -XGET 'localhost:9200/bank/_search?pretty' -d'
{
  "query": { "match_all": {} },
  "sort": [
    { "account_number": "asc" }
  ]
}'

這裡不同的地方是而不是在 URL 中傳遞 q= **,我們* *POST* 一個 JSON 風格的查詢請求體到 _search API。我們將在下一部分討論這個 JSON** 查詢。

需要了解,一旦您搜尋的結果被返回,Elasticsearch 完成了這次請求,並且不會維護任何服務端的資源或者結果的 cursor(遊標)。這與其它的平臺形成了鮮明的對比,例如 SQL,您可能首先獲得查詢結果的子集,如果您想要使用一些服務端有狀態的 cursor(游標)來抓取(或者通過分頁)其它的結果,然後您必須再次回到伺服器。

資料操作

官方API文件:https://www.elastic.co/guide/en/elasticsearch/reference/current/docs.html

中文API文件:https://doc.codingdict.com/elasticsearch/

新增記錄

向指定的 /Index/Type 傳送 PUT 請求,就可以在 Index 裡面新增一條記錄。比如,向/accounts/person傳送請求,就可以新增一條人員記錄。

curl -X PUT 'localhost:9200/accounts/person/1' -d '
{
  "user": "張三",
  "title": "工程師",
  "desc": "資料庫管理"
}' 

伺服器返回的 JSON 物件,會給出 Index、Type、Id、Version 等資訊。

{
  "_index":"accounts",
  "_type":"person",
  "_id":"1",
  "_version":1,
  "result":"created",
  "_shards":{"total":2,"successful":1,"failed":0},
  "created":true
}

如果你仔細看,會發現請求路徑是/accounts/person/1,最後的1是該條記錄的 Id。它不一定是數字,任意字串(比如abc)都可以。

新增記錄的時候,也可以不指定 Id,這時要改成 POST 請求。

$ curl -X POST 'localhost:9200/accounts/person' -d '
{
  "user": "李四",
  "title": "工程師",
  "desc": "系統管理"
}'

上面程式碼中,向/accounts/person發出一個 POST 請求,新增一個記錄。這時,伺服器返回的 JSON 物件裡面,_id欄位就是一個隨機字串。

{
  "_index":"accounts",
  "_type":"person",
  "_id":"AV3qGfrC6jMbsbXb6k1p",
  "_version":1,
  "result":"created",
  "_shards":{"total":2,"successful":1,"failed":0},
  "created":true
}

注意,如果沒有先建立 Index(這個例子是accounts),直接執行上面的命令,Elastic 也不會報錯,而是直接生成指定的 Index。所以,打字的時候要小心,不要寫錯 Index 的名稱。

檢視記錄

/Index/Type/Id發出 GET 請求,就可以檢視這條記錄。

curl 'localhost:9200/accounts/person/1?pretty=true'

上面程式碼請求檢視/accounts/person/1這條記錄,URL 的引數pretty=true表示以易讀的格式返回。

返回的資料中,found欄位表示查詢成功,_source欄位返回原始記錄。

{
  "_index" : "accounts",
  "_type" : "person",
  "_id" : "1",
  "_version" : 1,
  "found" : true,
  "_source" : {
    "user" : "張三",
    "title" : "工程師",
    "desc" : "資料庫管理"
  }
}

如果 Id 不正確,就查不到資料,found欄位就是false

$ curl 'localhost:9200/weather/beijing/abc?pretty=true'

{
  "_index" : "accounts",
  "_type" : "person",
  "_id" : "abc",
  "found" : false
}

刪除記錄

刪除記錄就是發出 DELETE 請求。

curl -X DELETE 'localhost:9200/accounts/person/1'

這裡先不要刪除這條記錄,後面還要用到。

更新記錄

更新記錄就是使用 PUT 請求,重新發送一次資料。

$ curl -X PUT 'localhost:9200/accounts/person/1' -d '
{
    "user" : "張三",
    "title" : "工程師",
    "desc" : "資料庫管理,軟體開發"
}' 

{
  "_index":"accounts",
  "_type":"person",
  "_id":"1",
  "_version":2,
  "result":"updated",
  "_shards":{"total":2,"successful":1,"failed":0},
  "created":false
}

上面程式碼中,我們將原始資料從"資料庫管理"改成"資料庫管理,軟體開發"。 返回結果裡面,有幾個欄位發生了變化。

"_version" : 2,
"result" : "updated",
"created" : false

可以看到,記錄的 Id 沒變,但是版本(version)從1變成2,操作型別(result)從created變成updatedcreated欄位變成false,因為這次不是新建記錄。

資料查詢

返回所有記錄

使用 GET 方法,直接請求/Index/Type/_search,就會返回所有記錄。

curl 'localhost:9200/accounts/person/_search'

{
  "took":2,
  "timed_out":false,
  "_shards":{"total":5,"successful":5,"failed":0},
  "hits":{
    "total":2,
    "max_score":1.0,
    "hits":[
      {
        "_index":"accounts",
        "_type":"person",
        "_id":"AV3qGfrC6jMbsbXb6k1p",
        "_score":1.0,
        "_source": {
          "user": "李四",
          "title": "工程師",
          "desc": "系統管理"
        }
      },
      {
        "_index":"accounts",
        "_type":"person",
        "_id":"1",
        "_score":1.0,
        "_source": {
          "user" : "張三",
          "title" : "工程師",
          "desc" : "資料庫管理,軟體開發"
        }
      }
    ]
  }
}

上面程式碼中,返回結果的 took欄位表示該操作的耗時(單位為毫秒),timed_out欄位表示是否超時,hits欄位表示命中的記錄,裡面子欄位的含義如下。

  • total:返回記錄數,本例是2條。
  • max_score:最高的匹配程度,本例是1.0
  • hits:返回的記錄組成的陣列。

返回的記錄中,每條記錄都有一個_score欄位,表示匹配的程式,預設是按照這個欄位降序排列。

全文搜尋

Elastic 的查詢非常特別,使用自己的查詢語法,要求 GET 請求帶有資料體。

curl 'localhost:9200/accounts/person/_search'  -d '
{
  "query" : { "match" : { "desc" : "軟體" }}
}'

上面程式碼使用 Match 查詢,指定的匹配條件是desc欄位裡面包含"軟體"這個詞。返回結果如下。

{
  "took":3,
  "timed_out":false,
  "_shards":{"total":5,"successful":5,"failed":0},
  "hits":{
    "total":1,
    "max_score":0.28582606,
    "hits":[
      {
        "_index":"accounts",
        "_type":"person",
        "_id":"1",
        "_score":0.28582606,
        "_source": {
          "user" : "張三",
          "title" : "工程師",
          "desc" : "資料庫管理,軟體開發"
        }
      }
    ]
  }
}

Elastic 預設一次返回10條結果,可以通過size欄位改變這個設定。

curl 'localhost:9200/accounts/person/_search'  -d '
{
  "query" : { "match" : { "desc" : "管理" }},
  "size": 1
}'

上面程式碼指定,每次只返回一條結果。

還可以通過from欄位,指定位移。

curl 'localhost:9200/accounts/person/_search'  -d '
{
  "query" : { "match" : { "desc" : "管理" }},
  "from": 1,
  "size": 1
}'

上面程式碼指定,從位置1開始(預設是從位置0開始),只返回一條結果。

邏輯運算

如果有多個搜尋關鍵字, Elastic 認為它們是or關係。

curl 'localhost:9200/accounts/person/_search'  -d '
{
  "query" : { "match" : { "desc" : "軟體 系統" }}
}'

上面程式碼搜尋的是軟體 or 系統

如果要執行多個關鍵詞的and搜尋,必須使用布林查詢

curl 'localhost:9200/accounts/person/_search'  -d '
{
  "query": {
    "bool": {
      "must": [
        { "match": { "desc": "軟體" } },
        { "match": { "desc": "系統" } }
      ]
    }
  }
}'

分片(sharding)

一個索引可以儲存超出單個結點硬體限制的大量資料。比如,一個具有10億文件的索引佔據1TB的磁碟空間,而任一節點都沒有這樣大的磁碟空間;或者單個節點處理搜尋請求,響應太慢。

為了解決這個問題,ES提供了將索引劃分成多份的能力,這些份就叫做分片。當你建立一個索引的時候,可以指定你想要的分片的數量。每個分片本身也是一個功能完善並且獨立的“索引”,這個“索引”可以被放置到叢集中的任何節點上。

分片分為主分片 (primary shard) 以及從分片 (replica shard)。主分片會被儘可能平均地 (rebalance) 分配在不同的節點上(例如你有 2 個節點,4 個主分片(不考慮備份),那麼每個節點會分到 2 個分片,後來你增加了 2 個節點,那麼你這 4 個節點上都會有 1 個分片,這個過程叫 relocation,ES 感知後自動完成)。

從分片只是主分片的一個副本,它用於提供資料的冗餘副本,從分片和主分片不會出現在同一個節點上(防止單點故障),預設情況下一個索引建立 5 個主分片,每個主分片會有一個從分片 (5 primary + 5 replica = 10 個分片)。如果你只有一個節點,那麼 5 個 replica 都無法被分配 (unassigned),此時 cluster status 會變成 Yellow。

分片是獨立的,對於一個 Search Request 的行為,每個分片都會執行這個 Request。每個分片都是一個 Lucene Index,所以一個分片只能存放 Integer.MAX_VALUE - 128 = 2,147,483,519 個 docs。

分片之所以重要,主要有兩方面的原因:

  1. 允許你水平分割/擴充套件你的內容容量

    允許你在分片(潛在地,位於多個節點上)之上進行分散式的、並行的操作,進而提高效能/吞吐量。至於一個分片怎樣分佈,它的文件怎樣聚合回搜尋請求,是完全由ES管理的,對於作為使用者的你來說,這些都是透明的。

  2. 在一個網路/雲的環境裡,失敗隨時都可能發生,在某個分片/節點不知怎麼的就處於離線狀態,或者由於任何原因消失了。這種情況下,有一個故障轉移機制是非常有用並且是強烈推薦的。為此目的,ES允許你建立分片的一份或多份拷貝,這些拷貝叫做複製分片,或者直接叫複製。

複製之所以重要,主要有兩方面的原因:

(1)在分片/節點失敗的情況下,提供了高可用性。因為這個原因,注意到複製分片從不與原/主要(original/primary)分片置於同一節點上是非常重要的。

(2)擴充套件你的搜尋量/吞吐量,因為搜尋可以在所有的複製上並行執行

總之,每個索引可以被分成多個分片。一個索引也可以被複制0次(意思是沒有複製)或多次。一旦複製了,每個索引就有了主分片(作為複製源的原來的分片)和複製分片(主分片的拷貝)之別。分片和複製的數量可以在索引建立的時候指定。在索引建立之後,你可以在任何時候動態地改變複製數量,但是不能改變分片的數量。

預設情況下,ES中的每個索引被分片5個主分片和1個複製,這意味著,如果你的叢集中至少有兩個節點,你的索引將會有5個主分片和另外5個複製分片(1個完全拷貝),這樣的話每個索引總共就有10個分片。一個索引的多個分片可以存放在叢集中的一臺主機上,也可以存放在多臺主機上,這取決於你的叢集機器數量。主分片和複製分片的具體位置是由ES內在的策略所決定的

叢集

節點通過設定叢集名稱,在同一網路中發現具有相同叢集名稱的節點,組成叢集。每個叢集都有一個 cluster name 作為標識,預設的叢集名稱為 elasticsearch。如果在同一網路中只有一個節點,則這個節點成為一個單節點叢集。

叢集狀態

  • Green:所有主分片和從分片都準備就緒(分配成功),即使有一臺機器掛了(假設一臺機器一個例項),資料都不會丟失,但會變成 Yellow 狀態。
  • Yellow:所有主分片準備就緒,但存在至少一個主分片(假設是 A)對應的從分片沒有就緒,此時叢集屬於警告狀態,意味著叢集高可用和容災能力下降,如果剛好 A 所在的機器掛了,而從分片還處於未就緒狀態,那麼 A 的資料就會丟失(查詢結果不完整),此時叢集進入 Red 狀態。
  • Red:至少有一個主分片沒有就緒(直接原因是找不到對應的從分片成為新的主分片),此時查詢的結果會出現資料丟失(不完整)。

ES內部分片處理機制

逆向索引

與傳統的資料庫不同,在Elasticsearch中,每個欄位裡面的每個單詞都是可以被搜尋的。如teacher:“zls,bgx,lidao,oldboy,alex”我們在搜尋關鍵字oldboy時,所有包含oldboy的文件都會被匹配到Elasticsearch的這個特性也叫做全文搜尋。

為了支援這個特性,Elasticsearch中會維護一個叫做“invertedindex”(也叫逆向索引)的表,表內包含了所有文件中出現的所有單詞,同時記錄了這個單詞在哪個文件中出現過。

例:當前有4個文件

txt1:“zls,bgx,lidao”

txt2:“zls,oldboy,alex”

txt3:“bgx,lidao,oldboy”

txt4:“oldboy,alex”

那麼Elasticsearch會維護下面一個數據結構表:

Term txt1 txt2 txt3 txt4
zls Y Y
bgx Y Y
lidao Y Y
oldboy Y Y Y
alex Y Y

隨意搜尋任意一個單詞,Elasticsearch只要遍歷一下這個表,就可以知道有些文件被匹配到了。

逆向索引裡面不止記錄了單詞與文件的對應關係,它還維護了很多其他有用的資料。如:每個文件一共包含了多少個單詞,單詞在不同文件中的出現頻率,每個文件的長度,所有文件的總長度等等。這些資料用來給搜尋結果進行打分,如搜尋zls時,那麼出現zls這個單詞次數最多的文件會被優先返回,因為它匹配的次數最多,和我們的搜尋條件關聯性最大,因此得分也最多。

逆向索引是不可更改的,一旦它被建立了,裡面的資料就不會再進行更改。這樣做就帶來了以下幾個好處:

  • 1.沒有必要給逆向索引加鎖,因為不允許被更改,只有讀操作,所以就不用考慮多執行緒導致互斥等問題。
  • 2.索引一旦被載入到了快取中,大部分訪問操作都是對記憶體的讀操作,省去了訪問磁碟帶來的io開銷。
  • 3.因為逆向索引的不可變性,所有基於該索引而產生的快取也不需要更改,因為沒有資料變更。
  • 4.使用逆向索引可以壓縮資料,減少磁碟io及對記憶體的消耗。

分片的儲存

寫索引過程

ES 叢集中每個節點通過路由都知道叢集中的文件的存放位置,所以每個節點都有處理讀寫請求的能力。

在一個寫請求被髮送到某個節點後,該節點即為協調節點,協調節點會根據路由公式計算出需要寫到哪個分片上,再將請求轉發到該分片的主分片節點上。假設 shard = hash(routing) % 4 = 0 ,則過程大致如下:

  1. 客戶端向 ES1節點(協調節點)傳送寫請求,通過路由計算公式得到值為0,則當前資料應被寫到主分片 S0 上。
  2. ES1 節點將請求轉發到 S0 主分片所在的節點 ES3,ES3 接受請求並寫入到磁碟。
  3. 併發將資料複製到兩個副本分片 R0 上,其中通過樂觀併發控制資料的衝突。一旦所有的副本分片都報告成功,則節點 ES3 將向協調節點報告成功,協調節點向客戶端報告成功。

儲存原理

索引的不可變性

寫入磁碟的倒排索引是不可變的,優勢主要表現在:

  • 不需要鎖。因為如果從來不需要更新一個索引,就不必擔心多個程式同時嘗試修改,也就不需要鎖。
  • 一旦索引被讀入核心的檔案系統快取,便會留在哪裡,由於其不變性,只要檔案系統快取中還有足夠的空間,那麼大部分讀請求會直接請求記憶體,而不會命中磁碟。這提供了很大的效能提升。
  • 其它快取(像filter快取),在索引的生命週期內始終有效。它們不需要在每次資料改變時被重建,因為資料不會變化。
  • 寫入單個大的倒排索引,可以壓縮資料,較少磁碟 IO 和需要快取索引的記憶體大小。

當然,不可變的索引有它的缺點:

  • 當對舊資料進行刪除時,舊資料不會馬上被刪除,而是在 .del檔案中被標記為刪除。而舊資料只能等到段更新時才能被移除,這樣會造成大量的空間浪費。
  • 若有一條資料頻繁的更新,每次更新都是新增新的標記舊的,則會有大量的空間浪費。
  • 每次新增資料時都需要新增一個段來儲存資料。當段的數量太多時,對伺服器的資源例如檔案控制代碼的消耗會非常大。
  • 在查詢的結果中包含所有的結果集,需要排除被標記刪除的舊資料,這增加了查詢的負擔。

段(Segment)的引入

在全文檢索的早些時候,會為整個文件集合建立一個大索引,並且寫入磁碟。只有新的索引準備好了,它就會替代舊的索引,最近的修改才可以被檢索。這無疑是低效的。

因為索引的不可變性帶來的好處,那如何在保持不可變同時更新倒排索引?

答案是,使用多個索引。不是重寫整個倒排索引,而是增加額外的索引反映最近的變化。每個倒排索引都可以按順序查詢,從最老的開始,最後把結果聚合。

這就引入了段 (segment)

  • 新的文件首先寫入記憶體區的索引快取,這時不可檢索。
  • 時不時(預設 1s 一次),記憶體區的索引快取被 refresh 到檔案系統快取(該過程比直接到磁碟代價低很多),成為一個新的段(segment)並被開啟,這時可以被檢索。
  • 新的段提交,寫入磁碟,提交後,新的段加入提交點,快取被清除,等待接收新的文件。

分片下的索引檔案被拆分為多個子檔案,每個子檔案叫作, 每一個段本身都是一個倒排索引,並且段具有不變性,一旦索引的資料被寫入硬碟,就不可再修改。

段被寫入到磁碟後會生成一個提交點,提交點是一個用來記錄所有提交後段資訊的檔案。一個段一旦擁有了提交點,就說明這個段只有讀的許可權,失去了寫的許可權。相反,當段在記憶體中時,就只有寫的許可權,而不具備讀資料的許可權,意味著不能被檢索。

在 Lucene 中的索引(Lucene 索引是 ES 中的分片,ES 中的索引是分片的集合)指的是段的集合,再加上提交點(commit point),如下圖:

在底層採用了分段的儲存模式,使它在讀寫時幾乎完全避免了鎖的出現,大大提升了讀寫效能。

索引檔案分段儲存並且不可修改,那麼新增、更新和刪除如何處理呢?

  • 新增,新增很好處理,由於資料是新的,所以只需要對當前文件新增一個段就可以了。
  • 刪除,由於不可修改,所以對於刪除操作,不會把文件從舊的段中移除,而是通過新增一個 .del檔案(每一個提交點都有一個 .del 檔案),包含了段上已經被刪除的文件。當一個文件被刪除,它實際上只是在.del檔案中被標記為刪除,依然可以匹配查詢,但是最終返回之前會被從結果中刪除。
  • 更新,不能修改舊的段來進行反映文件的更新,其實更新相當於是刪除和新增這兩個動作組成。會將舊的文件在 .del檔案中標記刪除,然後文件的新版本被索引到一個新的段中。可能兩個版本的文件都會被一個查詢匹配到,但被刪除的那個舊版本文件在結果集返回前就會被移除。

延遲寫策略--近實時搜尋--fresh

ES 是怎麼做到近實時全文搜尋?

磁碟是瓶頸。提交一個新的段到磁碟需要fsync操作,確保段被物理地寫入磁碟,即時電源失效也不會丟失資料。但是fsync是昂貴的,嚴重影響效能,當寫資料量大的時候會造成 ES 停頓卡死,查詢也無法做到快速響應。

所以fsync不能在每個文件被索引的時就觸發,需要一種更輕量級的方式使新的文件可以被搜尋,這意味移除fsync

為了提升寫的效能,ES 沒有每新增一條資料就增加一個段到磁碟上,而是採用延遲寫的策略。

每當有新增的資料時,就將其先寫入到記憶體中,在記憶體和磁碟之間是檔案系統快取,當達到預設的時間(1秒鐘)或者記憶體的資料達到一定量時,會觸發一次重新整理(Refresh),將記憶體中的資料生成到一個新的段上並快取到檔案快取系統 上,稍後再被重新整理到磁碟中並生成提交點

這裡的記憶體使用的是ES的JVM記憶體,而檔案快取系統使用的是作業系統的記憶體。新的資料會繼續的被寫入記憶體,但記憶體中的資料並不是以段的形式儲存的,因此不能提供檢索功能。由記憶體重新整理到檔案快取系統的時候會生成了新的段,並將段開啟以供搜尋使用,而不需要等到被重新整理到磁碟。

在 Elasticsearch 中,這種寫入和開啟一個新段的輕量的過程叫做 refresh (即記憶體重新整理到檔案快取系統)。預設情況下每個分片會每秒自動重新整理一次。 這就是為什麼說 Elasticsearch 是近實時的搜尋了:文件的改動不會立即被搜尋,但是會在一秒內可見。

也可以手動觸發 refresh。 POST /_refresh 重新整理所有索引, POST /index/_refresh重新整理指定的索引:

PUT /my_logs
{
    "settings": {
        "refresh_interval": "30s"
    }
}

Tips:儘管重新整理是比提交輕量很多的操作,它還是會有效能開銷。當寫測試的時候,手動重新整理很有用,但是不要在生產環境下每次索引一個文件都去手動重新整理。而且並不是所有的情況都需要每秒重新整理。在使用 Elasticsearch 索引大量的日誌檔案,可能想優化索引速度而不是近實時搜尋,這時可以在建立索引時在 settings中通過調大 refresh_interval="30s" 的值 , 降低每個索引的重新整理頻率,設值時需要注意後面帶上時間單位,否則預設是毫秒。當 refresh_interval=-1時表示關閉索引的自動重新整理。

持久化--flush

沒用fsync同步檔案系統快取到磁碟,不能確保電源失效,甚至正常退出應用後,資料的安全。為了 ES 的可靠性,需要確保變更持久化到磁碟。

雖然通過定時 Refresh 獲得近實時的搜尋,但是 Refresh 只是將資料挪到檔案快取系統,檔案快取系統也是記憶體空間,屬於作業系統的記憶體,只要是記憶體都存在斷電或異常情況下丟失資料的危險。

為了避免丟失資料,Elasticsearch添加了事務日誌(Translog),事務日誌記錄了所有還沒有持久化到磁碟的資料。

有了事務日誌,過程現在如下:

當一個文件被索引,它被加入到記憶體快取,同時加到事務日誌。不斷有新的文件被寫入到記憶體,同時也都會記錄到事務日誌中。這時新資料還不能被檢索和查詢。

當達到預設的重新整理時間或記憶體中的資料達到一定量後,會觸發一次 refresh:

  • 將記憶體中的資料以一個新段形式重新整理到檔案快取系統,但沒有fsync;
  • 段被開啟,使得新的文件可以搜尋;
  • 快取被清除。

隨著更多的文件加入到快取區,寫入日誌,這個過程會繼續。

隨著新文件索引不斷被寫入,當日志資料大小超過 512M 或者時間超過 30 分鐘時,會進行一次全提交:

  • 記憶體快取區的所有文件會寫入到新段中,同時清除快取;
  • 檔案系統快取通過fsync操作flush到硬碟,生成提交點;
  • 事務日誌檔案被刪除,建立一個空的新日誌。

事務日誌記錄了沒有flush到硬碟的所有操作。當故障重啟後,ES 會用最近一次提交點從硬碟恢復所有已知的段,並且從日誌裡恢復所有的操作。

在 ES 中,進行一次提交併刪除事務日誌的操作叫做flush。分片每 30 分鐘,或事務日誌過大會進行一次flush操作。flush API 也可用來進行一次手動flushPOST/ _flush針對所有索引有效,POST /index/_flush則指定的索引:

通常很少需要手動flush,通常自動的就夠了。

總體的流程大致如下:

合併段

由於自動重新整理流程每秒會建立一個新的段 ,這樣會導致短時間內的段數量暴增。而段數目太多會帶來較大的麻煩。每一個段都會消耗檔案控制代碼、記憶體和 cpu 執行週期。更重要的是,每個搜尋請求都必須輪流檢查每個段然後合併查詢結果,所以段越多,搜尋也就越慢。

ES 通過後臺合併段解決這個問題。小段被合併成大段,再合併成更大的段。這時舊的文件從檔案系統刪除的時候,舊的段不會再複製到更大的新段中。合併的過程中不會中斷索引和搜尋。

段合併在進行索引和搜尋時會自動進行,合併程序選擇一小部分大小相似的段,並且在後臺將它們合併到更大的段中,這些段既可以是未提交的也可以是已提交的。

合併結束後老的段會被刪除,新的段被 flush 到磁碟,同時寫入一個包含新段且排除舊的和較小的段的新提交點,新的段被開啟可以用來搜尋。

合併大的段會消耗很多 IO 和 CPU,如果不檢查會影響到搜素效能。預設情況下,ES 會限制合併過程,這樣搜尋就可以有足夠的資源進行。

如何合理分配索引分片

為什麼要考慮副本分片數量?

大多數ElasticSearch使用者在建立索引時通用會考慮一個重要問題是: 我需要建立多少個分片?

分片分配是個很重要的概念, 很多使用者對如何分片都有所疑惑, 當然是為了讓分配更合理. 在生產環境中, 隨著資料集的增長, 不合理的分配策略可能會給系統的擴充套件帶來嚴重的問題。

同時, 這方面的文件介紹也非常少。很多使用者只想要明確的答案而不僅僅一個數字範圍, 甚至都不關心隨意的設定可能帶來的問題。

首先,我們需要了解ES中以下幾個名詞,是做什麼的:

**叢集(cluster): **由一個或多個節點組成, 並通過叢集名稱與其他叢集進行區分

節點(node): 單個ElasticSearch例項. 通常一個節點執行在一個隔離的容器或虛擬機器中

**索引(index): **在ES中, 索引是一組文件的集合(就是我們所說的一個日誌)

**分片(shard): **因為ES是個分散式的搜尋引擎, 所以索引通常都會分解成不同部分, 而這些分佈在不同節點的資料就是分片. ES自動管理和組織分片, 並在必要的時候對分片資料進行再平衡分配, 所以使用者基本上不用擔心分片的處理細節,一個分片預設最大文件數量是20億.

**副本(replica): **ES預設為一個索引建立5個主分片, 並分別為其建立一個副本分片. 也就是說每個索引都由5個主分片成本, 而每個主分片都相應的有一個copy.

對於分散式搜尋引擎來說, 分片及副本的分配將是高可用及快速搜尋響應的設計核心.主分片與副本都能處理查詢請求, 它們的唯一區別在於只有主分片才能處理索引請求.

謹慎分片

副本對搜尋效能非常重要, 同時使用者也可在任何時候新增或刪除副本。額外的副本能給你帶來更大的容量, 更高的呑吐能力及更強的故障恢復能力。

當在ElasticSearch叢集中配置好你的索引後, 你要明白在叢集執行中你無法調整分片設定。既便以後你發現需要調整分片數量, 你也只能新建建立並對資料進行重新索引(reindex)(雖然reindex會比較耗時, 但至少能保證你不會停機)。

主分片的配置與硬碟分割槽很類似, 在對一塊空的硬碟空間進行分割槽時, 會要求使用者先進行資料備份, 然後配置新的分割槽, 最後把資料寫到新的分割槽上。

在分片時,主要考慮資料集的增長趨勢,一定要做到不要過度分片,並不是分片越多越好,從ES社群使用者對這個熱門主題(分片配置)的分享資料來看, 使用者可能認為過度分配是個絕對安全的策略(這裡講的過度分配是指對特定資料集, 為每個索引分配了超出當前資料量(文件數)所需要的分片數)。

稍有富餘是好的, 但過度分配分片卻是大錯特錯. 具體定義多少分片很難有定論, 取決於使用者的資料量和使用方式. 100個分片, 即便很少使用也可能是好的; 而2個分片, 即便使用非常頻繁, 也可能是多餘的.

我們要熟知以下幾點內容:

  1. 每分配一個分片,都會有額外的成本。
  2. 每個分片本質上就是一個Lucene索引,因此會消耗相應的檔案控制代碼,記憶體和CPU資源。
  3. 每個搜尋請求會排程到索引的每個分片中。如果分片分散在不同的節點倒是問題不太。但當分片開始競爭相同的硬體資源時,效能便會逐步下降。
  4. ES使用詞頻統計來計算相關性。當然這些統計也會分配到各個分片上。如果在大量分片上只維護了很少的資料,則將導致最終的文件相關性較差。

大規模資料集場景

如果真的擔心資料的快速增長, 我們建議你多關心這條限制: ElasticSearch推薦的最大JVM堆空間是30~32G, 所以把你的分片最大容量限制為30GB, 然後再對分片數量做合理估算. 例如, 你認為你的資料能達到200GB, 我們推薦你最多分配7到8個分片.

總之, 不要現在就為你可能在三年後才能達到的10TB資料做過多分配. 如果真到那一天, 你也會很早感知到效能變化的.

儘管本部分並未詳細討論副本分片, 但我們推薦你保持適度的副本數並隨時可做相應的增加. 如果你正在部署一個新的環境, 也許你可以參考我們的基於副本的叢集的設計.這個叢集有三個節點組成, 每個分片只分配了副本. 不過隨著需求變化, 你可以輕易的調整副本數量.

對大資料集, 我們非常鼓勵你為索引多分配些分片--當然也要在合理範圍內. 上面講到的每個分片最好不超過30GB的原則依然使用.

不過, 你最好還是能描述出每個節點上只放一個索引分片的必要性. 在開始階段, 一個好的方案是根據你的節點數量按照1.5~3倍的原則來建立分片. 例如:如果你有3個節點, 則推薦你建立的分片數最多不超過9(3x3)個.

隨著資料量的增加,如果你通過叢集狀態API發現了問題,或者遭遇了效能退化,則只需要增加額外的節點即可. ES會自動幫你完成分片在不同節點上的分佈平衡.

再強調一次, 雖然這裡我們暫未涉及副本節點的介紹, 但上面的指導原則依然使用: 是否有必要在每個節點上只分配一個索引的分片. 另外, 如果給每個分片分配1個副本, 你所需的節點數將加倍. 如果需要為每個分片分配2個副本, 則需要3倍的節點數。

總結

再次宣告, 資料分片也是要有相應資源消耗,並且需要持續投入.

當索引擁有較多分片時, 為了組裝查詢結果, ES必須單獨查詢每個分片(當然並行的方式)並對結果進行合併. 所以高效能IO裝置(SSDs)和多核處理器無疑對分片效能會有巨大幫助. 儘管如此, 你還是要多關心資料本身的大小,更新頻率以及未來的狀態. 在分片分配上並沒有絕對的答案.

分片操作

正如上文中提到,建立分片,不超過3倍,這裡有兩個節點,所以我們可以設定6個分片。

#在每個節點上執行,分配1個副本6個分片
[root@elkstack01 ~]# curl -XPUT 10.0.0.51:9200/_template/my_template -d'
{    "template": "*",
     "settings": {
        "index": {
            "number_of_shards": 6,
            "number_of_replicas": 1
        }
     }
}'
 
#返回結果為true,分片成功
{"acknowledged":true}

測試提交資料

驗證索引及頁面詳解

主節點和副本節點的區別

主節點的職責: 統計各node節點狀態資訊、叢集狀態資訊統計、索引的建立和刪除、索引分配的管理、關閉node節點等。

副本節點的職責: 同步資料,等待機會成為Master(當主節點宕機或者重啟時)。

參考

https://juejin.cn/post/6844903849786867720 從原理到應用,Elasticsearch詳解(下)

https://blog.csdn.net/weixin_44558760/article/details/89095770 ELKstack學習【第02篇】Elasticsearch內部分片及分片處理機制介紹

https://blog.csdn.net/zhenwei1994/article/details/94013059 ES基本介紹

https://www.jianshu.com/p/cc06f9adbe82 【ES】ElasticSearch 深入分片

https://blog.csdn.net/xiaolong2230/article/details/90610995 Elasticsearch及其head外掛安裝

https://developer.aliyun.com/article/707279 乾貨 | Elasticsearch索引生命週期管理探索

https://www.jianshu.com/p/217144c71724 ES中的索引生命週期管理