Elasticsearch 運行時類型 Runtime fields 深入詳解
1、實戰(zhàn)問題
實戰(zhàn)業(yè)務(wù)中,遇到數(shù)據(jù)導(dǎo)入后,但發(fā)現(xiàn)缺少部分必要字段,一般怎么解決?
比如:emotion 代表情感值,取值范圍為:0-1000。
其中:300-700 代表中性;0-300 代表負(fù)面;700-1000 代表正面。
但實際業(yè)務(wù)中,我們需要:中性:0;負(fù)面:-1;正面:1。
如何實現(xiàn)呢?
這時,可能想到的解決方案:
方案一:重新創(chuàng)建索引時添加字段,清除已有數(shù)據(jù)再重新導(dǎo)入數(shù)據(jù)。 方案二:重新創(chuàng)建索引時添加字段,原索引通過 reindex 寫入到新索引。 方案三:提前指定數(shù)據(jù)預(yù)處理,結(jié)合管道 ingest 重新導(dǎo)入或批量更新 update_by_query 實現(xiàn)。 方案四:保留原索引不動,通過script 腳本實現(xiàn)。
方案一、二類似,新加字段導(dǎo)入數(shù)據(jù)即可。
方案三、方案四 我們模擬實現(xiàn)一把。
2、方案三、四實現(xiàn)一把
2.1 方案三 Ingest 預(yù)處理實現(xiàn)
DELETE?news_00001
PUT?news_00001
{
??"mappings":?{
????"properties":?{
??????"emotion":?{
????????"type":?"integer"
??????}
????}
??}
}
POST?news_00001/_bulk
{"index":{"_id":1}}
{"emotion":558}
{"index":{"_id":2}}
{"emotion":125}
{"index":{"_id":3}}
{"emotion":900}
{"index":{"_id":4}}
{"emotion":600}
PUT?_ingest/pipeline/my-pipeline
{
??"processors":?[
????{
??????"script":?{
????????"description":?"Set?emotion?flag?param",
????????"lang":?"painless",
????????"source":?"""
??????????if?(ctx['emotion']?300?&&?ctx['emotion']?>?0)
????????????ctx['emotion_flag']?=?-1;
??????????if?(ctx['emotion']?>=?300?&&?ctx['emotion']?<=?700)
????????????ctx['emotion_flag']?=?0;
??????????if?(ctx['emotion']?>?700?&&?ctx['emotion']?1000)
????????????ctx['emotion_flag']?=?1;
??????????"""
??????}
????}
??]
}POST?news_00001/_update_by_query?pipeline=my-pipeline
{
??"query":?{
????"match_all":?{}
??}
}
方案三的核心:定義了預(yù)處理管道:my-pipeline,管道里做了邏輯判定,對于emotion 不同的取值區(qū)間,設(shè)置 emotion_flag 不同的結(jié)果值。
該方案必須提前創(chuàng)建管道,可以通過寫入時指定缺省管道 default_pipeline 或者結(jié)合批量更新實現(xiàn)。
實際是兩種細(xì)分實現(xiàn)方式:
方式一:udpate_by_query 批量更新。而更新索引尤其全量更新索引是有很大的成本開銷的。 方式二:寫入階段指定預(yù)處理管道,每寫入一條數(shù)據(jù)預(yù)處理一次。
2.2 方案四 script 腳本實現(xiàn)
POST?news_00001/_search
{
??"query":?{
????"match_all":?{}
??},
??"script_fields":?{
????"emotion_flag":?{
??????"script":?{
????????"lang":?"painless",
????????"source":?"if?(doc['emotion'].value?300?&&?doc['emotion'].value>0)?return?-1;?if?(doc['emotion'].value?>=?300?&&?doc['emotion'].value<=700)?return?0;?if?(doc['emotion'].value?>?700?&&?doc['emotion'].value<=1000)?return?1;"
??????}
????}
??}
}
方案四的核心:通過 script_field 腳本實現(xiàn)。
該方案僅是通過檢索獲取了結(jié)果值,該值不能用于別的用途,比如:聚合。
還要注意的是:script_field 腳本處理字段會有性能問題。
兩種方案各有利弊,這時候我們會進(jìn)一步思考:
能不能不改 Mapping、不重新導(dǎo)入數(shù)據(jù),就能得到我們想要的數(shù)據(jù)呢?
早期版本不可以,7.11 版本之后的版本有了新的解決方案——Runtime fields 運行時字段。
3、Runtime fields 產(chǎn)生背景
Runtime fields 運行時字段是舊的腳本字段 script field 的 Plus 版本,引入了一個有趣的概念,稱為“讀取建?!保⊿chema on read)。
有 Schema on read 自然會想到 Schema on write(寫時建模),傳統(tǒng)的非 runtime field 類型 都是寫時建模的,而 Schema on read 則是另辟蹊徑、讀時建模。
這樣,運行時字段不僅可以在索引前定義映射,還可以在查詢時動態(tài)定義映射,并且?guī)缀蹙哂谐R?guī)字段的所有優(yōu)點。
Runtime fields在索引映射或查詢中一旦定義,就可以立即用于搜索請求、聚合、篩選和排序。
4、Runtime fields 解決文章開頭問題
4.1 Runtime fields 實戰(zhàn)求解
PUT?news_00001/_mapping
{
??"runtime":?{
????"emotion_flag_new":?{
??????"type":?"keyword",
??????"script":?{
????????"source":?"if?(doc['emotion'].value?>?0?&&?doc['emotion'].value?300)?emit('-1');?if?(doc['emotion'].value?>=?300?&&?doc['emotion'].value<=700)?emit('0');?if?(doc['emotion'].value?>?700?&&?doc['emotion'].value<=1000)?emit('1');"
??????}
????}
??}
}
GET?news_00001/_search
{
??"fields"?:?["*"]
}
4.2 Runtime fields 核心語法解讀
第一:PUT news_00001/_mapping 是在已有 Mapping 的基礎(chǔ)上 更新 Mapping。
這是更新 Mapping 的方式。實際上,創(chuàng)建索引的同時,指定 runtime field 原理一致。實現(xiàn)如下:
PUT?news_00002
{
??"mappings":?{
????"runtime":?{
??????"emotion_flag_new":?{
????????"type":?"keyword",
????????"script":?{
??????????"source":?"if?(doc['emotion'].value?>?0?&&?doc['emotion'].value?300)?emit('-1');?if?(doc['emotion'].value?>=?300?&&?doc['emotion'].value<=700)?emit('0');?if?(doc['emotion'].value?>?700?&&?doc['emotion'].value<=1000)?emit('1');"
????????}
??????}
????},
????"properties":?{
??????"emotion":?{
????????"type":?"integer"
??????}
????}
??}
}
第二:更新的什么呢?
加了字段,確切的說,加了:runtime 類型的字段,字段名稱為:emotion_flag_new,字段類型為:keyword,字段數(shù)值是用腳本 script 實現(xiàn)的。
腳本實現(xiàn)的什么呢?
當(dāng) emotion 介于 0 到 300 之間時,emotion_flag_new 設(shè)置為 -1 。 當(dāng) emotion 介于 300 到 700 之間時,emotion_flag_new 設(shè)置為 0。 當(dāng) emotion 介于 700 到 1000 之間時,emotion_flag_new 設(shè)置為 1。
第三:如何實現(xiàn)檢索呢?
我們嘗試一下傳統(tǒng)的檢索,看一下結(jié)果。
我們先看一下 Mapping:
{
??"news_00001"?:?{
????"mappings"?:?{
??????"runtime"?:?{
????????"emotion_flag_new"?:?{
??????????"type"?:?"keyword",
??????????"script"?:?{
????????????"source"?:?"if?(doc['emotion'].value?>?0?&&?doc['emotion'].value?300)?emit('-1');?if?(doc['emotion'].value?>=?300?&&?doc['emotion'].value<=700)?emit('0');?if?(doc['emotion'].value?>?700?&&?doc['emotion'].value<=1000)?emit('1');",
????????????"lang"?:?"painless"
??????????}
????????}
??????},
??????"properties"?:?{
????????"emotion"?:?{
??????????"type"?:?"integer"
????????}
??????}
????}
??}
}
多了一個 runtime 類型的字段:emotion_flag_new。
執(zhí)行:
GET?news_00001/_search
返回結(jié)果如下:

