白日夢的ES筆記三:萬字長文 Elasticsearch基礎概念統一掃盲

賜我白日夢發表於2021-02-15

一、導讀

本篇是白日夢的第三篇ES筆記,前面已經跟大家分享過兩篇ES筆記了,分別是:

ES基礎篇--快速上手ES

ES進階篇--50個檢索、聚合案例


其實這個專題相對來說質量還是比較不錯的,看過前面兩篇文章之後基本上大家可以上手使用ES了,包括對一些花裡花哨的查詢相關的寫法也有所瞭解。然後這一篇文章會和大家調過頭來重新鞏固一下基礎概念上的掃盲。


二、彩蛋福利:賬號借用


三、ES的Index、Shard及擴容機制

首先你看下這個表格(ES6):

Elasticsearch 關係型資料庫
Document
type(ES7中被取消)
index Database

在ES中的Index的地位相當於是MySQL中的database。所以你讓ES幫你儲存資料你總得先建立一個Index吧,如果你手動的定製建立Index,你還可以為Index指定shard。

那什麼是shard呢?下文馬上說。

下面是對Index操作的Case:

# 建立索引
PUT my_index
{ 
  # 設定index的shard資訊
	"settings": {
		"number_of_shards": 3,
		"number_of_replicas": 2
	},
  # 設定index中各個欄位的型別,屬性(下文細講)
  "mapping":{
    ... 
  }
}

# 修改索引
PUT /my_index/_settings
{
  # 只能改number_of_replicas,不能改number_of_shards
  "number_of_replicas":3
}

# 刪除索引
DELETE /my_index
DELETE /my_index1,my_index2
DELETE /my_*
DELETE /_all # 刪掉所有索引

# 如果不想讓ES可以一下子刪除所有索引,可以通過配置檔案設定
elasticsearch.yml
action.destructive_requires_name:true

shard分為primary shard和replica shard ,其中的primary shard可以接受讀/寫請求,replica shard可以接受讀請求,起到一個負載的作用。預設情況下我建立的索引都有: number_of_shards = 5 和number_of_replicas = 1。表示一共有五個primary shard,並且每個primary 都有一個副本。也就是 5+5*1 =10個shard。

但是當你啟動單臺ES例項時,架構其實是下面這樣:

你會發現,其實系統中就有5個shard。不存在上面計算的10個shard。原因是因為ES要求Primary Shard 和它的備份 replica shard不能同時存在於一個Node上。所以你單個Node啟動後,就只有5個primary shard。並且這時你去看叢集的狀態,會發現整個叢集處於yellow狀態,表示叢集整體可用,但是存在replica shard不可用的情況。

然後你會不會好奇,假設我有2個Node(兩個ES例項)組成的ES叢集,你怎樣做,才能讓系統中的Shard是如何負載均衡分佈在兩個Node上呢?

回答:其實你不用操心,ES自己會幫你完成的。當你增加或減少節點時,ES會自動的進行rebalance,使資料平均分散在不同的節點中。

舉個例子:假設你真的又啟動了一個Node,這個Node會自動的加入到上面那個ES中去,自動組成一個有兩個Node的叢集,如果你依然使用的預設配置即:number_of_shards = 5 和 number_of_replicas = 1。這時ES會自動將系統rebalance成下圖這樣:

此時你再去看叢集的狀態,會發現為green。表示叢集中所有shard都可用。

Node2中會存在5個replica shard,他們是Node1中的Primary的備份。每個shard相當於是一個luncene例項,擁有完整的檢索資料、處理請求的能力。所以shard的數量越多,一定意義上意味著ES的吞吐量就越大。

但是你需要注意的是,primary shard的數量是不能改變的,但是它的副本的數量可以改變。

至於為什麼primary shard的數量是不能改變的,下文中的路由原理會說的。

所以當你想對現在有的ES叢集進行擴容的時,就存在兩種選擇:

1、縱向擴容:你不改變叢集的總shard數,然後去買配置更高,儲存更大的機器跑這些shard。

