Skip to content

Commit 0e91859

Browse files
authored
Format support for script doc fields (#60465)
Adds `format` and `locale` support to `runtime_script` fields, specifically those with the `runtime_type` of `date`. Others `runtime_type`s will return an error if provided with those parameters.
1 parent 0438030 commit 0e91859

File tree

6 files changed

+249
-20
lines changed

6 files changed

+249
-20
lines changed

x-pack/plugin/runtime-fields/src/main/java/org/elasticsearch/xpack/runtimefields/mapper/AbstractScriptMappedFieldType.java

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,4 +159,21 @@ protected final void checkAllowExpensiveQueries(QueryShardContext context) {
159159
);
160160
}
161161
}
162+
163+
/**
164+
* The format that this field should use. The default implementation is
165+
* {@code null} because most fields don't support formats.
166+
*/
167+
protected String format() {
168+
return null;
169+
}
170+
171+
/**
172+
* The locale that this field's format should use. The default
173+
* implementation is {@code null} because most fields don't
174+
* support formats.
175+
*/
176+
protected Locale formatLocale() {
177+
return null;
178+
}
162179
}

x-pack/plugin/runtime-fields/src/main/java/org/elasticsearch/xpack/runtimefields/mapper/RuntimeScriptFieldMapper.java

Lines changed: 50 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,11 @@
66

77
package org.elasticsearch.xpack.runtimefields.mapper;
88

9+
import org.elasticsearch.common.time.DateFormatter;
10+
import org.elasticsearch.common.util.LocaleUtils;
911
import org.elasticsearch.index.mapper.DateFieldMapper;
1012
import org.elasticsearch.index.mapper.FieldMapper;
1113
import org.elasticsearch.index.mapper.KeywordFieldMapper;
12-
import org.elasticsearch.index.mapper.MappedFieldType;
1314
import org.elasticsearch.index.mapper.Mapper;
1415
import org.elasticsearch.index.mapper.NumberFieldMapper.NumberType;
1516
import org.elasticsearch.index.mapper.ParametrizedFieldMapper;
@@ -23,6 +24,7 @@
2324
import org.elasticsearch.xpack.runtimefields.StringScriptFieldScript;
2425

2526
import java.util.List;
27+
import java.util.Locale;
2628
import java.util.Map;
2729
import java.util.function.BiFunction;
2830