執(zhí)行:
GET?news_00001/_search
{
??"query":?{
????"match":?{
??????"emotion_flag_new":?"-1"
????}
??}
}
返回結(jié)果如下:

執(zhí)行:
GET?news_00001/_search
{
??"fields"?:?["*"],
??"query":?{
????"match":?{
??????"emotion_flag_new":?"-1"
????}
??}
}
返回結(jié)果如下:

4.3 Runtime fields 核心語法解讀
為什么加了:field:[*] 才可以返回檢索匹配結(jié)果呢?
因為:Runtime fields 不會顯示在:_source 中,但是:fields API 會對所有 fields 起作用。
如果需要指定字段,就寫上對應(yīng)字段名稱;否則,寫 * 代表全部字段。
4.4 如果不想另起爐灶定義新字段,在原來字段上能實現(xiàn)嗎?
其實上面的示例已經(jīng)完美解決問題了,但是再吹毛求疵一下,在原有字段 emotion 上查詢時實現(xiàn)更新值可以嗎?
實戰(zhàn)一把如下:
POST?news_00001/_search
{
??"runtime_mappings":?{
????"emotion":?{
??????"type":?"keyword",
??????"script":?{
????????"source":?"""
?????????if(params._source['emotion']?>?0?&&?params._source['emotion']?300)?{emit('-1')}
?????????if(params._source['emotion']?>=?300?&&?params._source['emotion']?<=?700)?{emit('0')}
?????????if(params._source['emotion']?>?700?&&?params._source['emotion']?<=?1000)?{emit('1')}
????????"""
??????}
????}
??},
??"fields":?[
????"emotion"
??]
}
返回結(jié)果:

