-
Notifications
You must be signed in to change notification settings - Fork 25.3k
Make ScriptFieldMapper a parameterized mapper #59391
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
Changes from all commits
c2ec0a2
c913bbf
4458b71
99f1813
993ba66
7265381
6b7846d
67cc50d
0b69696
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -6,42 +6,54 @@ | |
|
||
package org.elasticsearch.xpack.runtimefields.mapper; | ||
|
||
import org.apache.lucene.document.FieldType; | ||
import org.apache.lucene.util.SetOnce; | ||
import org.elasticsearch.common.xcontent.XContentBuilder; | ||
import org.elasticsearch.common.xcontent.support.XContentMapValues; | ||
import org.elasticsearch.index.mapper.FieldMapper; | ||
import org.elasticsearch.index.mapper.MappedFieldType; | ||
import org.elasticsearch.index.mapper.Mapper; | ||
import org.elasticsearch.index.mapper.MapperParsingException; | ||
import org.elasticsearch.index.mapper.ParametrizedFieldMapper; | ||
import org.elasticsearch.index.mapper.ParseContext; | ||
import org.elasticsearch.script.Script; | ||
import org.elasticsearch.script.ScriptService; | ||
import org.elasticsearch.script.ScriptType; | ||
import org.elasticsearch.xpack.runtimefields.StringScriptFieldScript; | ||
|
||
import java.io.IOException; | ||
import java.util.Iterator; | ||
import java.util.List; | ||
import java.util.Map; | ||
|
||
public final class ScriptFieldMapper extends FieldMapper { | ||
public final class ScriptFieldMapper extends ParametrizedFieldMapper { | ||
|
||
public static final String CONTENT_TYPE = "script"; | ||
|
||
private static final FieldType FIELD_TYPE = new FieldType(); | ||
|
||
ScriptFieldMapper(String simpleName, MappedFieldType mappedFieldType, MultiFields multiFields, CopyTo copyTo) { | ||
super(simpleName, FIELD_TYPE, mappedFieldType, multiFields, copyTo); | ||
private final String runtimeType; | ||
private final Script script; | ||
private final ScriptService scriptService; | ||
|
||
protected ScriptFieldMapper( | ||
String simpleName, | ||
MappedFieldType mappedFieldType, | ||
MultiFields multiFields, | ||
CopyTo copyTo, | ||
String runtimeType, | ||
Script script, | ||
ScriptService scriptService | ||
) { | ||
super(simpleName, mappedFieldType, multiFields, copyTo); | ||
this.runtimeType = runtimeType; | ||
this.script = script; | ||
this.scriptService = scriptService; | ||
} | ||
|
||
@Override | ||
protected void parseCreateField(ParseContext context) { | ||
// there is no field! | ||
public ParametrizedFieldMapper.Builder getMergeBuilder() { | ||
return new ScriptFieldMapper.Builder(simpleName(), scriptService).init(this); | ||
} | ||
|
||
@Override | ||
protected void mergeOptions(FieldMapper other, List<String> conflicts) { | ||
// TODO implement this | ||
protected void parseCreateField(ParseContext context) { | ||
// there is no lucene field | ||
} | ||
|
||
@Override | ||
|
@@ -56,44 +68,76 @@ protected String contentType() { | |
return CONTENT_TYPE; | ||
} | ||
|
||
public static class Builder extends FieldMapper.Builder<Builder> { | ||
public static class Builder extends ParametrizedFieldMapper.Builder { | ||
|
||
private final ScriptService scriptService; | ||
private static ScriptFieldMapper toType(FieldMapper in) { | ||
return (ScriptFieldMapper) in; | ||
} | ||
|
||
private final Parameter<Map<String, String>> meta = Parameter.metaParam(); | ||
private final Parameter<String> runtimeType = Parameter.stringParam( | ||
"runtime_type", | ||
true, | ||
mapper -> toType(mapper).runtimeType, | ||
null | ||
).setValidator(runtimeType -> { | ||
if (runtimeType == null) { | ||
throw new IllegalArgumentException("runtime_type must be specified for script field [" + name + "]"); | ||
} | ||
}); | ||
// TODO script and runtime_type can be updated: what happens to the currently running queries when they get updated? | ||
// do all the shards get a consistent view? | ||
private final Parameter<Script> script = new Parameter<>( | ||
"script", | ||
true, | ||
null, | ||
Builder::parseScript, | ||
mapper -> toType(mapper).script | ||
).setValidator(script -> { | ||
if (script == null) { | ||
throw new IllegalArgumentException("script must be specified for script field [" + name + "]"); | ||
} | ||
}); | ||
|
||
private String runtimeType; | ||
private Script script; | ||
private final ScriptService scriptService; | ||
|
||
protected Builder(String name, ScriptService scriptService) { | ||
super(name, FIELD_TYPE); | ||
super(name); | ||
this.scriptService = scriptService; | ||
} | ||
|
||
public void runtimeType(String runtimeType) { | ||
this.runtimeType = runtimeType; | ||
} | ||
|
||
public void script(Script script) { | ||
this.script = script; | ||
@Override | ||
protected List<Parameter<?>> getParameters() { | ||
return List.of(meta, runtimeType, script); | ||
} | ||
|
||
@Override | ||
public ScriptFieldMapper build(BuilderContext context) { | ||
if (runtimeType == null) { | ||
throw new IllegalArgumentException("runtime_type must be specified"); | ||
} | ||
if (script == null) { | ||
throw new IllegalArgumentException("script must be specified"); | ||
} | ||
|
||
MappedFieldType mappedFieldType; | ||
if (runtimeType.equals("keyword")) { | ||
StringScriptFieldScript.Factory factory = scriptService.compile(script, StringScriptFieldScript.CONTEXT); | ||
mappedFieldType = new RuntimeKeywordMappedFieldType(buildFullName(context), script, factory, meta); | ||
if (runtimeType.getValue().equals("keyword")) { | ||
StringScriptFieldScript.Factory factory = scriptService.compile(script.getValue(), StringScriptFieldScript.CONTEXT); | ||
mappedFieldType = new RuntimeKeywordMappedFieldType(buildFullName(context), script.getValue(), factory, meta.getValue()); | ||
} else { | ||
throw new IllegalArgumentException("runtime_type [" + runtimeType + "] not supported"); | ||
} | ||
// TODO copy to and multi_fields... not sure what needs to be done. | ||
return new ScriptFieldMapper(name, mappedFieldType, multiFieldsBuilder.build(this, context), copyTo); | ||
// TODO copy to and multi_fields should not be supported, parametrized field mapper needs to be adapted | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @romseygeek and this one too. I think I would prefer making the changes upstream instead of adapting parameterized field mapper in the feature branch. Thoughts? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ++, I can work on this one. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. thanks that would be great! |
||
return new ScriptFieldMapper( | ||
name, | ||
mappedFieldType, | ||
multiFieldsBuilder.build(this, context), | ||
copyTo.build(), | ||
runtimeType.getValue(), | ||
script.getValue(), | ||
scriptService | ||
); | ||
} | ||
|
||
static Script parseScript(String name, Mapper.TypeParser.ParserContext parserContext, Object scriptObject) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I swear I've seen this before somewhere else... There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. you may have, but we only accept painless and we don't parse stored scripts. I will look though, I assumed that we never parse from a map but watcher may do that actually. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I found it, it was in update by query, I opened #59507 so that we can test and reuse that existing code |
||
Script script = Script.parse(scriptObject); | ||
if (script.getType() == ScriptType.STORED) { | ||
throw new IllegalArgumentException("stored scripts specified but not supported when defining script field [" + name + "]"); | ||
} | ||
return script; | ||
} | ||
} | ||
|
||
|
@@ -108,29 +152,9 @@ public void setScriptService(ScriptService scriptService) { | |
@Override | ||
public ScriptFieldMapper.Builder parse(String name, Map<String, Object> node, ParserContext parserContext) | ||
throws MapperParsingException { | ||
Builder builder = new Builder(name, scriptService.get()); | ||
for (Iterator<Map.Entry<String, Object>> iterator = node.entrySet().iterator(); iterator.hasNext();) { | ||
Map.Entry<String, Object> entry = iterator.next(); | ||
String propName = entry.getKey(); | ||
Object propNode = entry.getValue(); | ||
if (propName.equals("runtime_type")) { | ||
if (propNode == null) { | ||
throw new MapperParsingException("Property [runtime_type] cannot be null."); | ||
} | ||
builder.runtimeType(XContentMapValues.nodeStringValue(propNode, name + ".runtime_type")); | ||
iterator.remove(); | ||
} else if (propName.equals("script")) { | ||
if (propNode == null) { | ||
throw new MapperParsingException("Property [script] cannot be null."); | ||
} | ||
// TODO this should become an object and support the usual script syntax, including lang and params | ||
builder.script(new Script(XContentMapValues.nodeStringValue(propNode, name + ".script"))); | ||
iterator.remove(); | ||
} | ||
} | ||
// TODO these get passed in sometimes and we don't need them | ||
node.remove("doc_values"); | ||
node.remove("index"); | ||
|
||
ScriptFieldMapper.Builder builder = new ScriptFieldMapper.Builder(name, scriptService.get()); | ||
builder.parse(name, parserContext, node); | ||
return builder; | ||
} | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,160 @@ | ||
/* | ||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one | ||
* or more contributor license agreements. Licensed under the Elastic License; | ||
* you may not use this file except in compliance with the Elastic License. | ||
*/ | ||
|
||
package org.elasticsearch.xpack.runtimefields.mapper; | ||
|
||
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.MapperParsingException; | ||
import org.elasticsearch.index.mapper.MapperService; | ||
import org.elasticsearch.plugins.Plugin; | ||
import org.elasticsearch.plugins.ScriptPlugin; | ||
import org.elasticsearch.script.ScriptContext; | ||
import org.elasticsearch.script.ScriptEngine; | ||
import org.elasticsearch.test.ESSingleNodeTestCase; | ||
import org.elasticsearch.test.InternalSettingsPlugin; | ||
import org.elasticsearch.xpack.runtimefields.RuntimeFields; | ||
import org.elasticsearch.xpack.runtimefields.StringScriptFieldScript; | ||
|
||
import java.util.Collection; | ||
import java.util.Map; | ||
import java.util.Set; | ||
|
||
import static org.hamcrest.Matchers.instanceOf; | ||
|
||
public class ScriptFieldMapperTests extends ESSingleNodeTestCase { | ||
|
||
private static final String[] SUPPORTED_RUNTIME_TYPES = new String[] { "keyword" }; | ||
|
||
@Override | ||
protected Collection<Class<? extends Plugin>> getPlugins() { | ||
return pluginList(InternalSettingsPlugin.class, RuntimeFields.class, TestScriptPlugin.class); | ||
} | ||
|
||
public void testRuntimeTypeIsRequired() throws Exception { | ||
XContentBuilder mapping = XContentFactory.jsonBuilder() | ||
.startObject() | ||
.startObject("_doc") | ||
.startObject("properties") | ||
.startObject("my_field") | ||
.field("type", "script") | ||
.field("script", "value('test')") | ||
.endObject() | ||
.endObject() | ||
.endObject() | ||
.endObject(); | ||
|
||
MapperParsingException exception = expectThrows(MapperParsingException.class, () -> createIndex("test", Settings.EMPTY, mapping)); | ||
assertEquals("Failed to parse mapping: runtime_type must be specified for script field [my_field]", exception.getMessage()); | ||
} | ||
|
||
public void testScriptIsRequired() throws Exception { | ||
XContentBuilder mapping = XContentFactory.jsonBuilder() | ||
.startObject() | ||
.startObject("_doc") | ||
.startObject("properties") | ||
.startObject("my_field") | ||
.field("type", "script") | ||
.field("runtime_type", randomFrom(SUPPORTED_RUNTIME_TYPES)) | ||
.endObject() | ||
.endObject() | ||
.endObject() | ||
.endObject(); | ||
|
||
MapperParsingException exception = expectThrows(MapperParsingException.class, () -> createIndex("test", Settings.EMPTY, mapping)); | ||
assertEquals("Failed to parse mapping: script must be specified for script field [my_field]", exception.getMessage()); | ||
} | ||
|
||
public void testStoredScriptsAreNotSupported() throws Exception { | ||
XContentBuilder mapping = XContentFactory.jsonBuilder() | ||
.startObject() | ||
.startObject("_doc") | ||
.startObject("properties") | ||
.startObject("my_field") | ||
.field("type", "script") | ||
.field("runtime_type", randomFrom(SUPPORTED_RUNTIME_TYPES)) | ||
.startObject("script") | ||
.field("id", "test") | ||
.endObject() | ||
.endObject() | ||
.endObject() | ||
.endObject() | ||
.endObject(); | ||
MapperParsingException exception = expectThrows(MapperParsingException.class, () -> createIndex("test", Settings.EMPTY, mapping)); | ||
assertEquals( | ||
"Failed to parse mapping: stored scripts specified but not supported when defining script field [my_field]", | ||
exception.getMessage() | ||
); | ||
} | ||
|
||
public void testDefaultMapping() throws Exception { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is the idea to test that we can properly parse a simple version of the mapper? I don't really get the name of the method. I will try and hack on it locally and let you know what I find. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. the name is from a copy/paste. I wanted to test that the mapper works indeed. |
||
XContentBuilder mapping = XContentFactory.jsonBuilder() | ||
.startObject() | ||
.startObject("_doc") | ||
.startObject("properties") | ||
.startObject("field") | ||
.field("type", "script") | ||
.field("runtime_type", randomFrom(SUPPORTED_RUNTIME_TYPES)) | ||
.startObject("script") | ||
.field("source", "value('test')") | ||
.field("lang", "test") | ||
.endObject() | ||
.endObject() | ||
.endObject() | ||
.endObject() | ||
.endObject(); | ||
|
||
MapperService mapperService = createIndex("test", Settings.EMPTY, mapping).mapperService(); | ||
FieldMapper mapper = (FieldMapper) mapperService.documentMapper().mappers().getMapper("field"); | ||
assertThat(mapper, instanceOf(ScriptFieldMapper.class)); | ||
assertEquals(Strings.toString(mapping), Strings.toString(mapperService.documentMapper())); | ||
} | ||
|
||
public static class TestScriptPlugin extends Plugin implements ScriptPlugin { | ||
@Override | ||
public ScriptEngine getScriptEngine(Settings settings, Collection<ScriptContext<?>> contexts) { | ||
return new ScriptEngine() { | ||
@Override | ||
public String getType() { | ||
return "test"; | ||
} | ||
|
||
@Override | ||
public <FactoryType> FactoryType compile( | ||
String name, | ||
String code, | ||
ScriptContext<FactoryType> context, | ||
Map<String, String> paramsMap | ||
) { | ||
if ("value('test')".equals(code)) { | ||
StringScriptFieldScript.Factory factory = (params, lookup) -> ctx -> new StringScriptFieldScript( | ||
params, | ||
lookup, | ||
ctx | ||
) { | ||
@Override | ||
public void execute() { | ||
this.results.add("test"); | ||
} | ||
}; | ||
@SuppressWarnings("unchecked") | ||
FactoryType castFactory = (FactoryType) factory; | ||
return castFactory; | ||
} | ||
throw new IllegalArgumentException("No test script for [" + code + "]"); | ||
} | ||
|
||
@Override | ||
public Set<ScriptContext<?>> getSupportedContexts() { | ||
return Set.of(StringScriptFieldScript.CONTEXT); | ||
} | ||
}; | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@romseygeek do you have thoughts on this?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So currently
QueryShardContext#fieldMapper()
delegates field lookups back to the MapperService, which gets updated in-place, so if there's an update while a query is running then the script will get changed out and there may well be inconsistencies between phases, as well as between shards. We could make QueryShardContext take a FieldTypeLookup as a parameter which would at least remove the possibility of an update between phases?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We have this sort of problem now with any thing that can change. I guess scripts make this worse because they are "more mutable" than other stuff.