diff --git a/src/Nest/Indices/MappingManagement/PutMapping/PutMappingRequest.cs b/src/Nest/Indices/MappingManagement/PutMapping/PutMappingRequest.cs
index 19e0e4b1a8d..5133a051e03 100644
--- a/src/Nest/Indices/MappingManagement/PutMapping/PutMappingRequest.cs
+++ b/src/Nest/Indices/MappingManagement/PutMapping/PutMappingRequest.cs
@@ -56,6 +56,9 @@ public partial class PutMappingRequest
///
public IRoutingField RoutingField { get; set; }
+ ///
+ public IRuntimeFields RuntimeFields { get; set; }
+
///
public ISizeField SizeField { get; set; }
@@ -82,6 +85,7 @@ public partial class PutMappingDescriptor where TDocument : class
bool? ITypeMapping.NumericDetection { get; set; }
IProperties ITypeMapping.Properties { get; set; }
IRoutingField ITypeMapping.RoutingField { get; set; }
+ IRuntimeFields ITypeMapping.RuntimeFields { get; set; }
ISizeField ITypeMapping.SizeField { get; set; }
ISourceField ITypeMapping.SourceField { get; set; }
@@ -150,6 +154,10 @@ public PutMappingDescriptor SourceField(Func RoutingField(Func, IRoutingField> routingFieldSelector) =>
Assign(routingFieldSelector, (a, v) => a.RoutingField = v?.Invoke(new RoutingFieldDescriptor()));
+ ///
+ public PutMappingDescriptor RuntimeFields(Func> runtimeFieldsSelector) =>
+ Assign(runtimeFieldsSelector, (a, v) => a.RuntimeFields = v?.Invoke(new RuntimeFieldsDescriptor())?.Value);
+
///
public PutMappingDescriptor FieldNamesField(Func, IFieldNamesField> fieldNamesFieldSelector) =>
Assign(fieldNamesFieldSelector, (a, v) => a.FieldNamesField = v.Invoke(new FieldNamesFieldDescriptor()));
diff --git a/src/Nest/Mapping/Mappings.cs b/src/Nest/Mapping/Mappings.cs
index 694d69578a0..396c78981e9 100644
--- a/src/Nest/Mapping/Mappings.cs
+++ b/src/Nest/Mapping/Mappings.cs
@@ -57,6 +57,8 @@ public abstract class ObsoleteMappingsBase : ITypeMapping
IProperties ITypeMapping.Properties { get => Wrapped.Properties; set => Wrapped.Properties = value; }
[DataMember(Name = "_routing")]
IRoutingField ITypeMapping.RoutingField { get => Wrapped.RoutingField; set => Wrapped.RoutingField = value; }
+ [DataMember(Name = "runtime")]
+ IRuntimeFields ITypeMapping.RuntimeFields { get => Wrapped.RuntimeFields; set => Wrapped.RuntimeFields = value; }
[DataMember(Name = "_size")]
ISizeField ITypeMapping.SizeField { get => Wrapped.SizeField; set => Wrapped.SizeField = value; }
[DataMember(Name = "_source")]
diff --git a/src/Nest/Mapping/RuntimeFields/RuntimeField.cs b/src/Nest/Mapping/RuntimeFields/RuntimeField.cs
new file mode 100644
index 00000000000..0e8eb0630f0
--- /dev/null
+++ b/src/Nest/Mapping/RuntimeFields/RuntimeField.cs
@@ -0,0 +1,59 @@
+// Licensed to Elasticsearch B.V under one or more agreements.
+// Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
+// See the LICENSE file in the project root for more information
+
+using System.Runtime.Serialization;
+using Elasticsearch.Net.Utf8Json;
+
+namespace Nest
+{
+ [InterfaceDataContract]
+ [ReadAs(typeof(RuntimeField))]
+ public interface IRuntimeField
+ {
+ ///
+ /// Runtime fields with a type of date can accept the format parameter exactly as the date field type.
+ ///
+ ///
+ [DataMember(Name = "format")]
+ string Format { get; set; }
+
+ ///
+ /// The script to be evaluated for field calculation at search time.
+ ///
+ [DataMember(Name = "script")]
+ IStoredScript Script { get; set; }
+
+ ///
+ /// The datatype of the runtime field.
+ ///
+ [DataMember(Name = "type")]
+ FieldType Type { get; set; }
+ }
+
+ public class RuntimeField : IRuntimeField
+ {
+ ///
+ public string Format { get; set; }
+ ///
+ public IStoredScript Script { get; set; }
+ ///
+ public FieldType Type { get; set; }
+ }
+
+ public class RuntimeFieldDescriptor
+ : DescriptorBase, IRuntimeField
+ {
+ public RuntimeFieldDescriptor(FieldType fieldType) => Self.Type = fieldType;
+
+ string IRuntimeField.Format { get; set; }
+ IStoredScript IRuntimeField.Script { get; set; }
+ FieldType IRuntimeField.Type { get; set; }
+
+ public RuntimeFieldDescriptor Format(string format) => Assign(format, (a, v) => a.Format = v);
+
+ public RuntimeFieldDescriptor Script(IStoredScript script) => Assign(script, (a, v) => a.Script = v);
+
+ public RuntimeFieldDescriptor Script(string source) => Assign(source, (a, v) => a.Script = new PainlessScript(source));
+ }
+}
diff --git a/src/Nest/Mapping/RuntimeFields/RuntimeFields.cs b/src/Nest/Mapping/RuntimeFields/RuntimeFields.cs
new file mode 100644
index 00000000000..dac72883f71
--- /dev/null
+++ b/src/Nest/Mapping/RuntimeFields/RuntimeFields.cs
@@ -0,0 +1,36 @@
+// Licensed to Elasticsearch B.V under one or more agreements.
+// Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
+// See the LICENSE file in the project root for more information
+
+using System;
+using System.Collections.Generic;
+using Elasticsearch.Net.Utf8Json;
+
+namespace Nest
+{
+ [JsonFormatter(typeof(VerbatimDictionaryKeysFormatter))]
+ public interface IRuntimeFields : IIsADictionary { }
+
+ public class RuntimeFields : IsADictionaryBase, IRuntimeFields
+ {
+ public RuntimeFields() { }
+
+ public RuntimeFields(IDictionary container) : base(container) { }
+
+ public RuntimeFields(Dictionary container) : base(container) { }
+
+ public void Add(string name, IRuntimeField runtimeField) => BackingDictionary.Add(name, runtimeField);
+ }
+
+ public class RuntimeFieldsDescriptor
+ : IsADictionaryDescriptorBase
+ {
+ public RuntimeFieldsDescriptor() : base(new RuntimeFields()) { }
+
+ public RuntimeFieldsDescriptor RuntimeField(string name, FieldType type, Func selector) =>
+ Assign(name, selector?.Invoke(new RuntimeFieldDescriptor(type)));
+
+ public RuntimeFieldsDescriptor RuntimeField(string name, FieldType type) =>
+ Assign(name, new RuntimeFieldDescriptor(type));
+ }
+}
diff --git a/src/Nest/Mapping/TypeMapping.cs b/src/Nest/Mapping/TypeMapping.cs
index 0ca74b4ff92..0058e2d06f1 100644
--- a/src/Nest/Mapping/TypeMapping.cs
+++ b/src/Nest/Mapping/TypeMapping.cs
@@ -96,6 +96,12 @@ public interface ITypeMapping
[DataMember(Name = "_routing")]
IRoutingField RoutingField { get; set; }
+ ///
+ /// Specifies runtime fields for the mapping.
+ ///
+ [DataMember(Name = "runtime")]
+ IRuntimeFields RuntimeFields { get; set; }
+
///
/// If enabled, indexes the size in bytes of the original _source field.
/// Requires mapper-size plugin be installed
@@ -147,6 +153,9 @@ public class TypeMapping : ITypeMapping
///
public IRoutingField RoutingField { get; set; }
+ ///
+ public IRuntimeFields RuntimeFields { get; set; }
+
///
public ISizeField SizeField { get; set; }
@@ -171,6 +180,7 @@ public class TypeMappingDescriptor : DescriptorBase,
bool? ITypeMapping.NumericDetection { get; set; }
IProperties ITypeMapping.Properties { get; set; }
IRoutingField ITypeMapping.RoutingField { get; set; }
+ IRuntimeFields ITypeMapping.RuntimeFields { get; set; }
ISizeField ITypeMapping.SizeField { get; set; }
ISourceField ITypeMapping.SourceField { get; set; }
@@ -259,6 +269,10 @@ public TypeMappingDescriptor DisableIndexField(bool? disabled = true) =>
public TypeMappingDescriptor RoutingField(Func, IRoutingField> routingFieldSelector) =>
Assign(routingFieldSelector, (a, v) => a.RoutingField = v?.Invoke(new RoutingFieldDescriptor()));
+ ///
+ public TypeMappingDescriptor RuntimeFields(Func> runtimeFieldsSelector) =>
+ Assign(runtimeFieldsSelector, (a, v) => a.RuntimeFields = v?.Invoke(new RuntimeFieldsDescriptor())?.Value);
+
///
public TypeMappingDescriptor FieldNamesField(Func, IFieldNamesField> fieldNamesFieldSelector) =>
Assign(fieldNamesFieldSelector.Invoke(new FieldNamesFieldDescriptor()), (a, v) => a.FieldNamesField = v);
diff --git a/src/Nest/Search/Search/SearchRequest.cs b/src/Nest/Search/Search/SearchRequest.cs
index fb63c469458..0a999065451 100644
--- a/src/Nest/Search/Search/SearchRequest.cs
+++ b/src/Nest/Search/Search/SearchRequest.cs
@@ -167,6 +167,9 @@ public partial interface ISearchRequest : ITypedSearchRequest
[DataMember(Name = "version")]
bool? Version { get; set; }
+ ///
+ /// The to search over.
+ ///
[DataMember(Name = "pit")]
IPointInTime PointInTime { get; set; }
}
diff --git a/tests/Tests/Indices/MappingManagement/PutMapping/PutMappingApiTest.cs b/tests/Tests/Indices/MappingManagement/PutMapping/PutMappingApiTest.cs
index f1bb96790ef..42d7a020b0c 100644
--- a/tests/Tests/Indices/MappingManagement/PutMapping/PutMappingApiTest.cs
+++ b/tests/Tests/Indices/MappingManagement/PutMapping/PutMappingApiTest.cs
@@ -338,4 +338,61 @@ protected override LazyResponses ClientUsage() => Calls(
(client, r) => client.MapAsync(r)
);
}
+
+ [SkipVersion("<7.11.0", "Runtime fields introduced in 7.11.0")]
+ public class PutMappingWithRuntimeFieldsTests : ApiTestBase, PutMappingRequest>
+ {
+ // These test serialisation only. Integration tests take place in RuntimeFieldsTests.cs
+
+ private const string ScriptValue = "emit(doc['@timestamp'].value.dayOfWeekEnum.getDisplayName(TextStyle.FULL, Locale.ROOT))";
+
+ public PutMappingWithRuntimeFieldsTests(ReadOnlyCluster cluster, EndpointUsage usage) : base(cluster, usage) { }
+
+ protected override HttpMethod HttpMethod => HttpMethod.PUT;
+
+ protected override string UrlPath => $"/{CallIsolatedValue}/_mapping";
+
+ protected override PutMappingRequest Initializer => new(CallIsolatedValue)
+ {
+ RuntimeFields = new RuntimeFields
+ {
+ { "runtime_date", new RuntimeField { Type = FieldType.Date, Format = "yyyy-MM-dd" } },
+ { "runtime_scripted", new RuntimeField { Type = FieldType.Keyword, Script = new PainlessScript(ScriptValue) } }
+ }
+ };
+
+ protected override Func, IPutMappingRequest> Fluent => d => d
+ .Index(CallIsolatedValue)
+ .RuntimeFields(rtf => rtf
+ .RuntimeField("runtime_date", FieldType.Date, rf => rf.Format("yyyy-MM-dd"))
+ .RuntimeField("runtime_scripted", FieldType.Keyword, rf=> rf.Script(new PainlessScript(ScriptValue))));
+
+ protected override LazyResponses ClientUsage() => Calls(
+ (client, f) => client.Indices.PutMapping(f),
+ (client, f) => client.Indices.PutMappingAsync(f),
+ (client, r) => client.Indices.PutMapping(r),
+ (client, r) => client.Indices.PutMappingAsync(r)
+ );
+
+ protected override object ExpectJson => new
+ {
+ runtime = new
+ {
+ runtime_date = new
+ {
+ type = "date",
+ format = "yyyy-MM-dd"
+ },
+ runtime_scripted = new
+ {
+ type = "keyword",
+ script = new
+ {
+ lang = "painless",
+ source = ScriptValue
+ }
+ }
+ }
+ };
+ }
}
diff --git a/tests/Tests/Mapping/RuntimeFields/RuntimeFieldsTests.cs b/tests/Tests/Mapping/RuntimeFields/RuntimeFieldsTests.cs
new file mode 100644
index 00000000000..81e611f264f
--- /dev/null
+++ b/tests/Tests/Mapping/RuntimeFields/RuntimeFieldsTests.cs
@@ -0,0 +1,180 @@
+// Licensed to Elasticsearch B.V under one or more agreements.
+// Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
+// See the LICENSE file in the project root for more information
+
+using System.Linq;
+using System.Threading.Tasks;
+using Elastic.Elasticsearch.Xunit.XunitPlumbing;
+using FluentAssertions;
+using Nest;
+using Tests.Core.Extensions;
+using Tests.Core.ManagedElasticsearch.Clusters;
+using Tests.Domain;
+using Tests.Framework.EndpointTests;
+using Tests.Framework.EndpointTests.TestState;
+
+namespace Tests.Mapping.RuntimeFields
+{
+ [SkipVersion("<7.11.0", "Runtime fields introduced in 7.11.0")]
+ public class RuntimeFieldsTests : CoordinatedIntegrationTestBase
+ {
+ private const string ScriptValue = "emit(doc['lastActivity'].value.dayOfWeekEnum.getDisplayName(TextStyle.FULL, Locale.ROOT))";
+ private const string DateFormat = "yyyy-MM-dd";
+ private const string RuntimeFieldNameOne = "runtimeFieldOne";
+ private const string RuntimeFieldNameTwo = "runtimeFieldTwo";
+
+ private const string CreateIndexWithMappingStep = nameof(CreateIndexWithMappingStep);
+ private const string GetCreatedIndexMappingStep = nameof(GetCreatedIndexMappingStep);
+ private const string DeleteIndexStep = nameof(DeleteIndexStep);
+ private const string CreateIndexWithoutMappingStep = nameof(CreateIndexWithoutMappingStep);
+ private const string CreateMappingStep = nameof(CreateMappingStep);
+ private const string GetMappingStep = nameof(GetMappingStep);
+
+ public RuntimeFieldsTests(WritableCluster cluster, EndpointUsage usage) : base(new CoordinatedUsage(cluster, usage)
+ {
+ {
+ CreateIndexWithMappingStep, u =>
+ u.Calls(
+ v => new CreateIndexRequest(IndexName(v))
+ {
+ Mappings = new TypeMapping
+ {
+ RuntimeFields = new Nest.RuntimeFields
+ {
+ {RuntimeFieldNameOne, new RuntimeField
+ {
+ Type = FieldType.Keyword,
+ Script = new PainlessScript(ScriptValue)
+ }},
+ {RuntimeFieldNameTwo, new RuntimeField
+ {
+ Type = FieldType.Date,
+ Format = DateFormat
+ }}
+ }
+ }
+ },
+ (v, d) => d.Index(IndexName(v)).Map(mapping => mapping
+ .RuntimeFields(rtf => rtf
+ .RuntimeField(RuntimeFieldNameOne, FieldType.Keyword, f1 => f1
+ .Script(ScriptValue))
+ .RuntimeField(RuntimeFieldNameTwo, FieldType.Date, f2 => f2.Format(DateFormat)))),
+ (v, c, f) => c.Indices.Create(IndexName(v), f),
+ (v, c, f) => c.Indices.CreateAsync(IndexName(v), f),
+ (v, c, r) => c.Indices.Create(r),
+ (v, c, r) => c.Indices.CreateAsync(r)
+ )
+ },
+ {
+ GetCreatedIndexMappingStep, u =>
+ u.Calls, GetMappingRequest, IGetMappingRequest, GetMappingResponse>(
+ v => new GetMappingRequest(IndexName(v)),
+ (v, d) => d.Index(IndexName(v)),
+ (v, c, f) => c.Indices.GetMapping(f),
+ (v, c, f) => c.Indices.GetMappingAsync(f),
+ (v, c, r) => c.Indices.GetMapping(r),
+ (v, c, r) => c.Indices.GetMappingAsync(r)
+ )
+ },
+ {
+ DeleteIndexStep, u =>
+ u.Calls(
+ v => new DeleteIndexRequest(IndexName(v)),
+ (v, d) => d,
+ (v, c, f) => c.Indices.Delete(IndexName(v), f),
+ (v, c, f) => c.Indices.DeleteAsync(IndexName(v), f),
+ (v, c, r) => c.Indices.Delete(r),
+ (v, c, r) => c.Indices.DeleteAsync(r)
+ )
+ },
+ {
+ CreateIndexWithoutMappingStep, u =>
+ u.Calls(
+ v => new CreateIndexRequest(IndexName(v)),
+ (v, d) => d,
+ (v, c, f) => c.Indices.Create(IndexName(v), f),
+ (v, c, f) => c.Indices.CreateAsync(IndexName(v), f),
+ (v, c, r) => c.Indices.Create(r),
+ (v, c, r) => c.Indices.CreateAsync(r)
+ )
+ },
+ {
+ CreateMappingStep, u =>
+ u.Calls, PutMappingRequest, IPutMappingRequest, PutMappingResponse>(
+ v => new PutMappingRequest(IndexName(v))
+ {
+ RuntimeFields = new Nest.RuntimeFields
+ {
+ {RuntimeFieldNameOne, new RuntimeField
+ {
+ Type = FieldType.Keyword,
+ Script = new PainlessScript(ScriptValue)
+ }},
+ {RuntimeFieldNameTwo, new RuntimeField
+ {
+ Type = FieldType.Date,
+ Format = DateFormat
+ }}
+ }
+ },
+ (v, d) => d.Index(IndexName(v))
+ .RuntimeFields(rtf => rtf
+ .RuntimeField(RuntimeFieldNameOne, FieldType.Keyword, f1 => f1
+ .Script(ScriptValue))
+ .RuntimeField(RuntimeFieldNameTwo, FieldType.Date, f2 => f2.Format(DateFormat))),
+ (v, c, f) => c.Indices.PutMapping(f),
+ (v, c, f) => c.Indices.PutMappingAsync(f),
+ (v, c, r) => c.Indices.PutMapping(r),
+ (v, c, r) => c.Indices.PutMappingAsync(r)
+ )
+ },
+ {
+ GetMappingStep, u =>
+ u.Calls, GetMappingRequest, IGetMappingRequest, GetMappingResponse>(
+ v => new GetMappingRequest(IndexName(v)),
+ (v, d) => d.Index(IndexName(v)),
+ (v, c, f) => c.Indices.GetMapping(f),
+ (v, c, f) => c.Indices.GetMappingAsync(f),
+ (v, c, r) => c.Indices.GetMapping(r),
+ (v, c, r) => c.Indices.GetMappingAsync(r)
+ )
+ }
+ })
+ { }
+
+ private static string IndexName(string uniqueId) => $"runtime-{uniqueId}";
+
+ [I] public async Task CreateIndexWithRuntimeFieldsMapping() => await Assert(CreateIndexWithMappingStep, (v, r) =>
+ {
+ r.ShouldBeValid();
+ r.Acknowledged.Should().BeTrue();
+ });
+
+ [I] public async Task GetIndexMappingWithRuntimeFields() => await Assert(GetCreatedIndexMappingStep, (v, r) => AssertRuntimeFields(r));
+
+ [I] public async Task PutMappingWithRuntimeFields() => await Assert(CreateMappingStep, (v, r) =>
+ {
+ r.ShouldBeValid();
+ r.Acknowledged.Should().BeTrue();
+ });
+
+ [I] public async Task GetMappingWithRuntimeFields() => await Assert(GetMappingStep, (v, r) => AssertRuntimeFields(r));
+
+ private static void AssertRuntimeFields(GetMappingResponse response)
+ {
+ response.ShouldBeValid();
+ var runtimeFields = response.Indices.First().Value.Mappings.RuntimeFields;
+
+ runtimeFields.Count.Should().Be(2);
+ runtimeFields.TryGetValue(RuntimeFieldNameOne, out var fieldOne).Should().BeTrue();
+ runtimeFields.TryGetValue(RuntimeFieldNameTwo, out var fieldTwo).Should().BeTrue();
+
+ fieldOne!.Type.Should().Be(FieldType.Keyword);
+ fieldOne.Script.Lang.Should().Be("painless");
+ fieldOne.Script.Source.Should().Be(ScriptValue);
+
+ fieldTwo!.Type.Should().Be(FieldType.Date);
+ fieldTwo.Format.Should().Be(DateFormat);
+ }
+ }
+}