2、橫向擴容:你擴大replica shard的數量,然後去多購置幾個配置低的機器,你只需要寫好配置檔案,再啟動Node,它自己會加入到現有的叢集中。因為每個shard的都能對外提供服務嘛,所以你這樣擴容系統的效能肯定有提升。

根據現在雲伺服器例項的市場行情來看,方案二會更省錢一些。

當然瞭如果你想讓ES叢集有最好的效能,還是使用預設的配置:number_of_shards = 5 和number_of_replicas = 1,這時你需要10臺機器。每個叢集上都啟動一個ES例項,讓這10個例項組建叢集。就像下圖這樣:

這時每個shard都獨享作業系統的所有資源,效能自然會最好。

四、ES支援的核心資料型別

參考官網 https://www.elastic.co/guide/en/elasticsearch/reference/6.2/mapping-types.html

4.1、數字型別

long、integer、short、byte、double、float、half_float、scaled_float

示例:

PUT my_index
{
  "mappings": {
    "_doc": {
      "properties": {
        "number_of_bytes": {
          "type": "integer"
        },
        "time_in_seconds": {
          "type": "float"
        },
        "price": {
          "type": "scaled_float",
          "scaling_factor": 100
        }
      }
    }
  }
}

4.2、日期型別

date

示例:

PUT my_index
{
  "mappings": {
    "_doc": {
      "properties": {
        "birthday	": {
          "type": "date" 
        }
      }
    }
  }
}

PUT my_index/_doc/1
{ "date": "2015-01-01" } 

4.3、boolean型別

string型別的字串可以被ES解釋成boolean。

boolean

示例:

PUT my_index
{
  "mappings": {
    "_doc": {
      "properties": {
        "is_published": {
          "type": "boolean"
        }
      }
    }
  }
}

4.4、二進位制型別

binary

示例

PUT my_index
{
  "mappings": {
    "_doc": {
      "properties": {
        "name": {
          "type": "text"
        },
        "blob": {
          "type": "binary"
        }
      }
    }
  }
}

PUT my_index/_doc/1
{
  "name": "Some binary blob",
  "blob": "U29tZSBiaW5hcnkgYmxvYg==" 
}

4.5、範圍

integer_range、float_range、long_range、double_range、date_range

示例

PUT range_index
{
  "settings": {
    "number_of_shards": 2
  },
  "mappings": {
    "_doc": {
      "properties": {
        "expected_attendees": {
          "type": "integer_range"
        },
        "time_frame": {
          "type": "date_range", 
          "format": "yyyy-MM-dd HH:mm:ss||yyyy-MM-dd||epoch_millis"
        }
      }
    }
  }
}

PUT range_index/_doc/1?refresh
{
  "expected_attendees" : { 
    "gte" : 10,
    "lte" : 20
  },
  "time_frame" : { 
    "gte" : "2015-10-31 12:00:00", 
    "lte" : "2015-11-01"
  }
}

4.6、複雜資料型別

物件型別,巢狀物件型別

示例:

PUT my_index/_doc/1
{ 
  "region": "US",
  "manager": { 
    "age":     30,
    "name": { 
      "first": "John",
      "last":  "Smith"
    }
  }
}

在ES內部這些值被轉換成這種樣式

{
  "region":             "US",
  "manager.age":        30,
  "manager.name.first": "John",
  "manager.name.last":  "Smith"
}

4.7、Geo-type

ES支援地理上的定位點。

PUT my_index
{
  "mappings": {
    "_doc": {
      "properties": {
        "location": {
          "type": "geo_point"
        }
      }
    }
  }
}

PUT my_index/_doc/1
{
  "text": "Geo-point as an object",
  "location": { 
    "lat": 41.12,
    "lon": -71.34
  }
}

PUT my_index/_doc/4
{
  "text": "Geo-point as an array",
  "location": [ -71.34, 41.12 ] 
}

五、精確匹配與全文檢索

精確匹配和全文檢索是ES提供的兩種檢索方式,都不難理解。

5.1、精確匹配:exact value

搜尋時輸入的value必須和目標完全一致才算作命中。