解釋一下:
第一:原來 Mapping 里面 emotion是 integer 類型的。
第二:我們定義的是檢索時類型,mapping 沒有任何變化,但是:檢索時字段類型 emotion 在字段名稱保持不變的前提下,被修改為:keyword 類型。
這是一個非常牛逼的功能?。?!
早期 5.X、6.X 沒有這個功能的時候,實際業(yè)務(wù)中我們的處理思路如下:
步驟一:停掉實時寫入; 步驟二:創(chuàng)建新索引,指定新 Mapping,新增 emotion_flag 字段。 步驟三:恢復(fù)寫入,新數(shù)據(jù)會生效;老數(shù)據(jù) reindex 到新索引,reindex 同時結(jié)合 ingest 腳本處理。
有了 Runtime?field,這種相當(dāng)繁瑣的處理的“苦逼”日子一去不復(fù)回了!
5、Runtime fields 適用場景
比如:日志場景。運行時字段在處理日志數(shù)據(jù)時很有用,尤其是當(dāng)不確定數(shù)據(jù)結(jié)構(gòu)時。
使用了 runtime field,索引大小要小得多,可以更快地處理日志而無需對其進(jìn)行索引。
6、Runtime fields 優(yōu)缺點
優(yōu)點 1:靈活性強(qiáng)
運行時字段非常靈活。主要體現(xiàn)在:
需要時,可以將運行時字段添加到我們的映射中。
不需要時,輕松刪除它們。
刪除操作實戰(zhàn)如下:
PUT?news_00001/_mapping
{
?"runtime":?{
???"emotion_flag":?null
?}
}
也就是說將這個字段設(shè)置為:null,該字段便不再出現(xiàn)在 Mapping 中。
優(yōu)點 2:打破傳統(tǒng)先定義后使用方式
運行時字段可以在索引時或查詢時定義。
由于運行時字段未編入索引,因此添加運行時字段不會增加索引大小,也就是說 Runtime fields 可以降低存儲成本。
優(yōu)點3:能阻止 Mapping 爆炸
Runtime field 不被索引(indexed)和存儲(stored),能有效阻止 mapping “爆炸”。
原因在于 Runtime field 不計算在 ?index.mapping.total_fields 限制里面。
缺點1:對運行時字段查詢會降低搜索速度
對運行時字段的查詢有時會很耗費性能,也就是說,運行時字段會降低搜索速度。
7、Runtime fields 使用建議
權(quán)衡利弊:可以通過使用運行時字段來減少索引時間以節(jié)省 CPU 使用率,但是這會導(dǎo)致查詢時間變慢,因為數(shù)據(jù)的檢索需要額外的處理。
結(jié)合使用:建議將運行時字段與索引字段結(jié)合使用,以便在寫入速度、靈活性和搜索性能之間找到適當(dāng)?shù)钠胶狻?/p>
8、小結(jié)
本文通過實戰(zhàn)中添加字段的問題引出解決問題的幾個方案;傳統(tǒng)的解決方案大多都需要更改 Mapping、重建索引、reindex 數(shù)據(jù)等,相對復(fù)雜。
因而,引申出更為簡單、快捷的 7.11 版本后才有的方案——Runtime?fields。
Runtime?fields 的核心知識點如下:
Mapping 環(huán)節(jié)定義; 在已有 Mapping 基礎(chǔ)上更新; 檢索時使用 runtime fields 達(dá)到動態(tài)添加字段的目的; 覆蓋已有 Mapping 字段類型,保證字段名稱一致的情況下,實現(xiàn)特定用途 優(yōu)缺點、適用場景、使用建議。
你在實戰(zhàn)環(huán)節(jié)使用 Runtime fields 了嗎?效果如何呢?
歡迎留言反饋交流。
參考
推薦
更短時間更快習(xí)得更多干貨!
已帶領(lǐng)70位球友通過 Elastic 官方認(rèn)證!
中國僅通過百余人

