Elasticsearch內容彙總[持續更新]

一、Elasticsearch技術簡介

Elastic本身也是一個分佈式存儲系統,如同其他分佈式系統一樣,我們經常關注的一些特性如下。

  • 數據可靠性:通過分片副本和事務日誌機制保障數據安全
  • 服務可用性:在可用性和一致性的取捨方面,默認情況下Elastic更傾向於可用性,只要主分片可用即可執行寫入操作
  • 一致性:弱一致性。只要主分片寫成功,數據就可能被讀取。因此讀取操作在主分片和副本分片上可能會得到不同的結果
  • 原子性:索引的讀寫、別名更新是原子操作,不會出現中間狀態。但Bulk不是原子操作。不能用來實現事務
  • 擴展性:主副本分片都可以承擔讀請求,分擔系統負載

1.1 Elasticsearch與MySQL的關係

RDBMS Elasticsearch
Table Index(Type)
Row Document
Column Field
Schema Mapping
SQL DSL

1.1.1 Mapping

索引結構

1.1.2 DSL

查詢語句

1.1.3 倒排索引

正排索引: 文檔ID -> 文檔內容
倒排索引:文檔內容 -> 文檔ID

文檔ID 文檔內容
1 Mastering Elasticsearch
2 Elasticsearch Server
3 Elasticsearch Essentials
Term Count DocumentId:Position
Elasticsearch 3 1:1,2:0,3:0
Mastering 1 1:0
Server 1 2:1
Essentials 1 3:1

倒排索引的核心組成

  • 倒排索引包含兩個部分
    • 單詞詞典(Term Dictionary),記錄所有文檔的單詞,記錄單詞到倒排列表的關聯關係
      • 單詞詞典一般比較大,可以通過B+樹或哈希拉鍊法實現,以滿足高性能的插入與查詢
    • 倒排列表(Posting List)記錄了單詞對應的文檔結合,由倒排索引項組成
      • 倒排索引項(Posting)
        • 文檔ID
        • 詞頻TF - 該單詞在文檔中出現的次數,用於相關性評分
        • 位置(Position)- 單詞在文檔中分詞的位置。用於語句搜索(phrase query)
        • 便宜(Offset)- 記錄單詞的開始結束位置,實現高亮顯示
文檔ID 文檔內容
1 Mastering Elasticsearch
2 Elasticsearch Server
3 Elasticsearch Essentials

Posting List

DocId TF Position Offset
1 1 1 <10,23>
2 1 0 <0,13>
3 1 0 <0,13>
  • Elasticsearch的JSON文檔中的每個字段,都有自己的倒排索引
  • 可以指定對某些字段不做索引
    • 優點:節省存儲空間
    • 缺點:字段無法被搜索

1.1.4 Lucene字典數據結構FST

常見的詞典數據結構:

名稱 特點
排序列表Array/List 使用二分法查找,不平衡
HashMap/TreeMap 性能高,內存消耗大,幾乎是原始數組的三倍
Skip List 跳躍表,可快速查找詞語,在Lucene、Redis、HBase等均有實現。相對於TreeMap等結構,特別適合高併發場景
Trie 適合英文詞典,如果系統中存在大量字符串且這些字符串基本沒有公共前綴,則相應的trie樹將非常消耗內存
Double Array Trie 適合做中文詞典,內存佔用小,很多分詞工具均採用此算法
Ternary Search Tree 三叉樹,每一個node有3個節點,兼具省空間和查詢快的優點
Finite State Transducers(FST) 一種有限狀態機,Luncene 4有開源實現,並大量使用

FST數據結構:
插入單詞“cat”、”deep“、”do“、”dog“、”dogs“

1.2 Elasticsearch基本概念

高可用高擴展的分佈式搜索引擎——Elasticsearch

1.2.1 節點

節點即一個Elasticsearch的實例,本質上就是一個Java進程,一臺機器上可以運行多個Elasticsearch進程,但是生產環境一般建議一臺機器上只運行一個Elasticsearch實例