"query": {
	  # match_phrase 短語精確匹配的關鍵字
    # 只有name欄位 完全等於 “白日夢”的doc 才算命中然後返回
		"match_phrase": { 
				"name": "白日夢" 
		 } 
 } 

5.2、全文檢索:full text

全文檢索時存在各種優化處理如下:

  • 縮寫轉換: cn == china
  • 格式轉換 liked == like == likes
  • 大小寫轉換 Tom == tom
  • 同義詞轉換 like == love

示例

GET /_search
{
    "query": {
      	# match是全文檢索的關鍵字
        # 白日夢可以被分詞器分成:白、白日、白日夢
        # 所以當你使用:白、白日、白日夢、我是白日夢、白日夢是我 等等詞條檢索,都可以檢索出結果
        "match" : {
            "name" : "白日夢"
        }
    }
}

六、倒排索引 & 正排索引

6.1、倒排索引 inverted index

其實正排索引和倒排索引都是人們取的名字而已。主要是你理解它是什麼東西就好了。

正排索引:以doc為維度,記錄doc中出現了哪些詞。

倒排索引:以把doc打碎成一個個的詞條,以詞語為維度。記錄它在哪些doc中出現過。

倒排索引要做的事就是將一篇文章通過分詞器打散成很多詞,然後記錄各個詞分別在哪篇doc中出現過。使用者在使用的時候輸入一串搜尋串,這串字串同樣會使用一樣的分詞器打散成很多詞。再拿著這些詞去方才建立的倒排索引中匹配。同時結合相關性得分找到。

假設我們存在這樣兩句話。

doc1 : hello world you and me
doc2 : hi world how are you

建立倒排索引就是這樣

詞條 doc1(*表示出現過) doc2(-表示不曾出現過)
hello * -
world * *
you * *
and * -
me * -
hi - *
how - *
are - *

這時,我們拿著hello world you 來檢索,同樣需要先經過分詞器分詞,然後可以得到分出來的三個單詞:hello、world、you,然後拿著這三個單詞去上面的倒排索引表中找,你可以看到:

  • hello在doc1中出現過。

  • world在doc1、doc2中出現過。

  • you在doc1、doc2中出現過。

最終doc1、doc2都會被檢索出,但是doc1命中了更多的詞,因此doc1得分會更高,排名越靠前。

6.2、正排索引 doc value

doc value 是指所有不分詞的document的field。

在建立索引的時候,一方面會建立倒排索引,以供搜尋用。一方面會建立正排索引,也就是doc values,以供排序,聚合,過濾等操作使用。

正排索引大概長這樣:

document name age
doc1 張三 12
doc2 李四 34

os cache會快取正排索引,以提高訪問doc value的速度。當OS Cache中記憶體大小不夠存放整個正排索引時,doc value中的值會被寫入到磁碟中。

關於效能方面的問題:ES官方建議,大量使用OS Cache來進行快取和提升效能。不建議使用jvm記憶體來快取資料,那樣會導致一定的gc開銷,甚至可能導致oom問題。所以官方的建議給JVM更小的記憶體,給OS Cache更大的記憶體。假如我們的機器64g,只需要給JVM 16g即可。

6.3、禁用doc value

假設我們不使用聚合、排序等操作,為了節省空間,在建立mappings時,可以選擇禁用doc value,不建立正排索引。

PUT /index
{
    "mappings":{
        "my_type":{
            "properties":{
                "my_field":{
                    "type":"text",
                    "doc_values":false # 禁用doc value
                }
            }
        }
    }
}

七、簡述相關性評分

relevance score 相關度評分演算法, 直白說就是算出一個索引中的文字和搜尋文字之間的相似程度。

Elasticsearch使用的是 TF-IDF演算法 (term-frequency / inverser document frequency)。

  • term-frequency: 表示你搜尋的詞條在當前doc中出現的次數,出現的次數越多越相關。
  • inverse document frequency : 表示搜尋文字中的各個詞條在整個index中所有的document中出現的次數,出現的次數越多越不相關。
  • field-length: field長度越長,越不相關。

八、分詞器

