Skip to content

Commit 7fb98c0

Browse files
[ML] Add runtime mappings to data frame analytics source config (#69183)
Users can now specify runtime mappings as part of the source config of a data frame analytics job. Those runtime mappings become part of the mapping of the destination index. This ensures the fields are accessible in the destination index even if the relevant data frame analytics job gets deleted. Closes #65056
1 parent 623f547 commit 7fb98c0

File tree

35 files changed

+665
-119
lines changed

35 files changed

+665
-119
lines changed

client/rest-high-level/src/main/java/org/elasticsearch/client/ml/dataframe/DataFrameAnalyticsSource.java

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,13 @@
1515
import org.elasticsearch.common.xcontent.ToXContentObject;
1616
import org.elasticsearch.common.xcontent.XContentBuilder;
1717
import org.elasticsearch.common.xcontent.XContentParser;
18+
import org.elasticsearch.search.builder.SearchSourceBuilder;
1819
import org.elasticsearch.search.fetch.subphase.FetchSourceContext;
1920

2021
import java.io.IOException;
2122
import java.util.Arrays;
2223
import java.util.List;
24+
import java.util.Map;
2325
import java.util.Objects;
2426

2527
public class DataFrameAnalyticsSource implements ToXContentObject {
@@ -45,16 +47,20 @@ public static Builder builder() {
4547
(p, c) -> FetchSourceContext.fromXContent(p),
4648
_SOURCE,
4749
ObjectParser.ValueType.OBJECT_ARRAY_BOOLEAN_OR_STRING);
50+
PARSER.declareObject(Builder::setRuntimeMappings, (p, c) -> p.map(), SearchSourceBuilder.RUNTIME_MAPPINGS_FIELD);
4851
}
4952

5053
private final String[] index;
5154
private final QueryConfig queryConfig;
5255
private final FetchSourceContext sourceFiltering;
56+
private final Map<String, Object> runtimeMappings;
5357

54-
private DataFrameAnalyticsSource(String[] index, @Nullable QueryConfig queryConfig, @Nullable FetchSourceContext sourceFiltering) {
58+
private DataFrameAnalyticsSource(String[] index, @Nullable QueryConfig queryConfig, @Nullable FetchSourceContext sourceFiltering,
59+
@Nullable Map<String, Object> runtimeMappings) {
5560
this.index = Objects.requireNonNull(index);
5661
this.queryConfig = queryConfig;
5762
this.sourceFiltering = sourceFiltering;
63+
this.runtimeMappings = runtimeMappings;
5864
}
5965

6066
public String[] getIndex() {
@@ -69,6 +75,10 @@ public FetchSourceContext getSourceFiltering() {
6975
return sourceFiltering;
7076
}
7177

78+
public Map<String, Object> getRuntimeMappings() {
79+
return runtimeMappings;
80+
}
81+
7282
@Override
7383
public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
7484
builder.startObject();
@@ -79,6 +89,9 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws
7989
if (sourceFiltering != null) {
8090
builder.field(_SOURCE.getPreferredName(), sourceFiltering);
8191
}
92+
if (runtimeMappings != null) {
93+
builder.field(SearchSourceBuilder.RUNTIME_MAPPINGS_FIELD.getPreferredName(), runtimeMappings);
94+
}
8295
builder.endObject();
8396
return builder;
8497
}
@@ -91,12 +104,13 @@ public boolean equals(Object o) {
91104
DataFrameAnalyticsSource other = (DataFrameAnalyticsSource) o;
92105
return Arrays.equals(index, other.index)
93106
&& Objects.equals(queryConfig, other.queryConfig)
94-
&& Objects.equals(sourceFiltering, other.sourceFiltering);
107+
&& Objects.equals(sourceFiltering, other.sourceFiltering)
108+
&& Objects.equals(runtimeMappings, other.runtimeMappings);
95109
}
96110

97111
@Override
98112
public int hashCode() {
99-
return Objects.hash(Arrays.asList(index), queryConfig, sourceFiltering);
113+
return Objects.hash(Arrays.asList(index), queryConfig, sourceFiltering, runtimeMappings);
100114
}
101115

102116
@Override
@@ -109,6 +123,7 @@ public static class Builder {
109123
private String[] index;
110124
private QueryConfig queryConfig;
111125
private FetchSourceContext sourceFiltering;
126+
private Map<String, Object> runtimeMappings;
112127

113128
private Builder() {}
114129

@@ -132,8 +147,13 @@ public Builder setSourceFiltering(FetchSourceContext sourceFiltering) {
132147
return this;
133148
}
134149

150+
public Builder setRuntimeMappings(Map<String, Object> runtimeMappings) {
151+
this.runtimeMappings = runtimeMappings;
152+
return this;
153+
}
154+
135155
public DataFrameAnalyticsSource build() {
136-
return new DataFrameAnalyticsSource(index, queryConfig, sourceFiltering);
156+
return new DataFrameAnalyticsSource(index, queryConfig, sourceFiltering, runtimeMappings);
137157
}
138158
}
139159
}

client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/MlClientDocumentationIT.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2995,13 +2995,16 @@ public void testPutDataFrameAnalytics() throws Exception {
29952995
QueryConfig queryConfig = new QueryConfig(new MatchAllQueryBuilder());
29962996
// end::put-data-frame-analytics-query-config
29972997

2998+
Map<String, Object> runtimeMappings = Collections.emptyMap();
2999+
29983000
// tag::put-data-frame-analytics-source-config
29993001
DataFrameAnalyticsSource sourceConfig = DataFrameAnalyticsSource.builder() // <1>
30003002
.setIndex("put-test-source-index") // <2>
30013003
.setQueryConfig(queryConfig) // <3>
3004+
.setRuntimeMappings(runtimeMappings) // <4>
30023005
.setSourceFiltering(new FetchSourceContext(true,
30033006
new String[] { "included_field_1", "included_field_2" },
3004-
new String[] { "excluded_field" })) // <4>
3007+
new String[] { "excluded_field" })) // <5>
30053008
.build();
30063009
// end::put-data-frame-analytics-source-config
30073010

