問題背景
社區的一個伙伴想對一個 integer 的字段類型添加一個 keyword 類型的子字段,然后進行精確匹配的查詢優化,提高查詢的速度。
整個索引數據量不大,并不想進行 reindex 這樣的復雜操作,就想到了使用 update_by_query 的存量數據更新。
所以我們測試了下面這套方案,在設置完字段的子字段后,利用 set processor 來對這個子字段進行 update_by_query
操作記錄:
# 測試索引
PUT /test
{"mappings": {"properties": {"status": {"type": "integer"}}}
}
# 測試數據
POST /test/_bulk
{"index":{}}
{"status":404}
{"index":{}}
{"status":500}GET test/_search# 添加子字段
PUT test/_mapping
{"properties": {"status": {"type": "integer","fields": {"keyword": {"type": "keyword"}}}}
}GET test/_search#創建管道pipeline.實現更新邏輯
PUT _ingest/pipeline/copy_status_to_keyword
{"description": "resets the value of status and subfields","processors": [{"set": {"field": "status","value": "{{{status}}}"}}]
}#update 執行
POST test/_update_by_query?pipeline=copy_status_to_keyword
{"query": {"bool": {"must_not": {"exists": {"field": "status.keyword"}},"must": {"exists": {"field": "status"}}}}
}GET test/_search
{"query": {"exists": {"field": "status.keyword"}}
}# 返回結果"hits": [{"_index": "test_set","_type": "_doc","_id": "G7zHNpUBLvnTvXTpVIC4","_score": 1,"_source": {"status": "404"}},{"_index": "test_set","_type": "_doc","_id": "HLzHNpUBLvnTvXTpVIC4","_score": 1,"_source": {"status": "500"}}]
測試檢查了一下,status.keyword 可以被 search,可以滿足我們的預期要求。
但是,小伙伴到正式上線的時候卻發生了問題。應用程序讀取發現 _source 中 status 的類型變了,開始報錯字段類型不符合。
# 寫入的時候"hits": [{"_index": "test","_type": "_doc","_id": "2ry5NpUBLvnTvXTp1F5z","_score": 1,"_source": {"status": 404 # 這里還是 integer 類型}},{"_index": "test","_type": "_doc","_id": "27y5NpUBLvnTvXTp1F5z","_score": 1,"_source": {"status": 500}}]# update 完成后"hits": [{"_index": "test","_type": "_doc","_id": "2ry5NpUBLvnTvXTp1F5z","_score": 1,"_source": {"status": "404" # 字段內容添加上了引號,成為了 string 類型}},{"_index": "test","_type": "_doc","_id": "27y5NpUBLvnTvXTp1F5z","_score": 1,"_source": {"status": "500"}}]
解決方案
還好小伙伴那邊有數據主備庫,趕緊做了切換。然后開始對已有的數據進行修復。
最終商定了下面兩個方案進行 fix。
- 用 script 保持數據類型重寫
POST test/_update_by_query
{
"script": {"source": """if (ctx._source.status instanceof String) {ctx._source.status = Integer.parseInt(ctx._source.status);}""","lang": "painless"}
}
- 查詢結果讀取 docvalue 而不是 source。這個方案可以繞過這個問題,但是需要改動應用程序。
GET test/_search
{"_source": false,"docvalue_fields": ["status"]
}# 返回"hits": [{"_index": "test","_type": "_doc","_id": "wLy-NpUBLvnTvXTpRGvw","_score": 1,"fields": {"status": [404]}},{"_index": "test","_type": "_doc","_id": "wby-NpUBLvnTvXTpRGvw","_score": 1,"fields": {"status": [500]}}]
問題分析
好了,現在我們回過頭來分析一下之前方案出現的問題,用 set proceesor 為什么會導致 source 內的字段類型從 int 變成 string 呢?
因為 script 腳本寫法能夠成功,而 set 會失敗,我們從 set 的使用入手,去看看代碼里是不是有什么線索?
set processor 問題的細節
讓我們深入分析值類型轉換的核心代碼路徑:
// SetProcessor.java
document.setFieldValue(field, value, ignoreEmptyValue);
這里的 value 參數類型為 ValueSource,
// SetProcessor.Factory.create()
Object value = ConfigurationUtils.readObject(TYPE, processorTag, config, "value");
ValueSource valueSource = ValueSource.wrap(value, scriptService);
其核心實現邏輯在接口 ValueSource.java 中:
// ValueSource.java 關鍵方法 59行
public static ValueSource wrap(Object value, ScriptService scriptService) {......} else if (value instanceof String) {// This check is here because the DEFAULT_TEMPLATE_LANG(mustache) is not// installed for use by REST tests. `value` will not be// modified if templating is not availableif (scriptService.isLangSupported(DEFAULT_TEMPLATE_LANG) && ((String) value).contains("{{")) {Script script = new Script(ScriptType.INLINE, DEFAULT_TEMPLATE_LANG, (String) value, Collections.emptyMap());return new TemplatedValue(scriptService.compile(script, TemplateScript.CONTEXT));} else {return new ObjectValue(value);}}......
}
當配置中的 value 值為"{{{status}}}"字符串時,創建 TemplateValue 實例。
這里 “{{{status}}}” 的寫法屬于 Mustache 語法,一種輕量級的模板引擎語法。ES 在 search template 中會主要應用,在 set processor 也用了 Mustache 進行字段內容的引用。
// 在 ValueSource.java 內部
private static class TemplateValue extends ValueSource {private final TemplateScript.Factory template;@Overridepublic Object copyAndResolve(Map<String, Object> model) {return template.newInstance(model).execute();}}
繼續看抽象類 TemplateScript.java#execute() ,這個方法在定義的時候已經明確聲明返回的是 string
/** Run a template and return the resulting string, encoded in utf8 bytes. */public abstract String execute();
而實現的子類則很明顯是 MustacheExecutableScript.execute(),即 Mustache 語法引擎的實現。
private class MustacheExecutableScript extends TemplateScript {......@Overridepublic String execute() {final StringWriter writer = new StringWriter();try {// crazy reflection hereSpecialPermission.check();AccessController.doPrivileged((PrivilegedAction<Void>) () -> {template.execute(writer, params);return null;});} catch (Exception e) {logger.error((Supplier<?>) () -> new ParameterizedMessage("Error running {}", template), e);throw new GeneralScriptException("Error running " + template, e);}return writer.toString();}......
這里也可以印證了字段內容類型被強制轉為字符串
類型轉換過程
deepseek 幫我總結的類型轉換過程如下:
sequenceDiagramparticipant SetProcessorparticipant ValueSourceparticipant TemplateValueparticipant TemplateScriptparticipant MustacheEngineSetProcessor->>ValueSource: wrap("{{status}}")ValueSource->>TemplateValue: 創建實例SetProcessor->>TemplateValue: copyAndResolve(doc)TemplateValue->>TemplateScript: newInstance(doc)TemplateScript->>MustacheEngine: compile("{{status}}")MustacheEngine-->>TemplateScript: 返回Mustache編譯后模板實現TemplateValue->>TemplateScript: execute()MustacheEngine-->>TemplateScript: 在這里將結果渲染為StringTemplateValue-->>SetProcessor: 返回"200"(String)
小結
所以,這里 source 內字段類型被轉變的原因,是 ES 對 set processor 使用 Mustache 語法產生的結果值進行了特殊處理,將內容都處理成了 string。
假設這次使用 set 去處理的值都是一個默認值 404 ,則不會出現這個問題
PUT _ingest/pipeline/copy_status_to_keyword_1
{"description": "resets the value of status and subfields","processors": [{"set": {"field": "status","value": 404}}]
}#update 執行方式
POST test/_update_by_query?pipeline=copy_status_to_keyword_1
{"query": {"bool": {"must_not": {"exists": {"field": "status.keyword"}},"must": {"exists": {"field": "status"}}}}
}GET test/_search
# 返回內容{"_index": "test","_type": "_doc","_id": "tN0QRZUBLvnTvXTpJMTI","_score": 1,"_source": {"status": 404}},{"_index": "test","_type": "_doc","_id": "td0QRZUBLvnTvXTpJMTI","_score": 1,"_source": {"status": 404}}
那 ES 在 set 這段 Mustache 語法的處理里,使用 string 作為返回值,大家覺得合理么?如果需要保留原來的數據內容類型,不修改 TemplateScript.java#execute()
這個方法可以實現么?
作者:金多安,極限科技(INFINI Labs)搜索運維專家,Elastic 認證專家,搜索客社區日報責任編輯。一直從事與搜索運維相關的工作,日常會去挖掘 ES / Lucene 方向的搜索技術原理,保持搜索相關技術發展的關注。