/* * Licensed to Elasticsearch under one or more contributor * license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright * ownership. Elasticsearch licenses this file to you under * the Apache License, Version 2.0 (the "License"); you may * not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.elasticsearch.search.sort; import org.apache.lucene.search.SortField; import org.elasticsearch.common.xcontent.XContentParser; import org.elasticsearch.common.xcontent.json.JsonXContent; import org.elasticsearch.index.query.QueryParseContext; import org.elasticsearch.script.Script; import org.elasticsearch.script.ScriptType; import org.elasticsearch.search.DocValueFormat; import org.elasticsearch.search.sort.ScriptSortBuilder.ScriptSortType; import java.io.IOException; import java.util.Collections; import java.util.HashSet; import java.util.Set; public class ScriptSortBuilderTests extends AbstractSortTestCase<ScriptSortBuilder> { @Override protected ScriptSortBuilder createTestItem() { return randomScriptSortBuilder(); } public static ScriptSortBuilder randomScriptSortBuilder() { ScriptSortType type = randomBoolean() ? ScriptSortType.NUMBER : ScriptSortType.STRING; ScriptSortBuilder builder = new ScriptSortBuilder(mockScript(randomAlphaOfLengthBetween(5, 10)), type); if (randomBoolean()) { builder.order(randomFrom(SortOrder.values())); } if (randomBoolean()) { if (type == ScriptSortType.NUMBER) { builder.sortMode(randomValueOtherThan(builder.sortMode(), () -> randomFrom(SortMode.values()))); } else { Set<SortMode> exceptThis = new HashSet<>(); exceptThis.add(SortMode.SUM); exceptThis.add(SortMode.AVG); exceptThis.add(SortMode.MEDIAN); builder.sortMode(randomValueOtherThanMany(exceptThis::contains, () -> randomFrom(SortMode.values()))); } } if (randomBoolean()) { builder.setNestedFilter(randomNestedFilter()); } if (randomBoolean()) { builder.setNestedPath(randomAlphaOfLengthBetween(1, 10)); } return builder; } @Override protected ScriptSortBuilder mutate(ScriptSortBuilder original) throws IOException { ScriptSortBuilder result; if (randomBoolean()) { // change one of the constructor args, copy the rest over Script script = original.script(); ScriptSortType type = original.type(); if (randomBoolean()) { result = new ScriptSortBuilder(mockScript(script.getIdOrCode() + "_suffix"), type); } else { result = new ScriptSortBuilder(script, type.equals(ScriptSortType.NUMBER) ? ScriptSortType.STRING : ScriptSortType.NUMBER); } result.order(original.order()); if (original.sortMode() != null && result.type() == ScriptSortType.NUMBER) { result.sortMode(original.sortMode()); } result.setNestedFilter(original.getNestedFilter()); result.setNestedPath(original.getNestedPath()); return result; } result = new ScriptSortBuilder(original); switch (randomIntBetween(0, 3)) { case 0: if (original.order() == SortOrder.ASC) { result.order(SortOrder.DESC); } else { result.order(SortOrder.ASC); } break; case 1: if (original.type() == ScriptSortType.NUMBER) { result.sortMode(randomValueOtherThan(result.sortMode(), () -> randomFrom(SortMode.values()))); } else { // script sort type String only allows MIN and MAX, so we only switch if (original.sortMode() == SortMode.MIN) { result.sortMode(SortMode.MAX); } else { result.sortMode(SortMode.MIN); } } break; case 2: result.setNestedFilter(randomValueOtherThan( original.getNestedFilter(), () -> randomNestedFilter())); break; case 3: result.setNestedPath(original.getNestedPath() + "_some_suffix"); break; } return result; } @Override protected void sortFieldAssertions(ScriptSortBuilder builder, SortField sortField, DocValueFormat format) throws IOException { assertEquals(SortField.Type.CUSTOM, sortField.getType()); assertEquals(builder.order() == SortOrder.ASC ? false : true, sortField.getReverse()); } public void testScriptSortType() { // we rely on these ordinals in serialization, so changing them breaks bwc. assertEquals(0, ScriptSortType.STRING.ordinal()); assertEquals(1, ScriptSortType.NUMBER.ordinal()); assertEquals("string", ScriptSortType.STRING.toString()); assertEquals("number", ScriptSortType.NUMBER.toString()); assertEquals(ScriptSortType.STRING, ScriptSortType.fromString("string")); assertEquals(ScriptSortType.STRING, ScriptSortType.fromString("String")); assertEquals(ScriptSortType.STRING, ScriptSortType.fromString("STRING")); assertEquals(ScriptSortType.NUMBER, ScriptSortType.fromString("number")); assertEquals(ScriptSortType.NUMBER, ScriptSortType.fromString("Number")); assertEquals(ScriptSortType.NUMBER, ScriptSortType.fromString("NUMBER")); } public void testScriptSortTypeNull() { Exception e = expectThrows(NullPointerException.class, () -> ScriptSortType.fromString(null)); assertEquals("input string is null", e.getMessage()); } public void testScriptSortTypeIllegalArgument() { Exception e = expectThrows(IllegalArgumentException.class, () -> ScriptSortType.fromString("xyz")); assertEquals("Unknown ScriptSortType [xyz]", e.getMessage()); } public void testParseJson() throws IOException { String scriptSort = "{\n" + "\"_script\" : {\n" + "\"type\" : \"number\",\n" + "\"script\" : {\n" + "\"inline\": \"doc['field_name'].value * factor\",\n" + "\"params\" : {\n" + "\"factor\" : 1.1\n" + "}\n" + "},\n" + "\"mode\" : \"max\",\n" + "\"order\" : \"asc\"\n" + "} }\n"; XContentParser parser = createParser(JsonXContent.jsonXContent, scriptSort); parser.nextToken(); parser.nextToken(); parser.nextToken(); QueryParseContext context = new QueryParseContext(parser); ScriptSortBuilder builder = ScriptSortBuilder.fromXContent(context, null); assertEquals("doc['field_name'].value * factor", builder.script().getIdOrCode()); assertEquals(Script.DEFAULT_SCRIPT_LANG, builder.script().getLang()); assertEquals(1.1, builder.script().getParams().get("factor")); assertEquals(ScriptType.INLINE, builder.script().getType()); assertEquals(ScriptSortType.NUMBER, builder.type()); assertEquals(SortOrder.ASC, builder.order()); assertEquals(SortMode.MAX, builder.sortMode()); assertNull(builder.getNestedFilter()); assertNull(builder.getNestedPath()); } public void testParseJson_simple() throws IOException { String scriptSort = "{\n" + "\"_script\" : {\n" + "\"type\" : \"number\",\n" + "\"script\" : \"doc['field_name'].value\",\n" + "\"mode\" : \"max\",\n" + "\"order\" : \"asc\"\n" + "} }\n"; XContentParser parser = createParser(JsonXContent.jsonXContent, scriptSort); parser.nextToken(); parser.nextToken(); parser.nextToken(); QueryParseContext context = new QueryParseContext(parser); ScriptSortBuilder builder = ScriptSortBuilder.fromXContent(context, null); assertEquals("doc['field_name'].value", builder.script().getIdOrCode()); assertEquals(Script.DEFAULT_SCRIPT_LANG, builder.script().getLang()); assertEquals(builder.script().getParams(), Collections.emptyMap()); assertEquals(ScriptType.INLINE, builder.script().getType()); assertEquals(ScriptSortType.NUMBER, builder.type()); assertEquals(SortOrder.ASC, builder.order()); assertEquals(SortMode.MAX, builder.sortMode()); assertNull(builder.getNestedFilter()); assertNull(builder.getNestedPath()); } public void testParseBadFieldNameExceptions() throws IOException { String scriptSort = "{\"_script\" : {" + "\"bad_field\" : \"number\"" + "} }"; XContentParser parser = createParser(JsonXContent.jsonXContent, scriptSort); parser.nextToken(); parser.nextToken(); parser.nextToken(); QueryParseContext context = new QueryParseContext(parser); Exception e = expectThrows(IllegalArgumentException.class, () -> ScriptSortBuilder.fromXContent(context, null)); assertEquals("[_script] unknown field [bad_field], parser not found", e.getMessage()); } public void testParseBadFieldNameExceptionsOnStartObject() throws IOException { String scriptSort = "{\"_script\" : {" + "\"bad_field\" : { \"order\" : \"asc\" } } }"; XContentParser parser = createParser(JsonXContent.jsonXContent, scriptSort); parser.nextToken(); parser.nextToken(); parser.nextToken(); QueryParseContext context = new QueryParseContext(parser); Exception e = expectThrows(IllegalArgumentException.class, () -> ScriptSortBuilder.fromXContent(context, null)); assertEquals("[_script] unknown field [bad_field], parser not found", e.getMessage()); } public void testParseUnexpectedToken() throws IOException { String scriptSort = "{\"_script\" : {" + "\"script\" : [ \"order\" : \"asc\" ] } }"; XContentParser parser = createParser(JsonXContent.jsonXContent, scriptSort); parser.nextToken(); parser.nextToken(); parser.nextToken(); QueryParseContext context = new QueryParseContext(parser); Exception e = expectThrows(IllegalArgumentException.class, () -> ScriptSortBuilder.fromXContent(context, null)); assertEquals("[_script] script doesn't support values of type: START_ARRAY", e.getMessage()); } /** * script sort of type {@link ScriptSortType} does not work with {@link SortMode#AVG}, {@link SortMode#MEDIAN} or {@link SortMode#SUM} */ public void testBadSortMode() throws IOException { ScriptSortBuilder builder = new ScriptSortBuilder(mockScript("something"), ScriptSortType.STRING); String sortMode = randomFrom(new String[] { "avg", "median", "sum" }); Exception e = expectThrows(IllegalArgumentException.class, () -> builder.sortMode(SortMode.fromString(sortMode))); assertEquals("script sort of type [string] doesn't support mode [" + sortMode + "]", e.getMessage()); } @Override protected ScriptSortBuilder fromXContent(QueryParseContext context, String fieldName) throws IOException { return ScriptSortBuilder.fromXContent(context, fieldName); } }