client/rest-high-level/src/test/java/org/elasticsearch/client/ml/dataframe/DataFrameAnalyticsSourceTests.java

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616
import org.elasticsearch.test.AbstractXContentTestCase;
1717

1818
import java.io.IOException;
19+
import java.util.HashMap;
20+
import java.util.Map;
1921
import java.util.function.Predicate;
2022

2123
import static java.util.Collections.emptyList;
@@ -31,11 +33,19 @@ public static DataFrameAnalyticsSource randomSourceConfig() {
3133
generateRandomStringArray(10, 10, false, false),
3234
generateRandomStringArray(10, 10, false, false));
3335
}
34-
36+
Map<String, Object> runtimeMappings = null;
37+
if (randomBoolean()) {
38+
runtimeMappings = new HashMap<>();
39+
Map<String, Object> runtimeField = new HashMap<>();
40+
runtimeField.put("type", "keyword");
41+
runtimeField.put("script", "");
42+
runtimeMappings.put(randomAlphaOfLength(10), runtimeField);
43+
}
3544
return DataFrameAnalyticsSource.builder()
3645
.setIndex(generateRandomStringArray(10, 10, false, false))
3746
.setQueryConfig(randomBoolean() ? null : randomQueryConfig())
3847
.setSourceFiltering(sourceFiltering)
48+
.setRuntimeMappings(runtimeMappings)
3949
.build();
4050
}
4151

docs/java-rest/high-level/ml/put-data-frame-analytics.asciidoc

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,8 @@ include-tagged::{doc-tests-file}[{api}-source-config]
5555
<1> Constructing a new DataFrameAnalyticsSource
5656
<2> The source index
5757
<3> The query from which to gather the data. If query is not set, a `match_all` query is used by default.
58-
<4> Source filtering to select which fields will exist in the destination index.
58+
<4> Runtime mappings that will be added to the destination index mapping.
59+
<5> Source filtering to select which fields will exist in the destination index.
5960

6061
===== QueryConfig
6162

docs/reference/ml/df-analytics/apis/put-dfanalytics.asciidoc

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -523,7 +523,7 @@ setting, an error occurs when you try to create {dfanalytics-jobs} that have
523523
`source`::
524524
(object)
525525
The configuration of how to source the analysis data. It requires an `index`.
526-
Optionally, `query` and `_source` may be specified.
526+
Optionally, `query`, `runtime_mappings`, and `_source` may be specified.
527527
+
528528
.Properties of `source`
529529
[%collapsible%open]
@@ -543,6 +543,10 @@ options that are supported by {es} can be used, as this object is passed
543543
verbatim to {es}. By default, this property has the following value:
544544
`{"match_all": {}}`.
545545
546+
`runtime_mappings`:::
547+
(Optional, object) Definitions of runtime fields that will become part of the
548+
mapping of the destination index.
549+
546550
`_source`:::
547551
(Optional, object) Specify `includes` and/or `excludes` patterns to select which
548552
fields will be present in the destination. Fields that are excluded cannot be

server/src/main/java/org/elasticsearch/action/search/SearchRequestBuilder.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131

3232
import java.util.Arrays;
3333
import java.util.List;
34+
import java.util.Map;
3435