ES官網分詞器模組 https://www.elastic.co/guide/en/elasticsearch/reference/6.2/analysis.html

8.1、什麼是分詞器?

我們使用分詞器可以將一段話拆分成一個一個的單詞,甚至可以進一步對分出來的單詞進行詞性的轉換、時態的轉換、單複數的轉換的操作。

為什麼使用分詞器呢?

你想一個doc那麼長,成千上萬字。為了對它進行特徵的提取,分析。就得把它還原成組成它的詞條。這樣會提高檢索時的召回率,讓更多的doc被檢索到。

8.2、分詞器的組成

character filter:

在一段文字在分詞前先進行預處理,比如過濾html標籤, 將特殊符號轉換成123..這種 阿拉伯數字等特殊符號的轉換。

tokenizer:

進行分詞、拆解句子、記錄詞條的位置(在當前doc中佔第幾個位置term position)及順序。

token filter:

進行同義詞的轉換,去除同義詞,單複數的轉換等等。

ES內建的分詞器:

  • standard analyzer(預設)
  • simple analyzer
  • whitespace
  • language analyzer(特定語言的分詞器,English)

另外比較受歡迎的中文分詞器為IK分詞器,這個分詞器的外掛包、安裝方式我都整理成文件了,公眾號後臺回覆:es即可領取。

8.3、修改Index使用的分詞器

PUT /my_index
{
  "settings":{
    "analysis":{
      "analyzer":{
        "es_std":{
          # 指定分詞器的型別是:standard
          "type":"standard",
          # 指定分詞器的停用詞:_english_
          "stopwords":"_english_"
        }
      }
    }
  }
}

九、mapping

9.1、認識mapping

看到這裡你肯定知道了,我們想往ES中寫資料是需要一個index的。其實我們在往ES中PUT資料之前是可以手動建立Mapping,這裡的mapping其實好比你搞一個java類,做一次對資料結構的抽象,比如name 的型別是String,age的型別是Integer。

就好比下面這樣:

PUT my_index
{ 	
  # 指定index的primary shard數量以及 replicas的數量
  “settings”:{
    "number_of_shards":1,
    "number_of_repicas":0
  },
   # 關鍵字,我們手動自定my_index中的mapping
  "mappings": {
    "my_index": { # index的名稱
      "properties": { # 關鍵字,mapping的屬性,欄位
        "my_field1": { # 相當於Java中的   String my_field1
          "type": "text",
           "analyzer":"english"# 指定分詞器,說明這個欄位需要分詞建立倒排索引
        },
        "my_field2": { # 相當於Golang中的 var my_field2 float
          "type": "float",
		      # 指定是否要分詞。analyzed表示要,not_analyzed表示不要
  				"index":"not_analyzed"
        },
        "my_field3": {
          "type": "scaled_float",
          "scaling_factor": 100
        }
      }
    }
  }
}

1、mapping json中包含了諸如propertiesmatadata(_id,_source,_type)settings(analyzer)以及其他的settings。

2、我們把上面的json中的properties部分稱為:root object

3、自己建立mapping一般是為了更好的控制各個欄位的資料型別,包括使用到的分詞器。

4、另外注意:field的mapping只能新增,不能修改。

你也可以在往ES中PUT資料之前不建立任何Mapping,ES會自動為我們生成mapping。就像下面這樣,自動生成的mapping資訊叫做dynamic mapping,下文中我們還會詳細講這個dynamic

PUT my_index/_doc/1
{
  "title": "This is a document"
}

9.2、檢視mapping

# 檢視某個index下的某個type的mapping
GET /index/_mapping/type

# 檢視某個index的mapping
GET /index/_mapping

9.3、dynamic mapping (動態mapping)

就像下面這樣,我們直接往ES中PUT資料,ES在為我們建立index時就會自動生成dynamic mapping。其實用大白話講就是ES自動推斷你往它裡面存的json串的型別。比如下面的"first_name"會被dynamic mapping成string 型別的。

PUT my_index/_doc/1
{
  "first_name": "John"
}

