深入理解ElasticSearch(八):索引管理
索引管理
1、建立一個索引
到目前為止, 我們已經通過索引一篇文件建立了一個新的索引 。這個索引採用的是預設的配置,新的欄位通過動態對映的方式被新增到型別對映。現在我們需要對這個建立索引的過程做更多的控制:我們想要確保這個索引有數量適中的主分片,並且在我們索引任何資料 之前 ,分析器和對映已經被建立好。
為了達到這個目的,我們需要手動建立索引,在請求體裡面傳入設定或型別對映,如下所示:
PUT /my_index { "settings": { ... any settings ... }, "mappings": { "type_one": { ... any mappings ... }, "type_two": { ... any mappings ... }, ... } }
如果你想禁止自動建立索引,你 可以通過在 config/elasticsearch.yml 的每個節點下新增下面的配置:
action.auto_create_index: false
2、刪除一個索引
用以下的請求來 刪除索引:
DELETE /my_index
你也可以這樣刪除多個索引:
DELETE /index_one,index_two
DELETE /index_*
你甚至可以這樣刪除 全部 索引:
DELETE /_all
DELETE /*
3、索引設定
下面是兩個 最重要的設定:
number_of_shards
每個索引的主分片數,預設值是 5 。這個配置在索引建立後不能修改。
number_of_replicas
每個主分片的副本數,預設值是 1 。對於活動的索引庫,這個配置可以隨時修改。 例如,我們可以建立只有 一個主分片,沒有副本的小索引:
PUT /my_temp_index
{
"settings": {
"number_of_shards" : 1,
"number_of_replicas" : 0
}
}
然後,我們可以用 update-index-settings API 動態修改副本數:
PUT /my_temp_index/_settings { "number_of_replicas": 1 }
4、配置分析器
第三個重要的索引設定是 analysis 部分, 用來配置已存在的分析器或針對你的索引建立新的自定義分析器。
在 分析與分析器 ,我們介紹了一些內建的 分析器,用於將全文字串轉換為適合搜尋的倒排索引。
standard 分析器是用於全文欄位的預設分析器, 對於大部分西方語系來說是一個不錯的選擇。 它包括了以下幾點:
- standard 分詞器,通過單詞邊界分割輸入的文字。
- standard 語彙單元過濾器,目的是整理分詞器觸發的語彙單元(但是目前什麼都沒做)。
- lowercase 語彙單元過濾器,轉換所有的語彙單元為小寫。
- stop 語彙單元過濾器,刪除停用詞–對搜尋相關性影響不大的常用詞,如 a , the , and , is 。
預設情況下,停用詞過濾器是被禁用的。如需啟用它,你可以通過建立一個基於 standard 分析器的自定義分析器並設定 stopwords 引數。 可以給分析器提供一個停用詞列表,或者告知使用一個基於特定語言的預定義停用詞列表。
在下面的例子中,我們建立了一個新的分析器,叫做 es_std , 並使用預定義的 西班牙語停用詞列表:
PUT /spanish_docs
{
"settings": {
"analysis": {
"analyzer": {
"es_std": {
"type": "standard",
"stopwords": "_spanish_"
}
}
}
}
}
es_std 分析器不是全域性的–它僅僅存在於我們定義的 spanish_docs 索引中。 為了使用 analyze API來對它進行測試,我們必須使用特定的索引名:
GET /spanish_docs/_analyze?analyzer=es_std
El veloz zorro marrón
簡化的結果顯示西班牙語停用詞 El 已被正確的移除:
{
"tokens" : [
{ "token" : "veloz", "position" : 2 },
{ "token" : "zorro", "position" : 3 },
{ "token" : "marrón", "position" : 4 }
]
}
5、自定義分析器
雖然Elasticsearch帶有一些現成的分析器,然而在分析器上Elasticsearch真正的強大之處在於,你可以通過在一個適合你的特定資料的設定之中組合字元過濾器、分詞器、詞彙單元過濾器來建立自定義的分析器。
在 分析與分析器 我們說過,一個 分析器 就是在一個包裡面組合了三種函式的一個包裝器, 三種函式按照順序被執行:
字元過濾器 字元過濾器 用來 整理 一個尚未被分詞的字串。例如,如果我們的文字是HTML格式的,它會包含像
或者
這樣的HTML標籤,這些標籤是我們不想索引的。我們可以使用 html清除 字元過濾器 來移除掉所有的HTML標籤,並且像把 Á 轉換為相對應的Unicode字元 Á 這樣,轉換HTML實體。
一個分析器可能有0個或者多個字元過濾器。
分詞器 一個分析器 必須 有一個唯一的分詞器。 分詞器把字串分解成單個詞條或者詞彙單元。 標準 分析器裡使用的 標準 分詞器 把一個字串根據單詞邊界分解成單個詞條,並且移除掉大部分的標點符號,然而還有其他不同行為的分詞器存在。
例如, 關鍵詞 分詞器 完整地輸出 接收到的同樣的字串,並不做任何分詞。 空格 分詞器 只根據空格分割文字 。 正則 分詞器 根據匹配正則表示式來分割文字 。
詞單元過濾器 經過分詞,作為結果的 詞單元流 會按照指定的順序通過指定的詞單元過濾器 。
詞單元過濾器可以修改、新增或者移除詞單元。我們已經提到過 lowercase 和 stop 詞過濾器 ,但是在 Elasticsearch 裡面還有很多可供選擇的詞單元過濾器。 詞幹過濾器 把單詞 遏制 為 詞幹。 ascii_folding 過濾器移除變音符,把一個像 “très” 這樣的詞轉換為 “tres” 。 ngram 和 edge_ngram 詞單元過濾器 可以產生 適合用於部分匹配或者自動補全的詞單元。
在 深入搜尋 ,我們討論了在哪裡使用,以及怎樣使用分詞器和過濾器。但是首先,我們需要解釋一下怎樣建立自定義的分析器。
建立一個自定義分析器 和我們之前配置 es_std 分析器一樣,我們可以在 analysis 下的相應位置設定字元過濾器、分詞器和詞單元過濾器:
PUT /my_index
{
"settings": {
"analysis": {
"char_filter": { ... custom character filters ... },
"tokenizer": { ... custom tokenizers ... },
"filter": { ... custom token filters ... },
"analyzer": { ... custom analyzers ... }
}
}
}
作為示範,讓我們一起來建立一個自定義分析器吧,這個分析器可以做到下面的這些事:
- 1、使用 html清除 字元過濾器移除HTML部分。
- 2、使用一個自定義的 對映 字元過濾器把 & 替換為 ” 和 ” :
"char_filter": {
"&_to_and": {
"type": "mapping",
"mappings": [ "&=> and "]
}
}
- 3、使用 標準 分詞器分詞。
- 4、小寫詞條,使用 小寫 詞過濾器處理。
- 5、使用自定義 停止 詞過濾器移除自定義的停止詞列表中包含的詞:
"filter": {
"my_stopwords": {
"type": "stop",
"stopwords": [ "the", "a" ]
}
}
我們的分析器定義用我們之前已經設定好的自定義過濾器組合了已經定義好的分詞器和過濾器:
"analyzer": {
"my_analyzer": {
"type": "custom",
"char_filter": [ "html_strip", "&_to_and" ],
"tokenizer": "standard",
"filter": [ "lowercase", "my_stopwords" ]
}
}
彙總起來,完整的 建立索引 請求 看起來應該像這樣:
PUT /my_index
{
"settings": {
"analysis": {
"char_filter": {
"&_to_and": {
"type": "mapping",
"mappings": [ "&=> and "]
}},
"filter": {
"my_stopwords": {
"type": "stop",
"stopwords": [ "the", "a" ]
}},
"analyzer": {
"my_analyzer": {
"type": "custom",
"char_filter": [ "html_strip", "&_to_and" ],
"tokenizer": "standard",
"filter": [ "lowercase", "my_stopwords" ]
}}
}}}
索引被建立以後,使用 analyze API 來 測試這個新的分析器:
GET /my_index/_analyze?analyzer=my_analyzer
The quick & brown fox
下面的縮略結果展示出我們的分析器正在正確地執行:
{
"tokens" : [
{ "token" : "quick", "position" : 2 },
{ "token" : "and", "position" : 3 },
{ "token" : "brown", "position" : 4 },
{ "token" : "fox", "position" : 5 }
]
}
這個分析器現在是沒有多大用處的,除非我們告訴 Elasticsearch在哪裡用上它。我們可以像下面這樣把這個分析器應用在一個 string 欄位上:
PUT /my_index/_mapping/my_type
{
"properties": {
"title": {
"type": "string",
"analyzer": "my_analyzer"
}
}
}
6、型別和對映
型別 在 Elasticsearch 中表示一類相似的文件。 型別由 名稱 —比如 user 或 blogpost —和 對映 組成。
對映, 就像資料庫中的 schema ,描述了文件可能具有的欄位或 屬性 、 每個欄位的資料型別—比如 string, integer 或 date —以及Lucene是如何索引和儲存這些欄位的。
型別可以很好的抽象劃分相似但不相同的資料。但由於 Lucene 的處理方式,型別的使用有些限制。
6.1、Lucene 如何處理文件
在 Lucene 中,一個文件由一組簡單的鍵值對組成。 每個欄位都可以有多個值,但至少要有一個值。 類似的,一個字串可以通過分析過程轉化為多個值。Lucene 不關心這些值是字串、數字或日期–所有的值都被當做 不透明位元組 。
當我們在 Lucene 中索引一個文件時,每個欄位的值都被新增到相關欄位的倒排索引中。你也可以將未處理的原始資料 儲存 起來,以便這些原始資料在之後也可以被檢索到。
6.2、型別是如何實現的
Elasticsearch 型別是 以 Lucene 處理文件的這個方式為基礎來實現的。一個索引可以有多個型別,這些型別的文件可以儲存在相同的索引中。
Lucene 沒有文件型別的概念,每個文件的型別名被儲存在一個叫 _type 的元資料欄位上。 當我們要檢索某個型別的文件時, Elasticsearch 通過在 _type 欄位上使用過濾器限制只返回這個型別的文件。
Lucene 也沒有對映的概念。 對映是 Elasticsearch 將複雜 JSON 文件 對映 成 Lucene 需要的扁平化資料的方式。
例如,在 user 型別中, name 欄位的對映可以宣告這個欄位是 string 型別,並且它的值被索引到名叫 name 的倒排索引之前,需要通過 whitespace 分詞器分析:
"name": {
"type": "string",
"analyzer": "whitespace"
}
6.3、避免型別陷阱
這導致了一個有趣的思想實驗: 如果有兩個不同的型別,每個型別都有同名的欄位,但對映不同(例如:一個是字串一個是數字),將會出現什麼情況?
簡單回答是,Elasticsearch 不會允許你定義這個對映。當你配置這個對映時,將會出現異常。
詳細回答是,每個 Lucene 索引中的所有欄位都包含一個單一的、扁平的模式。一個特定欄位可以對映成 string 型別也可以是 number 型別,但是不能兩者兼具。因為型別是 Elasticsearch 新增的 優於 Lucene 的額外機制(以元資料 _type 欄位的形式),在 Elasticsearch 中的所有型別最終都共享相同的對映。
以 data 索引中兩種型別的對映為例:
{
"data": {
"mappings": {
"people": {
"properties": {
"name": {
"type": "string",
},
"address": {
"type": "string"
}
}
},
"transactions": {
"properties": {
"timestamp": {
"type": "date",
"format": "strict_date_optional_time"
},
"message": {
"type": "string"
}
}
}
}
}
}
每個型別定義兩個欄位 (分別是 "name"/"address"
和 "timestamp"/"message"
)。它們看起來是相互獨立的,但在後臺 Lucene 將建立一個對映,如:
{
"data": {
"mappings": {
"_type": {
"type": "string",
"index": "not_analyzed"
},
"name": {
"type": "string"
}
"address": {
"type": "string"
}
"timestamp": {
"type": "long"
}
"message": {
"type": "string"
}
}
}
}
注: 這不是真實有效的對映語法,只是用於演示
對於整個索引,對映在本質上被 扁平化 成一個單一的、全域性的模式。這就是為什麼兩個型別不能定義衝突的欄位:當對映被扁平化時,Lucene 不知道如何去處理。
6.4、型別結論
那麼,這個討論的結論是什麼?技術上講,多個型別可以在相同的索引中存在,只要它們的欄位不衝突(要麼因為欄位是互為獨佔模式,要麼因為它們共享相同的欄位)。
重要的一點是: 型別可以很好的區分同一個集合中的不同細分。在不同的細分中資料的整體模式是相同的(或相似的)。
型別不適合 完全不同型別的資料 。如果兩個型別的欄位集是互不相同的,這就意味著索引中將有一半的資料是空的(欄位將是 稀疏的 ),最終將導致效能問題。在這種情況下,最好是使用兩個單獨的索引。
總結:
- 正確: 將 kitchen 和 lawn-care 型別放在 products 索引中, 因為這兩種型別基本上是相同的模式
- 錯誤: 將 products 和 logs 型別放在 data 索引中, 因為這兩種型別互不相同。應該將它們放在不同的索引中。
7、根物件
對映的最高一層被稱為 根物件 ,它可能包含下面幾項:
- 一個 properties 節點,列出了文件中可能包含的每個欄位的對映 各種元資料欄位,它們都以一個下劃線開頭,例如 _type 、 _id 和 _source
- 設定項,控制如何動態處理新的欄位,例如 analyzer 、 dynamic_date_formats 和 dynamic_templates
- 其他設定,可以同時應用在根物件和其他 object 型別的欄位上,例如 enabled 、 dynamic 和 include_in_all
7.1、屬性
我們已經在 核心簡單域型別 和 複雜核心域型別 章節中介紹過文件欄位和屬性的三個 最重要的設定:
- type 欄位的資料型別,例如 string 或 date
- index 欄位是否應當被當成全文來搜尋( analyzed ),或被當成一個準確的值( not_analyzed ),還是完全不可被搜尋( no )
- analyzer 確定在索引和搜尋時全文欄位使用的 analyzer
我們將在本書的後續部分討論其他欄位型別,例如 ip 、 geo_point 和 geo_shape 。
7.2、元資料: _source 欄位
預設地,Elasticsearch 在 _source 欄位儲存代表文件體的JSON字串。和所有被儲存的欄位一樣, _source 欄位在被寫入磁碟之前先會被壓縮。
這個欄位的儲存幾乎總是我們想要的,因為它意味著下面的這些:
- 搜尋結果包括了整個可用的文件——不需要額外的從另一個的資料倉庫來取文件。
- 如果沒有 _source 欄位,部分 update 請求不會生效。
- 當你的對映改變時,你需要重新索引你的資料,有了_source欄位你可以直接從Elasticsearch這樣做,而不必從另一個(通常是速度更慢的)資料倉庫取回你的所有文件。
- 當你不需要看到整個文件時,單個欄位可以從 _source 欄位提取和通過 get 或者 search 請求返回。
- 除錯查詢語句更加簡單,因為你可以直接看到每個文件包括什麼,而不是從一列id猜測它們的內容。
然而,儲存 _source 欄位的確要使用磁碟空間。如果上面的原因對你來說沒有一個是重要的,你可以用下面的對映禁用 _source 欄位:
PUT /my_index
{
"mappings": {
"my_type": {
"_source": {
"enabled": false
}
}
}
}
在一個搜尋請求裡,你可以通過在請求體中指定 _source 引數,來達到只獲取特定的欄位的效果:
GET /_search
{
"query": { "match_all": {}},
"_source": [ "title", "created" ]
}
這些欄位的值會從 _source 欄位被提取和返回,而不是返回整個 _source 。
7.3、元資料: _all 欄位
在 輕量 搜尋 中,我們介紹了 _all 欄位:一個把其它欄位值 當作一個大字串來索引的特殊欄位。 query_string 查詢子句(搜尋 ?q=john )在沒有指定欄位時預設使用 _all 欄位。
_all 欄位在新應用的探索階段,當你還不清楚文件的最終結構時是比較有用的。你可以使用這個欄位來做任何查詢,並且有很大可能找到需要的文件:
GET /_search
{
"match": {
"_all": "john smith marketing"
}
}
隨著應用的發展,搜尋需求變得更加明確,你會發現自己越來越少使用 _all 欄位。 _all 欄位是搜尋的應急之策。通過查詢指定欄位,你的查詢更加靈活、強大,你也可以對相關性最高的搜尋結果進行更細粒度的控制。如果你不再需要 _all 欄位,你可以通過下面的對映來禁用:
PUT /my_index/_mapping/my_type
{
"my_type": {
"_all": { "enabled": false }
}
}
通過 include_in_all 設定來逐個控制欄位是否要包含在 _all 欄位中,預設值是 true。在一個物件(或根物件)上設定
include_in_all 可以修改這個物件中的所有欄位的預設行為。
你可能想要保留 _all 欄位作為一個只包含某些特定欄位的全文欄位,例如只包含 title,
overview,
summary 和 tags。 相對於完全禁用
_all 欄位,你可以為所有欄位預設禁用 include_in_all 選項,僅在你選擇的欄位上啟用:
PUT /my_index/my_type/_mapping
{
"my_type": {
"include_in_all": false,
"properties": {
"title": {
"type": "string",
"include_in_all": true
},
...
}
}
}
記住,_all 欄位僅僅是一個 經過分詞的 string 欄位。它使用預設分詞器來分析它的值,不管這個值原本所在欄位指定的分詞器。就像所有 string 欄位,你可以配置 _all 欄位使用的分詞器:
PUT /my_index/my_type/_mapping
{
"my_type": {
"_all": { "analyzer": "whitespace" }
}
}
7.4、元資料:文件標識
文件標識與四個元資料欄位 相關:
_id 文件的 ID 字串 _type 文件的型別名 _index 文件所在的索引 _uid _type 和 _id 連線在一起構造成 type#id 預設情況下, _uid 欄位是被儲存(可取回)和索引(可搜尋)的。 _type 欄位被索引但是沒有儲存, _id 和 _index 欄位則既沒有被索引也沒有被儲存,這意味著它們並不是真實存在的。
儘管如此,你仍然可以像真實欄位一樣查詢 _id 欄位。Elasticsearch 使用 _uid 欄位來派生出 _id 。 雖然你可以修改這些欄位的 index 和 store 設定,但是基本上不需要這麼做。
8、動態對映
當 Elasticsearch 遇到文件中以前 未遇到的欄位,它用 dynamic mapping 來確定欄位的資料型別並自動把新的欄位新增到型別對映。
有時這是想要的行為有時又不希望這樣。通常沒有人知道以後會有什麼新欄位加到文件,但是又希望這些欄位被自動的索引。也許你只想忽略它們。如果Elasticsearch是作為重要的資料儲存,可能就會期望遇到新欄位就會丟擲異常,這樣能及時發現問題。
幸運的是可以用 dynamic 配置來控制這種行為 ,可接受的選項如下:
true 動態新增新的欄位–預設 false 忽略新的欄位 strict 如果遇到新欄位丟擲異常 配置引數 dynamic 可以用在根 object 或任何 object 型別的欄位上。你可以將 dynamic 的預設值設定為 strict , 而只在指定的內部物件中開啟它, 例如:
PUT /my_index
{
"mappings": {
"my_type": {
"dynamic": "strict",
"properties": {
"title": { "type": "string"},
"stash": {
"type": "object",
"dynamic": true
}
}
}
}
}
使用上述動態對映, 你可以給 stash 物件新增新的可檢索的欄位:
PUT /my_index/my_type/1
{
"title": "This doc adds a new field",
"stash": { "new_field": "Success!" }
}
但是對根節點物件 my_type 進行同樣的操作會失敗:
PUT /my_index/my_type/1
{
"title": "This throws a StrictDynamicMappingException",
"new_field": "Fail!"
}
9、預設對映
通常,一個索引中的所有型別共享相同的欄位和設定。 default 對映更加方便地指定通用設定,而不是每次建立新型別時都要重複設定。 default 對映是新型別的模板。在設定 default 對映之後建立的所有型別都將應用這些預設的設定,除非型別在自己的對映中明確覆蓋這些設定。
例如,我們可以使用 default 對映為所有的型別禁用 _all 欄位, 而只在 blog 型別啟用:
PUT /my_index
{
"mappings": {
"_default_": {
"_all": { "enabled": false }
},
"blog": {
"_all": { "enabled": true }
}
}
}
default 對映也是一個指定索引 dynamic templates 的好方法。
10、重新索引你的資料
儘管可以增加新的型別到索引中,或者增加新的欄位到型別中,但是不能新增新的分析器或者對現有的欄位做改動。 如果你那麼做的話,結果就是那些已經被索引的資料就不正確, 搜尋也不能正常工作。
對現有資料的這類改變最簡單的辦法就是重新索引:用新的設定建立新的索引並把文件從舊的索引複製到新的索引。
欄位 _source 的一個優點是在Elasticsearch中已經有整個文件。你不必從源資料中重建索引,而且那樣通常比較慢。
為了有效的重新索引所有在舊的索引中的文件,用 scroll 從舊的索引檢索批量文件 , 然後用 bulk API 把文件推送到新的索引中。
從Elasticsearch v2.3.0開始, Reindex API 被引入。它能夠對文件重建索引而不需要任何外掛或外部工具。
批量重新索引
同時並行執行多個重建索引任務,但是你顯然不希望結果有重疊。正確的做法是按日期或者時間 這樣的欄位作為過濾條件把大的重建索引分成小的任務:
GET /old_index/_search?scroll=1m
{
"query": {
"range": {
"date": {
"gte": "2014-01-01",
"lt": "2014-02-01"
}
}
},
"sort": ["_doc"],
"size": 1000
}
如果舊的索引持續會有變化,你希望新的索引中也包括那些新加的文件。那就可以對新加的文件做重新索引, 但還是要用日期類欄位過濾來匹配那些新加的文件。
11、索引別名和零停機
在前面提到的,重建索引的問題是必須更新應用中的索引名稱。 索引別名就是用來解決這個問題的!
索引 別名 就像一個快捷方式或軟連線,可以指向一個或多個索引,也可以給任何一個需要索引名的API來使用。別名 帶給我們極大的靈活性,允許我們做下面這些:
- 在執行的叢集中可以無縫的從一個索引切換到另一個索引
- 給多個索引分組 (例如, last_three_months)
- 給索引的一個子集建立 檢視
在後面我們會討論更多關於別名的使用。現在,我們將解釋怎樣使用別名在零停機下從舊索引切換到新索引。
有兩種方式管理別名: _alias 用於單個操作, _aliases 用於執行多個原子級操作。
在本章中,我們假設你的應用有一個叫 my_index 的索引。事實上, my_index 是一個指向當前真實索引的別名。真實索引包含一個版本號: my_index_v1 , my_index_v2 等等。
首先,建立索引 my_index_v1 ,然後將別名 my_index 指向它:
PUT /my_index_v1
PUT /my_index_v1/_alias/my_index
你可以檢測這個別名指向哪一個索引:
GET /*/_alias/my_index
或哪些別名指向這個索引:
GET /my_index_v1/_alias/*
兩者都會返回下面的結果:
{
"my_index_v1" : {
"aliases" : {
"my_index" : { }
}
}
}
然後,我們決定修改索引中一個欄位的對映。當然,我們不能修改現存的對映,所以我們必須重新索引資料。 首先, 我們用新對映建立索引 my_index_v2 :
PUT /my_index_v2
{
"mappings": {
"my_type": {
"properties": {
"tags": {
"type": "string",
"index": "not_analyzed"
}
}
}
}
}
然後我們將資料從 my_index_v1 索引到 my_index_v2 ,下面的過程在 重新索引你的資料 中已經描述過。一旦我們確定文件已經被正確地重索引了,我們就將別名指向新的索引。
一個別名可以指向多個索引,所以我們在新增別名到新索引的同時必須從舊的索引中刪除它。這個操作需要原子化,這意味著我們需要使用 _aliases 操作:
POST /_aliases
{
"actions": [
{ "remove": { "index": "my_index_v1", "alias": "my_index" }},
{ "add": { "index": "my_index_v2", "alias": "my_index" }}
]
}
你的應用已經在零停機的情況下從舊索引遷移到新索引了。