3536
/**
3637
* A search action request builder.
@@ -592,4 +593,12 @@ public SearchRequestBuilder setPreFilterShardSize(int preFilterShardSize) {
592593
this.request.setPreFilterShardSize(preFilterShardSize);
593594
return this;
594595
}
596+
597+
/**
598+
* Set runtime mappings to create runtime fields that exist only as part of this particular search.
599+
*/
600+
public SearchRequestBuilder setRuntimeMappings(Map<String, Object> runtimeMappings) {
601+
sourceBuilder().runtimeMappings(runtimeMappings);
602+
return this;
603+
}
595604
}

x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/datafeed/DatafeedConfig.java

Lines changed: 2 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
import org.elasticsearch.xpack.core.ml.utils.ExceptionsHelper;
3737
import org.elasticsearch.xpack.core.ml.utils.MlStrings;
3838
import org.elasticsearch.xpack.core.ml.utils.QueryProvider;
39+
import org.elasticsearch.xpack.core.ml.utils.RuntimeMappingsValidator;
3940
import org.elasticsearch.xpack.core.ml.utils.ToXContentParams;
4041
import org.elasticsearch.xpack.core.ml.utils.XContentObjectTransformer;
4142

@@ -825,7 +826,7 @@ public DatafeedConfig build() {
825826
}
826827

827828
validateScriptFields();
828-
validateRuntimeMappings();
829+
RuntimeMappingsValidator.validate(runtimeMappings);
829830
setDefaultChunkingConfig();
830831

831832
setDefaultQueryDelay();
@@ -846,28 +847,6 @@ void validateScriptFields() {
846847
}
847848
}
848849

849-
/**
850-
* Perform a light check that the structure resembles runtime_mappings.
851-
* The full check cannot happen until search
852-
*/
853-
void validateRuntimeMappings() {
854-
for (Map.Entry<String, Object> entry : runtimeMappings.entrySet()) {
855-
// top level objects are fields
856-
String fieldName = entry.getKey();
857-
if (entry.getValue() instanceof Map) {
858-
@SuppressWarnings("unchecked")
859-
Map<String, Object> propNode = new HashMap<>(((Map<String, Object>) entry.getValue()));
860-
Object typeNode = propNode.get("type");
861-
if (typeNode == null) {
862-
throw ExceptionsHelper.badRequestException("No type specified for runtime field [" + fieldName + "]");
863-
}
864-
} else {
865-
throw ExceptionsHelper.badRequestException("Expected map for runtime field [" + fieldName + "] " +
866-
"definition but got a " + fieldName.getClass().getSimpleName());
867-
}
868-
}
869-
}
870-
871850
private static void checkNoMoreHistogramAggregations(Collection<AggregationBuilder> aggregations) {
872851
for (AggregationBuilder agg : aggregations) {
873852
if (ExtractorUtils.isHistogram(agg)) {

x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/DataFrameAnalyticsSource.java

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,15 +21,19 @@
2121
import org.elasticsearch.common.xcontent.ToXContentObject;
2222
import org.elasticsearch.common.xcontent.XContentBuilder;
2323
import org.elasticsearch.index.query.QueryBuilder;
24+
import org.elasticsearch.search.builder.SearchSourceBuilder;
2425
import org.elasticsearch.search.fetch.subphase.FetchSourceContext;
2526
import org.elasticsearch.xpack.core.ml.job.messages.Messages;
2627
import org.elasticsearch.xpack.core.ml.utils.ExceptionsHelper;
2728
import org.elasticsearch.xpack.core.ml.utils.QueryProvider;
29+
import org.elasticsearch.xpack.core.ml.utils.RuntimeMappingsValidator;
2830
import org.elasticsearch.xpack.core.ml.utils.XContentObjectTransformer;
2931

3032
import java.io.IOException;
3133
import java.util.ArrayList;
3234
import java.util.Arrays;
35+
import java.util.Collections;
36+
import java.util.HashMap;
3337
import java.util.List;
3438
import java.util.Map;
3539
import java.util.Objects;
@@ -45,22 +49,27 @@ public static ConstructingObjectParser<DataFrameAnalyticsSource, Void> createPar
4549
ignoreUnknownFields, a -> new DataFrameAnalyticsSource(
4650
((List<String>) a[0]).toArray(new String[0]),
4751
(QueryProvider) a[1],
48-
(FetchSourceContext) a[2]));
52+
(FetchSourceContext) a[2],
53+
(Map<String, Object>) a[3]));
4954
parser.declareStringArray(ConstructingObjectParser.constructorArg(), INDEX);
5055
parser.declareObject(ConstructingObjectParser.optionalConstructorArg(),
5156
(p, c) -> QueryProvider.fromXContent(p, ignoreUnknownFields, Messages.DATA_FRAME_ANALYTICS_BAD_QUERY_FORMAT), QUERY);
5257
parser.declareField(ConstructingObjectParser.optionalConstructorArg(),
5358
(p, c) -> FetchSourceContext.fromXContent(p),
5459
_SOURCE,
5560
ObjectParser.ValueType.OBJECT_ARRAY_BOOLEAN_OR_STRING);
61+
parser.declareObject(ConstructingObjectParser.optionalConstructorArg(), (p, c) -> p.map(),
62+
SearchSourceBuilder.RUNTIME_MAPPINGS_FIELD);
5663
return parser;
5764
}
5865

