Elasticsearch中的DDL

在上一篇文章中,我們知道client和ES交互的數(shù)據(jù)格式都是json,也知道了ES中的index和type的關(guān)系。那么如何像SQL類數(shù)據(jù)庫定義表結(jié)構(gòu)一樣去定義一個type呢?ES對與數(shù)據(jù)的定義有兩個特點。首先,ES是一種無模式的搜索引擎,也就是說可以并不事先定義數(shù)據(jù)的結(jié)構(gòu)和格式,在數(shù)據(jù)插入的時候ES會動態(tài)的決定結(jié)構(gòu)和數(shù)據(jù)類型。其次,在結(jié)構(gòu)定下來之后,ES只允許增加新的字段和分析器而不支持對已有結(jié)構(gòu)的修改,包括字段類型和已經(jīng)啟用的分析器。只能通過新建一個type,然后使用reindex API,把數(shù)據(jù)重新填充新的type中,最后刪除舊的type。

一個例子

mapping是對于一個document的定義,相當(dāng)于在SQL DB里面定義一個schema。由于是數(shù)據(jù)的傳輸都是JSON格式,所以沒法顯式的區(qū)分出關(guān)鍵字和自定義數(shù)據(jù)。以實際工作中的mapping結(jié)構(gòu)來說,建立一個名叫store的index并包含一個product的type是這樣的,

