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); + } + } +}