diff --git a/x-pack/plugin/runtime-fields/src/main/java/org/elasticsearch/xpack/runtimefields/mapper/RuntimeKeywordMappedFieldType.java b/x-pack/plugin/runtime-fields/src/main/java/org/elasticsearch/xpack/runtimefields/mapper/RuntimeKeywordMappedFieldType.java index fa39ab02bbb4a..a51e1a319a84f 100644 --- a/x-pack/plugin/runtime-fields/src/main/java/org/elasticsearch/xpack/runtimefields/mapper/RuntimeKeywordMappedFieldType.java +++ b/x-pack/plugin/runtime-fields/src/main/java/org/elasticsearch/xpack/runtimefields/mapper/RuntimeKeywordMappedFieldType.java @@ -16,6 +16,7 @@ import org.elasticsearch.common.unit.Fuzziness; import org.elasticsearch.common.xcontent.ToXContent.Params; import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.index.mapper.KeywordFieldMapper; import org.elasticsearch.index.mapper.MappedFieldType; import org.elasticsearch.index.mapper.TextSearchInfo; import org.elasticsearch.index.query.QueryShardContext; @@ -64,11 +65,24 @@ public Object valueForDisplay(Object value) { @Override public String typeName() { - // TODO not sure what we should return here: the runtime type or the field type? - // why is the same string returned from three different methods? return ScriptFieldMapper.CONTENT_TYPE; } + @Override + public String familyTypeName() { + return KeywordFieldMapper.CONTENT_TYPE; + } + + @Override + public boolean isSearchable() { + return true; + } + + @Override + public boolean isAggregatable() { + return true; + } + @Override public ScriptBinaryFieldData.Builder fielddataBuilder(String fullyQualifiedIndexName) { // TODO once we get SearchLookup as an argument, we can already call scriptFactory.newFactory here and pass through the result diff --git a/x-pack/plugin/runtime-fields/src/main/java/org/elasticsearch/xpack/runtimefields/mapper/ScriptFieldMapper.java b/x-pack/plugin/runtime-fields/src/main/java/org/elasticsearch/xpack/runtimefields/mapper/ScriptFieldMapper.java index c40794aa8b2c6..3746c4cb8b6ea 100644 --- a/x-pack/plugin/runtime-fields/src/main/java/org/elasticsearch/xpack/runtimefields/mapper/ScriptFieldMapper.java +++ b/x-pack/plugin/runtime-fields/src/main/java/org/elasticsearch/xpack/runtimefields/mapper/ScriptFieldMapper.java @@ -8,6 +8,7 @@ import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.index.mapper.FieldMapper; +import org.elasticsearch.index.mapper.KeywordFieldMapper; import org.elasticsearch.index.mapper.MappedFieldType; import org.elasticsearch.index.mapper.Mapper; import org.elasticsearch.index.mapper.MapperParsingException; @@ -21,6 +22,7 @@ import java.io.IOException; import java.util.List; import java.util.Map; +import java.util.function.BiFunction; public final class ScriptFieldMapper extends ParametrizedFieldMapper { @@ -69,6 +71,22 @@ protected String contentType() { public static class Builder extends ParametrizedFieldMapper.Builder { + static final Map> FIELD_TYPE_RESOLVER = Map.of( + KeywordFieldMapper.CONTENT_TYPE, + (builder, context) -> { + StringScriptFieldScript.Factory factory = builder.scriptCompiler.compile( + builder.script.getValue(), + StringScriptFieldScript.CONTEXT + ); + return new RuntimeKeywordMappedFieldType( + builder.buildFullName(context), + builder.script.getValue(), + factory, + builder.meta.getValue() + ); + } + ); + private static ScriptFieldMapper toType(FieldMapper in) { return (ScriptFieldMapper) in; } @@ -112,13 +130,13 @@ protected List> getParameters() { @Override public ScriptFieldMapper build(BuilderContext context) { - MappedFieldType mappedFieldType; - if (runtimeType.getValue().equals("keyword")) { - StringScriptFieldScript.Factory factory = scriptCompiler.compile(script.getValue(), StringScriptFieldScript.CONTEXT); - mappedFieldType = new RuntimeKeywordMappedFieldType(buildFullName(context), script.getValue(), factory, meta.getValue()); - } else { + BiFunction fieldTypeResolver = Builder.FIELD_TYPE_RESOLVER.get( + runtimeType.getValue() + ); + if (fieldTypeResolver == null) { throw new IllegalArgumentException("runtime_type [" + runtimeType.getValue() + "] not supported"); } + MappedFieldType mappedFieldType = fieldTypeResolver.apply(this, context); // TODO copy to and multi_fields should not be supported, parametrized field mapper needs to be adapted return new ScriptFieldMapper( name, diff --git a/x-pack/plugin/runtime-fields/src/test/java/org/elasticsearch/xpack/runtimefields/mapper/ScriptFieldMapperTests.java b/x-pack/plugin/runtime-fields/src/test/java/org/elasticsearch/xpack/runtimefields/mapper/ScriptFieldMapperTests.java index a0c791b61d91b..bbbf12db68c09 100644 --- a/x-pack/plugin/runtime-fields/src/test/java/org/elasticsearch/xpack/runtimefields/mapper/ScriptFieldMapperTests.java +++ b/x-pack/plugin/runtime-fields/src/test/java/org/elasticsearch/xpack/runtimefields/mapper/ScriptFieldMapperTests.java @@ -6,11 +6,14 @@ package org.elasticsearch.xpack.runtimefields.mapper; +import org.elasticsearch.action.fieldcaps.FieldCapabilities; +import org.elasticsearch.action.fieldcaps.FieldCapabilitiesResponse; import org.elasticsearch.common.Strings; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.common.xcontent.XContentFactory; import org.elasticsearch.index.mapper.FieldMapper; +import org.elasticsearch.index.mapper.KeywordFieldMapper; import org.elasticsearch.index.mapper.MapperParsingException; import org.elasticsearch.index.mapper.MapperService; import org.elasticsearch.plugins.Plugin; @@ -22,15 +25,22 @@ import org.elasticsearch.xpack.runtimefields.RuntimeFields; import org.elasticsearch.xpack.runtimefields.StringScriptFieldScript; +import java.util.Arrays; import java.util.Collection; import java.util.Map; import java.util.Set; +import static org.hamcrest.Matchers.arrayContainingInAnyOrder; import static org.hamcrest.Matchers.instanceOf; public class ScriptFieldMapperTests extends ESSingleNodeTestCase { - private static final String[] SUPPORTED_RUNTIME_TYPES = new String[] { "keyword" }; + private final String[] runtimeTypes; + + public ScriptFieldMapperTests() { + this.runtimeTypes = ScriptFieldMapper.Builder.FIELD_TYPE_RESOLVER.keySet().toArray(new String[0]); + Arrays.sort(runtimeTypes); + } @Override protected Collection> getPlugins() { @@ -45,7 +55,7 @@ public void testRuntimeTypeIsRequired() throws Exception { .startObject("properties") .startObject("my_field") .field("type", "script") - .field("script", "value('test')") + .field("script", "keyword('test')") .endObject() .endObject() .endObject() @@ -63,7 +73,7 @@ public void testScriptIsRequired() throws Exception { .startObject("properties") .startObject("my_field") .field("type", "script") - .field("runtime_type", randomFrom(SUPPORTED_RUNTIME_TYPES)) + .field("runtime_type", randomFrom(runtimeTypes)) .endObject() .endObject() .endObject() @@ -80,7 +90,7 @@ public void testStoredScriptsAreNotSupported() throws Exception { .startObject("properties") .startObject("my_field") .field("type", "script") - .field("runtime_type", randomFrom(SUPPORTED_RUNTIME_TYPES)) + .field("runtime_type", randomFrom(runtimeTypes)) .startObject("script") .field("id", "test") .endObject() @@ -104,7 +114,7 @@ public void testUnsupportedRuntimeType() throws Exception { .field("type", "script") .field("runtime_type", "unsupported") .startObject("script") - .field("source", "value('test')") + .field("source", "keyword('test')") .field("lang", "test") .endObject() .endObject() @@ -116,6 +126,53 @@ public void testUnsupportedRuntimeType() throws Exception { assertEquals("Failed to parse mapping: runtime_type [unsupported] not supported", exc.getMessage()); } + public void testFieldCaps() throws Exception { + for (String runtimeType : runtimeTypes) { + { + XContentBuilder mapping = XContentFactory.jsonBuilder() + .startObject() + .startObject("_doc") + .startObject("properties") + .startObject("field") + .field("type", "script") + .field("runtime_type", runtimeType) + .startObject("script") + .field("source", runtimeType + "('test')") + .field("lang", "test") + .endObject() + .endObject() + .endObject() + .endObject() + .endObject(); + createIndex("test_script", Settings.EMPTY, mapping); + } + { + XContentBuilder mapping = XContentFactory.jsonBuilder() + .startObject() + .startObject("_doc") + .startObject("properties") + .startObject("field") + .field("type", runtimeType) + .endObject() + .endObject() + .endObject() + .endObject(); + createIndex("test_concrete", Settings.EMPTY, mapping); + } + FieldCapabilitiesResponse response = client().prepareFieldCaps("test_*").setFields("field").get(); + assertThat(response.getIndices(), arrayContainingInAnyOrder("test_script", "test_concrete")); + Map field = response.getField("field"); + assertEquals(1, field.size()); + FieldCapabilities fieldCapabilities = field.get(KeywordFieldMapper.CONTENT_TYPE); + assertTrue(fieldCapabilities.isSearchable()); + assertTrue(fieldCapabilities.isAggregatable()); + assertEquals(runtimeType, fieldCapabilities.getType()); + assertNull(fieldCapabilities.nonAggregatableIndices()); + assertNull(fieldCapabilities.nonSearchableIndices()); + assertEquals("field", fieldCapabilities.getName()); + } + } + public void testDefaultMapping() throws Exception { XContentBuilder mapping = XContentFactory.jsonBuilder() .startObject() @@ -123,9 +180,9 @@ public void testDefaultMapping() throws Exception { .startObject("properties") .startObject("field") .field("type", "script") - .field("runtime_type", randomFrom(SUPPORTED_RUNTIME_TYPES)) + .field("runtime_type", randomFrom(runtimeTypes)) .startObject("script") - .field("source", "value('test')") + .field("source", "keyword('test')") .field("lang", "test") .endObject() .endObject() @@ -155,7 +212,7 @@ public FactoryType compile( ScriptContext context, Map paramsMap ) { - if ("value('test')".equals(code)) { + if ("keyword('test')".equals(code)) { StringScriptFieldScript.Factory factory = (params, lookup) -> ctx -> new StringScriptFieldScript( params, lookup,