ES使用_type來描述doc欄位的型別,原來我們直接往ES中儲存資料,並沒有指定欄位的型別,原因是ES存在動態型別推斷(ES支援的型別上文中我們也一起看過了,如果不記得闊以再去看一下哈)。預設的mapping中定義了每個field對應的資料型別以及如何進行分詞。

null          --> no field add
true flase    --> boolean
123           --> long
123.123       --> double
1999-11-11    --> date
"hello world" --> string
Object        --> object

9.4、定製dynamic mapping 策略

  • ture: 語法陌生欄位就進行dynamic mapping。
  • false: 遇到陌生欄位就忽略。
  • strict: 遇到預設欄位就報錯。

示例

PUT /my_index/
{
    "mappings":{
        "dynamic":"strict"
    }
}
  • 禁用ES的日期探測的Demo
# 建立mapping並制定:禁用ES的日期探測
PUT my_index
{
  "mappings": {
    "_doc": {
      "date_detection": false
    }
  }
}

# 新增一條doc
PUT my_index/_doc/1 
{
  "create": "1985/12/22"
}

# 檢視doc,結果如下
GET my_index/_doc/1 
{
  "_index": "my_index",
  "_type": "_doc",
  "_id": "1",
  "_version": 1,
  "found": true,
  "_source": {
    "create": "1985/12/22"
  }
}

# 檢視mapping 
GET my_index/_mapping
# 結果如下:
{
  "my_index": {
    "mappings": {
      "_doc": {
        "date_detection": false,
        "properties": {
          "create": {
            # 被任務是text型別
            "type": "text",
            # ES會自動幫你建立的下面的field部分
            # 即 create是text型別,create.ketword是keyword型別
            # keyword型別不會分詞,預設保留前256字元
            "fields": {
              "keyword": {
                "type": "keyword",
                "ignore_above": 256
              }
            }
          }
        }
      }
    }
  }
}
  • 定製日期發現規則
PUT my_index
{
  "mappings": {
    "_doc": {
      "dynamic_date_formats": ["MM/dd/yyyy"]
    }
  }
}

PUT my_index/_doc/1
{
  "create_date": "09/25/2015"
}
  • 定製數字型別的探測規則
PUT my_index
{
  "mappings": {
    "_doc": {
      "numeric_detection": true
    }
  }
}

PUT my_index/_doc/1
{
  "my_float":   "1.0",
  "my_integer": "1" 
}

定製type field

ES中type相當於MySQL的資料表嘛,ES中可以給現存的type新增field。但是不能修改,否則就會報錯。

type在高版本的ES7中被廢棄了,Index的概念依然保留著。

# 建立index:twitter
PUT twitter
{
  "mappings": {
    # user為type
    "user": {
      "properties": {
        "name": {
         # 會被全部檢索
        "type": "text" ,
         # 指定當前field使用 english分詞器
        "analyzer":"english" 
        },
        "user_name": { "type": "keyword" },
        "email": { "type": "keyword" }
      }
    },
    "tweet": {
      "properties": {
        "content": { "type": "text" },
        "user_name": { "type": "keyword" },
        # "tweeted_at": { "type": "date" },
        "tweeted_at": {
        "type": "date" 
         # 通過index設定為當前field  tweeted_at不能被分詞
         "index": "not_analyzeed" 
        }
      }
    }
  }
}

9.5、mapping複雜資料型別在底層的儲存格式

Object型別

# object型別的json
{
    "address":{
        "province":"shandong",
        "city":"qingdao"
    },
    "name":"bairimeng",
    "age":"12"
}

# ES會將上面的json轉換成如下的格式儲存
{
    "name" : [bairimeng],
    "age" : [12],
    "address.province" : [shandong]
    "address.city" : [qingdao]
}

Object陣列型別

# Object陣列型別
{
    "address":[
        {"age":"12","name":"張三"},
        {"age":"12","name":"張三"},
        {"age":"12","name":"張三"}
    ]
}

# ES會將上面的json轉換成如下的格式儲存
{
    "address.age" : [12,12,12],
    "address.name" : [張三,張三,張三]
}

9.6、ES7中廢棄了type的概念

