Skip to content

[Backport master] Support for calculated numeric fields #5626

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Apr 23, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/Nest/CommonOptions/Scripting/InlineScript.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
namespace Nest
{
[InterfaceDataContract]
[ReadAs(typeof(InlineScript))]
public interface IInlineScript : IScript
{
[DataMember(Name ="source")]
Expand Down
18 changes: 18 additions & 0 deletions src/Nest/CommonOptions/Scripting/OnScriptError.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// 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;

namespace Nest
{
[StringEnum]
public enum OnScriptError
{
[EnumMember(Value = "fail")]
Fail,
[EnumMember(Value = "continue")]
Continue
}
}
19 changes: 13 additions & 6 deletions src/Nest/Mapping/RuntimeFields/RuntimeField.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// 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.Runtime.Serialization;
using Nest.Utf8Json;

Expand All @@ -22,7 +23,7 @@ public interface IRuntimeField
/// The script to be evaluated for field calculation at search time.
/// </summary>
[DataMember(Name = "script")]
IStoredScript Script { get; set; }
IInlineScript Script { get; set; }

/// <summary>
/// The datatype of the runtime field.
Expand All @@ -36,7 +37,7 @@ public class RuntimeField : IRuntimeField
/// <inheritdoc />
public string Format { get; set; }
/// <inheritdoc />
public IStoredScript Script { get; set; }
public IInlineScript Script { get; set; }
/// <inheritdoc />
public FieldType Type { get; set; }
}
Expand All @@ -47,13 +48,19 @@ public class RuntimeFieldDescriptor
public RuntimeFieldDescriptor(FieldType fieldType) => Self.Type = fieldType;

string IRuntimeField.Format { get; set; }
IStoredScript IRuntimeField.Script { get; set; }
IInlineScript IRuntimeField.Script { get; set; }
FieldType IRuntimeField.Type { get; set; }

/// <inheritdoc cref="IRuntimeField.Format" />
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));
/// <inheritdoc cref="IRuntimeField.Script" />
public RuntimeFieldDescriptor Script(IInlineScript script) => Assign(script, (a, v) => a.Script = v);

/// <inheritdoc cref="IRuntimeField.Script" />
public RuntimeFieldDescriptor Script(string source) => Assign(source, (a, v) => a.Script = new InlineScript(source));

/// <inheritdoc cref="IRuntimeField.Script" />
public RuntimeFieldDescriptor Script(Func<InlineScriptDescriptor, IInlineScript> selector) => Assign(selector, (a, v) => a.Script = v?.Invoke(new InlineScriptDescriptor()));
}
}
16 changes: 15 additions & 1 deletion src/Nest/Mapping/Types/Core/Number/NumberAttribute.cs
Original file line number Diff line number Diff line change
Expand Up @@ -44,13 +44,27 @@ public double ScalingFactor
set => Self.ScalingFactor = value;
}

public IInlineScript Script
{
get => Self.Script;
set => Self.Script = value;
}

public OnScriptError OnScriptError
{
get => Self.OnScriptError.GetValueOrDefault();
set => Self.OnScriptError = value;
}

bool? INumberProperty.Coerce { get; set; }
INumericFielddata INumberProperty.Fielddata { get; set; }
bool? INumberProperty.IgnoreMalformed { get; set; }

bool? INumberProperty.Index { get; set; }
double? INumberProperty.NullValue { get; set; }
double? INumberProperty.ScalingFactor { get; set; }
IInlineScript INumberProperty.Script { get; set; }
OnScriptError? INumberProperty.OnScriptError { get; set; }

private INumberProperty Self => this;
}
}
38 changes: 36 additions & 2 deletions src/Nest/Mapping/Types/Core/Number/NumberProperty.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,22 @@ public interface INumberProperty : IDocValuesProperty

[DataMember(Name = "scaling_factor")]
double? ScalingFactor { get; set; }

/// <summary>
/// If this parameter is set, then the field will index values generated by this script, rather than reading the values directly from the source.
/// If a value is set for this field on the input document, then the document will be rejected with an error.
/// Scripts are in the same format as their runtime equivalent. Scripts can only be configured on long and double field types.
/// </summary>
[DataMember(Name = "script")]
IInlineScript Script { get; set; }

/// <summary>
/// Defines what to do if the script defined by the `script` parameter throws an error at indexing time.Accepts `reject` (default), which
/// will cause the entire document to be rejected, and `ignore`, which will register the field in the document's ignored metadata field and
/// continue indexing.This parameter can only be set if the `script` field is also set.
/// </summary>
[DataMember(Name = "on_script_error")]
OnScriptError? OnScriptError { get; set; }
}

