恕我直言,我也是才知道ElasticSearch條件更新是這麼玩的

猿天地發表於2020-07-29

背景

ElasticSearch 的使用度越來越普及了,很多公司都在使用。有做日誌搜尋的,有做商品搜尋的,有做訂單搜尋的。

大部分使用場景都是通過程式定期去匯入資料到 ElasticSearch 中,或者通過 CDC 的方式來構建索引。在這種場景下,更新資料都是單條更新,比如 ID=1 的資料發生了修改操作,那麼就會把 ElasticSearch 中 ID=1 的這條資料更新下。

但有些場景下需要根據條件同時更新多條資料,就像 Mysql 中我們使用 Update Table Set Name=XXX where Age=18 去更新一批資料一樣。

正好有同學微信問我怎麼批量更新,接下來就看看在 ElasticSearch 中是如何去進行按條件更新的操作。

單條更新

ElasticSearch 的客戶端官方推薦使用 elasticsearch-rest-high-level-client。所以本文也是基於 elasticsearch-rest-high-level-client 來構建程式碼。

首先來回顧下單條資料的更新是怎麼做的,程式碼如下:

UpdateRequest updateRequest = new UpdateRequest(index, type, id);
updateRequest.doc(documentJson, XContentType.JSON);
restHighLevelClient.update(updateRequest, options);

構建 UpdateRequest 的時候就指定了索引,型別,ID 三個欄位,也就精確到了某一條資料,所以更新的自然也是這一條資料。

條件更新

首先我們準備幾條測試資料,如下:

{
    id: 1,
    title: "Java怎麼學",
    type: 1,
    userId: 1,
    tags: [
        "java"
    ],
    textContent: "我要學Java",
    status: 1,
    heat: 100
}
{
    id: 2,
    title: "Java怎麼學",
    type: 1,
    userId: 1,
    tags: [
        "java"
    ],
    textContent: "我要學Java",
    status: 1,
    heat: 100
}

假如我們的需求是將 userId=1 的所有文件資料改成無效,也就是 status=0。如果不用按條件更新,你就得查詢出 userId=1 的所有資料,然後一條條更新,這就太慢了。

下面看看按條件更新是如何使用的,如下:

POST http://47.105.66.210:9200/article_v1/doc/_update_by_query
{
    "script": {
        "source":"ctx._source['status']=0;"
    },
    "query": {
        "term": {
            "userId": 1
        }
    } 
}

按條件更新需要使用_update_by_query 來進行,query 用於指定更新資料的匹配條件,script 用於更新的邏輯。

詳細使用文件:

https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-update-by-query.html

https://www.elastic.co/guide/en/elasticsearch/reference/current/modules-scripting-using.html

在 Java 程式碼中如何實現條件更新呢?

UpdateByQueryRequest request = new UpdateByQueryRequest("article_v1");
request.setQuery(new TermQueryBuilder("userId", 1));
request.setScript(new Script("ctx._source['status']=0;"));
restHighLevelClient.updateByQuery(request, RequestOptions.DEFAULT);

是不是也很簡單,跟單條資料更新差不多,使用 UpdateByQueryRequest 構建更新物件,然後設定 Query 和 Script 就可以了。

條件更新陣列

比如我們的需求是要移除 tags 中的 java,如下:

POST http://47.105.66.210:9200/article_v1/doc/_update_by_query
{
    "script": {
        "source":"ctx._source['tags'].removeIf(item -> item == 'java');"
    },
    "query": {
        "term": {
            "userId": 1
        }
    } 
}

新增的話只需要將 removeIf 改成 add 就可以了。

ctx._source['tags'].add('java');

如果有特殊的業務邏輯,Script 中還可以寫判斷來判斷是否需要修改。

POST http://47.105.66.210:9200/article_v1/doc/_update_by_query
{
    "script": {
        "source":"if(ctx._source.type == 11) {ctx._source['tags'].add('java');}"
    },
    "query": {
        "term": {
            "userId": 1
        }
    } 
}

封裝通用的條件更新

大部分場景下的更新都比較簡單,根據某個欄位去更新某個值,或者去更新多個值。在 Java 中如果每個地方都去寫指令碼,就重複了,最好是抽一個比較通用的方法來更新。

下面是簡單的示列,其中還有很多需要考慮的點,像資料型別我只處理了數字,字串,和 List,其他的大家需要自己去擴充套件。

public BulkByScrollResponse updateByQuery(String index, QueryBuilder query, Map<String, Object> document) {
    UpdateByQueryRequest updateByQueryRequest = new UpdateByQueryRequest(index);
    updateByQueryRequest.setQuery(query);
    StringBuilder script = new StringBuilder();
    Set<String> keys = document.keySet();
    for (String key : keys) {
        String appendValue = "";
        Object value = document.get(key);
        if (value instanceof Number) {
            appendValue = value.toString();
        } else if (value instanceof String) {
            appendValue = "'" + value.toString() + "'";
        } else if (value instanceof List){
            appendValue = JsonUtils.toJson(value);
        } else {
            appendValue = value.toString();
        }
        script.append("ctx._source.").append(key).append("=").append(appendValue).append(";");
    }
    updateByQueryRequest.setScript(new Script(script.toString()));
    return updateByQuery(updateByQueryRequest, RequestOptions.DEFAULT);
}
public BulkByScrollResponse updateByQuery(UpdateByQueryRequest updateByQueryRequest, RequestOptions options) {
    Map<String, Object> catData = new HashMap<>(1);
    catData.put(ElasticSearchConstant.UPDATE_BY_QUERY_REQUEST, updateByQueryRequest.toString());
    return CatTransactionManager.newTransaction(() -> {
        try {
            return restHighLevelClient.updateByQuery(updateByQueryRequest, options);
        }catch (IOException e) {
            throw new RuntimeException(e);
        }
    }, ElasticSearchConstant.ES_CAT_TYPE, ElasticSearchConstant.UPDATE, catData);
}

如果有了這麼一個方法,那麼使用方式如下:

@Test
public void testUpdate5() {
    Map<String, Object> document = new HashMap<>();
    document.put("title", "Java");
    document.put("status", 0);
    document.put("tags", Lists.newArrayList("JS", "CSS"));
    kittyRestHighLevelClient.updateByQuery(elasticSearchIndexConfig.getArticleSaveIndexName(), new TermQueryBuilder("userId", 1), document);
}

關於作者 :尹吉歡,簡單的技術愛好者,《Spring Cloud 微服務-全棧技術與案例解析》, 《Spring Cloud 微服務 入門 實戰與進階》作者, 公眾號 猿天地 發起人。

相關文章