Master-eligible節點和Master節點

  • 每個節點啓動後默認就是一個Master-eligible節點,該類型節點可以參加選主流程,成爲Master節點。
  • 當第一個節點啓動的時候,它會將自己選舉成Master節點
  • 每個節點都保存了集羣的狀態,但是隻有Master節點可以修改集羣的狀態信息(如所有節點的信息、所有的索引以及其相關的Mapping、Setting信息、分片路由信息等)

Data節點和Coordinating節點

  • Data節點

    • 可以保存數據的節點,叫做Data Node。負責保存分片數據。在數據擴展上起到了至關重要的作用。
  • Coordinating節點

    • 負責接收Client的請求,將請求分發到合適的節點,最終把結果彙集到一起
    • 每個節點默認都起到了Coordinating Node的職責
  • Ingest 節點

    • 數據前置處理轉換節點,支持pipeline管道設置
    • 可以使用ingest節點對數據進行過濾、轉換等操作
    • 每個節點默認都起到了該職責,即在文檔進入索引前做預處理

Hot節點和Warm節點

不同硬件配置的Data Node,用來實現Hot & Warm 架構,降低集羣部署成本

分片

又稱爲主分片,用以解決數據水平擴展問題。通過主分片,可以將數據分佈到集羣內的所有節點之上。

  • 一個分片是一個運行Lucene的實例
  • 主分片數在索引創建時指定,後續不允許修改,除非Reindex

副本

用以解決數據高可用問題,是主分片的拷貝。

  • 副本分片數可以動態調整
  • 增加副本數,可以在一定程度上提高服務的可用性(讀取的吞吐)

1.2.2 水平擴展

1.2.3 寫入流程

write
(1)客戶端向NODE1發送寫請求。
(2)NODE1使用文檔ID來確定文檔屬於分片0,通過集羣狀態中的內容路由表信息獲知分片0的主分片位於NODE3,因此請求被轉發到NODE3上。
(3)NODE3上的主分片執行寫操作。如果寫入成功,則它將請求並行轉發到 NODE1和NODE2的副分片上,等待返回結果。當所有的副分片都報告成功,NODE3將向協調節點報告成功,協調節點再向客戶端報告成功。

在客戶端收到成功響應時,意味着寫操作已經在主分片和所有副分片都執行完成。
寫入底層原理
官網translog配置說明

1.2.4 查詢流程

get流程

(1)客戶端向NODE1發送讀請求。
(2)NODE1使用文檔ID來確定文檔屬於分片0,通過集羣狀態中的內容路由表信息獲知分片0有三個副本數據,位於所有的三個節點中,此時它可以將請求發送到任意節點,這裏它將請求轉發到NODE2。
(3)NODE2將文檔返回給 NODE1,NODE1將文檔返回給客戶端。

1.2.5 搜索流程

search
(1)客戶端發送search請求到NODE 3。
(2)NODE 3將查詢請求轉發到索引的每個主分片或副分片中。
(3)每個分片在本地執行查詢,並使用本地的Term/Document Frequency信息進行打分,添加結果到大小爲from + size的本地有序優先隊列中。
(4)每個分片返回各自優先隊列中所有文檔的ID和排序值給協調節點,協調節點合併這些值到自己的優先隊列中,產生一個全局排序後的列表。

1.2.6 動態索引

動態索引

  • 新增加字段
    • Dynamic 設置爲true時,一旦有新增字段的文檔寫入,Mapping也同時被更新
    • Dynamic 設置爲false,Mapping不會被更新,新增字段的數據無法被索引,但是信息會出現在_source中
    • Dynamic 設置爲Strict,文檔寫入失敗
  • 對已有字段,一旦已經有數據寫入,就不再支持修改字段定義
    • Lucene實現的倒排索引,一旦生成後,就不允許修改
  • 如果希望改變字段類型,使用Reindex API,重建索引
    • 因爲如果修改了字段的數據類型,會導致已被索引的數據無法被搜索
    • 如果是增加新的字段,就不會有這樣的影響

