diff --git a/docs/changelog/101051.yaml b/docs/changelog/101051.yaml new file mode 100644 index 0000000000000..05e7443dac8b3 --- /dev/null +++ b/docs/changelog/101051.yaml @@ -0,0 +1,6 @@ +pr: 101051 +summary: Percolator to support parsing script score query with params +area: Mapping +type: bug +issues: + - 97377 diff --git a/modules/percolator/src/test/java/org/elasticsearch/percolator/PercolatorFieldMapperTests.java b/modules/percolator/src/test/java/org/elasticsearch/percolator/PercolatorFieldMapperTests.java index 5a12e0c9f3a37..b47364e3b1a08 100644 --- a/modules/percolator/src/test/java/org/elasticsearch/percolator/PercolatorFieldMapperTests.java +++ b/modules/percolator/src/test/java/org/elasticsearch/percolator/PercolatorFieldMapperTests.java @@ -30,6 +30,7 @@ import org.apache.lucene.search.join.ScoreMode; import org.apache.lucene.util.BytesRef; import org.elasticsearch.TransportVersion; +import org.elasticsearch.TransportVersions; import org.elasticsearch.action.support.PlainActionFuture; import org.elasticsearch.common.Strings; import org.elasticsearch.common.bytes.BytesArray; @@ -40,6 +41,7 @@ import org.elasticsearch.common.io.stream.NamedWriteableAwareStreamInput; import org.elasticsearch.common.io.stream.NamedWriteableRegistry; import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.common.network.InetAddresses; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.core.Tuple; @@ -53,6 +55,7 @@ import org.elasticsearch.index.mapper.ParsedDocument; import org.elasticsearch.index.mapper.SourceToParse; import org.elasticsearch.index.mapper.TestDocumentParserContext; +import org.elasticsearch.index.query.AbstractQueryBuilder; import org.elasticsearch.index.query.BoolQueryBuilder; import org.elasticsearch.index.query.BoostingQueryBuilder; import org.elasticsearch.index.query.ConstantScoreQueryBuilder; @@ -67,13 +70,17 @@ import org.elasticsearch.index.query.functionscore.FunctionScoreQueryBuilder; import org.elasticsearch.index.query.functionscore.RandomScoreFunctionBuilder; import org.elasticsearch.index.query.functionscore.ScriptScoreFunctionBuilder; +import org.elasticsearch.index.query.functionscore.ScriptScoreQueryBuilder; import org.elasticsearch.indices.TermsLookup; import org.elasticsearch.join.ParentJoinPlugin; import org.elasticsearch.join.query.HasChildQueryBuilder; import org.elasticsearch.join.query.HasParentQueryBuilder; import org.elasticsearch.plugins.Plugin; +import org.elasticsearch.plugins.SearchPlugin; import org.elasticsearch.script.MockScriptPlugin; import org.elasticsearch.script.Script; +import org.elasticsearch.script.ScriptType; +import org.elasticsearch.search.DummyQueryParserPlugin; import org.elasticsearch.test.ESSingleNodeTestCase; import org.elasticsearch.test.InternalSettingsPlugin; import org.elasticsearch.xcontent.XContentBuilder; @@ -92,6 +99,7 @@ import java.util.Collection; import java.util.Collections; import java.util.Comparator; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.function.Function; @@ -130,7 +138,13 @@ public class PercolatorFieldMapperTests extends ESSingleNodeTestCase { @Override protected Collection> getPlugins() { - return pluginList(InternalSettingsPlugin.class, PercolatorPlugin.class, FoolMeScriptPlugin.class, ParentJoinPlugin.class); + return pluginList( + InternalSettingsPlugin.class, + PercolatorPlugin.class, + FoolMeScriptPlugin.class, + ParentJoinPlugin.class, + CustomQueriesPlugin.class + ); } @Override @@ -540,6 +554,38 @@ public void testPercolatorFieldMapper() throws Exception { assertThat(doc.rootDoc().getFields(fieldType.extractionResultField.name()).get(0).stringValue(), equalTo(EXTRACTION_FAILED)); } + public void testParseScriptScoreQueryWithParams() throws Exception { + addQueryFieldMappings(); + ScriptScoreQueryBuilder scriptScoreQueryBuilder = new ScriptScoreQueryBuilder( + new MatchAllQueryBuilder(), + new Script(ScriptType.INLINE, Script.DEFAULT_SCRIPT_LANG, "score", Collections.singletonMap("param", "1")) + ); + ParsedDocument doc = mapperService.documentMapper() + .parse( + new SourceToParse( + "1", + BytesReference.bytes(XContentFactory.jsonBuilder().startObject().field(fieldName, scriptScoreQueryBuilder).endObject()), + XContentType.JSON + ) + ); + assertNotNull(doc); + } + + public void testParseCustomParserQuery() throws Exception { + addQueryFieldMappings(); + ParsedDocument doc = mapperService.documentMapper() + .parse( + new SourceToParse( + "1", + BytesReference.bytes( + XContentFactory.jsonBuilder().startObject().field(fieldName, new CustomParserQueryBuilder()).endObject() + ), + XContentType.JSON + ) + ); + assertNotNull(doc); + } + public void testStoringQueries() throws Exception { addQueryFieldMappings(); QueryBuilder[] queries = new QueryBuilder[] { @@ -1106,7 +1152,7 @@ public static class FoolMeScriptPlugin extends MockScriptPlugin { @Override protected Map, Object>> pluginScripts() { - return Collections.singletonMap("return true", (vars) -> true); + return Map.of("return true", (vars) -> true, "score", (vars) -> 0f); } @Override @@ -1114,4 +1160,139 @@ public String pluginScriptLang() { return Script.DEFAULT_SCRIPT_LANG; } } + + public static class CustomQueriesPlugin extends Plugin implements SearchPlugin { + @Override + public List> getQueries() { + return Collections.singletonList( + new QuerySpec( + CustomParserQueryBuilder.NAME, + CustomParserQueryBuilder::new, + CustomParserQueryBuilder::fromXContent + ) + ); + } + } + + public static final class CustomParserQueryBuilder extends AbstractQueryBuilder { + private static final String NAME = "CUSTOM"; + + CustomParserQueryBuilder() {} + + CustomParserQueryBuilder(StreamInput in) throws IOException { + super(in); + } + + @Override + protected void doWriteTo(StreamOutput out) { + // only the superclass has state + } + + @Override + protected Query doToQuery(SearchExecutionContext context) { + return new DummyQueryParserPlugin.DummyQuery(); + } + + @Override + protected int doHashCode() { + return 0; + } + + @Override + protected boolean doEquals(CustomParserQueryBuilder other) { + return true; + } + + @Override + public TransportVersion getMinimalSupportedVersion() { + return TransportVersions.ZERO; + } + + @Override + public String getWriteableName() { + return NAME; + } + + @Override + protected void doXContent(XContentBuilder builder, Params params) throws IOException { + builder.startObject(NAME); + builder.array("list", "value0", "value1", "value2"); + builder.array("listOrdered", "value0", "value1", "value2"); + builder.field("map"); + builder.map(Map.of("key1", "value1", "key2", "value2")); + builder.field("mapOrdered"); + builder.map(Map.of("key3", "value3", "key4", "value4")); + builder.field("mapStrings"); + builder.map(Map.of("key5", "value5", "key6", "value6")); + builder.field("mapSupplier"); + builder.map(Map.of("key7", "value7", "key8", "value8")); + builder.endObject(); + } + + public static CustomParserQueryBuilder fromXContent(XContentParser parser) throws IOException { + { + assertEquals(XContentParser.Token.FIELD_NAME, parser.nextToken()); + assertEquals("list", parser.currentName()); + List list = parser.list(); + assertEquals(3, list.size()); + for (int i = 0; i < 3; i++) { + assertEquals("value" + i, list.get(i).toString()); + } + assertEquals(XContentParser.Token.END_ARRAY, parser.currentToken()); + } + { + assertEquals(XContentParser.Token.FIELD_NAME, parser.nextToken()); + assertEquals("listOrdered", parser.currentName()); + List listOrdered = parser.listOrderedMap(); + assertEquals(3, listOrdered.size()); + for (int i = 0; i < 3; i++) { + assertEquals("value" + i, listOrdered.get(i).toString()); + } + assertEquals(XContentParser.Token.END_ARRAY, parser.currentToken()); + } + { + assertEquals(XContentParser.Token.FIELD_NAME, parser.nextToken()); + assertEquals("map", parser.currentName()); + assertEquals(XContentParser.Token.START_OBJECT, parser.nextToken()); + Map map = parser.map(); + assertEquals(2, map.size()); + assertEquals("value1", map.get("key1").toString()); + assertEquals("value2", map.get("key2").toString()); + assertEquals(XContentParser.Token.END_OBJECT, parser.currentToken()); + assertEquals(XContentParser.Token.FIELD_NAME, parser.nextToken()); + } + { + assertEquals("mapOrdered", parser.currentName()); + assertEquals(XContentParser.Token.START_OBJECT, parser.nextToken()); + Map mapOrdered = parser.mapOrdered(); + assertEquals(2, mapOrdered.size()); + assertEquals("value3", mapOrdered.get("key3").toString()); + assertEquals("value4", mapOrdered.get("key4").toString()); + assertEquals(XContentParser.Token.END_OBJECT, parser.currentToken()); + } + { + assertEquals(XContentParser.Token.FIELD_NAME, parser.nextToken()); + assertEquals("mapStrings", parser.currentName()); + assertEquals(XContentParser.Token.START_OBJECT, parser.nextToken()); + Map mapStrings = parser.map(); + assertEquals(2, mapStrings.size()); + assertEquals("value5", mapStrings.get("key5").toString()); + assertEquals("value6", mapStrings.get("key6").toString()); + assertEquals(XContentParser.Token.END_OBJECT, parser.currentToken()); + } + { + assertEquals(XContentParser.Token.FIELD_NAME, parser.nextToken()); + assertEquals("mapSupplier", parser.currentName()); + assertEquals(XContentParser.Token.START_OBJECT, parser.nextToken()); + Map mapSupplier = parser.map(HashMap::new, XContentParser::text); + assertEquals(2, mapSupplier.size()); + assertEquals("value7", mapSupplier.get("key7").toString()); + assertEquals("value8", mapSupplier.get("key8").toString()); + assertEquals(XContentParser.Token.END_OBJECT, parser.currentToken()); + } + + assertEquals(XContentParser.Token.END_OBJECT, parser.nextToken()); + return new CustomParserQueryBuilder(); + } + } } diff --git a/server/src/main/java/org/elasticsearch/index/mapper/DotExpandingXContentParser.java b/server/src/main/java/org/elasticsearch/index/mapper/DotExpandingXContentParser.java index 0d06eabb4f19b..6cf44ba6bc447 100644 --- a/server/src/main/java/org/elasticsearch/index/mapper/DotExpandingXContentParser.java +++ b/server/src/main/java/org/elasticsearch/index/mapper/DotExpandingXContentParser.java @@ -172,34 +172,60 @@ protected XContentParser delegate() { return parsers.peek(); } + /* + The following methods (map* and list*) are known not be called by DocumentParser when parsing documents, but we support indexing + percolator queries which are also parsed through DocumentParser, and their parsing code is completely up to each query, which are + also pluggable. That means that this parser needs to fully support parsing arbitrary content, when dots expansion is turned off. + We do throw UnsupportedOperationException when dots expansion is enabled as we don't expect such methods to be ever called in + those circumstances. + */ + @Override public Map map() throws IOException { + if (contentPath.isWithinLeafObject()) { + return super.map(); + } throw new UnsupportedOperationException(); } @Override public Map mapOrdered() throws IOException { + if (contentPath.isWithinLeafObject()) { + return super.mapOrdered(); + } throw new UnsupportedOperationException(); } @Override public Map mapStrings() throws IOException { + if (contentPath.isWithinLeafObject()) { + return super.mapStrings(); + } throw new UnsupportedOperationException(); } @Override public Map map(Supplier> mapFactory, CheckedFunction mapValueParser) throws IOException { + if (contentPath.isWithinLeafObject()) { + return super.map(mapFactory, mapValueParser); + } throw new UnsupportedOperationException(); } @Override public List list() throws IOException { + if (contentPath.isWithinLeafObject()) { + return super.list(); + } throw new UnsupportedOperationException(); } @Override public List listOrderedMap() throws IOException { + if (contentPath.isWithinLeafObject()) { + return super.listOrderedMap(); + } throw new UnsupportedOperationException(); } } diff --git a/server/src/test/java/org/elasticsearch/index/mapper/DotExpandingXContentParserTests.java b/server/src/test/java/org/elasticsearch/index/mapper/DotExpandingXContentParserTests.java index f9fcbebe221d4..c55ffaaa70a16 100644 --- a/server/src/test/java/org/elasticsearch/index/mapper/DotExpandingXContentParserTests.java +++ b/server/src/test/java/org/elasticsearch/index/mapper/DotExpandingXContentParserTests.java @@ -15,6 +15,9 @@ import org.elasticsearch.xcontent.json.JsonXContent; import java.io.IOException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; public class DotExpandingXContentParserTests extends ESTestCase { @@ -348,4 +351,94 @@ public void testGetTokenLocation() throws IOException { assertNull(dotExpandedParser.nextToken()); assertNull(expectedParser.nextToken()); } + + public void testParseMapUOE() throws Exception { + XContentParser dotExpandedParser = DotExpandingXContentParser.expandDots( + createParser(JsonXContent.jsonXContent, ""), + new ContentPath() + ); + expectThrows(UnsupportedOperationException.class, dotExpandedParser::map); + } + + public void testParseMapOrderedUOE() throws Exception { + XContentParser dotExpandedParser = DotExpandingXContentParser.expandDots( + createParser(JsonXContent.jsonXContent, ""), + new ContentPath() + ); + expectThrows(UnsupportedOperationException.class, dotExpandedParser::mapOrdered); + } + + public void testParseMapStringsUOE() throws Exception { + XContentParser dotExpandedParser = DotExpandingXContentParser.expandDots( + createParser(JsonXContent.jsonXContent, ""), + new ContentPath() + ); + expectThrows(UnsupportedOperationException.class, dotExpandedParser::mapStrings); + } + + public void testParseMapSupplierUOE() throws Exception { + XContentParser dotExpandedParser = DotExpandingXContentParser.expandDots( + createParser(JsonXContent.jsonXContent, ""), + new ContentPath() + ); + expectThrows(UnsupportedOperationException.class, () -> dotExpandedParser.map(HashMap::new, XContentParser::text)); + } + + public void testParseMap() throws Exception { + String jsonInput = """ + {"params":{"one":"one", + "two":"two"}}\ + """; + + ContentPath contentPath = new ContentPath(); + contentPath.setWithinLeafObject(true); + XContentParser dotExpandedParser = DotExpandingXContentParser.expandDots( + createParser(JsonXContent.jsonXContent, jsonInput), + contentPath + ); + assertEquals(XContentParser.Token.START_OBJECT, dotExpandedParser.nextToken()); + assertEquals(XContentParser.Token.FIELD_NAME, dotExpandedParser.nextToken()); + assertEquals("params", dotExpandedParser.currentName()); + assertEquals(XContentParser.Token.START_OBJECT, dotExpandedParser.nextToken()); + Map map = dotExpandedParser.map(); + assertEquals(2, map.size()); + assertEquals("one", map.get("one")); + assertEquals("two", map.get("two")); + } + + public void testParseListUOE() throws Exception { + XContentParser dotExpandedParser = DotExpandingXContentParser.expandDots( + createParser(JsonXContent.jsonXContent, ""), + new ContentPath() + ); + expectThrows(UnsupportedOperationException.class, dotExpandedParser::list); + } + + public void testParseListOrderedUOE() throws Exception { + XContentParser dotExpandedParser = DotExpandingXContentParser.expandDots( + createParser(JsonXContent.jsonXContent, ""), + new ContentPath() + ); + expectThrows(UnsupportedOperationException.class, dotExpandedParser::listOrderedMap); + } + + public void testParseList() throws Exception { + String jsonInput = """ + {"params":["one","two"]}\ + """; + + ContentPath contentPath = new ContentPath(); + contentPath.setWithinLeafObject(true); + XContentParser dotExpandedParser = DotExpandingXContentParser.expandDots( + createParser(JsonXContent.jsonXContent, jsonInput), + contentPath + ); + assertEquals(XContentParser.Token.START_OBJECT, dotExpandedParser.nextToken()); + assertEquals(XContentParser.Token.FIELD_NAME, dotExpandedParser.nextToken()); + assertEquals("params", dotExpandedParser.currentName()); + List list = dotExpandedParser.list(); + assertEquals(2, list.size()); + assertEquals("one", list.get(0)); + assertEquals("two", list.get(1)); + } }