[DebuggerDisplay("{DebugDisplay}")]
Expand All @@ -45,10 +61,15 @@ public NumberProperty(NumberType type) : base(type.ToFieldType()) { }
public bool? Coerce { get; set; }
public INumericFielddata Fielddata { get; set; }
public bool? IgnoreMalformed { get; set; }

public bool? Index { get; set; }
public double? NullValue { get; set; }
public double? ScalingFactor { get; set; }

/// <inheritdoc />
public IInlineScript Script { get; set; }

/// <inheritdoc />
public OnScriptError? OnScriptError { get; set; }
}

[DebuggerDisplay("{DebugDisplay}")]
Expand All @@ -65,10 +86,11 @@ protected NumberPropertyDescriptorBase(FieldType type) : base(type) { }
bool? INumberProperty.Coerce { get; set; }
INumericFielddata INumberProperty.Fielddata { get; set; }
bool? INumberProperty.IgnoreMalformed { get; set; }

bool? INumberProperty.Index { get; set; }
double? INumberProperty.NullValue { get; set; }
double? INumberProperty.ScalingFactor { get; set; }
IInlineScript INumberProperty.Script { get; set; }
OnScriptError? INumberProperty.OnScriptError { get; set; }

public TDescriptor Type(NumberType? type) => Assign(type?.GetStringValue(), (a, v) => a.Type = v);

Expand All @@ -85,6 +107,18 @@ public TDescriptor Fielddata(Func<NumericFielddataDescriptor, INumericFielddata>
Assign(selector(new NumericFielddataDescriptor()), (a, v) => a.Fielddata = v);

public TDescriptor ScalingFactor(double? scalingFactor) => Assign(scalingFactor, (a, v) => a.ScalingFactor = v);

/// <inheritdoc cref="INumberProperty.Script" />
public TDescriptor Script(IInlineScript inlineScript) => Assign(inlineScript, (a, v) => a.Script = v);

/// <inheritdoc cref="INumberProperty.Script" />
public TDescriptor Script(string source) => Assign(source, (a, v) => a.Script = new InlineScript(source));

/// <inheritdoc cref="INumberProperty.Script" />
public TDescriptor Script(Func<InlineScriptDescriptor, IInlineScript> selector) => Assign(selector, (a, v) => a.Script = v?.Invoke(new InlineScriptDescriptor()));

/// <inheritdoc cref="INumberProperty.OnScriptError" />
public TDescriptor OnScriptError(OnScriptError? onScriptError) => Assign(onScriptError, (a, v) => a.OnScriptError = v);
}

public class NumberPropertyDescriptor<T> : NumberPropertyDescriptorBase<NumberPropertyDescriptor<T>, INumberProperty, T>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -463,7 +463,6 @@ public void MappingRuntimeFields()
type = "keyword",
script = new
{
lang = "painless",
source = "emit(doc['@timestamp'].value.dayOfWeekEnum.getDisplayName(TextStyle.FULL, Locale.ROOT))"
}
}
Expand All @@ -487,6 +486,46 @@ public void MappingRuntimeFields()

//hide
Expect(expected).FromRequest(createIndexResponse);

/**
* One may also include and use parameters in the script.
*/
createIndexResponse = _client.Indices.Create("myindex", c => c
.Map<Company>(m => m
.RuntimeFields(rtf => rtf
.RuntimeField("birthDayOfWeek", FieldType.Keyword, f => f
.Script(s => s
.Source("emit(doc['@timestamp'].value.dayOfWeekEnum.getDisplayName(TextStyle.FULL, Locale.ROOT) + params.suffix)")
.Params(p => p.Add("suffix", " with a suffix."))
)))
)
);

//json
var finalExpected = new
{
mappings = new
{
runtime = new
{
birthDayOfWeek = new
{
type = "keyword",
script = new
{
source = "emit(doc['@timestamp'].value.dayOfWeekEnum.getDisplayName(TextStyle.FULL, Locale.ROOT) + params.suffix)",
@params = new Dictionary<string, string>
{
{"suffix", " with a suffix." }
}
}
}
}
}
};

//hide
Expect(finalExpected).FromRequest(createIndexResponse);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
// See the LICENSE file in the project root for more information

using System;
using System.Collections.Generic;
using Elastic.Elasticsearch.Xunit.XunitPlumbing;
using Elastic.Transport;
using Nest;
Expand Down Expand Up @@ -344,7 +345,7 @@ public class PutMappingWithRuntimeFieldsTests : ApiTestBase<ReadOnlyCluster, Put
{
// 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))";
private const string ScriptValue = "emit(doc['@timestamp'].value.dayOfWeekEnum.getDisplayName(TextStyle.FULL, Locale.ROOT) + params.foo";

public PutMappingWithRuntimeFieldsTests(ReadOnlyCluster cluster, EndpointUsage usage) : base(cluster, usage) { }

Expand All @@ -357,15 +358,18 @@ public PutMappingWithRuntimeFieldsTests(ReadOnlyCluster cluster, EndpointUsage u
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) } }
{ "runtime_scripted", new RuntimeField { Type = FieldType.Keyword, Script = new InlineScript(ScriptValue) { Lang = "painless", Params = new Dictionary<string, object>
{
{ "foo", " is a good day." }
}}}}
}
};

