diff --git a/docs/reference/docs/update.asciidoc b/docs/reference/docs/update.asciidoc index a7811fc2a25..64731cf81bc 100644 --- a/docs/reference/docs/update.asciidoc +++ b/docs/reference/docs/update.asciidoc @@ -234,3 +234,7 @@ It also allows to update the `ttl` of a document using `ctx._ttl` and timestamp using `ctx._timestamp`. Note that if the timestamp is not updated and not extracted from the `_source` it will be set to the update date. + +In addition to `_source`, the following variables are available through +the `ctx` map: `_index`, `_type`, `_id`, `_version`, `_routing`, +`_parent`, `_timestamp`, `_ttl`. diff --git a/src/main/java/org/elasticsearch/action/update/UpdateHelper.java b/src/main/java/org/elasticsearch/action/update/UpdateHelper.java index a38285d9f14..708b249b71e 100644 --- a/src/main/java/org/elasticsearch/action/update/UpdateHelper.java +++ b/src/main/java/org/elasticsearch/action/update/UpdateHelper.java @@ -41,6 +41,8 @@ import org.elasticsearch.index.get.GetResult; import org.elasticsearch.index.mapper.internal.ParentFieldMapper; import org.elasticsearch.index.mapper.internal.RoutingFieldMapper; import org.elasticsearch.index.mapper.internal.TTLFieldMapper; +import org.elasticsearch.index.mapper.internal.TimestampFieldMapper; +import org.elasticsearch.index.service.IndexService; import org.elasticsearch.index.shard.ShardId; import org.elasticsearch.index.shard.service.IndexShard; import org.elasticsearch.script.ExecutableScript; @@ -74,7 +76,7 @@ public class UpdateHelper extends AbstractComponent { public Result prepare(UpdateRequest request, IndexShard indexShard) { long getDate = System.currentTimeMillis(); final GetResult getResult = indexShard.getService().get(request.type(), request.id(), - new String[]{RoutingFieldMapper.NAME, ParentFieldMapper.NAME, TTLFieldMapper.NAME}, + new String[]{RoutingFieldMapper.NAME, ParentFieldMapper.NAME, TTLFieldMapper.NAME, TimestampFieldMapper.NAME}, true, request.version(), request.versionType(), FetchSourceContext.FETCH_SOURCE, false); if (!getResult.isExists()) { @@ -148,7 +150,7 @@ public class UpdateHelper extends AbstractComponent { Tuple> sourceAndContent = XContentHelper.convertToMap(getResult.internalSourceRef(), true); String operation = null; - String timestamp; + String timestamp = null; Long ttl = null; final Map updatedSourceAsMap; final XContentType updateSourceContentType = sourceAndContent.v1(); @@ -176,7 +178,17 @@ public class UpdateHelper extends AbstractComponent { operation = "none"; } } else { - Map ctx = new HashMap<>(2); + Map ctx = new HashMap<>(16); + Long originalTtl = getResult.getFields().containsKey(TTLFieldMapper.NAME) ? (Long) getResult.field(TTLFieldMapper.NAME).getValue() : null; + Long originalTimestamp = getResult.getFields().containsKey(TimestampFieldMapper.NAME) ? (Long) getResult.field(TimestampFieldMapper.NAME).getValue() : null; + ctx.put("_index", getResult.getIndex()); + ctx.put("_type", getResult.getType()); + ctx.put("_id", getResult.getId()); + ctx.put("_version", getResult.getVersion()); + ctx.put("_routing", routing); + ctx.put("_parent", parent); + ctx.put("_timestamp", originalTimestamp); + ctx.put("_ttl", originalTtl); ctx.put("_source", sourceAndContent.v2()); try { @@ -190,7 +202,14 @@ public class UpdateHelper extends AbstractComponent { } operation = (String) ctx.get("op"); - timestamp = (String) ctx.get("_timestamp"); + + Object fetchedTimestamp = ctx.get("_timestamp"); + if (fetchedTimestamp != null) { + timestamp = fetchedTimestamp.toString(); + } else if (originalTimestamp != null) { + // No timestamp has been given in the update script, so we keep the previous timestamp if there is one + timestamp = originalTimestamp.toString(); + } ttl = getTTLFromScriptContext(ctx); diff --git a/src/test/java/org/elasticsearch/update/UpdateTests.java b/src/test/java/org/elasticsearch/update/UpdateTests.java index 4a22a474a9d..7423926fb7a 100644 --- a/src/test/java/org/elasticsearch/update/UpdateTests.java +++ b/src/test/java/org/elasticsearch/update/UpdateTests.java @@ -461,6 +461,99 @@ public class UpdateTests extends ElasticsearchIntegrationTest { } } + @Test + public void testContextVariables() throws Exception { + createTestIndex(); + + // Add child type for testing the _parent context variable + client().admin().indices().preparePutMapping("test") + .setType("subtype1") + .setSource(XContentFactory.jsonBuilder() + .startObject() + .startObject("subtype1") + .startObject("_parent").field("type", "type1").endObject() + .startObject("_timestamp").field("enabled", true).field("store", "yes").endObject() + .startObject("_ttl").field("enabled", true).field("store", "yes").endObject() + .endObject() + .endObject()) + .execute().actionGet(); + ensureGreen(); + + // Index some documents + long timestamp = System.currentTimeMillis(); + client().prepareIndex() + .setIndex("test") + .setType("type1") + .setId("parentId1") + .setTimestamp(String.valueOf(timestamp-1)) + .setSource("field1", 0, "content", "bar") + .execute().actionGet(); + + client().prepareIndex() + .setIndex("test") + .setType("subtype1") + .setId("id1") + .setParent("parentId1") + .setRouting("routing1") + .setTimestamp(String.valueOf(timestamp)) + .setTTL(111211211) + .setSource("field1", 1, "content", "foo") + .execute().actionGet(); + long postIndexTs = System.currentTimeMillis(); + + // Update the first object and note context variables values + Map scriptParams = new HashMap<>(); + scriptParams.put("delim", "_"); + UpdateResponse updateResponse = client().prepareUpdate("test", "subtype1", "id1") + .setRouting("routing1") + .setScript( + "assert ctx._index == \"test\" : \"index should be \\\"test\\\"\"\n" + + "assert ctx._type == \"subtype1\" : \"type should be \\\"subtype1\\\"\"\n" + + "assert ctx._id == \"id1\" : \"id should be \\\"id1\\\"\"\n" + + "assert ctx._version == 1 : \"version should be 1\"\n" + + "assert ctx._parent == \"parentId1\" : \"parent should be \\\"parentId1\\\"\"\n" + + "assert ctx._routing == \"routing1\" : \"routing should be \\\"routing1\\\"\"\n" + + "assert ctx._timestamp == " + timestamp + " : \"timestamp should be " + timestamp + "\"\n" + + "def now = new Date().getTime()\n" + + "assert (111211211 - ctx._ttl) <= (now - " + postIndexTs + ") : \"ttl is not within acceptable range\"\n" + + "ctx._source.content = ctx._source.content + delim + ctx._source.content;\n" + + "ctx._source.field1 += 1;\n", + ScriptService.ScriptType.INLINE) + .setScriptParams(scriptParams) + .execute().actionGet(); + + assertEquals(2, updateResponse.getVersion()); + + GetResponse getResponse = client().prepareGet("test", "subtype1", "id1").setRouting("routing1").execute().actionGet(); + assertEquals(2, getResponse.getSourceAsMap().get("field1")); + assertEquals("foo_foo", getResponse.getSourceAsMap().get("content")); + + // Idem with the second object + scriptParams = new HashMap<>(); + scriptParams.put("delim", "_"); + updateResponse = client().prepareUpdate("test", "type1", "parentId1") + .setScript( + "assert ctx._index == \"test\" : \"index should be \\\"test\\\"\"\n" + + "assert ctx._type == \"type1\" : \"type should be \\\"type1\\\"\"\n" + + "assert ctx._id == \"parentId1\" : \"id should be \\\"parentId1\\\"\"\n" + + "assert ctx._version == 1 : \"version should be 1\"\n" + + "assert ctx._parent == null : \"parent should be null\"\n" + + "assert ctx._routing == null : \"routing should be null\"\n" + + "assert ctx._timestamp == " + (timestamp - 1) + " : \"timestamp should be " + (timestamp - 1) + "\"\n" + + "assert ctx._ttl == null : \"ttl should be null\"\n" + + "ctx._source.content = ctx._source.content + delim + ctx._source.content;\n" + + "ctx._source.field1 += 1;\n", + ScriptService.ScriptType.INLINE) + .setScriptParams(scriptParams) + .execute().actionGet(); + + assertEquals(2, updateResponse.getVersion()); + + getResponse = client().prepareGet("test", "type1", "parentId1").execute().actionGet(); + assertEquals(1, getResponse.getSourceAsMap().get("field1")); + assertEquals("bar_bar", getResponse.getSourceAsMap().get("content")); + } + @Test @Slow public void testConcurrentUpdateWithRetryOnConflict() throws Exception {