5966
private final String[] index;
6067
private final QueryProvider queryProvider;
6168
private final FetchSourceContext sourceFiltering;
69+
private final Map<String, Object> runtimeMappings;
6270

63-
public DataFrameAnalyticsSource(String[] index, @Nullable QueryProvider queryProvider, @Nullable FetchSourceContext sourceFiltering) {
71+
public DataFrameAnalyticsSource(String[] index, @Nullable QueryProvider queryProvider, @Nullable FetchSourceContext sourceFiltering,
72+
@Nullable Map<String, Object> runtimeMappings) {
6473
this.index = ExceptionsHelper.requireNonNull(index, INDEX);
6574
if (index.length == 0) {
6675
throw new IllegalArgumentException("source.index must specify at least one index");
@@ -73,6 +82,8 @@ public DataFrameAnalyticsSource(String[] index, @Nullable QueryProvider queryPro
7382
throw new IllegalArgumentException("source._source cannot be disabled");
7483
}
7584
this.sourceFiltering = sourceFiltering;
85+
this.runtimeMappings = runtimeMappings == null ? Collections.emptyMap() : Collections.unmodifiableMap(runtimeMappings);
86+
RuntimeMappingsValidator.validate(this.runtimeMappings);
7687
}
7788

7889
public DataFrameAnalyticsSource(StreamInput in) throws IOException {
@@ -83,13 +94,19 @@ public DataFrameAnalyticsSource(StreamInput in) throws IOException {
8394
} else {
8495
sourceFiltering = null;
8596
}
97+
if (in.getVersion().onOrAfter(Version.V_8_0_0)) {
98+
runtimeMappings = in.readMap();
99+
} else {
100+
runtimeMappings = Collections.emptyMap();
101+
}
86102
}
87103

88104
public DataFrameAnalyticsSource(DataFrameAnalyticsSource other) {
89105
this.index = Arrays.copyOf(other.index, other.index.length);
90106
this.queryProvider = new QueryProvider(other.queryProvider);
91107
this.sourceFiltering = other.sourceFiltering == null ? null : new FetchSourceContext(
92108
other.sourceFiltering.fetchSource(), other.sourceFiltering.includes(), other.sourceFiltering.excludes());
109+
this.runtimeMappings = Collections.unmodifiableMap(new HashMap<>(other.runtimeMappings));
93110
}
94111

95112
@Override
@@ -99,6 +116,9 @@ public void writeTo(StreamOutput out) throws IOException {
99116
if (out.getVersion().onOrAfter(Version.V_7_6_0)) {
100117
out.writeOptionalWriteable(sourceFiltering);
101118
}
119+
if (out.getVersion().onOrAfter(Version.V_8_0_0)) {
120+
out.writeMap(runtimeMappings);
121+
}
102122
}
103123

104124
@Override
@@ -109,6 +129,9 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws
109129
if (sourceFiltering != null) {
110130
builder.field(_SOURCE.getPreferredName(), sourceFiltering);
111131
}
132+
if (runtimeMappings.isEmpty() == false) {
133+
builder.field(SearchSourceBuilder.RUNTIME_MAPPINGS_FIELD.getPreferredName(), runtimeMappings);
134+
}
112135
builder.endObject();
113136
return builder;
114137
}
@@ -121,12 +144,13 @@ public boolean equals(Object o) {
121144
DataFrameAnalyticsSource other = (DataFrameAnalyticsSource) o;
122145
return Arrays.equals(index, other.index)
123146
&& Objects.equals(queryProvider, other.queryProvider)
124-
&& Objects.equals(sourceFiltering, other.sourceFiltering);
147+
&& Objects.equals(sourceFiltering, other.sourceFiltering)
148+
&& Objects.equals(runtimeMappings, other.runtimeMappings);
125149
}
126150

127151
@Override
128152
public int hashCode() {
129-
return Objects.hash(Arrays.asList(index), queryProvider, sourceFiltering);
153+
return Objects.hash(Arrays.asList(index), queryProvider, sourceFiltering, runtimeMappings);
130154
}
131155

132156
public String[] getIndex() {
@@ -189,6 +213,10 @@ Map<String, Object> getQuery() {
189213
return queryProvider.getQuery();
190214
}
191215

216+
public Map<String, Object> getRuntimeMappings() {
217+
return runtimeMappings;
218+
}
219+
192220
public boolean isFieldExcluded(String path) {
193221
if (sourceFiltering == null) {
194222
return false;

0 commit comments

Comments
 (0)