概要
Elasticsearch讓索引創建變得非常簡單,只要索引一條新的數據,索引會自動創建出來,但隨着數據量的增加,我們開始有了索引優化和搜索優化的需求之後,就會發現自動創建的索引在某些方面不能非常完美的適應我們的需求,我們開始考慮手動創建適合我們業務需求的索引。
索引的CRUD
爲了更好地貼切我們的業務數據需求,我們開始更精細的管理我們的索引。
創建索引
創建索引的語法示例如下:
PUT /music
{
"settings": {
"number_of_shards": 3,
"number_of_replicas": 1
},
"mappings": {
"children": {
"properties": {
"name": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword",
"ignore_above": 256
}
}
}
}
}
}
}
settings內的參數
- number_of_shards:每個索引的primary shard數量,索引創建後不可修改。
- number_of_replicas: 每個索引的replica shard的數量,可以隨時修改。
mappings內的參數
- type: 6.3.1版本只允許設置一個type
- properties:類型映射具體信息,索引文檔的字段名稱,類型,分詞器都在裏面指定。
默認Elasticsearch是允許自動創建索引的,生產環境上爲了避免自動索引可能出現的隱患,可以禁止自動創建索引,修改elasticsearch.yml配置文件即可:
action.auto_create_index: false
修改索引
可以單獨修改setting部分和mapping部分,修改setting部分示例如下:
PUT /music/_settings
{
"number_of_replicas": 2
}
如果要修改mapping信息,如給索引新增字段length、likes、content,示例如下:
PUT /music/_mapping/children
{
"properties": {
"length": {
"type": "long"
},
"likes": {
"type": "long"
},
"content": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword",
"ignore_above": 256
}
}
}
}
}
刪除索引
DELETE /music
DELETE /music,content
DELETE /music*
DELETE /_all
DELETE /*
如上命令均可刪除索引,但此操作一定要慎重,反覆確認後再操作,誤刪的後果不可想像,建議刪除操作一定要設置操作權限,另外Elasticsearch可以設置只限定索引名稱進行刪除,不允許通配符或_all刪除大量的索引,作如下設置即可:
action.destructive_requires_name: true
誤刪索引的後果非常嚴重,請在操作權限上加把鎖,寧可麻煩也不要誤刪。
查看索引信息
GET /music
GET /music/_settings
GET /music/_mapping
三條命令可以查看索引的完整信息,只查setting信息,只查mapping信息。
分詞器設置
analysis是索引設置中非常重要的一部分,默認的分詞器我們前面有介紹,有興趣可以翻一下。我們可以爲索引單獨配置特有的分詞器,或者自定義分詞器。
修改分詞器設置
例如,我們爲music索引創建一個新的分詞器,叫做music_std,啓用英文停用詞列表:
PUT /music
{
"settings": {
"analysis": {
"analyzer": {
"music_std": {
"type": "standard",
"stopwords": "_english_"
}
}
}
}
}
此命令只能在創建時候執行,已經存在的索引執行會報錯。
我們對music索引進行分詞器測試:
GET /music/_analyze
{
"analyzer": "music_std",
"text": "get up brightly early in the morning"
}
測試結果是"in","the"這兩個詞已經被正確的移除掉了。
自定義分詞器
Elasticsearch對分詞器的應用設置得非常靈活,用戶可以根據自己的需求靈活定製字符過濾器、分詞器、詞單元過濾器來創建自定義的分詞器。
文檔的分詞過程包含以下幾步:
- 字符過濾器
對字符串進行預處理,如HTML標籤清洗Love --> Love,I & you --> I and you等等。
- 分詞器
把字符串切分成單個的詞條,如英文的按空格和標點切分,中文的按詞語切分,針對不同的語言,有不同的分詞器,有相對簡單的標準分詞器,也有特別複雜的中文分詞器,裏面包含了非常複雜的切分邏輯如:
I Love you --> I/Love/you
我和我的祖國 --> 我/和/我的/祖國
- Token過濾器
將分詞器得到的詞條進一步的處理,如改變詞條(英文詞幹提取loves --> love),刪除無實際意義的詞條(英文的a, and, this,中文的"的","了","嗎"),增加詞條(補充同義詞)
如果我們自定義分詞器,可以從這三個組件入手,可以自行替換。我們舉一個示例:
PUT /music
{
"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"]
}
}
}
}
}
上面示例中我們自定義的分詞器有如下特點:
- 字符過濾器:把&轉換成and,並加上html_strip處理html文本
- token過濾器:將"the","a"作爲停用詞,全部改成小寫
我們對這個分詞器進行測試:
GET /music/_analyze
{
"text": "you & me the love, <a>, HAHA!!",
"analyzer": "my_analyzer"
}
響應的結果:
{
"tokens": [
{
"token": "you",
"start_offset": 0,
"end_offset": 3,
"type": "<ALPHANUM>",
"position": 0
},
{
"token": "and",
"start_offset": 4,
"end_offset": 5,
"type": "<ALPHANUM>",
"position": 1
},
{
"token": "me",
"start_offset": 6,
"end_offset": 8,
"type": "<ALPHANUM>",
"position": 2
},
{
"token": "love",
"start_offset": 13,
"end_offset": 17,
"type": "<ALPHANUM>",
"position": 4
},
{
"token": "haha",
"start_offset": 24,
"end_offset": 28,
"type": "<ALPHANUM>",
"position": 5
}
]
}
可以看到,"the"作爲停用詞被移除了,&變成了"and",html標籤<a>
移除了,HAHA小寫處理後得到haha。
自定義分詞器後,如果需要應用在索引上,需要將它綁定到具體的字段上:
PUT /music/_mapping/children
{
"properties": {
"content": {
"type": "text",
"analyzer": "my_analyzer"
}
}
}
後面只要有新的文檔進行索引,在content字段上都會使用我們自定義的分詞器。
映射對象
root object
映射對象信息是一組JSON結構,最頂層的叫根對象(root object),包括內容如下:
- properties: 索引中每個字段的映射信息。
- metadata:各種元數據信息,以下劃線開頭,如_id,_source,_type。
- settings:設置項信息,如analyzer。
- 其他settings:比如include_in_all
properties
主要是指文檔字段和屬性最重要的三個設置:
- type: 數據類型,如text、date、long等。
- index: 該字段是否需要全文搜索(analyzed),或精準搜索(not_analyzed)或是不支持搜索(no)。
- analyzer: 文檔索引和搜索時的分詞器。
例如節選了以下properties信息:
{
"music": {
"mappings": {
"children": {
"properties": {
"author": {
"type": "text",
"analyzer": "english"
}
}
}
}
}
}
_source
_source字段存儲的內容包含文檔的JSON字符串,_source字段在寫入磁盤前會被壓縮。
_source存儲的內容纔是我們真正關心的數據,我們可以更加方便的完成這些事:
- 查詢的時候可以一次性拿到完整的document,不需要先拿document id,再發送一次請求拿document
- partial update基於_source實現
- reindex時,直接基於_source實現,不需要從數據庫(或者其他外部存儲)查詢數據再修改
- 可以基於_source定製返回field
- debug query更容易,因爲可以直接看到_source
_all
建立索引時將所有field拼接在一起,作爲一個_all field ,沒指定任何field進行搜索時,就是搜索_all field,一般輕量搜索中用得比較多。
如果不需要_all field,可以設置成禁用:
PUT /music/_mapping/children
{
"_all": {"enabled": false}
}
也可以指定某些field不加入_all field
PUT /music/_mapping/children
{
"properties": {
"author": {
"type": "text",
"include_in_all": false
}
}
}
metadata
文檔標識主要的幾個字段:
- _id:文檔ID
- _type:類型名稱,6.x以後一個索引只會有一個type
- _index: 文檔所在的索引名稱
這三個字段是用來標識一個獨一無二的文檔所在的位置信息,從這三個字段我們基本上可以定位出來該文檔存儲在哪個shard中。
動態映射
dynamic屬性
Elasticsearch索引文檔時,如果JSON結構出現新的字段,Elasticsearch會根據dynamic mapping規則來識別字段的數據類型,並自動增加新的字段,如果我們對文檔的JSON結構有較嚴格的規定,這種自動增加字段的行爲,就不是我們期望的操作,我們可以爲properties設置dynamic屬性來決定這種行爲:
- true: 動態添加新的字段
- false:忽略新的字段
- strict: 遇到新的字段,拋出異常
這個dynamic參數可以在任何一層的object中使用,如:
PUT /music
{
"mappings": {
"children": {
"dynamic": "strict",
"properties": {
"name": {
"type": "text"
},
"address": {
"type": "object",
"dynamic": "true"
}
}
}
}
}
如果children下面遇到新字段,就會拋出異常
如果address內部對象中遇到新字段,會動態創建該字段
示例:
# address內部對象增加兩個新字段
PUT /music/children/1
{
"name":"sunshine",
"address": {
"province": "gd",
"city": "sz"
}
}
創建成功,響應如下:
{
"_index": "music",
"_type": "children",
"_id": "1",
"_version": 1,
"result": "created",
"_shards": {
"total": 2,
"successful": 1,
"failed": 0
},
"_seq_no": 0,
"_primary_term": 1
}
# children下直接增加新字段author
PUT /music/children/1
{
"name":"sunshine",
"author":"Johnny Cash"
}
創建失敗,報錯響應如下:
{
"error": {
"root_cause": [
{
"type": "strict_dynamic_mapping_exception",
"reason": "mapping set to strict, dynamic introduction of [author] within [children] is not allowed"
}
],
"type": "strict_dynamic_mapping_exception",
"reason": "mapping set to strict, dynamic introduction of [author] within [children] is not allowed"
},
"status": 400
}
定製dynamic mapping策略
Elasticsearch在運行中遇到新增的字段時,會根據動態映射模板爲新的字段定義類型,但字段類型是根據首次遇到的字段值來定義的,可能會出現誤判的情況。
我們先舉一個反例,假設我們有一個新增的字段remark,裏面的內容是"2019-12-17",是一個日期格式的內容,Elasticsearch會把這個note字段設置成日期格式,但remark字段第二條數據過來的卻是"Comment Submit",這只是一段文本,remark字段已經是日期格式了,第二條保存就會拋出異常。
針對日期檢測,我們可以選擇關閉,如下:
PUT /music
{
"mappings": {
"children": {
"date_detection": false
}
}
}
但我們針對Long類型,Boolean類型的,同樣有這種情況,逐一關閉可行性不高,爲此我們需要使用動態模板配置。
動態模板
使用動態模板(dynamic template),我們可以通過字段名稱或數據類型來應用不同的映射,來定製自己的模板。
例如我們使用字段名稱後綴的方式:
PUT /music
{
"mappings": {
"children": {
"dynamic_templates": [
{ "en": {
"match": "*_en",
"match_mapping_type": "string",
"mapping": {
"type": "text",
"analyzer": "english"
}
}}
]
}
}
}
這個含義是如果字段以_en結尾,那麼類型爲text,analyzer爲english,否則類型爲string,analyzer爲standard。
測試內容:
PUT /music/children/1
{
"content": "you are my sunshine"
}
PUT /music/children/2
{
"content_en": "you are my sunshine"
}
理論上content使用standard分詞器,4個單詞均可被索引到,content_en字段使用english分詞器,are作爲停用詞會被移除掉。
對索引進行搜索可知:
GET /music/children/_search
{
"query": {
"match": {
"content_en": "are"
}
}
}
結果是空
{
"took": 1,
"timed_out": false,
"_shards": {
"total": 5,
"successful": 5,
"skipped": 0,
"failed": 0
},
"hits": {
"total": 0,
"max_score": null,
"hits": []
}
}
使用其他的關鍵詞,或使用content字段,搜索均能出結果,符合預期。
小建議
以上只是動態映射模板的一個小案例,真實生產環境中文檔的複雜度遠高於此,對文檔的結構而言,優先手動創建索引,明確每個字段的含義和數據類型,其次再做通用的動態映射模板,但也需要定時檢查索引下的數據類型,以防出現意外情況。
小結
本篇主要介紹索引的相關知識,包含索引的CRUD、自定義分詞器、映射對象的知識,最後簡單介紹了映射模板的配置,實際生產如果有乃至動態模板配置,肯定遠比這個複雜,這裏僅作拋磚引玉,謝謝。
專注Java高併發、分佈式架構,更多技術乾貨分享與心得,請關注公衆號:Java架構社區
可以掃左邊二維碼添加好友,邀請你加入Java架構社區微信羣共同探討技術