如何設計一個高效能 Elasticsearch mapping

雙子孤狼發表於2021-05-26

前言

在關係型資料庫設計當中,表的設計尤其重要,然而關係型資料庫更關注的表與表之間的關係,以及表的劃分是否合理,而 Elasticsearch 中卻更加關注欄位型別的設計,一個好的欄位型別設計可以更好的利用 Elasticsearch 的搜尋分析特性。

mapping

如果說我們想要用好 Elasticsearch,那麼就必須要先了解 mapping 什麼是 mapping。一句話:mapping是定義如何儲存和索引文件及其包含的欄位的過程。

mapping 能做什麼

前面我們提到,在 Elasticsearch 中,mapping 類似於傳統關係型資料庫的表結構定義,主要做以下幾件事:

  • 定義欄位名稱和欄位型別。
  • 定義倒排索引相關的配置,比如是否被索引,是否可以被分詞等。

mapping 可以分為兩種:Dynamic mappingExplicit mapping

Dynamic mapping

Dynamic mapping 即:動態對映。動態對映顧名思義就是 mapping 會被動態建立,也就是說我們不需要定義 mapping 就可以往一個索引插入資料,插入索引資料之後,Elasticsearch 會根據插入的資料自動推測資料型別,進而動建立 mapping

比如下面就是往一個不存在的索引 index_001 插入一條資料:

PUT index_001/_doc/1
{
  "name":"lonely wolf",
  "age": 18,
  "create_date":"2021-05-19 20:45:11",
  "update_date":"2021-05-23"
}

插入資料之後,執行 GET index_001 來查詢一下索引資訊:

可以發現,這時候索引已經被自動建立了,而且 age 欄位被 Elasticsearch 定義為 long 型別,update_date 被定義為 data 型別,其他兩個欄位則被推測為 text 型別。

Elasticsearch 中自動對映型別規則可以通過 dynamic 引數進行配置,dynamic 型別有 4 種:

dynamic=true

預設值。當設定為 true 時,一旦有新欄位插入文件,則 mapping 會被同步更新。

我們在上面的文件中再插入一個新文件,新文件新增一個 address 欄位:

PUT index_001/_doc/2
{
  "name":"lonely wolf2",
  "age": 20,
  "create_date":"2021-05-23 11:37:11",
  "update_date":"2021-05-23",
  "address":"廣東深圳"
}

然後再檢視一下 mapping,可以看到 mapping 已經新增了一個 address 欄位,mapping 欄位被更新意味著該欄位會加入索引:

dynamic=runtime

這個型別和 true 型別非常相似,但是有一個非常大的區別就是,雖然加入新欄位也會更新 mapping,但是新加入的欄位不會被索引,也就是不會使得索引變大,不過雖然不被索引,但是新加入的欄位依然可以被查詢,只是查詢的代價會更大。所以這種型別一般不建議用在經常查詢的條件欄位上,而更適合用在一些不確定資料結構的日誌類索引中。

修改 dynamic 型別:

PUT index_001/_mapping
{
  "dynamic":  "runtime"
}

新增一個文件,並加入一個新欄位:

PUT index_001/_doc/3
{
  "email":"123@qq.com"
}

最後詢一下 mapping,可以看到欄位屬性是 runtime,而且型別是 keyword

下表就是自動建立 mapping 時,Elasticsearch 的對映關係:

插入資料型別 dynamic=true dynamic=runtime
null 不會新增任何欄位 不會新增任何欄位
true 或 false boolean boolean
double float double
integer long long
object object object
string(通過 date 校驗) date date
string(通過 numreic 校驗) float 或 long double 或 long
string(沒有通過 date 或 numreic 校驗) text ,並且同時會建立一個 keyword 子域 keyword
array 取決於陣列中第一個非 null 值 取決於陣列中第一個非 null 值

PS:keyword 表示 不參與分詞。

dynamic=false

當設定為 false 時,新加入的欄位不會被更新到 mapping,也就是說新欄位不會被索引,故以這個欄位為條件進行搜尋時,無法被搜尋到(這一點要注意和 runtime 型別進行區分),不過雖然無法被索引,但是該欄位會出現在 _source 中。也就是說該欄位不能作為查詢條件,但是能被查詢出來

接下來我們將 dynamic 修改為 false,並新增一個欄位來驗證,可以發現新增的欄位會出現在 _source 中,但是無法作為條件被查詢出來:

dynamic=strict

這種型別最為嚴格,表示不允許新增一個不在 mapping 中的欄位,一旦新增的欄位不在 mapping 定義中,則直接報錯:

是否可以修改 mapping 中的資料型別