在一開始我們將ElasticSearch的index比作MySQL中的database,將type比作table,其實這種類比是錯誤的。因為在MySQL中不同表之間的列在物理上是沒有關係的,各自佔有自己的空間。

但是在ES中不是這樣,可能type=Student中的name列和type=Teacher中的name列會被lucene認為是同一個field。導致Lucene處理效率下降。

所以在ES7中直接就將type概念廢棄了。

不過你也不用擔心,大部分企業都傾向於使用低版本的ES,比好比你現在用的依然是java8 而不是JDK14。

9.7、認識一些mate-field(後設資料欄位)

這裡說的後設資料欄位指定的是,當你檢索doc時,除了返回的doc本身的資料之外,其他的出現在檢索結果中的資料,我們是需要了解這些欄位都是什麼含義的。如下:

_index , _type , _id , _source , _version

_id

它是document的唯一標識資訊。上圖中我手動指定了id等於1。如果不指定的話,ES會自動為我們生成一個長20個字元的id,ES會保證叢集中的生成的doc id不會發生衝突。 有這種場景,比如你的資料是從MySQL這種資料庫中倒入進ES的,那其實完全可以使用MySQL中的資料行的ID作為doc id。

_index

你可以簡單粗暴的將es的index的地位理解成MYSQL中的資料庫。這裡的後設資料_index被用來標識當前的doc存在於哪個index中。index的命名規範,名稱小寫,不能用下劃線開頭,不能包含逗號。

ES支援跨域index進行檢索

詳情見官網 https://www.elastic.co/guide/en/elasticsearch/reference/6.2/mapping-index-field.html

_type

這個欄位用來標識doc的型別。但它其實是一個邏輯上的劃分。

field中的value在頂層的lucene建立索引的時候,全部使用的opaque bytes型別,不區分型別的lucene是沒有type概念的。

為了方便我們區分出不通doc的型別,於是在document中加了一個_type屬性。

ES會通過_type進行type的過濾和篩選,一個index中是存放的多個type實際上是存放在一起的,因此一個index下,不可能存在多個重名的type。

_version `_version`是doc的版本號,可以用來做併發控制,當一個doc被建立時它的`_version`是1,之後對它的每一次修改,都會使這個版本號+1,哪怕是你將這個doc刪除了,這個doc的版本號也會增加1。

_source

通過這個欄位可以定製我們想要返回欄位。比如說一個type = user型別的doc中存在100個欄位,但是可能前端並不是真的需要這100個欄位,於是我們使用_source去除一些欄位,注意和filter是不一樣的,因為filter不會影響相關性得分。

你可用像下面這樣禁用_source

PUT tweets
{
  "mappings": {
    "_doc": {
      "_source": {
        "enabled": false
      }
    }
  }
}

_all

首先它也是一個後設資料,當我們往ES中插入一條document時。ES會自動的將這個doc中的多個field的值串聯成一個字串,然後用這個作為_all欄位的值並建立索引。當使用者發起檢索卻沒有指定從哪個欄位查詢時,預設就會在這個_all中進行匹配。

_field_names

舉個例子說明這個屬性怎麼用:

首先往index=my_index的索引下灌兩條資料

# Example documents
PUT my_index/_doc/1
{
  "title": "This is a document"
}

PUT my_index/_doc/2?refresh=true
{
  "title": "This is another document",
  "body": "This document has a body"
}

然後像下面這樣使用_field_names檢索,並且指定了欄位=“title”。此時ES會將所有包含title欄位,且title欄位值不為空的doc檢索出來。

GET my_index/_search
{
  "query": {
    "terms": {
      "_field_names": [ "title" ] 
    }
  }
}

禁用_field_names:

PUT tweets
{
  "mappings": {
    "_doc": {
      "_field_names": {
        "enabled": false
      }
    }
  }
}

_routing

下面路由導航中細說。

_uid

在ES6.0中被棄用。

9.8、copy_to

在上一篇文章中跟大家介紹過可以像下面這樣跨越多個欄位搜尋