1.2.7 數據建模

1.2.7.1 Elasticsearch中處理關聯關係

  • 對象類型
  • 嵌套對象(Nested Object)
  • 父子關聯關係(Parent / Child)
  • 應用端關聯
對象類型

因爲Elasticsearch會把JSON打平(扁平式鍵值對結構),所以能夠搜索到名稱爲”John“,年齡爲31的文檔,因爲這些數據都能夠被搜索到。(對象之間沒有界限)
Object關聯關係

Nested Data Type
  • Nested數據類型:允許對象數組中的對象被獨立索引
  • 使用nested和properties關鍵字,將所有actors索引到多個分隔的文檔
  • 在內部,Nested文檔會被保存在兩個Lucene文檔中,在查詢時做Join處理

如下對對象設置了“nested”類型,則不再能夠搜索到”不正確的數據”了。
Nested

父子關聯關係
  • 對象和Nested對象的侷限性
    • 每次更新,需要重新索引整個對象(包括根對象和嵌套對象)
  • ES提供了類似關係型數據庫中Join的實現。使用Join數據類型實現,可以通過維護Parent/Child的關係,從而分離兩個對象
    • 父文檔和子文檔是兩個獨立的文檔
    • 更新父文檔無需重新索引子文檔。子文檔被添加,更新或者刪除也不會影響到父文檔和其他的子文檔
    • 父文檔和子文檔必須在相同的分片上 -> 確保查詢join的性能
    • 當指定子文檔的時候,必須指定它的父文檔Id -> 使用route參數保證分配到相同分片上

父子文檔

Nested Object Parent / Child
優點 文檔存儲在一起,讀取性能高 父子文檔可以獨立更新
缺點 更新嵌套的子文檔時,需要更新整個文檔 需要額外的內存維護關係。讀取性能相對差
適用場景 子文檔偶爾更新,以查詢爲主 子文檔更新頻繁

2.4 優化手段

2.4.1 深度分頁

2.4.1.1 FROM+SIZE

這種分頁方式,當FROM+SIZE > 10000的時候,Elasticsearch會報錯,因爲這裏它有個默認分頁窗口設置(當然也可以修改,一般不建議修改)。

  • ES天生就是分佈式的。查詢信息的時候需要從多個分片(多臺機器)上拉取數據,並且ES天生就需要滿足排序的需要(按照相關性算分)
  • 當一個查詢: From = 990, Size = 10
    • 會在每個分片上都獲取1000個文檔。然後,通過Coordinating Node聚合所有結果。最後再通過排序選取前1000個文檔
    • 頁數越深,佔用內存越多。爲了避免深度分頁帶來的內存開銷。ES有一個設定,默認限定到10000個文檔。
      • Index.max.result.window

特別注意:如果你的查詢沒有指定from,size的話ES默認會限制爲from,size=0,10。
提示:from是指偏移量,不是第幾頁,與MySQL的limit後的兩個參數一樣。(我就腦瓜子疼了很久,剛開始一直把from當頁碼。。)

2.4.1.2 SearchAfter

  • 避免深度分頁的性能問題,可以實時獲取下一頁文檔信息
    • 不支持指定偏移量
    • 只能繼續向後偏移翻頁
  • 第一步搜索需要指定sort,並且保證值是唯一的(可以通過加入_id保證唯一性)
  • 然後使用上一次查詢的結果集中,最後一個文檔的sort值繼續進行查詢
    SearchAfter
    關鍵點:根據提供的排序屬性排序後的sort值爲依據,向後繼續翻頁。類似於MySQL中,LIMIT 10000,30。我拿到了第9999條數據的id值,然後SELECT * FROM a WHERE id > 9999 LIMIT 30。
    特別注意:SearchAfter這種特性,很顯然不支持跳頁,但是它也是能夠實時向後翻頁的,而接下來介紹的Scoll翻頁方式就不支持實時。

