diff --git a/CHANGELOG.md b/CHANGELOG.md index 3fcd41e600707..50fb1421449b3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), ## [Unreleased 3.x] ### Added - Use Lucene `pack` method for `half_float` and `usigned_long` when using `ApproximatePointRangeQuery`. +- Add a mapper for context aware segments grouping criteria ([#19233](https://github.com/opensearch-project/OpenSearch/pull/19233)) ### Changed - Refactor to move prepareIndex and prepareDelete methods to Engine class ([#19551](https://github.com/opensearch-project/OpenSearch/pull/19551)) diff --git a/modules/lang-painless/src/main/java/org/opensearch/painless/PainlessModulePlugin.java b/modules/lang-painless/src/main/java/org/opensearch/painless/PainlessModulePlugin.java index ea34763c487a1..af858b57471e3 100644 --- a/modules/lang-painless/src/main/java/org/opensearch/painless/PainlessModulePlugin.java +++ b/modules/lang-painless/src/main/java/org/opensearch/painless/PainlessModulePlugin.java @@ -59,6 +59,7 @@ import org.opensearch.repositories.RepositoriesService; import org.opensearch.rest.RestController; import org.opensearch.rest.RestHandler; +import org.opensearch.script.ContextAwareGroupingScript; import org.opensearch.script.DerivedFieldScript; import org.opensearch.script.IngestScript; import org.opensearch.script.ScoreScript; @@ -120,6 +121,9 @@ public final class PainlessModulePlugin extends Plugin implements ScriptPlugin, derived.add(AllowlistLoader.loadFromResourceFiles(Allowlist.class, "org.opensearch.derived.txt")); map.put(DerivedFieldScript.CONTEXT, derived); + // Only basic painless support for ContextAwareGrouping script + map.put(ContextAwareGroupingScript.CONTEXT, new ArrayList<>(Allowlist.BASE_ALLOWLISTS)); + allowlists = map; } diff --git a/modules/lang-painless/src/test/java/org/opensearch/painless/ContextAwareGroupingScriptTests.java b/modules/lang-painless/src/test/java/org/opensearch/painless/ContextAwareGroupingScriptTests.java new file mode 100644 index 0000000000000..3c27eb7d9b730 --- /dev/null +++ b/modules/lang-painless/src/test/java/org/opensearch/painless/ContextAwareGroupingScriptTests.java @@ -0,0 +1,67 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +package org.opensearch.painless; + +import org.opensearch.common.settings.Settings; +import org.opensearch.painless.spi.Allowlist; +import org.opensearch.script.ContextAwareGroupingScript; +import org.opensearch.script.ScriptContext; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +public class ContextAwareGroupingScriptTests extends ScriptTestCase { + + private static PainlessScriptEngine SCRIPT_ENGINE; + + @Override + public void setUp() throws Exception { + super.setUp(); + + Map, List> contexts = newDefaultContexts(); + List allowlists = new ArrayList<>(Allowlist.BASE_ALLOWLISTS); + contexts.put(ContextAwareGroupingScript.CONTEXT, allowlists); + + SCRIPT_ENGINE = new PainlessScriptEngine(Settings.EMPTY, contexts); + } + + @Override + public void tearDown() throws Exception { + super.tearDown(); + SCRIPT_ENGINE = null; + } + + @Override + protected PainlessScriptEngine getEngine() { + return SCRIPT_ENGINE; + } + + public void testContextAwareGroupingScript() { + String stringConcat = "ctx.value + \"-context-aware\""; + ContextAwareGroupingScript script = compile(stringConcat); + + assertEquals("value-context-aware", script.execute(Map.of("value", "value"))); + + String integerAddition = "String.valueOf(ctx.value / 100)"; + ContextAwareGroupingScript integerAdditionScript = compile(integerAddition); + assertEquals("2", integerAdditionScript.execute(Map.of("value", 200))); + } + + private ContextAwareGroupingScript compile(String expression) { + ContextAwareGroupingScript.Factory factory = getEngine().compile( + expression, + expression, + ContextAwareGroupingScript.CONTEXT, + Collections.emptyMap() + ); + return factory.newInstance(); + } +} diff --git a/modules/lang-painless/src/yamlRestTest/resources/rest-api-spec/test/painless/71_context_api.yml b/modules/lang-painless/src/yamlRestTest/resources/rest-api-spec/test/painless/71_context_api.yml index 20e6fd351a4b9..10fa6fa62ca14 100644 --- a/modules/lang-painless/src/yamlRestTest/resources/rest-api-spec/test/painless/71_context_api.yml +++ b/modules/lang-painless/src/yamlRestTest/resources/rest-api-spec/test/painless/71_context_api.yml @@ -2,7 +2,7 @@ - do: scripts_painless_context: {} - match: { contexts.0: aggregation_selector} - - match: { contexts.24: update} + - match: { contexts.25: update} --- "Action to get all API values for score context": diff --git a/server/src/main/java/org/opensearch/index/mapper/CompositeMappedFieldType.java b/server/src/main/java/org/opensearch/index/mapper/CompositeMappedFieldType.java index 7239ddfb26c0d..1c8254d96682c 100644 --- a/server/src/main/java/org/opensearch/index/mapper/CompositeMappedFieldType.java +++ b/server/src/main/java/org/opensearch/index/mapper/CompositeMappedFieldType.java @@ -50,7 +50,8 @@ public CompositeMappedFieldType(String name, List fields, CompositeField */ @ExperimentalApi public enum CompositeFieldType { - STAR_TREE("star_tree"); + STAR_TREE("star_tree"), + CONTEXT_AWARE_GROUPING("context_aware_grouping"); private final String name; diff --git a/server/src/main/java/org/opensearch/index/mapper/ContextAwareGroupingFieldMapper.java b/server/src/main/java/org/opensearch/index/mapper/ContextAwareGroupingFieldMapper.java new file mode 100644 index 0000000000000..37267371f8bf7 --- /dev/null +++ b/server/src/main/java/org/opensearch/index/mapper/ContextAwareGroupingFieldMapper.java @@ -0,0 +1,184 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +package org.opensearch.index.mapper; + +import org.opensearch.script.ContextAwareGroupingScript; +import org.opensearch.script.Script; + +import java.io.IOException; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +import static org.opensearch.script.Script.DEFAULT_SCRIPT_LANG; + +/** + * A field mapper to specify context aware grouping mapper creation + * + * @opensearch.internal + */ +public class ContextAwareGroupingFieldMapper extends ParametrizedFieldMapper { + + public static final String CONTENT_TYPE = "context_aware_grouping"; + + public static final Mapper.TypeParser PARSER = new TypeParser(); + + private static class TypeParser implements Mapper.TypeParser { + + @Override + public Mapper.Builder parse(String name, Map node, ParserContext context) throws MapperParsingException { + throw new IllegalStateException("ContextAwareGroupingFieldMapper needs objbuilder to validate node"); + } + + @Override + public Mapper.Builder parse(String name, Map node, ParserContext context, ObjectMapper.Builder objBuilder) + throws MapperParsingException { + Builder builder = new Builder(name); + builder.parse(name, context, node); + + if (builder.fields.isConfigured() == false) { + throw new MapperParsingException("[fields] in context_aware_grouping is required"); + } + + Set propertyFieldNames = (Set) objBuilder.mappersBuilders.stream() + .map(b -> ((Mapper.Builder) b).name()) + .collect(Collectors.toSet()); + + if (propertyFieldNames.containsAll(builder.fields.getValue()) == false) { + throw new MapperParsingException( + "[fields] should be from properties: [" + propertyFieldNames + "] but found [" + builder.fields.getValue() + "]" + ); + } + + final Script s = builder.script.getValue(); + if (s != null) { + ContextAwareGroupingScript.Factory factory = context.scriptService() + .compile(builder.script.get(), ContextAwareGroupingScript.CONTEXT); + builder.compiledScript = factory.newInstance(); + } + return builder; + } + } + + /** + * Builder for this field mapper + * + * @opensearch.internal + */ + public static class Builder extends ParametrizedFieldMapper.Builder { + + private static ContextAwareGroupingFieldMapper toType(FieldMapper in) { + return (ContextAwareGroupingFieldMapper) in; + } + + private final Parameter> fields = new Parameter<>("fields", true, Collections::emptyList, (n, c, o) -> { + if (!(o instanceof List)) { + throw new MapperParsingException("Expected [fields] to be a list of strings but got [" + o + "]"); + } + + List fields = (List) o; + if (fields.isEmpty()) { + throw new MapperParsingException("Expected [fields] in context_aware_grouping to have one value"); + } + + if (fields.size() > 1) { + throw new MapperParsingException("Currently [fields] in context_aware_grouping does not support multiple values"); + } + + return fields; + }, m -> toType(m).fields); + + private final Parameter