# dis_max
GET /your_index/your_type/_search
{   
    # 基於 tie_breaker 優化dis_max
    # tie_breaker可以使dis_max考慮其它field的得分影響
    "query": { 
     # 直接取下面多個query中得分最高的query當成最終得分
     # 這也是best field策略
     "dis_max": { 
        "queries":[
           {"match":{"name":"關注"}},
           {"match":{"content":"白日夢"}}
        ],
        "tie_breaker":0.4
     }
    }
} 

# best_field
# 使用multi_match query簡化寫法如下:
GET /your_index/your_type/_search
{    
    "query": { 
       "multi_match":{
           "query":"關注 白日夢",
 					  # 指定檢索的策略 best_fields(因為dis_max就是best field策略)
           "type":"best_fields",
  					# content^2 表示增加權重,相當於:boost2
           "fields":["name","content^2"],
					 "tie_breaker":0.4,
					 "minimum_should_match":3
       }
    }
}

# most_field
GET /your_index/your_type/_search
{    
    # most_fields策略、優先返回命中更多關鍵詞的doc
    # 如下從title、name、content中搜尋包含“賜我白日夢”的doc
    "query": { 
       "multi_match":{
           "query":"賜我白日夢",
 					  # 指定檢索的策略most_fields
           "type":"most_fields",
           "fields":["title","name","content"]
       }
    }
}

針對跨越多個欄位的檢索除了上面的most_field和best_field之外,還可以使用copy_to預處理。

這個copy_to實際上是在允許我們自定義一個_all欄位, ES會將多個欄位的值複製到一個_all中,然後再次檢索時目標欄位就使用我們通過copy_to建立出來的_all新欄位中。

示例:

PUT my_index
{
  "mappings": {
    "_doc": {
      "properties": {
        "first_name": {
          "type": "text",
          # 把當前的first_name copy進full_name欄位中
          "copy_to": "full_name" 
        },
        "last_name": {
          "type": "text",
        	# 把當前的last_name copy進full_name欄位中
          "copy_to": "full_name" 
        },
        "full_name": {
          "type": "text"
        }
      }
    }
  }
}

PUT my_index/_doc/1
{
  "first_name": "John",
  "last_name": "Smith"
}

GET my_index/_search
{
  "query": {
    "match": {
      "full_name": { 
        "query": "John Smith",
        "operator": "and"
      }
    }
  }
}

9.9、Arrays 和 Multi-field

更多內容參見官網 https://www.elastic.co/guide/en/elasticsearch/reference/6.2/mapping-types.html

十、圖解: master的選舉、容錯以及資料的恢復。

https://i.iter01.com/images/f31a2d26ac41fc4b7130b4b0cbe16dc7c788c67fa9fda083928bb54d35c490d0.png

如上圖為初始狀態圖

假如圖上的第一個節點是master節點,並且它掛掉了。那它掛掉後,整個cluster的status會變成red,表示存在資料丟失了叢集不可用。然後叢集會按照下面的步驟恢復:

第一步:完成master的選舉,自動在剩下的節點中選出一個節點當成master節點。

第二步:選出master節點後,這個新的master節點會將P0在第三個節點中存在一個replica shard提升為primary shard,此時cluster 的 status = yellow,表示叢集中的資料是可以被訪問的但是存在部分replica shard不可用。

第三步:重新啟動因為故障當機的node,並且將右邊兩個節點中的資料拷貝到第一個節點中,進行資料的恢復。

十一、ES如何解決併發衝突

ES內部的多執行緒非同步併發修改時,通過_version版本號進行併發控制,每次建立一個document,它的_version內部版本號都是1,以後對這個doc的修改,刪除都會使這個版本號增1。

ES的內部需在Primary shard 和 replica shard之間同步資料,這就意味著多個修改請求其實是亂序的不一定按照先後順序執行。

相關語法:

PUT /index/type/2?version=1{
    "name":"XXX"
}

上面的命令中URL中的存在?version=1,此時,如果存在其他客戶端將id=2的這條記錄修改過,導致id=2的版本號不等於1了,那麼這條PUT語句將會失敗並有相應的錯誤提示。這樣也就規避了併發修改異常。