@@ -43,7 +45,7 @@ public <FactoryType> FactoryType compile(Script script, ScriptContext<FactoryTyp
4345

4446
protected RuntimeScriptFieldMapper(
4547
String simpleName,
46-
MappedFieldType mappedFieldType,
48+
AbstractScriptMappedFieldType mappedFieldType,
4749
MultiFields multiFields,
4850
CopyTo copyTo,
4951
String runtimeType,
@@ -78,22 +80,33 @@ protected String contentType() {
7880

7981
public static class Builder extends ParametrizedFieldMapper.Builder {
8082

81-
static final Map<String, BiFunction<Builder, BuilderContext, MappedFieldType>> FIELD_TYPE_RESOLVER = Map.of(
83+
static final Map<String, BiFunction<Builder, BuilderContext, AbstractScriptMappedFieldType>> FIELD_TYPE_RESOLVER = Map.of(
8284
DateFieldMapper.CONTENT_TYPE,
8385
(builder, context) -> {
8486
DateScriptFieldScript.Factory factory = builder.scriptCompiler.compile(
8587
builder.script.getValue(),
8688
DateScriptFieldScript.CONTEXT
8789
);
90+
String format = builder.format.getValue();
91+
if (format == null) {
92+
format = DateFieldMapper.DEFAULT_DATE_TIME_FORMATTER.pattern();
93+
}
94+
Locale locale = builder.locale.getValue();
95+
if (locale == null) {
96+
locale = Locale.ROOT;
97+
}
98+
DateFormatter dateTimeFormatter = DateFormatter.forPattern(format).withLocale(locale);
8899
return new ScriptDateMappedFieldType(
89100
builder.buildFullName(context),
90101
builder.script.getValue(),
91102
factory,
103+
dateTimeFormatter,
92104
builder.meta.getValue()
93105
);
94106
},
95107
NumberType.DOUBLE.typeName(),
96108
(builder, context) -> {
109+
builder.formatAndLocaleNotSupported();
97110
DoubleScriptFieldScript.Factory factory = builder.scriptCompiler.compile(
98111
builder.script.getValue(),
99112
DoubleScriptFieldScript.CONTEXT
@@ -107,6 +120,7 @@ public static class Builder extends ParametrizedFieldMapper.Builder {
107120
},
108121
KeywordFieldMapper.CONTENT_TYPE,
109122
(builder, context) -> {
123+
builder.formatAndLocaleNotSupported();
110124
StringScriptFieldScript.Factory factory = builder.scriptCompiler.compile(
111125
builder.script.getValue(),
112126
StringScriptFieldScript.CONTEXT
@@ -120,6 +134,7 @@ public static class Builder extends ParametrizedFieldMapper.Builder {
120134
},
121135
NumberType.LONG.typeName(),
122136
(builder, context) -> {
137+
builder.formatAndLocaleNotSupported();
123138
LongScriptFieldScript.Factory factory = builder.scriptCompiler.compile(
124139
builder.script.getValue(),
125140
LongScriptFieldScript.CONTEXT
@@ -159,6 +174,27 @@ private static RuntimeScriptFieldMapper toType(FieldMapper in) {
159174
throw new IllegalArgumentException("script must be specified for " + CONTENT_TYPE + " field [" + name + "]");
160175
}
161176
});
177+
private final Parameter<String> format = Parameter.stringParam(
178+
"format",
179+
true,
180+
mapper -> ((AbstractScriptMappedFieldType) mapper.fieldType()).format(),
181+
null
182+
).setSerializer((b, n, v) -> {
183+
if (v != null && false == v.equals(DateFieldMapper.DEFAULT_DATE_TIME_FORMATTER.pattern())) {
184+
b.field(n, v);
185+
}
186+
}).acceptsNull();
187+
private final Parameter<Locale> locale = new Parameter<>(
188+
"locale",
189+
true,
190+
() -> null,
191+
(n, c, o) -> o == null ? null : LocaleUtils.parse(o.toString()),
192+
mapper -> ((AbstractScriptMappedFieldType) mapper.fieldType()).formatLocale()
193+
).setSerializer((b, n, v) -> {
194+
if (v != null && false == v.equals(Locale.ROOT)) {
195+
b.field(n, v.toString());
196+
}
197+
}).acceptsNull();
162198

163199
private final ScriptCompiler scriptCompiler;
164200

@@ -169,12 +205,12 @@ protected Builder(String name, ScriptCompiler scriptCompiler) {
169205

170206
@Override
171207
protected List<Parameter<?>> getParameters() {
172-
return List.of(meta, runtimeType, script);
208+
return List.of(meta, runtimeType, script, format, locale);
173209
}
174210

175211
@Override
176212
public RuntimeScriptFieldMapper build(BuilderContext context) {
177-
BiFunction<Builder, BuilderContext, MappedFieldType> fieldTypeResolver = Builder.FIELD_TYPE_RESOLVER.get(
213+
BiFunction<Builder, BuilderContext, AbstractScriptMappedFieldType> fieldTypeResolver = Builder.FIELD_TYPE_RESOLVER.get(
178214
runtimeType.getValue()
179215
);
180216
if (fieldTypeResolver == null) {
@@ -203,6 +239,15 @@ static Script parseScript(String name, Mapper.TypeParser.ParserContext parserCon
203239
}
204240
return script;
205241
}
242+
243+
private void formatAndLocaleNotSupported() {
244+
if (format.getValue() != null) {
245+
throw new IllegalArgumentException("format can not be specified for runtime_type [" + runtimeType.getValue() + "]");
246+
}
247+
if (locale.getValue() != null) {
248+
throw new IllegalArgumentException("locale can not be specified for runtime_type [" + runtimeType.getValue() + "]");
249+
}
250+
}
206251
}
207252

208253
@FunctionalInterface

x-pack/plugin/runtime-fields/src/main/java/org/elasticsearch/xpack/runtimefields/mapper/ScriptDateMappedFieldType.java

Lines changed: 26 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -31,19 +31,24 @@
3131
import java.time.ZoneId;
3232
import java.time.ZoneOffset;
3333
import java.util.List;
34+
import java.util.Locale;
3435
import java.util.Map;
3536
import java.util.function.Supplier;
3637

3738
public class ScriptDateMappedFieldType extends AbstractScriptMappedFieldType {
3839
private final DateScriptFieldScript.Factory scriptFactory;
39-
40-
ScriptDateMappedFieldType(String name, Script script, DateScriptFieldScript.Factory scriptFactory, Map<String, String> meta) {
40+
private final DateFormatter dateTimeFormatter;
41+
42+
ScriptDateMappedFieldType(
43+
String name,
44+
Script script,
45+
DateScriptFieldScript.Factory scriptFactory,
46+
DateFormatter dateTimeFormatter,
47+
Map<String, String> meta
48+
) {
4149
super(name, script, meta);
4250
this.scriptFactory = scriptFactory;
43-
}
44-
45-
private DateFormatter dateTimeFormatter() {
46-
return DateFieldMapper.DEFAULT_DATE_TIME_FORMATTER; // TODO make configurable
51+
this.dateTimeFormatter = dateTimeFormatter;
4752
}
4853

4954
@Override
@@ -57,12 +62,12 @@ public Object valueForDisplay(Object value) {
5762
if (val == null) {
5863
return null;
5964
}
60-
return dateTimeFormatter().format(Resolution.MILLISECONDS.toInstant(val).atZone(ZoneOffset.UTC));
65+
return dateTimeFormatter.format(Resolution.MILLISECONDS.toInstant(val).atZone(ZoneOffset.UTC));
6166
}
6267

6368
@Override
6469
public DocValueFormat docValueFormat(@Nullable String format, ZoneId timeZone) {
65-
DateFormatter dateTimeFormatter = dateTimeFormatter();
70+
DateFormatter dateTimeFormatter = this.dateTimeFormatter;
6671
if (format != null) {
6772
dateTimeFormatter = DateFormatter.forPattern(format).withLocale(dateTimeFormatter.locale());
6873
}
@@ -97,7 +102,7 @@ public Query rangeQuery(
97102
@Nullable DateMathParser parser,
98103
QueryShardContext context
99104
) {
100-
parser = parser == null ? DateFieldMapper.DEFAULT_DATE_TIME_FORMATTER.toDateMathParser() : parser;
105+
parser = parser == null ? dateTimeFormatter.toDateMathParser() : parser;
101106
checkAllowExpensiveQueries(context);
102107
return DateFieldType.dateRangeQuery(
103108
lowerTerm,
@@ -119,7 +124,7 @@ public Query termQuery(Object value, QueryShardContext context) {
119124
value,
120125
false,
121126
null,
122-
dateTimeFormatter().toDateMathParser(),
127+
dateTimeFormatter.toDateMathParser(),
123128
now,
124129
DateFieldMapper.Resolution.MILLISECONDS
125130
);
@@ -141,7 +146,7 @@ public Query termsQuery(List<?> values, QueryShardContext context) {
141146
value,
142147
false,
143148
null,
144-
DateFieldMapper.DEFAULT_DATE_TIME_FORMATTER.toDateMathParser(),
149+
dateTimeFormatter.toDateMathParser(),
145150
now,
146151
DateFieldMapper.Resolution.MILLISECONDS
147152
)
@@ -151,4 +156,14 @@ public Query termsQuery(List<?> values, QueryShardContext context) {
151156
return new LongScriptFieldTermsQuery(script, leafFactory(context.lookup())::newInstance, name(), terms);
152157
});
153158
}
159+
160+
@Override
161+
protected String format() {
162+
return dateTimeFormatter.pattern();
163+
}
164+
165+
@Override
166+
protected Locale formatLocale() {
167+
return dateTimeFormatter.locale();
168+
}
154169
}

x-pack/plugin/runtime-fields/src/test/java/org/elasticsearch/xpack/runtimefields/mapper/RuntimeScriptFieldMapperTests.java

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88

99
import org.elasticsearch.action.fieldcaps.FieldCapabilities;
1010
import org.elasticsearch.action.fieldcaps.FieldCapabilitiesResponse;
11+
import org.elasticsearch.common.CheckedConsumer;
12+
import org.elasticsearch.common.CheckedSupplier;
1113
import org.elasticsearch.common.Strings;
1214
import org.elasticsearch.common.settings.Settings;
1315
import org.elasticsearch.common.xcontent.XContentBuilder;
@@ -34,6 +36,7 @@
3436
import java.util.Set;
3537

3638
import static org.hamcrest.Matchers.arrayContainingInAnyOrder;
39+
import static org.hamcrest.Matchers.equalTo;
3740
import static org.hamcrest.Matchers.instanceOf;
3841

3942
public class RuntimeScriptFieldMapperTests extends ESSingleNodeTestCase {
@@ -145,6 +148,51 @@ public void testDate() throws IOException {
145148
assertEquals(Strings.toString(mapping("date")), Strings.toString(mapperService.documentMapper()));
146149
}
147150

151+
public void testDateWithFormat() throws IOException {
152+
CheckedSupplier<XContentBuilder, IOException> mapping = () -> mapping("date", b -> b.field("format", "yyyy-MM-dd"));
153+
MapperService mapperService = createIndex("test", Settings.EMPTY, mapping.get()).mapperService();
154+
FieldMapper mapper = (FieldMapper) mapperService.documentMapper().mappers().getMapper("field");
155+
assertThat(mapper, instanceOf(RuntimeScriptFieldMapper.class));
156+
assertEquals(Strings.toString(mapping.get()), Strings.toString(mapperService.documentMapper()));
157+
}
158+
159+
public void testDateWithLocale() throws IOException {
160+
CheckedSupplier<XContentBuilder, IOException> mapping = () -> mapping("date", b -> b.field("locale", "en_GB"));
161+
MapperService mapperService = createIndex("test", Settings.EMPTY, mapping.get()).mapperService();
162+
FieldMapper mapper = (FieldMapper) mapperService.documentMapper().mappers().getMapper("field");
163+
assertThat(mapper, instanceOf(RuntimeScriptFieldMapper.class));
164+
assertEquals(Strings.toString(mapping.get()), Strings.toString(mapperService.documentMapper()));
165+
}
166+
167+
public void testDateWithLocaleAndFormat() throws IOException {
168+
CheckedSupplier<XContentBuilder, IOException> mapping = () -> mapping(
169+
"date",
170+
b -> b.field("format", "yyyy-MM-dd").field("locale", "en_GB")
171+
);
172+
MapperService mapperService = createIndex("test", Settings.EMPTY, mapping.get()).mapperService();
173+
FieldMapper mapper = (FieldMapper) mapperService.documentMapper().mappers().getMapper("field");
174+
assertThat(mapper, instanceOf(RuntimeScriptFieldMapper.class));
175+
assertEquals(Strings.toString(mapping.get()), Strings.toString(mapperService.documentMapper()));
176+
}
177+
178+
public void testNonDateWithFormat() throws IOException {
179+
String runtimeType = randomValueOtherThan("date", () -> randomFrom(runtimeTypes));
180+
Exception e = expectThrows(
181+
MapperParsingException.class,
182+
() -> createIndex("test", Settings.EMPTY, mapping(runtimeType, b -> b.field("format", "yyyy-MM-dd")))
183+
);
184+
assertThat(e.getMessage(), equalTo("Failed to parse mapping: format can not be specified for runtime_type [" + runtimeType + "]"));
185+
}
186+
187+
public void testNonDateWithLocale() throws IOException {
188+
String runtimeType = randomValueOtherThan("date", () -> randomFrom(runtimeTypes));
189+
Exception e = expectThrows(
190+
MapperParsingException.class,
191+
() -> createIndex("test", Settings.EMPTY, mapping(runtimeType, b -> b.field("locale", "en_GB")))
192+
);
193+
assertThat(e.getMessage(), equalTo("Failed to parse mapping: locale can not be specified for runtime_type [" + runtimeType + "]"));
194+
}
195+
148196
public void testFieldCaps() throws Exception {
149197
for (String runtimeType : runtimeTypes) {
150198
String scriptIndex = "test_" + runtimeType + "_script";
@@ -178,6 +226,10 @@ public void testFieldCaps() throws Exception {
178226
}
179227

180228
private XContentBuilder mapping(String type) throws IOException {
229+
return mapping(type, builder -> {});
230+
}
231+
232+
private XContentBuilder mapping(String type, CheckedConsumer<XContentBuilder, IOException> extra) throws IOException {
181233
XContentBuilder mapping = XContentFactory.jsonBuilder().startObject();
182234
{
183235
mapping.startObject("_doc");
@@ -192,6 +244,7 @@ private XContentBuilder mapping(String type) throws IOException {
192244
mapping.field("source", "dummy_source").field("lang", "test");
193245
}
194246
mapping.endObject();
247+
extra.accept(mapping);
195248
}
196249
mapping.endObject();
197250
}

0 commit comments

Comments
 (0)