protected override Func<PutMappingDescriptor<Project>, 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))));
.RuntimeField("runtime_scripted", FieldType.Keyword, rf=> rf.Script(s => s.Source(ScriptValue).Lang(ScriptLang.Painless).Params(p => p.Add("foo", " is a good day.")))));

protected override LazyResponses ClientUsage() => Calls(
(client, f) => client.Indices.PutMapping(f),
Expand All @@ -388,8 +392,12 @@ protected override LazyResponses ClientUsage() => Calls(
type = "keyword",
script = new
{
source = ScriptValue,
lang = "painless",
source = ScriptValue
@params = new Dictionary<string, string>
{
{ "foo", " is a good day." }
}
}
}
}
Expand Down
4 changes: 2 additions & 2 deletions tests/Tests/Mapping/RuntimeFields/RuntimeFieldsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ public RuntimeFieldsTests(WritableCluster cluster, EndpointUsage usage) : base(n
{RuntimeFieldNameOne, new RuntimeField
{
Type = FieldType.Keyword,
Script = new PainlessScript(ScriptValue)
Script = new InlineScript(ScriptValue)
}},
{RuntimeFieldNameTwo, new RuntimeField
{
Expand Down Expand Up @@ -108,7 +108,7 @@ public RuntimeFieldsTests(WritableCluster cluster, EndpointUsage usage) : base(n
{RuntimeFieldNameOne, new RuntimeField
{
Type = FieldType.Keyword,
Script = new PainlessScript(ScriptValue)
Script = new InlineScript(ScriptValue)
}},
{RuntimeFieldNameTwo, new RuntimeField
{
Expand Down
52 changes: 52 additions & 0 deletions tests/Tests/Mapping/Types/Core/Number/NumberPropertyTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
// See the LICENSE file in the project root for more information

using System;
using System.Collections.Generic;
using Elastic.Elasticsearch.Xunit.XunitPlumbing;
using Nest;
using Tests.Core.ManagedElasticsearch.Clusters;
Expand Down Expand Up @@ -157,4 +158,55 @@ public UnsignedLongNumberPropertyTests(WritableCluster cluster, EndpointUsage us
}
};
}

[SkipVersion("<7.13.0", "Script support added in 7.13.0")]
public class ScriptedNumberPropertyTests : PropertyTestsBase
{
public ScriptedNumberPropertyTests(WritableCluster cluster, EndpointUsage usage) : base(cluster, usage) { }

protected override object ExpectJson => new
{
properties = new
{
doublesCommits = new
{
type = "long",
script = new
{
source = "emit((long)(doc['numberOfCommits'].value * params.multiplier))",
@params = new Dictionary<string, int>
{
{"multiplier", 2 }
}
},
on_script_error = "continue"
}
}
};

protected override Func<PropertiesDescriptor<Project>, IPromise<IProperties>> FluentProperties => f => f
.Number(n => n
.Name("doublesCommits")
.Type(NumberType.Long)
.Script(s => s.Source("emit((long)(doc['numberOfCommits'].value * params.multiplier))").Params(p => p.Add("multiplier", 2)))
.OnScriptError(OnScriptError.Continue)
);

protected override IProperties InitializerProperties => new Properties
{
{
"doublesCommits", new NumberProperty(NumberType.Long)
{
Script = new InlineScript("emit((long)(doc['numberOfCommits'].value * params.multiplier))")
{
Params = new Dictionary<string, object>
{
{ "multiplier", 2 }
}
},
OnScriptError = OnScriptError.Continue
}
}
};
}
}
6 changes: 3 additions & 3 deletions tests/Tests/Search/Search/SearchApiTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -681,7 +681,6 @@ public SearchApiRuntimeFieldsTests(ReadOnlyCluster cluster, EndpointUsage usage)
{
script = new
{
lang = "painless",
source = RuntimeFieldScript
},
type = "keyword"
Expand Down Expand Up @@ -710,10 +709,11 @@ public SearchApiRuntimeFieldsTests(ReadOnlyCluster cluster, EndpointUsage usage)
.And(RuntimeFieldName),
RuntimeFields = new RuntimeFields
{
{ RuntimeFieldName, new RuntimeField
{
RuntimeFieldName, new RuntimeField
{
Type = FieldType.Keyword,
Script = new PainlessScript(RuntimeFieldScript)
Script = new InlineScript(RuntimeFieldScript)
}
}
}
Expand Down
Loading