擴充:

ES也允許你使用自己的維護的版本號來進行併發控制,用法如下:

PUT /index/type/2?version=1&version_type=external

對比兩者的不同:

  • 使用es提供的_version進行版本控制的話,需要你的PUT命令中提供的version == es的維護的version。

  • 新增引數version_type=external之後,假設當前ES中維護的doc版本號是1, 那麼只有當使用者提供的版本號大於1時,PUT才會成功。

十二、路由原理

什麼是資料路由?

一個index被分成了多個shard,文件被隨機的存在某一個分片 上。客戶端一個請求隨機打向index中的一個分片,但是請求的doc可能不存在於這個分片上,於是接受請求的shard會將請求路由到真正儲存資料的shard上,這個過程叫做資料路由。

其中接受到客戶端請求的節點稱為coordinate node(協調節點),比如現在是客戶端想修改服務端的一條訊息,shard A接受到請求了,那麼A就是 coordnate node協調節點。資料儲存在B primary shard 上,那麼協調節點就會將請求路由到B primary shard中,B處理完成後再向 B replica shard同步資料,資料同步完成後,B primary shard響應 coordinate node, 最後協調節點響應客戶端結果。

假如說你每個primary shard有多個存活的replica shard,預設情況下coordinate node會將請求使用round-robin的方式分散到replica shard和這個primary shard上(因為它們的資料是一樣的)

就像下圖這樣:

節點對等的架構

路由演算法,揭開primary_shard數量不可變的面紗

shard = hash(routing) % number_of_primary_shards

公式不復雜,可以將上面的routing當成doc的id。無論是使用者執行的還是自動生成的,反正肯定是唯一的。既然是唯一的,那每次hash得到的結果也是一樣的, 這樣一個唯一的值對主分片的數進行取餘數,得到的結果就會在 0~最大分片數 之間。

你看看上面的路由公式中後半部分使用的是 number_of_primary_shards ,這也是為什麼ES規定,primary shard的數量不能改變,但是replica shard 可以改變的原因。

除了上面說的路由方式,你還可以像下面這樣定製路由規則:比如PUT /index/type/id?routing=user_id ,可以保證這類doc一定被路由到指定的shard上,而且後續進行應用級負載均衡時會批量提升讀取的效能。

像下面這種用法,可以保證你的doc一定被路由到一個shard上,

# 新增一個doc,並制定routing
PUT my_index/_doc/1?routing=user1&refresh=true 
{
  "title": "This is a document"
}

# 通過id+routing獲取你想要的doc
GET my_index/_doc/1?routing=user1

十三、寫一致性及原理

我們在傳送任何一個增刪改查時,都可以帶上一個 consistency 引數,指明我們想要的寫一致性是什麼,如下

PUT /index/type/id?consistency=quorum

有哪些可選引數呢?

  • one:當我們進行寫操作時,只要存在一個primary_shard=active 就能寫入成功。
  • all:cluster中全部shard都為active時,可以寫入成功。
  • quorum(法定的):也是ES的預設值, 要求大部分的replica_shard存活時系統才可用。

quorum數量的計算公式: int((primary+number_of_replicas)/2)+1

算一算,假如我們的叢集中存在三個node,replica=1,那麼cluster中就存在3+3*1=6個shard。

int((3+1)/2)+1 = 3

看計算的結果,只有當quorum=3 即replica_shard=3時,叢集才是可用的。

但是當我們的單機部署時,由於ES不允許同一個server的primary_shard和replica_shard共存,也就是說我們的replica數目為0,為什麼ES依然可以用呢?這是因為ES提供了一種特殊的處理場景,也就是當number_of_replicas>1時,上述檢查叢集是否可用的機制才會生效。

quorum不全時 叢集進入wait()狀態。 預設1分鐘。在等待期間,期望活躍的shard的數量可以增加,到最後都沒有滿足這個數量的話就會timeout。

我們在寫入時也可以使用timeout引數, 比如: PUT /index/type/id?timeout=30通過自己設定超時時間來縮短超時時間預設的超時時間。

相關文章