2.4.1.3 ScollAPI

  • 創建一個快照,有新的數據寫入以後,無法被查到
  • 每次查詢後,輸入上一次的Scoll Id

ScollAPI
我理解和SearchAfter類似,一個是通過傳遞上一次的排序值,一個是通過傳遞上一次的Scoll值。不同的是,SearchAfter是實時的,而Scoll方式對翻頁過程中有數據變更是無感知的。

分頁總結

一般不建議深度分頁,儘可能讓業務增加時間範圍,減少搜索範圍,或者說直接使用另外兩種,滾動分頁即可。需要注意的是後兩者對數據變化的感知是不一樣的,具體需要根據場景來選擇分頁方式。
思考:Scoll是快照,即那一瞬間的快照,所以翻頁是在快照中自己玩,對數據的變化無感知了,那麼如果數據量很大,會不會把內存玩脫。

代碼片段

PUT nested_index
{
  "mappings": {
    "properties": {
      "actors" : {
        "type" : "nested",
        "properties": {
          "first_name" : {"type" : "keyword"},
          "last_name" : {"type" : "keyword"}
        }
      },
      "title": {
        "type" : "text",
        "fields" : {"keyword": {"type" : "keyword","ignore_above":256}}
      }
    }
  }
}
PUT nested_index/_doc/1
{
  "title": "Speed",
  "actors": [
    {"first_name": "Keanu","last_name": "Reeves"},
    {"first_name": "Dennis","last_name": "Hopper"}
  ]
}
POST nested_index/_search
{
  "query": {
    "bool": {
      "must": [
        {
          "match": {
            "title": "Speed"
          }
        },
        {
          "nested": {
            "path": "actors",
            "query": {
              "bool": {
                "must": [
                  {"match": {"actors.first_name": "Keanu"}},
                  {"match": {"actors.last_name": "Hopper"}}
                ]
              }
            }
          }
        }
      ]
    }
  }
}


#指定父子關係,父親爲博客“blog”,子爲評論“comment”
PUT parent_child_index
{
  "mappings": {
    "properties": {
      "blog_comments_relation": {
        "type" : "join",
        "relations": { "blog": "comment"}
      },
      "content": {"type":"text"},
      "title":{"type": "keyword"}
    }
  }
}
#索引父文檔,確認自身身份爲“blog”->父文檔
PUT parent_child_index/_doc/blog1
{
  "title" : "Learning Elasticsearch",
  "content": "hello Elasticsearch",
  "blog_comments_relation": {"name":"blog"}
}
#索引子文檔,引用父文檔。
PUT parent_child_index/_doc/comment1?routing=blog1
{
  "comment": "I am learning ELK",
  "username": "Jack",
  "blog_comments_relation": {"name":"comment","parent":"blog1"}
}

PUT parent_child_index/_doc/blog2
{
  "title" : "Learning Elasticsearch",
  "content": "hello Elasticsearch",
  "blog_comments_relation": {"name":"blog"}
}
PUT parent_child_index/_doc/comment2?routing=blog2
{
  "comment": "I am learning ELK too",
  "username": "Bob",
  "blog_comments_relation": {"name":"comment","parent":"blog2"}
}

#查詢所有文檔
POST parent_child_index/_search
{}

#根據Parent Id查詢
POST parent_child_index/_search
{
  "query": {
    "parent_id":{
      "type": "comment",
      "id": "blog2"
    }
  }
}
# Has Child查詢,返回父文檔
POST parent_child_index/_search
{
  "query": {
    "has_child": {
      "type": "comment",
      "query": {
        "match": {
          "username": "Jack"
        }
      }
    }
  }
}
# Has Parent查詢,返回相關子文檔
POST parent_child_index/_search
{
  "query": {
    "has_parent": {
      "parent_type": "blog",
      "query": {
        "match": {
          "title": "Learning Elasticsearch"
        }
      }
    }
  }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章