Elasticsearch 中,一旦一個欄位被定義在了 mapping 中,是無法被修改的,因為一旦欄位被修改了,就會無法被索引(新增欄位除外),所以一般我們需要修改索引的話,都會重建索引,並採用 reindex 操作來遷移資料。

關閉 dynamic mapping

可以通過以下兩個配置來關閉 dynamic mapping,以下兩個屬性預設值均為 true,如果需要關閉,則需要修改為 false

action.auto_create_index: true
index.mapper.dynamic: true

Explicit mapping

Explicit mapping 即:顯式對映。也就是說這時候我們需要顯示的定義欄位型別。

Elasticsearch 中支援的欄位型別很多,在這裡就舉一些比較常用的欄位型別:

text 型別

這是最常用的一種型別,儲存字串,用於全文索引。當欄位被定義為 text 型別時,預設不能用於聚合,排序等操作:

可以看到,用 text 型別欄位排序彙報湊,如果想要允許這些操作,可以通過設定 fielddata=true,如下

PUT my-index-011/_mapping
{
  "properties": {
    "my_field": { 
      "type":     "text",
      "fielddata": true
    }
  }
}

field 欄位儲存在堆記憶體中,因為其涉及到的計算比較消耗效能,所以一般不建議設定 fielddata=true,而是通過建立一個 keyword 子域來實現(預設方式):

PUT index_111
{
  "mappings": {
    "properties": {
      "my_field": { 
        "type": "text",
        "fields": {
          "keyword": { 
            "type": "keyword"
          }
        }
      }
    }
  }
}

這種定義方式我們可以將一個欄位同時作為 textkeyword 型別使用,如果要用於聚合或者排序等操作則可以使用 欄位名.keyword 來作為欄位名來進行操作:

keyword 型別

這種型別也非常常用,該欄位儲存的資料表示一個整體,不可被分詞,所以一般不會用來定義大本文的全文檢索欄位,而是用來儲存一些結構化的字串,比如:id,郵箱,標籤等。

keyword 型別一般用於聚合,排序等操作。除此之外,該欄位還有兩種衍生型別:constant_keywordwildcard

  • constant_keyword:一般用於定義常量型別,比如一個索引中某一個欄位全部為同一個值,可以定義為這種型別。
  • wildcard:一般用於模糊匹配查詢或者正則匹配查詢。

如下就是一個模糊匹配查詢的示例(可以配合萬用字元使用,類似於關係型資料庫的 like 操作):

GET index_112/_search
{
  "query": {
    "wildcard": {
      "my_wildcard": {
        "value": "*quite*lengthy"
      }
    }
  }
}

date 型別

用於定義日期型別,定義日期型別的同時,可以通過 format 來指定日期的格式:

PUT index_113
{
  "mappings": {
    "properties": {
      "date": {
        "type":   "date",
        "format": "yyyy-MM-dd HH:mm:ss||yyyy-MM-dd||epoch_millis"
      }
    }
  }
}

numeric 型別

Elasticsearch 中提供了比較多的格式用來表示不同長度的數字型別:

數字型別 長度
long 64 位有符號整數。範圍:-2 的 63 次方到 2 的 63 次方 -1
integer 32 位有符號整數。範圍:-2 的 31 次方到 2 的 31 次方 -1
short 16 位有符號整數。範圍:-32768 到 32767
byte 8 位有符號整數。範圍:-128 到 127
double 64 位雙精度小數
float 32 位單精度小數
half_float 16 位單精度小數
scaled_float 帶有縮放因子的浮點數,一般適用於存放金額之類的資料。比如 18.88 元,縮放因子是 100,那麼在索引時會被索引為 1888(即:原值 * 縮放因子)
unsigned_long 64 位無符號整數。範圍:0 到 2 的 64 次方減 1

定義方式如下所示:

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

boolean 型別

布林型別比較簡單,只有 truefalse 兩種:

PUT index_001
{
  "mappings": {
    "properties": {
      "is_published": {
        "type": "boolean"
      }
    }
  }
}

其他型別

除了上面介紹的一些比較常用的資料型別,Elasticsearch 中還有一些高階資料型別:如 Nested(巢狀型別),地理資料型別,ip 型別等。

總結

Elasticsearch 中支援動態 mapping 和顯示 mapping 兩種,在使用中有時候可以先插入一條資料到臨時索引,等自動生成 mapping 之後,在對現有 mapping 進行修改調整,在欄位上尤其要考慮好 text 型別和 keyword 型別的設定,如果需要支援全文搜尋和分詞搜尋,則需要使用 text 型別,需要支援關鍵字模糊搜尋或者聚合排序等操作可以考慮使用 keyword 欄位。

相關文章