PUT /store
  {
  *"mappings": {
  "product": {
    *"dynamic": "true",
    *"numeric_detection": false,
    *"_routing": {
      "required": true
    },
    *"dynamic_templates": [
      {
        "strings": {
          *"match_mapping_type": "string",
          *"mapping": {
            *"type": "text"
          }
        }
      },
      {
        "price": {
          *"path_match": "channels.prices.*",
          *"mapping": {
            *"type": "float"
          }
        }
      }
    ],
    *"properties": {
      "search_strong_fuzzy": {
        *"type": "text",
        *"analyzer": "strong_fuzzy_analyzer",
        *"search_analyzer": "standard"
      },
      "systemField": {
        *"properties": {
          "name": {
            *"type": "text",
            *"copy_to": "search_strong_fuzzy"
          },
          "channels": {
            *"type": "nested",
            *"properties": {
              "id": {
               *"type": "long"
             },
             "creationTime": {
               *"type": "date",
               *"format": "strict_date_optional_time||epoch_millis",
               *"index": false
            }
      ...

由于markdown的代碼片段不支持自定義格式,所以就在一些關(guān)鍵詞之前加了一個星號。然后我們來看定義一個index的關(guān)鍵點。

數(shù)據(jù)類型

常見的字段類型

  • boolean
  • date
  • numeric(long, integer, float, double)
  • text, keyword
  • nested
  • object

其中,text和keyword都是表示文本內(nèi)容,區(qū)別在于text會使用分析器建立去分割文本,建立倒排索引。而keyword只把整個字段都當(dāng)做一個整體來參與搜索,而不去做額外的分析。

nested可以看做是不定長的數(shù)組,比如上面的channels,就是channel的集合,每個channel都有id和createTime這樣的屬性。通常把一對多關(guān)系中多的一部分保存在這樣的field中,在查詢時候,需要使用path來指定field的名字才能進行這樣一對多的關(guān)聯(lián)查詢。

object類型是指每個filed還可以定義自己的properties,這些properties的個數(shù)是固定的,可以看做是定長數(shù)組。比如上面的systemFiled,還有一個叫name的field。但是,lucence并沒有這種數(shù)組的結(jié)構(gòu),ES會把這種object平鋪,實際在存儲是product.systemField.name的key。

字段參數(shù)

定義字段的時候除了指定數(shù)據(jù)類型,還可以額外指定一些參數(shù),常用到到參數(shù)有:

  • analyzer: [analyzer] 字段使用的分析器
  • copy_to: [field name] 把字段值復(fù)制一份到另一個字段,經(jīng)常被使用來進行不同字段的數(shù)據(jù)聚合
  • index [true | false]: 是否建立索引
  • fielddata [true | false] 建立完索引之后是否還要把原文放在內(nèi)存中,如果要對該字段進行排序,聚集等訪問形式時,需要設(shè)置為true (注:為了節(jié)省內(nèi)存空間,text字段的fielddata默認是false!!!)
  • format: [format] date類型字段的格式,參考官方文檔

動態(tài)類型確定

ES還支持在數(shù)據(jù)插入的時候自動檢測字段類型,在type的定義里加上,

"dynamic": true

如果只需要把數(shù)字映射成字符串的話,還需要

"numeric_detection": false

動態(tài)類型可能會遇到一些問題,一個是就是在query的時候,參與搜索的字段必須要和查詢的數(shù)據(jù)類型一致,動態(tài)產(chǎn)生的字段容易被忽略,導(dǎo)致在query的時候出錯。還有一個是,在默認情況下,一個string會產(chǎn)生兩個field,一個是keyword一個是text。所以可以定義dynamic的映射模板,

"dynamic_templates": [
  {
    "strings": {
      "match_mapping_type": "string",
      "mapping": {
        "type": "text"
      }
    }
  },
  {
    "price": {
      "path_match": "channels.prices.*",
      "mapping": {
        "type": "float"
      }
    }
  }
]

上面的模板把string變成text類型,把nested字段prices的所有字段都映射成float類型。對于dynamic的string使用模板也是官方推薦的節(jié)省存儲空間的最佳實踐。

索引的關(guān)聯(lián)關(guān)系

在項目的實踐過程中,我發(fā)現(xiàn)ES的長處還是在各種文本搜索能力。但是對于關(guān)聯(lián)查詢,ES就顯示出和SQL DB不同的地方,因為ES不支持index之間的join查詢,所以通常來說在遇到關(guān)聯(lián)關(guān)系,尤其是一對多和多對多關(guān)系時,通常使用兩種方法來建立對應(yīng)關(guān)系。

第一種是上文介紹的nested類型的field,本質(zhì)上是一種冗余,將多的一側(cè)數(shù)據(jù)保存在一的一測上。在查詢的時候使用nested查詢,指定path和主體數(shù)據(jù)做關(guān)聯(lián)。

第二種是把關(guān)聯(lián)的主體拆分成一個index下的多個type,并且在定義時使用_parent指定parent-child關(guān)系,

{
"mappings": {
    "product": {
      "properties": {
        ...
      }
    },
    "sku": {
      "_parent": {
        "type": "product"
      },
      ...
    }
  }
}

上面的例子就是建立了一個index,包含兩個type,product是parent type,sku是child type。在填充數(shù)據(jù)的時候,對于child要指定對應(yīng)的parent是什么,ES內(nèi)部會維護這樣的上下級關(guān)系,在查詢階段,使用has_child 或者has_parent查詢來進行關(guān)聯(lián)。具體的做法在后面說到DML會詳細說。

總的來說,ES建議盡量避免數(shù)據(jù)的關(guān)聯(lián),因為相比普通查詢,關(guān)聯(lián)查詢會非常的慢,其中nested查詢相對比parent-child查詢會快很多,但是parent-child的type結(jié)構(gòu)上清晰一些,實際使用的時候應(yīng)該酌情建模。

分析器

分析器(analyzer)用于分析數(shù)據(jù),建立倒排索引。由三個部分組成:character filter, tokenizer, 和 token filter。

Character filter

character filter接受最原始的字符流,然后做一些基本的過濾,可以去掉一些無意義的字符。

官方提供了三種filter:

  • html strip char filter: 過濾html標(biāo)簽的filter, e.g. <div>food</div> => food
  • mapping char filter: 自定義的字符映射關(guān)系,e.g. 1 => one, 2 => two
  • pattern replace character filter: 自定義的正則模式映射

Tokenizer

tokenizer接受來自于character filter傳遞過來的字符流,然后進行分詞。
es內(nèi)置的tokenizer可以也分成三類:

  • Word oriented tokenizer
  • Partial word oriented tokenizer
  • Structured text tokenizer
    分詞特點看名字就一目了然,下面來介紹一些有特點tokenizer

Standard Tokenizer
默認的tokenizer,使用Unicode文本分割算法進行分詞,會去掉一些跟語言五官的符號,

"The 2 QUICK Brown-Foxes jumped over the lazy dog's bone."
=>
[ The, 2, QUICK, Brown, Foxes, jumped, over, the, lazy, dog's, bone ]

NGram Tokenizer
使用一個滑動的窗口,在字符串的級別上進行滑動進行分詞,窗口可以設(shè)置上限和下限進行伸縮,

"Quick Fox" => [ Q, Qu, u, ui, i, ic, c, ck, k, "k ", " ", " F", F, Fo, o, ox, x ]

上面是使用min_gram: 1max_gram: 2的窗口進行分詞的結(jié)果,可以看到這種tokenizer不以單詞為單位,所以會出現(xiàn)"k "" F"這種情況。

需要注意的是,這種tokenizer會產(chǎn)生很多的分詞,導(dǎo)致索引的存儲空間消耗的非常高,所以在調(diào)優(yōu)的時候需要結(jié)合具體場景優(yōu)化滑動窗口的大小。

Token Filter

Token Filter接受來自Tokenizer處理后的token流,再進行一些加工處理,得到最終的term。

NGram Token Filter
工作原理和上面的NGram Tokenizer類似,區(qū)別在于處理的單位是token,也就是單詞級別。同樣的文本輸入,

"Quick Fox" => [ Q, Qu, u, ui, i, ic, c, ck, k, F, Fo, o, ox, x ]

可以看到它的max_gram參數(shù)只要能保證大于所有輸入的單詞長度就能夠包含所有的單詞內(nèi)容。NGram Token Filter可以用于模糊搜索。

Edge NGram Token Filter
一個變種的NGram,窗口并不滑動,用參數(shù)side控制在文本起始或者終點的一端,另一端擴大進行分詞,

"Quick Fox" => [ Q, Qu, Qui, Quick, F, Fo, Fox ]

由于分詞的建立符合用戶輸入的順序,Edge NGram Token Filter適合于自動完成功能。

Synonym Token Filter
一種提供了同義詞映射的token filter, 同義詞可以聲明在filter的定義中,

"synonym" : {
  "type" : "synonym",
  "synonyms" : [
    "i-pod, i pod => ipod",
    "universe, cosmos"
  ]
}

也可以定義在一個txt文件里,

"filter" : { 
  "synonym" : { 
    "type" : "synonym", 
    "synonyms_path" : "analysis/synonym.txt"
  }
}

這里的path是指和ES中config文件的相對路徑,具體的內(nèi)容格式可以參考:官方手冊。

如果寫成a, b, c => d這樣的形式,則同義詞關(guān)系是單向的,如果想設(shè)定為雙向的,需要在定義filter的時候加上屬性expand: true,或者寫成[a, b, c, d], 那么只要有一個單詞命中,就會同時產(chǎn)生a,b,c,d四個分詞。

自定義分析器

分析器本質(zhì)就是character filter, tokenizer和token filter的組合。ES默認給了一些預(yù)定義的分析器,如果不能滿足應(yīng)用場景的時候,我們需要自己來定義。

具體的規(guī)則是這樣:

  • 至少一個character filter,
  • 一個tokenizer,
  • 至少一個token filters

每個單位也都可以自定義,例如,

PUT my_index

{
  "settings": {
    "analysis": {
      "analyzer": {
        "my_custom_analyzer": {
          "type":      "custom",
          "tokenizer": "standard",
          "char_filter": [
            "html_strip"
          ],
          "filter": [
            "lowercase",
            "asciifolding"
          ]
        }
      }
    }
  }
}

分析器的驗證

ES提供了analyze API去查看分詞的結(jié)果,

GET _analyze

{
  "tokenizer" : "whitespace",
  "filter" : ["lowercase", {"type": "stop", "stopwords": ["a", "is"]}],
  "text" : "This is a test"
}

上面的例子會的到返回:

{
  "tokens": [
    {
      "token": "this",
      "start_offset": 0,
      "end_offset": 4,
      "type": "word",
      "position": 0
    },
    {
      "token": "test",
      "start_offset": 10,
      "end_offset": 14,
      "type": "word",
      "position": 3
    }
  ]
}

中文分詞

上面說到的分析器都是對英文支持的比較好,但是對中文來說,只能分解成一個字一個字的結(jié)果。顯然在中文語義上,我們希望得到中文詞組的分析結(jié)果。ES官方有一個叫smartcn的插件可以支持中文分詞,內(nèi)部實現(xiàn)是基于中科院計算所的ICTCLAS分詞系統(tǒng)。自己測試時候沒有發(fā)現(xiàn)太大問題,但是很多文章都指出smartcn在分詞精度上還是有所欠缺,多數(shù)文章推薦使用ik分析器,github地址,它有兩種分詞模式,一種叫ik_smart,采用最大長度的分詞結(jié)果,

“中華人民共和國” => “中華人民共和國”

還有一種叫ik_max_word,會把所有有意義的詞組都提取出來,

“中華人民共和國” => “中華”,“人民,“共和國”,”“中華人民共和國”

ik還支持自定義的詞典,可以在遠端維護一個詞典,把url配置在ik插件中,每次只要更新詞典而不用重啟ES。

ik的作者還編寫了一個拼音分詞插件。對每個字還會提取拼音和拼音首字母的索引,

“劉德華” => "liu", "de", "hua", "ldh", "劉德華"

其他一些分詞的放在在這篇文章都有介紹。

多語言的處理

如果一個document里面可能包含多種語言,通常有兩種處理方法,
當(dāng)能確定一個字段只會出現(xiàn)一種特定的語言時,根據(jù)語言加上analyzer。
當(dāng)不確定一個字段會出現(xiàn)什么語言的時候,可以使用copy_to復(fù)制到不同的字段里,每一個加上特定語言的analyzer。但是在這種情況下,分詞的結(jié)果包括了其他語言,會對搜索產(chǎn)生干擾。比如,

明天去City吃McDonald

要對中英文分開處理,中文的字段產(chǎn)生的結(jié)果是,

["明天", "去", "city", "吃", "mcdonald"]

英文字段,如果要支持模糊搜索,加上ngram后產(chǎn)生的分詞結(jié)果是,

["明", "天", "去", "ci", "it", "ty", "cit", "ity", "city", "吃", "mc", "cd", 
"do", "on", "na", "al", "ld", "mcd", "cdo", "don", "ona", "nal", "ald", 
"mcdo", "cdon", "dona", "noal", "oald", "mcdon", "cdona", "donal", "onald", 
"mcdonal", "cdonald","mcdonald"]

當(dāng)我們搜索去吃的時候,在中文分詞的環(huán)境下,應(yīng)該是搜不出結(jié)果的,但是英文字段中的中文字詞結(jié)果還是會匹配出來。這里就會產(chǎn)生一個問題,最好能將一種語言只放到一個字段里面去。
我開發(fā)了一個elasticsearch的插件,來實現(xiàn)一個char filter,根據(jù)配置,可以只保留一種語言的字符。
項目地址:https://github.com/stormisover/es-language-char-filter

Index結(jié)構(gòu)的更新

對一個已經(jīng)存在的index,更新它的mapping結(jié)構(gòu)是幾乎做不到的,只有幾種情況是不受限的:

  • 增加一個nested字段的property
  • 向一個已存在的字段添加multi-fileds
  • 改變字段的ignore_above屬性

Reindex

除此之外,只有通過reindex的方式去建立一個新的index,再在上面設(shè)置更新后的mapping結(jié)構(gòu),再通過reindex API將舊的index數(shù)據(jù)導(dǎo)入到新的index上去,導(dǎo)入過程中也可以做一些數(shù)據(jù)修改,

POST _reindex
{
  "source": {
    "index": "my_index_v1"
  },
  "dest": {
    "index": "my_index_v2"
  }
}

確認新的index沒有問題后再把舊的刪除。

默認reindex是同步的過程,當(dāng)數(shù)據(jù)量比較大的時候,reindex的過程會比較長,如果手動發(fā)API call當(dāng)然問題不大,但是在應(yīng)用里面做的時候,比如用Java的RestClient,很可能遇到超時的情況。ES還提供了一個異步的機制,調(diào)用_reindex的時候加上URL參數(shù)wait_for_completion=false,會立即得到返回,

{
  "task": "Vx-HgONLRJaUteegjOFW8Q:199879"
}

這是一個task ID,然后用_task API去定時輪詢這個task的狀態(tài),

GET _tasks/Vx-HgONLRJaUteegjOFW8Q:199879
{
  "completed": false,
  "task": {
    "node": "Vx-HgONLRJaUteegjOFW8Q",
    "id": 199879,
    "type": "transport",
    "action": "indices:data/write/reindex",
    "status": {
      "total": 351000,
      "updated": 0,
      "created": 153000,
      "deleted": 0,
      "batches": 154,
      "version_conflicts": 0,
    },
  },
  "description": "",
  "start_time_in_millis": 1510144926021,
  "running_time_in_nanos": 40072730773,
  "cancellable": true
  }
}

直到得到這個task完成,

{
  "completed": true,
  ...
}

如果這個task被標(biāo)記"cancellable": true的話,當(dāng)它沒有做完的時候,可以調(diào)用POST _task/[taskID]/_cancel去停止這個task,但是已經(jīng)做了的一部分數(shù)據(jù)并不會撤銷。

使用這種異步的task,ES會自動產(chǎn)生一個.task的index,里面保存著每次task的執(zhí)行記錄,使用者根據(jù)需要保留或者刪除當(dāng)異步任務(wù)執(zhí)行結(jié)束。

Alias

為了這樣的過程不對上層應(yīng)用產(chǎn)生影響,一個好的實踐方式是使用alias,

PUT /my_index_v1/_alias/my_index

或者在新建index的時候指定,

PUT /my_index_v1
{
  "aliases" : {
    "my_index" : {}
  },
  ...
}

這樣應(yīng)用里只需要始終使用這個my_index而不用管真實的index是如何更新的。
此外,alias在建立的時候,可以加filter條件,

{
 "my_index_v1" : {
   "aliases" : {
     "2016" : {
       "filter" : {
         "term" : {
           "year" : 2016
         }
       }
     }
   }
 }
}

這樣只會提供2016年的數(shù)據(jù),類似SQL DB中的視圖功能。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容