summaryrefslogtreecommitdiffstats
path: root/config-model/src/test/java/com/yahoo/schema
diff options
context:
space:
mode:
authorJon Bratseth <bratseth@gmail.com>2022-05-19 12:03:06 +0200
committerJon Bratseth <bratseth@gmail.com>2022-05-19 12:03:06 +0200
commit5c24dc5c9642a8d9ed70aee4c950fd0678a1ebec (patch)
treebd9b74bf00c832456f0b83c1b2cd7010be387d68 /config-model/src/test/java/com/yahoo/schema
parentf17c4fe7de4c55f5c4ee61897eab8c2f588d8405 (diff)
Rename the 'searchdefinition' package to 'schema'
Diffstat (limited to 'config-model/src/test/java/com/yahoo/schema')
-rw-r--r--config-model/src/test/java/com/yahoo/schema/AbstractSchemaTestCase.java82
-rw-r--r--config-model/src/test/java/com/yahoo/schema/AnnotationReferenceTestCase.java67
-rw-r--r--config-model/src/test/java/com/yahoo/schema/ArraysTestCase.java35
-rw-r--r--config-model/src/test/java/com/yahoo/schema/ArraysWeightedSetsTestCase.java41
-rw-r--r--config-model/src/test/java/com/yahoo/schema/AttributeSettingsTestCase.java344
-rw-r--r--config-model/src/test/java/com/yahoo/schema/AttributeUtils.java15
-rw-r--r--config-model/src/test/java/com/yahoo/schema/CommentTestCase.java27
-rw-r--r--config-model/src/test/java/com/yahoo/schema/DiversityTestCase.java110
-rw-r--r--config-model/src/test/java/com/yahoo/schema/DocumentGraphValidatorTest.java166
-rw-r--r--config-model/src/test/java/com/yahoo/schema/DocumentReferenceResolverTest.java103
-rw-r--r--config-model/src/test/java/com/yahoo/schema/FeatureNamesTestCase.java94
-rw-r--r--config-model/src/test/java/com/yahoo/schema/FieldOfTypeDocumentTestCase.java57
-rw-r--r--config-model/src/test/java/com/yahoo/schema/ImportedFieldsEnumeratorTest.java73
-rw-r--r--config-model/src/test/java/com/yahoo/schema/IncorrectRankingExpressionFileRefTestCase.java36
-rw-r--r--config-model/src/test/java/com/yahoo/schema/IncorrectSummaryTypesTestCase.java36
-rw-r--r--config-model/src/test/java/com/yahoo/schema/IndexSettingsTestCase.java62
-rw-r--r--config-model/src/test/java/com/yahoo/schema/IndexingParsingTestCase.java32
-rw-r--r--config-model/src/test/java/com/yahoo/schema/MultipleSummariesTestCase.java24
-rw-r--r--config-model/src/test/java/com/yahoo/schema/NameFieldCheckTestCase.java80
-rw-r--r--config-model/src/test/java/com/yahoo/schema/OutsideTestCase.java32
-rw-r--r--config-model/src/test/java/com/yahoo/schema/PredicateDataTypeTestCase.java199
-rw-r--r--config-model/src/test/java/com/yahoo/schema/RankProfileRegistryTest.java58
-rw-r--r--config-model/src/test/java/com/yahoo/schema/RankProfileTestCase.java436
-rw-r--r--config-model/src/test/java/com/yahoo/schema/RankPropertiesTestCase.java163
-rw-r--r--config-model/src/test/java/com/yahoo/schema/RankingConstantTest.java213
-rw-r--r--config-model/src/test/java/com/yahoo/schema/RankingExpressionConstantsTestCase.java229
-rw-r--r--config-model/src/test/java/com/yahoo/schema/RankingExpressionInliningTestCase.java274
-rw-r--r--config-model/src/test/java/com/yahoo/schema/RankingExpressionLoopDetectionTestCase.java246
-rw-r--r--config-model/src/test/java/com/yahoo/schema/RankingExpressionShadowingTestCase.java251
-rw-r--r--config-model/src/test/java/com/yahoo/schema/RankingExpressionValidationTestCase.java52
-rw-r--r--config-model/src/test/java/com/yahoo/schema/ReservedWordsAsFieldNamesTestCase.java24
-rwxr-xr-xconfig-model/src/test/java/com/yahoo/schema/SDDocumentTypeOrdererTestCase.java78
-rw-r--r--config-model/src/test/java/com/yahoo/schema/SchemaImporterTestCase.java189
-rw-r--r--config-model/src/test/java/com/yahoo/schema/SchemaParsingTestCase.java83
-rw-r--r--config-model/src/test/java/com/yahoo/schema/SchemaTestCase.java443
-rw-r--r--config-model/src/test/java/com/yahoo/schema/StemmingSettingTestCase.java51
-rwxr-xr-xconfig-model/src/test/java/com/yahoo/schema/StructTestCase.java59
-rw-r--r--config-model/src/test/java/com/yahoo/schema/SummaryTestCase.java286
-rw-r--r--config-model/src/test/java/com/yahoo/schema/UrlFieldValidationTestCase.java34
-rw-r--r--config-model/src/test/java/com/yahoo/schema/derived/AbstractExportingTestCase.java165
-rwxr-xr-xconfig-model/src/test/java/com/yahoo/schema/derived/AnnotationsTestCase.java66
-rw-r--r--config-model/src/test/java/com/yahoo/schema/derived/ArraysTestCase.java23
-rw-r--r--config-model/src/test/java/com/yahoo/schema/derived/AttributeListTestCase.java129
-rw-r--r--config-model/src/test/java/com/yahoo/schema/derived/AttributesTestCase.java36
-rw-r--r--config-model/src/test/java/com/yahoo/schema/derived/CasingTestCase.java36
-rw-r--r--config-model/src/test/java/com/yahoo/schema/derived/CombinedAttributeAndIndexSchemaTestCase.java21
-rw-r--r--config-model/src/test/java/com/yahoo/schema/derived/DeriverTestCase.java31
-rw-r--r--config-model/src/test/java/com/yahoo/schema/derived/DuplicateStructTestCase.java19
-rw-r--r--config-model/src/test/java/com/yahoo/schema/derived/EmptyRankProfileTestCase.java38
-rw-r--r--config-model/src/test/java/com/yahoo/schema/derived/ExactMatchTestCase.java17
-rw-r--r--config-model/src/test/java/com/yahoo/schema/derived/ExportingTestCase.java195
-rw-r--r--config-model/src/test/java/com/yahoo/schema/derived/ExpressionsAsArgsTestCase.java25
-rw-r--r--config-model/src/test/java/com/yahoo/schema/derived/FieldsetTestCase.java16
-rw-r--r--config-model/src/test/java/com/yahoo/schema/derived/GeminiTestCase.java70
-rw-r--r--config-model/src/test/java/com/yahoo/schema/derived/IdTestCase.java49
-rw-r--r--config-model/src/test/java/com/yahoo/schema/derived/ImportedFieldsTestCase.java42
-rw-r--r--config-model/src/test/java/com/yahoo/schema/derived/IndexSchemaTestCase.java208
-rw-r--r--config-model/src/test/java/com/yahoo/schema/derived/InheritanceTestCase.java192
-rw-r--r--config-model/src/test/java/com/yahoo/schema/derived/IntegerAttributeToStringIndexTestCase.java17
-rw-r--r--config-model/src/test/java/com/yahoo/schema/derived/LiteralBoostTestCase.java113
-rw-r--r--config-model/src/test/java/com/yahoo/schema/derived/LowercaseTestCase.java19
-rw-r--r--config-model/src/test/java/com/yahoo/schema/derived/MailTestCase.java24
-rwxr-xr-xconfig-model/src/test/java/com/yahoo/schema/derived/MatchSettingsResolvingTestCase.java63
-rw-r--r--config-model/src/test/java/com/yahoo/schema/derived/MultiStructTestCase.java27
-rw-r--r--config-model/src/test/java/com/yahoo/schema/derived/MultipleSummariesTestCase.java22
-rw-r--r--config-model/src/test/java/com/yahoo/schema/derived/NameCollisionTestCase.java27
-rw-r--r--config-model/src/test/java/com/yahoo/schema/derived/NativeRankTypeDefinitionsTestCase.java92
-rw-r--r--config-model/src/test/java/com/yahoo/schema/derived/NearestNeighborTestCase.java39
-rw-r--r--config-model/src/test/java/com/yahoo/schema/derived/NeuralNetTestCase.java42
-rw-r--r--config-model/src/test/java/com/yahoo/schema/derived/NuwaTestCase.java33
-rwxr-xr-xconfig-model/src/test/java/com/yahoo/schema/derived/OrderIlscriptsTestCase.java19
-rw-r--r--config-model/src/test/java/com/yahoo/schema/derived/PrefixExactAttributeTestCase.java21
-rw-r--r--config-model/src/test/java/com/yahoo/schema/derived/RankProfilesTestCase.java20
-rw-r--r--config-model/src/test/java/com/yahoo/schema/derived/RankPropertiesTestCase.java19
-rw-r--r--config-model/src/test/java/com/yahoo/schema/derived/ReferenceFieldsTestCase.java18
-rw-r--r--config-model/src/test/java/com/yahoo/schema/derived/ReferenceFromSeveralTestCase.java27
-rw-r--r--config-model/src/test/java/com/yahoo/schema/derived/SchemaInheritanceTestCase.java34
-rw-r--r--config-model/src/test/java/com/yahoo/schema/derived/SchemaOrdererTestCase.java146
-rw-r--r--config-model/src/test/java/com/yahoo/schema/derived/SimpleInheritTestCase.java47
-rw-r--r--config-model/src/test/java/com/yahoo/schema/derived/SliceTestCase.java23
-rw-r--r--config-model/src/test/java/com/yahoo/schema/derived/SortingTestCase.java22
-rwxr-xr-xconfig-model/src/test/java/com/yahoo/schema/derived/StreamingStructTestCase.java26
-rwxr-xr-xconfig-model/src/test/java/com/yahoo/schema/derived/StructAnyOrderTestCase.java17
-rw-r--r--config-model/src/test/java/com/yahoo/schema/derived/StructInheritanceTestCase.java52
-rw-r--r--config-model/src/test/java/com/yahoo/schema/derived/SummaryMapTestCase.java187
-rw-r--r--config-model/src/test/java/com/yahoo/schema/derived/SummaryTestCase.java182
-rw-r--r--config-model/src/test/java/com/yahoo/schema/derived/TestableDeployLogger.java30
-rwxr-xr-xconfig-model/src/test/java/com/yahoo/schema/derived/TokenizationTestCase.java19
-rw-r--r--config-model/src/test/java/com/yahoo/schema/derived/TwoStreamingStructsTestCase.java34
-rw-r--r--config-model/src/test/java/com/yahoo/schema/derived/TypeConversionTestCase.java44
-rw-r--r--config-model/src/test/java/com/yahoo/schema/derived/TypesTestCase.java21
-rw-r--r--config-model/src/test/java/com/yahoo/schema/derived/VsmFieldsTestCase.java42
-rw-r--r--config-model/src/test/java/com/yahoo/schema/document/ComplexAttributeFieldUtilsTestCase.java244
-rw-r--r--config-model/src/test/java/com/yahoo/schema/document/HnswIndexParamsTestCase.java51
-rw-r--r--config-model/src/test/java/com/yahoo/schema/parser/ConvertIntermediateTestCase.java95
-rw-r--r--config-model/src/test/java/com/yahoo/schema/parser/IntermediateCollectionTestCase.java236
-rw-r--r--config-model/src/test/java/com/yahoo/schema/parser/ParsedDocumentTestCase.java30
-rw-r--r--config-model/src/test/java/com/yahoo/schema/parser/SchemaParserTestCase.java276
-rw-r--r--config-model/src/test/java/com/yahoo/schema/processing/AddAttributeTransformToSummaryOfImportedFieldsTest.java75
-rw-r--r--config-model/src/test/java/com/yahoo/schema/processing/AdjustPositionSummaryFieldsTestCase.java260
-rw-r--r--config-model/src/test/java/com/yahoo/schema/processing/AssertIndexingScript.java43
-rw-r--r--config-model/src/test/java/com/yahoo/schema/processing/AssertSearchBuilder.java29
-rw-r--r--config-model/src/test/java/com/yahoo/schema/processing/AttributesExactMatchTestCase.java40
-rw-r--r--config-model/src/test/java/com/yahoo/schema/processing/BoldingTestCase.java65
-rw-r--r--config-model/src/test/java/com/yahoo/schema/processing/BoolAttributeValidatorTestCase.java50
-rw-r--r--config-model/src/test/java/com/yahoo/schema/processing/DictionaryTestCase.java250
-rw-r--r--config-model/src/test/java/com/yahoo/schema/processing/DisallowComplexMapAndWsetKeyTypesTestCase.java57
-rw-r--r--config-model/src/test/java/com/yahoo/schema/processing/FastAccessValidatorTest.java61
-rw-r--r--config-model/src/test/java/com/yahoo/schema/processing/ImplicitSchemaFieldsTestCase.java94
-rw-r--r--config-model/src/test/java/com/yahoo/schema/processing/ImplicitStructTypesTestCase.java69
-rw-r--r--config-model/src/test/java/com/yahoo/schema/processing/ImplicitSummariesTestCase.java78
-rw-r--r--config-model/src/test/java/com/yahoo/schema/processing/ImplicitSummaryFieldsTestCase.java29
-rw-r--r--config-model/src/test/java/com/yahoo/schema/processing/ImportedFieldsResolverTestCase.java152
-rw-r--r--config-model/src/test/java/com/yahoo/schema/processing/ImportedFieldsTestCase.java529
-rw-r--r--config-model/src/test/java/com/yahoo/schema/processing/IndexingInputsTestCase.java45
-rw-r--r--config-model/src/test/java/com/yahoo/schema/processing/IndexingOutputsTestCase.java30
-rw-r--r--config-model/src/test/java/com/yahoo/schema/processing/IndexingScriptRewriterTestCase.java200
-rw-r--r--config-model/src/test/java/com/yahoo/schema/processing/IndexingValidationTestCase.java76
-rw-r--r--config-model/src/test/java/com/yahoo/schema/processing/IndexingValuesTestCase.java30
-rw-r--r--config-model/src/test/java/com/yahoo/schema/processing/IntegerIndex2AttributeTestCase.java61
-rw-r--r--config-model/src/test/java/com/yahoo/schema/processing/MatchPhaseSettingsValidatorTestCase.java37
-rw-r--r--config-model/src/test/java/com/yahoo/schema/processing/MatchedElementsOnlyResolverTestCase.java192
-rw-r--r--config-model/src/test/java/com/yahoo/schema/processing/NGramTestCase.java88
-rw-r--r--config-model/src/test/java/com/yahoo/schema/processing/PagedAttributeValidatorTestCase.java119
-rw-r--r--config-model/src/test/java/com/yahoo/schema/processing/ParentChildSearchModel.java64
-rw-r--r--config-model/src/test/java/com/yahoo/schema/processing/PositionTestCase.java130
-rw-r--r--config-model/src/test/java/com/yahoo/schema/processing/RankModifierTestCase.java22
-rw-r--r--config-model/src/test/java/com/yahoo/schema/processing/RankProfileSearchFixture.java128
-rw-r--r--config-model/src/test/java/com/yahoo/schema/processing/RankPropertyVariablesTestCase.java47
-rw-r--r--config-model/src/test/java/com/yahoo/schema/processing/RankingExpressionTypeResolverTestCase.java521
-rw-r--r--config-model/src/test/java/com/yahoo/schema/processing/RankingExpressionWithLightGBMTestCase.java88
-rw-r--r--config-model/src/test/java/com/yahoo/schema/processing/RankingExpressionWithOnnxModelTestCase.java184
-rw-r--r--config-model/src/test/java/com/yahoo/schema/processing/RankingExpressionWithOnnxTestCase.java417
-rw-r--r--config-model/src/test/java/com/yahoo/schema/processing/RankingExpressionWithTensorTestCase.java202
-rw-r--r--config-model/src/test/java/com/yahoo/schema/processing/RankingExpressionWithTransformerTokensTestCase.java98
-rw-r--r--config-model/src/test/java/com/yahoo/schema/processing/RankingExpressionWithXGBoostTestCase.java90
-rw-r--r--config-model/src/test/java/com/yahoo/schema/processing/RankingExpressionsTestCase.java128
-rw-r--r--config-model/src/test/java/com/yahoo/schema/processing/ReferenceFieldTestCase.java92
-rw-r--r--config-model/src/test/java/com/yahoo/schema/processing/ReservedDocumentNamesTestCase.java27
-rw-r--r--config-model/src/test/java/com/yahoo/schema/processing/ReservedRankingExpressionFunctionNamesTestCase.java71
-rw-r--r--config-model/src/test/java/com/yahoo/schema/processing/SchemaMustHaveDocumentTest.java30
-rw-r--r--config-model/src/test/java/com/yahoo/schema/processing/SummaryConsistencyTestCase.java45
-rw-r--r--config-model/src/test/java/com/yahoo/schema/processing/SummaryFieldsMustHaveValidSourceTestCase.java60
-rw-r--r--config-model/src/test/java/com/yahoo/schema/processing/TensorFieldTestCase.java172
-rw-r--r--config-model/src/test/java/com/yahoo/schema/processing/TensorTransformTestCase.java234
-rw-r--r--config-model/src/test/java/com/yahoo/schema/processing/ValidateFieldTypesTest.java80
-rw-r--r--config-model/src/test/java/com/yahoo/schema/processing/VespaMlModelTestCase.java77
-rw-r--r--config-model/src/test/java/com/yahoo/schema/processing/WeightedSetSummaryToTestCase.java23
148 files changed, 14551 insertions, 0 deletions
diff --git a/config-model/src/test/java/com/yahoo/schema/AbstractSchemaTestCase.java b/config-model/src/test/java/com/yahoo/schema/AbstractSchemaTestCase.java
new file mode 100644
index 00000000000..e816456249f
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/schema/AbstractSchemaTestCase.java
@@ -0,0 +1,82 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.schema;
+
+import com.yahoo.io.IOUtils;
+
+import java.io.BufferedReader;
+import java.io.BufferedWriter;
+import java.io.IOException;
+
+import static helpers.CompareConfigTestHelper.assertSerializedConfigEquals;
+import static helpers.CompareConfigTestHelper.assertSerializedConfigFileEquals;
+
+public abstract class AbstractSchemaTestCase {
+
+ protected static void assertConfigFile(String filename, String cfg) throws IOException {
+ IOUtils.writeFile(filename + ".actual", cfg, false);
+ if (! cfg.endsWith("\n")) {
+ IOUtils.writeFile(filename + ".actual", "\n", true);
+ }
+ assertSerializedConfigFileEquals(filename, cfg);
+ }
+
+ protected static void assertConfigFiles(String expectedFile,
+ String cfgFile,
+ boolean orderMatters,
+ boolean updateOnAssert) throws IOException {
+ try {
+ assertSerializedConfigEquals(readAndCensorIndexes(expectedFile), readAndCensorIndexes(cfgFile), orderMatters);
+ } catch (AssertionError e) {
+ if (updateOnAssert) {
+ BufferedWriter writer = IOUtils.createWriter(expectedFile, false);
+ writer.write(readAndCensorIndexes(cfgFile));
+ writer.newLine();
+ writer.flush();
+ writer.close();
+ System.err.println(e.getMessage() + " [not equal files: >>>"+expectedFile+"<<< and >>>"+cfgFile+"<<< in assertConfigFiles]");
+ return;
+ }
+ throw new AssertionError(e.getMessage() + " [not equal files: >>>"+expectedFile+"<<< and >>>"+cfgFile+"<<< in assertConfigFiles]", e);
+ }
+ }
+ /**
+ * This is to avoid having to keep those pesky array index numbers in the config format up to date
+ * as new entries are added and removed.
+ */
+ private static String readAndCensorIndexes(String file) throws IOException {
+ StringBuilder b = new StringBuilder();
+ try (BufferedReader r = IOUtils.createReader(file)) {
+ int character;
+ boolean lastWasNewline = false;
+ boolean inBrackets = false;
+ while (-1 != (character = r.read())) {
+ // skip empty lines
+ if (character == '\n') {
+ if (lastWasNewline) continue;
+ lastWasNewline = true;
+ }
+ else {
+ lastWasNewline = false;
+ }
+
+ // skip quoted strings
+ if (character == '"') {
+ b.appendCodePoint(character);
+ while (-1 != (character = r.read()) && character != '"') {
+ b.appendCodePoint(character);
+ }
+ }
+
+ // skip bracket content
+ if (character == ']')
+ inBrackets = false;
+ if (! inBrackets)
+ b.appendCodePoint(character);
+ if (character == '[')
+ inBrackets = true;
+ }
+ }
+ return b.toString();
+ }
+
+}
diff --git a/config-model/src/test/java/com/yahoo/schema/AnnotationReferenceTestCase.java b/config-model/src/test/java/com/yahoo/schema/AnnotationReferenceTestCase.java
new file mode 100644
index 00000000000..dbe827ed67f
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/schema/AnnotationReferenceTestCase.java
@@ -0,0 +1,67 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.schema;
+
+import com.yahoo.document.DataType;
+import com.yahoo.document.StructDataType;
+import com.yahoo.document.Field;
+import com.yahoo.document.annotation.AnnotationReferenceDataType;
+import com.yahoo.schema.document.SDDocumentType;
+import com.yahoo.config.model.deploy.TestProperties;
+import org.junit.Test;
+
+import static com.yahoo.config.model.test.TestUtil.joinLines;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * @author arnej
+ */
+public class AnnotationReferenceTestCase {
+
+ static final String sd =
+ joinLines("search test {",
+ " document test { ",
+ " struct mystruct {",
+ " field x type int {}",
+ " }",
+ " field a type string {}",
+ " field b type mystruct {}",
+ " annotation marker {}",
+ " annotation person {",
+ " field name type string {}",
+ " field age type int {}",
+ " }",
+ " annotation complex {",
+ " field title type string {}",
+ " field tag type annotationreference<marker> {}",
+ " field owner type annotationreference<person> {}",
+ " }",
+ " }",
+ "}");
+
+ @Test
+ public void noAnnotationReferenceInDocument() throws Exception {
+ var builder = new ApplicationBuilder(new TestProperties());
+ builder.addSchema(sd);
+ builder.build(true);
+ var doc = builder.getSchema().getDocument();
+ checkForAnnRef(doc);
+ var complex = doc.findAnnotation("complex");
+ var dt = complex.getDataType();
+ assertTrue(dt instanceof StructDataType);
+ var struct = (StructDataType)dt;
+ var field = struct.getField("owner");
+ assertTrue(field.getDataType() instanceof AnnotationReferenceDataType);
+ }
+
+ void checkForAnnRef(SDDocumentType doc) {
+ for (var child : doc.getTypes()) {
+ checkForAnnRef(child);
+ }
+ for (Field field : doc.fieldSet()) {
+ DataType fieldType = field.getDataType();
+ assertFalse(fieldType instanceof AnnotationReferenceDataType);
+ }
+ }
+
+}
diff --git a/config-model/src/test/java/com/yahoo/schema/ArraysTestCase.java b/config-model/src/test/java/com/yahoo/schema/ArraysTestCase.java
new file mode 100644
index 00000000000..aab79617556
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/schema/ArraysTestCase.java
@@ -0,0 +1,35 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.schema;
+
+import com.yahoo.document.ArrayDataType;
+import com.yahoo.document.CollectionDataType;
+import com.yahoo.document.DataType;
+import com.yahoo.schema.document.SDField;
+import com.yahoo.schema.parser.ParseException;
+import org.junit.Test;
+
+import java.io.IOException;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * tests importing of document containing array type fields
+ *
+ * @author bratseth
+ */
+public class ArraysTestCase extends AbstractSchemaTestCase {
+
+ @Test
+ public void testArrayImporting() throws IOException, ParseException {
+ Schema schema = ApplicationBuilder.buildFromFile("src/test/examples/arrays.sd");
+
+ SDField tags = (SDField) schema.getDocument().getField("tags");
+ assertEquals(DataType.STRING, ((CollectionDataType)tags.getDataType()).getNestedType());
+
+ SDField ratings = (SDField) schema.getDocument().getField("ratings");
+ assertTrue(ratings.getDataType() instanceof ArrayDataType);
+ assertEquals(DataType.INT, ((ArrayDataType)ratings.getDataType()).getNestedType());
+ }
+
+}
diff --git a/config-model/src/test/java/com/yahoo/schema/ArraysWeightedSetsTestCase.java b/config-model/src/test/java/com/yahoo/schema/ArraysWeightedSetsTestCase.java
new file mode 100644
index 00000000000..57331bea6bd
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/schema/ArraysWeightedSetsTestCase.java
@@ -0,0 +1,41 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.schema;
+
+import com.yahoo.document.ArrayDataType;
+import com.yahoo.document.CollectionDataType;
+import com.yahoo.document.DataType;
+import com.yahoo.document.WeightedSetDataType;
+import com.yahoo.schema.document.SDField;
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * tests importing of document containing array type fields and weighted set type fields, new syntax.
+ *
+ * @author Einar M R Rosenvinge
+ */
+public class ArraysWeightedSetsTestCase extends AbstractSchemaTestCase {
+ @Test
+ public void testArrayWeightedSetsImporting() throws java.io.IOException, com.yahoo.schema.parser.ParseException {
+ Schema schema = ApplicationBuilder.buildFromFile("src/test/examples/arraysweightedsets.sd");
+
+ SDField tags = (SDField) schema.getDocument().getField("tags");
+ assertTrue(tags.getDataType() instanceof ArrayDataType);
+ assertEquals(DataType.STRING, ((CollectionDataType)tags.getDataType()).getNestedType());
+
+ SDField ratings = (SDField) schema.getDocument().getField("ratings");
+ assertTrue(ratings.getDataType() instanceof ArrayDataType);
+ assertEquals(DataType.INT, ((CollectionDataType)ratings.getDataType()).getNestedType());
+
+ SDField flags = (SDField) schema.getDocument().getField("flags");
+ assertTrue(flags.getDataType() instanceof WeightedSetDataType);
+ assertEquals(DataType.STRING, ((CollectionDataType)flags.getDataType()).getNestedType());
+
+ SDField banners = (SDField) schema.getDocument().getField("banners");
+ assertTrue(banners.getDataType() instanceof WeightedSetDataType);
+ assertEquals(DataType.INT, ((CollectionDataType)banners.getDataType()).getNestedType());
+ }
+
+}
diff --git a/config-model/src/test/java/com/yahoo/schema/AttributeSettingsTestCase.java b/config-model/src/test/java/com/yahoo/schema/AttributeSettingsTestCase.java
new file mode 100644
index 00000000000..bbe63f95787
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/schema/AttributeSettingsTestCase.java
@@ -0,0 +1,344 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.schema;
+
+import com.yahoo.document.StructDataType;
+import com.yahoo.schema.derived.AttributeFields;
+import com.yahoo.schema.derived.IndexingScript;
+import com.yahoo.schema.document.Attribute;
+import com.yahoo.schema.document.SDField;
+import com.yahoo.schema.parser.ParseException;
+import com.yahoo.tensor.TensorType;
+import com.yahoo.vespa.config.search.AttributesConfig;
+import com.yahoo.vespa.configdefinition.IlscriptsConfig;
+import org.junit.Test;
+
+import java.io.IOException;
+import java.util.Optional;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+/**
+ * Attribute settings
+ *
+ * @author bratseth
+ */
+public class AttributeSettingsTestCase extends AbstractSchemaTestCase {
+
+ @Test
+ public void testAttributeSettings() throws IOException, ParseException {
+ Schema schema = ApplicationBuilder.buildFromFile("src/test/examples/attributesettings.sd");
+
+ SDField f1=(SDField) schema.getDocument().getField("f1");
+ assertEquals(1, f1.getAttributes().size());
+ Attribute a1 = f1.getAttributes().get(f1.getName());
+ assertEquals(Attribute.Type.LONG, a1.getType());
+ assertEquals(Attribute.CollectionType.SINGLE, a1.getCollectionType());
+ assertTrue(a1.isHuge());
+ assertFalse(a1.isFastSearch());
+ assertFalse(a1.isFastAccess());
+ assertFalse(a1.isRemoveIfZero());
+ assertFalse(a1.isCreateIfNonExistent());
+
+ SDField f2=(SDField) schema.getDocument().getField("f2");
+ assertEquals(1, f2.getAttributes().size());
+ Attribute a2 = f2.getAttributes().get(f2.getName());
+ assertEquals(Attribute.Type.LONG, a2.getType());
+ assertEquals(Attribute.CollectionType.SINGLE, a2.getCollectionType());
+ assertFalse(a2.isHuge());
+ assertTrue(a2.isFastSearch());
+ assertFalse(a2.isFastAccess());
+ assertFalse(a2.isRemoveIfZero());
+ assertFalse(a2.isCreateIfNonExistent());
+ assertEquals("f2", f2.getAliasToName().get("f2alias"));
+ SDField f3=(SDField) schema.getDocument().getField("f3");
+ assertEquals(1, f3.getAttributes().size());
+ assertEquals("f3", f3.getAliasToName().get("f3alias"));
+
+ Attribute a3 = f3.getAttributes().get(f3.getName());
+ assertEquals(Attribute.Type.LONG, a3.getType());
+ assertEquals(Attribute.CollectionType.SINGLE, a3.getCollectionType());
+ assertFalse(a3.isHuge());
+ assertFalse(a3.isFastSearch());
+ assertFalse(a3.isFastAccess());
+ assertFalse(a3.isRemoveIfZero());
+ assertFalse(a3.isCreateIfNonExistent());
+
+ assertWeightedSet(schema, "f4", true, true);
+ assertWeightedSet(schema, "f5", true, true);
+ assertWeightedSet(schema, "f6", true, true);
+ assertWeightedSet(schema, "f7", true, false);
+ assertWeightedSet(schema, "f8", true, false);
+ assertWeightedSet(schema, "f9", false, true);
+ assertWeightedSet(schema, "f10", false, true);
+
+ assertAttrSettings(schema, "f4", false, false, false);
+ assertAttrSettings(schema, "f5", true, true, true);
+ assertAttrSettings(schema, "f6", false, false, false);
+ assertAttrSettings(schema, "f7", false, false, false);
+ assertAttrSettings(schema, "f8", false, false, false);
+ assertAttrSettings(schema, "f9", false, false, false);
+ assertAttrSettings(schema, "f10", false, false, false);
+ }
+
+ private void assertWeightedSet(Schema schema, String name, boolean createIfNonExistent, boolean removeIfZero) {
+ SDField f4 = (SDField) schema.getDocument().getField(name);
+ assertEquals(1, f4.getAttributes().size());
+ Attribute a4 = f4.getAttributes().get(f4.getName());
+ assertEquals(Attribute.Type.STRING, a4.getType());
+ assertEquals(Attribute.CollectionType.WEIGHTEDSET, a4.getCollectionType());
+ assertEquals(a4.isRemoveIfZero(), removeIfZero);
+ assertEquals(a4.isCreateIfNonExistent(), createIfNonExistent);
+ }
+
+ private void assertAttrSettings(Schema schema, String name, boolean fastAccess, boolean fastSearch, boolean paged) {
+ SDField f4 = (SDField) schema.getDocument().getField(name);
+ assertEquals(1, f4.getAttributes().size());
+ Attribute a4 = f4.getAttributes().get(f4.getName());
+ assertEquals(Attribute.Type.STRING, a4.getType());
+ assertEquals(Attribute.CollectionType.WEIGHTEDSET, a4.getCollectionType());
+ assertEquals(a4.isFastSearch(), fastSearch);
+ assertEquals(a4.isFastAccess(), fastAccess);
+ assertEquals(a4.isPaged(), paged);
+ }
+
+ @Test
+ public void requireThatFastAccessCanBeSet() throws IOException, ParseException {
+ Schema schema = ApplicationBuilder.buildFromFile("src/test/examples/attributesettings.sd");
+ SDField field = (SDField) schema.getDocument().getField("fast_access");
+ assertEquals(1, field.getAttributes().size());
+ Attribute attr = field.getAttributes().get(field.getName());
+ assertTrue(attr.isFastAccess());
+ }
+
+ private Schema getSchema(String sd) throws ParseException {
+ ApplicationBuilder builder = new ApplicationBuilder();
+ builder.addSchema(sd);
+ builder.build(true);
+ return builder.getSchema();
+ }
+
+ private Attribute getAttributeF(String sd) throws ParseException {
+ Schema schema = getSchema(sd);
+ SDField field = (SDField) schema.getDocument().getField("f");
+ return field.getAttributes().get(field.getName());
+ }
+
+ @Test
+ public void requireThatPagedIsDefaultOff() throws ParseException {
+ Attribute attr = getAttributeF(
+ "search test {\n" +
+ " document test { \n" +
+ " field f type tensor(x[2]) { \n" +
+ " indexing: attribute \n" +
+ " }\n" +
+ " }\n" +
+ "}\n");
+ assertFalse(attr.isPaged());
+ }
+ @Test
+ public void requireThatPagedCanBeSet() throws ParseException {
+ Attribute attr = getAttributeF(
+ "search test {\n" +
+ " document test { \n" +
+ " field f type tensor(x[2]) { \n" +
+ " indexing: attribute \n" +
+ " attribute: paged \n" +
+ " }\n" +
+ " }\n" +
+ "}\n");
+ assertTrue(attr.isPaged());
+ }
+
+ @Test
+ public void requireThatMutableIsDefaultOff() throws ParseException {
+ Attribute attr = getAttributeF(
+ "search test {\n" +
+ " document test { \n" +
+ " field f type int { \n" +
+ " indexing: attribute \n" +
+ " }\n" +
+ " }\n" +
+ "}\n");
+ assertFalse(attr.isMutable());
+ }
+
+ @Test
+ public void requireThatMutableCanNotbeSetInDocument() throws ParseException {
+ try {
+ getSchema("search test {\n" +
+ " document test {\n" +
+ " field f type int {\n" +
+ " indexing: attribute\n" +
+ " attribute: mutable\n" +
+ " }\n" +
+ " }\n" +
+ "}\n");
+ fail();
+ } catch (IllegalArgumentException e) {
+ assertEquals("Field 'f' in 'test' can not be marked mutable as it is inside the document clause.", e.getMessage());
+ }
+ }
+
+ @Test
+ public void requireThatMutableExtraFieldCanBeSet() throws ParseException {
+ Attribute attr = getAttributeF(
+ "search test {\n" +
+ " document test { \n" +
+ " field a type int { \n" +
+ " indexing: attribute \n" +
+ " }\n" +
+ " }\n" +
+ " field f type long {\n" +
+ " indexing: 0 | to_long | attribute\n" +
+ " attribute: mutable\n" +
+ " }\n" +
+ "}\n");
+ assertTrue(attr.isMutable());
+ }
+
+ private Schema getSearchWithMutables() throws ParseException {
+ return getSchema(
+ "search test {\n" +
+ " document test { \n" +
+ " field a type int { \n" +
+ " indexing: attribute \n" +
+ " }\n" +
+ " }\n" +
+ " field m type long {\n" +
+ " indexing: attribute\n" +
+ " attribute: mutable\n" +
+ " }\n" +
+ " field f type long {\n" +
+ " indexing: 0 | to_long | attribute\n" +
+ " }\n" +
+ "}\n");
+ }
+
+ @Test
+ public void requireThatMutableConfigIsProperlyPropagated() throws ParseException {
+ AttributeFields attributes = new AttributeFields(getSearchWithMutables());
+ AttributesConfig.Builder builder = new AttributesConfig.Builder();
+ attributes.getConfig(builder, AttributeFields.FieldSet.ALL, 13333, true);
+ AttributesConfig cfg = builder.build();
+ assertEquals("a", cfg.attribute().get(0).name());
+ assertFalse(cfg.attribute().get(0).ismutable());
+
+ assertEquals("f", cfg.attribute().get(1).name());
+ assertFalse(cfg.attribute().get(1).ismutable());
+
+ assertEquals("m", cfg.attribute().get(2).name());
+ assertTrue(cfg.attribute().get(2).ismutable());
+ }
+
+ @Test
+ public void requireMaxUnCommittedMemoryIsProperlyPropagated() throws ParseException {
+ AttributeFields attributes = new AttributeFields(getSearchWithMutables());
+ AttributesConfig.Builder builder = new AttributesConfig.Builder();
+ attributes.getConfig(builder, AttributeFields.FieldSet.ALL, 13333, true);
+ AttributesConfig cfg = builder.build();
+ assertEquals("a", cfg.attribute().get(0).name());
+ assertEquals(13333, cfg.attribute().get(0).maxuncommittedmemory());
+
+ assertEquals("f", cfg.attribute().get(1).name());
+ assertEquals(13333, cfg.attribute().get(1).maxuncommittedmemory());
+
+ assertEquals("m", cfg.attribute().get(2).name());
+ assertEquals(13333, cfg.attribute().get(2).maxuncommittedmemory());
+ }
+
+ private void verifyEnableBitVectorDefault(Schema schema, boolean enableBitVectors) {
+ AttributeFields attributes = new AttributeFields(schema);
+ AttributesConfig.Builder builder = new AttributesConfig.Builder();
+ attributes.getConfig(builder, AttributeFields.FieldSet.ALL, 13333, enableBitVectors);
+ AttributesConfig cfg = builder.build();
+ assertEquals("a", cfg.attribute().get(0).name());
+ assertEquals(enableBitVectors, cfg.attribute().get(0).enablebitvectors());
+
+ assertEquals("b", cfg.attribute().get(1).name());
+ assertFalse(cfg.attribute().get(1).enablebitvectors());
+ }
+
+ @Test
+ public void requireEnableBitVectorsIsProperlyPropagated() throws ParseException {
+ Schema schema = getSchema(
+ "search test {\n" +
+ " document test { \n" +
+ " field a type int { \n" +
+ " indexing: attribute \n" +
+ " attribute: fast-search\n" +
+ " }\n" +
+ " field b type int { \n" +
+ " indexing: attribute \n" +
+ " }\n" +
+ " }\n" +
+ "}\n");
+ verifyEnableBitVectorDefault(schema, false);
+ verifyEnableBitVectorDefault(schema, true);
+ }
+
+ @Test
+ public void requireThatMutableIsAllowedThroughIndexing() throws ParseException {
+ IndexingScript script = new IndexingScript(getSearchWithMutables());
+ IlscriptsConfig.Builder builder = new IlscriptsConfig.Builder();
+ script.getConfig(builder);
+ IlscriptsConfig cfg = builder.build();
+ assertEquals(1, cfg.ilscript().size());
+ IlscriptsConfig.Ilscript ils = cfg.ilscript(0);
+ assertEquals("test", ils.doctype());
+ assertEquals(2, ils.docfield().size());
+ assertEquals("a", ils.docfield(0));
+ assertEquals("m", ils.docfield(1));
+
+ }
+
+ @Test
+ public void attribute_convert_to_array_copies_internal_state() {
+ StructDataType refType = new StructDataType("my_struct");
+ Attribute single = new Attribute("foo", Attribute.Type.STRING, Attribute.CollectionType.SINGLE,
+ Optional.of(TensorType.fromSpec("tensor(x{})")), Optional.of(refType));
+ single.setRemoveIfZero(true);
+ single.setCreateIfNonExistent(true);
+ single.setPrefetch(Boolean.TRUE);
+ single.setEnableBitVectors(true);
+ single.setEnableOnlyBitVector(true);
+ single.setFastSearch(true);
+ single.setHuge(true);
+ single.setPaged(true);
+ single.setFastAccess(true);
+ single.setPosition(true);
+ single.setArity(5);
+ single.setLowerBound(7);
+ single.setUpperBound(11);
+ single.setDensePostingListThreshold(13.3);
+ single.getSorting().setAscending();
+ single.getAliases().add("foo");
+
+ Attribute array = single.convertToArray();
+ assertEquals("foo", array.getName());
+ assertEquals(Attribute.Type.STRING, array.getType());
+ assertEquals(Attribute.CollectionType.ARRAY, array.getCollectionType());
+ assertEquals(Optional.of(TensorType.fromSpec("tensor(x{})")), array.tensorType());
+ assertSame(single.referenceDocumentType(), array.referenceDocumentType());
+ assertTrue(array.isRemoveIfZero());
+ assertTrue(array.isCreateIfNonExistent());
+ assertTrue(array.isPrefetch());
+ assertTrue(array.isEnabledBitVectors());
+ assertTrue(array.isEnabledOnlyBitVector());
+ assertTrue(array.isFastSearch());
+ assertTrue(array.isHuge());
+ assertTrue(array.isPaged());
+ assertTrue(array.isFastAccess());
+ assertTrue(array.isPosition());
+ assertEquals(5, array.arity());
+ assertEquals(7, array.lowerBound());
+ assertEquals(11, array.upperBound());
+ assertEquals(13.3, array.densePostingListThreshold(), 0.00001);
+ assertSame(single.getSorting(), array.getSorting());
+ assertSame(single.getAliases(), array.getAliases());
+ }
+
+}
diff --git a/config-model/src/test/java/com/yahoo/schema/AttributeUtils.java b/config-model/src/test/java/com/yahoo/schema/AttributeUtils.java
new file mode 100644
index 00000000000..fb7d68faf2d
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/schema/AttributeUtils.java
@@ -0,0 +1,15 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.schema;
+
+import com.yahoo.schema.document.SDField;
+
+/**
+ * Convenience class for tests that need to set attribute properties on fields.
+ */
+public class AttributeUtils {
+
+ public static void addAttributeAspect(SDField field) {
+ field.parseIndexingScript("{ attribute }");
+ }
+
+}
diff --git a/config-model/src/test/java/com/yahoo/schema/CommentTestCase.java b/config-model/src/test/java/com/yahoo/schema/CommentTestCase.java
new file mode 100644
index 00000000000..4df4ead171b
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/schema/CommentTestCase.java
@@ -0,0 +1,27 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.schema;
+
+import com.yahoo.schema.document.SDField;
+import com.yahoo.schema.parser.ParseException;
+import org.junit.Test;
+
+import java.io.IOException;
+
+import static org.junit.Assert.assertEquals;
+
+/**
+ * Tests comment handling
+ *
+ * @author bratseth
+ */
+public class CommentTestCase extends AbstractSchemaTestCase {
+
+ @Test
+ public void testComments() throws IOException, ParseException {
+ Schema schema = ApplicationBuilder.buildFromFile("src/test/examples/comment.sd");
+ SDField field = schema.getConcreteField("a");
+ assertEquals("{ input a | tokenize normalize stem:\"BEST\" | summary a | index a; }",
+ field.getIndexingScript().toString());
+ }
+
+}
diff --git a/config-model/src/test/java/com/yahoo/schema/DiversityTestCase.java b/config-model/src/test/java/com/yahoo/schema/DiversityTestCase.java
new file mode 100644
index 00000000000..482bc877081
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/schema/DiversityTestCase.java
@@ -0,0 +1,110 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.schema;
+
+import com.yahoo.search.query.ranking.Diversity;
+import com.yahoo.schema.parser.ParseException;
+import org.junit.Test;
+import static org.junit.Assert.fail;
+
+import static org.junit.Assert.assertEquals;
+
+/**
+ * @author baldersheim
+ */
+public class DiversityTestCase {
+ @Test
+ public void testDiversity() throws ParseException {
+ RankProfileRegistry rankProfileRegistry = new RankProfileRegistry();
+ ApplicationBuilder builder = new ApplicationBuilder(rankProfileRegistry);
+ builder.addSchema(
+ "search test {\n" +
+ " document test { \n" +
+ " field a type int { \n" +
+ " indexing: attribute \n" +
+ " attribute: fast-search\n" +
+ " }\n" +
+ " field b type int {\n" +
+ " indexing: attribute \n" +
+ " }\n" +
+ " }\n" +
+ " \n" +
+ " rank-profile parent {\n" +
+ " match-phase {\n" +
+ " diversity {\n" +
+ " attribute: b\n" +
+ " min-groups: 74\n" +
+ " cutoff-factor: 17.3\n" +
+ " cutoff-strategy: strict" +
+ " }\n" +
+ " attribute: a\n" +
+ " max-hits: 120\n" +
+ " max-filter-coverage: 0.065" +
+ " }\n" +
+ " }\n" +
+ "}\n");
+ builder.build(true);
+ Schema s = builder.getSchema();
+ RankProfile.MatchPhaseSettings matchPhase = rankProfileRegistry.get(s, "parent").getMatchPhaseSettings();
+ RankProfile.DiversitySettings diversity = matchPhase.getDiversity();
+ assertEquals("b", diversity.getAttribute());
+ assertEquals(74, diversity.getMinGroups());
+ assertEquals(17.3, diversity.getCutoffFactor(), 1e-16);
+ assertEquals(Diversity.CutoffStrategy.strict, diversity.getCutoffStrategy());
+ assertEquals(120, matchPhase.getMaxHits());
+ assertEquals("a", matchPhase.getAttribute());
+ assertEquals(0.065, matchPhase.getMaxFilterCoverage(), 1e-16);
+ }
+
+ private static String getMessagePrefix() {
+ return "In search definition 'test', rank-profile 'parent': diversity attribute 'b' ";
+ }
+ @Test
+ public void requireSingleNumericOrString() throws ParseException {
+ ApplicationBuilder builder = getSearchBuilder("field b type predicate { indexing: attribute }");
+
+ try {
+ builder.build(true);
+ fail("Should throw.");
+ } catch (IllegalArgumentException e) {
+ assertEquals(getMessagePrefix() + "must be single value numeric, or enumerated attribute, but it is 'predicate'", e.getMessage());
+ }
+ }
+
+ @Test
+ public void requireSingle() throws ParseException {
+ ApplicationBuilder builder = getSearchBuilder("field b type array<int> { indexing: attribute }");
+
+ try {
+ builder.build(true);
+ fail("Should throw.");
+ } catch (IllegalArgumentException e) {
+ assertEquals(getMessagePrefix() + "must be single value numeric, or enumerated attribute, but it is 'Array<int>'", e.getMessage());
+ }
+ }
+ private ApplicationBuilder getSearchBuilder(String diversity) throws ParseException {
+ RankProfileRegistry rankProfileRegistry = new RankProfileRegistry();
+ ApplicationBuilder builder = new ApplicationBuilder(rankProfileRegistry);
+ builder.addSchema(
+ "search test {\n" +
+ " document test { \n" +
+ " field a type int { \n" +
+ " indexing: attribute \n" +
+ " attribute: fast-search\n" +
+ " }\n" +
+ diversity +
+ " }\n" +
+ " \n" +
+ " rank-profile parent {\n" +
+ " match-phase {\n" +
+ " diversity {\n" +
+ " attribute: b\n" +
+ " min-groups: 74\n" +
+ " }\n" +
+ " attribute: a\n" +
+ " max-hits: 120\n" +
+ " }\n" +
+ " }\n" +
+ "}\n");
+ return builder;
+ }
+}
diff --git a/config-model/src/test/java/com/yahoo/schema/DocumentGraphValidatorTest.java b/config-model/src/test/java/com/yahoo/schema/DocumentGraphValidatorTest.java
new file mode 100644
index 00000000000..ef4d8e05540
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/schema/DocumentGraphValidatorTest.java
@@ -0,0 +1,166 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.schema;
+
+import com.yahoo.config.model.test.MockApplicationPackage;
+import com.yahoo.documentmodel.NewDocumentReferenceDataType;
+import com.yahoo.schema.document.SDDocumentType;
+import com.yahoo.schema.document.SDField;
+import com.yahoo.schema.document.TemporarySDField;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import static java.util.stream.Collectors.toList;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
+
+/**
+ * @author bjorncs
+ */
+public class DocumentGraphValidatorTest {
+
+ @SuppressWarnings("deprecation")
+ @Rule
+ public final ExpectedException exceptionRule = ExpectedException.none();
+
+ @Test
+ public void simple_ref_dag_is_allowed() {
+ Schema advertiserSchema = createSearchWithName("advertiser");
+ Schema campaignSchema = createSearchWithName("campaign");
+ Schema adSchema = createSearchWithName("ad");
+ createDocumentReference(adSchema, advertiserSchema, "advertiser_ref");
+ createDocumentReference(adSchema, campaignSchema, "campaign_ref");
+
+ DocumentGraphValidator validator = new DocumentGraphValidator();
+ validator.validateDocumentGraph(documentListOf(advertiserSchema, campaignSchema, adSchema));
+ }
+
+ @Test
+ public void simple_inheritance_dag_is_allowed() {
+ Schema grandfather = createSearchWithName("grandfather");
+ Schema father = createSearchWithName("father", grandfather);
+ Schema son = createSearchWithName("son", father);
+
+ DocumentGraphValidator validator = new DocumentGraphValidator();
+ validator.validateDocumentGraph(documentListOf(son, father, grandfather));
+ }
+
+ @Test
+ public void complex_dag_is_allowed() {
+ Schema grandfather = createSearchWithName("grandfather");
+ Schema father = createSearchWithName("father", grandfather);
+ Schema mother = createSearchWithName("mother", grandfather);
+ createDocumentReference(father, mother, "wife_ref");
+ Schema son = createSearchWithName("son", father, mother);
+ Schema daughter = createSearchWithName("daughter", father, mother);
+ createDocumentReference(daughter, son, "brother_ref");
+
+ Schema randomGuy1 = createSearchWithName("randomguy1");
+ Schema randomGuy2 = createSearchWithName("randomguy2");
+ createDocumentReference(randomGuy1, mother, "secret_ref");
+
+ DocumentGraphValidator validator = new DocumentGraphValidator();
+ validator.validateDocumentGraph(documentListOf(son, father, grandfather, son, daughter, randomGuy1, randomGuy2));
+ }
+
+ @Test
+ public void ref_cycle_is_forbidden() {
+ Schema schema1 = createSearchWithName("doc1");
+ Schema schema2 = createSearchWithName("doc2");
+ Schema schema3 = createSearchWithName("doc3");
+ createDocumentReference(schema1, schema2, "ref_2");
+ createDocumentReference(schema2, schema3, "ref_3");
+ createDocumentReference(schema3, schema1, "ref_1");
+
+ DocumentGraphValidator validator = new DocumentGraphValidator();
+ exceptionRule.expect(DocumentGraphValidator.DocumentGraphException.class);
+ exceptionRule.expectMessage("Document dependency cycle detected: doc1->doc2->doc3->doc1.");
+ validator.validateDocumentGraph(documentListOf(schema1, schema2, schema3));
+ }
+
+ @Test
+ public void inherit_cycle_is_forbidden() {
+ Schema schema1 = createSearchWithName("doc1");
+ Schema schema2 = createSearchWithName("doc2", schema1);
+ Schema schema3 = createSearchWithName("doc3", schema2);
+ schema1.getDocument().inherit(schema3.getDocument());
+
+ DocumentGraphValidator validator = new DocumentGraphValidator();
+ exceptionRule.expect(DocumentGraphValidator.DocumentGraphException.class);
+ exceptionRule.expectMessage("Document dependency cycle detected: doc1->doc3->doc2->doc1.");
+ validator.validateDocumentGraph(documentListOf(schema1, schema2, schema3));
+ }
+
+ @Test
+ public void combined_inherit_and_ref_cycle_is_forbidden() {
+ Schema schema1 = createSearchWithName("doc1");
+ Schema schema2 = createSearchWithName("doc2", schema1);
+ Schema schema3 = createSearchWithName("doc3", schema2);
+ createDocumentReference(schema1, schema3, "ref_1");
+
+ DocumentGraphValidator validator = new DocumentGraphValidator();
+ exceptionRule.expect(DocumentGraphValidator.DocumentGraphException.class);
+ exceptionRule.expectMessage("Document dependency cycle detected: doc1->doc3->doc2->doc1.");
+ validator.validateDocumentGraph(documentListOf(schema1, schema2, schema3));
+ }
+
+ @Test
+ public void self_reference_is_forbidden() {
+ Schema adSchema = createSearchWithName("ad");
+ createDocumentReference(adSchema, adSchema, "ad_ref");
+
+ DocumentGraphValidator validator = new DocumentGraphValidator();
+ exceptionRule.expect(DocumentGraphValidator.DocumentGraphException.class);
+ exceptionRule.expectMessage("Document dependency cycle detected: ad->ad.");
+ validator.validateDocumentGraph(documentListOf(adSchema));
+ }
+
+ /**
+ * Self inheritance is checked early because it is possible, and because it otherwise
+ * produces a stack overflow before getting to graph validation.
+ */
+ @Test
+ public void self_inheritance_forbidden() {
+ try {
+ Schema adSchema = createSearchWithName("ad");
+ SDDocumentType document = adSchema.getDocument();
+ document.inherit(document);
+ fail("Expected exception");
+ }
+ catch (IllegalArgumentException e) {
+ assertEquals("Document type 'ad' cannot inherit itself", e.getMessage());
+ }
+ }
+
+ private static List<SDDocumentType> documentListOf(Schema... schemas) {
+ return Arrays.stream(schemas).map(Schema::getDocument).collect(toList());
+ }
+
+ private static Schema createSearchWithName(String name, Schema... parents) {
+ Schema campaignSchema = new Schema(name, MockApplicationPackage.createEmpty());
+ SDDocumentType document = new SDDocumentType(name);
+ campaignSchema.addDocument(document);
+ document.setDocumentReferences(new DocumentReferences(Collections.emptyMap()));
+ Arrays.stream(parents)
+ .map(Schema::getDocument)
+ .forEach(document::inherit);
+ return campaignSchema;
+ }
+
+ @SuppressWarnings("deprecation")
+ private static void createDocumentReference(Schema from, Schema to, String refFieldName) {
+ SDDocumentType fromDocument = from.getDocument();
+ SDField refField = new TemporarySDField(fromDocument, refFieldName, NewDocumentReferenceDataType.forDocumentName(to.getName()));
+ fromDocument.addField(refField);
+ Map<String, DocumentReference> originalMap = fromDocument.getDocumentReferences().get().referenceMap();
+ HashMap<String, DocumentReference> modifiedMap = new HashMap<>(originalMap);
+ modifiedMap.put(refFieldName, new DocumentReference(refField, to));
+ fromDocument.setDocumentReferences(new DocumentReferences(modifiedMap));
+ }
+}
diff --git a/config-model/src/test/java/com/yahoo/schema/DocumentReferenceResolverTest.java b/config-model/src/test/java/com/yahoo/schema/DocumentReferenceResolverTest.java
new file mode 100644
index 00000000000..1592060f466
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/schema/DocumentReferenceResolverTest.java
@@ -0,0 +1,103 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.schema;
+
+import com.yahoo.config.model.test.MockApplicationPackage;
+import com.yahoo.document.DataType;
+import com.yahoo.documentmodel.NewDocumentReferenceDataType;
+import com.yahoo.schema.document.SDDocumentType;
+import com.yahoo.schema.document.SDField;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+
+import java.util.Map;
+
+import static java.util.Arrays.asList;
+import static java.util.Collections.singletonList;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * @author bjorncs
+ */
+public class DocumentReferenceResolverTest {
+
+ private static final String BAR = "bar";
+ private static final String FOO = "foo";
+ @SuppressWarnings("deprecation")
+ @Rule
+ public final ExpectedException exceptionRule = ExpectedException.none();
+
+ @Test
+ public void reference_from_one_document_to_another_is_resolved() {
+ // Create bar document with no fields
+ Schema barSchema = new Schema(BAR, MockApplicationPackage.createEmpty());
+ SDDocumentType barDocument = new SDDocumentType(BAR, barSchema);
+ barSchema.addDocument(barDocument);
+
+ // Create foo document with document reference to bar and add another field
+ Schema fooSchema = new Schema(FOO, MockApplicationPackage.createEmpty());
+ SDDocumentType fooDocument = new SDDocumentType("foo", fooSchema);
+ SDField fooRefToBarField = new SDField
+ (fooDocument, "bar_ref", new NewDocumentReferenceDataType(barDocument.getDocumentType()));
+ AttributeUtils.addAttributeAspect(fooRefToBarField);
+ SDField irrelevantField = new SDField(fooDocument, "irrelevant_stuff", DataType.INT);
+ fooDocument.addField(fooRefToBarField);
+ fooDocument.addField(irrelevantField);
+ fooSchema.addDocument(fooDocument);
+
+ DocumentReferenceResolver resolver = new DocumentReferenceResolver(asList(fooSchema, barSchema));
+ resolver.resolveReferences(fooDocument);
+ assertTrue(fooDocument.getDocumentReferences().isPresent());
+
+ Map<String, DocumentReference> fooReferenceMap = fooDocument.getDocumentReferences().get().referenceMap();
+ assertEquals(1, fooReferenceMap.size());
+ assertSame(barSchema, fooReferenceMap.get("bar_ref").targetSearch());
+ assertSame(fooRefToBarField, fooReferenceMap.get("bar_ref").referenceField());
+ }
+
+ @SuppressWarnings("deprecation")
+ @Test
+ public void throws_user_friendly_exception_if_referenced_document_does_not_exist() {
+ // Create foo document with document reference to non-existing document bar
+ Schema fooSchema = new Schema(FOO, MockApplicationPackage.createEmpty());
+ SDDocumentType fooDocument = new SDDocumentType("foo", fooSchema);
+ SDField fooRefToBarField = new SDField(
+ fooDocument,
+ "bar_ref", NewDocumentReferenceDataType.forDocumentName("bar"));
+ AttributeUtils.addAttributeAspect(fooRefToBarField);
+ fooDocument.addField(fooRefToBarField);
+ fooSchema.addDocument(fooDocument);
+
+ DocumentReferenceResolver resolver = new DocumentReferenceResolver(singletonList(fooSchema));
+
+ exceptionRule.expect(IllegalArgumentException.class);
+ exceptionRule.expectMessage(
+ "Invalid document reference 'bar_ref': Could not find document type 'bar'");
+ resolver.resolveReferences(fooDocument);
+ }
+
+ @Test
+ public void throws_exception_if_reference_is_not_an_attribute() {
+ // Create bar document with no fields
+ Schema barSchema = new Schema(BAR, MockApplicationPackage.createEmpty());
+ SDDocumentType barDocument = new SDDocumentType("bar", barSchema);
+ barSchema.addDocument(barDocument);
+
+ // Create foo document with document reference to bar
+ Schema fooSchema = new Schema(FOO, MockApplicationPackage.createEmpty());
+ SDDocumentType fooDocument = new SDDocumentType("foo", fooSchema);
+ SDField fooRefToBarField = new SDField
+ (fooDocument, "bar_ref", new NewDocumentReferenceDataType(barDocument.getDocumentType()));
+ fooDocument.addField(fooRefToBarField);
+ fooSchema.addDocument(fooDocument);
+
+ DocumentReferenceResolver resolver = new DocumentReferenceResolver(asList(fooSchema, barSchema));
+ exceptionRule.expect(IllegalArgumentException.class);
+ exceptionRule.expectMessage(
+ "The field 'bar_ref' is an invalid document reference. The field must be an attribute.");
+ resolver.resolveReferences(fooDocument);
+ }
+
+}
diff --git a/config-model/src/test/java/com/yahoo/schema/FeatureNamesTestCase.java b/config-model/src/test/java/com/yahoo/schema/FeatureNamesTestCase.java
new file mode 100644
index 00000000000..bff4b434408
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/schema/FeatureNamesTestCase.java
@@ -0,0 +1,94 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.schema;
+
+import org.junit.Ignore;
+import org.junit.Test;
+
+import java.util.function.Function;
+import java.util.regex.Pattern;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * Tests rank feature names.
+ *
+ * @author bratseth
+ */
+public class FeatureNamesTestCase {
+
+ @Test
+ public void testArgument() {
+ assertFalse(FeatureNames.argumentOf("foo(bar)").isPresent());
+ assertFalse(FeatureNames.argumentOf("foo(bar.baz)").isPresent());
+ assertEquals("bar", FeatureNames.argumentOf("query(bar)").get());
+ assertEquals("bar.baz", FeatureNames.argumentOf("query(bar.baz)").get());
+ assertEquals("bar", FeatureNames.argumentOf("attribute(bar)").get());
+ assertEquals("bar.baz", FeatureNames.argumentOf("attribute(bar.baz)").get());
+ assertEquals("bar", FeatureNames.argumentOf("constant(bar)").get());
+ assertEquals("bar.baz", FeatureNames.argumentOf("constant(bar.baz)").get());
+ }
+
+ @Test
+ public void testConstantFeature() {
+ assertEquals("constant(foo)",
+ FeatureNames.asConstantFeature("foo").toString());
+ }
+
+ @Test
+ public void testAttributeFeature() {
+ assertEquals("attribute(foo)",
+ FeatureNames.asAttributeFeature("foo").toString());
+ }
+
+ @Test
+ public void testQueryFeature() {
+ assertEquals("query(\"foo.bar\")",
+ FeatureNames.asQueryFeature("foo.bar").toString());
+ }
+
+ @Test
+ public void testLegalFeatureNames() {
+ assertTrue(FeatureNames.notNeedQuotes("_"));
+ assertFalse(FeatureNames.notNeedQuotes("-"));
+ assertTrue(FeatureNames.notNeedQuotes("_-"));
+ assertTrue(FeatureNames.notNeedQuotes("0_-azAZxy98-_"));
+ assertFalse(FeatureNames.notNeedQuotes("0_-azAZxy98-_+"));
+ }
+
+ @Test
+ @Ignore
+ /*
+ * Unignore to verify performance
+ * 2021/09/05 performance was a factor of 5.25
+ * 'Identifier handcoded validity check took 4301ms
+ * Identifier regexp validity check took 22609ms'
+ */
+ public void benchMarkPatternMatching() {
+ Pattern identifierRegexp = Pattern.compile("[A-Za-z0-9_][A-Za-z0-9_-]*");
+ String[] strings = new String[1000];
+ for (int i = 0; i < strings.length; i++) {
+ strings[i] = i + "-legal_string" + i;
+ }
+
+ countValid(strings, 1000, "handcoded warmup", FeatureNames::notNeedQuotes);
+ countValid(strings, 1000, "regexp warmup", (s) -> identifierRegexp.matcher(s).matches());
+
+ countValid(strings, 100000, "handcoded", FeatureNames::notNeedQuotes);
+ countValid(strings, 100000, "regexp", (s) -> identifierRegexp.matcher(s).matches());
+ }
+
+ private void countValid(String [] strings, int numReps, String text, Function<String, Boolean> func) {
+ long start = System.nanoTime();
+ int validCount = 0;
+ for (int i = 0; i < numReps; i++) {
+ for (String s : strings) {
+ if (func.apply(s)) validCount++;
+ }
+ }
+ long end = System.nanoTime();
+ assertEquals(strings.length * numReps, validCount);
+ System.out.println("Identifier " + text + " validity check took " + (end - start)/1000000 + "ms");
+ }
+}
diff --git a/config-model/src/test/java/com/yahoo/schema/FieldOfTypeDocumentTestCase.java b/config-model/src/test/java/com/yahoo/schema/FieldOfTypeDocumentTestCase.java
new file mode 100644
index 00000000000..4a590288d53
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/schema/FieldOfTypeDocumentTestCase.java
@@ -0,0 +1,57 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.schema;
+
+import com.yahoo.document.DataType;
+import com.yahoo.document.DocumentType;
+import com.yahoo.document.DocumentTypeManager;
+import com.yahoo.document.DocumentTypeManagerConfigurer;
+import com.yahoo.document.Field;
+import com.yahoo.document.config.DocumentmanagerConfig;
+import com.yahoo.schema.derived.Deriver;
+import org.junit.Test;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertSame;
+
+/**
+ * @author Einar M R Rosenvinge
+ */
+public class FieldOfTypeDocumentTestCase extends AbstractSchemaTestCase {
+
+ @Test
+ public void testDocument() throws IOException {
+
+ List<String> sds = new ArrayList<>();
+ sds.add("src/test/examples/music.sd");
+ sds.add("src/test/examples/fieldoftypedocument.sd");
+ DocumentmanagerConfig.Builder value = Deriver.getDocumentManagerConfig(sds);
+ assertConfigFile("src/test/examples/fieldoftypedocument.cfg",
+ new DocumentmanagerConfig(value).toString() + "\n");
+
+ DocumentTypeManager manager = new DocumentTypeManager();
+ DocumentTypeManagerConfigurer.configure(manager, "raw:" + new DocumentmanagerConfig(value).toString());
+
+
+ DocumentType musicType = manager.getDocumentType("music");
+ assertEquals(3, musicType.getFieldCount());
+
+ Field intField = musicType.getField("intfield");
+ assertEquals(DataType.INT, intField.getDataType());
+ Field stringField = musicType.getField("stringfield");
+ assertEquals(DataType.STRING, stringField.getDataType());
+ Field longField = musicType.getField("longfield");
+ assertEquals(DataType.LONG, longField.getDataType());
+
+
+ DocumentType bookType = manager.getDocumentType("book");
+ assertEquals(1, bookType.getFieldCount());
+
+ Field musicField = bookType.getField("soundtrack");
+ assertSame(musicType, musicField.getDataType());
+ }
+
+}
+
diff --git a/config-model/src/test/java/com/yahoo/schema/ImportedFieldsEnumeratorTest.java b/config-model/src/test/java/com/yahoo/schema/ImportedFieldsEnumeratorTest.java
new file mode 100644
index 00000000000..92d11b3a18a
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/schema/ImportedFieldsEnumeratorTest.java
@@ -0,0 +1,73 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.schema;
+
+import com.yahoo.config.model.test.MockApplicationPackage;
+import com.yahoo.document.DataType;
+import com.yahoo.schema.document.SDDocumentType;
+import com.yahoo.schema.document.SDField;
+import com.yahoo.schema.document.TemporaryImportedField;
+import org.junit.Test;
+
+import java.util.HashSet;
+import java.util.List;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+
+public class ImportedFieldsEnumeratorTest {
+
+ @Test
+ public void imported_fields_are_enumerated_and_copied_from_correct_search_instance() {
+ String PARENT = "parent";
+ Schema parentSchema = new Schema(PARENT, MockApplicationPackage.createEmpty());
+ SDDocumentType parentDocument = new SDDocumentType(PARENT, parentSchema);
+ var parentField = new SDField(parentDocument, "their_field", DataType.INT);
+ AttributeUtils.addAttributeAspect(parentField);
+ parentDocument.addField(parentField);
+ parentSchema.addDocument(parentDocument);
+
+ String FOO = "foo";
+ Schema fooSchema = new Schema(FOO, MockApplicationPackage.createEmpty());
+ /*
+ SDField fooRefToParent = new SDField(
+ "foo_ref", NewDocumentReferenceDataType.createWithInferredId(parentDocument.getDocumentType()));
+ AttributeUtils.addAttributeAspect(fooRefToParent);
+ */
+ var fooImports = fooSchema.temporaryImportedFields().get();
+ fooImports.add(new TemporaryImportedField("my_first_import", "foo_ref", "their_field"));
+ fooImports.add(new TemporaryImportedField("my_second_import", "foo_ref", "their_field"));
+ SDDocumentType fooDocument = new SDDocumentType(FOO, fooSchema);
+ fooSchema.addDocument(fooDocument);
+
+ String BAR = "bar";
+ Schema barSchema = new Schema(BAR, MockApplicationPackage.createEmpty());
+ /*
+ SDField barRefToParent = new SDField(
+ "bar_ref", NewDocumentReferenceDataType.createWithInferredId(parentDocument.getDocumentType()));
+ AttributeUtils.addAttributeAspect(barRefToParent);
+ */
+ var barImports = barSchema.temporaryImportedFields().get();
+ barImports.add(new TemporaryImportedField("my_cool_import", "my_ref", "their_field"));
+ SDDocumentType barDocument = new SDDocumentType(BAR, barSchema);
+ barSchema.addDocument(barDocument);
+
+ var enumerator = new ImportedFieldsEnumerator(List.of(parentSchema, fooSchema, barSchema));
+
+ enumerator.enumerateImportedFields(parentDocument);
+ assertImportedFieldsAre(parentDocument, List.of()); // No imported fields in parent
+
+ enumerator.enumerateImportedFields(fooDocument);
+ assertImportedFieldsAre(fooDocument, List.of("my_first_import", "my_second_import"));
+
+ enumerator.enumerateImportedFields(barDocument);
+ assertImportedFieldsAre(barDocument, List.of("my_cool_import"));
+ }
+
+ private void assertImportedFieldsAre(SDDocumentType documentType, List<String> expectedNames) {
+ assertNotNull(documentType.getTemporaryImportedFields());
+ var actualNames = documentType.getTemporaryImportedFields().fields().keySet();
+ var expectedNameSet = new HashSet<>(expectedNames);
+ assertEquals(expectedNameSet, actualNames);
+ }
+
+}
diff --git a/config-model/src/test/java/com/yahoo/schema/IncorrectRankingExpressionFileRefTestCase.java b/config-model/src/test/java/com/yahoo/schema/IncorrectRankingExpressionFileRefTestCase.java
new file mode 100644
index 00000000000..87e168adb66
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/schema/IncorrectRankingExpressionFileRefTestCase.java
@@ -0,0 +1,36 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.schema;
+
+import com.yahoo.search.query.profile.QueryProfileRegistry;
+import com.yahoo.schema.derived.DerivedConfiguration;
+import com.yahoo.schema.parser.ParseException;
+import com.yahoo.yolean.Exceptions;
+import org.junit.Test;
+
+import java.io.IOException;
+
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+/**
+ * @author bratseth
+ */
+public class IncorrectRankingExpressionFileRefTestCase extends AbstractSchemaTestCase {
+
+ @Test
+ public void testIncorrectRef() throws IOException, ParseException {
+ try {
+ RankProfileRegistry registry = new RankProfileRegistry();
+ Schema schema = ApplicationBuilder.buildFromFile("src/test/examples/incorrectrankingexpressionfileref.sd",
+ registry,
+ new QueryProfileRegistry());
+ new DerivedConfiguration(schema, registry); // cause rank profile parsing
+ fail("parsing should have failed");
+ } catch (IllegalArgumentException e) {
+ String message = Exceptions.toMessageString(e);
+ assertTrue(message.contains("Could not read ranking expression file"));
+ assertTrue(message.contains("wrongending.expr.expression"));
+ }
+ }
+
+}
diff --git a/config-model/src/test/java/com/yahoo/schema/IncorrectSummaryTypesTestCase.java b/config-model/src/test/java/com/yahoo/schema/IncorrectSummaryTypesTestCase.java
new file mode 100644
index 00000000000..e58cce6472a
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/schema/IncorrectSummaryTypesTestCase.java
@@ -0,0 +1,36 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.schema;
+
+import com.yahoo.schema.parser.ParseException;
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
+/**
+ * Tests importing a search definition with conflicting summary types
+ *
+ * @author bratseth
+ */
+public class IncorrectSummaryTypesTestCase extends AbstractSchemaTestCase {
+ @Test
+ public void testImportingIncorrect() throws ParseException {
+ try {
+ ApplicationBuilder.createFromString(
+ "search incorrectsummarytypes {\n" +
+ " document incorrectsummarytypes {\n" +
+ " field somestring type string {\n" +
+ " indexing: summary\n" +
+ " }\n" +
+ " }\n" +
+ " document-summary incorrect {\n" +
+ " summary somestring type int {\n" +
+ " }\n" +
+ " }\n" +
+ "}\n");
+ fail("processing should have failed");
+ } catch (RuntimeException e) {
+ assertEquals("'summary somestring type string' in 'destinations(default )' is inconsistent with 'summary somestring type int' in 'destinations(incorrect )': All declarations of the same summary field must have the same type", e.getMessage());
+ }
+ }
+
+}
diff --git a/config-model/src/test/java/com/yahoo/schema/IndexSettingsTestCase.java b/config-model/src/test/java/com/yahoo/schema/IndexSettingsTestCase.java
new file mode 100644
index 00000000000..6082372b428
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/schema/IndexSettingsTestCase.java
@@ -0,0 +1,62 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.schema;
+
+import com.yahoo.schema.document.SDField;
+import com.yahoo.schema.document.Stemming;
+import com.yahoo.schema.parser.ParseException;
+import org.junit.Test;
+
+import java.io.IOException;
+
+import static com.yahoo.config.model.test.TestUtil.joinLines;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * Rank settings
+ *
+ * @author bratseth
+ */
+public class IndexSettingsTestCase extends AbstractSchemaTestCase {
+
+ @Test
+ public void testStemmingSettings() throws IOException, ParseException {
+ Schema schema = ApplicationBuilder.buildFromFile("src/test/examples/indexsettings.sd");
+
+ SDField usingDefault=(SDField) schema.getDocument().getField("usingdefault");
+ assertEquals(Stemming.SHORTEST,usingDefault.getStemming(schema));
+
+ SDField notStemmed=(SDField) schema.getDocument().getField("notstemmed");
+ assertEquals(Stemming.NONE,notStemmed.getStemming(schema));
+
+ SDField allStemmed=(SDField) schema.getDocument().getField("allstemmed");
+ assertEquals(Stemming.SHORTEST,allStemmed.getStemming(schema));
+
+ SDField multiStemmed=(SDField) schema.getDocument().getField("multiplestems");
+ assertEquals(Stemming.MULTIPLE, multiStemmed.getStemming(schema));
+ }
+
+ @Test
+ public void requireThatInterlavedFeaturesAreSetOnExtraField() throws ParseException {
+ ApplicationBuilder builder = ApplicationBuilder.createFromString(joinLines(
+ "search test {",
+ " document test {",
+ " field content type string {",
+ " indexing: index | summary",
+ " index: enable-bm25",
+ " }",
+ " }",
+ " field extra type string {",
+ " indexing: input content | index | summary",
+ " index: enable-bm25",
+ " }",
+ "}"
+ ));
+ Schema schema = builder.getSchema();
+ Index contentIndex = schema.getIndex("content");
+ assertTrue(contentIndex.useInterleavedFeatures());
+ Index extraIndex = schema.getIndex("extra");
+ assertTrue(extraIndex.useInterleavedFeatures());
+ }
+
+}
diff --git a/config-model/src/test/java/com/yahoo/schema/IndexingParsingTestCase.java b/config-model/src/test/java/com/yahoo/schema/IndexingParsingTestCase.java
new file mode 100644
index 00000000000..6a51000fffe
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/schema/IndexingParsingTestCase.java
@@ -0,0 +1,32 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.schema;
+
+import com.yahoo.schema.parser.ParseException;
+import org.junit.Test;
+
+import static org.junit.Assert.assertNotNull;
+
+/**
+ * Tests that indexing statements are parsed correctly.
+ *
+ * @author frodelu
+ */
+public class IndexingParsingTestCase extends AbstractSchemaTestCase {
+
+ @Test
+ public void requireThatIndexingExpressionsCanBeParsed() throws Exception {
+ assertNotNull(ApplicationBuilder.buildFromFile("src/test/examples/indexing.sd"));
+ }
+
+ @Test
+ public void requireThatParseExceptionPositionIsCorrect() throws Exception {
+ try {
+ ApplicationBuilder.buildFromFile("src/test/examples/indexing_invalid_expression.sd");
+ } catch (ParseException e) {
+ if (!e.getMessage().contains("at line 5, column 57.")) {
+ throw e;
+ }
+ }
+ }
+
+}
diff --git a/config-model/src/test/java/com/yahoo/schema/MultipleSummariesTestCase.java b/config-model/src/test/java/com/yahoo/schema/MultipleSummariesTestCase.java
new file mode 100644
index 00000000000..6d6249dc372
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/schema/MultipleSummariesTestCase.java
@@ -0,0 +1,24 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.schema;
+
+import com.yahoo.config.model.deploy.TestProperties;
+import com.yahoo.schema.parser.ParseException;
+import org.junit.Test;
+
+import java.io.IOException;
+
+/**
+ * tests importing of document containing array type fields
+ *
+ * @author bratseth
+ */
+public class MultipleSummariesTestCase extends AbstractSchemaTestCase {
+
+ @Test
+ public void testArrayImporting() throws IOException, ParseException {
+ var builder = new ApplicationBuilder(new TestProperties());
+ builder.addSchemaFile("src/test/examples/multiplesummaries.sd");
+ builder.build(true);
+ }
+
+}
diff --git a/config-model/src/test/java/com/yahoo/schema/NameFieldCheckTestCase.java b/config-model/src/test/java/com/yahoo/schema/NameFieldCheckTestCase.java
new file mode 100644
index 00000000000..9b4b6864309
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/schema/NameFieldCheckTestCase.java
@@ -0,0 +1,80 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.schema;
+
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+/**
+ * Tests that "name" is not allowed as name for a field.
+ *
+ * And that duplicate names are not allowed.
+ *
+ * @author Lars Christian Jensen
+ */
+public class NameFieldCheckTestCase extends AbstractSchemaTestCase {
+
+ @Test
+ public void testNameField() {
+ try {
+ ApplicationBuilder.createFromString(
+ "search simple {\n" +
+ " document name-check {\n" +
+ " field title type string {\n" +
+ " indexing: summary | index\n" +
+ " }\n" +
+ " # reserved name, should trigger error\n" +
+ " field sddocname type string {\n" +
+ " indexing: index\n" +
+ " }\n" +
+ " }\n" +
+ "}");
+ fail("Should throw exception.");
+ } catch (Exception expected) {
+ // Success
+ }
+ }
+
+ @Test
+ public void testDuplicateNamesInSearchDifferentType() {
+ try {
+ ApplicationBuilder.createFromString(
+ "search duplicatenamesinsearch {\n" +
+ " document {\n" +
+ " field grpphotoids64 type string { }\n" +
+ " }\n" +
+ " field grpphotoids64 type array<long> {\n" +
+ " indexing: input grpphotoids64 | split \" \" | for_each {\n" +
+ " base64decode } | attribute\n" +
+ " }\n" +
+ "}");
+ fail("Should throw exception.");
+ } catch (Exception e) {
+ assertEquals("For schema 'duplicatenamesinsearch', field 'grpphotoids64': " +
+ "Incompatible types. Expected Array<long> for index field 'grpphotoids64', got string.", e.getMessage());
+ }
+ }
+
+ @Test
+ public void testDuplicateNamesInDoc() {
+ try {
+ ApplicationBuilder.createFromString(
+ "search duplicatenamesindoc {\n" +
+ " document {\n" +
+ " field foo type int {\n" +
+ " indexing: attribute\n" +
+ " }\n" +
+ " field fOo type string {\n" +
+ " indexing: index\n" +
+ " }\n" +
+ " }\n" +
+ "}");
+ fail("Should throw exception.");
+ } catch (Exception e) {
+ assertTrue(e.getMessage().matches(".*Duplicate.*"));
+ }
+ }
+
+}
diff --git a/config-model/src/test/java/com/yahoo/schema/OutsideTestCase.java b/config-model/src/test/java/com/yahoo/schema/OutsideTestCase.java
new file mode 100644
index 00000000000..0c0684e23e3
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/schema/OutsideTestCase.java
@@ -0,0 +1,32 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.schema;
+
+import com.yahoo.schema.parser.ParseException;
+import org.junit.Test;
+
+import java.io.IOException;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+/**
+ * Tests settings outside the document
+ *
+ * @author bratseth
+ */
+public class OutsideTestCase extends AbstractSchemaTestCase {
+
+ @Test
+ public void testOutsideIndex() throws IOException, ParseException {
+ Schema schema = ApplicationBuilder.buildFromFile("src/test/examples/outsidedoc.sd");
+
+ Index defaultIndex= schema.getIndex("default");
+ assertTrue(defaultIndex.isPrefix());
+ assertEquals("default.default",defaultIndex.aliasIterator().next());
+ }
+
+ @Test
+ public void testOutsideSummary() throws IOException, ParseException {
+ ApplicationBuilder.buildFromFile("src/test/examples/outsidesummary.sd");
+ }
+
+}
diff --git a/config-model/src/test/java/com/yahoo/schema/PredicateDataTypeTestCase.java b/config-model/src/test/java/com/yahoo/schema/PredicateDataTypeTestCase.java
new file mode 100644
index 00000000000..dbea8fb8aeb
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/schema/PredicateDataTypeTestCase.java
@@ -0,0 +1,199 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.schema;
+
+import com.yahoo.schema.document.ImmutableSDField;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+
+import com.yahoo.document.DataType;
+import com.yahoo.schema.parser.ParseException;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+/**
+ * @author Lester Solbakken
+ */
+
+public class PredicateDataTypeTestCase {
+
+ private String searchSd(String field) {
+ return "search p {\n document p {\n" + field + "}\n}\n";
+ }
+
+ private String predicateFieldSd(String index) {
+ return "field pf type predicate {\n" + index + "}\n";
+ }
+
+ private String arrayPredicateFieldSd(String index) {
+ return "field apf type array<predicate> {\n" + index + "}\n";
+ }
+
+ private String stringFieldSd(String index) {
+ return "field sf type string {\n" + index + "}\n";
+ }
+
+ private String attributeFieldSd(String terms) {
+ return "indexing: attribute\n index {\n" + terms + "}\n";
+ }
+
+ private String arityParameter(int arity) {
+ return "arity: " + arity + "\n";
+ }
+
+ private String lowerBoundParameter(long bound) {
+ return "lower-bound: " + bound + "\n";
+ }
+
+ private String upperBoundParameter(long bound) {
+ return "upper-bound: " + bound + "\n";
+ }
+
+ @SuppressWarnings("deprecation")
+ @Rule
+ public ExpectedException exception = ExpectedException.none();
+
+ @Test
+ public void requireThatBuilderSetsIndexParametersCorrectly() throws ParseException {
+ int arity = 2;
+ long lowerBound = -100;
+ long upperBound = 100;
+ String sd = searchSd(
+ predicateFieldSd(
+ attributeFieldSd(
+ arityParameter(arity) +
+ lowerBoundParameter(lowerBound) +
+ upperBoundParameter(upperBound))));
+
+ ApplicationBuilder sb = ApplicationBuilder.createFromString(sd);
+ for (ImmutableSDField field : sb.getSchema().allConcreteFields()) {
+ if (field.getDataType() == DataType.PREDICATE) {
+ for (Index index : field.getIndices().values()) {
+ assertTrue(index.getBooleanIndexDefiniton().hasArity());
+ assertEquals(arity, index.getBooleanIndexDefiniton().getArity());
+ assertTrue(index.getBooleanIndexDefiniton().hasLowerBound());
+ assertEquals(lowerBound, index.getBooleanIndexDefiniton().getLowerBound());
+ assertTrue(index.getBooleanIndexDefiniton().hasUpperBound());
+ assertEquals(upperBound, index.getBooleanIndexDefiniton().getUpperBound());
+ }
+ }
+ }
+ }
+
+ @Test
+ public void requireThatBuilderHandlesLongValues() throws ParseException {
+ int arity = 2;
+ long lowerBound = -100000000000000000L;
+ long upperBound = 1000000000000000000L;
+ String sd = searchSd(
+ predicateFieldSd(
+ attributeFieldSd(
+ arityParameter(arity) +
+ "lower-bound: -100000000000000000L\n" + // +'L'
+ upperBoundParameter(upperBound))));
+
+ ApplicationBuilder sb = ApplicationBuilder.createFromString(sd);
+ for (ImmutableSDField field : sb.getSchema().allConcreteFields()) {
+ if (field.getDataType() == DataType.PREDICATE) {
+ for (Index index : field.getIndices().values()) {
+ assertEquals(arity, index.getBooleanIndexDefiniton().getArity());
+ assertEquals(lowerBound, index.getBooleanIndexDefiniton().getLowerBound());
+ assertEquals(upperBound, index.getBooleanIndexDefiniton().getUpperBound());
+ }
+ }
+ }
+ }
+
+ @Test
+ public void requireThatBuilderHandlesMissingParameters() throws ParseException {
+ String sd = searchSd(
+ predicateFieldSd(
+ attributeFieldSd(
+ arityParameter(2))));
+ ApplicationBuilder sb = ApplicationBuilder.createFromString(sd);
+ for (ImmutableSDField field : sb.getSchema().allConcreteFields()) {
+ if (field.getDataType() == DataType.PREDICATE) {
+ for (Index index : field.getIndices().values()) {
+ assertTrue(index.getBooleanIndexDefiniton().hasArity());
+ assertFalse(index.getBooleanIndexDefiniton().hasLowerBound());
+ assertFalse(index.getBooleanIndexDefiniton().hasUpperBound());
+ }
+ }
+ }
+ }
+
+ @Test
+ public void requireThatBuilderFailsIfNoArityValue() throws ParseException {
+ String sd = searchSd(predicateFieldSd(attributeFieldSd("")));
+
+ exception.expect(IllegalArgumentException.class);
+ exception.expectMessage("Missing arity value in predicate field.");
+ ApplicationBuilder.createFromString(sd);
+ fail();
+ }
+
+ @Test
+ public void requireThatBuilderFailsIfBothIndexAndAttribute() throws ParseException {
+ String sd = searchSd(predicateFieldSd("indexing: summary | index | attribute\nindex { arity: 2 }"));
+
+ exception.expect(IllegalArgumentException.class);
+ exception.expectMessage("For schema 'p', field 'pf': Use 'attribute' instead of 'index'. This will require a refeed if you have upgraded.");
+ ApplicationBuilder.createFromString(sd);
+ }
+
+ @Test
+ public void requireThatBuilderFailsIfIndex() throws ParseException {
+ String sd = searchSd(predicateFieldSd("indexing: summary | index \nindex { arity: 2 }"));
+
+ exception.expect(IllegalArgumentException.class);
+ exception.expectMessage("For schema 'p', field 'pf': Use 'attribute' instead of 'index'. This will require a refeed if you have upgraded.");
+ ApplicationBuilder.createFromString(sd);
+ }
+
+
+ @Test
+ public void requireThatBuilderFailsIfIllegalArityValue() throws ParseException {
+ String sd = searchSd(predicateFieldSd(attributeFieldSd(arityParameter(0))));
+
+ exception.expect(IllegalArgumentException.class);
+ exception.expectMessage("Invalid arity value in predicate field, must be greater than 1.");
+ ApplicationBuilder.createFromString(sd);
+ }
+
+ @Test
+ public void requireThatBuilderFailsIfArityParameterExistButNotPredicateField() throws ParseException {
+ String sd = searchSd(stringFieldSd(attributeFieldSd(arityParameter(2))));
+
+ exception.expect(IllegalArgumentException.class);
+ exception.expectMessage("Arity parameter is used only for predicate type fields.");
+ ApplicationBuilder.createFromString(sd);
+ }
+
+ @Test
+ public void requireThatBuilderFailsIfBoundParametersExistButNotPredicateField() throws ParseException {
+ String sd = searchSd(
+ stringFieldSd(
+ attributeFieldSd(
+ lowerBoundParameter(100) + upperBoundParameter(1000))));
+
+ exception.expect(IllegalArgumentException.class);
+ exception.expectMessage("Parameters lower-bound and upper-bound are used only for predicate type fields.");
+ ApplicationBuilder.createFromString(sd);
+ }
+
+ @Test
+ public void requireThatArrayOfPredicateFails() throws ParseException {
+ String sd = searchSd(
+ arrayPredicateFieldSd(
+ attributeFieldSd(
+ arityParameter(1))));
+
+ exception.expect(IllegalArgumentException.class);
+ exception.expectMessage("Collections of predicates are not allowed.");
+ ApplicationBuilder.createFromString(sd);
+ }
+
+}
diff --git a/config-model/src/test/java/com/yahoo/schema/RankProfileRegistryTest.java b/config-model/src/test/java/com/yahoo/schema/RankProfileRegistryTest.java
new file mode 100644
index 00000000000..de061defb87
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/schema/RankProfileRegistryTest.java
@@ -0,0 +1,58 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.schema;
+
+import com.yahoo.config.model.application.provider.FilesApplicationPackage;
+import com.yahoo.config.model.test.MockApplicationPackage;
+import com.yahoo.config.model.test.TestDriver;
+import com.yahoo.config.model.test.TestRoot;
+import com.yahoo.searchlib.rankingexpression.ExpressionFunction;
+import com.yahoo.searchlib.rankingexpression.RankingExpression;
+import com.yahoo.vespa.config.search.RankProfilesConfig;
+import org.junit.Test;
+
+import java.io.File;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+
+/**
+ * @author Ulf Lilleengen
+ */
+public class RankProfileRegistryTest {
+
+ private static final String TESTDIR = "src/test/cfg/search/data/v2/inherited_rankprofiles";
+
+ @Test
+ public void testRankProfileInheritance() {
+ TestRoot root = new TestDriver().buildModel(FilesApplicationPackage.fromFile(new File(TESTDIR)));
+ RankProfilesConfig left = root.getConfig(RankProfilesConfig.class, "inherit/search/cluster.inherit/left");
+ RankProfilesConfig right = root.getConfig(RankProfilesConfig.class, "inherit/search/cluster.inherit/right");
+ assertEquals(3, left.rankprofile().size());
+ assertEquals(2, right.rankprofile().size());
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testRankProfileDuplicateNameIsIllegal() {
+ Schema schema = new Schema("foo", MockApplicationPackage.createEmpty());
+ RankProfileRegistry rankProfileRegistry = RankProfileRegistry.createRankProfileRegistryWithBuiltinRankProfiles(schema);
+ RankProfile barRankProfile = new RankProfile("bar", schema, rankProfileRegistry);
+ rankProfileRegistry.add(barRankProfile);
+ rankProfileRegistry.add(barRankProfile);
+ }
+
+ @Test
+ public void testRankProfileDuplicateNameLegalForOverridableRankProfiles() {
+ Schema schema = new Schema("foo", MockApplicationPackage.createEmpty());
+ RankProfileRegistry rankProfileRegistry = RankProfileRegistry.createRankProfileRegistryWithBuiltinRankProfiles(schema);
+
+ for (String rankProfileName : RankProfileRegistry.overridableRankProfileNames) {
+ assertNull(rankProfileRegistry.get(schema, rankProfileName).getFunctions().get("foo"));
+ RankProfile rankProfileWithAddedFunction = new RankProfile(rankProfileName, schema, rankProfileRegistry);
+ rankProfileWithAddedFunction.addFunction(new ExpressionFunction("foo", RankingExpression.from("1+2")), true);
+ rankProfileRegistry.add(rankProfileWithAddedFunction);
+ assertNotNull(rankProfileRegistry.get(schema, rankProfileName).getFunctions().get("foo"));
+ }
+ }
+
+}
diff --git a/config-model/src/test/java/com/yahoo/schema/RankProfileTestCase.java b/config-model/src/test/java/com/yahoo/schema/RankProfileTestCase.java
new file mode 100644
index 00000000000..c66d44556ca
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/schema/RankProfileTestCase.java
@@ -0,0 +1,436 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.schema;
+
+import com.yahoo.collections.Pair;
+import com.yahoo.component.ComponentId;
+import com.yahoo.config.model.api.ModelContext;
+import com.yahoo.config.model.application.provider.MockFileRegistry;
+import com.yahoo.config.model.deploy.TestProperties;
+import com.yahoo.config.model.test.MockApplicationPackage;
+import com.yahoo.document.DataType;
+import com.yahoo.search.query.profile.QueryProfile;
+import com.yahoo.search.query.profile.QueryProfileRegistry;
+import com.yahoo.search.query.profile.types.FieldDescription;
+import com.yahoo.search.query.profile.types.FieldType;
+import com.yahoo.search.query.profile.types.QueryProfileType;
+import com.yahoo.search.query.profile.types.QueryProfileTypeRegistry;
+import com.yahoo.schema.derived.AttributeFields;
+import com.yahoo.schema.derived.RawRankProfile;
+import com.yahoo.schema.document.RankType;
+import com.yahoo.schema.document.SDDocumentType;
+import com.yahoo.schema.document.SDField;
+import com.yahoo.schema.parser.ParseException;
+import ai.vespa.rankingexpression.importer.configmodelview.ImportedMlModels;
+
+import static com.yahoo.config.model.test.TestUtil.joinLines;
+
+import org.junit.Test;
+
+import java.util.Iterator;
+import java.util.List;
+import java.util.Optional;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+/**
+ * Tests rank profiles
+ *
+ * @author bratseth
+ */
+public class RankProfileTestCase extends AbstractSchemaTestCase {
+
+ @Test
+ public void testRankProfileInheritance() {
+ Schema schema = new Schema("test", MockApplicationPackage.createEmpty());
+ RankProfileRegistry rankProfileRegistry = RankProfileRegistry.createRankProfileRegistryWithBuiltinRankProfiles(schema);
+ SDDocumentType document = new SDDocumentType("test");
+ SDField a = document.addField("a", DataType.STRING);
+ a.setRankType(RankType.IDENTITY);
+ document.addField("b", DataType.STRING);
+ schema.addDocument(document);
+ RankProfile child = new RankProfile("child", schema, rankProfileRegistry);
+ child.inherit("default");
+ rankProfileRegistry.add(child);
+
+ Iterator<RankProfile.RankSetting> i = child.rankSettingIterator();
+
+ RankProfile.RankSetting setting = i.next();
+ assertEquals(RankType.IDENTITY, setting.getValue());
+ assertEquals("a", setting.getFieldName());
+ assertEquals(RankProfile.RankSetting.Type.RANKTYPE, setting.getType());
+
+ setting = i.next();
+ assertEquals(RankType.DEFAULT, setting.getValue());
+ }
+
+ @Test
+ public void requireThatIllegalInheritanceIsChecked() throws ParseException {
+ try {
+ RankProfileRegistry registry = new RankProfileRegistry();
+ ApplicationBuilder builder = new ApplicationBuilder(registry, setupQueryProfileTypes());
+ builder.addSchema(joinLines(
+ "search test {",
+ " document test { } ",
+ " rank-profile p1 inherits notexist {}",
+ "}"));
+ builder.build(true);
+ fail();
+ } catch (IllegalArgumentException e) {
+ assertEquals("rank-profile 'p1' inherits 'notexist', but this is not found in schema 'test'", e.getMessage());
+ }
+ }
+
+ @Test
+ public void requireThatSelfInheritanceIsIllegal() throws ParseException {
+ try {
+ RankProfileRegistry registry = new RankProfileRegistry();
+ ApplicationBuilder builder = new ApplicationBuilder(registry, setupQueryProfileTypes());
+ builder.addSchema(joinLines(
+ "schema test {",
+ " document test { } ",
+ " rank-profile self inherits self {}",
+ "}"));
+ builder.build(true);
+ fail();
+ } catch (IllegalArgumentException e) {
+ assertEquals("There is a cycle in the inheritance for rank-profile 'test.self' = [test.self, test.self]", e.getMessage());
+ }
+ }
+
+ @Test
+ public void requireThatSelfInheritanceIsLegalWhenOverloading() throws ParseException {
+ RankProfileRegistry registry = new RankProfileRegistry();
+ ApplicationBuilder builder = new ApplicationBuilder(registry, setupQueryProfileTypes());
+ builder.addSchema(joinLines(
+ "schema base {",
+ " document base { } ",
+ " rank-profile self inherits default {}",
+ "}"));
+ builder.addSchema(joinLines(
+ "schema test {",
+ " document test inherits base { } ",
+ " rank-profile self inherits self {}",
+ "}"));
+ builder.build(true);
+ }
+
+ @Test
+ public void requireThatSidewaysInheritanceIsImpossible() throws ParseException {
+ RankProfileRegistry registry = new RankProfileRegistry();
+ ApplicationBuilder builder = new ApplicationBuilder(registry, setupQueryProfileTypes());
+ builder.addSchema(joinLines(
+ "schema child1 {",
+ " document child1 {",
+ " field field1 type int {",
+ " indexing: attribute",
+ " }",
+ " }",
+ " rank-profile child inherits parent {",
+ " function function2() {",
+ " expression: attribute(field1) + 5",
+ " }",
+ " first-phase {",
+ " expression: function2() * function1()",
+ " }",
+ " summary-features {",
+ " function1",
+ " function2",
+ " attribute(field1)",
+ " }",
+ " }",
+ "}\n"));
+ builder.addSchema(joinLines(
+ "schema child2 {",
+ " document child2 {",
+ " field field1 type int {",
+ " indexing: attribute",
+ " }",
+ " }",
+ " rank-profile parent {",
+ " first-phase {",
+ " expression: function1()",
+ " }",
+ " function function1() {",
+ " expression: attribute(field1) + 7",
+ " }",
+ " summary-features {",
+ " function1",
+ " attribute(field1)",
+ " }",
+ " }",
+ "}"));
+ try {
+ builder.build(true);
+ fail("Sideways inheritance should have been enforced");
+ } catch (IllegalArgumentException e) {
+ assertEquals("rank-profile 'child' inherits 'parent', but this is not found in schema 'child1'", e.getMessage());
+ }
+ }
+
+ @Test
+ public void requireThatDefaultInheritingDefaultIsIgnored() throws ParseException {
+ RankProfileRegistry registry = new RankProfileRegistry();
+ ApplicationBuilder builder = new ApplicationBuilder(registry, setupQueryProfileTypes());
+ builder.addSchema(joinLines(
+ "schema test {",
+ " document test { } ",
+ " rank-profile default inherits default {}",
+ "}"));
+ builder.build(true);
+ }
+
+ @Test
+ public void requireThatCyclicInheritanceIsIllegal() throws ParseException {
+ try {
+ RankProfileRegistry registry = new RankProfileRegistry();
+ ApplicationBuilder builder = new ApplicationBuilder(registry, setupQueryProfileTypes());
+ builder.addSchema(joinLines(
+ "search test {",
+ " document test { } ",
+ " rank-profile a inherits b {}",
+ " rank-profile b inherits c {}",
+ " rank-profile c inherits a {}",
+ "}"));
+ builder.build(true);
+ fail();
+ } catch (IllegalArgumentException e) {
+ assertEquals("There is a cycle in the inheritance for rank-profile 'test.c' = [test.c, test.a, test.b, test.c]", e.getMessage());
+ }
+ }
+
+ @Test
+ public void requireThatRankProfilesCanInheritNotYetSeenProfiles() throws ParseException
+ {
+ RankProfileRegistry registry = new RankProfileRegistry();
+ ApplicationBuilder builder = new ApplicationBuilder(registry, setupQueryProfileTypes());
+ builder.addSchema(joinLines(
+ "search test {",
+ " document test { } ",
+ " rank-profile p1 inherits not_yet_defined {}",
+ " rank-profile not_yet_defined {}",
+ "}"));
+ builder.build(true);
+ assertNotNull(registry.get("test","p1"));
+ assertTrue(registry.get("test","p1").inherits("not_yet_defined"));
+ assertNotNull(registry.get("test","not_yet_defined"));
+ }
+
+ private String createSD(Double termwiseLimit) {
+ return joinLines(
+ "search test {",
+ " document test { ",
+ " field a type string { ",
+ " indexing: index ",
+ " }",
+ " }",
+ " ",
+ " rank-profile parent {",
+ (termwiseLimit != null ? (" termwise-limit:" + termwiseLimit + "\n") : ""),
+ " num-threads-per-search:8",
+ " min-hits-per-thread:70",
+ " num-search-partitions:1200",
+ " }",
+ " rank-profile child inherits parent { }",
+ "}");
+ }
+
+ @Test
+ public void testTermwiseLimitWithDeployOverride() throws ParseException {
+ verifyTermwiseLimitAndSomeMoreIncludingInheritance(new TestProperties(), createSD(null), null);
+ verifyTermwiseLimitAndSomeMoreIncludingInheritance(new TestProperties(), createSD(0.78), 0.78);
+ verifyTermwiseLimitAndSomeMoreIncludingInheritance(new TestProperties().setDefaultTermwiseLimit(0.09), createSD(null), 0.09);
+ verifyTermwiseLimitAndSomeMoreIncludingInheritance(new TestProperties().setDefaultTermwiseLimit(0.09), createSD(0.37), 0.37);
+ }
+
+ private void verifyTermwiseLimitAndSomeMoreIncludingInheritance(ModelContext.Properties deployProperties, String sd, Double termwiseLimit) throws ParseException {
+ RankProfileRegistry rankProfileRegistry = new RankProfileRegistry();
+ ApplicationBuilder builder = new ApplicationBuilder(rankProfileRegistry);
+ builder.addSchema(sd);
+ builder.build(true);
+ Schema schema = builder.getSchema();
+ AttributeFields attributeFields = new AttributeFields(schema);
+ verifyRankProfile(rankProfileRegistry.get(schema, "parent"), attributeFields, deployProperties, termwiseLimit);
+ verifyRankProfile(rankProfileRegistry.get(schema, "child"), attributeFields, deployProperties, termwiseLimit);
+ }
+
+ private void verifyRankProfile(RankProfile rankProfile, AttributeFields attributeFields, ModelContext.Properties deployProperties,
+ Double expectedTermwiseLimit) {
+ assertEquals(8, rankProfile.getNumThreadsPerSearch());
+ assertEquals(70, rankProfile.getMinHitsPerThread());
+ assertEquals(1200, rankProfile.getNumSearchPartitions());
+ RawRankProfile rawRankProfile = new RawRankProfile(rankProfile, new LargeRankExpressions(new MockFileRegistry()), new QueryProfileRegistry(),
+ new ImportedMlModels(), attributeFields, deployProperties);
+ if (expectedTermwiseLimit != null) {
+ assertTrue(findProperty(rawRankProfile.configProperties(), "vespa.matching.termwise_limit").isPresent());
+ assertEquals(String.valueOf(expectedTermwiseLimit), findProperty(rawRankProfile.configProperties(), "vespa.matching.termwise_limit").get());
+ } else {
+ assertFalse(findProperty(rawRankProfile.configProperties(), "vespa.matching.termwise_limit").isPresent());
+ }
+ assertTrue(findProperty(rawRankProfile.configProperties(), "vespa.matching.numthreadspersearch").isPresent());
+ assertEquals("8", findProperty(rawRankProfile.configProperties(), "vespa.matching.numthreadspersearch").get());
+ assertTrue(findProperty(rawRankProfile.configProperties(), "vespa.matching.minhitsperthread").isPresent());
+ assertEquals("70", findProperty(rawRankProfile.configProperties(), "vespa.matching.minhitsperthread").get());
+ assertTrue(findProperty(rawRankProfile.configProperties(), "vespa.matching.numsearchpartitions").isPresent());
+ assertEquals("1200", findProperty(rawRankProfile.configProperties(), "vespa.matching.numsearchpartitions").get());
+ }
+
+ @Test
+ public void requireThatConfigIsDerivedForAttributeTypeSettings() throws ParseException {
+ RankProfileRegistry registry = new RankProfileRegistry();
+ ApplicationBuilder builder = new ApplicationBuilder(registry);
+ builder.addSchema(joinLines(
+ "search test {",
+ " document test { ",
+ " field a type tensor(x[10]) { indexing: attribute }",
+ " field b type tensor(y{}) { indexing: attribute }",
+ " field c type tensor(x[5]) { indexing: attribute }",
+ " }",
+ " rank-profile p1 {}",
+ " rank-profile p2 {}",
+ "}"));
+ builder.build(true);
+ Schema schema = builder.getSchema();
+
+ assertEquals(4, registry.all().size());
+ assertAttributeTypeSettings(registry.get(schema, "default"), schema);
+ assertAttributeTypeSettings(registry.get(schema, "unranked"), schema);
+ assertAttributeTypeSettings(registry.get(schema, "p1"), schema);
+ assertAttributeTypeSettings(registry.get(schema, "p2"), schema);
+ }
+
+ @Test
+ public void requireThatDenseDimensionsMustBeBound() throws ParseException {
+ try {
+ ApplicationBuilder builder = new ApplicationBuilder(new RankProfileRegistry());
+ builder.addSchema(joinLines(
+ "search test {",
+ " document test { ",
+ " field a type tensor(x[]) { indexing: attribute }",
+ " }",
+ "}"));
+ builder.build(true);
+ }
+ catch (IllegalArgumentException e) {
+ assertEquals("Illegal type in field a type tensor(x[]): Dense tensor dimensions must have a size",
+ e.getMessage());
+ }
+ }
+
+ private static RawRankProfile createRawRankProfile(RankProfile profile, Schema schema) {
+ return new RawRankProfile(profile, new LargeRankExpressions(new MockFileRegistry()), new QueryProfileRegistry(), new ImportedMlModels(), new AttributeFields(schema), new TestProperties());
+ }
+
+ private static void assertAttributeTypeSettings(RankProfile profile, Schema schema) {
+ RawRankProfile rawProfile = createRawRankProfile(profile, schema);
+ assertEquals("tensor(x[10])", findProperty(rawProfile.configProperties(), "vespa.type.attribute.a").get());
+ assertEquals("tensor(y{})", findProperty(rawProfile.configProperties(), "vespa.type.attribute.b").get());
+ assertEquals("tensor(x[5])", findProperty(rawProfile.configProperties(), "vespa.type.attribute.c").get());
+ }
+
+ @Test
+ public void requireThatConfigIsDerivedForQueryFeatureTypeSettings() throws ParseException {
+ RankProfileRegistry registry = new RankProfileRegistry();
+ ApplicationBuilder builder = new ApplicationBuilder(registry, setupQueryProfileTypes());
+ builder.addSchema(joinLines(
+ "search test {",
+ " document test { } ",
+ " rank-profile p1 {}",
+ " rank-profile p2 {}",
+ "}"));
+ builder.build(true);
+ Schema schema = builder.getSchema();
+
+ assertEquals(4, registry.all().size());
+ assertQueryFeatureTypeSettings(registry.get(schema, "default"), schema);
+ assertQueryFeatureTypeSettings(registry.get(schema, "unranked"), schema);
+ assertQueryFeatureTypeSettings(registry.get(schema, "p1"), schema);
+ assertQueryFeatureTypeSettings(registry.get(schema, "p2"), schema);
+ }
+
+ private static QueryProfileRegistry setupQueryProfileTypes() {
+ QueryProfileRegistry registry = new QueryProfileRegistry();
+ QueryProfileTypeRegistry typeRegistry = registry.getTypeRegistry();
+ QueryProfileType type = new QueryProfileType(new ComponentId("testtype"));
+ type.addField(new FieldDescription("ranking.features.query(tensor1)",
+ FieldType.fromString("tensor(x[10])", typeRegistry)), typeRegistry);
+ type.addField(new FieldDescription("ranking.features.query(tensor2)",
+ FieldType.fromString("tensor(y{})", typeRegistry)), typeRegistry);
+ type.addField(new FieldDescription("ranking.features.invalid(tensor3)",
+ FieldType.fromString("tensor(x{})", typeRegistry)), typeRegistry);
+ type.addField(new FieldDescription("ranking.features.query(numeric)",
+ FieldType.fromString("integer", typeRegistry)), typeRegistry);
+ typeRegistry.register(type);
+ var profile = new QueryProfile(new ComponentId("testprofile"));
+ profile.setType(type);
+ registry.register(profile);
+ return registry;
+ }
+
+ private static void assertQueryFeatureTypeSettings(RankProfile profile, Schema schema) {
+ RawRankProfile rawProfile =createRawRankProfile(profile, schema);
+ assertEquals("tensor(x[10])", findProperty(rawProfile.configProperties(), "vespa.type.query.tensor1").get());
+ assertEquals("tensor(y{})", findProperty(rawProfile.configProperties(), "vespa.type.query.tensor2").get());
+ assertFalse(findProperty(rawProfile.configProperties(), "vespa.type.query.tensor3").isPresent());
+ assertFalse(findProperty(rawProfile.configProperties(), "vespa.type.query.numeric").isPresent());
+ }
+
+ private static Optional<String> findProperty(List<Pair<String, String>> properties, String key) {
+ for (Pair<String, String> property : properties)
+ if (property.getFirst().equals(key))
+ return Optional.of(property.getSecond());
+ return Optional.empty();
+ }
+
+ @Test
+ public void approximate_nearest_neighbor_threshold_settings_are_configurable() throws ParseException {
+ verifyApproximateNearestNeighborThresholdSettings(0.7, null);
+ verifyApproximateNearestNeighborThresholdSettings(null, 0.3);
+ verifyApproximateNearestNeighborThresholdSettings(0.7, 0.3);
+ }
+
+ private void verifyApproximateNearestNeighborThresholdSettings(Double postFilterThreshold, Double approximateThreshold) throws ParseException {
+ var rankProfileRegistry = new RankProfileRegistry();
+ var props = new TestProperties();
+ var queryProfileRegistry = new QueryProfileRegistry();
+ var builder = new ApplicationBuilder(rankProfileRegistry, queryProfileRegistry, props);
+ builder.addSchema(createSDWithRankProfileThresholds(postFilterThreshold, approximateThreshold));
+ builder.build(true);
+
+ var schema = builder.getSchema();
+ var rankProfile = rankProfileRegistry.get(schema, "my_profile");
+ var rawRankProfile = new RawRankProfile(rankProfile, new LargeRankExpressions(new MockFileRegistry()), queryProfileRegistry,
+ new ImportedMlModels(), new AttributeFields(schema), props);
+
+ if (postFilterThreshold != null) {
+ assertEquals((double)postFilterThreshold, rankProfile.getPostFilterThreshold().getAsDouble(), 0.000001);
+ assertEquals(String.valueOf(postFilterThreshold), findProperty(rawRankProfile.configProperties(), "vespa.matching.global_filter.upper_limit").get());
+ } else {
+ assertTrue(rankProfile.getPostFilterThreshold().isEmpty());
+ assertFalse(findProperty(rawRankProfile.configProperties(), "vespa.matching.global_filter.upper_limit").isPresent());
+ }
+
+ if (approximateThreshold != null) {
+ assertEquals((double)approximateThreshold, rankProfile.getApproximateThreshold().getAsDouble(), 0.000001);
+ assertEquals(String.valueOf(approximateThreshold), findProperty(rawRankProfile.configProperties(), "vespa.matching.global_filter.lower_limit").get());
+ } else {
+ assertTrue(rankProfile.getApproximateThreshold().isEmpty());
+ assertFalse(findProperty(rawRankProfile.configProperties(), "vespa.matching.global_filter.lower_limit").isPresent());
+ }
+ }
+
+ private String createSDWithRankProfileThresholds(Double postFilterThreshold, Double approximateThreshold) {
+ return joinLines(
+ "search test {",
+ " document test {}",
+ " rank-profile my_profile {",
+ (postFilterThreshold != null ? (" post-filter-threshold: " + postFilterThreshold) : ""),
+ (approximateThreshold != null ? (" approximate-threshold: " + approximateThreshold) : ""),
+ " }",
+ "}");
+ }
+
+}
diff --git a/config-model/src/test/java/com/yahoo/schema/RankPropertiesTestCase.java b/config-model/src/test/java/com/yahoo/schema/RankPropertiesTestCase.java
new file mode 100644
index 00000000000..c3595717220
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/schema/RankPropertiesTestCase.java
@@ -0,0 +1,163 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.schema;
+
+import com.yahoo.config.model.application.provider.MockFileRegistry;
+import com.yahoo.config.model.deploy.TestProperties;
+import com.yahoo.search.query.profile.QueryProfileRegistry;
+import com.yahoo.schema.derived.AttributeFields;
+import com.yahoo.schema.derived.RawRankProfile;
+import com.yahoo.schema.parser.ParseException;
+import ai.vespa.rankingexpression.importer.configmodelview.ImportedMlModels;
+import org.junit.Test;
+
+import java.util.List;
+
+import static com.yahoo.config.model.test.TestUtil.joinLines;
+import static org.junit.Assert.assertEquals;
+
+/**
+ * @author bratseth
+ */
+public class RankPropertiesTestCase extends AbstractSchemaTestCase {
+
+ @Test
+ public void testRankPropertyInheritance() throws ParseException {
+ RankProfileRegistry rankProfileRegistry = new RankProfileRegistry();
+ ApplicationBuilder builder = new ApplicationBuilder(rankProfileRegistry);
+ builder.addSchema(joinLines(
+ "search test {",
+ " document test {",
+ " field a type string { ",
+ " indexing: index ",
+ " }",
+ " }",
+ " rank-profile parent {",
+ " first-phase {",
+ " expression: a",
+ " }",
+ " rank-properties {",
+ " query(a): 1500 ",
+ " }",
+ " }",
+ " rank-profile child inherits parent {",
+ " first-phase {",
+ " expression: a",
+ " }",
+ " rank-properties {",
+ " query(a): 2000 ",
+ " }",
+ " }",
+ "}"));
+ builder.build(true);
+ Schema schema = builder.getSchema();
+ AttributeFields attributeFields = new AttributeFields(schema);
+
+ {
+ // Check declared model
+ RankProfile parent = rankProfileRegistry.get(schema, "parent");
+ assertEquals("query(a) = 1500", parent.getRankProperties().get(0).toString());
+
+ // Check derived model
+ RawRankProfile rawParent = new RawRankProfile(parent, new LargeRankExpressions(new MockFileRegistry()), new QueryProfileRegistry(), new ImportedMlModels(), attributeFields, new TestProperties());
+ assertEquals("(query(a), 1500)", rawParent.configProperties().get(0).toString());
+ }
+
+ {
+ // Check declared model
+ RankProfile parent = rankProfileRegistry.get(schema, "child");
+ assertEquals("query(a) = 2000", parent.getRankProperties().get(0).toString());
+
+ // Check derived model
+ RawRankProfile rawChild = new RawRankProfile(rankProfileRegistry.get(schema, "child"),
+ new LargeRankExpressions(new MockFileRegistry()),
+ new QueryProfileRegistry(),
+ new ImportedMlModels(),
+ attributeFields,
+ new TestProperties());
+ assertEquals("(query(a), 2000)", rawChild.configProperties().get(0).toString());
+ }
+ }
+ @Test
+ public void testRankProfileMutate() throws ParseException {
+ RankProfileRegistry rankProfileRegistry = new RankProfileRegistry();
+ ApplicationBuilder builder = new ApplicationBuilder(rankProfileRegistry);
+ builder.addSchema(joinLines(
+ "search test {",
+ " document test {",
+ " field a type int { ",
+ " indexing: attribute ",
+ " }",
+ " }",
+ " field synthetic_attribute_a type int {",
+ " indexing: attribute",
+ " attribute: mutable",
+ " }",
+ " field synthetic_attribute_b type double {",
+ " indexing: attribute",
+ " attribute: mutable",
+ " }",
+ " field synthetic_attribute_c type long {",
+ " indexing: attribute",
+ " attribute: mutable",
+ " }",
+ " rank-profile a {",
+ " mutate {",
+ " on-match {",
+ " synthetic_attribute_a += 7",
+ " }",
+ " on-first-phase {",
+ " synthetic_attribute_b +=1",
+ " }",
+ " on-second-phase {",
+ " synthetic_attribute_b = 1.01",
+ " }",
+ " on-summary {",
+ " synthetic_attribute_c -= 1",
+ " }",
+ " }",
+ " first-phase {",
+ " expression: a",
+ " }",
+ " }",
+ " rank-profile b {",
+ " first-phase {",
+ " expression: a",
+ " }",
+ " second-phase {",
+ " expression: a",
+ " }",
+ " }",
+ "}"));
+ builder.build(true);
+ Schema schema = builder.getSchema();
+ RankProfile a = rankProfileRegistry.get(schema, "a");
+ List<RankProfile.MutateOperation> operations = a.getMutateOperations();
+ assertEquals(4, operations.size());
+ assertEquals(RankProfile.MutateOperation.Phase.on_match, operations.get(0).phase);
+ assertEquals("synthetic_attribute_a", operations.get(0).attribute);
+ assertEquals("+=7", operations.get(0).operation);
+ assertEquals(RankProfile.MutateOperation.Phase.on_first_phase, operations.get(1).phase);
+ assertEquals("synthetic_attribute_b", operations.get(1).attribute);
+ assertEquals("+=1", operations.get(1).operation);
+ assertEquals(RankProfile.MutateOperation.Phase.on_second_phase, operations.get(2).phase);
+ assertEquals("synthetic_attribute_b", operations.get(2).attribute);
+ assertEquals("=1.01", operations.get(2).operation);
+ assertEquals(RankProfile.MutateOperation.Phase.on_summary, operations.get(3).phase);
+ assertEquals("synthetic_attribute_c", operations.get(3).attribute);
+ assertEquals("-=1", operations.get(3).operation);
+
+ AttributeFields attributeFields = new AttributeFields(schema);
+ RawRankProfile raw = new RawRankProfile(a, new LargeRankExpressions(new MockFileRegistry()), new QueryProfileRegistry(), new ImportedMlModels(), attributeFields, new TestProperties());
+ assertEquals(9, raw.configProperties().size());
+ assertEquals("(vespa.mutate.on_match.attribute, synthetic_attribute_a)", raw.configProperties().get(0).toString());
+ assertEquals("(vespa.mutate.on_match.operation, +=7)", raw.configProperties().get(1).toString());
+ assertEquals("(vespa.mutate.on_first_phase.attribute, synthetic_attribute_b)", raw.configProperties().get(2).toString());
+ assertEquals("(vespa.mutate.on_first_phase.operation, +=1)", raw.configProperties().get(3).toString());
+ assertEquals("(vespa.mutate.on_second_phase.attribute, synthetic_attribute_b)", raw.configProperties().get(4).toString());
+ assertEquals("(vespa.mutate.on_second_phase.operation, =1.01)", raw.configProperties().get(5).toString());
+ assertEquals("(vespa.mutate.on_summary.attribute, synthetic_attribute_c)", raw.configProperties().get(6).toString());
+ assertEquals("(vespa.mutate.on_summary.operation, -=1)", raw.configProperties().get(7).toString());
+ assertEquals("(vespa.rank.firstphase, a)", raw.configProperties().get(8).toString());
+ }
+
+}
diff --git a/config-model/src/test/java/com/yahoo/schema/RankingConstantTest.java b/config-model/src/test/java/com/yahoo/schema/RankingConstantTest.java
new file mode 100644
index 00000000000..883e6b50abb
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/schema/RankingConstantTest.java
@@ -0,0 +1,213 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.schema;
+
+import com.yahoo.schema.parser.ParseException;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+
+import java.util.Iterator;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static com.yahoo.config.model.test.TestUtil.joinLines;
+import static org.junit.Assert.fail;
+
+/**
+ * @author gjoranv
+ */
+public class RankingConstantTest {
+
+ @SuppressWarnings("deprecation")
+ @Rule
+ public ExpectedException thrown = ExpectedException.none();
+
+ @Test
+ public void tensor_constant_properties_are_set() throws Exception {
+ final String TENSOR_NAME = "my_global_tensor";
+ final String TENSOR_FILE = "path/my-tensor-file.json";
+ final String TENSOR_TYPE = "tensor(x{})";
+ RankProfileRegistry rankProfileRegistry = new RankProfileRegistry();
+ ApplicationBuilder schemaBuilder = new ApplicationBuilder(rankProfileRegistry);
+ schemaBuilder.addSchema(joinLines(
+ "schema test {",
+ " document test { }",
+ " rank-profile my_rank_profile {",
+ " first-phase {",
+ " expression: sum(constant(my_global_tensor))",
+ " }",
+ " }",
+ " constant " + TENSOR_NAME + " {",
+ " file: " + TENSOR_FILE,
+ " type: " + TENSOR_TYPE,
+ " }",
+ "}"
+ ));
+ schemaBuilder.build(true);
+ Schema schema = schemaBuilder.getSchema();
+
+ Iterator<RankProfile.Constant> constantIterator = schema.constants().values().iterator();
+ RankProfile.Constant constant = constantIterator.next();
+ assertEquals(TENSOR_NAME, constant.name().simpleArgument().get());
+ assertEquals(TENSOR_FILE, constant.valuePath().get());
+ assertEquals(TENSOR_TYPE, constant.type().toString());
+ assertEquals(DistributableResource.PathType.FILE, constant.pathType().get());
+
+ assertFalse(constantIterator.hasNext());
+ }
+
+ @Test
+ public void tensor_constant_must_have_a_type() throws Exception {
+ RankProfileRegistry rankProfileRegistry = new RankProfileRegistry();
+ ApplicationBuilder schemaBuilder = new ApplicationBuilder(rankProfileRegistry);
+ thrown.expect(IllegalArgumentException.class);
+ thrown.expectMessage("must have a type");
+ schemaBuilder.addSchema(joinLines(
+ "schema test {",
+ " document test { }",
+ " constant foo {",
+ " file: bar.baz",
+ " }",
+ "}"
+ ));
+ }
+
+ @Test
+ public void tensor_constant_must_have_a_file() throws Exception {
+ RankProfileRegistry rankProfileRegistry = new RankProfileRegistry();
+ ApplicationBuilder schemaBuilder = new ApplicationBuilder(rankProfileRegistry);
+ thrown.expect(IllegalArgumentException.class);
+ thrown.expectMessage("must have a file");
+ schemaBuilder.addSchema(joinLines(
+ "schema test {",
+ " document test { }",
+ " constant foo {",
+ " type: tensor(x[])",
+ " }",
+ "}"
+ ));
+ }
+
+ @Test
+ public void constant_file_does_not_need_path_or_ending() throws Exception {
+ RankProfileRegistry rankProfileRegistry = new RankProfileRegistry();
+ ApplicationBuilder schemaBuilder = new ApplicationBuilder(rankProfileRegistry);
+ schemaBuilder.addSchema(joinLines(
+ "schema test {",
+ " document test { }",
+ " constant foo {",
+ " type: tensor(x{})",
+ " file: simplename",
+ " }",
+ "}"
+ ));
+ schemaBuilder.build(true);
+ Schema schema = schemaBuilder.getSchema();
+ RankProfile.Constant constant = schema.constants().values().iterator().next();
+ assertEquals("simplename", constant.valuePath().get());
+ }
+
+ @Test
+ public void constant_uri_is_allowed() throws Exception {
+ RankProfileRegistry rankProfileRegistry = new RankProfileRegistry();
+ ApplicationBuilder schemaBuilder = new ApplicationBuilder(rankProfileRegistry);
+ schemaBuilder.addSchema(joinLines(
+ "schema test {",
+ " document test { }",
+ " constant foo {",
+ " type: tensor(x{})",
+ " uri: http://somewhere.far.away/in/another-galaxy",
+ " }",
+ "}"
+ ));
+ schemaBuilder.build(true);
+ Schema schema = schemaBuilder.getSchema();
+ RankProfile.Constant constant = schema.constants().values().iterator().next();
+ assertEquals(DistributableResource.PathType.URI, constant.pathType().get());
+ assertEquals("http://somewhere.far.away/in/another-galaxy", constant.valuePath().get());
+ }
+
+ @Test
+ public void constant_https_uri_is_allowed() throws Exception {
+ RankProfileRegistry rankProfileRegistry = new RankProfileRegistry();
+ ApplicationBuilder schemaBuilder = new ApplicationBuilder(rankProfileRegistry);
+ schemaBuilder.addSchema(joinLines(
+ "schema test {",
+ " document test { }",
+ " constant foo {",
+ " type: tensor(x{})",
+ " uri: https://somewhere.far.away:4443/in/another-galaxy",
+ " }",
+ "}"
+ ));
+ schemaBuilder.build(true);
+ Schema schema = schemaBuilder.getSchema();
+ RankProfile.Constant constant = schema.constants().values().iterator().next();
+ assertEquals(DistributableResource.PathType.URI, constant.pathType().get());
+ assertEquals("https://somewhere.far.away:4443/in/another-galaxy", constant.valuePath().get());
+ }
+
+ @Test
+ public void constant_uri_with_port_is_allowed() throws Exception {
+ RankProfileRegistry rankProfileRegistry = new RankProfileRegistry();
+ ApplicationBuilder schemaBuilder = new ApplicationBuilder(rankProfileRegistry);
+ schemaBuilder.addSchema(joinLines(
+ "schema test {",
+ " document test { }",
+ " constant foo {",
+ " type: tensor(x{})",
+ " uri: http://somewhere.far.away:4080/in/another-galaxy",
+ " }",
+ "}"
+ ));
+ schemaBuilder.build(true);
+ Schema schema = schemaBuilder.getSchema();
+ RankProfile.Constant constant = schema.constants().values().iterator().next();
+ assertEquals(DistributableResource.PathType.URI, constant.pathType().get());
+ assertEquals("http://somewhere.far.away:4080/in/another-galaxy", constant.valuePath().get());
+ }
+
+ @Test
+ public void constant_uri_no_dual_slashes_is_allowed() throws Exception {
+ RankProfileRegistry rankProfileRegistry = new RankProfileRegistry();
+ ApplicationBuilder schemaBuilder = new ApplicationBuilder(rankProfileRegistry);
+ schemaBuilder.addSchema(joinLines(
+ "schema test {",
+ " document test { }",
+ " constant foo {",
+ " type: tensor(x{})",
+ " uri: http:somewhere.far.away/in/another-galaxy",
+ " }",
+ "}"
+ ));
+ schemaBuilder.build(true);
+ Schema schema = schemaBuilder.getSchema();
+ RankProfile.Constant constant = schema.constants().values().iterator().next();
+ assertEquals(DistributableResource.PathType.URI, constant.pathType().get());
+ assertEquals("http:somewhere.far.away/in/another-galaxy", constant.valuePath().get());
+ }
+
+ @Test
+ public void constant_uri_only_supports_http_and_https() {
+ RankProfileRegistry rankProfileRegistry = new RankProfileRegistry();
+ ApplicationBuilder schemaBuilder = new ApplicationBuilder(rankProfileRegistry);
+ String expectedMessage = "Encountered \" <IDENTIFIER> \"ftp\"\" at line 5, column 10.\n\n" +
+ "Was expecting:\n\n" +
+ "<URI_PATH> ...";
+ try {
+ schemaBuilder.addSchema(joinLines(
+ "schema test {",
+ " document test { }",
+ " constant foo {",
+ " type: tensor(x{})",
+ " uri: ftp:somewhere.far.away/in/another-galaxy",
+ " }",
+ "}"
+ ));
+ } catch (ParseException e) {
+ if (! e.getMessage().startsWith(expectedMessage))
+ fail("Expected exception with message starting with:\n'" + expectedMessage + "\nBut got:\n'" + e.getMessage());
+ }
+ }
+
+}
diff --git a/config-model/src/test/java/com/yahoo/schema/RankingExpressionConstantsTestCase.java b/config-model/src/test/java/com/yahoo/schema/RankingExpressionConstantsTestCase.java
new file mode 100644
index 00000000000..bd0bd65295c
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/schema/RankingExpressionConstantsTestCase.java
@@ -0,0 +1,229 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.schema;
+
+import com.yahoo.collections.Pair;
+import com.yahoo.config.model.application.provider.MockFileRegistry;
+import com.yahoo.config.model.deploy.TestProperties;
+import com.yahoo.search.query.profile.QueryProfileRegistry;
+import ai.vespa.rankingexpression.importer.configmodelview.ImportedMlModels;
+import com.yahoo.yolean.Exceptions;
+import com.yahoo.schema.derived.AttributeFields;
+import com.yahoo.schema.derived.RawRankProfile;
+import com.yahoo.schema.parser.ParseException;
+import org.junit.Test;
+
+import java.util.List;
+
+import static org.junit.Assert.*;
+
+/**
+ * @author bratseth
+ */
+public class RankingExpressionConstantsTestCase extends AbstractSchemaTestCase {
+
+ @Test
+ public void testConstants() throws ParseException {
+ RankProfileRegistry rankProfileRegistry = new RankProfileRegistry();
+ QueryProfileRegistry queryProfileRegistry = new QueryProfileRegistry();
+ ApplicationBuilder builder = new ApplicationBuilder(rankProfileRegistry);
+ builder.addSchema(
+ "schema test {\n" +
+ " document test { \n" +
+ " field a type string { \n" +
+ " indexing: index \n" +
+ " }\n" +
+ " }\n" +
+ " \n" +
+ " rank-profile parent {\n" +
+ " constants {\n" +
+ " p1 double: 7 \n" +
+ " constant(p2) double: 0 \n" +
+ " }\n" +
+ " first-phase {\n" +
+ " expression: p2 * (1.3 + p1 )\n" +
+ " }\n" +
+ " }\n" +
+ " rank-profile child1 inherits parent {\n" +
+ " first-phase {\n" +
+ " expression: a + b + c \n" +
+ " }\n" +
+ " second-phase {\n" +
+ " expression: a + p1 + c \n" +
+ " }\n" +
+ " constants {\n" +
+ " a: 1.0 \n" +
+ " constant(b): 2 \n" +
+ " c: 3.5 \n" +
+ " }\n" +
+ " }\n" +
+ " rank-profile child2 inherits parent {\n" +
+ " constants {\n" +
+ " p2: 2.0 \n" +
+ " }\n" +
+ " function foo() {\n" +
+ " expression: p2*p1\n" +
+ " }\n" +
+ " }\n" +
+ "\n" +
+ "}\n");
+ builder.build(true);
+ Schema s = builder.getSchema();
+ RankProfile parent = rankProfileRegistry.get(s, "parent").compile(queryProfileRegistry, new ImportedMlModels());
+ assertEquals("0.0", parent.getFirstPhaseRanking().getRoot().toString());
+
+ RankProfile child1 = rankProfileRegistry.get(s, "child1").compile(queryProfileRegistry, new ImportedMlModels());
+ assertEquals("6.5", child1.getFirstPhaseRanking().getRoot().toString());
+ assertEquals("11.5", child1.getSecondPhaseRanking().getRoot().toString());
+
+ RankProfile child2 = rankProfileRegistry.get(s, "child2").compile(queryProfileRegistry, new ImportedMlModels());
+ assertEquals("16.6", child2.getFirstPhaseRanking().getRoot().toString());
+ assertEquals("foo: 14.0", child2.getFunctions().get("foo").function().getBody().toString());
+ List<Pair<String, String>> rankProperties = new RawRankProfile(child2,
+ new LargeRankExpressions(new MockFileRegistry()),
+ queryProfileRegistry,
+ new ImportedMlModels(),
+ new AttributeFields(s),
+ new TestProperties()).configProperties();
+ assertEquals("(rankingExpression(foo).rankingScript, 14.0)", rankProperties.get(0).toString());
+ assertEquals("(rankingExpression(firstphase).rankingScript, 16.6)", rankProperties.get(2).toString());
+ }
+
+ @Test
+ public void testNameCollision() throws ParseException {
+ RankProfileRegistry rankProfileRegistry = new RankProfileRegistry();
+ ApplicationBuilder builder = new ApplicationBuilder(rankProfileRegistry);
+ builder.addSchema(
+ "schema test {\n" +
+ " document test { \n" +
+ " field a type string { \n" +
+ " indexing: index \n" +
+ " }\n" +
+ " }\n" +
+ " \n" +
+ " rank-profile test {\n" +
+ " constants {\n" +
+ " c: 7 \n" +
+ " }\n" +
+ " function c() {\n" +
+ " expression: p2*p1\n" +
+ " }\n" +
+ " }\n" +
+ "\n" +
+ "}\n");
+ builder.build(true);
+ Schema s = builder.getSchema();
+ try {
+ rankProfileRegistry.get(s, "test").compile(new QueryProfileRegistry(), new ImportedMlModels());
+ fail("Should have caused an exception");
+ }
+ catch (IllegalArgumentException e) {
+ assertEquals("Rank profile 'test' is invalid: Cannot have both a constant and function named 'c'",
+ Exceptions.toMessageString(e));
+ }
+ }
+
+ @Test
+ public void testNegativeLiteralArgument() throws ParseException {
+ RankProfileRegistry rankProfileRegistry = new RankProfileRegistry();
+ ApplicationBuilder builder = new ApplicationBuilder(rankProfileRegistry);
+ builder.addSchema(
+ "search test {\n" +
+ " document test { \n" +
+ " field a type string { \n" +
+ " indexing: index \n" +
+ " }\n" +
+ " }\n" +
+ " \n" +
+ " rank-profile test {\n" +
+ " function POP_SLOW_SCORE() {\n" +
+ " expression: safeLog(popShareSlowDecaySignal, -9.21034037)\n" +
+ " }\n" +
+ " }\n" +
+ "\n" +
+ "}\n");
+ builder.build(true);
+ Schema s = builder.getSchema();
+ RankProfile profile = rankProfileRegistry.get(s, "test");
+ assertEquals("safeLog(popShareSlowDecaySignal,-9.21034037)", profile.getFunctions().get("POP_SLOW_SCORE").function().getBody().getRoot().toString());
+ }
+
+ @Test
+ public void testNegativeConstantArgument() throws ParseException {
+ RankProfileRegistry rankProfileRegistry = new RankProfileRegistry();
+ ApplicationBuilder builder = new ApplicationBuilder(rankProfileRegistry);
+ builder.addSchema(
+ "schema test {\n" +
+ " document test { \n" +
+ " field a type string { \n" +
+ " indexing: index \n" +
+ " }\n" +
+ " }\n" +
+ " \n" +
+ " rank-profile test {\n" +
+ " constants {\n" +
+ " myValue: -9.21034037\n" +
+ " }\n" +
+ " function POP_SLOW_SCORE() {\n" +
+ " expression: safeLog(popShareSlowDecaySignal, myValue)\n" +
+ " }\n" +
+ " }\n" +
+ "\n" +
+ "}\n");
+ builder.build(true);
+ Schema s = builder.getSchema();
+ RankProfile profile = rankProfileRegistry.get(s, "test");
+ assertEquals("safeLog(popShareSlowDecaySignal,myValue)", profile.getFunctions().get("POP_SLOW_SCORE").function().getBody().getRoot().toString());
+ assertEquals("safeLog(popShareSlowDecaySignal,-9.21034037)",
+ profile.compile(new QueryProfileRegistry(), new ImportedMlModels()).getFunctions().get("POP_SLOW_SCORE").function().getBody().getRoot().toString());
+ }
+
+ @Test
+ public void testConstantDivisorInFunction() throws ParseException {
+ RankProfileRegistry rankProfileRegistry = new RankProfileRegistry();
+ ApplicationBuilder builder = new ApplicationBuilder(rankProfileRegistry);
+ builder.addSchema(
+ "search test {\n" +
+ " document test { \n" +
+ " }\n" +
+ " \n" +
+ " rank-profile test {\n" +
+ " function rank_default(){\n" +
+ " expression: k1 + (k2 + k3) / 100000000.0\n\n" +
+ " }\n" +
+ " }\n" +
+ "\n" +
+ "}\n");
+ builder.build(true);
+ Schema s = builder.getSchema();
+ RankProfile profile = rankProfileRegistry.get(s, "test");
+ assertEquals("k1 + (k2 + k3) / 1.0E8",
+ profile.compile(new QueryProfileRegistry(), new ImportedMlModels()).getFunctions().get("rank_default").function().getBody().getRoot().toString());
+ }
+
+ @Test
+ public void test3() throws ParseException {
+ RankProfileRegistry rankProfileRegistry = new RankProfileRegistry();
+ ApplicationBuilder builder = new ApplicationBuilder(rankProfileRegistry);
+ builder.addSchema(
+ "search test {\n" +
+ " document test { \n" +
+ " field rating_yelp type int {" +
+ " indexing: attribute" +
+ " }" +
+ " }\n" +
+ " \n" +
+ " rank-profile test {\n" +
+ " function rank_default(){\n" +
+ " expression: 0.5+50*(attribute(rating_yelp)-3)\n\n" +
+ " }\n" +
+ " }\n" +
+ "\n" +
+ "}\n");
+ builder.build(true);
+ Schema s = builder.getSchema();
+ RankProfile profile = rankProfileRegistry.get(s, "test");
+ assertEquals("0.5 + 50 * (attribute(rating_yelp) - 3)",
+ profile.compile(new QueryProfileRegistry(), new ImportedMlModels()).getFunctions().get("rank_default").function().getBody().getRoot().toString());
+ }
+
+}
diff --git a/config-model/src/test/java/com/yahoo/schema/RankingExpressionInliningTestCase.java b/config-model/src/test/java/com/yahoo/schema/RankingExpressionInliningTestCase.java
new file mode 100644
index 00000000000..0695e20d780
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/schema/RankingExpressionInliningTestCase.java
@@ -0,0 +1,274 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.schema;
+
+import com.yahoo.collections.Pair;
+import com.yahoo.config.application.api.DeployLogger;
+import com.yahoo.config.model.application.provider.MockFileRegistry;
+import com.yahoo.config.model.deploy.TestProperties;
+import com.yahoo.config.model.test.MockApplicationPackage;
+import com.yahoo.search.query.profile.QueryProfileRegistry;
+import com.yahoo.schema.derived.AttributeFields;
+import com.yahoo.schema.derived.RawRankProfile;
+import com.yahoo.schema.parser.ParseException;
+import ai.vespa.rankingexpression.importer.configmodelview.ImportedMlModels;
+import org.junit.Test;
+
+import java.util.ArrayList;
+import java.util.Optional;
+import java.util.logging.Level;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * @author bratseth
+ */
+public class RankingExpressionInliningTestCase extends AbstractSchemaTestCase {
+
+ @Test
+ public void testFunctionInliningPreserveArithmeticOrdering() throws ParseException {
+ RankProfileRegistry rankProfileRegistry = new RankProfileRegistry();
+ ApplicationBuilder builder = new ApplicationBuilder(rankProfileRegistry);
+ builder.addSchema(
+ "search test {\n" +
+ " document test { \n" +
+ " field a type double { \n" +
+ " indexing: attribute \n" +
+ " }\n" +
+ " field b type double { \n" +
+ " indexing: attribute \n" +
+ " }\n" +
+ " }\n" +
+ " \n" +
+ " rank-profile parent {\n" +
+ " constants {\n" +
+ " p1: 7 \n" +
+ " p2: 0 \n" +
+ " }\n" +
+ " first-phase {\n" +
+ " expression: p1 * add\n" +
+ " }\n" +
+ " function inline add() {\n" +
+ " expression: 3 + attribute(a) + attribute(b) * mul3\n" +
+ " }\n" +
+ " function inline mul3() {\n" +
+ " expression: attribute(a) * 3 + singleif\n" +
+ " }\n" +
+ " function inline singleif() {\n" +
+ " expression: if (p1 < attribute(a), 1, 2) == 0\n" +
+ " }\n" +
+ " }\n" +
+ " rank-profile child inherits parent {\n" +
+ " function inline add() {\n" +
+ " expression: 9 + attribute(a)\n" +
+ " }\n" +
+ " }\n" +
+ "\n" +
+ "}\n");
+ builder.build(true);
+ Schema s = builder.getSchema();
+
+ RankProfile parent = rankProfileRegistry.get(s, "parent").compile(new QueryProfileRegistry(), new ImportedMlModels());
+ assertEquals("7.0 * (3 + attribute(a) + attribute(b) * (attribute(a) * 3 + if (7.0 < attribute(a), 1, 2) == 0))",
+ parent.getFirstPhaseRanking().getRoot().toString());
+ RankProfile child = rankProfileRegistry.get(s, "child").compile(new QueryProfileRegistry(), new ImportedMlModels());
+ assertEquals("7.0 * (9 + attribute(a))",
+ child.getFirstPhaseRanking().getRoot().toString());
+ }
+
+ @Test
+ public void testConstants() throws ParseException {
+ RankProfileRegistry rankProfileRegistry = new RankProfileRegistry();
+ ApplicationBuilder builder = new ApplicationBuilder(rankProfileRegistry);
+ builder.addSchema(
+ "search test {\n" +
+ " document test { \n" +
+ " field a type string { \n" +
+ " indexing: index \n" +
+ " }\n" +
+ " }\n" +
+ " \n" +
+ " rank-profile parent {\n" +
+ " constants {\n" +
+ " p1: 7 \n" +
+ " p2: 0 \n" +
+ " }\n" +
+ " first-phase {\n" +
+ " expression: p1 + foo\n" +
+ " }\n" +
+ " second-phase {\n" +
+ " expression: p2 * foo\n" +
+ " }\n" +
+ " function inline foo() {\n" +
+ " expression: 3 + p1 + p2\n" +
+ " }\n" +
+ " }\n" +
+ " rank-profile child inherits parent {\n" +
+ " first-phase {\n" +
+ " expression: p1 + foo + baz + bar + arg(4.0)\n" +
+ " }\n" +
+ " constants {\n" +
+ " p2: 2.0 \n" +
+ " }\n" +
+ " function bar() {\n" +
+ " expression: p2*p1\n" +
+ " }\n" +
+ " function inline baz() {\n" +
+ " expression: p2+p1+boz\n" +
+ " }\n" +
+ " function inline boz() {\n" +
+ " expression: 3.0\n" +
+ " }\n" +
+ " function inline arg(a1) {\n" +
+ " expression: a1*2\n" +
+ " }\n" +
+ " }\n" +
+ "\n" +
+ "}\n");
+ builder.build(true);
+ Schema s = builder.getSchema();
+
+ RankProfile parent = rankProfileRegistry.get(s, "parent").compile(new QueryProfileRegistry(), new ImportedMlModels());
+ assertEquals("17.0", parent.getFirstPhaseRanking().getRoot().toString());
+ assertEquals("0.0", parent.getSecondPhaseRanking().getRoot().toString());
+ assertEquals("10.0", getRankingExpression("foo", parent, s));
+ assertEquals("17.0", getRankingExpression("firstphase", parent, s));
+ assertEquals("0.0", getRankingExpression("secondphase", parent, s));
+
+ RankProfile child = rankProfileRegistry.get(s, "child").compile(new QueryProfileRegistry(), new ImportedMlModels());
+ assertEquals("31.0 + bar + arg(4.0)", child.getFirstPhaseRanking().getRoot().toString());
+ assertEquals("24.0", child.getSecondPhaseRanking().getRoot().toString());
+ assertEquals("12.0", getRankingExpression("foo", child, s));
+ assertEquals("12.0", getRankingExpression("baz", child, s));
+ assertEquals("3.0", getRankingExpression("boz", child, s));
+ assertEquals("14.0", getRankingExpression("bar", child, s));
+ assertEquals("a1 * 2", getRankingExpression("arg", child, s));
+ assertEquals("31.0 + rankingExpression(bar) + rankingExpression(arg@)", getRankingExpression("firstphase", child, s));
+ assertEquals("24.0", getRankingExpression("secondphase", child, s));
+ }
+
+ @Test
+ public void testNonTopLevelInlining() throws ParseException {
+ RankProfileRegistry rankProfileRegistry = new RankProfileRegistry();
+ ApplicationBuilder builder = new ApplicationBuilder(rankProfileRegistry);
+ builder.addSchema(
+ "search test {\n" +
+ " document test { \n" +
+ " field a type double { \n" +
+ " indexing: attribute \n" +
+ " }\n" +
+ " field b type double { \n" +
+ " indexing: attribute \n" +
+ " }\n" +
+ " }\n" +
+ " \n" +
+ " rank-profile test {\n" +
+ " first-phase {\n" +
+ " expression: A + C + D\n" +
+ " }\n" +
+ " function inline D() {\n" +
+ " expression: B + 1\n" +
+ " }\n" +
+ " function C() {\n" +
+ " expression: A + B\n" +
+ " }\n" +
+ " function inline B() {\n" +
+ " expression: attribute(b)\n" +
+ " }\n" +
+ " function inline A() {\n" +
+ " expression: attribute(a)\n" +
+ " }\n" +
+ " }\n" +
+ "\n" +
+ "}\n");
+ builder.build(true);
+ Schema s = builder.getSchema();
+
+ RankProfile test = rankProfileRegistry.get(s, "test").compile(new QueryProfileRegistry(), new ImportedMlModels());
+ assertEquals("attribute(a) + C + (attribute(b) + 1)", test.getFirstPhaseRanking().getRoot().toString());
+ assertEquals("attribute(a) + attribute(b)", getRankingExpression("C", test, s));
+ assertEquals("attribute(b) + 1", getRankingExpression("D", test, s));
+ }
+
+ @Test
+ public void testFunctionInliningWithReplacement() throws ParseException {
+ RankProfileRegistry rankProfileRegistry = new RankProfileRegistry();
+ MockDeployLogger deployLogger = new MockDeployLogger();
+ ApplicationBuilder builder = new ApplicationBuilder(MockApplicationPackage.createEmpty(),
+ new MockFileRegistry(),
+ deployLogger,
+ new TestProperties(),
+ rankProfileRegistry,
+ new QueryProfileRegistry());
+ builder.addSchema(
+ "search test {\n" +
+ " document test { }\n" +
+ " rank-profile test {\n" +
+ " first-phase {\n" +
+ " expression: foo\n" +
+ " }\n" +
+ " function foo(x) {\n" +
+ " expression: x + x\n" +
+ " }\n" +
+ " function inline foo() {\n" + // replaces previous "foo" during parsing
+ " expression: foo(2)\n" +
+ " }\n" +
+ " }\n" +
+ "}\n");
+ builder.build(true);
+ Schema s = builder.getSchema();
+ RankProfile test = rankProfileRegistry.get(s, "test").compile(new QueryProfileRegistry(), new ImportedMlModels());
+ assertEquals("foo(2)", test.getFirstPhaseRanking().getRoot().toString());
+ assertTrue("Does not contain expected warning",
+ deployLogger.contains("Function 'foo' is defined twice in rank profile 'test'"));
+ }
+
+ /**
+ * Expression evaluation has no stack so function arguments are bound at config time creating a separate version of
+ * each function for each binding, using hashes to name the bound variants of the function.
+ * This method censors those hashes for string comparison.
+ */
+ private String censorBindingHash(String s) {
+ StringBuilder b = new StringBuilder();
+ boolean areInHash = false;
+ for (int i = 0; i < s.length() ; i++) {
+ char current = s.charAt(i);
+
+ if ( ! Character.isLetterOrDigit(current)) // end of hash
+ areInHash = false;
+
+ if ( ! areInHash)
+ b.append(current);
+
+ if (current == '@') // start of hash
+ areInHash = true;
+ }
+ return b.toString();
+ }
+
+ private String getRankingExpression(String name, RankProfile rankProfile, Schema schema) {
+ Optional<String> rankExpression =
+ new RawRankProfile(rankProfile, new LargeRankExpressions(new MockFileRegistry()), new QueryProfileRegistry(), new ImportedMlModels(), new AttributeFields(schema), new TestProperties())
+ .configProperties()
+ .stream()
+ .filter(r -> r.getFirst().equals("rankingExpression(" + name + ").rankingScript"))
+ .map(Pair::getSecond)
+ .findFirst();
+ assertTrue(rankExpression.isPresent());
+ return censorBindingHash(rankExpression.get());
+ }
+
+ private static class MockDeployLogger implements DeployLogger {
+ private final ArrayList<String> msgs = new ArrayList<>();
+
+ @Override
+ public void log(Level level, String message) {
+ msgs.add(message);
+ }
+
+ public boolean contains(String expected) {
+ return msgs.stream().anyMatch(msg -> msg.equals(expected));
+ }
+ }
+
+}
diff --git a/config-model/src/test/java/com/yahoo/schema/RankingExpressionLoopDetectionTestCase.java b/config-model/src/test/java/com/yahoo/schema/RankingExpressionLoopDetectionTestCase.java
new file mode 100644
index 00000000000..dd69fb6c591
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/schema/RankingExpressionLoopDetectionTestCase.java
@@ -0,0 +1,246 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.schema;
+
+import com.yahoo.schema.parser.ParseException;
+import com.yahoo.yolean.Exceptions;
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
+
+/**
+ * @author bratseth
+ */
+public class RankingExpressionLoopDetectionTestCase {
+
+ @Test
+ public void testSelfLoop() throws ParseException {
+ RankProfileRegistry rankProfileRegistry = new RankProfileRegistry();
+ ApplicationBuilder builder = new ApplicationBuilder(rankProfileRegistry);
+ builder.addSchema(
+ "search test {\n" +
+ " document test { \n" +
+ " field a type string { \n" +
+ " indexing: index \n" +
+ " }\n" +
+ " }\n" +
+ " \n" +
+ " rank-profile test {\n" +
+ " first-phase {\n" +
+ " expression: foo\n" +
+ " }\n" +
+ " function foo() {\n" +
+ " expression: foo\n" +
+ " }\n" +
+ " }\n" +
+ "\n" +
+ "}\n");
+ try {
+ builder.build(true);
+ fail("Excepted exception");
+ }
+ catch (IllegalArgumentException e) {
+ assertEquals("In schema 'test', rank profile 'test': The function 'foo' is invalid: foo is invalid: Invocation loop: foo -> foo",
+ Exceptions.toMessageString(e));
+ }
+ }
+
+ @Test
+ public void testNestedLoop() throws ParseException {
+ RankProfileRegistry rankProfileRegistry = new RankProfileRegistry();
+ ApplicationBuilder builder = new ApplicationBuilder(rankProfileRegistry);
+ builder.addSchema(
+ "search test {\n" +
+ " document test { \n" +
+ " field a type string { \n" +
+ " indexing: index \n" +
+ " }\n" +
+ " }\n" +
+ " \n" +
+ " rank-profile test {\n" +
+ " first-phase {\n" +
+ " expression: foo\n" +
+ " }\n" +
+ " function foo() {\n" +
+ " expression: arg(5)\n" +
+ " }\n" +
+ " function arg(a1) {\n" +
+ " expression: foo + a1*2\n" +
+ " }\n" +
+ " }\n" +
+ "\n" +
+ "}\n");
+ try {
+ builder.build(true);
+ fail("Excepted exception");
+ }
+ catch (IllegalArgumentException e) {
+ assertEquals("In schema 'test', rank profile 'test': The function 'foo' is invalid: arg(5) is invalid: foo is invalid: arg(5) is invalid: Invocation loop: arg(5) -> foo -> arg(5)",
+ Exceptions.toMessageString(e));
+ }
+ }
+
+ @Test
+ public void testSelfArgumentLoop() throws ParseException {
+ RankProfileRegistry rankProfileRegistry = new RankProfileRegistry();
+ ApplicationBuilder builder = new ApplicationBuilder(rankProfileRegistry);
+ builder.addSchema(
+ "search test {\n" +
+ " document test { \n" +
+ " field a type string { \n" +
+ " indexing: index \n" +
+ " }\n" +
+ " }\n" +
+ " \n" +
+ " rank-profile test {\n" +
+ " first-phase {\n" +
+ " expression: foo\n" +
+ " }\n" +
+ " function foo() {\n" +
+ " expression: arg(foo)\n" +
+ " }\n" +
+ " function arg(a1) {\n" +
+ " expression: a1*2\n" +
+ " }\n" +
+ " }\n" +
+ "\n" +
+ "}\n");
+ try {
+ builder.build(true);
+ fail("Excepted exception");
+ }
+ catch (IllegalArgumentException e) {
+ assertEquals("In schema 'test', rank profile 'test': The function 'foo' is invalid: arg(foo) is invalid: a1 is invalid: foo is invalid: arg(foo) is invalid: Invocation loop: arg(foo) -> foo -> arg(foo)",
+ Exceptions.toMessageString(e));
+ }
+ }
+
+ @Test
+ public void testNoLoopWithSameLocalArgument() throws ParseException {
+ RankProfileRegistry rankProfileRegistry = new RankProfileRegistry();
+ ApplicationBuilder builder = new ApplicationBuilder(rankProfileRegistry);
+ builder.addSchema(
+ "search test {\n" +
+ " document test { \n" +
+ " field a type string { \n" +
+ " indexing: index \n" +
+ " }\n" +
+ " }\n" +
+ " \n" +
+ " rank-profile test {\n" +
+ " first-phase {\n" +
+ " expression: foo(3)\n" +
+ " }\n" +
+ " function foo(a1) {\n" +
+ " expression: bar(3)\n" +
+ " }\n" +
+ " function bar(a1) {\n" +
+ " expression: a1*2\n" +
+ " }\n" +
+ " }\n" +
+ "\n" +
+ "}\n");
+ builder.build(true);
+ }
+
+ @Test
+ public void testNoLoopWithMultipleInvocations() throws ParseException {
+ RankProfileRegistry rankProfileRegistry = new RankProfileRegistry();
+ ApplicationBuilder builder = new ApplicationBuilder(rankProfileRegistry);
+ builder.addSchema(
+ "search test {\n" +
+ " document test { \n" +
+ " field a type string { \n" +
+ " indexing: index \n" +
+ " }\n" +
+ " }\n" +
+ " \n" +
+ " rank-profile test {\n" +
+ " first-phase {\n" +
+ " expression: foo(3)\n" +
+ " }\n" +
+ " function foo(a1) {\n" +
+ " expression: bar(3) + bar(a1)\n" +
+ " }\n" +
+ " function bar(a1) {\n" +
+ " expression: a1*2\n" +
+ " }\n" +
+ " }\n" +
+ "\n" +
+ "}\n");
+ builder.build(true);
+ }
+
+ @Test
+ public void testNoLoopWithBoundIdentifiers() throws ParseException {
+ RankProfileRegistry rankProfileRegistry = new RankProfileRegistry();
+ ApplicationBuilder builder = new ApplicationBuilder(rankProfileRegistry);
+ builder.addSchema(
+ "search test {\n" +
+ " document test { \n" +
+ " }\n" +
+ " rank-profile test {\n" +
+ " first-phase {\n" +
+ " expression: foo(bar(2))\n" +
+ " }\n" +
+ " function foo(x) {\n" +
+ " expression: x * x\n" +
+ " }\n" +
+ " function bar(x) {\n" +
+ " expression: x + x\n" +
+ " }\n" +
+ " }\n" +
+ "}\n");
+ builder.build(true);
+ }
+
+ @Test
+ public void testNoLoopWithTheSameNestedIdentifierWhichIsUnbound() throws ParseException {
+ RankProfileRegistry rankProfileRegistry = new RankProfileRegistry();
+ ApplicationBuilder builder = new ApplicationBuilder(rankProfileRegistry);
+ builder.addSchema(
+ "search test {\n" +
+ " document test { \n" +
+ " }\n" +
+ " rank-profile test {\n" +
+ " first-phase {\n" +
+ " expression: foo()\n" +
+ " }\n" +
+ " function foo() {\n" +
+ " expression: bar(x)\n" +
+ " }\n" +
+ " function bar(x) {\n" +
+ " expression: x + x\n" +
+ " }\n" +
+ " }\n" +
+ "}\n");
+ builder.build(true);
+ }
+
+ @Test
+ public void testNoLoopWithTheSameAlternatingNestedIdentifierWhichIsUnbound() throws ParseException {
+ RankProfileRegistry rankProfileRegistry = new RankProfileRegistry();
+ ApplicationBuilder builder = new ApplicationBuilder(rankProfileRegistry);
+ builder.addSchema(
+ "search test {\n" +
+ " document test { \n" +
+ " }\n" +
+ " rank-profile test {\n" +
+ " first-phase {\n" +
+ " expression: foo()\n" +
+ " }\n" +
+ " function foo() {\n" +
+ " expression: bar(x)\n" +
+ " }\n" +
+ " function bar(y) {\n" +
+ " expression: baz(y)\n" +
+ " }\n" +
+ " function baz(x) {\n" +
+ " expression: x + x\n" +
+ " }\n" +
+ " }\n" +
+ "}\n");
+ builder.build(true);
+ }
+
+}
diff --git a/config-model/src/test/java/com/yahoo/schema/RankingExpressionShadowingTestCase.java b/config-model/src/test/java/com/yahoo/schema/RankingExpressionShadowingTestCase.java
new file mode 100644
index 00000000000..250879b1570
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/schema/RankingExpressionShadowingTestCase.java
@@ -0,0 +1,251 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.schema;
+
+import com.yahoo.collections.Pair;
+import com.yahoo.config.model.application.provider.MockFileRegistry;
+import com.yahoo.config.model.deploy.TestProperties;
+import com.yahoo.search.query.profile.QueryProfile;
+import com.yahoo.search.query.profile.QueryProfileRegistry;
+import com.yahoo.search.query.profile.types.FieldDescription;
+import com.yahoo.search.query.profile.types.QueryProfileType;
+import com.yahoo.schema.derived.AttributeFields;
+import com.yahoo.schema.derived.RawRankProfile;
+import com.yahoo.schema.parser.ParseException;
+import ai.vespa.rankingexpression.importer.configmodelview.ImportedMlModels;
+import org.junit.Test;
+
+import java.util.List;
+
+import static org.junit.Assert.assertEquals;
+
+/**
+ * @author lesters
+ */
+public class RankingExpressionShadowingTestCase extends AbstractSchemaTestCase {
+
+ @Test
+ public void testBasicFunctionShadowing() throws ParseException {
+ RankProfileRegistry rankProfileRegistry = new RankProfileRegistry();
+ ApplicationBuilder builder = new ApplicationBuilder(rankProfileRegistry);
+ builder.addSchema(
+ "search test {\n" +
+ " document test { \n" +
+ " field a type string { \n" +
+ " indexing: index \n" +
+ " }\n" +
+ " }\n" +
+ " \n" +
+ " rank-profile test {\n" +
+ " function sin(x) {\n" +
+ " expression: x * x\n" +
+ " }\n" +
+ " first-phase {\n" +
+ " expression: sin(2)\n" +
+ " }\n" +
+ " }\n" +
+ "\n" +
+ "}\n");
+ builder.build(true);
+ Schema s = builder.getSchema();
+ RankProfile test = rankProfileRegistry.get(s, "test").compile(new QueryProfileRegistry(), new ImportedMlModels());
+ List<Pair<String, String>> testRankProperties = createRawRankProfile(test, new QueryProfileRegistry(), s).configProperties();
+ assertEquals("(rankingExpression(sin@).rankingScript, 2 * 2)",
+ censorBindingHash(testRankProperties.get(0).toString()));
+ assertEquals("(rankingExpression(sin).rankingScript, x * x)",
+ testRankProperties.get(1).toString());
+ assertEquals("(vespa.rank.firstphase, rankingExpression(sin@))",
+ censorBindingHash(testRankProperties.get(2).toString()));
+ }
+
+
+ @Test
+ public void testMultiLevelFunctionShadowing() throws ParseException {
+ RankProfileRegistry rankProfileRegistry = new RankProfileRegistry();
+ ApplicationBuilder builder = new ApplicationBuilder(rankProfileRegistry);
+ builder.addSchema(
+ "search test {\n" +
+ " document test { \n" +
+ " field a type string { \n" +
+ " indexing: index \n" +
+ " }\n" +
+ " }\n" +
+ " \n" +
+ " rank-profile test {\n" +
+ " function tan(x) {\n" +
+ " expression: x * x\n" +
+ " }\n" +
+ " function cos(x) {\n" +
+ " expression: tan(x)\n" +
+ " }\n" +
+ " function sin(x) {\n" +
+ " expression: cos(x)\n" +
+ " }\n" +
+ " first-phase {\n" +
+ " expression: sin(2)\n" +
+ " }\n" +
+ " }\n" +
+ "\n" +
+ "}\n");
+ builder.build(true);
+ Schema s = builder.getSchema();
+ RankProfile test = rankProfileRegistry.get(s, "test").compile(new QueryProfileRegistry(), new ImportedMlModels());
+ List<Pair<String, String>> testRankProperties = createRawRankProfile(test, new QueryProfileRegistry(), s).configProperties();
+ assertEquals("(rankingExpression(tan@).rankingScript, 2 * 2)",
+ censorBindingHash(testRankProperties.get(0).toString()));
+ assertEquals("(rankingExpression(cos@).rankingScript, rankingExpression(tan@))",
+ censorBindingHash(testRankProperties.get(1).toString()));
+ assertEquals("(rankingExpression(sin@).rankingScript, rankingExpression(cos@))",
+ censorBindingHash(testRankProperties.get(2).toString()));
+ assertEquals("(rankingExpression(tan).rankingScript, x * x)",
+ testRankProperties.get(3).toString());
+ assertEquals("(rankingExpression(tan@).rankingScript, x * x)",
+ censorBindingHash(testRankProperties.get(4).toString()));
+ assertEquals("(rankingExpression(cos).rankingScript, rankingExpression(tan@))",
+ censorBindingHash(testRankProperties.get(5).toString()));
+ assertEquals("(rankingExpression(cos@).rankingScript, rankingExpression(tan@))",
+ censorBindingHash(testRankProperties.get(6).toString()));
+ assertEquals("(rankingExpression(sin).rankingScript, rankingExpression(cos@))",
+ censorBindingHash(testRankProperties.get(7).toString()));
+ assertEquals("(vespa.rank.firstphase, rankingExpression(sin@))",
+ censorBindingHash(testRankProperties.get(8).toString()));
+ }
+
+ @Test
+ public void testFunctionShadowingArguments() throws ParseException {
+ RankProfileRegistry rankProfileRegistry = new RankProfileRegistry();
+ ApplicationBuilder builder = new ApplicationBuilder(rankProfileRegistry);
+ builder.addSchema(
+ "search test {\n" +
+ " document test { \n" +
+ " field a type string { \n" +
+ " indexing: index \n" +
+ " }\n" +
+ " }\n" +
+ " \n" +
+ " rank-profile test {\n" +
+ " function sin(x) {\n" +
+ " expression: x * x\n" +
+ " }\n" +
+ " first-phase {\n" +
+ " expression: cos(sin(2*2)) + sin(cos(1+4))\n" +
+ " }\n" +
+ " }\n" +
+ "\n" +
+ "}\n");
+ builder.build(true);
+ Schema s = builder.getSchema();
+ RankProfile test = rankProfileRegistry.get(s, "test").compile(new QueryProfileRegistry(), new ImportedMlModels());
+ List<Pair<String, String>> testRankProperties = createRawRankProfile(test, new QueryProfileRegistry(), s).configProperties();
+ assertEquals("(rankingExpression(sin@).rankingScript, 4.0 * 4.0)",
+ censorBindingHash(testRankProperties.get(0).toString()));
+ assertEquals("(rankingExpression(sin@).rankingScript, cos(5.0) * cos(5.0))",
+ censorBindingHash(testRankProperties.get(1).toString()));
+ assertEquals("(rankingExpression(sin).rankingScript, x * x)",
+ testRankProperties.get(2).toString());
+ assertEquals("(vespa.rank.firstphase, rankingExpression(firstphase))",
+ censorBindingHash(testRankProperties.get(3).toString()));
+ assertEquals("(rankingExpression(firstphase).rankingScript, cos(rankingExpression(sin@)) + rankingExpression(sin@))",
+ censorBindingHash(testRankProperties.get(4).toString()));
+ }
+
+ @Test
+ public void testNeuralNetworkSetup() throws ParseException {
+ // Note: the type assigned to query profile and constant tensors here is not the correct type
+ RankProfileRegistry rankProfileRegistry = new RankProfileRegistry();
+ QueryProfileRegistry queryProfiles = queryProfileWith("query(q)", "tensor(input[1])");
+ ApplicationBuilder builder = new ApplicationBuilder(rankProfileRegistry, queryProfiles);
+ builder.addSchema(
+ "search test {\n" +
+ " document test { \n" +
+ " field a type string { \n" +
+ " indexing: index \n" +
+ " }\n" +
+ " }\n" +
+ " \n" +
+ " rank-profile test {\n" +
+ " function relu(x) {\n" + // relu is a built in function, redefined here
+ " expression: max(1.0, x)\n" +
+ " }\n" +
+ " function hidden_layer() {\n" +
+ " expression: relu(sum(query(q) * constant(W_hidden), input) + constant(b_input))\n" +
+ " }\n" +
+ " function final_layer() {\n" +
+ " expression: sigmoid(sum(hidden_layer * constant(W_final), hidden) + constant(b_final))\n" +
+ " }\n" +
+ " second-phase {\n" +
+ " expression: sum(final_layer)\n" +
+ " }\n" +
+ " }\n" +
+ " constant W_hidden {\n" +
+ " type: tensor(hidden[1])\n" +
+ " file: ignored.json\n" +
+ " }\n" +
+ " constant b_input {\n" +
+ " type: tensor(hidden[1])\n" +
+ " file: ignored.json\n" +
+ " }\n" +
+ " constant W_final {\n" +
+ " type: tensor(final[1])\n" +
+ " file: ignored.json\n" +
+ " }\n" +
+ " constant b_final {\n" +
+ " type: tensor(final[1])\n" +
+ " file: ignored.json\n" +
+ " }\n" +
+ "}\n");
+ builder.build(true);
+ Schema s = builder.getSchema();
+ RankProfile test = rankProfileRegistry.get(s, "test").compile(queryProfiles, new ImportedMlModels());
+ List<Pair<String, String>> testRankProperties = createRawRankProfile(test, queryProfiles, s).configProperties();
+ assertEquals("(rankingExpression(autogenerated_ranking_feature@).rankingScript, reduce(query(q) * constant(W_hidden), sum, input) + constant(b_input))",
+ censorBindingHash(testRankProperties.get(0).toString()));
+ assertEquals("(rankingExpression(relu@).rankingScript, max(1.0,rankingExpression(autogenerated_ranking_feature@)))",
+ censorBindingHash(testRankProperties.get(1).toString()));
+ assertEquals("(rankingExpression(hidden_layer).rankingScript, rankingExpression(relu@))",
+ censorBindingHash(testRankProperties.get(2).toString()));
+ assertEquals("(rankingExpression(final_layer).rankingScript, sigmoid(reduce(rankingExpression(hidden_layer) * constant(W_final), sum, hidden) + constant(b_final)))",
+ testRankProperties.get(4).toString());
+ assertEquals("(rankingExpression(relu).rankingScript, max(1.0,x))",
+ testRankProperties.get(6).toString());
+ assertEquals("(vespa.rank.secondphase, rankingExpression(secondphase))",
+ testRankProperties.get(7).toString());
+ assertEquals("(rankingExpression(secondphase).rankingScript, reduce(rankingExpression(final_layer), sum))",
+ testRankProperties.get(8).toString());
+ }
+
+ private static RawRankProfile createRawRankProfile(RankProfile profile, QueryProfileRegistry queryProfiles, Schema schema) {
+ return new RawRankProfile(profile,
+ new LargeRankExpressions(new MockFileRegistry()),
+ queryProfiles,
+ new ImportedMlModels(),
+ new AttributeFields(schema),
+ new TestProperties());
+ }
+
+ private QueryProfileRegistry queryProfileWith(String field, String type) {
+ QueryProfileType queryProfileType = new QueryProfileType("root");
+ queryProfileType.addField(new FieldDescription(field, type));
+ QueryProfileRegistry queryProfileRegistry = new QueryProfileRegistry();
+ queryProfileRegistry.getTypeRegistry().register(queryProfileType);
+ QueryProfile profile = new QueryProfile("default");
+ profile.setType(queryProfileType);
+ queryProfileRegistry.register(profile);
+ return queryProfileRegistry;
+ }
+
+ private String censorBindingHash(String s) {
+ StringBuilder b = new StringBuilder();
+ boolean areInHash = false;
+ for (int i = 0; i < s.length() ; i++) {
+ char current = s.charAt(i);
+ if ( ! Character.isLetterOrDigit(current)) // end of hash
+ areInHash = false;
+ if ( ! areInHash)
+ b.append(current);
+ if (current == '@') // start of hash
+ areInHash = true;
+ }
+ return b.toString();
+ }
+
+}
diff --git a/config-model/src/test/java/com/yahoo/schema/RankingExpressionValidationTestCase.java b/config-model/src/test/java/com/yahoo/schema/RankingExpressionValidationTestCase.java
new file mode 100644
index 00000000000..e42acee9bed
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/schema/RankingExpressionValidationTestCase.java
@@ -0,0 +1,52 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.schema;
+
+import com.yahoo.schema.derived.DerivedConfiguration;
+import com.yahoo.schema.parser.ParseException;
+import com.yahoo.yolean.Exceptions;
+import org.junit.Test;
+
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+/**
+ * @author bratseth
+ */
+public class RankingExpressionValidationTestCase extends AbstractSchemaTestCase {
+
+ @Test
+ public void testInvalidExpressionProducesException() throws ParseException {
+ assertFailsExpression("&/%(/%&");
+ assertFailsExpression("if(a==b,b)");
+ }
+
+ private void assertFailsExpression(String expression) throws ParseException {
+ try {
+ RankProfileRegistry registry = new RankProfileRegistry();
+ Schema schema = importWithExpression(expression, registry);
+ new DerivedConfiguration(schema, registry); // cause rank profile parsing
+ fail("No exception on incorrect ranking expression " + expression);
+ } catch (IllegalArgumentException e) {
+ // Success
+ assertTrue(Exceptions.toMessageString(e).startsWith("Illegal first phase ranking function: Could not parse ranking expression '" + expression + "' in default, firstphase.:"));
+ }
+ }
+
+ private Schema importWithExpression(String expression, RankProfileRegistry registry) throws ParseException {
+ ApplicationBuilder builder = new ApplicationBuilder(registry);
+ builder.addSchema("search test {" +
+ " document test { " +
+ " field a type string { " +
+ " indexing: index " +
+ " }" +
+ " }" +
+ " rank-profile default {" +
+ " first-phase {" +
+ " expression: " + expression +
+ " }" +
+ " }" +
+ "}");
+ builder.build(true);
+ return builder.getSchema();
+ }
+
+}
diff --git a/config-model/src/test/java/com/yahoo/schema/ReservedWordsAsFieldNamesTestCase.java b/config-model/src/test/java/com/yahoo/schema/ReservedWordsAsFieldNamesTestCase.java
new file mode 100644
index 00000000000..df9d4a63650
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/schema/ReservedWordsAsFieldNamesTestCase.java
@@ -0,0 +1,24 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.schema;
+
+import com.yahoo.schema.parser.ParseException;
+import org.junit.Test;
+
+import java.io.IOException;
+
+import static org.junit.Assert.assertNotNull;
+
+/**
+ * @author bratseth
+ */
+public class ReservedWordsAsFieldNamesTestCase extends AbstractSchemaTestCase {
+
+ @Test
+ public void testIt() throws IOException, ParseException {
+ Schema schema = ApplicationBuilder.buildFromFile("src/test/examples/reserved_words_as_field_names.sd");
+ assertNotNull(schema.getDocument().getField("inline"));
+ assertNotNull(schema.getDocument().getField("constants"));
+ assertNotNull(schema.getDocument().getField("reference"));
+ }
+
+}
diff --git a/config-model/src/test/java/com/yahoo/schema/SDDocumentTypeOrdererTestCase.java b/config-model/src/test/java/com/yahoo/schema/SDDocumentTypeOrdererTestCase.java
new file mode 100755
index 00000000000..45780d39021
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/schema/SDDocumentTypeOrdererTestCase.java
@@ -0,0 +1,78 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.schema;
+
+import com.yahoo.config.model.application.provider.BaseDeployLogger;
+import com.yahoo.document.DataType;
+import com.yahoo.document.DataTypeName;
+import com.yahoo.schema.document.SDDocumentType;
+import com.yahoo.schema.document.SDField;
+import com.yahoo.schema.document.TemporarySDDocumentType;
+import com.yahoo.schema.document.TemporarySDField;
+import org.junit.Test;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import static org.junit.Assert.assertEquals;
+
+/**
+ * @author Einar M R Rosenvinge
+ */
+public class SDDocumentTypeOrdererTestCase {
+
+ @Test
+ public void testOrder() {
+ List<SDDocumentType> types = new ArrayList<>();
+
+ SDDocumentType a = new SDDocumentType("a");
+ SDDocumentType b = new SDDocumentType("b");
+ SDDocumentType c = new SDDocumentType("c");
+ SDDocumentType d = new SDDocumentType("d");
+ SDDocumentType e = new SDDocumentType("e");
+ SDDocumentType f = new SDDocumentType("f");
+ SDDocumentType g = new SDDocumentType("g");
+ b.inherit(new TemporarySDDocumentType(new DataTypeName("a")));
+ c.inherit(new TemporarySDDocumentType(new DataTypeName("b")));
+ d.inherit(new TemporarySDDocumentType(new DataTypeName("e")));
+ g.inherit(new TemporarySDDocumentType(new DataTypeName("e")));
+ g.inherit(new TemporarySDDocumentType(new DataTypeName("c")));
+
+ SDField aFieldTypeB = new TemporarySDField(a, "atypeb", DataType.STRING);
+ a.addField(aFieldTypeB);
+
+ SDField bFieldTypeC = new TemporarySDField(b, "btypec", DataType.STRING);
+ b.addField(bFieldTypeC);
+
+ SDField cFieldTypeG = new TemporarySDField(c, "ctypeg", DataType.STRING);
+ c.addField(cFieldTypeG);
+
+ SDField gFieldTypeF = new TemporarySDField(g, "gtypef", DataType.STRING);
+ g.addField(gFieldTypeF);
+
+ SDField fFieldTypeC = new TemporarySDField(f, "ftypec", DataType.STRING);
+ f.addField(fFieldTypeC);
+
+ SDField dFieldTypeE = new TemporarySDField(d, "dtypee", DataType.STRING);
+ d.addField(dFieldTypeE);
+
+ types.add(a);
+ types.add(b);
+ types.add(c);
+ types.add(d);
+ types.add(e);
+ types.add(f);
+ types.add(g);
+
+ SDDocumentTypeOrderer app = new SDDocumentTypeOrderer(types, new BaseDeployLogger());
+ app.process();
+ assertEquals(7, app.processingOrder.size());
+ assertEquals(a, app.processingOrder.get(0));
+ assertEquals(b, app.processingOrder.get(1));
+ assertEquals(c, app.processingOrder.get(2));
+ assertEquals(e, app.processingOrder.get(3));
+ assertEquals(d, app.processingOrder.get(4));
+ assertEquals(f, app.processingOrder.get(5));
+ assertEquals(g, app.processingOrder.get(6));
+ }
+
+}
diff --git a/config-model/src/test/java/com/yahoo/schema/SchemaImporterTestCase.java b/config-model/src/test/java/com/yahoo/schema/SchemaImporterTestCase.java
new file mode 100644
index 00000000000..e93dd0e0a8f
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/schema/SchemaImporterTestCase.java
@@ -0,0 +1,189 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.schema;
+
+import com.yahoo.config.model.application.provider.BaseDeployLogger;
+import com.yahoo.document.DataType;
+import com.yahoo.search.query.profile.QueryProfileRegistry;
+import com.yahoo.schema.document.Attribute;
+import com.yahoo.schema.document.RankType;
+import com.yahoo.schema.document.SDDocumentType;
+import com.yahoo.schema.document.SDField;
+import com.yahoo.schema.document.Stemming;
+import com.yahoo.schema.parser.ParseException;
+import com.yahoo.schema.processing.MakeAliases;
+import com.yahoo.vespa.documentmodel.SummaryTransform;
+import com.yahoo.vespa.model.container.search.QueryProfiles;
+import org.junit.Test;
+
+import java.io.IOException;
+import java.util.Iterator;
+
+import static com.google.common.collect.testing.Helpers.assertEmpty;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+/**
+ * Tests importing of search definitions
+ *
+ * @author bratseth
+ */
+public class SchemaImporterTestCase extends AbstractSchemaTestCase {
+
+ @Test
+ @SuppressWarnings("deprecation")
+ public void testSimpleImporting() throws IOException, ParseException {
+ RankProfileRegistry rankProfileRegistry = new RankProfileRegistry();
+ ApplicationBuilder sb = new ApplicationBuilder(rankProfileRegistry, new QueryProfileRegistry());
+ sb.addSchemaFile("src/test/examples/simple.sd");
+ sb.build(true);
+ Schema schema = sb.getSchema();
+ assertEquals("simple", schema.getName());
+ assertTrue(schema.hasDocument());
+
+ SDDocumentType document = schema.getDocument();
+ assertEquals("simple", document.getName());
+ assertEquals(23, document.getFieldCount());
+
+ SDField field;
+ Attribute attribute;
+
+ new MakeAliases(schema, new BaseDeployLogger(), rankProfileRegistry, new QueryProfiles()).process(true, false);
+
+ // First field
+ field = (SDField) document.getField("title");
+ assertEquals(DataType.STRING, field.getDataType());
+ assertEquals("{ input title | tokenize normalize stem:\"BEST\" | summary title | index title; }", field.getIndexingScript().toString());
+ assertFalse(schema.getIndex("default").isPrefix());
+ assertTrue(schema.getIndex("title").isPrefix());
+ Iterator<String> titleAliases = schema.getIndex("title").aliasIterator();
+ assertEquals("aliaz", titleAliases.next());
+ assertEquals("analias.totitle", titleAliases.next());
+ assertEquals("analias.todefault",
+ schema.getIndex("default").aliasIterator().next());
+ assertEquals(RankType.IDENTITY, field.getRankType());
+ assertEquals(0, field.getAttributes().size());
+ assertNull(field.getStemming());
+ assertTrue(field.getNormalizing().doRemoveAccents());
+ assertTrue(field.isHeader());
+
+ // Second field
+ field = (SDField) document.getField("description");
+ assertEquals(RankType.ABOUT, field.getRankType());
+ assertEquals(SummaryTransform.NONE,
+ field.getSummaryField("description").getTransform());
+ assertEquals(SummaryTransform.DYNAMICTEASER,
+ field.getSummaryField("dyndesc").getTransform());
+ assertNull(field.getStemming());
+ assertTrue(field.getNormalizing().doRemoveAccents());
+ assertEquals("hallo", schema.getIndex("description").aliasIterator().next());
+
+ // Third field
+ field = (SDField) document.getField("chatter");
+ assertEquals(RankType.ABOUT, field.getRankType());
+ assertNull(field.getStemming());
+ assertTrue(field.getNormalizing().doRemoveAccents());
+
+ // Fourth field
+ field = (SDField) document.getField("category");
+ assertEquals(0, field.getAttributes().size());
+ assertEquals(Stemming.NONE, field.getStemming());
+ assertFalse(field.getNormalizing().doRemoveAccents());
+
+ // Fifth field
+ field = (SDField) document.getField("popularity");
+ assertEquals("{ input popularity | attribute popularity; }",
+ field.getIndexingScript().toString());
+
+ // Sixth field
+ field = (SDField) document.getField("measurement");
+ assertEquals(DataType.INT, field.getDataType());
+ assertEquals(RankType.EMPTY, field.getRankType());
+ assertEquals(1, field.getAttributes().size());
+
+ // Seventh field
+ field = schema.getConcreteField("categories");
+ assertEquals("{ input categories_src | lowercase | normalize | tokenize normalize stem:\"BEST\" | index categories; }",
+ field.getIndexingScript().toString());
+ assertTrue(field.isHeader());
+
+ // Eight field
+ field= schema.getConcreteField("categoriesagain");
+ assertEquals("{ input categoriesagain_src | lowercase | normalize | tokenize normalize stem:\"BEST\" | index categoriesagain; }",
+ field.getIndexingScript().toString());
+ assertTrue(field.isHeader());
+
+ // Ninth field
+ field= schema.getConcreteField("exactemento");
+ assertEquals("{ input exactemento_src | lowercase | tokenize normalize stem:\"BEST\" | index exactemento | summary exactemento; }",
+ field.getIndexingScript().toString());
+
+ // Tenth field
+ field = schema.getConcreteField("category_arr");
+ assertEquals(1, field.getAttributes().size());
+ attribute = field.getAttributes().get("category_arr");
+ assertNotNull(attribute);
+ assertEquals("category_arr", attribute.getName());
+ assertEquals(Attribute.Type.STRING, attribute.getType());
+ assertEquals(Attribute.CollectionType.ARRAY, attribute.getCollectionType());
+ assertTrue(field.isHeader());
+
+ // Eleventh field
+ field = schema.getConcreteField("measurement_arr");
+ assertEquals(1, field.getAttributes().size());
+ attribute = field.getAttributes().get("measurement_arr");
+ assertEquals("measurement_arr", attribute.getName());
+ assertEquals(Attribute.Type.INTEGER, attribute.getType());
+ assertEquals(Attribute.CollectionType.ARRAY, attribute.getCollectionType());
+
+ // Rank Profiles
+ RankProfile profile = rankProfileRegistry.get(schema, "default");
+ assertNotNull(profile);
+ assertEmpty(profile.inheritedNames());
+ assertNull(profile.getDeclaredRankSetting("measurement", RankProfile.RankSetting.Type.RANKTYPE));
+ assertEquals(RankType.EMPTY,
+ profile.getRankSetting("measurement", RankProfile.RankSetting.Type.RANKTYPE).getValue());
+ profile = rankProfileRegistry.get(schema, "experimental");
+ assertNotNull(profile);
+ assertEquals("default", profile.inheritedNames().get(0));
+ assertEquals(RankType.IDENTITY,
+ profile.getDeclaredRankSetting("measurement", RankProfile.RankSetting.Type.RANKTYPE).getValue());
+
+ profile = rankProfileRegistry.get(schema, "other");
+ assertNotNull(profile);
+ assertEquals("experimental", profile.inheritedNames().get(0));
+
+ // The extra-document field
+ SDField exact = schema.getConcreteField("exact");
+ assertNotNull("Extra field was parsed", exact);
+ assertEquals("exact", exact.getName());
+ assertEquals(Stemming.NONE, exact.getStemming());
+ assertFalse(exact.getNormalizing().doRemoveAccents());
+ assertEquals("{ input title . \" \" . input category | tokenize | summary exact | index exact; }",
+ exact.getIndexingScript().toString());
+ assertEquals(RankType.IDENTITY, exact.getRankType());
+ }
+
+ @Test
+ public void testDocumentImporting() throws IOException, ParseException {
+ try {
+ // Having two documents in one sd-file is illegal.
+ ApplicationBuilder.buildFromFile("src/test/examples/documents.sd");
+ fail();
+ } catch (IllegalArgumentException e) {
+ }
+ }
+
+ @Test
+ public void testIdImporting() throws IOException, ParseException {
+ Schema schema = ApplicationBuilder.buildFromFile("src/test/examples/strange.sd");
+ SDField idecidemyide = (SDField)schema.getDocument().getField("idecidemyide");
+ assertEquals(5, idecidemyide.getId());
+ SDField sodoi = (SDField) schema.getDocument().getField("sodoi");
+ assertEquals(7, sodoi.getId());
+ }
+
+}
diff --git a/config-model/src/test/java/com/yahoo/schema/SchemaParsingTestCase.java b/config-model/src/test/java/com/yahoo/schema/SchemaParsingTestCase.java
new file mode 100644
index 00000000000..8fe691db802
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/schema/SchemaParsingTestCase.java
@@ -0,0 +1,83 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.schema;
+
+import java.io.IOException;
+
+import com.yahoo.schema.parser.ParseException;
+
+import org.junit.Test;
+
+import static org.junit.Assert.*;
+
+/**
+ * Tests that search definitions are parsed correctly and that correct line number is reported in
+ * error message.
+ *
+ * @author hmusum
+ */
+public class SchemaParsingTestCase extends AbstractSchemaTestCase {
+
+ @Test
+ public void requireThatIndexingExpressionsCanBeParsed() throws Exception {
+ assertNotNull(ApplicationBuilder.buildFromFile("src/test/examples/simple.sd"));
+ }
+
+ @Test
+ public void requireThatParseExceptionPositionIsCorrect() throws Exception {
+ try {
+ ApplicationBuilder.buildFromFile("src/test/examples/invalid_sd_construct.sd");
+ } catch (ParseException e) {
+ if ( ! e.getMessage().contains("at line 5, column 36.")) {
+ throw e;
+ }
+ }
+ }
+
+ @Test
+ public void requireThatParserHandlesLexicalError() throws Exception {
+ try {
+ ApplicationBuilder.buildFromFile("src/test/examples/invalid_sd_lexical_error.sd");
+ } catch (ParseException e) {
+ if (!e.getMessage().contains("at line 7, column 27.")) {
+ throw e;
+ }
+ }
+ }
+
+ @Test
+ public void requireErrorWhenJunkAfterSearchBlock() throws IOException, ParseException {
+ try {
+ ApplicationBuilder.buildFromFile("src/test/examples/invalid_sd_junk_at_end.sd");
+ fail("Illegal junk at end of SD passed");
+ } catch (ParseException e) {
+ if (!e.getMessage().contains("at line 10, column 1")) {
+ throw e;
+ }
+ }
+ }
+
+ @Test
+ public void requireErrorWhenMissingClosingSearchBracket() throws IOException, ParseException {
+ try {
+ ApplicationBuilder.buildFromFile("src/test/examples/invalid_sd_no_closing_bracket.sd");
+ fail("SD without closing bracket passed");
+ } catch (ParseException e) {
+ if (!e.getMessage().contains("Encountered \"<EOF>\" at line 8, column 1")) {
+ throw e;
+ }
+ }
+ }
+
+ @Test
+ public void illegalSearchDefinitionName() throws IOException, ParseException {
+ try {
+ ApplicationBuilder.buildFromFile("src/test/examples/invalid-name.sd");
+ fail("Name with dash passed");
+ } catch (ParseException e) {
+ if ( ! e.getMessage().contains("invalid-name")) {
+ throw e;
+ }
+ }
+ }
+
+}
diff --git a/config-model/src/test/java/com/yahoo/schema/SchemaTestCase.java b/config-model/src/test/java/com/yahoo/schema/SchemaTestCase.java
new file mode 100644
index 00000000000..67d8ce4ff78
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/schema/SchemaTestCase.java
@@ -0,0 +1,443 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.schema;
+
+import com.yahoo.schema.document.Stemming;
+import com.yahoo.schema.parser.ParseException;
+import com.yahoo.schema.processing.ImportedFieldsResolver;
+import com.yahoo.schema.processing.OnnxModelTypeResolver;
+import com.yahoo.vespa.documentmodel.DocumentSummary;
+import com.yahoo.vespa.model.test.utils.DeployLoggerStub;
+import org.junit.Test;
+
+import static com.yahoo.config.model.test.TestUtil.joinLines;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+/**
+ * Schema tests that don't depend on files.
+ *
+ * @author bratseth
+ */
+public class SchemaTestCase {
+
+ @Test
+ public void testValidationOfInheritedSchema() throws ParseException {
+ try {
+ String schema = joinLines(
+ "schema test inherits nonesuch {" +
+ " document test inherits nonesuch {" +
+ " }" +
+ "}");
+ DeployLoggerStub logger = new DeployLoggerStub();
+ ApplicationBuilder.createFromStrings(logger, schema);
+ assertEquals("schema 'test' inherits 'nonesuch', but this schema does not exist",
+ logger.entries.get(0).message);
+ fail("Expected exception");
+ }
+ catch (IllegalArgumentException e) {
+ assertEquals("schema 'test' inherits 'nonesuch', but this schema does not exist", e.getMessage());
+ }
+ }
+
+ @Test
+ public void testValidationOfSchemaAndDocumentInheritanceConsistency() throws ParseException {
+ try {
+ String parent = joinLines(
+ "schema parent {" +
+ " document parent {" +
+ " field pf1 type string {" +
+ " indexing: summary" +
+ " }" +
+ " }" +
+ "}");
+ String child = joinLines(
+ "schema child inherits parent {" +
+ " document child {" +
+ " field cf1 type string {" +
+ " indexing: summary" +
+ " }" +
+ " }" +
+ "}");
+ ApplicationBuilder.createFromStrings(new DeployLoggerStub(), parent, child);
+ fail("Expected exception");
+ }
+ catch (IllegalArgumentException e) {
+ assertEquals("schema 'child' inherits 'parent', " +
+ "but its document type does not inherit the parent's document type"
+ , e.getMessage());
+ }
+ }
+
+ @Test
+ public void testSchemaInheritance() throws ParseException {
+ String parentLines = joinLines(
+ "schema parent {" +
+ " document parent {" +
+ " field pf1 type string {" +
+ " indexing: summary" +
+ " }" +
+ " }" +
+ " fieldset parent_set {" +
+ " fields: pf1" +
+ " }" +
+ " stemming: none" +
+ " index parent_index {" +
+ " stemming: best" +
+ " }" +
+ " field parent_field type string {" +
+ " indexing: input pf1 | lowercase | index | attribute | summary" +
+ " }" +
+ " rank-profile parent_profile {" +
+ " }" +
+ " constant parent_constant {" +
+ " file: constants/my_constant_tensor_file.json" +
+ " type: tensor<float>(x{},y{})" +
+ " }" +
+ " onnx-model parent_model {" +
+ " file: models/my_model.onnx" +
+ " }" +
+ " document-summary parent_summary {" +
+ " summary pf1 type string {}" +
+ " }" +
+ " import field parentschema_ref.name as parent_imported {}" +
+ " raw-as-base64-in-summary" +
+ "}");
+ String child1Lines = joinLines(
+ "schema child1 inherits parent {" +
+ " document child1 inherits parent {" +
+ " field c1f1 type string {" +
+ " indexing: summary" +
+ " }" +
+ " }" +
+ " fieldset child1_set {" +
+ " fields: c1f1, pf1" +
+ " }" +
+ " stemming: shortest" +
+ " index child1_index {" +
+ " stemming: shortest" +
+ " }" +
+ " field child1_field type string {" +
+ " indexing: input pf1 | lowercase | index | attribute | summary" +
+ " }" +
+ " rank-profile child1_profile inherits parent_profile {" +
+ " constants {" +
+ " child1_constant tensor<float>(x{},y{}): file:constants/my_constant_tensor_file.json" +
+ " }" +
+ " }" +
+ " onnx-model child1_model {" +
+ " file: models/my_model.onnx" +
+ " }" +
+ " document-summary child1_summary inherits parent_summary {" +
+ " summary c1f1 type string {}" +
+ " }" +
+ " import field parentschema_ref.name as child1_imported {}" +
+ "}");
+ String child2Lines = joinLines(
+ "schema child2 inherits parent {" +
+ " document child2 inherits parent {" +
+ " field c2f1 type string {" +
+ " indexing: summary" +
+ " }" +
+ " }" +
+ " fieldset child2_set {" +
+ " fields: c2f1, pf1" +
+ " }" +
+ " stemming: shortest" +
+ " index child2_index {" +
+ " stemming: shortest" +
+ " }" +
+ " field child2_field type string {" +
+ " indexing: input pf1 | lowercase | index | attribute | summary" +
+ " }" +
+ " rank-profile child2_profile inherits parent_profile {" +
+ " }" +
+ " constant child2_constant {" +
+ " file: constants/my_constant_tensor_file.json" +
+ " type: tensor<float>(x{},y{})" +
+ " }" +
+ " onnx-model child2_model {" +
+ " file: models/my_model.onnx" +
+ " }" +
+ " document-summary child2_summary inherits parent_summary {" +
+ " summary c2f1 type string {}" +
+ " }" +
+ " import field parentschema_ref.name as child2_imported {}" +
+ "}");
+
+ ApplicationBuilder builder = new ApplicationBuilder(new DeployLoggerStub());
+ builder.processorsToSkip().add(OnnxModelTypeResolver.class); // Avoid discovering the Onnx model referenced does not exist
+ builder.processorsToSkip().add(ImportedFieldsResolver.class); // Avoid discovering the document reference leads nowhere
+ builder.addSchema(parentLines);
+ builder.addSchema(child1Lines);
+ builder.addSchema(child2Lines);
+ builder.build(true);
+ var application = builder.application();
+
+ var child1 = application.schemas().get("child1");
+ assertEquals("pf1", child1.fieldSets().userFieldSets().get("parent_set").getFieldNames().stream().findFirst().get());
+ assertEquals("[c1f1, pf1]", child1.fieldSets().userFieldSets().get("child1_set").getFieldNames().toString());
+ assertEquals(Stemming.SHORTEST, child1.getStemming());
+ assertEquals(Stemming.BEST, child1.getIndex("parent_index").getStemming());
+ assertEquals(Stemming.SHORTEST, child1.getIndex("child1_index").getStemming());
+ assertNotNull(child1.getField("parent_field"));
+ assertNotNull(child1.getField("child1_field"));
+ assertNotNull(child1.getExtraField("parent_field"));
+ assertNotNull(child1.getExtraField("child1_field"));
+ assertNotNull(builder.getRankProfileRegistry().get(child1, "parent_profile"));
+ assertNotNull(builder.getRankProfileRegistry().get(child1, "child1_profile"));
+ var child1profile = builder.getRankProfileRegistry().get(child1, "child1_profile");
+ assertEquals("parent_profile", builder.getRankProfileRegistry().get(child1, "child1_profile").inheritedNames().get(0));
+ assertNotNull(child1.constants().get(FeatureNames.asConstantFeature("parent_constant")));
+ assertNotNull(child1profile.constants().get(FeatureNames.asConstantFeature("child1_constant")));
+ assertTrue(child1.constants().containsKey(FeatureNames.asConstantFeature("parent_constant")));
+ assertTrue(child1profile.constants().containsKey(FeatureNames.asConstantFeature("child1_constant")));
+ assertTrue(child1profile.constants().containsKey(FeatureNames.asConstantFeature("parent_constant")));
+ assertNotNull(child1.onnxModels().get("parent_model"));
+ assertNotNull(child1.onnxModels().get("child1_model"));
+ assertTrue(child1.onnxModels().containsKey("parent_model"));
+ assertTrue(child1.onnxModels().containsKey("child1_model"));
+ assertNotNull(child1.getSummary("parent_summary"));
+ assertNotNull(child1.getSummary("child1_summary"));
+ assertEquals("parent_summary", child1.getSummary("child1_summary").inherited().get().getName());
+ assertTrue(child1.getSummaries().containsKey("parent_summary"));
+ assertTrue(child1.getSummaries().containsKey("child1_summary"));
+ assertNotNull(child1.getSummaryField("pf1"));
+ assertNotNull(child1.getSummaryField("c1f1"));
+ assertNotNull(child1.getExplicitSummaryField("pf1"));
+ assertNotNull(child1.getExplicitSummaryField("c1f1"));
+ assertNotNull(child1.getUniqueNamedSummaryFields().get("pf1"));
+ assertNotNull(child1.getUniqueNamedSummaryFields().get("c1f1"));
+ assertNotNull(child1.temporaryImportedFields().get().fields().get("parent_imported"));
+ assertNotNull(child1.temporaryImportedFields().get().fields().get("child1_imported"));
+
+ var child2 = application.schemas().get("child2");
+ assertEquals("pf1", child2.fieldSets().userFieldSets().get("parent_set").getFieldNames().stream().findFirst().get());
+ assertEquals("[c2f1, pf1]", child2.fieldSets().userFieldSets().get("child2_set").getFieldNames().toString());
+ assertEquals(Stemming.SHORTEST, child2.getStemming());
+ assertEquals(Stemming.BEST, child2.getIndex("parent_index").getStemming());
+ assertEquals(Stemming.SHORTEST, child2.getIndex("child2_index").getStemming());
+ assertNotNull(child2.getField("parent_field"));
+ assertNotNull(child2.getField("child2_field"));
+ assertNotNull(child2.getExtraField("parent_field"));
+ assertNotNull(child2.getExtraField("child2_field"));
+ assertNotNull(builder.getRankProfileRegistry().get(child2, "parent_profile"));
+ assertNotNull(builder.getRankProfileRegistry().get(child2, "child2_profile"));
+ assertEquals("parent_profile", builder.getRankProfileRegistry().get(child2, "child2_profile").inheritedNames().get(0));
+ assertNotNull(child2.constants().get(FeatureNames.asConstantFeature("parent_constant")));
+ assertNotNull(child2.constants().get(FeatureNames.asConstantFeature("child2_constant")));
+ assertTrue(child2.constants().containsKey(FeatureNames.asConstantFeature("parent_constant")));
+ assertTrue(child2.constants().containsKey(FeatureNames.asConstantFeature("child2_constant")));
+ assertNotNull(child2.onnxModels().get("parent_model"));
+ assertNotNull(child2.onnxModels().get("child2_model"));
+ assertTrue(child2.onnxModels().containsKey("parent_model"));
+ assertTrue(child2.onnxModels().containsKey("child2_model"));
+ assertNotNull(child2.getSummary("parent_summary"));
+ assertNotNull(child2.getSummary("child2_summary"));
+ assertEquals("parent_summary", child2.getSummary("child2_summary").inherited().get().getName());
+ assertTrue(child2.getSummaries().containsKey("parent_summary"));
+ assertTrue(child2.getSummaries().containsKey("child2_summary"));
+ assertNotNull(child2.getSummaryField("pf1"));
+ assertNotNull(child2.getSummaryField("c2f1"));
+ assertNotNull(child2.getExplicitSummaryField("pf1"));
+ assertNotNull(child2.getExplicitSummaryField("c2f1"));
+ assertNotNull(child2.getUniqueNamedSummaryFields().get("pf1"));
+ assertNotNull(child2.getUniqueNamedSummaryFields().get("c2f1"));
+ assertNotNull(child2.temporaryImportedFields().get().fields().get("parent_imported"));
+ assertNotNull(child2.temporaryImportedFields().get().fields().get("child2_imported"));
+ DocumentSummary child2DefaultSummary = child2.getSummary("default");
+ assertEquals(6, child2DefaultSummary.getSummaryFields().size());
+ assertTrue(child2DefaultSummary.getSummaryFields().containsKey("child2_field"));
+ assertTrue(child2DefaultSummary.getSummaryFields().containsKey("parent_field"));
+ assertTrue(child2DefaultSummary.getSummaryFields().containsKey("pf1"));
+ assertTrue(child2DefaultSummary.getSummaryFields().containsKey("c2f1"));
+ DocumentSummary child2AttributeprefetchSummary = child2.getSummary("attributeprefetch");
+ assertEquals(4, child2AttributeprefetchSummary.getSummaryFields().size());
+ assertTrue(child2AttributeprefetchSummary.getSummaryFields().containsKey("child2_field"));
+ assertTrue(child2AttributeprefetchSummary.getSummaryFields().containsKey("parent_field"));
+ }
+
+ @Test
+ public void testSchemaInheritanceEmptyChildren() throws ParseException {
+ String parentLines = joinLines(
+ "schema parent {" +
+ " document parent {" +
+ " field pf1 type string {" +
+ " indexing: summary" +
+ " }" +
+ " }" +
+ " fieldset parent_set {" +
+ " fields: pf1" +
+ " }" +
+ " stemming: none" +
+ " index parent_index {" +
+ " stemming: best" +
+ " }" +
+ " field parent_field type string {" +
+ " indexing: input pf1 | lowercase | index | attribute | summary" +
+ " }" +
+ " rank-profile parent_profile {" +
+ " }" +
+ " constant parent_constant {" +
+ " file: constants/my_constant_tensor_file.json" +
+ " type: tensor<float>(x{},y{})" +
+ " }" +
+ " onnx-model parent_model {" +
+ " file: models/my_model.onnx" +
+ " }" +
+ " document-summary parent_summary {" +
+ " summary pf1 type string {}" +
+ " }" +
+ " import field parentschema_ref.name as parent_imported {}" +
+ " raw-as-base64-in-summary" +
+ "}");
+ String childLines = joinLines(
+ "schema child inherits parent {" +
+ " document child inherits parent {" +
+ " field cf1 type string {" +
+ " indexing: summary" +
+ " }" +
+ " }" +
+ "}");
+ String grandchildLines = joinLines(
+ "schema grandchild inherits child {" +
+ " document grandchild inherits child {" +
+ " field gf1 type string {" +
+ " indexing: summary" +
+ " }" +
+ " }" +
+ "}");
+
+ ApplicationBuilder builder = new ApplicationBuilder(new DeployLoggerStub());
+ builder.processorsToSkip().add(OnnxModelTypeResolver.class); // Avoid discovering the Onnx model referenced does not exist
+ builder.processorsToSkip().add(ImportedFieldsResolver.class); // Avoid discovering the document reference leads nowhere
+ builder.addSchema(parentLines);
+ builder.addSchema(childLines);
+ builder.addSchema(grandchildLines);
+ builder.build(true);
+ var application = builder.application();
+
+ assertInheritedFromParent(application.schemas().get("child"), builder.getRankProfileRegistry());
+ assertInheritedFromParent(application.schemas().get("grandchild"), builder.getRankProfileRegistry());
+ }
+
+ @Test
+ public void testInheritingMultipleRankProfilesWithOverlappingConstructsIsDisallowed1() throws ParseException {
+ try {
+ String profile = joinLines(
+ "schema test {" +
+ " document test {" +
+ " field title type string {" +
+ " indexing: summary" +
+ " }" +
+ " }" +
+ " rank-profile r1 {" +
+ " first-phase {" +
+ " expression: fieldMatch(title)" +
+ " }" +
+ " }" +
+ " rank-profile r2 {" +
+ " first-phase {" +
+ " expression: fieldMatch(title)" +
+ " }" +
+ " }" +
+ " rank-profile r3 inherits r1, r2 {" +
+ " }" +
+ "}");
+ ApplicationBuilder.createFromStrings(new DeployLoggerStub(), profile);
+ fail("Expected exception");
+ }
+ catch (IllegalArgumentException e) {
+ assertEquals("Only one of the profiles inherited by rank profile 'r3' can contain first-phase expression, but it is present in multiple",
+ e.getMessage());
+ }
+ }
+
+ @Test
+ public void testInheritingMultipleRankProfilesWithOverlappingConstructsIsAllowedWhenDefinedInChild() throws ParseException {
+ String profile = joinLines(
+ "schema test {" +
+ " document test {" +
+ " field title type string {" +
+ " indexing: summary" +
+ " }" +
+ " field myFilter type string {" +
+ " indexing: attribute\n" +
+ " rank: filter" +
+ " }" +
+ " }" +
+ " rank-profile r1 {" +
+ " first-phase {" +
+ " expression: fieldMatch(title)" +
+ " }" +
+ " }" +
+ " rank-profile r2 {" +
+ " first-phase {" +
+ " expression: fieldMatch(title)" +
+ " }" +
+ " }" +
+ " rank-profile r3 inherits r1, r2 {" +
+ " first-phase {" + // Redefined here so this does not cause failure
+ " expression: nativeRank" +
+ " }" +
+ " }" +
+ "}");
+ var builder = ApplicationBuilder.createFromStrings(new DeployLoggerStub(), profile);
+ var r3 = builder.getRankProfileRegistry().resolve(builder.application().schemas().get("test").getDocument(), "r3");
+ assertEquals(1, r3.allFilterFields().size());
+ }
+
+ @Test
+ public void testInheritingMultipleRankProfilesWithOverlappingConstructsIsDisallowed2() throws ParseException {
+ try {
+ String profile = joinLines(
+ "schema test {" +
+ " document test {" +
+ " field title type string {" +
+ " indexing: summary" +
+ " }" +
+ " }" +
+ " rank-profile r1 {" +
+ " function f1() {" +
+ " expression: fieldMatch(title)" +
+ " }" +
+ " }" +
+ " rank-profile r2 {" +
+ " function f1() {" +
+ " expression: fieldMatch(title)" +
+ " }" +
+ " }" +
+ " rank-profile r3 inherits r1, r2 {" +
+ " }" +
+ "}");
+ ApplicationBuilder.createFromStrings(new DeployLoggerStub(), profile);
+ fail("Expected exception");
+ }
+ catch (IllegalArgumentException e) {
+ assertEquals("rank profile 'r3' inherits rank profile 'r2' which contains function 'f1', but this function is already defined in another profile this inherits",
+ e.getMessage());
+ }
+ }
+
+ private void assertInheritedFromParent(Schema schema, RankProfileRegistry rankProfileRegistry) {
+ assertEquals("pf1", schema.fieldSets().userFieldSets().get("parent_set").getFieldNames().stream().findFirst().get());
+ assertEquals(Stemming.NONE, schema.getStemming());
+ assertEquals(Stemming.BEST, schema.getIndex("parent_index").getStemming());
+ assertNotNull(schema.getField("parent_field"));
+ assertNotNull(schema.getExtraField("parent_field"));
+ assertNotNull(rankProfileRegistry.get(schema, "parent_profile"));
+ assertNotNull(schema.constants().get(FeatureNames.asConstantFeature("parent_constant")));
+ assertTrue(schema.constants().containsKey(FeatureNames.asConstantFeature("parent_constant")));
+ assertNotNull(schema.onnxModels().get("parent_model"));
+ assertTrue(schema.onnxModels().containsKey("parent_model"));
+ assertNotNull(schema.getSummary("parent_summary"));
+ assertTrue(schema.getSummaries().containsKey("parent_summary"));
+ assertNotNull(schema.getSummaryField("pf1"));
+ assertNotNull(schema.getExplicitSummaryField("pf1"));
+ assertNotNull(schema.getUniqueNamedSummaryFields().get("pf1"));
+ assertNotNull(schema.temporaryImportedFields().get().fields().get("parent_imported"));
+ assertTrue(schema.isRawAsBase64());
+ }
+
+}
diff --git a/config-model/src/test/java/com/yahoo/schema/StemmingSettingTestCase.java b/config-model/src/test/java/com/yahoo/schema/StemmingSettingTestCase.java
new file mode 100644
index 00000000000..5dd75166783
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/schema/StemmingSettingTestCase.java
@@ -0,0 +1,51 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.schema;
+
+import com.yahoo.schema.document.SDField;
+import com.yahoo.schema.document.Stemming;
+import com.yahoo.schema.parser.ParseException;
+import org.junit.Test;
+
+import java.io.IOException;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+
+/**
+ * Stemming settings test
+ *
+ * @author bratseth
+ */
+public class StemmingSettingTestCase extends AbstractSchemaTestCase {
+
+ @Test
+ public void testStemmingSettings() throws IOException, ParseException {
+ Schema schema = ApplicationBuilder.buildFromFile("src/test/examples/stemmingsetting.sd");
+
+ SDField artist = (SDField) schema.getDocument().getField("artist");
+ assertEquals(Stemming.SHORTEST, artist.getStemming(schema));
+
+ SDField title = (SDField) schema.getDocument().getField("title");
+ assertEquals(Stemming.NONE, title.getStemming(schema));
+
+ SDField song = (SDField) schema.getDocument().getField("song");
+ assertEquals(Stemming.MULTIPLE, song.getStemming(schema));
+
+ SDField track = (SDField) schema.getDocument().getField("track");
+ assertEquals(Stemming.SHORTEST, track.getStemming(schema));
+
+ SDField backward = (SDField) schema.getDocument().getField("backward");
+ assertEquals(Stemming.SHORTEST, backward.getStemming(schema));
+
+ Index defaultIndex = schema.getIndex("default");
+ assertEquals(Stemming.SHORTEST, defaultIndex.getStemming());
+ }
+
+ @Test
+ public void requireThatStemmingIsDefaultBest() throws IOException, ParseException {
+ Schema schema = ApplicationBuilder.buildFromFile("src/test/examples/stemmingdefault.sd");
+ assertNull(schema.getConcreteField("my_str").getStemming());
+ assertEquals(Stemming.BEST, schema.getConcreteField("my_str").getStemming(schema));
+ }
+
+}
diff --git a/config-model/src/test/java/com/yahoo/schema/StructTestCase.java b/config-model/src/test/java/com/yahoo/schema/StructTestCase.java
new file mode 100755
index 00000000000..b140892ed5e
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/schema/StructTestCase.java
@@ -0,0 +1,59 @@
+
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.schema;
+
+import com.yahoo.document.config.DocumenttypesConfig;
+import com.yahoo.document.config.DocumentmanagerConfig;
+import com.yahoo.schema.derived.Deriver;
+import com.yahoo.schema.parser.ParseException;
+import org.junit.Ignore;
+import org.junit.Test;
+import java.io.IOException;
+import static org.junit.Assert.fail;
+
+/**
+ * Tests importing of document containing array type fields
+ *
+ * @author bratseth
+ */
+public class StructTestCase extends AbstractSchemaTestCase {
+
+ @Test
+ public void testStruct() throws IOException {
+ assertConfigFile("src/test/examples/structresult.cfg",
+ new DocumentmanagerConfig(Deriver.getDocumentManagerConfig("src/test/examples/struct.sd")) + "\n");
+ }
+
+ @Test
+ public void testBadStruct() throws IOException {
+ try {
+ ApplicationBuilder.buildFromFile("src/test/examples/badstruct.sd");
+ fail("Should throw exception.");
+ } catch (IllegalArgumentException|ParseException expected) {
+ System.err.println("As expected, with message: "+expected.getMessage());
+ // success
+ }
+ }
+
+ @Test
+ @Ignore
+ public void testStructAndDocumentWithSameNames() {
+ try {
+ DocumenttypesConfig.Builder dt = Deriver.getDocumentTypesConfig("src/test/examples/structanddocumentwithsamenames.sd");
+ // while the above line may work, the config generated will fail.
+ // See also NameCollisionTestCase.
+ } catch (Exception e) {
+ fail("Should not have thrown exception " + e);
+ }
+ }
+
+ /**
+ * Declaring a struct before a document should work
+ */
+ @Test
+ public void testStructOutsideDocumentLegal() throws IOException, ParseException {
+ new ApplicationBuilder().addSchemaFile("src/test/examples/structoutsideofdocument.sd");
+ }
+
+}
+
diff --git a/config-model/src/test/java/com/yahoo/schema/SummaryTestCase.java b/config-model/src/test/java/com/yahoo/schema/SummaryTestCase.java
new file mode 100644
index 00000000000..0fab9b381fc
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/schema/SummaryTestCase.java
@@ -0,0 +1,286 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.schema;
+
+import com.yahoo.schema.parser.ParseException;
+import com.yahoo.vespa.documentmodel.DocumentSummary;
+import com.yahoo.vespa.model.test.utils.DeployLoggerStub;
+import com.yahoo.vespa.objects.FieldBase;
+import org.junit.Test;
+import static com.yahoo.config.model.test.TestUtil.joinLines;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Optional;
+import java.util.logging.Level;
+import java.util.stream.Collectors;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+/**
+ * Tests summary validation
+ *
+ * @author bratseth
+ */
+public class SummaryTestCase {
+
+ @Test
+ public void testMemorySummary() throws ParseException {
+ String sd = joinLines(
+ "schema memorysummary {",
+ " document memorysummary {",
+ " field inmemory type string {",
+ " indexing: attribute | summary",
+ " }",
+ " field ondisk type string {",
+ " indexing: index # no summary, so ignored",
+ " }",
+ " }",
+ "}");
+ DeployLoggerStub logger = new DeployLoggerStub();
+ ApplicationBuilder.createFromString(sd, logger);
+ assertTrue(logger.entries.isEmpty());
+ }
+
+ @Test
+ public void testDiskSummary() throws ParseException {
+ String sd = joinLines(
+ "schema disksummary {",
+ " document-summary foobar {",
+ " summary foo1 type string { source: inmemory }",
+ " summary foo2 type string { source: ondisk }",
+ " }",
+ " document disksummary {",
+ " field inmemory type string {",
+ " indexing: attribute | summary",
+ " }",
+ " field ondisk type string {",
+ " indexing: index | summary",
+ " }",
+ " }",
+ "}");
+ DeployLoggerStub logger = new DeployLoggerStub();
+ ApplicationBuilder.createFromString(sd, logger);
+ assertEquals(1, logger.entries.size());
+ assertEquals(Level.WARNING, logger.entries.get(0).level);
+ assertEquals("summary field 'foo2' in document summary 'foobar' references source field 'ondisk', " +
+ "which is not an attribute: Using this summary will cause disk accesses. " +
+ "Set 'from-disk' on this summary class to silence this warning.",
+ logger.entries.get(0).message);
+ }
+
+ @Test
+ public void testDiskSummaryExplicit() throws ParseException {
+ String sd = joinLines(
+ "schema disksummary {",
+ " document disksummary {",
+ " field inmemory type string {",
+ " indexing: attribute | summary",
+ " }",
+ " field ondisk type string {",
+ " indexing: index | summary",
+ " }",
+ " }",
+ " document-summary foobar {",
+ " summary foo1 type string { source: inmemory }",
+ " summary foo2 type string { source: ondisk }",
+ " from-disk",
+ " }",
+ "}");
+ DeployLoggerStub logger = new DeployLoggerStub();
+ ApplicationBuilder.createFromString(sd, logger);
+ assertTrue(logger.entries.isEmpty());
+ }
+
+ @Test
+ public void testStructMemorySummary() throws ParseException {
+ String sd = joinLines(
+ "schema structmemorysummary {",
+ " document structmemorysummary {",
+ " struct elem {",
+ " field name type string {}",
+ " field weight type int {}",
+ " }",
+ " field elem_array type array<elem> {",
+ " indexing: summary",
+ " struct-field name {",
+ " indexing: attribute",
+ " }",
+ " struct-field weight {",
+ " indexing: attribute",
+ " }",
+ " }",
+ " }",
+ " document-summary filtered {",
+ " summary elem_array_filtered type array<elem> {",
+ " source: elem_array",
+ " matched-elements-only",
+ " }",
+ " }",
+ "}");
+ DeployLoggerStub logger = new DeployLoggerStub();
+ ApplicationBuilder.createFromString(sd, logger);
+ assertTrue(logger.entries.isEmpty());
+ }
+
+ @Test
+ public void testInheritance() throws Exception {
+ String sd = joinLines(
+ "schema music {",
+ " document music {",
+ " field title type string {",
+ " indexing: summary | attribute | index",
+ " }",
+ " field artist type string {",
+ " indexing: summary | attribute | index",
+ " }",
+ " field album type string {",
+ " indexing: summary | attribute | index",
+ " }",
+ " }",
+ " document-summary title {",
+ " summary title type string {",
+ " source: title",
+ " }",
+ " }",
+ " document-summary title_artist inherits title {",
+ " summary artist type string {",
+ " source: artist",
+ " }",
+ " }",
+ " document-summary everything inherits title_artist {",
+ " summary album type string {",
+ " source: album",
+ " }",
+ " }",
+ "}");
+ var logger = new DeployLoggerStub();
+ var search = ApplicationBuilder.createFromString(sd, logger).getSchema();
+ assertEquals(List.of(), logger.entries);
+
+ var titleField = "title";
+ var artistField = "artist";
+ var albumField = "album";
+ var titleSummary = search.getSummary(titleField);
+ var titleArtistSummary = search.getSummary(titleField + "_" + artistField);
+ var everythingSummary = search.getSummary("everything");
+
+ var implicitFields = List.of("rankfeatures", "summaryfeatures");
+ var tests = List.of(
+ new TestValue(titleSummary, null, List.of(List.of(titleField), implicitFields)),
+ new TestValue(titleArtistSummary, titleSummary, List.of(List.of(titleField), implicitFields, List.of(artistField))),
+ new TestValue(everythingSummary, titleArtistSummary, List.of(List.of(titleField), implicitFields, List.of(artistField, albumField)))
+ );
+ tests.forEach(testValue -> {
+ var actualFields = testValue.summary.getSummaryFields().values().stream()
+ .map(FieldBase::getName)
+ .collect(Collectors.toList());
+ assertEquals(testValue.summary.getName() + (testValue.parent == null ? " does not inherit anything" : " inherits " + testValue.parent.getName()),
+ Optional.ofNullable(testValue.parent),
+ testValue.summary.inherited());
+ assertEquals("Summary " + testValue.summary.getName() + " has expected fields", testValue.fields, actualFields);
+ });
+ }
+
+ @Test
+ public void testRedeclaringInheritedFieldFails() throws Exception {
+ String sd = joinLines(
+ "schema music {",
+ " document music {",
+ " field title type string {",
+ " indexing: summary | attribute | index",
+ " }",
+ " field title_short type string {",
+ " indexing: summary | attribute | index",
+ " }",
+ " }",
+ " document-summary title {",
+ " summary title type string {",
+ " source: title",
+ " }",
+ " }",
+ " document-summary title2 inherits title {",
+ " summary title type string {",
+ " source: title_short",
+ " }",
+ " }",
+ "}");
+ var logger = new DeployLoggerStub();
+ try {
+ ApplicationBuilder.createFromString(sd, logger);
+ fail("Expected exception");
+ } catch (IllegalArgumentException e) {
+ assertEquals("For schema 'music', summary class 'title2', summary field 'title': Can not use " +
+ "source 'title_short' for this summary field, an equally named field in summary class 'title' " +
+ "uses a different source: 'title'.", e.getMessage());
+ }
+ }
+
+ @Test
+ public void testValidationOfInheritedSummary() throws ParseException {
+ try {
+ String schema = joinLines(
+ "schema test {" +
+ " document test {" +
+ " }" +
+ " document-summary test_summary inherits nonesuch {" +
+ " }" +
+ "}");
+ DeployLoggerStub logger = new DeployLoggerStub();
+ ApplicationBuilder.createFromStrings(logger, schema);
+ assertEquals("document summary 'test_summary' inherits nonesuch but this is not present in schema 'test'",
+ logger.entries.get(0).message);
+ // fail("Expected failure");
+ }
+ catch (IllegalArgumentException e) {
+ // assertEquals("document summary 'test_summary' inherits nonesuch but this is not present in schema 'test'",
+ // e.getMessage());
+ }
+ }
+
+ @Test
+ public void testInheritingParentSummary() throws ParseException {
+ String parent = joinLines(
+ "schema parent {" +
+ " document parent {" +
+ " field pf1 type string {" +
+ " indexing: summary" +
+ " }" +
+ " }" +
+ " document-summary parent_summary {" +
+ " summary pf1 type string {}" +
+ " }" +
+ "}");
+ String child = joinLines(
+ "schema child inherits parent {" +
+ " document child inherits parent {" +
+ " field cf1 type string {" +
+ " indexing: summary" +
+ " }" +
+ " }" +
+ " document-summary child_summary inherits parent_summary {" +
+ " summary cf1 type string {}" +
+ " }" +
+ "}");
+ DeployLoggerStub logger = new DeployLoggerStub();
+ ApplicationBuilder.createFromStrings(logger, parent, child);
+ logger.entries.forEach(e -> System.out.println(e));
+ //assertTrue(logger.entries.isEmpty());
+ }
+
+ private static class TestValue {
+
+ private final DocumentSummary summary;
+ private final DocumentSummary parent;
+ private final List<String> fields;
+
+ public TestValue(DocumentSummary summary, DocumentSummary parent, List<List<String>> fields) {
+ this.summary = summary;
+ this.parent = parent;
+ this.fields = fields.stream().flatMap(Collection::stream).collect(Collectors.toList());;
+ }
+
+ }
+
+}
diff --git a/config-model/src/test/java/com/yahoo/schema/UrlFieldValidationTestCase.java b/config-model/src/test/java/com/yahoo/schema/UrlFieldValidationTestCase.java
new file mode 100644
index 00000000000..83bb0d4548c
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/schema/UrlFieldValidationTestCase.java
@@ -0,0 +1,34 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.schema;
+
+import com.yahoo.schema.parser.ParseException;
+import com.yahoo.yolean.Exceptions;
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
+
+/**
+ * @author bratseth
+ */
+public class UrlFieldValidationTestCase {
+
+ @Test
+ public void requireThatInheritedRiseFieldsStillCanBeInConflictButDontThrowException() throws ParseException {
+ ApplicationBuilder builder = new ApplicationBuilder();
+ builder.addSchema("search test {" +
+ " document test { " +
+ " field a type uri { indexing: attribute | summary }" +
+ " }" +
+ "}");
+ try {
+ builder.build(true);
+ fail("Should have caused an exception");
+ // success
+ } catch (IllegalArgumentException e) {
+ assertEquals("Error in field 'a' in schema 'test': uri type fields cannot be attributes",
+ Exceptions.toMessageString(e));
+ }
+ }
+
+}
diff --git a/config-model/src/test/java/com/yahoo/schema/derived/AbstractExportingTestCase.java b/config-model/src/test/java/com/yahoo/schema/derived/AbstractExportingTestCase.java
new file mode 100644
index 00000000000..ad2c2d6078d
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/schema/derived/AbstractExportingTestCase.java
@@ -0,0 +1,165 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.schema.derived;
+
+import com.yahoo.config.application.api.DeployLogger;
+import com.yahoo.config.model.application.provider.MockFileRegistry;
+import com.yahoo.config.model.deploy.DeployState;
+import com.yahoo.config.model.deploy.TestProperties;
+import com.yahoo.document.config.DocumenttypesConfig;
+import com.yahoo.document.config.DocumentmanagerConfig;
+import com.yahoo.schema.Schema;
+import com.yahoo.schema.ApplicationBuilder;
+import com.yahoo.schema.AbstractSchemaTestCase;
+import com.yahoo.schema.parser.ParseException;
+import com.yahoo.vespa.configmodel.producers.DocumentManager;
+import com.yahoo.vespa.configmodel.producers.DocumentTypes;
+
+import java.io.File;
+import java.io.IOException;
+
+/**
+ * Superclass of tests needing file comparisons
+ *
+ * @author bratseth
+ */
+public abstract class AbstractExportingTestCase extends AbstractSchemaTestCase {
+
+ private static final String tempDir = "temp/";
+ private static final String schemaRoot = "src/test/derived/";
+
+ private DerivedConfiguration derive(String dirName,
+ String schemaName,
+ TestProperties properties,
+ DeployLogger logger) throws IOException, ParseException {
+ File toDir = new File(tempDir + dirName);
+ toDir.mkdirs();
+ deleteContent(toDir);
+
+ ApplicationBuilder builder = ApplicationBuilder.createFromDirectory(schemaRoot + dirName + "/",
+ new MockFileRegistry(),
+ logger,
+ properties);
+ return derive(dirName, schemaName, properties, builder, logger);
+ }
+
+ private DerivedConfiguration derive(String dirName,
+ String schemaName,
+ TestProperties properties,
+ ApplicationBuilder builder,
+ DeployLogger logger) throws IOException {
+ DerivedConfiguration config = new DerivedConfiguration(builder.getSchema(schemaName),
+ new DeployState.Builder().properties(properties)
+ .deployLogger(logger)
+ .rankProfileRegistry(builder.getRankProfileRegistry())
+ .queryProfiles(builder.getQueryProfileRegistry())
+ .build());
+ return export(dirName, builder, config);
+ }
+
+ DerivedConfiguration derive(String dirName, ApplicationBuilder builder, Schema schema) throws IOException {
+ DerivedConfiguration config = new DerivedConfiguration(schema,
+ builder.getRankProfileRegistry(),
+ builder.getQueryProfileRegistry());
+ return export(dirName, builder, config);
+ }
+
+ private DerivedConfiguration export(String name, ApplicationBuilder builder, DerivedConfiguration config) throws IOException {
+ String path = exportConfig(name, config);
+ DerivedConfiguration.exportDocuments(new DocumentManager()
+ .produce(builder.getModel(), new DocumentmanagerConfig.Builder()), path);
+ DerivedConfiguration.exportDocuments(new DocumentTypes().produce(builder.getModel(), new DocumenttypesConfig.Builder()), path);
+ DerivedConfiguration.exportQueryProfiles(builder.getQueryProfileRegistry(), path);
+ config.exportConstants(path);
+ return config;
+ }
+
+ private String exportConfig(String name, DerivedConfiguration config) throws IOException {
+ String path = tempDir + name;
+ config.export(path);
+ return path;
+ }
+
+ /**
+ * Derives a config from name/name.sd below the test dir and verifies that every .cfg file in name/ has a
+ * corresponding file with the same content in temp/name. Versions can and should be omitted from the .cfg file
+ * names. This will fail if the search definition dir has multiple search definitions.
+ *
+ * @param dirName the name of the directory containing the searchdef file to verify
+ * @throws ParseException if the .sd file could not be parsed
+ * @throws IOException if file access failed
+ */
+ protected DerivedConfiguration assertCorrectDeriving(String dirName) throws IOException, ParseException {
+ return assertCorrectDeriving(dirName, new TestableDeployLogger());
+ }
+ protected DerivedConfiguration assertCorrectDeriving(String dirName, DeployLogger logger) throws IOException, ParseException {
+ return assertCorrectDeriving(dirName, null, logger);
+ }
+
+ protected DerivedConfiguration assertCorrectDeriving(String dirName,
+ String searchDefinitionName,
+ DeployLogger logger) throws IOException, ParseException {
+ return assertCorrectDeriving(dirName, searchDefinitionName, new TestProperties(), logger);
+ }
+
+ protected DerivedConfiguration assertCorrectDeriving(String dirName,
+ TestProperties properties) throws IOException, ParseException {
+ return assertCorrectDeriving(dirName, null, properties, new TestableDeployLogger());
+ }
+
+ protected DerivedConfiguration assertCorrectDeriving(String dirName,
+ String schemaName,
+ TestProperties properties,
+ DeployLogger logger) throws IOException, ParseException {
+ DerivedConfiguration derived = derive(dirName, schemaName, properties, logger);
+ assertCorrectConfigFiles(dirName);
+ return derived;
+ }
+
+ /**
+ * Asserts config is correctly derived given a builder.
+ * This will fail if the builder contains multiple search definitions.
+ */
+ protected DerivedConfiguration assertCorrectDeriving(ApplicationBuilder builder, String dirName, DeployLogger logger) throws IOException {
+ builder.build(true);
+ DerivedConfiguration derived = derive(dirName, null, new TestProperties(), builder, logger);
+ assertCorrectConfigFiles(dirName);
+ return derived;
+ }
+
+ protected DerivedConfiguration assertCorrectDeriving(ApplicationBuilder builder, Schema schema, String name) throws IOException {
+ DerivedConfiguration derived = derive(name, builder, schema);
+ assertCorrectConfigFiles(name);
+ return derived;
+ }
+
+ /**
+ * Assert that search is derived into the files in the directory given by name.
+ *
+ * @param name the local name of the directory containing the files to check
+ * @throws IOException if file access failed
+ */
+ void assertCorrectConfigFiles(String name) throws IOException {
+ File[] files = new File(schemaRoot, name).listFiles();
+ if (files == null) return;
+ for (File file : files) {
+ if ( ! file.getName().endsWith(".cfg")) continue;
+ boolean orderMatters = file.getName().equals("ilscripts.cfg");
+ assertEqualFiles(file.getPath(), tempDir + name + "/" + file.getName(), orderMatters);
+ }
+ }
+
+ static void assertEqualFiles(String correctFileName, String checkFileName, boolean orderMatters) throws IOException {
+ // Set updateOnAssert to true if you want update the files with correct answer.
+ assertConfigFiles(correctFileName, checkFileName, orderMatters, false);
+ }
+
+ void deleteContent(File dir) {
+ File[] files = dir.listFiles();
+ if (files == null) return;
+
+ for (File file : files) {
+ file.delete();
+ }
+ }
+
+}
diff --git a/config-model/src/test/java/com/yahoo/schema/derived/AnnotationsTestCase.java b/config-model/src/test/java/com/yahoo/schema/derived/AnnotationsTestCase.java
new file mode 100755
index 00000000000..60867261f93
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/schema/derived/AnnotationsTestCase.java
@@ -0,0 +1,66 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.schema.derived;
+
+import com.yahoo.schema.parser.ParseException;
+import org.junit.Test;
+
+import java.io.IOException;
+
+/**
+ * @author Einar M R Rosenvinge
+ */
+public class AnnotationsTestCase extends AbstractExportingTestCase {
+
+ @Test
+ public void requireThatStructRegistersIfOnlyUsedByAnnotation() throws IOException, ParseException {
+ assertCorrectDeriving("annotationsstruct");
+ }
+
+ @Test
+ public void requireThatStructRegistersIfOnlyUsedAsArrayByAnnotation() throws IOException, ParseException {
+ assertCorrectDeriving("annotationsstructarray");
+ }
+
+ @Test
+ public void testSimpleAnnotationDeriving() throws IOException, ParseException {
+ assertCorrectDeriving("annotationssimple");
+ }
+
+ @Test
+ public void testAnnotationDerivingWithImplicitStruct() throws IOException, ParseException {
+ assertCorrectDeriving("annotationsimplicitstruct");
+ }
+
+ @Test
+ public void testAnnotationDerivingInheritance() throws IOException, ParseException {
+ assertCorrectDeriving("annotationsinheritance");
+ }
+
+ @Test
+ public void testAnnotationDerivingInheritance2() throws IOException, ParseException {
+ assertCorrectDeriving("annotationsinheritance2");
+ }
+
+ @Test
+ public void testSimpleReference() throws IOException, ParseException {
+ assertCorrectDeriving("annotationsreference");
+ }
+
+ @Test
+ public void testAdvancedReference() throws IOException, ParseException {
+ assertCorrectDeriving("annotationsreference2");
+ }
+
+ @Test
+ public void testAnnotationsPolymorphy() throws IOException, ParseException {
+ assertCorrectDeriving("annotationspolymorphy");
+ }
+
+ /**
+ * An annotation declared before document {} should work.
+ */
+ @Test
+ public void testAnnotationOutsideOfDocumentNew() throws IOException, ParseException {
+ assertCorrectDeriving("annotationsoutsideofdocument");
+ }
+}
diff --git a/config-model/src/test/java/com/yahoo/schema/derived/ArraysTestCase.java b/config-model/src/test/java/com/yahoo/schema/derived/ArraysTestCase.java
new file mode 100644
index 00000000000..5b138413a7a
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/schema/derived/ArraysTestCase.java
@@ -0,0 +1,23 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.schema.derived;
+
+import com.yahoo.schema.parser.ParseException;
+import org.junit.Test;
+
+import java.io.IOException;
+
+/**
+ * Tests array type deriving. Indexing statements over array
+ * types is not yet supported, so this tests document type
+ * configuration deriving only. Expand later.
+ *
+ * @author bratseth
+ */
+public class ArraysTestCase extends AbstractExportingTestCase {
+
+ @Test
+ public void testDocumentDeriving() throws IOException, ParseException {
+ assertCorrectDeriving("arrays");
+ }
+
+}
diff --git a/config-model/src/test/java/com/yahoo/schema/derived/AttributeListTestCase.java b/config-model/src/test/java/com/yahoo/schema/derived/AttributeListTestCase.java
new file mode 100644
index 00000000000..1c51d3ec365
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/schema/derived/AttributeListTestCase.java
@@ -0,0 +1,129 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.schema.derived;
+
+import com.yahoo.schema.Schema;
+import com.yahoo.schema.ApplicationBuilder;
+import com.yahoo.schema.AbstractSchemaTestCase;
+import com.yahoo.schema.document.Attribute;
+import com.yahoo.schema.parser.ParseException;
+import org.junit.Test;
+
+import java.io.IOException;
+import java.util.Iterator;
+
+import static com.yahoo.config.model.test.TestUtil.joinLines;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+
+/**
+ * Tests attribute deriving
+ *
+ * @author bratseth
+ */
+public class AttributeListTestCase extends AbstractSchemaTestCase {
+
+ @Test
+ public void testDeriving() throws IOException, ParseException {
+ // Test attribute importing
+ Schema schema = ApplicationBuilder.buildFromFile("src/test/examples/simple.sd");
+
+ // Test attribute deriving
+ AttributeFields attributeFields = new AttributeFields(schema);
+ Iterator attributes = attributeFields.attributeIterator();
+ Attribute attribute;
+ attribute = (Attribute)attributes.next();
+ assertEquals("popularity", attribute.getName());
+ assertEquals(Attribute.Type.INTEGER, attribute.getType());
+ assertEquals(Attribute.CollectionType.SINGLE, attribute.getCollectionType());
+
+ attribute = (Attribute)attributes.next();
+ assertEquals("measurement", attribute.getName());
+ assertEquals(Attribute.Type.INTEGER, attribute.getType());
+ assertEquals(Attribute.CollectionType.SINGLE, attribute.getCollectionType());
+
+ attribute = (Attribute)attributes.next();
+ assertEquals("smallattribute", attribute.getName());
+ assertEquals(Attribute.Type.BYTE, attribute.getType());
+ assertEquals(Attribute.CollectionType.ARRAY, attribute.getCollectionType());
+
+ attribute = (Attribute)attributes.next();
+ assertEquals("access", attribute.getName());
+ assertEquals(Attribute.Type.BYTE, attribute.getType());
+ assertEquals(Attribute.CollectionType.SINGLE, attribute.getCollectionType());
+
+ attribute = (Attribute)attributes.next();
+ assertEquals("category_arr", attribute.getName());
+ assertEquals(Attribute.Type.STRING, attribute.getType());
+ assertEquals(Attribute.CollectionType.ARRAY, attribute.getCollectionType());
+
+ attribute = (Attribute)attributes.next();
+ assertEquals("measurement_arr", attribute.getName());
+ assertEquals(Attribute.Type.INTEGER, attribute.getType());
+ assertEquals(Attribute.CollectionType.ARRAY, attribute.getCollectionType());
+
+ attribute = (Attribute)attributes.next();
+ assertEquals("popsiness", attribute.getName());
+ assertEquals(Attribute.Type.INTEGER, attribute.getType());
+ assertEquals(Attribute.CollectionType.SINGLE, attribute.getCollectionType());
+
+ assertFalse(attributes.hasNext());
+ }
+
+ @Test
+ public void fields_in_array_of_struct_are_derived_into_array_attributes() throws IOException, ParseException {
+ Schema schema = ApplicationBuilder.buildFromFile("src/test/derived/array_of_struct_attribute/test.sd");
+ Iterator<Attribute> attributes = new AttributeFields(schema).attributeIterator();
+
+ assertAttribute("elem_array.name", Attribute.Type.STRING, Attribute.CollectionType.ARRAY, true, attributes.next());
+ assertAttribute("elem_array.weight", Attribute.Type.INTEGER, Attribute.CollectionType.ARRAY, false, attributes.next());
+ assertFalse(attributes.hasNext());
+ }
+
+ @Test
+ public void fields_in_map_of_struct_are_derived_into_array_attributes() throws IOException, ParseException {
+ Schema schema = ApplicationBuilder.buildFromFile("src/test/derived/map_of_struct_attribute/test.sd");
+ Iterator<Attribute> attributes = new AttributeFields(schema).attributeIterator();
+
+ assertAttribute("str_elem_map.key", Attribute.Type.STRING, Attribute.CollectionType.ARRAY, true, attributes.next());
+ assertAttribute("str_elem_map.value.name", Attribute.Type.STRING, Attribute.CollectionType.ARRAY, false, attributes.next());
+ assertAttribute("str_elem_map.value.weight", Attribute.Type.INTEGER, Attribute.CollectionType.ARRAY, false, attributes.next());
+ assertAttribute("int_elem_map.key", Attribute.Type.INTEGER, Attribute.CollectionType.ARRAY, false, attributes.next());
+ assertAttribute("int_elem_map.value.name", Attribute.Type.STRING, Attribute.CollectionType.ARRAY, true, attributes.next());
+ assertFalse(attributes.hasNext());
+ }
+
+ private static void assertAttribute(String name, Attribute.Type type, Attribute.CollectionType collection, boolean isFastSearch, Attribute attr) {
+ assertEquals(name, attr.getName());
+ assertEquals(type, attr.getType());
+ assertEquals(collection, attr.getCollectionType());
+ assertEquals(isFastSearch, attr.isFastSearch());
+ }
+
+ @Test
+ public void only_zcurve_attribute_is_derived_from_array_of_position_field() throws ParseException {
+ Schema schema = ApplicationBuilder.createFromString(
+ joinLines("search test {",
+ " document test {",
+ " field pos_array type array<position> {",
+ " indexing: attribute",
+ " }",
+ " }",
+ "}")).getSchema();
+ Iterator<Attribute> attributes = new AttributeFields(schema).attributeIterator();
+
+ assertAttribute("pos_array_zcurve", Attribute.Type.LONG, Attribute.CollectionType.ARRAY, true, attributes.next());
+ assertFalse(attributes.hasNext());
+ }
+
+ @Test
+ public void fields_in_map_of_primitive_are_derived_into_array_attributes() throws IOException, ParseException {
+ Schema schema = ApplicationBuilder.buildFromFile("src/test/derived/map_attribute/test.sd");
+ Iterator<Attribute> attributes = new AttributeFields(schema).attributeIterator();
+
+ assertAttribute("str_map.key", Attribute.Type.STRING, Attribute.CollectionType.ARRAY, true, attributes.next());
+ assertAttribute("str_map.value", Attribute.Type.STRING, Attribute.CollectionType.ARRAY, false, attributes.next());
+ assertAttribute("int_map.key", Attribute.Type.INTEGER, Attribute.CollectionType.ARRAY, false, attributes.next());
+ assertFalse(attributes.hasNext());
+ }
+
+}
diff --git a/config-model/src/test/java/com/yahoo/schema/derived/AttributesTestCase.java b/config-model/src/test/java/com/yahoo/schema/derived/AttributesTestCase.java
new file mode 100644
index 00000000000..3d08805acdf
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/schema/derived/AttributesTestCase.java
@@ -0,0 +1,36 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.schema.derived;
+
+import com.yahoo.schema.parser.ParseException;
+import org.junit.Test;
+
+import java.io.IOException;
+
+/**
+ * Tests attribute settings
+ *
+ * @author bratseth
+ */
+public class AttributesTestCase extends AbstractExportingTestCase {
+
+ @Test
+ public void testDocumentDeriving() throws IOException, ParseException {
+ assertCorrectDeriving("attributes");
+ }
+
+ @Test
+ public void testArrayOfStructAttribute() throws IOException, ParseException {
+ assertCorrectDeriving("array_of_struct_attribute");
+ }
+
+ @Test
+ public void testMapOfStructAttribute() throws IOException, ParseException {
+ assertCorrectDeriving("map_of_struct_attribute");
+ }
+
+ @Test
+ public void testMapOfPrimitiveAttribute() throws IOException, ParseException {
+ assertCorrectDeriving("map_attribute");
+ }
+
+}
diff --git a/config-model/src/test/java/com/yahoo/schema/derived/CasingTestCase.java b/config-model/src/test/java/com/yahoo/schema/derived/CasingTestCase.java
new file mode 100644
index 00000000000..80ebcb825f4
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/schema/derived/CasingTestCase.java
@@ -0,0 +1,36 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.schema.derived;
+
+import com.yahoo.schema.Schema;
+import com.yahoo.schema.ApplicationBuilder;
+import com.yahoo.schema.AbstractSchemaTestCase;
+import com.yahoo.schema.parser.ParseException;
+import org.junit.Test;
+
+import java.io.IOException;
+
+import static org.junit.Assert.assertEquals;
+
+/**
+ * Correct casing for derived attributes
+ *
+ * @author vegardh
+ */
+public class CasingTestCase extends AbstractSchemaTestCase {
+
+ @Test
+ public void testCasing() throws IOException, ParseException {
+ Schema schema = ApplicationBuilder.buildFromFile("src/test/examples/casing.sd");
+ assertEquals(schema.getIndex("color").getName(), "color");
+ assertEquals(schema.getIndex("Foo").getName(), "Foo");
+ assertEquals(schema.getIndex("Price").getName(), "Price");
+ assertEquals(schema.getAttribute("artist").getName(), "artist");
+ assertEquals(schema.getAttribute("Drummer").getName(), "Drummer");
+ assertEquals(schema.getAttribute("guitarist").getName(), "guitarist");
+ assertEquals(schema.getAttribute("title").getName(), "title");
+ assertEquals(schema.getAttribute("Trumpetist").getName(), "Trumpetist");
+ assertEquals(schema.getAttribute("Saxophonist").getName(), "Saxophonist");
+ assertEquals(schema.getAttribute("TenorSaxophonist").getName(), "TenorSaxophonist");
+ assertEquals(schema.getAttribute("Flutist").getName(), "Flutist");
+ }
+}
diff --git a/config-model/src/test/java/com/yahoo/schema/derived/CombinedAttributeAndIndexSchemaTestCase.java b/config-model/src/test/java/com/yahoo/schema/derived/CombinedAttributeAndIndexSchemaTestCase.java
new file mode 100644
index 00000000000..542320d9670
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/schema/derived/CombinedAttributeAndIndexSchemaTestCase.java
@@ -0,0 +1,21 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.schema.derived;
+
+import com.yahoo.schema.parser.ParseException;
+import org.junit.Test;
+
+import java.io.IOException;
+
+/**
+ * Tests deriving a configuration with multiple summaries
+ *
+ * @author bratseth
+ */
+public class CombinedAttributeAndIndexSchemaTestCase extends AbstractExportingTestCase {
+
+ @Test
+ public void testMultipleSummaries() throws IOException, ParseException {
+ assertCorrectDeriving("combinedattributeandindexsearch");
+ }
+
+}
diff --git a/config-model/src/test/java/com/yahoo/schema/derived/DeriverTestCase.java b/config-model/src/test/java/com/yahoo/schema/derived/DeriverTestCase.java
new file mode 100644
index 00000000000..422f4522b26
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/schema/derived/DeriverTestCase.java
@@ -0,0 +1,31 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.schema.derived;
+
+import com.yahoo.document.DataType;
+import com.yahoo.document.DocumentTypeManager;
+import com.yahoo.document.config.DocumentmanagerConfig;
+import com.yahoo.schema.AbstractSchemaTestCase;
+import org.junit.Test;
+
+import java.util.List;
+
+import static org.junit.Assert.assertEquals;
+
+/**
+ * Tests deriving using the Deriver facade
+ *
+ * @author bratseth
+ */
+public class DeriverTestCase extends AbstractSchemaTestCase {
+
+ @Test
+ public void testDeriveDocManager() {
+ DocumentTypeManager dtm = new DocumentTypeManager(new DocumentmanagerConfig(
+ Deriver.getDocumentManagerConfig(List.of(
+ "src/test/derived/deriver/child.sd",
+ "src/test/derived/deriver/parent.sd",
+ "src/test/derived/deriver/grandparent.sd"))));
+ assertEquals(dtm.getDocumentType("child").getField("a").getDataType(), DataType.STRING);
+ }
+
+}
diff --git a/config-model/src/test/java/com/yahoo/schema/derived/DuplicateStructTestCase.java b/config-model/src/test/java/com/yahoo/schema/derived/DuplicateStructTestCase.java
new file mode 100644
index 00000000000..7915a1d7763
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/schema/derived/DuplicateStructTestCase.java
@@ -0,0 +1,19 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.schema.derived;
+
+import com.yahoo.config.model.deploy.TestProperties;
+import org.junit.Test;
+
+/**
+ * @author arnej
+ */
+public class DuplicateStructTestCase extends AbstractExportingTestCase {
+
+ @Test
+ public void exact_duplicate_struct_works() throws Exception {
+ assertCorrectDeriving("duplicate_struct", "foobar",
+ new TestProperties(),
+ new TestableDeployLogger());
+ }
+
+}
diff --git a/config-model/src/test/java/com/yahoo/schema/derived/EmptyRankProfileTestCase.java b/config-model/src/test/java/com/yahoo/schema/derived/EmptyRankProfileTestCase.java
new file mode 100644
index 00000000000..c3195d1a626
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/schema/derived/EmptyRankProfileTestCase.java
@@ -0,0 +1,38 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.schema.derived;
+
+import com.yahoo.config.model.test.MockApplicationPackage;
+import com.yahoo.document.DataType;
+import com.yahoo.search.query.profile.QueryProfileRegistry;
+import com.yahoo.schema.RankProfileRegistry;
+import com.yahoo.schema.Schema;
+import com.yahoo.schema.ApplicationBuilder;
+import com.yahoo.schema.AbstractSchemaTestCase;
+import com.yahoo.schema.document.SDDocumentType;
+import com.yahoo.schema.document.SDField;
+import org.junit.Test;
+
+/**
+ * Tests deriving rank for files from search definitions
+ *
+ * @author bratseth
+ */
+public class EmptyRankProfileTestCase extends AbstractSchemaTestCase {
+
+ @Test
+ public void testDeriving() {
+ Schema schema = new Schema("test", MockApplicationPackage.createEmpty());
+ RankProfileRegistry rankProfileRegistry = RankProfileRegistry.createRankProfileRegistryWithBuiltinRankProfiles(schema);
+ SDDocumentType doc = new SDDocumentType("test");
+ schema.addDocument(doc);
+ doc.addField(new SDField(doc, "a", DataType.STRING));
+ SDField field = new SDField(doc, "b", DataType.STRING);
+ field.setLiteralBoost(500);
+ doc.addField(field);
+ doc.addField(new SDField(doc, "c", DataType.STRING));
+
+ schema = ApplicationBuilder.buildFromRawSchema(schema, rankProfileRegistry, new QueryProfileRegistry());
+ new DerivedConfiguration(schema, rankProfileRegistry);
+ }
+
+}
diff --git a/config-model/src/test/java/com/yahoo/schema/derived/ExactMatchTestCase.java b/config-model/src/test/java/com/yahoo/schema/derived/ExactMatchTestCase.java
new file mode 100644
index 00000000000..13a0a8201fb
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/schema/derived/ExactMatchTestCase.java
@@ -0,0 +1,17 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.schema.derived;
+
+import com.yahoo.schema.parser.ParseException;
+import org.junit.Test;
+
+import java.io.IOException;
+
+/**
+ * @author bratseth
+ */
+public class ExactMatchTestCase extends AbstractExportingTestCase {
+ @Test
+ public void testExactString() throws IOException, ParseException {
+ assertCorrectDeriving("exactmatch");
+ }
+}
diff --git a/config-model/src/test/java/com/yahoo/schema/derived/ExportingTestCase.java b/config-model/src/test/java/com/yahoo/schema/derived/ExportingTestCase.java
new file mode 100644
index 00000000000..16a9a459dcb
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/schema/derived/ExportingTestCase.java
@@ -0,0 +1,195 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.schema.derived;
+
+import com.yahoo.config.model.deploy.TestProperties;
+import com.yahoo.schema.ApplicationBuilder;
+import com.yahoo.schema.parser.ParseException;
+import org.junit.Test;
+
+import java.io.IOException;
+
+import static org.junit.Assert.assertEquals;
+
+/**
+ * Tests exporting
+ *
+ * @author bratseth
+ */
+public class ExportingTestCase extends AbstractExportingTestCase {
+
+ @Test
+ public void testIndexInfoLowerCase() throws IOException, ParseException {
+ assertCorrectDeriving("indexinfo_lowercase");
+ }
+
+ @Test
+ public void testPositionArray() throws IOException, ParseException {
+ assertCorrectDeriving("position_array",
+ new TestProperties().setUseV8GeoPositions(true));
+ }
+
+ @Test
+ public void testPositionAttribute() throws IOException, ParseException {
+ assertCorrectDeriving("position_attribute",
+ new TestProperties().setUseV8GeoPositions(true));
+ }
+
+ @Test
+ public void testPositionExtra() throws IOException, ParseException {
+ assertCorrectDeriving("position_extra",
+ new TestProperties().setUseV8GeoPositions(true));
+ }
+
+ @Test
+ public void testPositionNoSummary() throws IOException, ParseException {
+ assertCorrectDeriving("position_nosummary",
+ new TestProperties().setUseV8GeoPositions(true));
+ }
+
+ @Test
+ public void testPositionSummary() throws IOException, ParseException {
+ assertCorrectDeriving("position_summary",
+ new TestProperties().setUseV8GeoPositions(true));
+ }
+
+ @Test
+ public void testUriArray() throws IOException, ParseException {
+ assertCorrectDeriving("uri_array");
+ }
+
+ @Test
+ public void testUriWSet() throws IOException, ParseException {
+ assertCorrectDeriving("uri_wset");
+ }
+
+ @Test
+ public void testMusic() throws IOException, ParseException {
+ assertCorrectDeriving("music");
+ }
+
+ @Test
+ public void testComplexPhysicalExporting() throws IOException, ParseException {
+ assertCorrectDeriving("complex");
+ }
+
+ @Test
+ public void testAttributePrefetch() throws IOException, ParseException {
+ assertCorrectDeriving("attributeprefetch");
+ }
+
+ @Test
+ public void testAdvancedIL() throws IOException, ParseException {
+ assertCorrectDeriving("advanced");
+ }
+
+ @Test
+ public void testEmptyDefaultIndex() throws IOException, ParseException {
+ assertCorrectDeriving("emptydefault");
+ }
+
+ @Test
+ public void testIndexSwitches() throws IOException, ParseException {
+ assertCorrectDeriving("indexswitches");
+ }
+
+ @Test
+ public void testRankTypes() throws IOException, ParseException {
+ assertCorrectDeriving("ranktypes");
+ }
+
+ @Test
+ public void testAttributeRank() throws IOException, ParseException {
+ assertCorrectDeriving("attributerank");
+ }
+
+ @Test
+ public void testNewRank() throws IOException, ParseException {
+ assertCorrectDeriving("newrank");
+ }
+
+ @Test
+ public void testRankingExpression() throws IOException, ParseException {
+ assertCorrectDeriving("rankingexpression");
+ }
+
+ @Test
+ public void testAvoidRenamingRankingExpression() throws IOException, ParseException {
+ assertCorrectDeriving("renamedfeatures", "foo",
+ new TestProperties().setAvoidRenamingSummaryFeatures(true),
+ new TestableDeployLogger());
+ }
+
+ @Test
+ public void testMlr() throws IOException, ParseException {
+ assertCorrectDeriving("mlr");
+ }
+
+ @Test
+ public void testMusic3() throws IOException, ParseException {
+ assertCorrectDeriving("music3");
+ }
+
+ @Test
+ public void testIndexSchema() throws IOException, ParseException {
+ assertCorrectDeriving("indexschema");
+ }
+
+ @Test
+ public void testIndexinfoFieldsets() throws IOException, ParseException {
+ assertCorrectDeriving("indexinfo_fieldsets");
+ }
+
+ @Test
+ public void testStreamingJuniper() throws IOException, ParseException {
+ assertCorrectDeriving("streamingjuniper");
+ }
+
+ @Test
+ public void testPredicateAttribute() throws IOException, ParseException {
+ assertCorrectDeriving("predicate_attribute");
+ }
+
+ @Test
+ public void testTensor() throws IOException, ParseException {
+ assertCorrectDeriving("tensor");
+ }
+
+ @Test
+ public void testTensor2() throws IOException, ParseException {
+ String dir = "src/test/derived/tensor2/";
+ ApplicationBuilder builder = new ApplicationBuilder();
+ builder.addSchemaFile(dir + "first.sd");
+ builder.addSchemaFile(dir + "second.sd");
+ builder.build(true);
+ derive("tensor2", builder, builder.getSchema("second"));
+ assertCorrectConfigFiles("tensor2");
+ }
+
+ @Test
+ public void testHnswIndex() throws IOException, ParseException {
+ assertCorrectDeriving("hnsw_index");
+ }
+
+ @Test
+ public void testRankProfileInheritance() throws IOException, ParseException {
+ assertCorrectDeriving("rankprofileinheritance", "child", new TestableDeployLogger());
+ }
+
+ @Test
+ public void testLanguage() throws IOException, ParseException {
+ TestableDeployLogger logger = new TestableDeployLogger();
+ assertCorrectDeriving("language", logger);
+ assertEquals(0, logger.warnings.size());
+ }
+
+ @Test
+ public void testRankProfileModularity() throws IOException, ParseException {
+ assertCorrectDeriving("rankprofilemodularity");
+ }
+
+ @Test
+ public void testStructAndFieldSet() throws IOException, ParseException {
+ assertCorrectDeriving("structandfieldset");
+ }
+
+}
diff --git a/config-model/src/test/java/com/yahoo/schema/derived/ExpressionsAsArgsTestCase.java b/config-model/src/test/java/com/yahoo/schema/derived/ExpressionsAsArgsTestCase.java
new file mode 100644
index 00000000000..d2020305bc1
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/schema/derived/ExpressionsAsArgsTestCase.java
@@ -0,0 +1,25 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.schema.derived;
+
+import com.yahoo.schema.parser.ParseException;
+import org.junit.Test;
+
+import java.io.IOException;
+
+/**
+ * Tests correct deriving of expressions as arguments to functions.
+ *
+ * @author lesters
+ */
+public class ExpressionsAsArgsTestCase extends AbstractExportingTestCase {
+
+ @Test
+ public void testDocumentDeriving() throws IOException, ParseException {
+ assertCorrectDeriving("function_arguments");
+ assertCorrectDeriving("function_arguments_with_expressions");
+ }
+
+}
+
+
+
diff --git a/config-model/src/test/java/com/yahoo/schema/derived/FieldsetTestCase.java b/config-model/src/test/java/com/yahoo/schema/derived/FieldsetTestCase.java
new file mode 100644
index 00000000000..fdab49c9fff
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/schema/derived/FieldsetTestCase.java
@@ -0,0 +1,16 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.schema.derived;
+
+import com.yahoo.schema.parser.ParseException;
+import org.junit.Test;
+
+import java.io.IOException;
+
+public class FieldsetTestCase extends AbstractExportingTestCase {
+
+ @Test
+ public void testRankProfiles() throws IOException, ParseException {
+ assertCorrectDeriving("fieldset");
+ }
+
+}
diff --git a/config-model/src/test/java/com/yahoo/schema/derived/GeminiTestCase.java b/config-model/src/test/java/com/yahoo/schema/derived/GeminiTestCase.java
new file mode 100644
index 00000000000..5531fb65942
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/schema/derived/GeminiTestCase.java
@@ -0,0 +1,70 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.schema.derived;
+
+import com.yahoo.collections.Pair;
+import com.yahoo.schema.parser.ParseException;
+import org.junit.Test;
+import static org.junit.Assert.assertEquals;
+
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * @author bratseth
+ */
+public class GeminiTestCase extends AbstractExportingTestCase {
+
+ @Test
+ public void testRanking2() throws IOException, ParseException {
+ DerivedConfiguration c = assertCorrectDeriving("gemini2");
+ RawRankProfile p = c.getRankProfileList().getRankProfiles().get("test");
+ Map<String, String> ranking = removePartKeySuffixes(asMap(p.configProperties()));
+ assertEquals("attribute(right)", resolve(lookup("toplevel", ranking), ranking));
+ }
+
+ private Map<String, String> asMap(List<Pair<String, String>> properties) {
+ Map<String, String> map = new HashMap<>();
+ for (Pair<String, String> property : properties)
+ map.put(property.getFirst(), property.getSecond());
+ return map;
+ }
+
+ private Map<String, String> removePartKeySuffixes(Map<String, String> p) {
+ Map<String, String> pWithoutSuffixes = new HashMap<>();
+ for (Map.Entry<String, String> entry : p.entrySet())
+ pWithoutSuffixes.put(removePartSuffix(entry.getKey()), entry.getValue());
+ return pWithoutSuffixes;
+ }
+
+ private String removePartSuffix(String s) {
+ int partIndex = s.indexOf(".part");
+ if (partIndex <= 0) return s;
+ return s.substring(0, partIndex);
+ }
+
+ /**
+ * Recursively resolves references to other ranking expressions - rankingExpression(name) -
+ * and replaces the reference by the expression
+ */
+ private String resolve(String expression, Map<String, String> ranking) {
+ int referenceStartIndex;
+ while ((referenceStartIndex = expression.indexOf("rankingExpression(")) >= 0) {
+ int referenceEndIndex = expression.indexOf(")", referenceStartIndex);
+ expression = expression.substring(0, referenceStartIndex) +
+ resolve(lookup(expression.substring(referenceStartIndex + "rankingExpression(".length(), referenceEndIndex), ranking), ranking) +
+ expression.substring(referenceEndIndex + 1);
+ }
+ return expression;
+ }
+
+ private String lookup(String expressionName, Map<String, String> ranking) {
+ String value = ranking.get("rankingExpression(" + expressionName + ").rankingScript");
+ if (value == null) {
+ return expressionName;
+ }
+ return value;
+ }
+
+}
diff --git a/config-model/src/test/java/com/yahoo/schema/derived/IdTestCase.java b/config-model/src/test/java/com/yahoo/schema/derived/IdTestCase.java
new file mode 100644
index 00000000000..1e57d52e3b0
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/schema/derived/IdTestCase.java
@@ -0,0 +1,49 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.schema.derived;
+
+import com.yahoo.config.model.application.provider.BaseDeployLogger;
+import com.yahoo.config.model.test.MockApplicationPackage;
+import com.yahoo.document.DataType;
+import com.yahoo.schema.RankProfileRegistry;
+import com.yahoo.schema.Schema;
+import com.yahoo.schema.document.SDDocumentType;
+import com.yahoo.schema.document.SDField;
+import com.yahoo.schema.processing.Processing;
+import com.yahoo.vespa.model.container.search.QueryProfiles;
+import org.junit.Test;
+
+import java.util.Set;
+
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+
+/**
+ * Tests that documents ids are treated as they should
+ *
+ * @author bratseth
+ */
+public class IdTestCase extends AbstractExportingTestCase {
+
+ @Test
+ public void testExplicitUpperCaseIdField() {
+ Schema schema = new Schema("test", MockApplicationPackage.createEmpty());
+ SDDocumentType document = new SDDocumentType("test");
+ schema.addDocument(document);
+ SDField uri = new SDField(document, "URI", DataType.URI);
+ uri.parseIndexingScript("{ summary | index }");
+ document.addField(uri);
+
+ new Processing().process(schema, new BaseDeployLogger(), new RankProfileRegistry(), new QueryProfiles(),
+ true, false, Set.of());
+
+ assertNull(document.getField("uri"));
+ assertNull(document.getField("Uri"));
+ assertNotNull(document.getField("URI"));
+ }
+
+ @Test
+ public void testCompleteDeriving() throws Exception {
+ assertCorrectDeriving("id");
+ }
+
+}
diff --git a/config-model/src/test/java/com/yahoo/schema/derived/ImportedFieldsTestCase.java b/config-model/src/test/java/com/yahoo/schema/derived/ImportedFieldsTestCase.java
new file mode 100644
index 00000000000..5578a1a602b
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/schema/derived/ImportedFieldsTestCase.java
@@ -0,0 +1,42 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.schema.derived;
+
+import com.yahoo.config.model.deploy.TestProperties;
+import com.yahoo.schema.parser.ParseException;
+import org.junit.Test;
+
+import java.io.IOException;
+
+/**
+ * @author geirst
+ */
+public class ImportedFieldsTestCase extends AbstractExportingTestCase {
+
+ @Test
+ public void configs_for_imported_fields_are_derived() throws IOException, ParseException {
+ assertCorrectDeriving("importedfields", "child", new TestableDeployLogger());
+ }
+
+ @Test
+ public void configs_for_imported_struct_fields_are_derived() throws IOException, ParseException {
+ assertCorrectDeriving("imported_struct_fields", "child",
+ new TestProperties(),
+ new TestableDeployLogger());
+ }
+
+ @Test
+ public void configs_for_imported_position_field_are_derived() throws IOException, ParseException {
+ assertCorrectDeriving("imported_position_field", "child", new TestableDeployLogger());
+ }
+
+ @Test
+ public void configs_for_imported_position_field_summary_are_derived() throws IOException, ParseException {
+ assertCorrectDeriving("imported_position_field_summary", "child", new TestableDeployLogger());
+ }
+
+ @Test
+ public void derives_configs_for_imported_fields_when_reference_fields_are_inherited() throws IOException, ParseException {
+ assertCorrectDeriving("imported_fields_inherited_reference", "child_c", new TestableDeployLogger());
+ }
+
+}
diff --git a/config-model/src/test/java/com/yahoo/schema/derived/IndexSchemaTestCase.java b/config-model/src/test/java/com/yahoo/schema/derived/IndexSchemaTestCase.java
new file mode 100644
index 00000000000..1f40c6bcb50
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/schema/derived/IndexSchemaTestCase.java
@@ -0,0 +1,208 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.schema.derived;
+
+import com.yahoo.document.DataType;
+import com.yahoo.document.Field;
+import com.yahoo.document.StructDataType;
+import org.junit.Test;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.LinkedList;
+import java.util.List;
+
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+/**
+ * @author Simon Thoresen Hult
+ */
+public class IndexSchemaTestCase {
+
+ @Test
+ public void requireThatPrimitiveIsNotFlattened() {
+ assertFlat(new Field("foo", DataType.BYTE), new Field("foo", DataType.BYTE));
+ assertFlat(new Field("foo", DataType.DOUBLE), new Field("foo", DataType.DOUBLE));
+ assertFlat(new Field("foo", DataType.FLOAT), new Field("foo", DataType.FLOAT));
+ assertFlat(new Field("foo", DataType.INT), new Field("foo", DataType.INT));
+ assertFlat(new Field("foo", DataType.LONG), new Field("foo", DataType.LONG));
+ assertFlat(new Field("foo", DataType.RAW), new Field("foo", DataType.RAW));
+ assertFlat(new Field("foo", DataType.STRING), new Field("foo", DataType.STRING));
+ assertFlat(new Field("foo", DataType.URI), new Field("foo", DataType.URI));
+ assertFlat(new Field("foo", DataType.PREDICATE), new Field("foo", DataType.PREDICATE));
+ }
+
+ @Test
+ public void requireThatArrayOfPrimitiveIsNotFlattened() {
+ assertFlat(new Field("foo", DataType.getArray(DataType.BYTE)),
+ new Field("foo", DataType.getArray(DataType.BYTE)));
+ assertFlat(new Field("foo", DataType.getArray(DataType.DOUBLE)),
+ new Field("foo", DataType.getArray(DataType.DOUBLE)));
+ assertFlat(new Field("foo", DataType.getArray(DataType.FLOAT)),
+ new Field("foo", DataType.getArray(DataType.FLOAT)));
+ assertFlat(new Field("foo", DataType.getArray(DataType.INT)),
+ new Field("foo", DataType.getArray(DataType.INT)));
+ assertFlat(new Field("foo", DataType.getArray(DataType.LONG)),
+ new Field("foo", DataType.getArray(DataType.LONG)));
+ assertFlat(new Field("foo", DataType.getArray(DataType.RAW)),
+ new Field("foo", DataType.getArray(DataType.RAW)));
+ assertFlat(new Field("foo", DataType.getArray(DataType.STRING)),
+ new Field("foo", DataType.getArray(DataType.STRING)));
+ assertFlat(new Field("foo", DataType.getArray(DataType.URI)),
+ new Field("foo", DataType.getArray(DataType.URI)));
+ assertFlat(new Field("foo", DataType.getArray(DataType.PREDICATE)),
+ new Field("foo", DataType.getArray(DataType.PREDICATE)));
+ }
+
+ @Test
+ public void requireThatStructIsFlattened() {
+ StructDataType type = new StructDataType("my_struct");
+ type.addField(new Field("my_byte", DataType.BYTE));
+ type.addField(new Field("my_double", DataType.DOUBLE));
+ type.addField(new Field("my_float", DataType.FLOAT));
+ type.addField(new Field("my_int", DataType.INT));
+ type.addField(new Field("my_long", DataType.LONG));
+ type.addField(new Field("my_raw", DataType.RAW));
+ type.addField(new Field("my_string", DataType.STRING));
+ type.addField(new Field("my_uri", DataType.URI));
+
+ assertFlat(new Field("foo", type),
+ new Field("foo.my_byte", DataType.BYTE),
+ new Field("foo.my_double", DataType.DOUBLE),
+ new Field("foo.my_float", DataType.FLOAT),
+ new Field("foo.my_int", DataType.INT),
+ new Field("foo.my_long", DataType.LONG),
+ new Field("foo.my_raw", DataType.RAW),
+ new Field("foo.my_string", DataType.STRING),
+ new Field("foo.my_uri", DataType.URI));
+ }
+
+ @Test
+ public void requireThatArrayOfStructIsFlattened() {
+ StructDataType type = new StructDataType("my_struct");
+ type.addField(new Field("my_byte", DataType.BYTE));
+ type.addField(new Field("my_double", DataType.DOUBLE));
+ type.addField(new Field("my_float", DataType.FLOAT));
+ type.addField(new Field("my_int", DataType.INT));
+ type.addField(new Field("my_long", DataType.LONG));
+ type.addField(new Field("my_raw", DataType.RAW));
+ type.addField(new Field("my_string", DataType.STRING));
+ type.addField(new Field("my_uri", DataType.URI));
+
+ assertFlat(new Field("foo", DataType.getArray(type)),
+ new Field("foo.my_byte", DataType.getArray(DataType.BYTE)),
+ new Field("foo.my_double", DataType.getArray(DataType.DOUBLE)),
+ new Field("foo.my_float", DataType.getArray(DataType.FLOAT)),
+ new Field("foo.my_int", DataType.getArray(DataType.INT)),
+ new Field("foo.my_long", DataType.getArray(DataType.LONG)),
+ new Field("foo.my_raw", DataType.getArray(DataType.RAW)),
+ new Field("foo.my_string", DataType.getArray(DataType.STRING)),
+ new Field("foo.my_uri", DataType.getArray(DataType.URI)));
+ }
+
+ @Test
+ public void requireThatArrayOfArrayOfStructIsFlattened() {
+ StructDataType type = new StructDataType("my_struct");
+ type.addField(new Field("my_byte", DataType.BYTE));
+ type.addField(new Field("my_double", DataType.DOUBLE));
+ type.addField(new Field("my_float", DataType.FLOAT));
+ type.addField(new Field("my_int", DataType.INT));
+ type.addField(new Field("my_long", DataType.LONG));
+ type.addField(new Field("my_raw", DataType.RAW));
+ type.addField(new Field("my_string", DataType.STRING));
+ type.addField(new Field("my_uri", DataType.URI));
+
+ assertFlat(new Field("foo", DataType.getArray(DataType.getArray(type))),
+ new Field("foo.my_byte", DataType.getArray(DataType.getArray(DataType.BYTE))),
+ new Field("foo.my_double", DataType.getArray(DataType.getArray(DataType.DOUBLE))),
+ new Field("foo.my_float", DataType.getArray(DataType.getArray(DataType.FLOAT))),
+ new Field("foo.my_int", DataType.getArray(DataType.getArray(DataType.INT))),
+ new Field("foo.my_long", DataType.getArray(DataType.getArray(DataType.LONG))),
+ new Field("foo.my_raw", DataType.getArray(DataType.getArray(DataType.RAW))),
+ new Field("foo.my_string", DataType.getArray(DataType.getArray(DataType.STRING))),
+ new Field("foo.my_uri", DataType.getArray(DataType.getArray(DataType.URI))));
+ }
+
+ @Test
+ public void requireThatStructWithArrayFieldIsFlattened() {
+ StructDataType type = new StructDataType("my_struct");
+ type.addField(new Field("my_byte", DataType.getArray(DataType.BYTE)));
+ type.addField(new Field("my_double", DataType.getArray(DataType.DOUBLE)));
+ type.addField(new Field("my_float", DataType.getArray(DataType.FLOAT)));
+ type.addField(new Field("my_int", DataType.getArray(DataType.INT)));
+ type.addField(new Field("my_long", DataType.getArray(DataType.LONG)));
+ type.addField(new Field("my_raw", DataType.getArray(DataType.RAW)));
+ type.addField(new Field("my_string", DataType.getArray(DataType.STRING)));
+ type.addField(new Field("my_uri", DataType.getArray(DataType.URI)));
+
+ assertFlat(new Field("foo", type),
+ new Field("foo.my_byte", DataType.getArray(DataType.BYTE)),
+ new Field("foo.my_double", DataType.getArray(DataType.DOUBLE)),
+ new Field("foo.my_float", DataType.getArray(DataType.FLOAT)),
+ new Field("foo.my_int", DataType.getArray(DataType.INT)),
+ new Field("foo.my_long", DataType.getArray(DataType.LONG)),
+ new Field("foo.my_raw", DataType.getArray(DataType.RAW)),
+ new Field("foo.my_string", DataType.getArray(DataType.STRING)),
+ new Field("foo.my_uri", DataType.getArray(DataType.URI)));
+ }
+
+ @Test
+ public void requireThatStructWithArrayOfArrayFieldIsFlattened() {
+ StructDataType type = new StructDataType("my_struct");
+ type.addField(new Field("my_byte", DataType.getArray(DataType.getArray(DataType.BYTE))));
+ type.addField(new Field("my_double", DataType.getArray(DataType.getArray(DataType.DOUBLE))));
+ type.addField(new Field("my_float", DataType.getArray(DataType.getArray(DataType.FLOAT))));
+ type.addField(new Field("my_int", DataType.getArray(DataType.getArray(DataType.INT))));
+ type.addField(new Field("my_long", DataType.getArray(DataType.getArray(DataType.LONG))));
+ type.addField(new Field("my_raw", DataType.getArray(DataType.getArray(DataType.RAW))));
+ type.addField(new Field("my_string", DataType.getArray(DataType.getArray(DataType.STRING))));
+ type.addField(new Field("my_uri", DataType.getArray(DataType.getArray(DataType.URI))));
+
+ assertFlat(new Field("foo", type),
+ new Field("foo.my_byte", DataType.getArray(DataType.getArray(DataType.BYTE))),
+ new Field("foo.my_double", DataType.getArray(DataType.getArray(DataType.DOUBLE))),
+ new Field("foo.my_float", DataType.getArray(DataType.getArray(DataType.FLOAT))),
+ new Field("foo.my_int", DataType.getArray(DataType.getArray(DataType.INT))),
+ new Field("foo.my_long", DataType.getArray(DataType.getArray(DataType.LONG))),
+ new Field("foo.my_raw", DataType.getArray(DataType.getArray(DataType.RAW))),
+ new Field("foo.my_string", DataType.getArray(DataType.getArray(DataType.STRING))),
+ new Field("foo.my_uri", DataType.getArray(DataType.getArray(DataType.URI))));
+ }
+
+ @Test
+ public void requireThatArrayOfStructWithArrayFieldIsFlattened() {
+ StructDataType type = new StructDataType("my_struct");
+ type.addField(new Field("my_byte", DataType.getArray(DataType.BYTE)));
+ type.addField(new Field("my_double", DataType.getArray(DataType.DOUBLE)));
+ type.addField(new Field("my_float", DataType.getArray(DataType.FLOAT)));
+ type.addField(new Field("my_int", DataType.getArray(DataType.INT)));
+ type.addField(new Field("my_long", DataType.getArray(DataType.LONG)));
+ type.addField(new Field("my_raw", DataType.getArray(DataType.RAW)));
+ type.addField(new Field("my_string", DataType.getArray(DataType.STRING)));
+ type.addField(new Field("my_uri", DataType.getArray(DataType.URI)));
+
+ assertFlat(new Field("foo", DataType.getArray(type)),
+ new Field("foo.my_byte", DataType.getArray(DataType.getArray(DataType.BYTE))),
+ new Field("foo.my_double", DataType.getArray(DataType.getArray(DataType.DOUBLE))),
+ new Field("foo.my_float", DataType.getArray(DataType.getArray(DataType.FLOAT))),
+ new Field("foo.my_int", DataType.getArray(DataType.getArray(DataType.INT))),
+ new Field("foo.my_long", DataType.getArray(DataType.getArray(DataType.LONG))),
+ new Field("foo.my_raw", DataType.getArray(DataType.getArray(DataType.RAW))),
+ new Field("foo.my_string", DataType.getArray(DataType.getArray(DataType.STRING))),
+ new Field("foo.my_uri", DataType.getArray(DataType.getArray(DataType.URI))));
+ }
+
+ private static void assertFlat(Field fieldToFlatten, Field... expectedFields) {
+ List<Field> actual = new LinkedList<>(IndexSchema.flattenField(fieldToFlatten));
+ List<Field> expected = new LinkedList<>(Arrays.asList(expectedFields));
+ Collections.sort(actual);
+ Collections.sort(expected);
+ for (Field field : actual) {
+ if (!expected.remove(field)) {
+ fail("Unexpected field: " + field);
+ }
+ }
+ assertTrue("Missing fields: " + expected, expected.isEmpty());
+ }
+
+}
diff --git a/config-model/src/test/java/com/yahoo/schema/derived/InheritanceTestCase.java b/config-model/src/test/java/com/yahoo/schema/derived/InheritanceTestCase.java
new file mode 100644
index 00000000000..1c433237fd8
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/schema/derived/InheritanceTestCase.java
@@ -0,0 +1,192 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.schema.derived;
+
+import com.yahoo.config.model.deploy.TestProperties;
+import com.yahoo.config.model.test.MockApplicationPackage;
+import com.yahoo.document.DataType;
+import com.yahoo.document.config.DocumentmanagerConfig;
+import com.yahoo.schema.Index;
+import com.yahoo.schema.Schema;
+import com.yahoo.schema.ApplicationBuilder;
+import com.yahoo.schema.document.SDDocumentType;
+import com.yahoo.schema.document.SDField;
+import com.yahoo.schema.parser.ParseException;
+import com.yahoo.vespa.configmodel.producers.DocumentManager;
+import org.junit.Rule;
+import org.junit.Test;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.List;
+
+import org.junit.rules.TemporaryFolder;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+
+/**
+ * Tests inheritance
+ *
+ * @author bratseth
+ */
+public class InheritanceTestCase extends AbstractExportingTestCase {
+
+ @Rule
+ public TemporaryFolder tmpDir = new TemporaryFolder();
+
+ @Test
+ public void requireThatIndexedStructFieldCanBeInherited() throws IOException, ParseException {
+ String dir = "src/test/derived/inheritstruct/";
+ ApplicationBuilder builder = new ApplicationBuilder();
+ builder.addSchemaFile(dir + "parent.sd");
+ builder.addSchemaFile(dir + "child.sd");
+ builder.build(true);
+ derive("inheritstruct", builder, builder.getSchema("child"));
+ assertCorrectConfigFiles("inheritstruct");
+ }
+
+ @Test
+ public void requireThatInheritFromNullIsCaught() throws IOException, ParseException {
+ try {
+ assertCorrectDeriving("inheritfromnull");
+ } catch (IllegalArgumentException e) {
+ assertEquals("document inheritfromnull inherits from unavailable document foo", e.getMessage());
+ }
+ }
+
+ @Test
+ public void requireThatStructTypesAreInheritedThroughDiamond() throws IOException, ParseException {
+ String dir = "src/test/derived/inheritdiamond/";
+ {
+ ApplicationBuilder builder = new ApplicationBuilder();
+ builder.addSchemaFile(dir + "grandparent.sd");
+ builder.addSchemaFile(dir + "mother.sd");
+ builder.addSchemaFile(dir + "father.sd");
+ builder.addSchemaFile(dir + "child.sd");
+ builder.build(true);
+ derive("inheritdiamond", builder, builder.getSchema("child"));
+ assertCorrectConfigFiles("inheritdiamond");
+ }
+ List<String> files = Arrays.asList("grandparent.sd", "mother.sd", "father.sd", "child.sd");
+ File outDir = tmpDir.newFolder("out");
+ for (int startIdx = 0; startIdx < files.size(); ++startIdx) {
+ var builder = new ApplicationBuilder(new TestProperties());
+ for (int fileIdx = startIdx; fileIdx < startIdx + files.size(); ++fileIdx) {
+ String fileName = files.get(fileIdx % files.size());
+ builder.addSchemaFile(dir + fileName);
+ }
+ builder.build(true);
+ DocumentmanagerConfig.Builder b = new DocumentmanagerConfig.Builder();
+ DerivedConfiguration.exportDocuments(new DocumentManager().
+ produce(builder.getModel(), b), outDir.getPath());
+ DocumentmanagerConfig dc = b.build();
+ assertEquals(5, dc.doctype().size());
+
+ assertNull(structType("child.body", dc));
+ var childHeader = structType("child.header", dc);
+ assertEquals(childHeader.field(0).name(), "foo");
+ assertEquals(childHeader.field(1).name(), "bar");
+ assertEquals(childHeader.field(2).name(), "baz");
+ assertEquals(childHeader.field(3).name(), "cox");
+
+ var root = documentType("document", dc);
+ var child = documentType("child", dc);
+ var mother = documentType("mother", dc);
+ var father = documentType("father", dc);
+ var grandparent = documentType("grandparent", dc);
+
+ assertEquals(child.inherits(0).idx(), root.idx());
+ assertEquals(child.inherits(1).idx(), mother.idx());
+ assertEquals(child.inherits(2).idx(), father.idx());
+ assertEquals(mother.inherits(0).idx(), root.idx());
+ assertEquals(mother.inherits(1).idx(), grandparent.idx());
+ }
+ }
+
+ private DocumentmanagerConfig.Doctype.Structtype structType(String name, DocumentmanagerConfig dc) {
+ for (var dt : dc.doctype()) {
+ for (var st : dt.structtype()) {
+ if (name.equals(st.name())) return st;
+ }
+ }
+ return null;
+ }
+
+ private DocumentmanagerConfig.Doctype documentType(String name, DocumentmanagerConfig dc) {
+ for (var dot : dc.doctype()) {
+ if (name.equals(dot.name())) return dot;
+ }
+ return null;
+ }
+
+ @Test
+ public void requireThatStructTypesAreInheritedFromParent() throws IOException, ParseException {
+ String dir = "src/test/derived/inheritfromparent/";
+ ApplicationBuilder builder = new ApplicationBuilder();
+ builder.addSchemaFile(dir + "parent.sd");
+ builder.addSchemaFile(dir + "child.sd");
+ builder.build(true);
+ derive("inheritfromparent", builder, builder.getSchema("child"));
+ assertCorrectConfigFiles("inheritfromparent");
+ }
+
+ @Test
+ public void requireThatStructTypesAreInheritedFromGrandParent() throws IOException, ParseException {
+ String dir = "src/test/derived/inheritfromgrandparent/";
+ ApplicationBuilder builder = new ApplicationBuilder();
+ builder.addSchemaFile(dir + "grandparent.sd");
+ builder.addSchemaFile(dir + "parent.sd");
+ builder.addSchemaFile(dir + "child.sd");
+ builder.build(true);
+ derive("inheritfromgrandparent", builder, builder.getSchema("child"));
+ assertCorrectConfigFiles("inheritfromgrandparent");
+ }
+
+ @Test
+ public void testInheritance() throws IOException, ParseException {
+ String dir = "src/test/derived/inheritance/";
+ ApplicationBuilder builder = new ApplicationBuilder();
+ builder.addSchemaFile(dir + "grandparent.sd");
+ builder.addSchemaFile(dir + "father.sd");
+ builder.addSchemaFile(dir + "mother.sd");
+ builder.addSchemaFile(dir + "child.sd");
+ builder.build(true);
+ derive("inheritance", builder, builder.getSchema("child"));
+ assertCorrectConfigFiles("inheritance");
+ }
+
+ @Test
+ public void testIndexSettingInheritance() {
+ SDDocumentType parent = new SDDocumentType("parent");
+ Schema parentSchema = new Schema("parent", MockApplicationPackage.createEmpty());
+ parentSchema.addDocument(parent);
+ SDField prefixed = parent.addField("prefixed", DataType.STRING);
+ prefixed.parseIndexingScript("{ index }");
+ prefixed.addIndex(new Index("prefixed", true));
+
+ SDDocumentType child = new SDDocumentType("child");
+ child.inherit(parent);
+ Schema childSchema = new Schema("child", MockApplicationPackage.createEmpty());
+ childSchema.addDocument(child);
+
+ prefixed = (SDField)child.getField("prefixed");
+ assertNotNull(prefixed);
+ assertEquals(new Index("prefixed", true), childSchema.getIndex("prefixed"));
+ }
+
+ @Test
+ public void testInheritStructDiamondNew() throws IOException, ParseException {
+ String dir = "src/test/derived/declstruct/";
+ List<String> files = Arrays.asList("common.sd", "foo.sd", "bar.sd", "foobar.sd");
+ var builder = new ApplicationBuilder(new TestProperties());
+ for (String fileName : files) {
+ builder.addSchemaFile(dir + fileName);
+ }
+ builder.build(true);
+ derive("declstruct", builder, builder.getSchema("foobar"));
+ assertCorrectConfigFiles("declstruct");
+ }
+
+}
diff --git a/config-model/src/test/java/com/yahoo/schema/derived/IntegerAttributeToStringIndexTestCase.java b/config-model/src/test/java/com/yahoo/schema/derived/IntegerAttributeToStringIndexTestCase.java
new file mode 100644
index 00000000000..b5f222673ab
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/schema/derived/IntegerAttributeToStringIndexTestCase.java
@@ -0,0 +1,17 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.schema.derived;
+
+import com.yahoo.schema.parser.ParseException;
+import org.junit.Test;
+
+import java.io.IOException;
+
+/**
+ * @author bratseth
+ */
+public class IntegerAttributeToStringIndexTestCase extends AbstractExportingTestCase {
+ @Test
+ public void testIt() throws IOException, ParseException {
+ assertCorrectDeriving("integerattributetostringindex");
+ }
+}
diff --git a/config-model/src/test/java/com/yahoo/schema/derived/LiteralBoostTestCase.java b/config-model/src/test/java/com/yahoo/schema/derived/LiteralBoostTestCase.java
new file mode 100644
index 00000000000..c5090c88a1b
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/schema/derived/LiteralBoostTestCase.java
@@ -0,0 +1,113 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.schema.derived;
+
+import com.yahoo.config.model.application.provider.BaseDeployLogger;
+import com.yahoo.config.model.test.MockApplicationPackage;
+import com.yahoo.document.DataType;
+import com.yahoo.search.query.profile.QueryProfileRegistry;
+import com.yahoo.schema.RankProfile;
+import com.yahoo.schema.RankProfileRegistry;
+import com.yahoo.schema.Schema;
+import com.yahoo.schema.ApplicationBuilder;
+import com.yahoo.schema.document.SDDocumentType;
+import com.yahoo.schema.document.SDField;
+import com.yahoo.schema.processing.Processing;
+import com.yahoo.vespa.model.container.search.QueryProfiles;
+import org.junit.Test;
+
+import java.util.Arrays;
+import java.util.Set;
+
+import static com.yahoo.schema.processing.AssertIndexingScript.assertIndexing;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * @author bratseth
+ */
+public class LiteralBoostTestCase extends AbstractExportingTestCase {
+
+ /**
+ * Tests adding of literal boost constructs
+ */
+ @Test
+ public void testLiteralBoost() {
+ Schema schema = new Schema("literalboost", MockApplicationPackage.createEmpty());
+ RankProfileRegistry rankProfileRegistry = RankProfileRegistry.createRankProfileRegistryWithBuiltinRankProfiles(schema);
+ SDDocumentType document = new SDDocumentType("literalboost");
+ schema.addDocument(document);
+ SDField field1 = document.addField("a", DataType.STRING);
+ field1.parseIndexingScript("{ index }");
+ field1.setLiteralBoost(20);
+ RankProfile other = new RankProfile("other", schema, rankProfileRegistry);
+ rankProfileRegistry.add(other);
+ other.addRankSetting(new RankProfile.RankSetting("a", RankProfile.RankSetting.Type.LITERALBOOST, 333));
+
+ new Processing().process(schema, new BaseDeployLogger(), rankProfileRegistry, new QueryProfiles(),
+ true, false, Set.of());
+ DerivedConfiguration derived = new DerivedConfiguration(schema, rankProfileRegistry);
+
+ // Check attribute fields
+ derived.getAttributeFields(); // TODO: assert content
+
+ // Check il script addition
+ assertIndexing(Arrays.asList("clear_state | guard { input a | tokenize normalize stem:\"BEST\" | index a; }",
+ "clear_state | guard { input a | tokenize | index a_literal; }"),
+ schema);
+
+ // Check index info addition
+ IndexInfo indexInfo = derived.getIndexInfo();
+ assertTrue(indexInfo.hasCommand("a", "literal-boost"));
+ }
+
+ /**
+ * Tests adding a literal boost in a non-default rank profile only
+ */
+ @Test
+ public void testNonDefaultRankLiteralBoost() {
+ Schema schema = new Schema("literalboost", MockApplicationPackage.createEmpty());
+ RankProfileRegistry rankProfileRegistry = RankProfileRegistry.createRankProfileRegistryWithBuiltinRankProfiles(schema);
+ SDDocumentType document = new SDDocumentType("literalboost");
+ schema.addDocument(document);
+ SDField field1 = document.addField("a", DataType.STRING);
+ field1.parseIndexingScript("{ index }");
+ RankProfile other = new RankProfile("other", schema, rankProfileRegistry);
+ rankProfileRegistry.add(other);
+ other.addRankSetting(new RankProfile.RankSetting("a", RankProfile.RankSetting.Type.LITERALBOOST, 333));
+
+ schema = ApplicationBuilder.buildFromRawSchema(schema, rankProfileRegistry, new QueryProfileRegistry());
+ DerivedConfiguration derived = new DerivedConfiguration(schema, rankProfileRegistry);
+
+ // Check il script addition
+ assertIndexing(Arrays.asList("clear_state | guard { input a | tokenize normalize stem:\"BEST\" | index a; }",
+ "clear_state | guard { input a | tokenize | index a_literal; }"),
+ schema);
+
+ // Check index info addition
+ IndexInfo indexInfo = derived.getIndexInfo();
+ assertTrue(indexInfo.hasCommand("a","literal-boost"));
+ }
+
+ /** Tests literal boosts in two fields going to the same index */
+ @Test
+ public void testTwoLiteralBoostFields() {
+ Schema schema = new Schema("msb", MockApplicationPackage.createEmpty());
+ RankProfileRegistry rankProfileRegistry = RankProfileRegistry.createRankProfileRegistryWithBuiltinRankProfiles(schema);
+ SDDocumentType document = new SDDocumentType("msb");
+ schema.addDocument(document);
+ SDField field1 = document.addField("title", DataType.STRING);
+ field1.parseIndexingScript("{ summary | index }");
+ field1.setLiteralBoost(20);
+ SDField field2 = document.addField("body", DataType.STRING);
+ field2.parseIndexingScript("{ summary | index }");
+ field2.setLiteralBoost(20);
+
+ schema = ApplicationBuilder.buildFromRawSchema(schema, rankProfileRegistry, new QueryProfileRegistry());
+ new DerivedConfiguration(schema, rankProfileRegistry);
+ assertIndexing(Arrays.asList("clear_state | guard { input title | tokenize normalize stem:\"BEST\" | summary title | index title; }",
+ "clear_state | guard { input body | tokenize normalize stem:\"BEST\" | summary body | index body; }",
+ "clear_state | guard { input title | tokenize | index title_literal; }",
+ "clear_state | guard { input body | tokenize | index body_literal; }"),
+ schema);
+ }
+
+}
diff --git a/config-model/src/test/java/com/yahoo/schema/derived/LowercaseTestCase.java b/config-model/src/test/java/com/yahoo/schema/derived/LowercaseTestCase.java
new file mode 100644
index 00000000000..f234a9cc324
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/schema/derived/LowercaseTestCase.java
@@ -0,0 +1,19 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.schema.derived;
+
+import com.yahoo.schema.parser.ParseException;
+import org.junit.Test;
+
+import java.io.IOException;
+
+/**
+ * @author bratseth
+ */
+public class LowercaseTestCase extends AbstractExportingTestCase {
+
+ @Test
+ public void testDeriving() throws IOException, ParseException {
+ assertCorrectDeriving("lowercase");
+ }
+
+}
diff --git a/config-model/src/test/java/com/yahoo/schema/derived/MailTestCase.java b/config-model/src/test/java/com/yahoo/schema/derived/MailTestCase.java
new file mode 100644
index 00000000000..c48c44554ed
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/schema/derived/MailTestCase.java
@@ -0,0 +1,24 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.schema.derived;
+
+import com.yahoo.schema.ApplicationBuilder;
+import com.yahoo.schema.parser.ParseException;
+import org.junit.Test;
+import java.io.IOException;
+
+/**
+ * Tests streaming configuration deriving
+ *
+ * @author bratseth
+ */
+public class MailTestCase extends AbstractExportingTestCase {
+
+ @Test
+ public void testMail() throws IOException, ParseException {
+ String dir = "src/test/derived/mail/";
+ ApplicationBuilder sb = new ApplicationBuilder();
+ sb.addSchemaFile(dir + "mail.sd");
+ assertCorrectDeriving(sb, dir, new TestableDeployLogger());
+ }
+
+}
diff --git a/config-model/src/test/java/com/yahoo/schema/derived/MatchSettingsResolvingTestCase.java b/config-model/src/test/java/com/yahoo/schema/derived/MatchSettingsResolvingTestCase.java
new file mode 100755
index 00000000000..a7df862134a
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/schema/derived/MatchSettingsResolvingTestCase.java
@@ -0,0 +1,63 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.schema.derived;
+
+import com.yahoo.config.model.deploy.TestProperties;
+import com.yahoo.schema.parser.ParseException;
+import org.junit.Test;
+
+import java.io.IOException;
+
+/**
+ * @author arnej
+ */
+public class MatchSettingsResolvingTestCase extends AbstractExportingTestCase {
+
+ @Test
+ public void testSimpleDefaults() throws IOException, ParseException {
+ assertCorrectDeriving("matchsettings_simple_def", new TestProperties());
+ }
+
+ @Test
+ public void testSimpleWithStructSettings() throws IOException, ParseException {
+ assertCorrectDeriving("matchsettings_simple_wss",
+ new TestProperties());
+ }
+
+ @Test
+ public void testSimpleWithFieldSettings() throws IOException, ParseException {
+ assertCorrectDeriving("matchsettings_simple_wfs", new TestProperties());
+ }
+
+ @Test
+ public void testSimpleStructAndFieldSettings() throws IOException, ParseException {
+ assertCorrectDeriving("matchsettings_simple_wss_wfs", new TestProperties());
+ }
+
+ @Test
+ public void testMapDefaults() throws IOException, ParseException {
+ assertCorrectDeriving("matchsettings_map_def", new TestProperties());
+ }
+
+ @Test
+ public void testMapWithStructSettings() throws IOException, ParseException {
+ assertCorrectDeriving("matchsettings_map_wss", new TestProperties());
+ }
+
+ @Test
+ public void testMapWithFieldSettings() throws IOException, ParseException {
+ assertCorrectDeriving("matchsettings_map_wfs", new TestProperties());
+ }
+
+ @Test
+ public void testMapAfter() throws IOException, ParseException {
+ assertCorrectDeriving("matchsettings_map_after", new TestProperties());
+ }
+
+
+ @Test
+ public void testMapInStruct() throws IOException, ParseException {
+ assertCorrectDeriving("matchsettings_map_in_struct", new TestProperties());
+ }
+
+
+}
diff --git a/config-model/src/test/java/com/yahoo/schema/derived/MultiStructTestCase.java b/config-model/src/test/java/com/yahoo/schema/derived/MultiStructTestCase.java
new file mode 100644
index 00000000000..66b3698b38c
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/schema/derived/MultiStructTestCase.java
@@ -0,0 +1,27 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.schema.derived;
+
+import com.yahoo.config.model.deploy.TestProperties;
+import com.yahoo.config.model.application.provider.MockFileRegistry;
+import com.yahoo.schema.ApplicationBuilder;
+import org.junit.Test;
+
+/**
+ * Tests deriving a configuration with structs in multiple .sd files
+ *
+ * @author arnej
+ */
+public class MultiStructTestCase extends AbstractExportingTestCase {
+
+ @Test
+ public void testDocTypeConfigs() throws Exception {
+ var logger = new TestableDeployLogger();
+ var props = new TestProperties();
+ ApplicationBuilder builder = ApplicationBuilder.createFromDirectory
+ ("src/test/derived/multi_struct/", new MockFileRegistry(), logger, props);
+ derive("multi_struct", builder, builder.getSchema("shop"));
+ assertCorrectConfigFiles("multi_struct");
+ }
+
+}
+
diff --git a/config-model/src/test/java/com/yahoo/schema/derived/MultipleSummariesTestCase.java b/config-model/src/test/java/com/yahoo/schema/derived/MultipleSummariesTestCase.java
new file mode 100644
index 00000000000..d434673e43a
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/schema/derived/MultipleSummariesTestCase.java
@@ -0,0 +1,22 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.schema.derived;
+
+import com.yahoo.config.model.deploy.TestProperties;
+import com.yahoo.schema.parser.ParseException;
+import org.junit.Test;
+
+import java.io.IOException;
+
+/**
+ * Tests deriving a configuration with multiple summaries
+ *
+ * @author bratseth
+ */
+public class MultipleSummariesTestCase extends AbstractExportingTestCase {
+
+ @Test
+ public void testMultipleSummariesNew() throws IOException, ParseException {
+ assertCorrectDeriving("multiplesummaries", new TestProperties());
+ }
+
+}
diff --git a/config-model/src/test/java/com/yahoo/schema/derived/NameCollisionTestCase.java b/config-model/src/test/java/com/yahoo/schema/derived/NameCollisionTestCase.java
new file mode 100644
index 00000000000..689ff9814cc
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/schema/derived/NameCollisionTestCase.java
@@ -0,0 +1,27 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+
+package com.yahoo.schema.derived;
+
+import com.yahoo.config.model.deploy.TestProperties;
+import com.yahoo.document.DocumentTypeManager;
+
+import org.junit.Test;
+import static org.junit.Assert.assertThrows;
+
+/**
+ * Verifies that a struct in a document type is preferred over another document type
+ * of the same name.
+ *
+ * @author bratseth
+ */
+public class NameCollisionTestCase extends AbstractExportingTestCase {
+
+ @Test
+ public void testNameCollision() throws Exception {
+ assertCorrectDeriving("namecollision", "collisionstruct",
+ new TestProperties(),
+ new TestableDeployLogger());
+ DocumentTypeManager.fromFile("temp/namecollision/documentmanager.cfg");
+ }
+
+}
diff --git a/config-model/src/test/java/com/yahoo/schema/derived/NativeRankTypeDefinitionsTestCase.java b/config-model/src/test/java/com/yahoo/schema/derived/NativeRankTypeDefinitionsTestCase.java
new file mode 100644
index 00000000000..f628420556a
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/schema/derived/NativeRankTypeDefinitionsTestCase.java
@@ -0,0 +1,92 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.schema.derived;
+
+import com.yahoo.schema.AbstractSchemaTestCase;
+import com.yahoo.schema.document.RankType;
+import org.junit.Test;
+
+import java.util.Iterator;
+
+import static org.junit.Assert.*;
+
+/**
+ * Testing stuff related to native rank type definitions
+ *
+ * @author geirst
+ */
+public class NativeRankTypeDefinitionsTestCase extends AbstractSchemaTestCase {
+ @Test
+ public void testTables() {
+ assertEquals(NativeTable.Type.FIRST_OCCURRENCE.getName(), "firstOccurrenceTable");
+ assertEquals(NativeTable.Type.OCCURRENCE_COUNT.getName(), "occurrenceCountTable");
+ assertEquals(NativeTable.Type.PROXIMITY.getName(), "proximityTable");
+ assertEquals(NativeTable.Type.REVERSE_PROXIMITY.getName(), "reverseProximityTable");
+ assertEquals(NativeTable.Type.WEIGHT.getName(), "weightTable");
+ }
+ @Test
+ public void testDefinitions() {
+ NativeRankTypeDefinitionSet defs = new NativeRankTypeDefinitionSet("default");
+
+ NativeRankTypeDefinition rank;
+ Iterator<NativeTable> tables;
+
+ assertEquals(4, defs.types().size());
+
+ {
+ rank = defs.getRankTypeDefinition(RankType.EMPTY);
+ assertNotNull(rank);
+ assertEquals(RankType.EMPTY, rank.getType());
+ tables = rank.rankSettingIterator();
+ assertEquals(new NativeTable(NativeTable.Type.FIRST_OCCURRENCE, "linear(0,0)"), tables.next());
+ assertEquals(new NativeTable(NativeTable.Type.OCCURRENCE_COUNT, "linear(0,0)"), tables.next());
+ assertEquals(new NativeTable(NativeTable.Type.PROXIMITY, "linear(0,0)"), tables.next());
+ assertEquals(new NativeTable(NativeTable.Type.REVERSE_PROXIMITY, "linear(0,0)"), tables.next());
+ assertEquals(new NativeTable(NativeTable.Type.WEIGHT, "linear(0,0)"), tables.next());
+ assertFalse(tables.hasNext());
+ }
+
+ {
+ rank = defs.getRankTypeDefinition(RankType.ABOUT);
+ assertNotNull(rank);
+ assertEquals(RankType.ABOUT, rank.getType());
+ tables = rank.rankSettingIterator();
+ assertEquals(new NativeTable(NativeTable.Type.FIRST_OCCURRENCE, "expdecay(8000,12.50)"), tables.next());
+ assertEquals(new NativeTable(NativeTable.Type.OCCURRENCE_COUNT, "loggrowth(1500,4000,19)"), tables.next());
+ assertEquals(new NativeTable(NativeTable.Type.PROXIMITY, "expdecay(500,3)"), tables.next());
+ assertEquals(new NativeTable(NativeTable.Type.REVERSE_PROXIMITY, "expdecay(400,3)"), tables.next());
+ assertEquals(new NativeTable(NativeTable.Type.WEIGHT, "linear(1,0)"), tables.next());
+ assertFalse(tables.hasNext());
+ }
+
+ {
+ rank = defs.getRankTypeDefinition(RankType.IDENTITY);
+ assertNotNull(rank);
+ assertEquals(RankType.IDENTITY, rank.getType());
+ tables = rank.rankSettingIterator();
+ assertEquals(new NativeTable(NativeTable.Type.FIRST_OCCURRENCE, "expdecay(100,12.50)"), tables.next());
+ assertEquals(new NativeTable(NativeTable.Type.OCCURRENCE_COUNT, "loggrowth(1500,4000,19)"), tables.next());
+ assertEquals(new NativeTable(NativeTable.Type.PROXIMITY, "expdecay(5000,3)"), tables.next());
+ assertEquals(new NativeTable(NativeTable.Type.REVERSE_PROXIMITY, "expdecay(3000,3)"), tables.next());
+ assertEquals(new NativeTable(NativeTable.Type.WEIGHT, "linear(1,0)"), tables.next());
+ assertFalse(tables.hasNext());
+ }
+
+ {
+ rank = defs.getRankTypeDefinition(RankType.TAGS);
+ assertNotNull(rank);
+ assertEquals(RankType.TAGS, rank.getType());
+ tables = rank.rankSettingIterator();
+ assertEquals(new NativeTable(NativeTable.Type.FIRST_OCCURRENCE, "expdecay(8000,12.50)"), tables.next());
+ assertEquals(new NativeTable(NativeTable.Type.OCCURRENCE_COUNT, "loggrowth(1500,4000,19)"), tables.next());
+ assertEquals(new NativeTable(NativeTable.Type.PROXIMITY, "expdecay(500,3)"), tables.next());
+ assertEquals(new NativeTable(NativeTable.Type.REVERSE_PROXIMITY, "expdecay(400,3)"), tables.next());
+ assertEquals(new NativeTable(NativeTable.Type.WEIGHT, "loggrowth(38,50,1)"), tables.next());
+ assertFalse(tables.hasNext());
+ }
+
+ {
+ assertEquals(RankType.ABOUT, defs.getRankTypeDefinition(RankType.DEFAULT).getType());
+ }
+ }
+
+}
diff --git a/config-model/src/test/java/com/yahoo/schema/derived/NearestNeighborTestCase.java b/config-model/src/test/java/com/yahoo/schema/derived/NearestNeighborTestCase.java
new file mode 100644
index 00000000000..baee7bec2a2
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/schema/derived/NearestNeighborTestCase.java
@@ -0,0 +1,39 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.schema.derived;
+
+import com.yahoo.component.ComponentId;
+import com.yahoo.search.Query;
+import com.yahoo.search.query.profile.compiled.CompiledQueryProfileRegistry;
+import com.yahoo.search.query.profile.config.QueryProfileConfigurer;
+import com.yahoo.schema.parser.ParseException;
+import com.yahoo.vespa.model.container.search.QueryProfiles;
+import org.junit.Test;
+
+import java.io.IOException;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
+
+public class NearestNeighborTestCase extends AbstractExportingTestCase {
+
+ @Test
+ public void testNearestNeighbor() throws IOException, ParseException {
+ try {
+ ComponentId.resetGlobalCountersForTests();
+ DerivedConfiguration c = assertCorrectDeriving("nearestneighbor");
+
+ CompiledQueryProfileRegistry queryProfiles =
+ QueryProfileConfigurer.createFromConfig(new QueryProfiles(c.getQueryProfiles(), (level, message) -> {}).getConfig()).compile();
+ Query q = new Query("?ranking.features.query(q_vec)=[1,2,3,4,5,6]", // length is 6, not 5
+ queryProfiles.getComponent("default"));
+ fail("This should fail when q_vec is parsed as a tensor");
+ } catch (IllegalArgumentException e) {
+ // success
+ assertEquals("Could not set 'ranking.features.query(q_vec)' to '[1,2,3,4,5,6]'", e.getMessage());
+ } catch (RuntimeException e) {
+ e.printStackTrace();
+ throw e;
+ }
+ }
+
+}
diff --git a/config-model/src/test/java/com/yahoo/schema/derived/NeuralNetTestCase.java b/config-model/src/test/java/com/yahoo/schema/derived/NeuralNetTestCase.java
new file mode 100644
index 00000000000..6e584099331
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/schema/derived/NeuralNetTestCase.java
@@ -0,0 +1,42 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.schema.derived;
+
+import com.yahoo.search.Query;
+import com.yahoo.search.query.profile.compiled.CompiledQueryProfile;
+import com.yahoo.search.query.profile.compiled.CompiledQueryProfileRegistry;
+import com.yahoo.search.query.profile.config.QueryProfileConfigurer;
+import com.yahoo.schema.parser.ParseException;
+import com.yahoo.vespa.model.container.search.QueryProfiles;
+import org.junit.Test;
+
+import java.io.IOException;
+
+import com.yahoo.component.ComponentId;
+
+import static org.junit.Assert.assertEquals;
+
+public class NeuralNetTestCase extends AbstractExportingTestCase {
+
+ @Test
+ public void testNeuralNet() throws IOException, ParseException {
+ ComponentId.resetGlobalCountersForTests();
+ DerivedConfiguration c = assertCorrectDeriving("neuralnet");
+ // Verify that query profiles end up correct when passed through the same intermediate forms as a full system
+ CompiledQueryProfileRegistry queryProfiles =
+ QueryProfileConfigurer.createFromConfig(new QueryProfiles(c.getQueryProfiles(), (level, message) -> {}).getConfig()).compile();
+ assertNeuralNetQuery(c, queryProfiles.getComponent("default"));
+ }
+
+ @Test
+ public void testNeuralNet_noQueryProfiles() throws IOException, ParseException {
+ ComponentId.resetGlobalCountersForTests();
+ DerivedConfiguration c = assertCorrectDeriving("neuralnet_noqueryprofile");
+ }
+
+ private void assertNeuralNetQuery(DerivedConfiguration c, CompiledQueryProfile defaultprofile) {
+ Query q = new Query("?test=foo&ranking.features.query(b_1)=[1,2,3,4,5,6,7,8,9]", defaultprofile);
+ assertEquals("tensor(out[9]):[1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0]",
+ q.properties().get("ranking.features.query(b_1)").toString());
+ }
+
+}
diff --git a/config-model/src/test/java/com/yahoo/schema/derived/NuwaTestCase.java b/config-model/src/test/java/com/yahoo/schema/derived/NuwaTestCase.java
new file mode 100644
index 00000000000..210c8a9bdd4
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/schema/derived/NuwaTestCase.java
@@ -0,0 +1,33 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.schema.derived;
+
+import com.yahoo.schema.parser.ParseException;
+import org.junit.Ignore;
+import org.junit.Test;
+
+import java.io.IOException;
+
+/**
+ * A real world mlr ranking model, useful to benchmark memory and cpu usage.
+ *
+ * @author bratseth
+ */
+public class NuwaTestCase extends AbstractExportingTestCase {
+
+ @Test
+ @Ignore
+ public void testNuwa() throws IOException, ParseException {
+ System.gc();
+ long freeBytesBefore = Runtime.getRuntime().freeMemory();
+ long totalBytesBefore = Runtime.getRuntime().totalMemory();
+
+ DerivedConfiguration configuration = assertCorrectDeriving("nuwa");
+
+ System.gc();
+ long freeBytesAfter = Runtime.getRuntime().freeMemory();
+ long totalBytesAfter = Runtime.getRuntime().totalMemory();
+ long additionalAllocated = totalBytesAfter - totalBytesBefore;
+ System.out.println("Consumed " + ((freeBytesBefore - (freeBytesAfter - additionalAllocated) ) / 1000000) + " Mb");
+ }
+
+}
diff --git a/config-model/src/test/java/com/yahoo/schema/derived/OrderIlscriptsTestCase.java b/config-model/src/test/java/com/yahoo/schema/derived/OrderIlscriptsTestCase.java
new file mode 100755
index 00000000000..8af0d0a21d3
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/schema/derived/OrderIlscriptsTestCase.java
@@ -0,0 +1,19 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.schema.derived;
+
+import com.yahoo.schema.parser.ParseException;
+import org.junit.Test;
+
+import java.io.IOException;
+
+/**
+ * @author Einar M R Rosenvinge
+ */
+public class OrderIlscriptsTestCase extends AbstractExportingTestCase {
+
+ @Test
+ public void testOrderIlscripts() throws IOException, ParseException {
+ assertCorrectDeriving("orderilscripts");
+ }
+
+}
diff --git a/config-model/src/test/java/com/yahoo/schema/derived/PrefixExactAttributeTestCase.java b/config-model/src/test/java/com/yahoo/schema/derived/PrefixExactAttributeTestCase.java
new file mode 100644
index 00000000000..fdcb71432e4
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/schema/derived/PrefixExactAttributeTestCase.java
@@ -0,0 +1,21 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.schema.derived;
+
+import com.yahoo.schema.parser.ParseException;
+import org.junit.Test;
+
+import java.io.IOException;
+
+/**
+ * Tests deriving of various field types
+ *
+ * @author bratseth
+ */
+public class PrefixExactAttributeTestCase extends AbstractExportingTestCase {
+
+ @Test
+ public void testTypes() throws IOException, ParseException {
+ assertCorrectDeriving("prefixexactattribute");
+ }
+
+}
diff --git a/config-model/src/test/java/com/yahoo/schema/derived/RankProfilesTestCase.java b/config-model/src/test/java/com/yahoo/schema/derived/RankProfilesTestCase.java
new file mode 100644
index 00000000000..a83db0caf5a
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/schema/derived/RankProfilesTestCase.java
@@ -0,0 +1,20 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.schema.derived;
+
+import com.yahoo.config.model.deploy.TestProperties;
+import com.yahoo.schema.parser.ParseException;
+import org.junit.Test;
+
+import java.io.IOException;
+
+/**
+ * Tests a search definition with various rank profiles having different settings
+ *
+ * @author bratseth
+ */
+public class RankProfilesTestCase extends AbstractExportingTestCase {
+ @Test
+ public void testRankProfiles() throws IOException, ParseException {
+ assertCorrectDeriving("rankprofiles", null, new TestProperties(), new TestableDeployLogger());
+ }
+}
diff --git a/config-model/src/test/java/com/yahoo/schema/derived/RankPropertiesTestCase.java b/config-model/src/test/java/com/yahoo/schema/derived/RankPropertiesTestCase.java
new file mode 100644
index 00000000000..8db880e56fe
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/schema/derived/RankPropertiesTestCase.java
@@ -0,0 +1,19 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.schema.derived;
+
+import com.yahoo.schema.parser.ParseException;
+import org.junit.Test;
+
+import java.io.IOException;
+
+/**
+ * @author bratseth
+ */
+public class RankPropertiesTestCase extends AbstractExportingTestCase {
+
+ @Test
+ public void testRankProperties() throws IOException, ParseException {
+ assertCorrectDeriving("rankproperties");
+ }
+
+}
diff --git a/config-model/src/test/java/com/yahoo/schema/derived/ReferenceFieldsTestCase.java b/config-model/src/test/java/com/yahoo/schema/derived/ReferenceFieldsTestCase.java
new file mode 100644
index 00000000000..99d0cf8bf6d
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/schema/derived/ReferenceFieldsTestCase.java
@@ -0,0 +1,18 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.schema.derived;
+
+import com.yahoo.schema.parser.ParseException;
+import org.junit.Test;
+
+import java.io.IOException;
+
+/**
+ * @author geirst
+ */
+public class ReferenceFieldsTestCase extends AbstractExportingTestCase {
+
+ @Test
+ public void configs_related_to_reference_fields_are_derived() throws IOException, ParseException {
+ assertCorrectDeriving("reference_fields", "ad", new TestableDeployLogger());
+ }
+}
diff --git a/config-model/src/test/java/com/yahoo/schema/derived/ReferenceFromSeveralTestCase.java b/config-model/src/test/java/com/yahoo/schema/derived/ReferenceFromSeveralTestCase.java
new file mode 100644
index 00000000000..ff4506a7f57
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/schema/derived/ReferenceFromSeveralTestCase.java
@@ -0,0 +1,27 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.schema.derived;
+
+import com.yahoo.config.model.deploy.TestProperties;
+import com.yahoo.config.model.application.provider.MockFileRegistry;
+import com.yahoo.schema.ApplicationBuilder;
+import org.junit.Test;
+
+/**
+ * Tests deriving a configuration with references from multiple .sd files
+ *
+ * @author arnej
+ */
+public class ReferenceFromSeveralTestCase extends AbstractExportingTestCase {
+
+ @Test
+ public void testDocManConfigs() throws Exception {
+ var logger = new TestableDeployLogger();
+ var props = new TestProperties();
+ ApplicationBuilder builder = ApplicationBuilder.createFromDirectory
+ ("src/test/derived/reference_from_several/", new MockFileRegistry(), logger, props);
+ derive("reference_from_several", builder, builder.getSchema("foo"));
+ assertCorrectConfigFiles("reference_from_several");
+ }
+
+}
+
diff --git a/config-model/src/test/java/com/yahoo/schema/derived/SchemaInheritanceTestCase.java b/config-model/src/test/java/com/yahoo/schema/derived/SchemaInheritanceTestCase.java
new file mode 100644
index 00000000000..1b5d55158b5
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/schema/derived/SchemaInheritanceTestCase.java
@@ -0,0 +1,34 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.schema.derived;
+
+import com.yahoo.config.model.application.provider.MockFileRegistry;
+import com.yahoo.config.model.deploy.TestProperties;
+import com.yahoo.io.IOUtils;
+import com.yahoo.schema.ApplicationBuilder;
+import com.yahoo.schema.parser.ParseException;
+import org.junit.Test;
+
+import java.io.File;
+import java.io.IOException;
+
+/**
+ * @author bratseth
+ */
+public class SchemaInheritanceTestCase extends AbstractExportingTestCase {
+
+ @Test
+ public void testIt() throws IOException, ParseException {
+ try {
+ ApplicationBuilder builder = ApplicationBuilder.createFromDirectory("src/test/derived/schemainheritance/",
+ new MockFileRegistry(),
+ new TestableDeployLogger(),
+ new TestProperties());
+ derive("schemainheritance", builder, builder.getSchema("child"));
+ assertCorrectConfigFiles("schemainheritance");
+ }
+ finally {
+ IOUtils.recursiveDeleteDir(new File("src/test/derived/schemainheritance/models.generated/"));
+ }
+ }
+
+}
diff --git a/config-model/src/test/java/com/yahoo/schema/derived/SchemaOrdererTestCase.java b/config-model/src/test/java/com/yahoo/schema/derived/SchemaOrdererTestCase.java
new file mode 100644
index 00000000000..e672763f13c
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/schema/derived/SchemaOrdererTestCase.java
@@ -0,0 +1,146 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.schema.derived;
+
+import com.yahoo.config.model.test.MockApplicationPackage;
+import com.yahoo.documentmodel.NewDocumentReferenceDataType;
+import com.yahoo.schema.DocumentReference;
+import com.yahoo.schema.DocumentReferences;
+import com.yahoo.schema.Schema;
+import com.yahoo.schema.AbstractSchemaTestCase;
+import com.yahoo.schema.document.SDDocumentType;
+import com.yahoo.schema.document.SDField;
+import com.yahoo.schema.document.TemporarySDField;
+import org.junit.Test;
+
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+
+import static java.util.Collections.emptyMap;
+import static java.util.stream.Collectors.toList;
+import static org.junit.Assert.assertEquals;
+
+/**
+ * @author bratseth
+ * @author bjorncs
+ */
+public class SchemaOrdererTestCase extends AbstractSchemaTestCase {
+
+ private static Map<String, Schema> createSchemas() {
+ Map<String, Schema> schemas = new HashMap<>();
+
+ Schema grandParent = createSchema("grandParent", schemas);
+
+ Schema mother = createSchema("mother", schemas);
+ inherit(mother, grandParent);
+
+ Schema father = createSchema("father", schemas);
+ inherit(father, grandParent);
+ createDocumentReference(father, mother, "wife_ref");
+
+ Schema daugther = createSchema("daughter", schemas);
+ inherit(daugther, father);
+ inherit(daugther, mother);
+
+ Schema son = createSchema("son", schemas);
+ inherit(son, father);
+ inherit(son, mother);
+
+ Schema product = createSchema("product", schemas);
+
+ Schema pc = createSchema("pc", schemas);
+ inherit(pc, product);
+ Schema pcAccessory = createSchema("accessory-pc", schemas);
+ inherit(pcAccessory, product);
+ createDocumentReference(pcAccessory, pc, "pc_ref");
+
+ createSchema("alone", schemas);
+
+ return schemas;
+ }
+
+ private static Schema createSchema(String name, Map<String, Schema> schemas) {
+ Schema schema = new Schema(name, MockApplicationPackage.createEmpty());
+ SDDocumentType document = new SDDocumentType(name);
+ document.setDocumentReferences(new DocumentReferences(emptyMap()));
+ schema.addDocument(document);
+ schemas.put(schema.getName(), schema);
+ return schema;
+ }
+
+ private static void inherit(Schema inheritee, Schema inherited) {
+ inheritee.getDocument().inherit(inherited.getDocument());
+ }
+
+ private static void assertOrder(List<String> expectedSearchOrder, List<String> inputNames) {
+ Map<String, Schema> schemas = createSchemas();
+ List<Schema> inputSchemas = inputNames.stream()
+ .map(schemas::get)
+ .map(Objects::requireNonNull)
+ .collect(toList());
+ List<String> actualSearchOrder = new SearchOrderer()
+ .order(inputSchemas)
+ .stream()
+ .map(Schema::getName)
+ .collect(toList());
+ assertEquals(expectedSearchOrder, actualSearchOrder);
+ }
+
+ @SuppressWarnings("deprecation")
+ private static void createDocumentReference(Schema from, Schema to, String refFieldName) {
+ SDDocumentType fromDocument = from.getDocument();
+ SDField refField = new TemporarySDField(fromDocument, refFieldName, NewDocumentReferenceDataType.forDocumentName(to.getName()));
+ fromDocument.addField(refField);
+ Map<String, DocumentReference> originalMap = fromDocument.getDocumentReferences().get().referenceMap();
+ HashMap<String, DocumentReference> modifiedMap = new HashMap<>(originalMap);
+ modifiedMap.put(refFieldName, new DocumentReference(refField, to));
+ fromDocument.setDocumentReferences(new DocumentReferences(modifiedMap));
+ }
+
+
+ @Test
+ public void testPerfectOrderingIsKept() {
+ assertOrder(Arrays.asList("alone", "grandParent", "mother", "father", "daughter", "product", "pc", "son"),
+ Arrays.asList("grandParent", "mother", "father", "daughter", "son", "product", "pc", "alone"));
+ }
+
+ @Test
+ public void testOneLevelReordering() {
+ assertOrder(Arrays.asList("alone", "grandParent", "mother", "father", "daughter", "product", "pc", "son"),
+ Arrays.asList("grandParent", "daughter", "son", "mother", "father", "pc", "product", "alone"));
+ }
+
+ @Test
+ public void testMultiLevelReordering() {
+ assertOrder(Arrays.asList("alone", "grandParent", "mother", "father", "daughter", "product", "pc", "son"),
+ Arrays.asList("daughter", "son", "mother", "father", "grandParent", "pc", "product", "alone"));
+ }
+
+ @Test
+ public void testAloneIsKeptInPlaceWithMultiLevelReordering() {
+ assertOrder(Arrays.asList("alone", "grandParent", "mother", "father", "daughter", "product", "pc", "son"),
+ Arrays.asList("alone", "daughter", "son", "mother", "father", "grandParent", "pc", "product"));
+ }
+
+ @Test
+ public void testPartialMultiLevelReordering() {
+ assertOrder(Arrays.asList("alone", "grandParent", "mother", "father", "daughter", "product", "pc", "son"),
+ Arrays.asList("daughter", "grandParent", "mother", "son", "father", "product", "pc", "alone"));
+ }
+
+ @Test
+ public void testMultilevelReorderingAccrossHierarchies() {
+ assertOrder(Arrays.asList("alone", "grandParent", "mother", "father", "daughter", "product", "pc", "son"),
+ Arrays.asList("daughter", "pc", "son", "mother", "grandParent", "father", "product", "alone"));
+ }
+
+ @Test
+ public void referees_are_ordered_before_referrer() {
+ assertOrder(Arrays.asList("alone", "grandParent", "mother", "father", "daughter", "product", "pc", "accessory-pc", "son"),
+ Arrays.asList("accessory-pc", "daughter", "pc", "son", "mother", "grandParent", "father", "product", "alone"));
+ }
+
+
+}
diff --git a/config-model/src/test/java/com/yahoo/schema/derived/SimpleInheritTestCase.java b/config-model/src/test/java/com/yahoo/schema/derived/SimpleInheritTestCase.java
new file mode 100644
index 00000000000..d8b39bfd978
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/schema/derived/SimpleInheritTestCase.java
@@ -0,0 +1,47 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.schema.derived;
+
+import com.yahoo.schema.Schema;
+import com.yahoo.schema.ApplicationBuilder;
+import com.yahoo.schema.parser.ParseException;
+import org.junit.Test;
+
+import java.io.File;
+import java.io.IOException;
+
+/**
+ * Tests really simple inheriting
+ */
+public class SimpleInheritTestCase extends AbstractExportingTestCase {
+
+ @Test
+ public void testEmptyChild() throws IOException, ParseException {
+ String name = "emptychild";
+ final String expectedResultsDirName = "src/test/derived/" + name + "/";
+
+ ApplicationBuilder builder = new ApplicationBuilder();
+ builder.addSchemaFile(expectedResultsDirName + "parent.sd");
+ builder.addSchemaFile(expectedResultsDirName + "child.sd");
+ builder.build(true);
+
+ Schema schema = builder.getSchema("child");
+
+ String toDirName = "temp/" + name;
+ File toDir = new File(toDirName);
+ toDir.mkdirs();
+ deleteContent(toDir);
+
+ DerivedConfiguration config = new DerivedConfiguration(schema, builder.getRankProfileRegistry());
+ config.export(toDirName);
+
+ checkDir(toDirName, expectedResultsDirName);
+ }
+
+ private void checkDir(String toDirName, String expectedResultsDirName) throws IOException {
+ File[] files = new File(expectedResultsDirName).listFiles();
+ for (File file : files) {
+ if ( ! file.getName().endsWith(".cfg")) continue;
+ assertEqualFiles(file.getPath(), toDirName + "/" + file.getName(), false);
+ }
+ }
+}
diff --git a/config-model/src/test/java/com/yahoo/schema/derived/SliceTestCase.java b/config-model/src/test/java/com/yahoo/schema/derived/SliceTestCase.java
new file mode 100644
index 00000000000..2aad47dae6c
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/schema/derived/SliceTestCase.java
@@ -0,0 +1,23 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.schema.derived;
+
+import com.yahoo.component.ComponentId;
+import com.yahoo.schema.parser.ParseException;
+import org.junit.Test;
+
+import java.io.IOException;
+
+import static org.junit.Assert.assertEquals;
+
+/**
+ * @author bratseth
+ */
+public class SliceTestCase extends AbstractExportingTestCase {
+
+ @Test
+ public void testSlice() throws IOException, ParseException {
+ ComponentId.resetGlobalCountersForTests();
+ DerivedConfiguration c = assertCorrectDeriving("slice");
+ }
+
+}
diff --git a/config-model/src/test/java/com/yahoo/schema/derived/SortingTestCase.java b/config-model/src/test/java/com/yahoo/schema/derived/SortingTestCase.java
new file mode 100644
index 00000000000..0c091a7a367
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/schema/derived/SortingTestCase.java
@@ -0,0 +1,22 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.schema.derived;
+
+import com.yahoo.config.model.deploy.TestProperties;
+import com.yahoo.schema.parser.ParseException;
+import org.junit.Test;
+
+import java.io.IOException;
+
+/**
+ * Tests sort settings
+ *
+ * @author baldersheim
+ */
+public class SortingTestCase extends AbstractExportingTestCase {
+
+ @Test
+ public void testDocumentDerivingNewParser() throws IOException, ParseException {
+ assertCorrectDeriving("sorting", new TestProperties());
+ }
+
+}
diff --git a/config-model/src/test/java/com/yahoo/schema/derived/StreamingStructTestCase.java b/config-model/src/test/java/com/yahoo/schema/derived/StreamingStructTestCase.java
new file mode 100755
index 00000000000..6f27930e239
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/schema/derived/StreamingStructTestCase.java
@@ -0,0 +1,26 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.schema.derived;
+
+import com.yahoo.schema.parser.ParseException;
+import org.junit.Test;
+
+import java.io.IOException;
+
+/**
+ * Tests streaming search configuration deriving for structs
+ *
+ * @author bratseth
+ */
+public class StreamingStructTestCase extends AbstractExportingTestCase {
+
+ @Test
+ public void testStreamingStruct() throws IOException, ParseException {
+ assertCorrectDeriving("streamingstruct");
+ }
+
+ @Test
+ public void testStreamingStructExplicitDefaultSummaryClass() throws IOException, ParseException {
+ assertCorrectDeriving("streamingstructdefault");
+ }
+
+}
diff --git a/config-model/src/test/java/com/yahoo/schema/derived/StructAnyOrderTestCase.java b/config-model/src/test/java/com/yahoo/schema/derived/StructAnyOrderTestCase.java
new file mode 100755
index 00000000000..865b5da87cf
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/schema/derived/StructAnyOrderTestCase.java
@@ -0,0 +1,17 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.schema.derived;
+
+import com.yahoo.schema.parser.ParseException;
+import org.junit.Test;
+
+import java.io.IOException;
+
+/**
+ * @author Einar M R Rosenvinge
+ */
+public class StructAnyOrderTestCase extends AbstractExportingTestCase {
+ @Test
+ public void testStructAnyOrder() throws IOException, ParseException {
+ assertCorrectDeriving("structanyorder");
+ }
+}
diff --git a/config-model/src/test/java/com/yahoo/schema/derived/StructInheritanceTestCase.java b/config-model/src/test/java/com/yahoo/schema/derived/StructInheritanceTestCase.java
new file mode 100644
index 00000000000..092e64420e8
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/schema/derived/StructInheritanceTestCase.java
@@ -0,0 +1,52 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.schema.derived;
+
+
+import com.yahoo.schema.ApplicationBuilder;
+
+import com.yahoo.schema.parser.ParseException;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+
+import java.io.IOException;
+
+
+import org.junit.rules.TemporaryFolder;
+
+/**
+ * Tests struct inheritance
+ *
+ * @author arnej
+ */
+public class StructInheritanceTestCase extends AbstractExportingTestCase {
+
+ @Rule
+ public TemporaryFolder tmpDir = new TemporaryFolder();
+
+ @SuppressWarnings("deprecation")
+ @Rule
+ public final ExpectedException exceptionRule = ExpectedException.none();
+
+ @Test
+ public void requireThatStructCanInherit() throws IOException, ParseException {
+ String dir = "src/test/derived/structinheritance/";
+ ApplicationBuilder builder = new ApplicationBuilder();
+ builder.addSchemaFile(dir + "simple.sd");
+ builder.build(false);
+ derive("structinheritance", builder, builder.getSchema("simple"));
+ assertCorrectConfigFiles("structinheritance");
+ }
+
+ @Test
+ public void requireThatRedeclareIsNotAllowed() throws IOException, ParseException {
+ exceptionRule.expect(IllegalArgumentException.class);
+ exceptionRule.expectMessage("cannot inherit from base and redeclare field name");
+ String dir = "src/test/derived/structinheritance/";
+ ApplicationBuilder builder = new ApplicationBuilder();
+ builder.addSchemaFile(dir + "bad.sd");
+ builder.build(true);
+ derive("structinheritance", builder, builder.getSchema("bad"));
+ }
+
+}
diff --git a/config-model/src/test/java/com/yahoo/schema/derived/SummaryMapTestCase.java b/config-model/src/test/java/com/yahoo/schema/derived/SummaryMapTestCase.java
new file mode 100644
index 00000000000..f0fc58b97e5
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/schema/derived/SummaryMapTestCase.java
@@ -0,0 +1,187 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.schema.derived;
+
+import com.yahoo.config.model.test.MockApplicationPackage;
+import com.yahoo.schema.*;
+import com.yahoo.vespa.config.search.SummarymapConfig;
+import com.yahoo.config.model.application.provider.BaseDeployLogger;
+import com.yahoo.document.PositionDataType;
+import com.yahoo.schema.document.SDDocumentType;
+import com.yahoo.schema.document.SDField;
+import com.yahoo.schema.parser.ParseException;
+import com.yahoo.schema.processing.Processing;
+import com.yahoo.vespa.documentmodel.SummaryTransform;
+import com.yahoo.vespa.model.container.search.QueryProfiles;
+import org.junit.Test;
+
+import java.io.IOException;
+import java.util.Iterator;
+import java.util.Set;
+
+import static com.yahoo.config.model.test.TestUtil.joinLines;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+/**
+ * Tests summary map extraction
+ *
+ * @author bratseth
+ */
+public class SummaryMapTestCase extends AbstractSchemaTestCase {
+ @Test
+ public void testDeriving() throws IOException, ParseException {
+ Schema schema = ApplicationBuilder.buildFromFile("src/test/examples/simple.sd");
+ SummaryMap summaryMap = new SummaryMap(schema);
+
+ Iterator<FieldResultTransform> transforms = summaryMap.resultTransforms().values().iterator();
+ FieldResultTransform transform = transforms.next();
+ assertEquals("dyndesc", transform.getFieldName());
+ assertEquals(SummaryTransform.DYNAMICTEASER, transform.getTransform());
+
+ transform = transforms.next();
+ assertEquals("dynlong", transform.getFieldName());
+ assertEquals(SummaryTransform.DYNAMICTEASER, transform.getTransform());
+
+ transform = transforms.next();
+ assertEquals("dyndesc2", transform.getFieldName());
+ assertEquals(SummaryTransform.DYNAMICTEASER, transform.getTransform());
+
+ transform = transforms.next();
+ assertEquals("measurement", transform.getFieldName());
+ assertEquals(SummaryTransform.ATTRIBUTE, transform.getTransform());
+
+ transform = transforms.next();
+ assertEquals("rankfeatures", transform.getFieldName());
+ assertEquals(SummaryTransform.RANKFEATURES, transform.getTransform());
+
+ transform = transforms.next();
+ assertEquals("summaryfeatures", transform.getFieldName());
+ assertEquals(SummaryTransform.SUMMARYFEATURES, transform.getTransform());
+
+ transform = transforms.next();
+ assertEquals("popsiness", transform.getFieldName());
+ assertEquals(SummaryTransform.ATTRIBUTE, transform.getTransform());
+
+ transform = transforms.next();
+ assertEquals("popularity", transform.getFieldName());
+ assertEquals(SummaryTransform.ATTRIBUTE, transform.getTransform());
+
+ transform = transforms.next();
+ assertEquals("access", transform.getFieldName());
+ assertEquals(SummaryTransform.ATTRIBUTE, transform.getTransform());
+
+ assertFalse(transforms.hasNext());
+ }
+ @Test
+ public void testPositionDeriving() {
+ Schema schema = new Schema("store", MockApplicationPackage.createEmpty());
+ SDDocumentType document = new SDDocumentType("store");
+ schema.addDocument(document);
+ String fieldName = "location";
+ SDField field = document.addField(fieldName, PositionDataType.INSTANCE);
+ field.parseIndexingScript("{ attribute | summary }");
+ new Processing().process(schema, new BaseDeployLogger(), new RankProfileRegistry(), new QueryProfiles(),
+ true, false, Set.of());
+ SummaryMap summaryMap = new SummaryMap(schema);
+
+ Iterator<FieldResultTransform> transforms = summaryMap.resultTransforms().values().iterator();
+
+ FieldResultTransform transform = transforms.next();
+
+ assertEquals(fieldName, transform.getFieldName());
+ assertEquals(SummaryTransform.GEOPOS, transform.getTransform());
+
+ transform = transforms.next();
+ assertEquals("rankfeatures", transform.getFieldName());
+ assertEquals(SummaryTransform.RANKFEATURES, transform.getTransform());
+
+ transform = transforms.next();
+ assertEquals("summaryfeatures", transform.getFieldName());
+ assertEquals(SummaryTransform.SUMMARYFEATURES, transform.getTransform());
+
+ transform = transforms.next();
+ assertEquals("location_zcurve", transform.getFieldName());
+ assertEquals(SummaryTransform.ATTRIBUTE,transform.getTransform());
+
+ assertFalse(transforms.hasNext());
+
+ SummarymapConfig.Builder scb = new SummarymapConfig.Builder();
+ summaryMap.getConfig(scb);
+ SummarymapConfig c = scb.build();
+
+ assertEquals(-1, c.defaultoutputclass());
+ assertEquals(c.override().size(), 4);
+
+ assertEquals(c.override(0).field(), fieldName);
+ assertEquals(c.override(0).command(), "geopos");
+ assertEquals(c.override(0).arguments(), PositionDataType.getZCurveFieldName(fieldName));
+
+ assertEquals(c.override(1).field(), "rankfeatures");
+ assertEquals(c.override(1).command(), "rankfeatures");
+ assertEquals(c.override(1).arguments(), "");
+
+ assertEquals(c.override(2).field(), "summaryfeatures");
+ assertEquals(c.override(2).command(), "summaryfeatures");
+ assertEquals(c.override(2).arguments(), "");
+
+ assertEquals(c.override(3).field(), "location_zcurve");
+ assertEquals(c.override(3).command(), "attribute");
+ assertEquals(c.override(3).arguments(), "location_zcurve");
+ }
+
+ @Test
+ public void testFailOnSummaryFieldSourceCollision() {
+ try {
+ ApplicationBuilder.buildFromFile("src/test/examples/summaryfieldcollision.sd");
+ } catch (Exception e) {
+ assertTrue(e.getMessage().matches(".*equally named field.*"));
+ }
+ }
+
+ @Test
+ public void source_field_is_passed_as_argument_in_matched_elements_filter_transforms() throws ParseException {
+ assertOverride(joinLines("field my_field type map<string, string> {",
+ " indexing: summary",
+ " summary: matched-elements-only",
+ " struct-field key { indexing: attribute }",
+ "}"), "my_field", SummaryTransform.MATCHED_ELEMENTS_FILTER.getName());
+
+ assertOverride(joinLines("field my_field type map<string, string> {",
+ " indexing: summary",
+ " summary: matched-elements-only",
+ " struct-field key { indexing: attribute }",
+ " struct-field value { indexing: attribute }",
+ "}"), "my_field", SummaryTransform.MATCHED_ATTRIBUTE_ELEMENTS_FILTER.getName());
+ }
+
+ @Test
+ public void commands_that_are_dynamic_and_require_the_query() {
+ assertTrue(SummaryMap.isDynamicCommand("dynamicteaser"));
+ assertTrue(SummaryMap.isDynamicCommand(SummaryTransform.MATCHED_ELEMENTS_FILTER.getName()));
+ assertTrue(SummaryMap.isDynamicCommand(SummaryTransform.MATCHED_ATTRIBUTE_ELEMENTS_FILTER.getName()));
+ assertFalse(SummaryMap.isDynamicCommand(SummaryTransform.ATTRIBUTE.getName()));
+ }
+
+ private void assertOverride(String fieldContent, String expFieldName, String expCommand) throws ParseException {
+ var summaryMap = new SummaryMap(buildSearch(fieldContent));
+ var cfgBuilder = new SummarymapConfig.Builder();
+ summaryMap.getConfig(cfgBuilder);
+ var cfg = new SummarymapConfig(cfgBuilder);
+ var override = cfg.override(0);
+ assertEquals(expFieldName, override.field());
+ assertEquals(expCommand, override.command());
+ assertEquals(expFieldName, override.arguments());
+ }
+
+ private Schema buildSearch(String field) throws ParseException {
+ var builder = new ApplicationBuilder(new RankProfileRegistry());
+ builder.addSchema(joinLines("search test {",
+ " document test {",
+ field,
+ " }",
+ "}"));
+ builder.build(true);
+ return builder.getSchema();
+ }
+
+}
diff --git a/config-model/src/test/java/com/yahoo/schema/derived/SummaryTestCase.java b/config-model/src/test/java/com/yahoo/schema/derived/SummaryTestCase.java
new file mode 100644
index 00000000000..341d3ef7d43
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/schema/derived/SummaryTestCase.java
@@ -0,0 +1,182 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.schema.derived;
+
+import com.yahoo.config.model.application.provider.BaseDeployLogger;
+import com.yahoo.schema.Schema;
+import com.yahoo.schema.ApplicationBuilder;
+import com.yahoo.schema.AbstractSchemaTestCase;
+import com.yahoo.schema.parser.ParseException;
+import com.yahoo.vespa.config.search.SummaryConfig;
+import org.junit.Test;
+
+import java.io.IOException;
+import java.util.Iterator;
+
+import static com.yahoo.config.model.test.TestUtil.joinLines;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * Tests summary extraction
+ *
+ * @author bratseth
+ */
+public class SummaryTestCase extends AbstractSchemaTestCase {
+
+ @Test
+ public void deriveRawAsBase64() throws ParseException {
+ String sd = joinLines(
+ "schema s {",
+ " raw-as-base64-in-summary",
+ " document s {",
+ " field raw_field type raw {",
+ " indexing: summary",
+ " }",
+ " }",
+ "}");
+ Schema schema = ApplicationBuilder.createFromString(sd).getSchema();
+ SummaryClass summary = new SummaryClass(schema, schema.getSummary("default"), new BaseDeployLogger());
+ assertEquals(SummaryClassField.Type.RAW, summary.fields().get("raw_field").getType());
+ }
+
+ @Test
+ public void deriveRawAsLegacy() throws ParseException {
+ String sd = joinLines(
+ "schema s {",
+ " document s {",
+ " field raw_field type raw {",
+ " indexing: summary",
+ " }",
+ " }",
+ "}");
+ Schema schema = ApplicationBuilder.createFromString(sd).getSchema();
+ SummaryClass summary = new SummaryClass(schema, schema.getSummary("default"), new BaseDeployLogger());
+ assertEquals(SummaryClassField.Type.DATA, summary.fields().get("raw_field").getType());
+ }
+
+ @Test
+ public void testDeriving() throws IOException, ParseException {
+ Schema schema = ApplicationBuilder.buildFromFile("src/test/examples/simple.sd");
+ SummaryClass summary = new SummaryClass(schema, schema.getSummary("default"), new BaseDeployLogger());
+ assertEquals("default", summary.getName());
+
+ Iterator<SummaryClassField> fields = summary.fields().values().iterator();
+
+ SummaryClassField field;
+
+ assertEquals(13, summary.fields().size());
+
+ field = fields.next();
+ assertEquals("exactemento", field.getName());
+ assertEquals(SummaryClassField.Type.LONGSTRING, field.getType());
+
+ field = fields.next();
+ assertEquals("exact", field.getName());
+ assertEquals(SummaryClassField.Type.LONGSTRING, field.getType());
+
+ field = fields.next();
+ assertEquals("title", field.getName());
+ assertEquals(SummaryClassField.Type.LONGSTRING, field.getType());
+
+ field = fields.next();
+ assertEquals("description", field.getName());
+ assertEquals(SummaryClassField.Type.LONGSTRING, field.getType());
+
+ field = fields.next();
+ assertEquals("dyndesc", field.getName());
+ assertEquals(SummaryClassField.Type.LONGSTRING, field.getType());
+
+ field = fields.next();
+ assertEquals("longdesc", field.getName());
+ assertEquals(SummaryClassField.Type.LONGSTRING, field.getType());
+
+ field = fields.next();
+ assertEquals("longstat", field.getName());
+ assertEquals(SummaryClassField.Type.LONGSTRING, field.getType());
+
+ field = fields.next();
+ assertEquals("dynlong", field.getName());
+ assertEquals(SummaryClassField.Type.LONGSTRING, field.getType());
+
+ field = fields.next();
+ assertEquals("dyndesc2", field.getName());
+ assertEquals(SummaryClassField.Type.LONGSTRING, field.getType());
+
+ field = fields.next();
+ assertEquals("measurement", field.getName());
+ assertEquals(SummaryClassField.Type.INTEGER, field.getType());
+
+ field = fields.next();
+ assertEquals("rankfeatures", field.getName());
+ assertEquals(SummaryClassField.Type.FEATUREDATA, field.getType());
+
+ field = fields.next();
+ assertEquals("summaryfeatures", field.getName());
+ assertEquals(SummaryClassField.Type.FEATUREDATA, field.getType());
+
+ field = fields.next();
+ assertEquals("documentid", field.getName());
+ assertEquals(SummaryClassField.Type.LONGSTRING, field.getType());
+ }
+
+ @Test
+ public void reference_fields_can_be_part_of_summary_classes() throws ParseException {
+ Schema adSchema = buildCampaignAdModel();
+
+ SummaryClass defaultClass = new SummaryClass(adSchema, adSchema.getSummary("default"), new BaseDeployLogger());
+ assertEquals(SummaryClassField.Type.LONGSTRING, defaultClass.fields().get("campaign_ref").getType());
+ assertEquals(SummaryClassField.Type.LONGSTRING, defaultClass.fields().get("other_campaign_ref").getType());
+
+ SummaryClass myClass = new SummaryClass(adSchema, adSchema.getSummary("my_summary"), new BaseDeployLogger());
+ assertNull(myClass.fields().get("campaign_ref"));
+ assertEquals(SummaryClassField.Type.LONGSTRING, myClass.fields().get("other_campaign_ref").getType());
+ }
+
+ private static Schema buildCampaignAdModel() throws ParseException {
+ ApplicationBuilder builder = new ApplicationBuilder();
+ builder.addSchema("search campaign { document campaign {} }");
+ builder.addSchema(joinLines("search ad {",
+ " document ad {",
+ " field campaign_ref type reference<campaign> {",
+ " indexing: summary | attribute",
+ " }",
+ " field other_campaign_ref type reference<campaign> {",
+ " indexing: summary | attribute",
+ " }",
+ " }",
+ " document-summary my_summary {",
+ " summary other_campaign_ref type reference<campaign> {}",
+ " }",
+ "}"));
+ builder.build(true);
+ return builder.getSchema("ad");
+ }
+
+ @Test
+ public void omit_summary_features_specified_for_document_summary() throws ParseException {
+ String sd = joinLines(
+ "schema test {",
+ " document test {",
+ " field foo type string { indexing: summary }",
+ " }",
+ " document-summary bar {",
+ " summary foo type string {}",
+ " omit-summary-features",
+ " }",
+ " document-summary baz {",
+ " summary foo type string {}",
+ " }",
+ "}");
+ var search = ApplicationBuilder.createFromString(sd).getSchema();
+ assertOmitSummaryFeatures(true, search, "bar");
+ assertOmitSummaryFeatures(false, search, "baz");
+ }
+
+ private void assertOmitSummaryFeatures(boolean expected, Schema schema, String summaryName) {
+ var summary = new SummaryClass(schema, schema.getSummary(summaryName), new BaseDeployLogger());
+ var config = new SummaryConfig.Classes(summary.getSummaryClassConfig());
+ assertEquals(expected, config.omitsummaryfeatures());
+ }
+
+}
diff --git a/config-model/src/test/java/com/yahoo/schema/derived/TestableDeployLogger.java b/config-model/src/test/java/com/yahoo/schema/derived/TestableDeployLogger.java
new file mode 100644
index 00000000000..bcde0f6b544
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/schema/derived/TestableDeployLogger.java
@@ -0,0 +1,30 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.schema.derived;
+
+import com.yahoo.config.application.api.DeployLogger;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * @author bratseth
+ */
+public class TestableDeployLogger implements DeployLogger {
+
+ private static final Logger log = Logger.getLogger("DeployLogger");
+
+ public List<String> warnings = new ArrayList<>();
+ public List<String> info = new ArrayList<>();
+
+ @Override
+ public final void log(Level level, String message) {
+ log.log(level, message);
+ if (level.equals(Level.WARNING))
+ warnings.add(message);
+ if (level.equals(Level.INFO))
+ info.add(message);
+ }
+
+}
diff --git a/config-model/src/test/java/com/yahoo/schema/derived/TokenizationTestCase.java b/config-model/src/test/java/com/yahoo/schema/derived/TokenizationTestCase.java
new file mode 100755
index 00000000000..ac6acf172e9
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/schema/derived/TokenizationTestCase.java
@@ -0,0 +1,19 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.schema.derived;
+
+import com.yahoo.schema.parser.ParseException;
+import org.junit.Test;
+
+import java.io.IOException;
+
+/**
+ * @author bratseh
+ */
+public class TokenizationTestCase extends AbstractExportingTestCase {
+
+ @Test
+ public void testTokenizationScripts() throws IOException, ParseException {
+ assertCorrectDeriving("tokenization");
+ }
+
+}
diff --git a/config-model/src/test/java/com/yahoo/schema/derived/TwoStreamingStructsTestCase.java b/config-model/src/test/java/com/yahoo/schema/derived/TwoStreamingStructsTestCase.java
new file mode 100644
index 00000000000..72411fa1770
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/schema/derived/TwoStreamingStructsTestCase.java
@@ -0,0 +1,34 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.schema.derived;
+
+import com.yahoo.schema.ApplicationBuilder;
+import com.yahoo.schema.parser.ParseException;
+import org.junit.Test;
+
+import java.io.IOException;
+
+/**
+ * Test structs for streaming with another unrelated .sd present
+ *
+ * @author arnej27959
+ */
+public class TwoStreamingStructsTestCase extends AbstractExportingTestCase {
+
+ @Test
+ public void testTwoStreamingStructsExporting() throws ParseException, IOException {
+
+ String root = "src/test/derived/twostreamingstructs";
+ ApplicationBuilder builder = new ApplicationBuilder();
+ builder.addSchemaFile(root + "/streamingstruct.sd");
+ builder.addSchemaFile(root + "/whatever.sd");
+ builder.build(true);
+ assertCorrectDeriving(builder, builder.getSchema("streamingstruct"), root);
+
+ builder = new ApplicationBuilder();
+ builder.addSchemaFile(root + "/streamingstruct.sd");
+ builder.addSchemaFile(root + "/whatever.sd");
+ builder.build(true);
+ assertCorrectDeriving(builder, builder.getSchema("streamingstruct"), root);
+ }
+
+}
diff --git a/config-model/src/test/java/com/yahoo/schema/derived/TypeConversionTestCase.java b/config-model/src/test/java/com/yahoo/schema/derived/TypeConversionTestCase.java
new file mode 100644
index 00000000000..84a561924ca
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/schema/derived/TypeConversionTestCase.java
@@ -0,0 +1,44 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.schema.derived;
+
+import com.yahoo.config.model.application.provider.BaseDeployLogger;
+import com.yahoo.config.model.test.MockApplicationPackage;
+import com.yahoo.document.DataType;
+import com.yahoo.schema.RankProfileRegistry;
+import com.yahoo.schema.Schema;
+import com.yahoo.schema.AbstractSchemaTestCase;
+import com.yahoo.schema.document.SDDocumentType;
+import com.yahoo.schema.document.SDField;
+import com.yahoo.schema.processing.Processing;
+import com.yahoo.vespa.model.container.search.QueryProfiles;
+import org.junit.Test;
+
+import java.util.Set;
+
+import static org.junit.Assert.assertFalse;
+/**
+ * Tests automatic type conversion using multifield indices
+ *
+ * @author bratseth
+ */
+public class TypeConversionTestCase extends AbstractSchemaTestCase {
+
+ /** Tests that exact-string stuff is not spilled over to the default index */
+ @Test
+ public void testExactStringToStringTypeConversion() {
+ Schema schema = new Schema("test", MockApplicationPackage.createEmpty());
+ RankProfileRegistry rankProfileRegistry = RankProfileRegistry.createRankProfileRegistryWithBuiltinRankProfiles(schema);
+ SDDocumentType document = new SDDocumentType("test");
+ schema.addDocument(document);
+ SDField a = new SDField(document, "a", DataType.STRING);
+ a.parseIndexingScript("{ index }");
+ document.addField(a);
+
+ new Processing().process(schema, new BaseDeployLogger(), rankProfileRegistry, new QueryProfiles(),
+ true, false, Set.of());
+ DerivedConfiguration derived = new DerivedConfiguration(schema, rankProfileRegistry);
+ IndexInfo indexInfo = derived.getIndexInfo();
+ assertFalse(indexInfo.hasCommand("default", "compact-to-term"));
+ }
+
+}
diff --git a/config-model/src/test/java/com/yahoo/schema/derived/TypesTestCase.java b/config-model/src/test/java/com/yahoo/schema/derived/TypesTestCase.java
new file mode 100644
index 00000000000..7443ef01c95
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/schema/derived/TypesTestCase.java
@@ -0,0 +1,21 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.schema.derived;
+
+import com.yahoo.schema.parser.ParseException;
+import org.junit.Test;
+
+import java.io.IOException;
+
+/**
+ * Tests deriving of various field types
+ *
+ * @author bratseth
+ */
+public class TypesTestCase extends AbstractExportingTestCase {
+
+ @Test
+ public void testTypes() throws IOException, ParseException {
+ assertCorrectDeriving("types");
+ }
+
+}
diff --git a/config-model/src/test/java/com/yahoo/schema/derived/VsmFieldsTestCase.java b/config-model/src/test/java/com/yahoo/schema/derived/VsmFieldsTestCase.java
new file mode 100644
index 00000000000..c59f82a2c12
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/schema/derived/VsmFieldsTestCase.java
@@ -0,0 +1,42 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.schema.derived;
+
+import com.yahoo.config.model.application.provider.MockFileRegistry;
+import com.yahoo.config.model.deploy.TestProperties;
+import com.yahoo.config.model.test.MockApplicationPackage;
+import com.yahoo.documentmodel.NewDocumentReferenceDataType;
+import com.yahoo.schema.Schema;
+import com.yahoo.schema.document.SDDocumentType;
+import com.yahoo.schema.document.SDField;
+import com.yahoo.schema.document.TemporarySDField;
+import com.yahoo.vespa.config.search.vsm.VsmfieldsConfig;
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+
+/**
+ * @author geirst
+ */
+public class VsmFieldsTestCase {
+
+ @SuppressWarnings("deprecation")
+ @Test
+ public void reference_type_field_is_unsearchable() {
+ Schema schema = new Schema("test", MockApplicationPackage.createEmpty(), new MockFileRegistry(), new TestableDeployLogger(), new TestProperties());
+ var sdoc = new SDDocumentType("test");
+ schema.addDocument(sdoc);
+ SDField refField = new TemporarySDField(sdoc, "ref_field", NewDocumentReferenceDataType.forDocumentName("parent_type"));
+ refField.parseIndexingScript("{ summary }");
+ schema.getDocument().addField(refField);
+
+ VsmFields vsmFields = new VsmFields(schema);
+ VsmfieldsConfig.Builder cfgBuilder = new VsmfieldsConfig.Builder();
+ vsmFields.getConfig(cfgBuilder);
+ VsmfieldsConfig cfg = cfgBuilder.build();
+
+ assertEquals(1, cfg.fieldspec().size());
+ VsmfieldsConfig.Fieldspec fieldSpec = cfg.fieldspec().get(0);
+ assertEquals("ref_field", fieldSpec.name());
+ assertEquals(VsmfieldsConfig.Fieldspec.Searchmethod.NONE, fieldSpec.searchmethod());
+ }
+}
diff --git a/config-model/src/test/java/com/yahoo/schema/document/ComplexAttributeFieldUtilsTestCase.java b/config-model/src/test/java/com/yahoo/schema/document/ComplexAttributeFieldUtilsTestCase.java
new file mode 100644
index 00000000000..ea3f207df91
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/schema/document/ComplexAttributeFieldUtilsTestCase.java
@@ -0,0 +1,244 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.schema.document;
+
+import com.yahoo.schema.Schema;
+import com.yahoo.schema.ApplicationBuilder;
+import com.yahoo.schema.parser.ParseException;
+import org.junit.Test;
+
+import static com.yahoo.config.model.test.TestUtil.joinLines;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+public class ComplexAttributeFieldUtilsTestCase {
+
+ private static class FixtureBase {
+
+ private final ImmutableSDField field;
+
+ FixtureBase(String fieldName, String sdContent) throws ParseException {
+ Schema schema = ApplicationBuilder.createFromString(sdContent).getSchema();
+ field = schema.getConcreteField(fieldName);
+ }
+
+ public ImmutableSDField field() {
+ return field;
+ }
+
+ boolean isSupportedComplexField() {
+ return ComplexAttributeFieldUtils.isSupportedComplexField(field());
+ }
+
+ boolean isArrayOfSimpleStruct() {
+ return ComplexAttributeFieldUtils.isArrayOfSimpleStruct(field());
+ }
+
+ boolean isMapOfSimpleStruct() {
+ return ComplexAttributeFieldUtils.isMapOfSimpleStruct(field());
+ }
+
+ boolean isMapOfPrimitiveType() {
+ return ComplexAttributeFieldUtils.isMapOfPrimitiveType(field());
+ }
+
+ boolean isComplexFieldWithOnlyStructFieldAttributes() {
+ return ComplexAttributeFieldUtils.isComplexFieldWithOnlyStructFieldAttributes(field());
+ }
+ }
+
+ private static class Fixture extends FixtureBase {
+
+ Fixture(String fieldName, String sdFieldContent) throws ParseException {
+ super(fieldName, joinLines("search test {",
+ " document test {",
+ " struct elem {",
+ " field name type string {}",
+ " field weight type int {}",
+ " }",
+ sdFieldContent,
+ " }",
+ "}"));
+ }
+ }
+
+ private static class ComplexFixture extends FixtureBase {
+
+ ComplexFixture(String fieldName, String sdFieldContent) throws ParseException {
+ super(fieldName, joinLines("search test {",
+ " document test {",
+ " struct elem {",
+ " field name type string {}",
+ " field weights type array<int> {}",
+ " }",
+ sdFieldContent,
+ " }",
+ "}"));
+ }
+ }
+
+ @Test
+ public void array_of_struct_with_only_struct_field_attributes_is_tagged_as_such() throws ParseException {
+ Fixture f = new Fixture("elem_array",
+ joinLines("field elem_array type array<elem> {",
+ " indexing: summary",
+ " struct-field name { indexing: attribute }",
+ " struct-field weight { indexing: attribute }",
+ "}"));
+ assertTrue(f.isSupportedComplexField());
+ assertTrue(f.isArrayOfSimpleStruct());
+ assertTrue(f.isComplexFieldWithOnlyStructFieldAttributes());
+ }
+
+ @Test
+ public void array_of_struct_with_some_struct_field_attributes_is_tagged_as_such() throws ParseException {
+ Fixture f = new Fixture("elem_array",
+ joinLines("field elem_array type array<elem> {",
+ " indexing: summary",
+ " struct-field weight { indexing: attribute }",
+ "}"));
+ assertTrue(f.isSupportedComplexField());
+ assertTrue(f.isArrayOfSimpleStruct());
+ assertFalse(f.isComplexFieldWithOnlyStructFieldAttributes());
+ }
+
+ @Test
+ public void map_of_struct_with_only_struct_field_attributes_is_tagged_as_such() throws ParseException {
+ Fixture f = new Fixture("elem_map",
+ joinLines("field elem_map type map<string, elem> {",
+ " indexing: summary",
+ " struct-field key { indexing: attribute }",
+ " struct-field value.name { indexing: attribute }",
+ " struct-field value.weight { indexing: attribute }",
+ "}"));
+ assertTrue(f.isSupportedComplexField());
+ assertTrue(f.isMapOfSimpleStruct());
+ assertFalse(f.isMapOfPrimitiveType());
+ assertTrue(f.isComplexFieldWithOnlyStructFieldAttributes());
+ }
+
+ @Test
+ public void map_of_struct_with_some_struct_field_attributes_is_tagged_as_such() throws ParseException {
+ {
+ Fixture f = new Fixture("elem_map",
+ joinLines("field elem_map type map<int, elem> {",
+ " indexing: summary",
+ " struct-field value.name { indexing: attribute }",
+ " struct-field value.weight { indexing: attribute }",
+ "}"));
+ assertTrue(f.isSupportedComplexField());
+ assertTrue(f.isMapOfSimpleStruct());
+ assertFalse(f.isMapOfPrimitiveType());
+ assertFalse(f.isComplexFieldWithOnlyStructFieldAttributes());
+ }
+ {
+ Fixture f = new Fixture("elem_map",
+ joinLines("field elem_map type map<int, elem> {",
+ " indexing: summary",
+ " struct-field key { indexing: attribute }",
+ " struct-field value.weight { indexing: attribute }",
+ "}"));
+ assertTrue(f.isSupportedComplexField());
+ assertTrue(f.isMapOfSimpleStruct());
+ assertFalse(f.isMapOfPrimitiveType());
+ assertFalse(f.isComplexFieldWithOnlyStructFieldAttributes());
+ }
+ }
+
+ @Test
+ public void map_of_primitive_type_with_only_struct_field_attributes_is_tagged_as_such() throws ParseException {
+ Fixture f = new Fixture("str_map",
+ joinLines("field str_map type map<string, string> {",
+ " indexing: summary",
+ " struct-field key { indexing: attribute }",
+ " struct-field value { indexing: attribute }",
+ "}"));
+ assertTrue(f.isSupportedComplexField());
+ assertTrue(f.isMapOfPrimitiveType());
+ assertFalse(f.isMapOfSimpleStruct());
+ assertTrue(f.isComplexFieldWithOnlyStructFieldAttributes());
+ }
+
+ @Test
+ public void map_of_primitive_type_with_some_struct_field_attributes_is_tagged_as_such() throws ParseException {
+ {
+ Fixture f = new Fixture("int_map",
+ joinLines("field int_map type map<int, int> {",
+ " indexing: summary",
+ " struct-field key { indexing: attribute }",
+ "}"));
+ assertTrue(f.isSupportedComplexField());
+ assertTrue(f.isMapOfPrimitiveType());
+ assertFalse(f.isMapOfSimpleStruct());
+ assertFalse(f.isComplexFieldWithOnlyStructFieldAttributes());
+ }
+ {
+ Fixture f = new Fixture("int_map",
+ joinLines("field int_map type map<int, int> {",
+ " indexing: summary",
+ " struct-field value { indexing: attribute }",
+ "}"));
+ assertTrue(f.isSupportedComplexField());
+ assertTrue(f.isMapOfPrimitiveType());
+ assertFalse(f.isMapOfSimpleStruct());
+ assertFalse(f.isComplexFieldWithOnlyStructFieldAttributes());
+ }
+ }
+
+ @Test
+ public void unsupported_complex_field_is_tagged_as_such() throws ParseException {
+ {
+ ComplexFixture f = new ComplexFixture("elem_array",
+ joinLines("field elem_array type array<elem> {",
+ " struct-field name { indexing: attribute }",
+ " struct-field weights { indexing: attribute }",
+ "}"));
+ assertFalse(f.isSupportedComplexField());
+ assertFalse(f.isArrayOfSimpleStruct());
+ assertFalse(f.isMapOfSimpleStruct());
+ assertFalse(f.isMapOfPrimitiveType());
+ assertFalse(f.isComplexFieldWithOnlyStructFieldAttributes());
+ }
+ {
+ ComplexFixture f = new ComplexFixture("elem_map",
+ joinLines("field elem_map type map<int, elem> {",
+ " indexing: summary",
+ " struct-field key { indexing: attribute }",
+ " struct-field value.weights { indexing: attribute }",
+ "}"));
+ assertFalse(f.isSupportedComplexField());
+ assertFalse(f.isArrayOfSimpleStruct());
+ assertFalse(f.isMapOfSimpleStruct());
+ assertFalse(f.isMapOfPrimitiveType());
+ assertFalse(f.isComplexFieldWithOnlyStructFieldAttributes());
+ }
+ }
+
+ @Test
+ public void only_struct_field_attributes_are_considered_when_tagging_a_complex_field() throws ParseException {
+ {
+ ComplexFixture f = new ComplexFixture("elem_array",
+ joinLines("field elem_array type array<elem> {",
+ " struct-field name { indexing: attribute }",
+ "}"));
+ assertTrue(f.isSupportedComplexField());
+ assertTrue(f.isArrayOfSimpleStruct());
+ assertFalse(f.isMapOfSimpleStruct());
+ assertFalse(f.isMapOfPrimitiveType());
+ assertFalse(f.isComplexFieldWithOnlyStructFieldAttributes());
+ }
+ {
+ ComplexFixture f = new ComplexFixture("elem_map",
+ joinLines("field elem_map type map<int, elem> {",
+ " indexing: summary",
+ " struct-field key { indexing: attribute }",
+ " struct-field value.name { indexing: attribute }",
+ "}"));
+ assertTrue(f.isSupportedComplexField());
+ assertFalse(f.isArrayOfSimpleStruct());
+ assertTrue(f.isMapOfSimpleStruct());
+ assertFalse(f.isMapOfPrimitiveType());
+ assertFalse(f.isComplexFieldWithOnlyStructFieldAttributes());
+ }
+ }
+
+}
diff --git a/config-model/src/test/java/com/yahoo/schema/document/HnswIndexParamsTestCase.java b/config-model/src/test/java/com/yahoo/schema/document/HnswIndexParamsTestCase.java
new file mode 100644
index 00000000000..8ef51369ecb
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/schema/document/HnswIndexParamsTestCase.java
@@ -0,0 +1,51 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+
+package com.yahoo.schema.document;
+
+import java.util.Optional;
+import org.junit.Test;
+
+import static org.hamcrest.core.Is.is;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+public class HnswIndexParamsTestCase {
+
+ @Test
+ public void override_from() throws Exception {
+ var empty = new HnswIndexParams();
+ var builder = new HnswIndexParams.Builder();
+ builder.setMaxLinksPerNode(7);
+ builder.setMultiThreadedIndexing(false);
+ var one = builder.build();
+ builder.setNeighborsToExploreAtInsert(42);
+ var three = builder.build();
+ builder.setMaxLinksPerNode(17);
+ builder.setNeighborsToExploreAtInsert(500);
+ builder.setMultiThreadedIndexing(true);
+ var four = builder.build();
+
+ assertThat(empty.maxLinksPerNode(), is(16));
+ assertThat(empty.neighborsToExploreAtInsert(), is(200));
+ assertThat(empty.multiThreadedIndexing(), is(true));
+
+ assertThat(one.maxLinksPerNode(), is(7));
+ assertThat(one.multiThreadedIndexing(), is(false));
+ assertThat(three.neighborsToExploreAtInsert(), is(42));
+
+ assertThat(four.maxLinksPerNode(), is(17));
+ assertThat(four.neighborsToExploreAtInsert(), is(500));
+ assertThat(four.multiThreadedIndexing(), is(true));
+
+ var five = four.overrideFrom(Optional.of(empty));
+ assertThat(five.maxLinksPerNode(), is(17));
+ assertThat(five.neighborsToExploreAtInsert(), is(500));
+ assertThat(five.multiThreadedIndexing(), is(true));
+
+ var six = four.overrideFrom(Optional.of(one));
+ assertThat(six.maxLinksPerNode(), is(7));
+ assertThat(six.neighborsToExploreAtInsert(), is(500));
+ // This is explicitly set to false in 'one'
+ assertThat(six.multiThreadedIndexing(), is(false));
+ }
+
+}
diff --git a/config-model/src/test/java/com/yahoo/schema/parser/ConvertIntermediateTestCase.java b/config-model/src/test/java/com/yahoo/schema/parser/ConvertIntermediateTestCase.java
new file mode 100644
index 00000000000..516c259013f
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/schema/parser/ConvertIntermediateTestCase.java
@@ -0,0 +1,95 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.schema.parser;
+
+import com.yahoo.document.DocumentTypeManager;
+import static com.yahoo.config.model.test.TestUtil.joinLines;
+
+import org.junit.Test;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.assertThrows;
+
+/**
+ * @author arnej
+ */
+public class ConvertIntermediateTestCase {
+
+ @Test
+ public void can_convert_minimal_schema() throws Exception {
+ String input = joinLines
+ ("schema foo {",
+ " document foo {",
+ " }",
+ "}");
+ var collection = new IntermediateCollection();
+ ParsedSchema schema = collection.addSchemaFromString(input);
+ assertEquals("foo", schema.getDocument().name());
+ var docMan = new DocumentTypeManager();
+ var converter = new ConvertSchemaCollection(collection, docMan);
+ converter.convertTypes();
+ var dt = docMan.getDocumentType("foo");
+ assertTrue(dt != null);
+ }
+
+ @Test
+ public void can_convert_schema_files() throws Exception {
+ var collection = new IntermediateCollection();
+ collection.addSchemaFromFile("src/test/derived/deriver/child.sd");
+ collection.addSchemaFromFile("src/test/derived/deriver/grandparent.sd");
+ collection.addSchemaFromFile("src/test/derived/deriver/parent.sd");
+ assertEquals(collection.getParsedSchemas().size(), 3);
+ var docMan = new DocumentTypeManager();
+ var converter = new ConvertSchemaCollection(collection, docMan);
+ converter.convertTypes();
+ var dt = docMan.getDocumentType("child");
+ assertTrue(dt != null);
+ dt = docMan.getDocumentType("parent");
+ assertTrue(dt != null);
+ dt = docMan.getDocumentType("grandparent");
+ assertTrue(dt != null);
+ }
+
+ @Test
+ public void can_convert_structs_and_annotations() throws Exception {
+ var collection = new IntermediateCollection();
+ collection.addSchemaFromFile("src/test/converter/child.sd");
+ collection.addSchemaFromFile("src/test/converter/other.sd");
+ collection.addSchemaFromFile("src/test/converter/parent.sd");
+ collection.addSchemaFromFile("src/test/converter/grandparent.sd");
+ var docMan = new DocumentTypeManager();
+ var converter = new ConvertSchemaCollection(collection, docMan);
+ converter.convertTypes();
+ var dt = docMan.getDocumentType("child");
+ assertTrue(dt != null);
+ for (var parent : dt.getInheritedTypes()) {
+ System.err.println("dt "+dt.getName()+" inherits from "+parent.getName());
+ }
+ for (var field : dt.fieldSetAll()) {
+ System.err.println("dt "+dt.getName()+" contains field "+field.getName()+" of type "+field.getDataType());
+ }
+ dt = docMan.getDocumentType("parent");
+ assertTrue(dt != null);
+ for (var parent : dt.getInheritedTypes()) {
+ System.err.println("dt "+dt.getName()+" inherits from "+parent.getName());
+ }
+ for (var field : dt.fieldSetAll()) {
+ System.err.println("dt "+dt.getName()+" contains field "+field.getName()+" of type "+field.getDataType());
+ }
+ dt = docMan.getDocumentType("grandparent");
+ assertTrue(dt != null);
+ for (var parent : dt.getInheritedTypes()) {
+ System.err.println("dt "+dt.getName()+" inherits from "+parent.getName());
+ }
+ for (var field : dt.fieldSetAll()) {
+ System.err.println("dt "+dt.getName()+" contains field "+field.getName()+" of type "+field.getDataType());
+ }
+ dt = docMan.getDocumentType("other");
+ assertTrue(dt != null);
+ for (var parent : dt.getInheritedTypes()) {
+ System.err.println("dt "+dt.getName()+" inherits from "+parent.getName());
+ }
+ for (var field : dt.fieldSetAll()) {
+ System.err.println("dt "+dt.getName()+" contains field "+field.getName()+" of type "+field.getDataType());
+ }
+ }
+}
diff --git a/config-model/src/test/java/com/yahoo/schema/parser/IntermediateCollectionTestCase.java b/config-model/src/test/java/com/yahoo/schema/parser/IntermediateCollectionTestCase.java
new file mode 100644
index 00000000000..c4ee1d27c8c
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/schema/parser/IntermediateCollectionTestCase.java
@@ -0,0 +1,236 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.schema.parser;
+
+import com.yahoo.io.reader.NamedReader;
+import static com.yahoo.config.model.test.TestUtil.joinLines;
+
+import java.nio.charset.StandardCharsets;
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileReader;
+import java.util.List;
+
+import org.junit.Test;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.assertThrows;
+
+/**
+ * @author arnej
+ */
+public class IntermediateCollectionTestCase {
+
+ @Test
+ public void can_add_minimal_schema() throws Exception {
+ String input = joinLines
+ ("schema foo {",
+ " document foo {",
+ " }",
+ "}");
+ var collection = new IntermediateCollection();
+ ParsedSchema schema = collection.addSchemaFromString(input);
+ assertEquals("foo", schema.name());
+ assertTrue(schema.hasDocument());
+ assertEquals("foo", schema.getDocument().name());
+ }
+
+ @Test
+ public void names_may_differ() throws Exception {
+ String input = joinLines
+ ("schema foo_search {",
+ " document foo {",
+ " }",
+ "}");
+ var collection = new IntermediateCollection();
+ ParsedSchema schema = collection.addSchemaFromString(input);
+ assertEquals("foo_search", schema.name());
+ assertTrue(schema.hasDocument());
+ assertEquals("foo", schema.getDocument().name());
+ }
+
+ @Test
+ public void can_add_schema_files() throws Exception {
+ var collection = new IntermediateCollection();
+ collection.addSchemaFromFile("src/test/derived/deriver/child.sd");
+ collection.addSchemaFromFile("src/test/derived/deriver/grandparent.sd");
+ collection.addSchemaFromFile("src/test/derived/deriver/parent.sd");
+ var schemes = collection.getParsedSchemas();
+ assertEquals(schemes.size(), 3);
+ var schema = schemes.get("child");
+ assertTrue(schema != null);
+ assertEquals(schema.name(), "child");
+ schema = schemes.get("parent");
+ assertTrue(schema != null);
+ assertEquals(schema.name(), "parent");
+ schema = schemes.get("grandparent");
+ assertTrue(schema != null);
+ assertEquals(schema.name(), "grandparent");
+ }
+
+ NamedReader readerOf(String fileName) throws Exception {
+ File f = new File(fileName);
+ FileReader fr = new FileReader(f, StandardCharsets.UTF_8);
+ BufferedReader br = new BufferedReader(fr);
+ return new NamedReader(fileName, br);
+ }
+
+ @Test
+ public void can_add_schemas() throws Exception {
+ var collection = new IntermediateCollection();
+ collection.addSchemaFromReader(readerOf("src/test/derived/deriver/child.sd"));
+ collection.addSchemaFromReader(readerOf("src/test/derived/deriver/grandparent.sd"));
+ collection.addSchemaFromReader(readerOf("src/test/derived/deriver/parent.sd"));
+ var schemes = collection.getParsedSchemas();
+ assertEquals(schemes.size(), 3);
+ var schema = schemes.get("child");
+ assertTrue(schema != null);
+ assertEquals(schema.name(), "child");
+ schema = schemes.get("parent");
+ assertTrue(schema != null);
+ assertEquals(schema.name(), "parent");
+ schema = schemes.get("grandparent");
+ assertTrue(schema != null);
+ assertEquals(schema.name(), "grandparent");
+ }
+
+ ParsedRankProfile get(List<ParsedRankProfile> all, String name) {
+ for (var rp : all) {
+ if (rp.name().equals(name)) return rp;
+ }
+ return null;
+ }
+
+ @Test
+ public void can_add_extra_rank_profiles() throws Exception {
+ var collection = new IntermediateCollection();
+ collection.addSchemaFromFile("src/test/derived/rankprofilemodularity/test.sd");
+ collection.addRankProfileFile("test", "src/test/derived/rankprofilemodularity/test/outside_schema1.profile");
+ collection.addRankProfileFile("test", readerOf("src/test/derived/rankprofilemodularity/test/subdirectory/outside_schema2.profile"));
+ var schemes = collection.getParsedSchemas();
+ assertEquals(schemes.size(), 1);
+ var schema = schemes.get("test");
+ assertTrue(schema != null);
+ assertEquals(schema.name(), "test");
+ var rankProfiles = schema.getRankProfiles();
+ assertEquals(rankProfiles.size(), 7);
+ var outside = get(rankProfiles, "outside_schema1");
+ assertTrue(outside != null);
+ assertEquals(outside.name(), "outside_schema1");
+ var functions = outside.getFunctions();
+ assertEquals(functions.size(), 1);
+ assertEquals(functions.get(0).name(), "fo1");
+ outside = get(rankProfiles, "outside_schema2");
+ assertTrue(outside != null);
+ assertEquals(outside.name(), "outside_schema2");
+ functions = outside.getFunctions();
+ assertEquals(functions.size(), 1);
+ assertEquals(functions.get(0).name(), "fo2");
+ }
+
+ @Test
+ public void name_mismatch_throws() throws Exception {
+ var collection = new IntermediateCollection();
+ var ex = assertThrows(IllegalArgumentException.class, () ->
+ collection.addSchemaFromReader(readerOf("src/test/cfg/application/sdfilenametest/schemas/notmusic.sd")));
+ assertEquals("The file containing schema 'music' must be named 'music.sd', was 'notmusic.sd'",
+ ex.getMessage());
+ }
+
+ @Test
+ public void bad_parse_throws() throws Exception {
+ var collection = new IntermediateCollection();
+ var ex = assertThrows(ParseException.class, () ->
+ collection.addSchemaFromFile("src/test/examples/badparse.sd"));
+ assertTrue(ex.getMessage().startsWith("Failed parsing schema from src/test/examples/badparse.sd: Encountered"));
+ ex = assertThrows(ParseException.class, () ->
+ collection.addSchemaFromReader(readerOf("src/test/examples/badparse.sd")));
+ assertTrue(ex.getMessage().startsWith("Failed parsing schema from src/test/examples/badparse.sd: Encountered"));
+ collection.addSchemaFromFile("src/test/derived/rankprofilemodularity/test.sd");
+ collection.addRankProfileFile("test", "src/test/derived/rankprofilemodularity/test/outside_schema1.profile");
+ ex = assertThrows(ParseException.class, () ->
+ collection.addRankProfileFile("test", "src/test/examples/badparse.sd"));
+ assertTrue(ex.getMessage().startsWith("Failed parsing rank-profile from src/test/examples/badparse.sd: Encountered"));
+ }
+
+ @Test
+ public void can_resolve_document_inheritance() throws Exception {
+ var collection = new IntermediateCollection();
+ collection.addSchemaFromFile("src/test/derived/deriver/child.sd");
+ collection.addSchemaFromFile("src/test/derived/deriver/grandparent.sd");
+ collection.addSchemaFromFile("src/test/derived/deriver/parent.sd");
+ collection.resolveInternalConnections();
+ var schemes = collection.getParsedSchemas();
+ assertEquals(schemes.size(), 3);
+ var childDoc = schemes.get("child").getDocument();
+ var inherits = childDoc.getResolvedInherits();
+ assertEquals(inherits.size(), 1);
+ var parentDoc = inherits.get(0);
+ assertEquals(parentDoc.name(), "parent");
+ inherits = parentDoc.getResolvedInherits();
+ assertEquals(inherits.size(), 1);
+ assertEquals(inherits.get(0).name(), "grandparent");
+ }
+
+ @Test
+ public void can_detect_schema_inheritance_cycles() throws Exception {
+ var collection = new IntermediateCollection();
+ collection.addSchemaFromString("schema foo inherits bar { document foo {} }");
+ collection.addSchemaFromString("schema bar inherits qux { document bar {} }");
+ collection.addSchemaFromString("schema qux inherits foo { document qux {} }");
+ assertEquals(collection.getParsedSchemas().size(), 3);
+ var ex = assertThrows(IllegalArgumentException.class, () ->
+ collection.resolveInternalConnections());
+ assertTrue(ex.getMessage().startsWith("Inheritance/reference cycle for schemas: "));
+ }
+
+ @Test
+ public void can_detect_document_inheritance_cycles() throws Exception {
+ var collection = new IntermediateCollection();
+ collection.addSchemaFromString("schema foo { document foo inherits bar {} }");
+ collection.addSchemaFromString("schema bar { document bar inherits qux {} }");
+ collection.addSchemaFromString("schema qux { document qux inherits foo {} }");
+ assertEquals(collection.getParsedSchemas().size(), 3);
+ var ex = assertThrows(IllegalArgumentException.class, () ->
+ collection.resolveInternalConnections());
+ System.err.println("ex: "+ex.getMessage());
+ assertTrue(ex.getMessage().startsWith("Inheritance/reference cycle for documents: "));
+ }
+
+ @Test
+ public void can_detect_missing_doc() throws Exception {
+ var collection = new IntermediateCollection();
+ collection.addSchemaFromString("schema foo { document foo inherits bar {} }");
+ collection.addSchemaFromString("schema qux { document qux inherits foo {} }");
+ assertEquals(collection.getParsedSchemas().size(), 2);
+ var ex = assertThrows(IllegalArgumentException.class, () ->
+ collection.resolveInternalConnections());
+ assertEquals("document foo inherits from unavailable document bar", ex.getMessage());
+ }
+
+ @Test
+ public void can_detect_document_reference_cycle() throws Exception {
+ var collection = new IntermediateCollection();
+ collection.addSchemaFromString("schema foo { document foo { field oneref type reference<bar> {} } }");
+ collection.addSchemaFromString("schema bar { document bar { field tworef type reference<foo> {} } }");
+ assertEquals(collection.getParsedSchemas().size(), 2);
+ var ex = assertThrows(IllegalArgumentException.class, () ->
+ collection.resolveInternalConnections());
+ System.err.println("ex: "+ex.getMessage());
+ assertTrue(ex.getMessage().startsWith("Inheritance/reference cycle for documents: "));
+ }
+
+ @Test
+ public void can_detect_cycles_with_reference() throws Exception {
+ var collection = new IntermediateCollection();
+ collection.addSchemaFromString("schema foo { document foodoc inherits bardoc {} }");
+ collection.addSchemaFromString("schema bar { document bardoc { field myref type reference<qux> { } } }");
+ collection.addSchemaFromString("schema qux inherits foo { document qux inherits foodoc {} }");
+ assertEquals(collection.getParsedSchemas().size(), 3);
+ var ex = assertThrows(IllegalArgumentException.class, () ->
+ collection.resolveInternalConnections());
+ System.err.println("ex: "+ex.getMessage());
+ assertTrue(ex.getMessage().startsWith("Inheritance/reference cycle for documents: "));
+ }
+
+
+}
diff --git a/config-model/src/test/java/com/yahoo/schema/parser/ParsedDocumentTestCase.java b/config-model/src/test/java/com/yahoo/schema/parser/ParsedDocumentTestCase.java
new file mode 100644
index 00000000000..9245b64b09e
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/schema/parser/ParsedDocumentTestCase.java
@@ -0,0 +1,30 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.schema.parser;
+
+import org.junit.Test;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThrows;
+
+/**
+ * @author arnej
+ */
+public class ParsedDocumentTestCase {
+
+ @Test
+ public void fields_can_be_added_once() throws Exception {
+ var doc = new ParsedDocument("foo");
+ var stringType = ParsedType.fromName("string");
+ doc.addField(new ParsedField("bar1", stringType));
+ doc.addField(new ParsedField("zap", stringType));
+ doc.addField(new ParsedField("bar2", stringType));
+ doc.addField(new ParsedField("bar3", stringType));
+ var e = assertThrows(IllegalArgumentException.class, () ->
+ doc.addField(new ParsedField("zap", stringType)));
+ System.err.println("As expected: "+e);
+ assertEquals("document 'foo' error: Duplicate (case insensitively) field 'zap' in document type 'foo'", e.getMessage());
+ e = assertThrows(IllegalArgumentException.class, () ->
+ doc.addField(new ParsedField("ZAP", stringType)));
+ assertEquals("document 'foo' error: Duplicate (case insensitively) field 'ZAP' in document type 'foo'", e.getMessage());
+ }
+
+}
diff --git a/config-model/src/test/java/com/yahoo/schema/parser/SchemaParserTestCase.java b/config-model/src/test/java/com/yahoo/schema/parser/SchemaParserTestCase.java
new file mode 100644
index 00000000000..d4e4f1dbb88
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/schema/parser/SchemaParserTestCase.java
@@ -0,0 +1,276 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.schema.parser;
+
+import com.yahoo.config.model.application.provider.BaseDeployLogger;
+import com.yahoo.config.model.deploy.TestProperties;
+import com.yahoo.io.IOUtils;
+import static com.yahoo.config.model.test.TestUtil.joinLines;
+
+import java.io.File;
+
+import org.junit.Test;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.assertThrows;
+
+/**
+ * @author arnej
+ */
+public class SchemaParserTestCase {
+
+ ParsedSchema parseString(String input) throws Exception {
+ var deployLogger = new BaseDeployLogger();
+ var modelProperties = new TestProperties();
+ var stream = new SimpleCharStream(input);
+ try {
+ var parser = new SchemaParser(stream, deployLogger, modelProperties);
+ return parser.schema();
+ } catch (ParseException pe) {
+ throw new ParseException(stream.formatException(pe.getMessage()));
+ }
+ }
+
+ ParsedSchema parseFile(String fileName) throws Exception {
+ File file = new File(fileName);
+ return parseString(IOUtils.readFile(file));
+ }
+
+ @Test
+ public void minimal_schema_can_be_parsed() throws Exception {
+ String input = joinLines
+ ("schema foo {",
+ " document foo {",
+ " }",
+ "}");
+ ParsedSchema schema = parseString(input);
+ assertEquals("foo", schema.name());
+ assertTrue(schema.hasDocument());
+ assertEquals("foo", schema.getDocument().name());
+ }
+
+ @Test
+ public void document_only_can_be_parsed() throws Exception {
+ String input = joinLines
+ ("document bar {",
+ "}");
+ ParsedSchema schema = parseString(input);
+ assertEquals("bar", schema.name());
+ assertTrue(schema.hasDocument());
+ assertEquals("bar", schema.getDocument().name());
+ }
+
+ @Test
+ public void multiple_documents_disallowed() {
+ String input = joinLines
+ ("schema foo {",
+ " document foo {",
+ " }",
+ " document foo2 {",
+ " }",
+ "}");
+ var e = assertThrows(IllegalArgumentException.class, () -> parseString(input));
+ assertEquals("schema 'foo' error: already has document 'foo' so cannot add document 'foo2'", e.getMessage());
+ }
+
+ @Test
+ public void backwards_path_is_disallowed() {
+ assertEquals("'..' is not allowed in path",
+ assertThrows(IllegalArgumentException.class,
+ () -> parseString("schema foo {\n" +
+ " constant my_constant_tensor {\n" +
+ " file: foo/../bar\n" +
+ " type: tensor<float>(x{},y{})\n" +
+ " }\n" +
+ "}\n")).getMessage());
+ }
+
+ void checkFileParses(String fileName) throws Exception {
+ var schema = parseFile(fileName);
+ assertNotNull(schema);
+ assertNotNull(schema.name());
+ assertNotEquals("", schema.name());
+ }
+
+ // TODO: Many (all)? of the files below are parsed from other tests and can be removed from here
+ @Test
+ public void parse_various_old_sdfiles() throws Exception {
+ checkFileParses("src/test/cfg/search/data/travel/schemas/TTData.sd");
+ checkFileParses("src/test/cfg/search/data/travel/schemas/TTEdge.sd");
+ checkFileParses("src/test/cfg/search/data/travel/schemas/TTPOI.sd");
+ checkFileParses("src/test/configmodel/types/other_doc.sd");
+ checkFileParses("src/test/configmodel/types/types.sd");
+ checkFileParses("src/test/configmodel/types/type_with_doc_field.sd");
+ checkFileParses("src/test/derived/advanced/advanced.sd");
+ checkFileParses("src/test/derived/annotationsimplicitstruct/annotationsimplicitstruct.sd");
+ checkFileParses("src/test/derived/annotationsinheritance2/annotationsinheritance2.sd");
+ checkFileParses("src/test/derived/annotationsinheritance/annotationsinheritance.sd");
+ checkFileParses("src/test/derived/annotationsoutsideofdocument/annotationsoutsideofdocument.sd");
+ checkFileParses("src/test/derived/annotationspolymorphy/annotationspolymorphy.sd");
+ checkFileParses("src/test/derived/annotationsreference2/annotationsreference2.sd");
+ checkFileParses("src/test/derived/annotationsreference/annotationsreference.sd");
+ checkFileParses("src/test/derived/annotationssimple/annotationssimple.sd");
+ checkFileParses("src/test/derived/annotationsstruct/annotationsstruct.sd");
+ checkFileParses("src/test/derived/annotationsstructarray/annotationsstructarray.sd");
+ checkFileParses("src/test/derived/array_of_struct_attribute/test.sd");
+ checkFileParses("src/test/derived/arrays/arrays.sd");
+ checkFileParses("src/test/derived/attributeprefetch/attributeprefetch.sd");
+ checkFileParses("src/test/derived/attributerank/attributerank.sd");
+ checkFileParses("src/test/derived/attributes/attributes.sd");
+ checkFileParses("src/test/derived/combinedattributeandindexsearch/combinedattributeandindexsearch.sd");
+ checkFileParses("src/test/derived/complex/complex.sd");
+ checkFileParses("src/test/derived/deriver/child.sd");
+ checkFileParses("src/test/derived/deriver/grandparent.sd");
+ checkFileParses("src/test/derived/deriver/parent.sd");
+ checkFileParses("src/test/derived/emptychild/child.sd");
+ checkFileParses("src/test/derived/emptychild/parent.sd");
+ checkFileParses("src/test/derived/emptydefault/emptydefault.sd");
+ checkFileParses("src/test/derived/exactmatch/exactmatch.sd");
+ checkFileParses("src/test/derived/fieldset/test.sd");
+ checkFileParses("src/test/derived/flickr/flickrphotos.sd");
+ checkFileParses("src/test/derived/function_arguments/test.sd");
+ checkFileParses("src/test/derived/function_arguments_with_expressions/test.sd");
+ checkFileParses("src/test/derived/gemini2/gemini.sd");
+ checkFileParses("src/test/derived/hnsw_index/test.sd");
+ checkFileParses("src/test/derived/id/id.sd");
+ checkFileParses("src/test/derived/importedfields/child.sd");
+ checkFileParses("src/test/derived/importedfields/grandparent.sd");
+ checkFileParses("src/test/derived/imported_fields_inherited_reference/child_a.sd");
+ checkFileParses("src/test/derived/imported_fields_inherited_reference/child_b.sd");
+ checkFileParses("src/test/derived/imported_fields_inherited_reference/child_c.sd");
+ checkFileParses("src/test/derived/imported_fields_inherited_reference/parent.sd");
+ checkFileParses("src/test/derived/importedfields/parent_a.sd");
+ checkFileParses("src/test/derived/importedfields/parent_b.sd");
+ checkFileParses("src/test/derived/imported_position_field/child.sd");
+ checkFileParses("src/test/derived/imported_position_field/parent.sd");
+ checkFileParses("src/test/derived/imported_position_field_summary/child.sd");
+ checkFileParses("src/test/derived/imported_position_field_summary/parent.sd");
+ checkFileParses("src/test/derived/imported_struct_fields/child.sd");
+ checkFileParses("src/test/derived/imported_struct_fields/parent.sd");
+ checkFileParses("src/test/derived/indexinfo_fieldsets/indexinfo_fieldsets.sd");
+ checkFileParses("src/test/derived/indexinfo_lowercase/indexinfo_lowercase.sd");
+ checkFileParses("src/test/derived/indexschema/indexschema.sd");
+ checkFileParses("src/test/derived/indexswitches/indexswitches.sd");
+ checkFileParses("src/test/derived/inheritance/child.sd");
+ checkFileParses("src/test/derived/inheritance/father.sd");
+ checkFileParses("src/test/derived/inheritance/grandparent.sd");
+ checkFileParses("src/test/derived/inheritance/mother.sd");
+ checkFileParses("src/test/derived/inheritdiamond/child.sd");
+ checkFileParses("src/test/derived/inheritdiamond/father.sd");
+ checkFileParses("src/test/derived/inheritdiamond/grandparent.sd");
+ checkFileParses("src/test/derived/inheritdiamond/mother.sd");
+ checkFileParses("src/test/derived/inheritfromgrandparent/child.sd");
+ checkFileParses("src/test/derived/inheritfromgrandparent/grandparent.sd");
+ checkFileParses("src/test/derived/inheritfromgrandparent/parent.sd");
+ checkFileParses("src/test/derived/inheritfromnull/inheritfromnull.sd");
+ checkFileParses("src/test/derived/inheritfromparent/child.sd");
+ checkFileParses("src/test/derived/inheritfromparent/parent.sd");
+ checkFileParses("src/test/derived/inheritstruct/child.sd");
+ checkFileParses("src/test/derived/inheritstruct/parent.sd");
+ checkFileParses("src/test/derived/integerattributetostringindex/integerattributetostringindex.sd");
+ checkFileParses("src/test/derived/language/language.sd");
+ checkFileParses("src/test/derived/lowercase/lowercase.sd");
+ checkFileParses("src/test/derived/mail/mail.sd");
+ checkFileParses("src/test/derived/map_attribute/test.sd");
+ checkFileParses("src/test/derived/map_of_struct_attribute/test.sd");
+ checkFileParses("src/test/derived/mlr/mlr.sd");
+ checkFileParses("src/test/derived/multiplesummaries/multiplesummaries.sd");
+ checkFileParses("src/test/derived/music3/music3.sd");
+ checkFileParses("src/test/derived/music/music.sd");
+ checkFileParses("src/test/derived/namecollision/collision.sd");
+ checkFileParses("src/test/derived/namecollision/collisionstruct.sd");
+ checkFileParses("src/test/derived/nearestneighbor/test.sd");
+ checkFileParses("src/test/derived/newrank/newrank.sd");
+ checkFileParses("src/test/derived/nuwa/newsindex.sd");
+ checkFileParses("src/test/derived/orderilscripts/orderilscripts.sd");
+ checkFileParses("src/test/derived/position_array/position_array.sd");
+ checkFileParses("src/test/derived/position_attribute/position_attribute.sd");
+ checkFileParses("src/test/derived/position_extra/position_extra.sd");
+ checkFileParses("src/test/derived/position_nosummary/position_nosummary.sd");
+ checkFileParses("src/test/derived/position_summary/position_summary.sd");
+ checkFileParses("src/test/derived/predicate_attribute/predicate_attribute.sd");
+ checkFileParses("src/test/derived/prefixexactattribute/prefixexactattribute.sd");
+ checkFileParses("src/test/derived/rankingexpression/rankexpression.sd");
+ checkFileParses("src/test/derived/rankprofileinheritance/child.sd");
+ checkFileParses("src/test/derived/rankprofileinheritance/parent1.sd");
+ checkFileParses("src/test/derived/rankprofileinheritance/parent2.sd");
+ checkFileParses("src/test/derived/rankprofilemodularity/test.sd");
+ checkFileParses("src/test/derived/rankprofiles/rankprofiles.sd");
+ checkFileParses("src/test/derived/rankproperties/rankproperties.sd");
+ checkFileParses("src/test/derived/ranktypes/ranktypes.sd");
+ checkFileParses("src/test/derived/reference_fields/ad.sd");
+ checkFileParses("src/test/derived/reference_fields/campaign.sd");
+ checkFileParses("src/test/derived/renamedfeatures/foo.sd");
+ checkFileParses("src/test/derived/reserved_position/reserved_position.sd");
+ checkFileParses("src/test/derived/schemainheritance/child.sd");
+ checkFileParses("src/test/derived/schemainheritance/importedschema.sd");
+ checkFileParses("src/test/derived/schemainheritance/parent.sd");
+ checkFileParses("src/test/derived/slice/test.sd");
+ checkFileParses("src/test/derived/streamingjuniper/streamingjuniper.sd");
+ checkFileParses("src/test/derived/streamingstructdefault/streamingstructdefault.sd");
+ checkFileParses("src/test/derived/streamingstruct/streamingstruct.sd");
+ checkFileParses("src/test/derived/structandfieldset/test.sd");
+ checkFileParses("src/test/derived/structanyorder/structanyorder.sd");
+ checkFileParses("src/test/derived/structinheritance/bad.sd");
+ checkFileParses("src/test/derived/structinheritance/simple.sd");
+ checkFileParses("src/test/derived/tensor2/first.sd");
+ checkFileParses("src/test/derived/tensor2/second.sd");
+ checkFileParses("src/test/derived/tensor/tensor.sd");
+ checkFileParses("src/test/derived/tokenization/tokenization.sd");
+ checkFileParses("src/test/derived/twostreamingstructs/streamingstruct.sd");
+ checkFileParses("src/test/derived/twostreamingstructs/whatever.sd");
+ checkFileParses("src/test/derived/types/types.sd");
+ checkFileParses("src/test/derived/uri_array/uri_array.sd");
+ checkFileParses("src/test/derived/uri_wset/uri_wset.sd");
+ checkFileParses("src/test/examples/arrays.sd");
+ checkFileParses("src/test/examples/arraysweightedsets.sd");
+ checkFileParses("src/test/examples/attributeposition.sd");
+ checkFileParses("src/test/examples/attributesettings.sd");
+ checkFileParses("src/test/examples/attributesexactmatch.sd");
+ checkFileParses("src/test/examples/casing.sd");
+ checkFileParses("src/test/examples/comment.sd");
+ checkFileParses("src/test/examples/documentidinsummary.sd");
+ checkFileParses("src/test/examples/fieldoftypedocument.sd");
+ checkFileParses("src/test/examples/implicitsummaries_attribute.sd");
+ checkFileParses("src/test/examples/implicitsummaryfields.sd");
+ checkFileParses("src/test/examples/incorrectrankingexpressionfileref.sd");
+ checkFileParses("src/test/examples/indexing_extra.sd");
+ checkFileParses("src/test/examples/indexing_modify_field_no_output.sd");
+ checkFileParses("src/test/examples/indexing.sd");
+ checkFileParses("src/test/examples/indexrewrite.sd");
+ checkFileParses("src/test/examples/indexsettings.sd");
+ checkFileParses("src/test/examples/integerindex2attribute.sd");
+ checkFileParses("src/test/examples/invalidimplicitsummarysource.sd");
+ checkFileParses("src/test/examples/multiplesummaries.sd");
+ checkFileParses("src/test/examples/music.sd");
+ checkFileParses("src/test/examples/nextgen/boldedsummaryfields.sd");
+ checkFileParses("src/test/examples/nextgen/dynamicsummaryfields.sd");
+ checkFileParses("src/test/examples/nextgen/extrafield.sd");
+ checkFileParses("src/test/examples/nextgen/implicitstructtypes.sd");
+ checkFileParses("src/test/examples/nextgen/simple.sd");
+ checkFileParses("src/test/examples/nextgen/summaryfield.sd");
+ checkFileParses("src/test/examples/nextgen/toggleon.sd");
+ checkFileParses("src/test/examples/nextgen/untransformedsummaryfields.sd");
+ checkFileParses("src/test/examples/ngram.sd");
+ checkFileParses("src/test/examples/outsidedoc.sd");
+ checkFileParses("src/test/examples/outsidesummary.sd");
+ checkFileParses("src/test/examples/position_array.sd");
+ checkFileParses("src/test/examples/position_attribute.sd");
+ checkFileParses("src/test/examples/position_base.sd");
+ checkFileParses("src/test/examples/position_extra.sd");
+ checkFileParses("src/test/examples/position_index.sd");
+ checkFileParses("src/test/examples/position_inherited.sd");
+ checkFileParses("src/test/examples/position_summary.sd");
+ checkFileParses("src/test/examples/rankmodifier/literal.sd");
+ checkFileParses("src/test/examples/rankpropvars.sd");
+ checkFileParses("src/test/examples/reserved_words_as_field_names.sd");
+ checkFileParses("src/test/examples/simple.sd");
+ checkFileParses("src/test/examples/stemmingdefault.sd");
+ checkFileParses("src/test/examples/stemmingsetting.sd");
+ checkFileParses("src/test/examples/strange.sd");
+ checkFileParses("src/test/examples/struct.sd");
+ checkFileParses("src/test/examples/summaryfieldcollision.sd");
+ checkFileParses("src/test/examples/weightedset-summaryto.sd");
+ }
+}
diff --git a/config-model/src/test/java/com/yahoo/schema/processing/AddAttributeTransformToSummaryOfImportedFieldsTest.java b/config-model/src/test/java/com/yahoo/schema/processing/AddAttributeTransformToSummaryOfImportedFieldsTest.java
new file mode 100644
index 00000000000..0d64dd5c953
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/schema/processing/AddAttributeTransformToSummaryOfImportedFieldsTest.java
@@ -0,0 +1,75 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.schema.processing;
+
+import com.yahoo.config.model.application.provider.MockFileRegistry;
+import com.yahoo.config.model.deploy.TestProperties;
+import com.yahoo.config.model.test.MockApplicationPackage;
+import com.yahoo.document.DataType;
+import com.yahoo.document.Field;
+import com.yahoo.schema.DocumentReference;
+import com.yahoo.schema.Schema;
+import com.yahoo.schema.derived.TestableDeployLogger;
+import com.yahoo.schema.document.ImportedField;
+import com.yahoo.schema.document.ImportedFields;
+import com.yahoo.schema.document.ImportedSimpleField;
+import com.yahoo.schema.document.SDDocumentType;
+import com.yahoo.schema.document.SDField;
+import com.yahoo.vespa.documentmodel.DocumentSummary;
+import com.yahoo.vespa.documentmodel.SummaryField;
+import com.yahoo.vespa.documentmodel.SummaryTransform;
+import org.junit.Test;
+
+import java.util.Collections;
+
+import static org.junit.Assert.assertEquals;
+
+/**
+ * @author bjorncs
+ */
+public class AddAttributeTransformToSummaryOfImportedFieldsTest {
+
+ private static final String IMPORTED_FIELD_NAME = "imported_myfield";
+ private static final String DOCUMENT_NAME = "mydoc";
+ private static final String SUMMARY_NAME = "mysummary";
+
+ @Test
+ public void attribute_summary_transform_applied_to_summary_field_of_imported_field() {
+ Schema schema = createSearchWithDocument(DOCUMENT_NAME);
+ schema.setImportedFields(createSingleImportedField(IMPORTED_FIELD_NAME));
+ schema.addSummary(createDocumentSummary(IMPORTED_FIELD_NAME, schema));
+
+ AddAttributeTransformToSummaryOfImportedFields processor = new AddAttributeTransformToSummaryOfImportedFields(
+ schema, null, null, null);
+ processor.process(true, false);
+ SummaryField summaryField = schema.getSummaries().get(SUMMARY_NAME).getSummaryField(IMPORTED_FIELD_NAME);
+ SummaryTransform actualTransform = summaryField.getTransform();
+ assertEquals(SummaryTransform.ATTRIBUTE, actualTransform);
+ }
+
+ private static Schema createSearch(String documentType) {
+ return new Schema(documentType, MockApplicationPackage.createEmpty(), new MockFileRegistry(), new TestableDeployLogger(), new TestProperties());
+ }
+
+ private static Schema createSearchWithDocument(String documentName) {
+ Schema schema = createSearch(documentName);
+ SDDocumentType document = new SDDocumentType(documentName, schema);
+ schema.addDocument(document);
+ return schema;
+ }
+
+ private static ImportedFields createSingleImportedField(String fieldName) {
+ Schema targetSchema = createSearchWithDocument("target_doc");
+ var doc = targetSchema.getDocument();
+ SDField targetField = new SDField(doc, "target_field", DataType.INT);
+ DocumentReference documentReference = new DocumentReference(new Field("reference_field"), targetSchema);
+ ImportedField importedField = new ImportedSimpleField(fieldName, documentReference, targetField);
+ return new ImportedFields(Collections.singletonMap(fieldName, importedField));
+ }
+
+ private static DocumentSummary createDocumentSummary(String fieldName, Schema schema) {
+ DocumentSummary summary = new DocumentSummary("mysummary", schema);
+ summary.add(new SummaryField(fieldName, DataType.INT));
+ return summary;
+ }
+
+}
diff --git a/config-model/src/test/java/com/yahoo/schema/processing/AdjustPositionSummaryFieldsTestCase.java b/config-model/src/test/java/com/yahoo/schema/processing/AdjustPositionSummaryFieldsTestCase.java
new file mode 100644
index 00000000000..103d08b39a8
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/schema/processing/AdjustPositionSummaryFieldsTestCase.java
@@ -0,0 +1,260 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.schema.processing;
+
+import com.yahoo.document.DataType;
+import com.yahoo.document.PositionDataType;
+import com.yahoo.schema.Schema;
+import com.yahoo.vespa.documentmodel.DocumentSummary;
+import com.yahoo.vespa.documentmodel.SummaryField;
+import com.yahoo.vespa.documentmodel.SummaryTransform;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+
+public class AdjustPositionSummaryFieldsTestCase {
+
+ @Test
+ public void test_pos_summary() {
+ SearchModel model = new SearchModel(false);
+ model.addSummaryField("my_pos", PositionDataType.INSTANCE, null, "pos");
+ model.resolve();
+ model.assertSummaryField("my_pos", PositionDataType.INSTANCE, SummaryTransform.GEOPOS, "pos_zcurve");
+ model.assertSummaryField("my_pos.position", DataType.getArray(DataType.STRING), SummaryTransform.POSITIONS, "pos_zcurve");
+ model.assertSummaryField("my_pos.distance", DataType.INT, SummaryTransform.DISTANCE, "pos_zcurve");
+ }
+
+ @Test
+ public void test_imported_pos_summary() {
+ SearchModel model = new SearchModel();
+ model.addSummaryField("my_pos", PositionDataType.INSTANCE, null, null);
+ model.resolve();
+ model.assertSummaryField("my_pos", PositionDataType.INSTANCE, SummaryTransform.GEOPOS, "my_pos_zcurve");
+ model.assertSummaryField("my_pos.position", DataType.getArray(DataType.STRING), SummaryTransform.POSITIONS, "my_pos_zcurve");
+ model.assertSummaryField("my_pos.distance", DataType.INT, SummaryTransform.DISTANCE, "my_pos_zcurve");
+ }
+
+ @Test
+ public void test_imported_pos_summary_bad_source() {
+ SearchModel model = new SearchModel();
+ model.addSummaryField("my_pos", PositionDataType.INSTANCE, null, "pos");
+ model.resolve();
+ // SummaryFieldsMustHaveValidSource processing not run in this test.
+ model.assertSummaryField("my_pos", PositionDataType.INSTANCE, SummaryTransform.NONE, "pos");
+ model.assertNoSummaryField("my_pos.position");
+ model.assertNoSummaryField("my_pos.distance");
+ }
+
+ @Test
+ public void test_imported_pos_summary_bad_datatype() {
+ SearchModel model = new SearchModel();
+ model.addSummaryField("my_pos", DataType.getArray(PositionDataType.INSTANCE), null, "pos");
+ model.resolve();
+ model.assertSummaryField("my_pos", DataType.getArray(PositionDataType.INSTANCE), SummaryTransform.NONE, "pos");
+ model.assertNoSummaryField("my_pos.position");
+ model.assertNoSummaryField("my_pos.distance");
+ }
+
+ @Test
+ public void test_pos_summary_no_attr_no_rename() {
+ SearchModel model = new SearchModel(false, false, false);
+ model.addSummaryField("pos", PositionDataType.INSTANCE, null, "pos");
+ model.resolve();
+ model.assertSummaryField("pos", PositionDataType.INSTANCE, SummaryTransform.NONE, "pos");
+ model.assertNoSummaryField("pos.position");
+ model.assertNoSummaryField("pos.distance");
+ }
+
+ @Test
+ public void test_pos_default_summary_no_attr_no_rename() {
+ SearchModel model = new SearchModel(false, false, false);
+ model.resolve();
+ assertNull(model.childSchema.getSummary("default")); // ImplicitSummaries processing not run in this test
+ }
+
+ @Test
+ public void test_pos_summary_no_rename() {
+ SearchModel model = new SearchModel(false, true, false);
+ model.addSummaryField("pos", PositionDataType.INSTANCE, null, "pos");
+ model.resolve();
+ model.assertSummaryField("pos", PositionDataType.INSTANCE, SummaryTransform.GEOPOS, "pos_zcurve");
+ model.assertSummaryField("pos.position", DataType.getArray(DataType.STRING), SummaryTransform.POSITIONS, "pos_zcurve");
+ model.assertSummaryField("pos.distance", DataType.INT, SummaryTransform.DISTANCE, "pos_zcurve");
+ }
+
+ @SuppressWarnings("deprecation")
+ @Rule
+ public final ExpectedException exceptionRule = ExpectedException.none();
+
+ @Test
+ public void test_pos_summary_no_attr() {
+ exceptionRule.expect(IllegalArgumentException.class);
+ exceptionRule.expectMessage("For schema 'child', field 'my_pos': No position attribute 'pos_zcurve'");
+ SearchModel model = new SearchModel(false, false, false);
+ model.addSummaryField("my_pos", PositionDataType.INSTANCE, null, "pos");
+ model.resolve();
+ }
+
+ @Test
+ public void test_pos_summary_bad_attr() {
+ exceptionRule.expect(IllegalArgumentException.class);
+ exceptionRule.expectMessage("For schema 'child', field 'my_pos': No position attribute 'pos_zcurve'");
+ SearchModel model = new SearchModel(false, false, true);
+ model.addSummaryField("my_pos", PositionDataType.INSTANCE, null, "pos");
+ model.resolve();
+ }
+
+ @Test
+ public void test_imported_pos_summary_no_attr() {
+ exceptionRule.expect(IllegalArgumentException.class);
+ exceptionRule.expectMessage("For schema 'child', import field 'my_pos_zcurve': "
+ + "Field 'pos_zcurve' via reference field 'ref': Not found");
+ SearchModel model = new SearchModel(true, false, false);
+ model.addSummaryField("my_pos", PositionDataType.INSTANCE, null, null);
+ model.resolve();
+ }
+
+ @Test
+ public void test_imported_pos_summary_bad_attr() {
+ exceptionRule.expect(IllegalArgumentException.class);
+ exceptionRule.expectMessage("For schema 'child', field 'my_pos': "
+ + "No position attribute 'my_pos_zcurve'");
+ SearchModel model = new SearchModel(true, false, true);
+ model.addSummaryField("my_pos", PositionDataType.INSTANCE, null, null);
+ model.resolve();
+ }
+
+ @Test
+ public void test_my_pos_position_summary_bad_datatype() {
+ exceptionRule.expect(IllegalArgumentException.class);
+ exceptionRule.expectMessage("For schema 'child', field 'my_pos.position': "
+ + "exists with type 'datatype string (code: 2)', should be of type 'datatype Array<string> (code: -1486737430)");
+ SearchModel model = new SearchModel();
+ model.addSummaryField("my_pos", PositionDataType.INSTANCE, null, null);
+ model.addSummaryField("my_pos.position", DataType.STRING, null, "pos");
+ model.resolve();
+ }
+
+ @Test
+ public void test_my_pos_position_summary_bad_transform() {
+ exceptionRule.expect(IllegalArgumentException.class);
+ exceptionRule.expectMessage("For schema 'child', field 'my_pos.position': "
+ + "has summary transform 'none', should have transform 'positions'");
+ SearchModel model = new SearchModel();
+ model.addSummaryField("my_pos", PositionDataType.INSTANCE, null, null);
+ model.addSummaryField("my_pos.position", DataType.getArray(DataType.STRING), null, "pos");
+ model.resolve();
+ }
+
+ @Test
+ public void test_my_pos_position_summary_bad_source() {
+ exceptionRule.expect(IllegalArgumentException.class);
+ exceptionRule.expectMessage("For schema 'child', field 'my_pos.position': "
+ + "has source '[source field 'pos']', should have source 'source field 'my_pos_zcurve''");
+ SearchModel model = new SearchModel();
+ model.addSummaryField("my_pos", PositionDataType.INSTANCE, null, null);
+ model.addSummaryField("my_pos.position", DataType.getArray(DataType.STRING), SummaryTransform.POSITIONS, "pos");
+ model.resolve();
+ }
+
+ static class SearchModel extends ParentChildSearchModel {
+
+ SearchModel() {
+ this(true);
+ }
+
+ SearchModel(boolean importedPos) {
+ this(importedPos, true, false);
+ }
+
+ SearchModel(boolean importedPos, boolean setupPosAttr, boolean setupBadAttr) {
+ super();
+ if (importedPos) {
+ createPositionField(parentSchema, setupPosAttr, setupBadAttr);
+ }
+ addRefField(childSchema, parentSchema, "ref");
+ if (importedPos) {
+ addImportedField("my_pos", "ref", "pos");
+ } else {
+ createPositionField(childSchema, setupPosAttr, setupBadAttr);
+ }
+ }
+
+ private void createPositionField(Schema schema, boolean setupPosAttr, boolean setupBadAttr) {
+ String ilScript = setupPosAttr ? "{ summary | attribute }" : "{ summary }";
+ var doc = schema.getDocument();
+ doc.addField(createField(doc, "pos", PositionDataType.INSTANCE, ilScript));
+ if (setupBadAttr) {
+ doc.addField(createField(doc, "pos_zcurve", DataType.LONG, "{ attribute }"));
+ }
+ }
+
+ void addSummaryField(String fieldName, DataType dataType, SummaryTransform transform, String source) {
+ addSummaryField("my_summary", fieldName, dataType, transform, source);
+ }
+
+ public void addSummaryField(String summaryName, String fieldName, DataType dataType, SummaryTransform transform, String source) {
+ DocumentSummary summary = childSchema.getSummary(summaryName);
+ if (summary == null) {
+ summary = new DocumentSummary(summaryName, childSchema);
+ childSchema.addSummary(summary);
+ }
+ SummaryField summaryField = new SummaryField(fieldName, dataType);
+ if (source != null) {
+ summaryField.addSource(source);
+ }
+ if (transform != null) {
+ summaryField.setTransform(transform);
+ }
+ summary.add(summaryField);
+ }
+
+ public void assertNoSummaryField(String fieldName) {
+ assertNoSummaryField("my_summary", fieldName);
+ }
+
+ public void assertNoSummaryField(String summaryName, String fieldName) {
+ DocumentSummary summary = childSchema.getSummary(summaryName);
+ assertNotNull(summary);
+ SummaryField summaryField = summary.getSummaryField(fieldName);
+ assertNull(summaryField);
+ }
+
+ public void assertSummaryField(String fieldName, DataType dataType, SummaryTransform transform, String source) {
+ assertSummaryField("my_summary", fieldName, dataType, transform, source);
+ }
+
+ public void assertSummaryField(String summaryName, String fieldName, DataType dataType, SummaryTransform transform, String source) {
+ DocumentSummary summary = childSchema.getSummary(summaryName);
+ assertNotNull(summary);
+ SummaryField summaryField = summary.getSummaryField(fieldName);
+ assertNotNull(summaryField);
+ assertEquals(dataType, summaryField.getDataType());
+ assertEquals(transform, summaryField.getTransform());
+ if (source == null) {
+ assertEquals(0, summaryField.getSourceCount());
+ } else {
+ assertEquals(1, summaryField.getSourceCount());
+ assertEquals(source, summaryField.getSingleSource());
+ }
+ }
+
+ public void resolve() {
+ resolve(parentSchema);
+ resolve(childSchema);
+ }
+
+ private static void resolve(Schema schema) {
+ new CreatePositionZCurve(schema, null, null, null).process(true, false);
+ assertNotNull(schema.temporaryImportedFields().get());
+ assertFalse(schema.importedFields().isPresent());
+ new ImportedFieldsResolver(schema, null, null, null).process(true, false);
+ assertNotNull(schema.importedFields().get());
+ new AdjustPositionSummaryFields(schema, null, null, null).process(true, false);
+ }
+ }
+}
diff --git a/config-model/src/test/java/com/yahoo/schema/processing/AssertIndexingScript.java b/config-model/src/test/java/com/yahoo/schema/processing/AssertIndexingScript.java
new file mode 100644
index 00000000000..82650598f29
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/schema/processing/AssertIndexingScript.java
@@ -0,0 +1,43 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.schema.processing;
+
+import com.yahoo.schema.Schema;
+import com.yahoo.schema.derived.IndexingScript;
+import com.yahoo.vespa.indexinglanguage.expressions.Expression;
+import com.yahoo.vespa.indexinglanguage.parser.ParseException;
+
+import java.util.LinkedList;
+import java.util.List;
+
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+/**
+ * @author Simon Thoresen Hult
+ */
+public abstract class AssertIndexingScript {
+
+ public static void assertIndexing(List<String> expected, Schema schema) {
+ assertIndexing(expected, new IndexingScript(schema).expressions());
+ }
+
+ public static void assertIndexing(List<String> expected, IndexingScript script) {
+ assertIndexing(expected, script.expressions());
+ }
+
+ public static void assertIndexing(List<String> expected, Iterable<Expression> actual) {
+ List<String> parsedExpected = new LinkedList<>();
+ for (String str : expected) {
+ try {
+ parsedExpected.add(Expression.fromString(str).toString());
+ } catch (ParseException e) {
+ fail(e.getMessage());
+ }
+ }
+ for (Expression actualExp : actual) {
+ String str = actualExp.toString();
+ assertTrue("Unexpected: " + str, parsedExpected.remove(str));
+ }
+ assertTrue("Missing: " + parsedExpected.toString(), parsedExpected.isEmpty());
+ }
+}
diff --git a/config-model/src/test/java/com/yahoo/schema/processing/AssertSearchBuilder.java b/config-model/src/test/java/com/yahoo/schema/processing/AssertSearchBuilder.java
new file mode 100644
index 00000000000..0b4d7c3a2b6
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/schema/processing/AssertSearchBuilder.java
@@ -0,0 +1,29 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.schema.processing;
+
+import com.yahoo.schema.ApplicationBuilder;
+import com.yahoo.schema.parser.ParseException;
+
+import java.io.IOException;
+
+import static org.junit.Assert.*;
+
+/**
+ * @author Simon Thoresen Hult
+ */
+public abstract class AssertSearchBuilder {
+
+ public static void assertBuilds(String searchDefinitionFileName) throws IOException, ParseException {
+ assertNotNull(ApplicationBuilder.buildFromFile(searchDefinitionFileName));
+ }
+
+ public static void assertBuildFails(String searchDefinitionFileName, String expectedException)
+ throws IOException, ParseException {
+ try {
+ ApplicationBuilder.buildFromFile(searchDefinitionFileName);
+ fail(searchDefinitionFileName);
+ } catch (IllegalArgumentException e) {
+ assertEquals(expectedException, e.getMessage());
+ }
+ }
+}
diff --git a/config-model/src/test/java/com/yahoo/schema/processing/AttributesExactMatchTestCase.java b/config-model/src/test/java/com/yahoo/schema/processing/AttributesExactMatchTestCase.java
new file mode 100644
index 00000000000..40ebe458c74
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/schema/processing/AttributesExactMatchTestCase.java
@@ -0,0 +1,40 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.schema.processing;
+
+import com.yahoo.schema.Schema;
+import com.yahoo.schema.ApplicationBuilder;
+import com.yahoo.schema.AbstractSchemaTestCase;
+import com.yahoo.schema.document.MatchType;
+import com.yahoo.schema.parser.ParseException;
+import org.junit.Test;
+
+import java.io.IOException;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+/**
+ * Attributes should be implicitly exact-match in some cases
+ * @author vegardh
+ *
+ */
+public class AttributesExactMatchTestCase extends AbstractSchemaTestCase {
+ @Test
+ public void testAttributesExactMatch() throws IOException, ParseException {
+ Schema schema = ApplicationBuilder.buildFromFile("src/test/examples/attributesexactmatch.sd");
+ assertEquals(schema.getConcreteField("color").getMatching().getType(), MatchType.EXACT);
+ assertEquals(schema.getConcreteField("artist").getMatching().getType(), MatchType.WORD);
+ assertEquals(schema.getConcreteField("drummer").getMatching().getType(), MatchType.WORD);
+ assertEquals(schema.getConcreteField("guitarist").getMatching().getType(), MatchType.TEXT);
+ assertEquals(schema.getConcreteField("saxophonist_arr").getMatching().getType(), MatchType.WORD);
+ assertEquals(schema.getConcreteField("flutist").getMatching().getType(), MatchType.TEXT);
+
+ assertFalse(schema.getConcreteField("genre").getMatching().getType().equals(MatchType.EXACT));
+ assertFalse(schema.getConcreteField("title").getMatching().getType().equals(MatchType.EXACT));
+ assertFalse(schema.getConcreteField("trumpetist").getMatching().getType().equals(MatchType.EXACT));
+ assertFalse(schema.getConcreteField("genre").getMatching().getType().equals(MatchType.WORD));
+ assertFalse(schema.getConcreteField("title").getMatching().getType().equals(MatchType.WORD));
+ assertFalse(schema.getConcreteField("trumpetist").getMatching().getType().equals(MatchType.WORD));
+
+ }
+
+}
diff --git a/config-model/src/test/java/com/yahoo/schema/processing/BoldingTestCase.java b/config-model/src/test/java/com/yahoo/schema/processing/BoldingTestCase.java
new file mode 100644
index 00000000000..c37bc8085c7
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/schema/processing/BoldingTestCase.java
@@ -0,0 +1,65 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.schema.processing;
+
+import com.yahoo.schema.ApplicationBuilder;
+import com.yahoo.schema.AbstractSchemaTestCase;
+import com.yahoo.schema.parser.ParseException;
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
+
+/**
+ * @author bratseth
+ */
+public class BoldingTestCase extends AbstractSchemaTestCase {
+
+ private final String boldonnonstring =
+ "search boldnonstring {\n" +
+ " document boldnonstring {\n" +
+ " field title type string {\n" +
+ " indexing: summary | index\n" +
+ " }\n" +
+ "\n" +
+ " field year4 type int {\n" +
+ " indexing: summary | attribute\n" +
+ " bolding: on\n" +
+ " }\n" +
+ " }\n" +
+ "}\n";
+
+ @Test
+ public void testBoldOnNonString() throws ParseException {
+ try {
+ ApplicationBuilder.createFromString(boldonnonstring);
+ fail("Expected exception");
+ } catch (IllegalArgumentException e) {
+ assertEquals("'bolding: on' for non-text field 'year4' (datatype int (code: 0)) is not allowed",
+ e.getMessage());
+ }
+ }
+
+ private final String boldonarray =
+ "search boldonarray {\n" +
+ " document boldonarray {\n" +
+ " field myarray type array<string> {\n" +
+ " indexing: summary | index\n" +
+ " bolding: on\n" +
+ " }\n" +
+ " }\n" +
+ "}\n";
+
+ @Test
+ public void testBoldOnArray() throws ParseException {
+ try {
+ ApplicationBuilder.createFromString(boldonarray);
+ fail("Expected exception");
+ } catch (IllegalArgumentException e) {
+ assertEquals("'bolding: on' for non-text field 'myarray' (datatype Array<string> (code: -1486737430)) is not allowed",
+ e.getMessage());
+ }
+ }
+
+}
+
+
diff --git a/config-model/src/test/java/com/yahoo/schema/processing/BoolAttributeValidatorTestCase.java b/config-model/src/test/java/com/yahoo/schema/processing/BoolAttributeValidatorTestCase.java
new file mode 100644
index 00000000000..287cc6559d1
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/schema/processing/BoolAttributeValidatorTestCase.java
@@ -0,0 +1,50 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.schema.processing;
+
+import com.yahoo.schema.parser.ParseException;
+import org.junit.Test;
+
+import static com.yahoo.schema.ApplicationBuilder.createFromString;
+import static com.yahoo.config.model.test.TestUtil.joinLines;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
+
+/**
+ * @author geirst
+ */
+public class BoolAttributeValidatorTestCase {
+
+ @Test
+ public void array_of_bool_attribute_is_not_supported() throws ParseException {
+ try {
+ createFromString(getSd("field b type array<bool> { indexing: attribute }"));
+ fail("Expected exception");
+ }
+ catch (IllegalArgumentException e) {
+ assertEquals("For schema 'test', field 'b': Only single value bool attribute fields are supported",
+ e.getMessage());
+ }
+ }
+
+ @Test
+ public void weigtedset_of_bool_attribute_is_not_supported() throws ParseException {
+ try {
+ createFromString(getSd("field b type weightedset<bool> { indexing: attribute }"));
+ fail("Expected exception");
+ }
+ catch (IllegalArgumentException e) {
+ assertEquals("For schema 'test', field 'b': Only single value bool attribute fields are supported",
+ e.getMessage());
+ }
+ }
+
+ private String getSd(String field) {
+ return joinLines(
+ "schema test {",
+ " document test {",
+ " " + field,
+ " }",
+ "}");
+ }
+
+}
diff --git a/config-model/src/test/java/com/yahoo/schema/processing/DictionaryTestCase.java b/config-model/src/test/java/com/yahoo/schema/processing/DictionaryTestCase.java
new file mode 100644
index 00000000000..1956b87a689
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/schema/processing/DictionaryTestCase.java
@@ -0,0 +1,250 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+
+package com.yahoo.schema.processing;
+
+import com.yahoo.config.model.test.TestUtil;
+import com.yahoo.schema.Schema;
+import com.yahoo.schema.ApplicationBuilder;
+import com.yahoo.schema.derived.AttributeFields;
+import com.yahoo.schema.document.Case;
+import com.yahoo.schema.document.Dictionary;
+import com.yahoo.schema.document.ImmutableSDField;
+import com.yahoo.schema.parser.ParseException;
+import com.yahoo.vespa.config.search.AttributesConfig;
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.fail;
+
+/**
+ * Test configuration of dictionary control.
+ *
+ * @author baldersheim
+ */
+public class DictionaryTestCase {
+ private static AttributesConfig getConfig(Schema schema) {
+ AttributeFields attributes = new AttributeFields(schema);
+ AttributesConfig.Builder builder = new AttributesConfig.Builder();
+ attributes.getConfig(builder, AttributeFields.FieldSet.ALL, 130000, true);
+ return builder.build();
+ }
+ private Schema createSearch(String def) throws ParseException {
+ ApplicationBuilder sb = ApplicationBuilder.createFromString(def);
+ return sb.getSchema();
+ }
+ @Test
+ public void testDefaultDictionarySettings() throws ParseException {
+ String def = TestUtil.joinLines(
+ "search test {",
+ " document test {",
+ " field s1 type string {",
+ " indexing: attribute | summary",
+ " }",
+ " field n1 type int {",
+ " indexing: summary | attribute",
+ " }",
+ " }",
+ "}");
+ Schema schema = createSearch(def);
+ assertNull(schema.getAttribute("s1").getDictionary());
+ assertNull(schema.getAttribute("n1").getDictionary());
+ assertEquals(AttributesConfig.Attribute.Dictionary.Type.BTREE,
+ getConfig(schema).attribute().get(0).dictionary().type());
+ assertEquals(AttributesConfig.Attribute.Dictionary.Type.BTREE,
+ getConfig(schema).attribute().get(1).dictionary().type());
+ assertEquals(AttributesConfig.Attribute.Dictionary.Match.UNCASED,
+ getConfig(schema).attribute().get(0).dictionary().match());
+ assertEquals(AttributesConfig.Attribute.Dictionary.Match.UNCASED,
+ getConfig(schema).attribute().get(1).dictionary().match());
+ }
+
+ Schema verifyDictionaryControl(Dictionary.Type expected, String type, String ... cfg) throws ParseException
+ {
+ String def = TestUtil.joinLines(
+ "search test {",
+ " document test {",
+ " field n1 type " + type + " {",
+ " indexing: summary | attribute",
+ " attribute:fast-search",
+ TestUtil.joinLines(cfg),
+ " }",
+ " }",
+ "}");
+ Schema schema = createSearch(def);
+ AttributesConfig.Attribute.Dictionary.Type.Enum expectedConfig = toCfg(expected);
+ assertEquals(expected, schema.getAttribute("n1").getDictionary().getType());
+ assertEquals(expectedConfig, getConfig(schema).attribute().get(0).dictionary().type());
+ return schema;
+ }
+
+ AttributesConfig.Attribute.Dictionary.Type.Enum toCfg(Dictionary.Type v) {
+ return (v == Dictionary.Type.HASH)
+ ? AttributesConfig.Attribute.Dictionary.Type.Enum.HASH
+ : (v == Dictionary.Type.BTREE)
+ ? AttributesConfig.Attribute.Dictionary.Type.Enum.BTREE
+ : AttributesConfig.Attribute.Dictionary.Type.Enum.BTREE_AND_HASH;
+ }
+ AttributesConfig.Attribute.Dictionary.Match.Enum toCfg(Case v) {
+ return (v == Case.CASED)
+ ? AttributesConfig.Attribute.Dictionary.Match.Enum.CASED
+ : AttributesConfig.Attribute.Dictionary.Match.Enum.UNCASED;
+ }
+
+ void verifyStringDictionaryControl(Dictionary.Type expectedType, Case expectedCase, Case matchCasing,
+ String ... cfg) throws ParseException
+ {
+ Schema schema = verifyDictionaryControl(expectedType, "string", cfg);
+ ImmutableSDField f = schema.getField("n1");
+ AttributesConfig.Attribute.Dictionary.Match.Enum expectedCaseCfg = toCfg(expectedCase);
+ assertEquals(matchCasing, f.getMatching().getCase());
+ assertEquals(expectedCase, schema.getAttribute("n1").getDictionary().getMatch());
+ assertEquals(expectedCaseCfg, getConfig(schema).attribute().get(0).dictionary().match());
+ }
+
+ @Test
+ public void testCasedBtreeSettings() throws ParseException {
+ verifyDictionaryControl(Dictionary.Type.BTREE, "int", "dictionary:cased");
+ }
+
+ @Test
+ public void testNumericBtreeSettings() throws ParseException {
+ verifyDictionaryControl(Dictionary.Type.BTREE, "int", "dictionary:btree");
+ }
+ @Test
+ public void testNumericHashSettings() throws ParseException {
+ verifyDictionaryControl(Dictionary.Type.HASH, "int", "dictionary:hash");
+ }
+ @Test
+ public void testNumericBtreeAndHashSettings() throws ParseException {
+ verifyDictionaryControl(Dictionary.Type.BTREE_AND_HASH, "int", "dictionary:btree", "dictionary:hash");
+ }
+ @Test
+ public void testNumericArrayBtreeAndHashSettings() throws ParseException {
+ verifyDictionaryControl(Dictionary.Type.BTREE_AND_HASH, "array<int>", "dictionary:btree", "dictionary:hash");
+ }
+ @Test
+ public void testNumericWSetBtreeAndHashSettings() throws ParseException {
+ verifyDictionaryControl(Dictionary.Type.BTREE_AND_HASH, "weightedset<int>", "dictionary:btree", "dictionary:hash");
+ }
+ @Test
+ public void testStringBtreeSettings() throws ParseException {
+ verifyStringDictionaryControl(Dictionary.Type.BTREE, Case.UNCASED, Case.UNCASED, "dictionary:btree");
+ }
+ @Test
+ public void testStringBtreeUnCasedSettings() throws ParseException {
+ verifyStringDictionaryControl(Dictionary.Type.BTREE, Case.UNCASED, Case.UNCASED, "dictionary { btree\nuncased\n}");
+ }
+ @Test
+ public void testStringBtreeCasedSettings() throws ParseException {
+ verifyStringDictionaryControl(Dictionary.Type.BTREE, Case.CASED, Case.CASED, "dictionary { btree\ncased\n}", "match:cased");
+ }
+ @Test
+ public void testStringHashSettings() throws ParseException {
+ try {
+ verifyStringDictionaryControl(Dictionary.Type.HASH, Case.UNCASED, Case.UNCASED, "dictionary:hash");
+ } catch (IllegalArgumentException e) {
+ assertEquals("For schema 'test', field 'n1': hash dictionary require cased match", e.getMessage());
+ }
+ }
+ @Test
+ public void testStringHashUnCasedSettings() throws ParseException {
+ try {
+ verifyStringDictionaryControl(Dictionary.Type.HASH, Case.UNCASED, Case.UNCASED, "dictionary { hash\nuncased\n}");
+ } catch (IllegalArgumentException e) {
+ assertEquals("For schema 'test', field 'n1': hash dictionary require cased match", e.getMessage());
+ }
+ }
+ @Test
+ public void testStringHashBothCasedSettings() throws ParseException {
+ verifyStringDictionaryControl(Dictionary.Type.HASH, Case.CASED, Case.CASED, "dictionary { hash\ncased\n}", "match:cased");
+ }
+ @Test
+ public void testStringHashCasedSettings() throws ParseException {
+ try {
+ verifyStringDictionaryControl(Dictionary.Type.HASH, Case.CASED, Case.CASED, "dictionary { hash\ncased\n}");
+ fail();
+ } catch (IllegalArgumentException e) {
+ assertEquals("For schema 'test', field 'n1': Dictionary casing 'CASED' does not match field match casing 'UNCASED'", e.getMessage());
+ }
+ }
+ @Test
+ public void testStringBtreeHashSettings() throws ParseException {
+ verifyStringDictionaryControl(Dictionary.Type.BTREE_AND_HASH, Case.UNCASED, Case.UNCASED, "dictionary{hash\nbtree\n}");
+ }
+ @Test
+ public void testStringBtreeHashUnCasedSettings() throws ParseException {
+ verifyStringDictionaryControl(Dictionary.Type.BTREE_AND_HASH, Case.UNCASED, Case.UNCASED, "dictionary { hash\nbtree\nuncased\n}");
+ }
+ @Test
+ public void testStringBtreeHashCasedSettings() throws ParseException {
+ try {
+ verifyStringDictionaryControl(Dictionary.Type.BTREE_AND_HASH, Case.CASED, Case.CASED, "dictionary { btree\nhash\ncased\n}");
+ } catch (IllegalArgumentException e) {
+ assertEquals("For schema 'test', field 'n1': Dictionary casing 'CASED' does not match field match casing 'UNCASED'", e.getMessage());
+ }
+ }
+ @Test
+ public void testNonNumericFieldsFailsDictionaryControl() throws ParseException {
+ String def = TestUtil.joinLines(
+ "schema test {",
+ " document test {",
+ " field n1 type bool {",
+ " indexing: summary | attribute",
+ " dictionary:btree",
+ " }",
+ " }",
+ "}");
+ try {
+ ApplicationBuilder sb = ApplicationBuilder.createFromString(def);
+ fail("Controlling dictionary for non-numeric fields are not yet supported.");
+ } catch (IllegalArgumentException e) {
+ assertEquals("For schema 'test', field 'n1': You can only specify 'dictionary:' for numeric or string fields", e.getMessage());
+ }
+ }
+ @Test
+ public void testNonFastSearchNumericFieldsFailsDictionaryControl() throws ParseException {
+ String def = TestUtil.joinLines(
+ "schema test {",
+ " document test {",
+ " field n1 type int {",
+ " indexing: summary | attribute",
+ " dictionary:btree",
+ " }",
+ " }",
+ "}");
+ try {
+ ApplicationBuilder sb = ApplicationBuilder.createFromString(def);
+ fail("Controlling dictionary for non-fast-search fields are not allowed.");
+ } catch (IllegalArgumentException e) {
+ assertEquals("For schema 'test', field 'n1': You must specify 'attribute:fast-search' to allow dictionary control", e.getMessage());
+ }
+ }
+
+ @Test
+ public void testCasingForNonFastSearch() throws ParseException {
+ String def = TestUtil.joinLines(
+ "schema test {",
+ " document test {",
+ " field s1 type string {",
+ " indexing: attribute | summary",
+ " }",
+ " field s2 type string {",
+ " indexing: attribute | summary",
+ " match:uncased",
+ " }",
+ " field s3 type string {",
+ " indexing: attribute | summary",
+ " match:cased",
+ " }",
+ " }",
+ "}");
+ Schema schema = createSearch(def);
+ assertEquals(Case.UNCASED, schema.getAttribute("s1").getCase());
+ assertEquals(Case.UNCASED, schema.getAttribute("s2").getCase());
+ assertEquals(Case.CASED, schema.getAttribute("s3").getCase());
+ assertEquals(AttributesConfig.Attribute.Match.UNCASED, getConfig(schema).attribute().get(0).match());
+ assertEquals(AttributesConfig.Attribute.Match.UNCASED, getConfig(schema).attribute().get(1).match());
+ assertEquals(AttributesConfig.Attribute.Match.CASED, getConfig(schema).attribute().get(2).match());
+ }
+}
diff --git a/config-model/src/test/java/com/yahoo/schema/processing/DisallowComplexMapAndWsetKeyTypesTestCase.java b/config-model/src/test/java/com/yahoo/schema/processing/DisallowComplexMapAndWsetKeyTypesTestCase.java
new file mode 100644
index 00000000000..64b0a437b1d
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/schema/processing/DisallowComplexMapAndWsetKeyTypesTestCase.java
@@ -0,0 +1,57 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.schema.processing;
+
+import com.yahoo.schema.RankProfileRegistry;
+import com.yahoo.schema.ApplicationBuilder;
+import com.yahoo.schema.parser.ParseException;
+import org.junit.Test;
+
+/**
+ * @author lesters
+ */
+public class DisallowComplexMapAndWsetKeyTypesTestCase {
+
+ @Test(expected = IllegalArgumentException.class)
+ public void requireThatComplexTypesForMapKeysFail() throws ParseException {
+ testFieldType("map<mystruct,string>");
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void requireThatComplexTypesForWsetFail() throws ParseException {
+ testFieldType("weightedset<mystruct>");
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void requireThatNestedComplexTypesForMapFail() throws ParseException {
+ testFieldType("array<map<mystruct,string>>");
+ }
+
+ @Test
+ public void requireThatNestedComplexValuesForMapSucceed() throws ParseException {
+ testFieldType("array<map<string,mystruct>>");
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void requireThatNestedComplexTypesForWsetFail() throws ParseException {
+ testFieldType("array<weightedset<mystruct>>");
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void requireThatDeepNestedComplexTypesForMapFail() throws ParseException {
+ testFieldType("map<string,map<mystruct,string>>");
+ }
+
+ private void testFieldType(String fieldType) throws ParseException {
+ RankProfileRegistry rankProfileRegistry = new RankProfileRegistry();
+ ApplicationBuilder builder = new ApplicationBuilder(rankProfileRegistry);
+ builder.addSchema(
+ "search test {\n" +
+ " document test { \n" +
+ " struct mystruct {}\n" +
+ " field a type " + fieldType + " {}\n" +
+ " }\n" +
+ "}\n");
+ builder.build(true);
+ }
+
+}
diff --git a/config-model/src/test/java/com/yahoo/schema/processing/FastAccessValidatorTest.java b/config-model/src/test/java/com/yahoo/schema/processing/FastAccessValidatorTest.java
new file mode 100644
index 00000000000..b249b407c7b
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/schema/processing/FastAccessValidatorTest.java
@@ -0,0 +1,61 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.schema.processing;
+
+import com.yahoo.config.model.test.TestUtil;
+import com.yahoo.schema.RankProfileRegistry;
+import com.yahoo.schema.ApplicationBuilder;
+import com.yahoo.schema.parser.ParseException;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+
+/**
+ * @author bjorncs
+ */
+public class FastAccessValidatorTest {
+
+ @SuppressWarnings("deprecation")
+ @Rule
+ public final ExpectedException exceptionRule = ExpectedException.none();
+
+ @Test
+ public void throws_exception_on_incompatible_use_of_fastaccess() throws ParseException {
+ ApplicationBuilder builder = new ApplicationBuilder(new RankProfileRegistry());
+ builder.addSchema(
+ TestUtil.joinLines(
+ "schema parent {",
+ " document parent {",
+ " field int_field type int { indexing: attribute }",
+ " }",
+ "}"));
+ builder.addSchema(
+ TestUtil.joinLines(
+ "schema test {",
+ " document test { ",
+ " field int_attribute type int { ",
+ " indexing: attribute ",
+ " attribute: fast-access",
+ " }",
+ " field predicate_attribute type predicate {",
+ " indexing: attribute ",
+ " attribute: fast-access",
+ " }",
+ " field tensor_attribute type tensor(x[5]) {",
+ " indexing: attribute ",
+ " attribute: fast-access",
+ " }",
+ " field reference_attribute type reference<parent> {",
+ " indexing: attribute ",
+ " attribute: fast-access",
+ " }",
+ " }",
+ "}"));
+ exceptionRule.expect(IllegalArgumentException.class);
+ exceptionRule.expectMessage(
+ "For schema 'test': The following attributes have a type that is incompatible " +
+ "with fast-access: predicate_attribute, tensor_attribute, reference_attribute. " +
+ "Predicate, tensor and reference attributes are incompatible with fast-access.");
+ builder.build(true);
+ }
+
+}
diff --git a/config-model/src/test/java/com/yahoo/schema/processing/ImplicitSchemaFieldsTestCase.java b/config-model/src/test/java/com/yahoo/schema/processing/ImplicitSchemaFieldsTestCase.java
new file mode 100644
index 00000000000..594124c9500
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/schema/processing/ImplicitSchemaFieldsTestCase.java
@@ -0,0 +1,94 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.schema.processing;
+
+import com.yahoo.schema.Schema;
+import com.yahoo.schema.ApplicationBuilder;
+import com.yahoo.schema.AbstractSchemaTestCase;
+import com.yahoo.schema.derived.DerivedConfiguration;
+import com.yahoo.schema.document.SDDocumentType;
+import com.yahoo.schema.parser.ParseException;
+import org.junit.Test;
+
+import java.io.IOException;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+
+public class ImplicitSchemaFieldsTestCase extends AbstractSchemaTestCase {
+
+ @Test
+ public void testRequireThatExtraFieldsAreIncluded() throws IOException, ParseException {
+ Schema schema = ApplicationBuilder.buildFromFile("src/test/examples/nextgen/extrafield.sd");
+ assertNotNull(schema);
+
+ SDDocumentType docType = schema.getDocument();
+ assertNotNull(docType);
+ assertNotNull(docType.getField("foo"));
+ assertNotNull(docType.getField("bar"));
+ assertEquals(2, docType.getFieldCount());
+ }
+
+ @Test
+ public void testRequireThatSummaryFieldsAreIncluded() throws IOException, ParseException {
+ Schema schema = ApplicationBuilder.buildFromFile("src/test/examples/nextgen/summaryfield.sd");
+ assertNotNull(schema);
+
+ SDDocumentType docType = schema.getDocument();
+ assertNotNull(docType);
+ assertNotNull(docType.getField("foo"));
+ assertNotNull(docType.getField("bar"));
+ assertNotNull(docType.getField("cox"));
+ assertNotNull(docType.getField("mytags"));
+ assertNotNull(docType.getField("alltags"));
+ assertEquals(5, docType.getFieldCount());
+ }
+
+ @Test
+ public void testRequireThatBoldedSummaryFieldsAreIncluded() throws IOException, ParseException {
+ Schema schema = ApplicationBuilder.buildFromFile("src/test/examples/nextgen/boldedsummaryfields.sd");
+ assertNotNull(schema);
+
+ SDDocumentType docType = schema.getDocument();
+ assertNotNull(docType);
+ assertNotNull(docType.getField("foo"));
+ assertNotNull(docType.getField("bar"));
+ assertNotNull(docType.getField("baz"));
+ assertNotNull(docType.getField("cox"));
+ assertEquals(4, docType.getFieldCount());
+ }
+
+ @Test
+ public void testRequireThatUntransformedSummaryFieldsAreIgnored() throws IOException, ParseException {
+ Schema schema = ApplicationBuilder.buildFromFile("src/test/examples/nextgen/untransformedsummaryfields.sd");
+ assertNotNull(schema);
+
+ SDDocumentType docType = schema.getDocument();
+ assertNotNull(docType);
+ assertNotNull(docType.getField("foo"));
+ assertNotNull(docType.getField("bar"));
+ assertNotNull(docType.getField("baz"));
+ assertEquals(3, docType.getFieldCount());
+ }
+
+ @Test
+ public void testRequireThatDynamicSummaryFieldsAreIgnored() throws IOException, ParseException {
+ Schema schema = ApplicationBuilder.buildFromFile("src/test/examples/nextgen/dynamicsummaryfields.sd");
+ assertNotNull(schema);
+
+ SDDocumentType docType = schema.getDocument();
+ assertNotNull(docType);
+ assertNotNull(docType.getField("foo"));
+ assertNotNull(docType.getField("bar"));
+ assertEquals(2, docType.getFieldCount());
+ }
+
+ @Test
+ public void testRequireThatDerivedConfigurationWorks() throws IOException, ParseException {
+ ApplicationBuilder sb = new ApplicationBuilder();
+ sb.addSchemaFile("src/test/examples/nextgen/simple.sd");
+ sb.build(true);
+ assertNotNull(sb.getSchema());
+ new DerivedConfiguration(sb.getSchema(), sb.getRankProfileRegistry());
+ }
+
+}
diff --git a/config-model/src/test/java/com/yahoo/schema/processing/ImplicitStructTypesTestCase.java b/config-model/src/test/java/com/yahoo/schema/processing/ImplicitStructTypesTestCase.java
new file mode 100644
index 00000000000..111ed266d74
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/schema/processing/ImplicitStructTypesTestCase.java
@@ -0,0 +1,69 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.schema.processing;
+
+import com.yahoo.document.*;
+import com.yahoo.schema.Schema;
+import com.yahoo.schema.ApplicationBuilder;
+import com.yahoo.schema.AbstractSchemaTestCase;
+import com.yahoo.schema.document.SDDocumentType;
+import com.yahoo.schema.document.SDField;
+import com.yahoo.schema.parser.ParseException;
+import org.junit.Test;
+
+import java.io.IOException;
+
+import static org.junit.Assert.*;
+public class ImplicitStructTypesTestCase extends AbstractSchemaTestCase {
+ @Test
+ public void testRequireThatImplicitStructsAreCreated() throws IOException, ParseException {
+ Schema schema = ApplicationBuilder.buildFromFile("src/test/examples/nextgen/toggleon.sd");
+ assertNotNull(schema);
+
+ SDDocumentType docType = schema.getDocument();
+ assertNotNull(docType);
+ assertStruct(docType, PositionDataType.INSTANCE);
+ }
+ @Test
+ public void testRequireThatImplicitStructsAreUsed() throws IOException, ParseException {
+ Schema schema = ApplicationBuilder.buildFromFile("src/test/examples/nextgen/implicitstructtypes.sd");
+ assertNotNull(schema);
+
+ SDDocumentType docType = schema.getDocument();
+ assertNotNull(docType);
+
+ assertField(docType, "doc_str", DataType.STRING);
+ assertField(docType, "doc_str_sum", DataType.STRING);
+ assertField(docType, "doc_uri", DataType.URI);
+ assertField(docType, "docsum_str", DataType.STRING);
+ }
+
+ @SuppressWarnings({ "UnusedDeclaration" })
+ private static void assertStruct(SDDocumentType docType, StructDataType expectedStruct) {
+ // TODO: When structs are refactored from a static register to a member of the owning document types, this test
+ // TODO: must be changed to retrieve struct type from the provided document type.
+ StructDataType structType = (StructDataType) docType.getType(expectedStruct.getName()).getStruct();
+ assertNotNull(structType);
+ for (Field expectedField : expectedStruct.getFields()) {
+ Field field = structType.getField(expectedField.getName());
+ assertNotNull(field);
+ assertEquals(expectedField.getDataType(), field.getDataType());
+ }
+ assertEquals(expectedStruct.getFieldCount(), structType.getFieldCount());
+ }
+
+ private static void assertField(SDDocumentType docType, String fieldName, DataType type) {
+ Field field = getSecretField(docType, fieldName); // TODO: get rid of this stupidity
+ assertNotNull(field);
+ assertEquals(type, field.getDataType());
+ assertTrue(field instanceof SDField);
+ }
+
+ private static Field getSecretField(SDDocumentType docType, String fieldName) {
+ for (Field field : docType.fieldSet()) {
+ if (field.getName().equals(fieldName)) {
+ return field;
+ }
+ }
+ return null;
+ }
+}
diff --git a/config-model/src/test/java/com/yahoo/schema/processing/ImplicitSummariesTestCase.java b/config-model/src/test/java/com/yahoo/schema/processing/ImplicitSummariesTestCase.java
new file mode 100644
index 00000000000..50deb5d5b42
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/schema/processing/ImplicitSummariesTestCase.java
@@ -0,0 +1,78 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.schema.processing;
+
+import com.yahoo.schema.Schema;
+import com.yahoo.schema.ApplicationBuilder;
+import com.yahoo.schema.parser.ParseException;
+import com.yahoo.vespa.documentmodel.SummaryTransform;
+import org.junit.Test;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.logging.Handler;
+import java.util.logging.Level;
+import java.util.logging.LogRecord;
+import java.util.logging.Logger;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * @author Simon Thoresen Hult
+ */
+public class ImplicitSummariesTestCase {
+
+ @Test
+ public void requireThatSummaryFromAttributeDoesNotWarn() throws IOException, ParseException {
+ LogHandler log = new LogHandler();
+ Logger.getLogger("").addHandler(log);
+
+ Schema schema = ApplicationBuilder.buildFromFile("src/test/examples/implicitsummaries_attribute.sd");
+ assertNotNull(schema);
+ assertTrue(log.records.isEmpty());
+ }
+
+ private static class LogHandler extends Handler {
+
+ final List<LogRecord> records = new ArrayList<>();
+
+ @Override
+ public void publish(LogRecord record) {
+ if (record.getLevel() == Level.WARNING ||
+ record.getLevel() == Level.SEVERE)
+ {
+ records.add(record);
+ }
+ }
+
+ @Override
+ public void flush() {
+
+ }
+
+ @Override
+ public void close() throws SecurityException {
+
+ }
+ }
+
+ @Test
+ public void attribute_combiner_transform_is_set_on_array_of_struct_with_only_struct_field_attributes() throws IOException, ParseException {
+ Schema schema = ApplicationBuilder.buildFromFile("src/test/derived/array_of_struct_attribute/test.sd");
+ assertEquals(SummaryTransform.ATTRIBUTECOMBINER, schema.getSummaryField("elem_array").getTransform());
+ }
+
+ @Test
+ public void attribute_combiner_transform_is_set_on_map_of_struct_with_only_struct_field_attributes() throws IOException, ParseException {
+ Schema schema = ApplicationBuilder.buildFromFile("src/test/derived/map_of_struct_attribute/test.sd");
+ assertEquals(SummaryTransform.ATTRIBUTECOMBINER, schema.getSummaryField("str_elem_map").getTransform());
+ }
+
+ @Test
+ public void attribute_combiner_transform_is_not_set_when_map_of_struct_has_some_struct_field_attributes() throws IOException, ParseException {
+ Schema schema = ApplicationBuilder.buildFromFile("src/test/derived/map_of_struct_attribute/test.sd");
+ assertEquals(SummaryTransform.NONE, schema.getSummaryField("int_elem_map").getTransform());
+ }
+}
diff --git a/config-model/src/test/java/com/yahoo/schema/processing/ImplicitSummaryFieldsTestCase.java b/config-model/src/test/java/com/yahoo/schema/processing/ImplicitSummaryFieldsTestCase.java
new file mode 100644
index 00000000000..f32c9079d36
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/schema/processing/ImplicitSummaryFieldsTestCase.java
@@ -0,0 +1,29 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.schema.processing;
+
+import com.yahoo.schema.Schema;
+import com.yahoo.schema.ApplicationBuilder;
+import com.yahoo.schema.AbstractSchemaTestCase;
+import com.yahoo.schema.parser.ParseException;
+import com.yahoo.vespa.documentmodel.DocumentSummary;
+import org.junit.Test;
+
+import java.io.IOException;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+
+public class ImplicitSummaryFieldsTestCase extends AbstractSchemaTestCase {
+
+ @Test
+ public void testRequireThatImplicitFieldsAreCreated() throws IOException, ParseException {
+ Schema schema = ApplicationBuilder.buildFromFile("src/test/examples/implicitsummaryfields.sd");
+ assertNotNull(schema);
+
+ DocumentSummary docsum = schema.getSummary("default");
+ assertNotNull(docsum);
+ assertNotNull(docsum.getSummaryField("rankfeatures"));
+ assertNotNull(docsum.getSummaryField("summaryfeatures"));
+ assertEquals(2, docsum.getSummaryFields().size());
+ }
+}
diff --git a/config-model/src/test/java/com/yahoo/schema/processing/ImportedFieldsResolverTestCase.java b/config-model/src/test/java/com/yahoo/schema/processing/ImportedFieldsResolverTestCase.java
new file mode 100644
index 00000000000..5baa64d06d4
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/schema/processing/ImportedFieldsResolverTestCase.java
@@ -0,0 +1,152 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.schema.processing;
+
+import com.yahoo.document.DataType;
+import com.yahoo.document.TensorDataType;
+import com.yahoo.schema.Schema;
+import com.yahoo.schema.document.ImmutableImportedSDField;
+import com.yahoo.schema.document.ImmutableSDField;
+import com.yahoo.schema.document.ImportedField;
+import com.yahoo.schema.document.ImportedFields;
+import com.yahoo.schema.document.SDField;
+import com.yahoo.schema.document.TemporarySDField;
+import com.yahoo.tensor.TensorType;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertSame;
+
+/**
+ * @author geirst
+ */
+public class ImportedFieldsResolverTestCase {
+
+ @SuppressWarnings("deprecation")
+ @Rule
+ public final ExpectedException exceptionRule = ExpectedException.none();
+
+ private void resolve_imported_field(String fieldName, String targetFieldName) {
+ SearchModel model = new SearchModel();
+ model.addImportedField(fieldName, "ref", targetFieldName).resolve();
+
+ assertEquals(1, model.importedFields.fields().size());
+ ImportedField myField = model.importedFields.fields().get(fieldName);
+ assertNotNull(myField);
+ assertEquals(fieldName, myField.fieldName());
+ assertSame(model.childSchema.getConcreteField("ref"), myField.reference().referenceField());
+ assertSame(model.parentSchema, myField.reference().targetSearch());
+ ImmutableSDField targetField = model.parentSchema.getField(targetFieldName);
+ if (targetField instanceof SDField) {
+ assertSame(targetField, myField.targetField());
+ } else {
+ assertSame(getImportedField(targetField), getImportedField(myField.targetField()));
+ }
+ }
+
+ private static ImportedField getImportedField(ImmutableSDField field) {
+ return ((ImmutableImportedSDField) field).getImportedField();
+ }
+
+ @Test
+ public void valid_imported_fields_are_resolved() {
+ resolve_imported_field("my_attribute_field", "attribute_field");
+ resolve_imported_field("my_tensor_field", "tensor_field");
+ resolve_imported_field("my_ancient_field", "ancient_field");
+ }
+
+ @Test
+ public void resolver_fails_if_document_reference_is_not_found() {
+ exceptionRule.expect(IllegalArgumentException.class);
+ exceptionRule.expectMessage("For schema 'child', import field 'my_attribute_field': "
+ + "Reference field 'not_ref' not found");
+ new SearchModel().addImportedField("my_attribute_field", "not_ref", "budget").resolve();
+ }
+
+ @Test
+ public void resolver_fails_if_referenced_field_is_not_found() {
+ exceptionRule.expect(IllegalArgumentException.class);
+ exceptionRule.expectMessage("For schema 'child', import field 'my_attribute_field': "
+ + "Field 'not_existing' via reference field 'ref': Not found");
+ new SearchModel().addImportedField("my_attribute_field", "ref", "not_existing").resolve();
+ }
+
+ @Test
+ public void resolver_fails_if_imported_field_is_not_an_attribute() {
+ exceptionRule.expect(IllegalArgumentException.class);
+ exceptionRule.expectMessage("For schema 'child', import field 'my_not_attribute': "
+ + "Field 'not_attribute' via reference field 'ref': Is not an attribute field. Only attribute fields supported");
+ new SearchModel().addImportedField("my_not_attribute", "ref", "not_attribute").resolve();
+ }
+
+ @Test
+ public void resolver_fails_if_imported_field_is_indexing() {
+ exceptionRule.expect(IllegalArgumentException.class);
+ exceptionRule.expectMessage(
+ "For schema 'child', import field 'my_attribute_and_index': " +
+ "Field 'attribute_and_index' via reference field 'ref': Is an index field. Not supported");
+ new SearchModel()
+ .addImportedField("my_attribute_and_index", "ref", "attribute_and_index")
+ .resolve();
+ }
+
+ @Test
+ public void resolver_fails_if_imported_field_is_of_type_predicate() {
+ exceptionRule.expect(IllegalArgumentException.class);
+ exceptionRule.expectMessage(
+ "For schema 'child', import field 'my_predicate_field': " +
+ "Field 'predicate_field' via reference field 'ref': Is of type 'predicate'. Not supported");
+ new SearchModel().addImportedField("my_predicate_field", "ref", "predicate_field").resolve();
+ }
+
+ static class SearchModel extends ParentChildSearchModel {
+
+ public final Schema grandParentSchema;
+ public ImportedFields importedFields;
+
+ public SearchModel() {
+ super();
+ grandParentSchema = createSearch("grandparent");
+ var grandParentDoc = grandParentSchema.getDocument();
+ grandParentDoc.addField(createField(grandParentDoc, "ancient_field", DataType.INT, "{ attribute }"));
+ var parentDoc = parentSchema.getDocument();
+ parentDoc.addField(createField(parentDoc, "attribute_field", DataType.INT, "{ attribute }"));
+ parentDoc.addField(createField(parentDoc, "attribute_and_index", DataType.INT, "{ attribute | index }"));
+ parentDoc.addField(new TemporarySDField(parentDoc, "not_attribute", DataType.INT));
+ parentDoc.addField(createField(parentDoc, "tensor_field", new TensorDataType(TensorType.fromSpec("tensor(x[5])")), "{ attribute }"));
+ parentDoc.addField(createField(parentDoc, "predicate_field", DataType.PREDICATE, "{ attribute }"));
+ addRefField(parentSchema, grandParentSchema, "ref");
+ addImportedField(parentSchema, "ancient_field", "ref", "ancient_field");
+
+ addRefField(childSchema, parentSchema, "ref");
+ }
+
+
+ protected SearchModel addImportedField(String fieldName, String referenceFieldName, String targetFieldName) {
+ return addImportedField(childSchema, fieldName, referenceFieldName, targetFieldName);
+ }
+
+ protected SearchModel addImportedField(Schema schema, String fieldName, String referenceFieldName, String targetFieldName) {
+ super.addImportedField(schema, fieldName, referenceFieldName, targetFieldName);
+ return this;
+ }
+
+ public void resolve() {
+ resolve(grandParentSchema);
+ resolve(parentSchema);
+ importedFields = resolve(childSchema);
+ }
+
+ private static ImportedFields resolve(Schema schema) {
+ assertNotNull(schema.temporaryImportedFields().get());
+ assertFalse(schema.importedFields().isPresent());
+ new ImportedFieldsResolver(schema, null, null, null).process(true, false);
+ assertNotNull(schema.importedFields().get());
+ return schema.importedFields().get();
+ }
+ }
+
+}
diff --git a/config-model/src/test/java/com/yahoo/schema/processing/ImportedFieldsTestCase.java b/config-model/src/test/java/com/yahoo/schema/processing/ImportedFieldsTestCase.java
new file mode 100644
index 00000000000..ab702154527
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/schema/processing/ImportedFieldsTestCase.java
@@ -0,0 +1,529 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.schema.processing;
+
+import com.yahoo.schema.Schema;
+import com.yahoo.schema.ApplicationBuilder;
+import com.yahoo.schema.derived.AttributeFields;
+import com.yahoo.schema.document.ImportedComplexField;
+import com.yahoo.schema.document.ImportedField;
+import com.yahoo.schema.parser.ParseException;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+
+import static org.junit.Assert.assertEquals;
+import static com.yahoo.config.model.test.TestUtil.joinLines;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * @author geirst
+ */
+public class ImportedFieldsTestCase {
+
+ @Test
+ public void fields_can_be_imported_from_referenced_document_types() throws ParseException {
+ Schema schema = buildAdSearch(joinLines(
+ "search ad {",
+ " document ad {",
+ " field campaign_ref type reference<campaign> { indexing: attribute }",
+ " field person_ref type reference<person> { indexing: attribute }",
+ " }",
+ " import field campaign_ref.budget as my_budget {}",
+ " import field person_ref.name as my_name {}",
+ "}"));
+ assertEquals(2, schema.importedFields().get().fields().size());
+ assertSearchContainsImportedField("my_budget", "campaign_ref", "campaign", "budget", schema);
+ assertSearchContainsImportedField("my_name", "person_ref", "person", "name", schema);
+ }
+
+ @SuppressWarnings("deprecation")
+ @Rule
+ public ExpectedException exception = ExpectedException.none();
+
+ @Test
+ public void field_reference_spec_must_include_dot() throws ParseException {
+ exception.expect(IllegalArgumentException.class);
+ exception.expectMessage("Illegal field reference spec 'campaignrefbudget': Does not include a single '.'");
+ buildAdSearch(joinLines(
+ "search ad {",
+ " document ad {}",
+ " import field campaignrefbudget as budget {}",
+ "}"));
+ }
+
+ @Test
+ public void fail_duplicate_import() throws ParseException {
+ exception.expect(IllegalArgumentException.class);
+ exception.expectMessage("For schema 'ad', import field as 'my_budget': Field already imported");
+ Schema schema = buildAdSearch(joinLines(
+ "schema ad {",
+ " document ad {",
+ " field campaign_ref type reference<campaign> { indexing: attribute }",
+ " }",
+ " import field campaign_ref.budget as my_budget {}",
+ " import field campaign_ref.budget as my_budget {}",
+ "}"));
+ }
+
+ private static Schema buildAdSearch(String sdContent) throws ParseException {
+ ApplicationBuilder builder = new ApplicationBuilder();
+ builder.addSchema(joinLines(
+ "schema campaign {",
+ " document campaign {",
+ " field budget type int { indexing: attribute }",
+ " }",
+ "}"));
+ builder.addSchema(joinLines(
+ "schema person {",
+ " document person {",
+ " field name type string { indexing: attribute }",
+ " }",
+ "}"));
+ builder.addSchema(sdContent);
+ builder.build(true);
+ return builder.getSchema("ad");
+ }
+
+ private static void checkStructImport(AncestorStructSdBuilder parentBuilder) throws ParseException {
+ Schema schema = buildChildSearch(parentBuilder.build(), new ChildStructSdBuilder().build());
+ checkImportedStructFields(schema, parentBuilder);
+ }
+
+ private static void checkNestedStructImport(AncestorStructSdBuilder grandParentBuilder) throws ParseException {
+ Schema schema = buildChildSearch(grandParentBuilder.build(),
+ new IntermediateParentStructSdBuilder().build(),
+ new ChildStructSdBuilder().build());
+ checkImportedStructFields(schema, grandParentBuilder);
+ }
+
+ private static void checkImportedStructFields(Schema schema, AncestorStructSdBuilder ancestorBuilder) {
+ assertEquals(3, schema.importedFields().get().fields().size());
+ checkImportedField("my_elem_array.name", "parent_ref", "parent", "elem_array.name", schema, ancestorBuilder.elem_array_name_attr);
+ checkImportedField("my_elem_array.weight", "parent_ref", "parent", "elem_array.weight", schema, ancestorBuilder.elem_array_weight_attr);
+ checkImportedField("my_elem_map.key", "parent_ref", "parent", "elem_map.key", schema, ancestorBuilder.elem_map_key_attr);
+ checkImportedField("my_elem_map.value.name", "parent_ref", "parent", "elem_map.value.name", schema, ancestorBuilder.elem_map_value_name_attr);
+ checkImportedField("my_elem_map.value.weight", "parent_ref", "parent", "elem_map.value.weight", schema, ancestorBuilder.elem_map_value_weight_attr);
+ checkImportedField("my_str_int_map.key", "parent_ref", "parent", "str_int_map.key", schema, ancestorBuilder.str_int_map_key_attr);
+ checkImportedField("my_str_int_map.value", "parent_ref", "parent", "str_int_map.value", schema, ancestorBuilder.str_int_map_value_attr);
+ checkImportedField("my_elem_array", "parent_ref", "parent", "elem_array", schema, true);
+ checkImportedField("my_elem_map", "parent_ref", "parent", "elem_map", schema, true);
+ checkImportedField("my_str_int_map", "parent_ref", "parent", "str_int_map", schema, true);
+ }
+
+ @Test
+ public void check_struct_import() throws ParseException {
+ checkStructImport(new ParentStructSdBuilder());
+ checkStructImport(new ParentStructSdBuilder().elem_array_weight_attr(false).elem_map_value_weight_attr(false));
+ checkStructImport(new ParentStructSdBuilder().elem_array_name_attr(false).elem_map_value_name_attr(false));
+ }
+
+ @Test
+ public void check_nested_struct_import() throws ParseException {
+ checkNestedStructImport(new GrandParentStructSdBuilder());
+ checkNestedStructImport(new GrandParentStructSdBuilder().elem_array_weight_attr(false).elem_map_value_weight_attr(false));
+ checkNestedStructImport(new GrandParentStructSdBuilder().elem_array_name_attr(false).elem_map_value_name_attr(false));
+ }
+
+ @Test
+ public void check_illegal_struct_import_missing_array_of_struct_attributes() throws ParseException {
+ exception.expect(IllegalArgumentException.class);
+ exception.expectMessage("For schema 'child', import field 'my_elem_array': Field 'elem_array' via reference field 'parent_ref': Is not a struct containing an attribute field.");
+ checkStructImport(new ParentStructSdBuilder().elem_array_name_attr(false).elem_array_weight_attr(false));
+ }
+
+ @Test
+ public void check_illegal_struct_import_missing_map_of_struct_key_attribute() throws ParseException {
+ exception.expect(IllegalArgumentException.class);
+ exception.expectMessage("For schema 'child', import field 'my_elem_map' (nested to 'my_elem_map.key'): Field 'elem_map.key' via reference field 'parent_ref': Is not an attribute field. Only attribute fields supported");
+ checkStructImport(new ParentStructSdBuilder().elem_map_key_attr(false));
+ }
+
+ @Test
+ public void check_illegal_struct_import_missing_map_of_struct_value_attributes() throws ParseException {
+ exception.expect(IllegalArgumentException.class);
+ exception.expectMessage("For schema 'child', import field 'my_elem_map' (nested to 'my_elem_map.value'): Field 'elem_map.value' via reference field 'parent_ref': Is not a struct containing an attribute field.");
+ checkStructImport(new ParentStructSdBuilder().elem_map_value_name_attr(false).elem_map_value_weight_attr(false));
+ }
+
+ @Test
+ public void check_illegal_struct_import_missing_map_of_primitive_key_attribute() throws ParseException {
+ exception.expect(IllegalArgumentException.class);
+ exception.expectMessage("For schema 'child', import field 'my_str_int_map' (nested to 'my_str_int_map.key'): Field 'str_int_map.key' via reference field 'parent_ref': Is not an attribute field. Only attribute fields supported");
+ checkStructImport(new ParentStructSdBuilder().str_int_map_key_attr(false));
+ }
+
+ @Test
+ public void check_illegal_struct_import_missing_map_of_primitive_value_attribute() throws ParseException {
+ exception.expect(IllegalArgumentException.class);
+ exception.expectMessage("For schema 'child', import field 'my_str_int_map' (nested to 'my_str_int_map.value'): Field 'str_int_map.value' via reference field 'parent_ref': Is not an attribute field. Only attribute fields supported");
+ checkStructImport(new ParentStructSdBuilder().str_int_map_value_attr(false));
+ }
+
+ private static class NamedSdBuilder {
+ protected String name;
+ private String fieldPrefix;
+
+ public NamedSdBuilder(String name, String fieldPrefix) {
+ this.name = name;
+ this.fieldPrefix = fieldPrefix;
+ }
+
+ protected String prefixedFieldName(String name) {
+ return fieldPrefix + name;
+ }
+ }
+
+ private static class AncestorStructSdBuilder extends NamedSdBuilder {
+ private boolean elem_array_name_attr;
+ private boolean elem_array_weight_attr;
+ private boolean elem_map_key_attr;
+ private boolean elem_map_value_name_attr;
+ private boolean elem_map_value_weight_attr;
+ private boolean str_int_map_key_attr;
+ private boolean str_int_map_value_attr;
+
+ public AncestorStructSdBuilder(String name, String fieldPrefix) {
+ super(name, fieldPrefix);
+ elem_array_name_attr = true;
+ elem_array_weight_attr = true;
+ elem_map_key_attr = true;
+ elem_map_value_name_attr = true;
+ elem_map_value_weight_attr = true;
+ str_int_map_key_attr = true;
+ str_int_map_value_attr = true;
+ }
+
+ public AncestorStructSdBuilder elem_array_name_attr(boolean v) { elem_array_name_attr = v; return this; }
+ public AncestorStructSdBuilder elem_array_weight_attr(boolean v) { elem_array_weight_attr = v; return this; }
+ public AncestorStructSdBuilder elem_map_key_attr(boolean v) { elem_map_key_attr = v; return this; }
+ public AncestorStructSdBuilder elem_map_value_name_attr(boolean v) { elem_map_value_name_attr = v; return this; }
+ public AncestorStructSdBuilder elem_map_value_weight_attr(boolean v) { elem_map_value_weight_attr = v; return this; }
+ public AncestorStructSdBuilder str_int_map_key_attr(boolean v) { str_int_map_key_attr = v; return this; }
+ public AncestorStructSdBuilder str_int_map_value_attr(boolean v) { str_int_map_value_attr = v; return this; }
+
+ public String build() {
+ return joinLines("search " + name + " {",
+ " document " + name + " {",
+ " struct elem {",
+ " field name type string {}",
+ " field weight type int {}",
+ " }",
+ " field " + prefixedFieldName("elem_array") + " type array<elem> {",
+ " indexing: summary",
+ " struct-field name {",
+ structFieldSpec(elem_array_name_attr),
+ " }",
+ " struct-field weight {",
+ structFieldSpec(elem_array_weight_attr),
+ " }",
+ " }",
+ " field " + prefixedFieldName("elem_map") + " type map<string, elem> {",
+ " indexing: summary",
+ " struct-field key {",
+ structFieldSpec(elem_map_key_attr),
+ " }",
+ " struct-field value.name {",
+ structFieldSpec(elem_map_value_name_attr),
+ " }",
+ " struct-field value.weight {",
+ structFieldSpec(elem_map_value_weight_attr),
+ " }",
+ " }",
+ " field " + prefixedFieldName("str_int_map") + " type map<string, int> {",
+ " indexing: summary",
+ " struct-field key {",
+ structFieldSpec(str_int_map_key_attr),
+ " }",
+ " struct-field value {",
+ structFieldSpec(str_int_map_value_attr),
+ " }",
+ " }",
+ " }",
+ "}");
+ }
+
+ private static String structFieldSpec(boolean isAttribute) {
+ return isAttribute ? " indexing: attribute" : "";
+ }
+ }
+
+ private static class ParentStructSdBuilder extends AncestorStructSdBuilder {
+ ParentStructSdBuilder() {
+ super("parent", "");
+ }
+ }
+
+ private static class GrandParentStructSdBuilder extends AncestorStructSdBuilder {
+ GrandParentStructSdBuilder() {
+ super("grandparent", "gp_");
+ }
+ }
+
+ private static class DescendantSdBuilder extends NamedSdBuilder {
+ protected String parentName;
+ private String parentFieldPrefix;
+
+ public DescendantSdBuilder(String name, String fieldPrefix, String parentName, String parentFieldPrefix) {
+ super(name, fieldPrefix);
+ this.parentName = parentName;
+ this.parentFieldPrefix = parentFieldPrefix;
+ }
+
+ protected String parentRef() {
+ return parentName + "_ref";
+ }
+
+ protected String importParentField(String fieldName) {
+ return " import field " + parentRef() + "." + parentFieldPrefix + fieldName + " as " + prefixedFieldName(fieldName) + " {}";
+ }
+ }
+
+ private static class DescendantStructSdBuilder extends DescendantSdBuilder {
+ public DescendantStructSdBuilder(String name, String fieldPrefix, String parentName, String parentFieldPrefix) {
+ super(name, fieldPrefix, parentName, parentFieldPrefix);
+ }
+
+ public String build() {
+ return joinLines("search " + name + " {",
+ " document " + name + " {",
+ " field " + parentRef() + " type reference<" + parentName + "> {",
+ " indexing: attribute | summary",
+ " }",
+ " }",
+ importParentField("elem_array"),
+ importParentField("elem_map"),
+ importParentField("str_int_map"),
+ "}");
+ }
+ }
+
+ private static class ChildStructSdBuilder extends DescendantStructSdBuilder {
+ public ChildStructSdBuilder() {
+ super("child", "my_", "parent", "");
+ }
+ }
+
+ private static class IntermediateParentStructSdBuilder extends DescendantStructSdBuilder {
+ public IntermediateParentStructSdBuilder() {
+ super("parent", "", "grandparent", "gp_");
+ }
+ }
+
+ private static Schema buildChildSearch(String parentSdContent, String sdContent) throws ParseException {
+ ApplicationBuilder builder = new ApplicationBuilder();
+ builder.addSchema(parentSdContent);
+ builder.addSchema(sdContent);
+ builder.build(true);
+ return builder.getSchema("child");
+ }
+
+ private static Schema buildChildSearch(String grandParentSdContent, String parentSdContent, String sdContent) throws ParseException {
+ ApplicationBuilder builder = new ApplicationBuilder();
+ builder.addSchema(grandParentSdContent);
+ builder.addSchema(parentSdContent);
+ builder.addSchema(sdContent);
+ builder.build(true);
+ return builder.getSchema("child");
+ }
+
+ private static class AncestorPosSdBuilder extends NamedSdBuilder {
+ public AncestorPosSdBuilder(String name, String fieldPrefix) {
+ super(name, fieldPrefix);
+ }
+
+ public String build() {
+ return joinLines("search " + name + " {",
+ " document " + name + " {",
+ "field " + prefixedFieldName("pos") + " type position {",
+ "indexing: attribute | summary",
+ " }",
+ " }",
+ "}");
+ }
+ }
+
+ private static class ParentPosSdBuilder extends AncestorPosSdBuilder {
+ public ParentPosSdBuilder() { super("parent", ""); }
+ }
+
+ private static class GrandParentPosSdBuilder extends AncestorPosSdBuilder {
+ public GrandParentPosSdBuilder() { super("grandparent", "gp_"); }
+ }
+
+ private static class DescendantPosSdBuilder extends DescendantSdBuilder {
+ private boolean import_pos_zcurve_before;
+
+ public DescendantPosSdBuilder(String name, String fieldPrefix, String parentName, String parentFieldPrefix) {
+ super(name, fieldPrefix, parentName, parentFieldPrefix);
+ import_pos_zcurve_before = false;
+ }
+
+ DescendantPosSdBuilder import_pos_zcurve_before(boolean v) { import_pos_zcurve_before = v; return this; }
+
+ public String build() {
+ return joinLines("search " + name + " {",
+ " document " + name + " {",
+ " field " + parentRef() + " type reference<" + parentName + "> {",
+ " indexing: attribute | summary",
+ " }",
+ " }",
+ importPosZCurve(import_pos_zcurve_before),
+ importParentField("pos"),
+ "}");
+ }
+
+ private static String importPosZCurve(boolean doImport) {
+ return doImport ? "import field parent_ref.pos_zcurve as my_pos_zcurve {}" : "";
+ }
+ }
+
+ private static class ChildPosSdBuilder extends DescendantPosSdBuilder {
+ public ChildPosSdBuilder() {
+ super("child", "my_", "parent", "");
+ }
+ }
+
+ private static class IntermediateParentPosSdBuilder extends DescendantPosSdBuilder {
+ public IntermediateParentPosSdBuilder() {
+ super("parent", "", "grandparent", "gp_");
+ }
+ }
+
+ private static void checkPosImport(ParentPosSdBuilder parentBuilder, DescendantPosSdBuilder childBuilder) throws ParseException {
+ Schema schema = buildChildSearch(parentBuilder.build(), childBuilder.build());
+ checkImportedPosFields(schema);
+ }
+
+ private static void checkNestedPosImport(GrandParentPosSdBuilder grandParentBuilder, DescendantPosSdBuilder childBuilder) throws ParseException {
+ Schema schema = buildChildSearch(grandParentBuilder.build(), new IntermediateParentPosSdBuilder().build(), childBuilder.build());
+ checkImportedPosFields(schema);
+ }
+
+ private static void checkImportedPosFields(Schema schema) {
+ assertEquals(2, schema.importedFields().get().fields().size());
+ assertSearchContainsImportedField("my_pos_zcurve", "parent_ref", "parent", "pos_zcurve", schema);
+ assertSearchContainsImportedField("my_pos", "parent_ref", "parent", "pos", schema);
+ }
+
+ @Test
+ public void check_pos_import() throws ParseException {
+ checkPosImport(new ParentPosSdBuilder(), new ChildPosSdBuilder());
+ }
+
+ @Test
+ public void check_nested_pos_import() throws ParseException {
+ checkNestedPosImport(new GrandParentPosSdBuilder(), new ChildPosSdBuilder());
+ }
+
+ @Test
+ public void check_pos_import_after_pos_zcurve_import() throws ParseException {
+ exception.expect(IllegalArgumentException.class);
+ exception.expectMessage("For schema 'child', import field 'my_pos_zcurve': Field 'pos_zcurve' via reference field 'parent_ref': Field already imported");
+ checkPosImport(new ParentPosSdBuilder(), new ChildPosSdBuilder().import_pos_zcurve_before(true));
+ }
+
+ private static ImportedField getImportedField(String name, Schema schema) {
+ if (name.contains(".")) {
+ assertNull(schema.importedFields().get().fields().get(name));
+ String superFieldName = name.substring(0,name.indexOf("."));
+ String subFieldName = name.substring(name.indexOf(".")+1);
+ ImportedField superField = schema.importedFields().get().fields().get(superFieldName);
+ if (superField != null && superField instanceof ImportedComplexField) {
+ return ((ImportedComplexField)superField).getNestedField(subFieldName);
+ }
+ return null;
+ }
+ return schema.importedFields().get().fields().get(name);
+ }
+
+ private static void assertSearchNotContainsImportedField(String fieldName, Schema schema) {
+ ImportedField importedField = getImportedField(fieldName, schema);
+ assertNull(importedField);
+ }
+
+ private static void assertSearchContainsImportedField(String fieldName,
+ String referenceFieldName,
+ String referenceDocType,
+ String targetFieldName,
+ Schema schema) {
+ ImportedField importedField = getImportedField(fieldName, schema);
+ assertNotNull(importedField);
+ assertEquals(fieldName, importedField.fieldName());
+ assertEquals(referenceFieldName, importedField.reference().referenceField().getName());
+ assertEquals(referenceDocType, importedField.reference().targetSearch().getName());
+ assertEquals(targetFieldName, importedField.targetField().getName());
+ }
+
+ private static void checkImportedField(String fieldName, String referenceFieldName, String referenceDocType,
+ String targetFieldName, Schema schema, boolean present) {
+ if (present) {
+ assertSearchContainsImportedField(fieldName, referenceFieldName, referenceDocType, targetFieldName, schema);
+ } else {
+ assertSearchNotContainsImportedField(fieldName, schema);
+ }
+ }
+
+ @Test
+ public void field_with_struct_field_attributes_can_be_imported_from_parents_that_use_inheritance() throws ParseException {
+ var builder = buildParentsUsingInheritance();
+
+ assertParentContainsEntriesAttributes(builder.getSchema("parent_a"));
+ assertParentContainsEntriesAttributes(builder.getSchema("parent_b"));
+
+ var child = builder.getSchema("child");
+ checkImportedField("entries_from_a", "ref_parent_a", "parent_a", "entries", child, true);
+ checkImportedField("entries_from_a.key", "ref_parent_a", "parent_a", "entries.key", child, true);
+ checkImportedField("entries_from_a.value", "ref_parent_a", "parent_a", "entries.value", child, true);
+
+ checkImportedField("entries_from_b", "ref_parent_b", "parent_b", "entries", child, true);
+ checkImportedField("entries_from_b.key", "ref_parent_b", "parent_b", "entries.key", child, true);
+ checkImportedField("entries_from_b.value", "ref_parent_b", "parent_b", "entries.value", child, true);
+ }
+
+ private void assertParentContainsEntriesAttributes(Schema parent) {
+ var attrs = new AttributeFields(parent);
+ assertTrue(attrs.containsAttribute("entries.key"));
+ assertTrue(attrs.containsAttribute("entries.value"));
+ }
+
+ private ApplicationBuilder buildParentsUsingInheritance() throws ParseException {
+ var builder = new ApplicationBuilder();
+ builder.addSchema(joinLines("schema parent_a {",
+ "document parent_a {",
+ " struct Entry {",
+ " field key type string {}",
+ " field value type string {}",
+ " }",
+ " field entries type array<Entry> {",
+ " indexing: summary",
+ " struct-field key { indexing: attribute }",
+ " struct-field value { indexing: attribute }",
+ " }",
+ "}",
+ "}"));
+
+ builder.addSchema(joinLines("schema parent_b {",
+ "document parent_b inherits parent_a {",
+ "}",
+ "}"));
+
+ builder.addSchema(joinLines("schema child {",
+ "document child {",
+ " field ref_parent_a type reference<parent_a> {",
+ " indexing: attribute",
+ " }",
+ " field ref_parent_b type reference<parent_b> {",
+ " indexing: attribute",
+ " }",
+ "}",
+ "import field ref_parent_a.entries as entries_from_a {}",
+ "import field ref_parent_b.entries as entries_from_b {}",
+ "}"));
+
+ builder.build(true);
+ return builder;
+ }
+
+ }
diff --git a/config-model/src/test/java/com/yahoo/schema/processing/IndexingInputsTestCase.java b/config-model/src/test/java/com/yahoo/schema/processing/IndexingInputsTestCase.java
new file mode 100644
index 00000000000..71c79feedc1
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/schema/processing/IndexingInputsTestCase.java
@@ -0,0 +1,45 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.schema.processing;
+
+import com.yahoo.schema.parser.ParseException;
+import org.junit.Test;
+
+import java.io.IOException;
+
+import static com.yahoo.schema.processing.AssertSearchBuilder.assertBuildFails;
+
+/**
+ * @author Simon Thoresen Hult
+ */
+public class IndexingInputsTestCase {
+
+ @Test
+ public void requireThatExtraFieldInputExtraFieldThrows() throws IOException, ParseException {
+ assertBuildFails("src/test/examples/indexing_extra_field_input_extra_field.sd",
+ "For schema 'indexing_extra_field_input_extra_field', field 'bar': Indexing script refers " +
+ "to field 'bar' which does not exist in document type " +
+ "'indexing_extra_field_input_extra_field', and is not a mutable attribute.");
+ }
+
+ @Test
+ public void requireThatExtraFieldInputImplicitThrows() throws IOException, ParseException {
+ assertBuildFails("src/test/examples/indexing_extra_field_input_implicit.sd",
+ "For schema 'indexing_extra_field_input_implicit', field 'foo': Indexing script refers to " +
+ "field 'foo' which does not exist in document type 'indexing_extra_field_input_implicit', and is not a mutable attribute.");
+ }
+
+ @Test
+ public void requireThatExtraFieldInputNullThrows() throws IOException, ParseException {
+ assertBuildFails("src/test/examples/indexing_extra_field_input_null.sd",
+ "For schema 'indexing_extra_field_input_null', field 'foo': Indexing script refers to field " +
+ "'foo' which does not exist in document type 'indexing_extra_field_input_null', and is not a mutable attribute.");
+ }
+
+ @Test
+ public void requireThatExtraFieldInputSelfThrows() throws IOException, ParseException {
+ assertBuildFails("src/test/examples/indexing_extra_field_input_self.sd",
+ "For schema 'indexing_extra_field_input_self', field 'foo': Indexing script refers to field " +
+ "'foo' which does not exist in document type 'indexing_extra_field_input_self', and is not a mutable attribute.");
+ }
+
+}
diff --git a/config-model/src/test/java/com/yahoo/schema/processing/IndexingOutputsTestCase.java b/config-model/src/test/java/com/yahoo/schema/processing/IndexingOutputsTestCase.java
new file mode 100644
index 00000000000..687549f920e
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/schema/processing/IndexingOutputsTestCase.java
@@ -0,0 +1,30 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.schema.processing;
+
+import com.yahoo.schema.parser.ParseException;
+import org.junit.Test;
+
+import java.io.IOException;
+
+import static com.yahoo.schema.processing.AssertSearchBuilder.assertBuildFails;
+
+
+/**
+ * @author Simon Thoresen Hult
+ */
+public class IndexingOutputsTestCase {
+
+ @Test
+ public void requireThatOutputOtherFieldThrows() throws IOException, ParseException {
+ assertBuildFails("src/test/examples/indexing_output_other_field.sd",
+ "For schema 'indexing_output_other_field', field 'foo': Indexing expression 'index bar' " +
+ "attempts to write to a field other than 'foo'.");
+ }
+
+ @Test
+ public void requireThatOutputConflictThrows() throws IOException, ParseException {
+ assertBuildFails("src/test/examples/indexing_output_conflict.sd",
+ "For schema 'indexing_output_confict', field 'bar': For expression 'index bar': Attempting " +
+ "to assign conflicting values to field 'bar'.");
+ }
+}
diff --git a/config-model/src/test/java/com/yahoo/schema/processing/IndexingScriptRewriterTestCase.java b/config-model/src/test/java/com/yahoo/schema/processing/IndexingScriptRewriterTestCase.java
new file mode 100644
index 00000000000..76cb6a5505c
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/schema/processing/IndexingScriptRewriterTestCase.java
@@ -0,0 +1,200 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.schema.processing;
+
+import com.yahoo.config.model.application.provider.BaseDeployLogger;
+import com.yahoo.config.model.test.MockApplicationPackage;
+import com.yahoo.document.DataType;
+import com.yahoo.schema.Index;
+import com.yahoo.schema.RankProfileRegistry;
+import com.yahoo.schema.Schema;
+import com.yahoo.schema.ApplicationBuilder;
+import com.yahoo.schema.AbstractSchemaTestCase;
+import com.yahoo.schema.document.BooleanIndexDefinition;
+import com.yahoo.schema.document.SDDocumentType;
+import com.yahoo.schema.document.SDField;
+import com.yahoo.vespa.documentmodel.SummaryField;
+import com.yahoo.vespa.documentmodel.SummaryTransform;
+import com.yahoo.vespa.indexinglanguage.expressions.ScriptExpression;
+import com.yahoo.vespa.model.container.search.QueryProfiles;
+import org.junit.Test;
+
+import java.util.Arrays;
+import java.util.OptionalDouble;
+import java.util.OptionalInt;
+import java.util.OptionalLong;
+import java.util.Set;
+
+import static com.yahoo.schema.processing.AssertIndexingScript.assertIndexing;
+import static org.junit.Assert.assertEquals;
+
+/**
+ * @author Simon Thoresen Hult
+ */
+public class IndexingScriptRewriterTestCase extends AbstractSchemaTestCase {
+
+ @Test
+ public void testSetLanguageRewriting() {
+ assertIndexingScript("{ input test | set_language; }",
+ createField("test", DataType.STRING, "{ set_language }"));
+ }
+
+ @Test
+ public void testSummaryRewriting() {
+ assertIndexingScript("{ input test | summary test; }",
+ createField("test", DataType.STRING, "{ summary }"));
+ }
+
+ @Test
+ public void testDynamicSummaryRewriting() {
+ SDField field = createField("test", DataType.STRING, "{ summary }");
+ field.addSummaryField(createDynamicSummaryField(field, "dyn"));
+ assertIndexingScript("{ input test | tokenize normalize stem:\"BEST\" | summary dyn; }", field);
+ }
+
+ @Test
+ public void testSummaryRewritingWithIndexing() {
+ assertIndexingScript("{ input test | tokenize normalize stem:\"BEST\" | summary test | index test; }",
+ createField("test", DataType.STRING, "{ summary | index }"));
+ }
+
+ @Test
+ public void testDynamicAndStaticSummariesRewritingWithIndexing() {
+ SDField field = createField("test", DataType.STRING, "{ summary | index }");
+ field.addSummaryField(createDynamicSummaryField(field, "dyn"));
+ field.addSummaryField(createStaticSummaryField(field, "test"));
+ field.addSummaryField(createStaticSummaryField(field, "other"));
+ field.addSummaryField(createDynamicSummaryField(field, "dyn2"));
+ assertIndexingScript("{ input test | tokenize normalize stem:\"BEST\" | summary dyn | summary dyn2 | summary other | " +
+ "summary test | index test; }", field);
+ }
+
+ @Test
+ public void testIntSummaryRewriting() {
+ assertIndexingScript("{ input test | summary test | attribute test; }",
+ createField("test", DataType.INT, "{ summary | index }"));
+ }
+
+ @Test
+ public void testStringAttributeSummaryRewriting() {
+ assertIndexingScript("{ input test | summary test | attribute test; }",
+ createField("test", DataType.STRING, "{ summary | attribute }"));
+ }
+
+ @Test
+ public void testMultiblockTokenize() {
+ SDField field = createField("test", DataType.STRING,
+ "{ input test | tokenize | { summary test; }; }");
+ assertIndexingScript("{ input test | tokenize | { summary test; }; }", field);
+ }
+
+ @Test
+ public void requireThatOutputDefaultsToCurrentField() {
+ assertIndexingScript("{ input test | attribute test; }",
+ createField("test", DataType.STRING, "{ attribute; }"));
+ assertIndexingScript("{ input test | tokenize normalize stem:\"BEST\" | index test; }",
+ createField("test", DataType.STRING, "{ index; }"));
+ assertIndexingScript("{ input test | summary test; }",
+ createField("test", DataType.STRING, "{ summary; }"));
+ }
+
+ @Test
+ public void testTokenizeComparisonDisregardsConfig() {
+ assertIndexingScript("{ input test | tokenize normalize stem:\"BEST\" | summary test | index test; }",
+ createField("test", DataType.STRING, "{ summary | tokenize | index; }"));
+ }
+
+ @Test
+ public void testDerivingFromSimple() throws Exception {
+ assertIndexing(Arrays.asList("clear_state | guard { input access | attribute access; }",
+ "clear_state | guard { input category | split \";\" | attribute category_arr; }",
+ "clear_state | guard { input category | tokenize | index category; }",
+ "clear_state | guard { input categories_src | lowercase | normalize | tokenize normalize stem:\"BEST\" | index categories; }",
+ "clear_state | guard { input categoriesagain_src | lowercase | normalize | tokenize normalize stem:\"BEST\" | index categoriesagain; }",
+ "clear_state | guard { input chatter | tokenize normalize stem:\"BEST\" | index chatter; }",
+ "clear_state | guard { input description | tokenize normalize stem:\"BEST\" | summary description | summary dyndesc | index description; }",
+ "clear_state | guard { input exactemento_src | lowercase | tokenize normalize stem:\"BEST\" | index exactemento | summary exactemento; }",
+ "clear_state | guard { input longdesc | tokenize normalize stem:\"BEST\" | summary dyndesc2 | summary dynlong | summary longdesc | summary longstat; }",
+ "clear_state | guard { input measurement | attribute measurement | summary measurement; }",
+ "clear_state | guard { input measurement | to_array | attribute measurement_arr; }",
+ "clear_state | guard { input popularity | attribute popularity; }",
+ "clear_state | guard { input popularity * input measurement | attribute popsiness; }",
+ "clear_state | guard { input smallattribute | attribute smallattribute; }",
+ "clear_state | guard { input title | tokenize normalize stem:\"BEST\" | summary title | index title; }",
+ "clear_state | guard { input title . \" \" . input category | tokenize | summary exact | index exact; }"),
+ ApplicationBuilder.buildFromFile("src/test/examples/simple.sd"));
+ }
+
+ @Test
+ public void testIndexRewrite() throws Exception {
+ assertIndexing(
+ Arrays.asList("clear_state | guard { input title_src | lowercase | normalize | " +
+ " tokenize | index title; }",
+ "clear_state | guard { input title_src | summary title_s; }"),
+ ApplicationBuilder.buildFromFile("src/test/examples/indexrewrite.sd"));
+ }
+
+ @Test
+ public void requireThatPredicateFieldsGetOptimization() {
+ assertIndexingScript("{ 10 | set_var arity | { input test | optimize_predicate | attribute test; }; }",
+ createPredicateField(
+ "test", DataType.PREDICATE, "{ attribute; }", 10, OptionalLong.empty(), OptionalLong.empty()));
+ assertIndexingScript("{ 10 | set_var arity | { input test | optimize_predicate | summary test | attribute test; }; }",
+ createPredicateField(
+ "test", DataType.PREDICATE, "{ summary | attribute ; }", 10, OptionalLong.empty(), OptionalLong.empty()));
+ assertIndexingScript(
+ "{ 2 | set_var arity | 0L | set_var lower_bound | 1023L | set_var upper_bound | " +
+ "{ input test | optimize_predicate | attribute test; }; }",
+ createPredicateField("test", DataType.PREDICATE, "{ attribute; }", 2, OptionalLong.of(0L), OptionalLong.of(1023L)));
+ }
+
+ private static void assertIndexingScript(String expectedScript, SDField unprocessedField) {
+ assertEquals(expectedScript,
+ processField(unprocessedField).toString());
+ }
+
+ private static ScriptExpression processField(SDField unprocessedField) {
+ SDDocumentType sdoc = new SDDocumentType("test");
+ sdoc.addField(unprocessedField);
+ Schema schema = new Schema("test", MockApplicationPackage.createEmpty());
+ schema.addDocument(sdoc);
+ new Processing().process(schema, new BaseDeployLogger(), new RankProfileRegistry(),
+ new QueryProfiles(), true, false, Set.of());
+ return unprocessedField.getIndexingScript();
+ }
+
+ private static SDField createField(String name, DataType type, String script) {
+ SDField field = new SDField(null, name, type);
+ field.parseIndexingScript(script);
+ return field;
+ }
+
+ private static SDField createPredicateField(
+ String name, DataType type, String script, int arity, OptionalLong lower_bound, OptionalLong upper_bound) {
+ SDField field = new SDField(null, name, type);
+ field.parseIndexingScript(script);
+ Index index = new Index("foo");
+ index.setBooleanIndexDefiniton(new BooleanIndexDefinition(
+ OptionalInt.of(arity), lower_bound, upper_bound, OptionalDouble.empty()));
+ field.addIndex(index);
+ return field;
+ }
+
+ private static SummaryField createDynamicSummaryField(SDField field, String name) {
+ return createSummaryField(field, name, true);
+ }
+
+ private static SummaryField createStaticSummaryField(SDField field, String name) {
+ return createSummaryField(field, name, false);
+ }
+
+ private static SummaryField createSummaryField(SDField field, String name, boolean dynamic) {
+ SummaryField summaryField = new SummaryField(name, field.getDataType());
+ if (dynamic) {
+ summaryField.setTransform(SummaryTransform.DYNAMICTEASER);
+ }
+ summaryField.addDestination("default");
+ summaryField.addSource(field.getName());
+ return summaryField;
+ }
+
+}
diff --git a/config-model/src/test/java/com/yahoo/schema/processing/IndexingValidationTestCase.java b/config-model/src/test/java/com/yahoo/schema/processing/IndexingValidationTestCase.java
new file mode 100644
index 00000000000..4da6880aa26
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/schema/processing/IndexingValidationTestCase.java
@@ -0,0 +1,76 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.schema.processing;
+
+import com.yahoo.schema.ApplicationBuilder;
+import com.yahoo.schema.derived.AbstractExportingTestCase;
+import com.yahoo.schema.parser.ParseException;
+import org.junit.Test;
+
+import java.io.IOException;
+import java.util.Arrays;
+
+import static com.yahoo.schema.processing.AssertIndexingScript.assertIndexing;
+import static com.yahoo.schema.processing.AssertSearchBuilder.assertBuildFails;
+
+/**
+ * @author Simon Thoresen Hult
+ */
+public class IndexingValidationTestCase extends AbstractExportingTestCase {
+
+ @Test
+ public void testAttributeChanged() throws IOException, ParseException {
+ assertBuildFails("src/test/examples/indexing_attribute_changed.sd",
+ "For schema 'indexing_attribute_changed', field 'foo': For expression 'attribute foo': " +
+ "Attempting to assign conflicting values to field 'foo'.");
+ }
+
+ @Test
+ public void testAttributeOther() throws IOException, ParseException {
+ assertBuildFails("src/test/examples/indexing_attribute_other.sd",
+ "For schema 'indexing_attribute_other', field 'foo': Indexing expression 'attribute bar' " +
+ "attempts to write to a field other than 'foo'.");
+ }
+
+ @Test
+ public void testIndexChanged() throws IOException, ParseException {
+ assertBuildFails("src/test/examples/indexing_index_changed.sd",
+ "For schema 'indexing_index_changed', field 'foo': For expression 'index foo': " +
+ "Attempting to assign conflicting values to field 'foo'.");
+ }
+
+ @Test
+ public void testIndexOther() throws IOException, ParseException {
+ assertBuildFails("src/test/examples/indexing_index_other.sd",
+ "For schema 'indexing_index_other', field 'foo': Indexing expression 'index bar' " +
+ "attempts to write to a field other than 'foo'.");
+ }
+
+ @Test
+ public void testSummaryChanged() throws IOException, ParseException {
+ assertBuildFails("src/test/examples/indexing_summary_changed.sd",
+ "For schema 'indexing_summary_fail', field 'foo': For expression 'summary foo': Attempting " +
+ "to assign conflicting values to field 'foo'.");
+ }
+
+ @Test
+ public void testSummaryOther() throws IOException, ParseException {
+ assertBuildFails("src/test/examples/indexing_summary_other.sd",
+ "For schema 'indexing_summary_other', field 'foo': Indexing expression 'summary bar' " +
+ "attempts to write to a field other than 'foo'.");
+ }
+
+ @Test
+ public void testExtraField() throws IOException, ParseException {
+ assertIndexing(
+ Arrays.asList("clear_state | guard { input my_index | tokenize normalize stem:\"BEST\" | index my_index | summary my_index }",
+ "clear_state | guard { input my_input | tokenize normalize stem:\"BEST\" | index my_extra | summary my_extra }"),
+ ApplicationBuilder.buildFromFile("src/test/examples/indexing_extra.sd"));
+ }
+
+ @Test
+ public void requireThatMultilineOutputConflictThrows() throws IOException, ParseException {
+ assertBuildFails("src/test/examples/indexing_multiline_output_conflict.sd",
+ "For schema 'indexing_multiline_output_confict', field 'cox': For expression 'index cox': " +
+ "Attempting to assign conflicting values to field 'cox'.");
+ }
+}
diff --git a/config-model/src/test/java/com/yahoo/schema/processing/IndexingValuesTestCase.java b/config-model/src/test/java/com/yahoo/schema/processing/IndexingValuesTestCase.java
new file mode 100644
index 00000000000..2784fe69b28
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/schema/processing/IndexingValuesTestCase.java
@@ -0,0 +1,30 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.schema.processing;
+
+import com.yahoo.schema.parser.ParseException;
+import org.junit.Test;
+
+import java.io.IOException;
+
+import static com.yahoo.schema.processing.AssertSearchBuilder.assertBuildFails;
+import static com.yahoo.schema.processing.AssertSearchBuilder.assertBuilds;
+
+/**
+ * @author Simon Thoresen Hult
+ */
+public class IndexingValuesTestCase {
+
+ @Test
+ public void requireThatModifyFieldNoOutputDoesNotThrow() throws IOException, ParseException {
+ assertBuilds("src/test/examples/indexing_modify_field_no_output.sd");
+ }
+
+ @Test
+ public void requireThatInputOtherFieldThrows() throws IOException, ParseException {
+ assertBuildFails("src/test/examples/indexing_input_other_field.sd",
+ "For schema 'indexing_input_other_field', field 'bar': Indexing expression 'input foo' " +
+ "attempts to modify the value of the document field 'bar'. " +
+ "Use a field outside the document block instead.");
+ }
+
+}
diff --git a/config-model/src/test/java/com/yahoo/schema/processing/IntegerIndex2AttributeTestCase.java b/config-model/src/test/java/com/yahoo/schema/processing/IntegerIndex2AttributeTestCase.java
new file mode 100644
index 00000000000..f36effab146
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/schema/processing/IntegerIndex2AttributeTestCase.java
@@ -0,0 +1,61 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.schema.processing;
+
+import com.yahoo.config.model.application.provider.BaseDeployLogger;
+import com.yahoo.schema.RankProfileRegistry;
+import com.yahoo.schema.Schema;
+import com.yahoo.schema.ApplicationBuilder;
+import com.yahoo.schema.AbstractSchemaTestCase;
+import com.yahoo.schema.document.SDField;
+import com.yahoo.schema.parser.ParseException;
+import com.yahoo.vespa.model.container.search.QueryProfiles;
+import org.junit.Test;
+
+import java.io.IOException;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * @author baldersheim
+ */
+public class IntegerIndex2AttributeTestCase extends AbstractSchemaTestCase {
+
+ @Test
+ public void testIntegerIndex2Attribute() throws IOException, ParseException {
+ Schema schema = ApplicationBuilder.buildFromFile("src/test/examples/integerindex2attribute.sd");
+ new IntegerIndex2Attribute(schema, new BaseDeployLogger(), new RankProfileRegistry(), new QueryProfiles()).process(true, false);
+
+ SDField f;
+ f = schema.getConcreteField("s1");
+ assertTrue(f.getAttributes().isEmpty());
+ assertTrue(f.existsIndex("s1"));
+ f = schema.getConcreteField("s2");
+ assertEquals(f.getAttributes().size(), 1);
+ assertTrue(f.existsIndex("s2"));
+
+ f = schema.getConcreteField("as1");
+ assertTrue(f.getAttributes().isEmpty());
+ assertTrue(f.existsIndex("as1"));
+ f = schema.getConcreteField("as2");
+ assertEquals(f.getAttributes().size(), 1);
+ assertTrue(f.existsIndex("as2"));
+
+ f = schema.getConcreteField("i1");
+ assertEquals(f.getAttributes().size(), 1);
+ assertFalse(f.existsIndex("i1"));
+
+ f = schema.getConcreteField("i2");
+ assertEquals(f.getAttributes().size(), 1);
+ assertFalse(f.existsIndex("i2"));
+
+ f = schema.getConcreteField("ai1");
+ assertEquals(schema.getConcreteField("ai1").getAttributes().size(), 1);
+ assertFalse(schema.getConcreteField("ai1").existsIndex("ai1"));
+ f = schema.getConcreteField("ai2");
+ assertEquals(f.getAttributes().size(), 1);
+ assertFalse(f.existsIndex("ai2"));
+ }
+
+}
diff --git a/config-model/src/test/java/com/yahoo/schema/processing/MatchPhaseSettingsValidatorTestCase.java b/config-model/src/test/java/com/yahoo/schema/processing/MatchPhaseSettingsValidatorTestCase.java
new file mode 100644
index 00000000000..530b6a95ce8
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/schema/processing/MatchPhaseSettingsValidatorTestCase.java
@@ -0,0 +1,37 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.schema.processing;
+
+import org.junit.Test;
+
+import static com.yahoo.schema.processing.AssertSearchBuilder.assertBuildFails;
+
+public class MatchPhaseSettingsValidatorTestCase {
+
+ private static String getMessagePrefix() {
+ return "In search definition 'test', rank-profile 'default': match-phase attribute 'foo' ";
+ }
+
+ @Test
+ public void requireThatAttributeMustExists() throws Exception {
+ assertBuildFails("src/test/examples/matchphase/non_existing_attribute.sd",
+ getMessagePrefix() + "does not exists");
+ }
+
+ @Test
+ public void requireThatAttributeMustBeNumeric() throws Exception {
+ assertBuildFails("src/test/examples/matchphase/wrong_data_type_attribute.sd",
+ getMessagePrefix() + "must be single value numeric, but it is 'string'");
+ }
+
+ @Test
+ public void requireThatAttributeMustBeSingleValue() throws Exception {
+ assertBuildFails("src/test/examples/matchphase/wrong_collection_type_attribute.sd",
+ getMessagePrefix() + "must be single value numeric, but it is 'Array<int>'");
+ }
+
+ @Test
+ public void requireThatAttributeMustHaveFastSearch() throws Exception {
+ assertBuildFails("src/test/examples/matchphase/non_fast_search_attribute.sd",
+ getMessagePrefix() + "must be fast-search, but it is not");
+ }
+}
diff --git a/config-model/src/test/java/com/yahoo/schema/processing/MatchedElementsOnlyResolverTestCase.java b/config-model/src/test/java/com/yahoo/schema/processing/MatchedElementsOnlyResolverTestCase.java
new file mode 100644
index 00000000000..c401376ac3a
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/schema/processing/MatchedElementsOnlyResolverTestCase.java
@@ -0,0 +1,192 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.schema.processing;
+
+import com.yahoo.schema.RankProfileRegistry;
+import com.yahoo.schema.Schema;
+import com.yahoo.schema.ApplicationBuilder;
+import com.yahoo.schema.parser.ParseException;
+import com.yahoo.vespa.documentmodel.SummaryField;
+import com.yahoo.vespa.documentmodel.SummaryTransform;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+
+import static com.yahoo.config.model.test.TestUtil.joinLines;
+import static org.junit.Assert.assertEquals;
+
+/**
+ * @author geirst
+ */
+public class MatchedElementsOnlyResolverTestCase {
+
+ @SuppressWarnings("deprecation")
+ @Rule
+ public final ExpectedException exceptionRule = ExpectedException.none();
+
+ @Test
+ public void complex_field_with_some_struct_field_attributes_gets_default_transform() throws ParseException {
+ assertSummaryField(joinLines("field my_field type map<string, string> {",
+ " indexing: summary",
+ " summary: matched-elements-only",
+ " struct-field key { indexing: attribute }",
+ "}"),
+ "my_field", SummaryTransform.MATCHED_ELEMENTS_FILTER);
+
+ assertSummaryField(joinLines("field my_field type map<string, elem> {",
+ " indexing: summary",
+ " summary: matched-elements-only",
+ " struct-field key { indexing: attribute }",
+ "}"),
+ "my_field", SummaryTransform.MATCHED_ELEMENTS_FILTER);
+
+ assertSummaryField(joinLines("field my_field type array<elem> {",
+ " indexing: summary",
+ " summary: matched-elements-only",
+ " struct-field name { indexing: attribute }",
+ "}"),
+ "my_field", SummaryTransform.MATCHED_ELEMENTS_FILTER);
+ }
+
+ @Test
+ public void complex_field_with_only_struct_field_attributes_gets_attribute_transform() throws ParseException {
+ assertSummaryField(joinLines("field my_field type map<string, string> {",
+ " indexing: summary",
+ " summary: matched-elements-only",
+ " struct-field key { indexing: attribute }",
+ " struct-field value { indexing: attribute }",
+ "}"),
+ "my_field", SummaryTransform.MATCHED_ATTRIBUTE_ELEMENTS_FILTER);
+
+ assertSummaryField(joinLines("field my_field type map<string, elem> {",
+ " indexing: summary",
+ " summary: matched-elements-only",
+ " struct-field key { indexing: attribute }",
+ " struct-field value.name { indexing: attribute }",
+ " struct-field value.weight { indexing: attribute }",
+ "}"),
+ "my_field", SummaryTransform.MATCHED_ATTRIBUTE_ELEMENTS_FILTER);
+
+ assertSummaryField(joinLines("field my_field type array<elem> {",
+ " indexing: summary",
+ " summary: matched-elements-only",
+ " struct-field name { indexing: attribute }",
+ " struct-field weight { indexing: attribute }",
+ "}"),
+ "my_field", SummaryTransform.MATCHED_ATTRIBUTE_ELEMENTS_FILTER);
+ }
+
+ @Test
+ public void explicit_complex_summary_field_can_use_filter_transform_with_reference_to_source_field() throws ParseException {
+ String documentSummary = joinLines("document-summary my_summary {",
+ " summary my_filter_field type map<string, string> {",
+ " source: my_field",
+ " matched-elements-only",
+ " }",
+ "}");
+ {
+ var search = buildSearch(joinLines("field my_field type map<string, string> {",
+ " indexing: summary",
+ " struct-field key { indexing: attribute }",
+ "}"),
+ documentSummary);
+ assertSummaryField(search.getSummaryField("my_filter_field"),
+ SummaryTransform.MATCHED_ELEMENTS_FILTER, "my_field");
+ assertSummaryField(search.getSummaryField("my_field"),
+ SummaryTransform.NONE, "my_field");
+ }
+ {
+ var search = buildSearch(joinLines("field my_field type map<string, string> {",
+ " indexing: summary",
+ " struct-field key { indexing: attribute }",
+ " struct-field value { indexing: attribute }",
+ "}"),
+ documentSummary);
+ assertSummaryField(search.getSummaryField("my_filter_field"),
+ SummaryTransform.MATCHED_ATTRIBUTE_ELEMENTS_FILTER, "my_field");
+ assertSummaryField(search.getSummaryField("my_field"),
+ SummaryTransform.ATTRIBUTECOMBINER, "my_field");
+ }
+ }
+
+ @Test
+ public void primitive_array_attribute_field_gets_attribute_transform() throws ParseException {
+ assertSummaryField(joinLines("field my_field type array<string> {",
+ " indexing: attribute | summary",
+ " summary: matched-elements-only",
+ "}"),
+ "my_field", SummaryTransform.MATCHED_ATTRIBUTE_ELEMENTS_FILTER);
+ }
+
+ @Test
+ public void primitive_weighted_set_attribute_field_gets_attribute_transform() throws ParseException {
+ assertSummaryField(joinLines("field my_field type weightedset<string> {",
+ " indexing: attribute | summary",
+ " summary: matched-elements-only",
+ "}"),
+ "my_field", SummaryTransform.MATCHED_ATTRIBUTE_ELEMENTS_FILTER);
+ }
+
+ @Test
+ public void explicit_summary_field_can_use_filter_transform_with_reference_to_attribute_source_field() throws ParseException {
+ String documentSummary = joinLines("document-summary my_summary {",
+ " summary my_filter_field type array<string> {",
+ " source: my_field",
+ " matched-elements-only",
+ " }",
+ "}");
+
+ var search = buildSearch(joinLines(
+ "field my_field type array<string> {",
+ " indexing: attribute | summary",
+ "}"),
+ documentSummary);
+ assertSummaryField(search.getSummaryField("my_filter_field"),
+ SummaryTransform.MATCHED_ATTRIBUTE_ELEMENTS_FILTER, "my_field");
+ assertSummaryField(search.getSummaryField("my_field"),
+ SummaryTransform.ATTRIBUTE, "my_field");
+ }
+
+ @Test
+ public void unsupported_field_type_throws() throws ParseException {
+ exceptionRule.expect(IllegalArgumentException.class);
+ exceptionRule.expectMessage("For schema 'test', document summary 'default', summary field 'my_field': " +
+ "'matched-elements-only' is not supported for this field type. " +
+ "Supported field types are: array of primitive, weighted set of primitive, " +
+ "array of simple struct, map of primitive type to simple struct, " +
+ "and map of primitive type to primitive type");
+ buildSearch(joinLines("field my_field type string {",
+ " indexing: summary",
+ " summary: matched-elements-only",
+ "}"));
+ }
+
+ private void assertSummaryField(String fieldContent, String fieldName, SummaryTransform expTransform) throws ParseException {
+ var search = buildSearch(fieldContent);
+ assertSummaryField(search.getSummaryField(fieldName), expTransform, fieldName);
+ }
+
+ private void assertSummaryField(SummaryField field, SummaryTransform expTransform, String expSourceField) {
+ assertEquals(expTransform, field.getTransform());
+ assertEquals(expSourceField, field.getSingleSource());
+ }
+
+ private Schema buildSearch(String field) throws ParseException {
+ return buildSearch(field, "");
+ }
+
+ private Schema buildSearch(String field, String summary) throws ParseException {
+ var builder = new ApplicationBuilder(new RankProfileRegistry());
+ builder.addSchema(joinLines("search test {",
+ " document test {",
+ " struct elem {",
+ " field name type string {}",
+ " field weight type int {}",
+ " }",
+ field,
+ " }",
+ summary,
+ "}"));
+ builder.build(true);
+ return builder.getSchema();
+ }
+}
diff --git a/config-model/src/test/java/com/yahoo/schema/processing/NGramTestCase.java b/config-model/src/test/java/com/yahoo/schema/processing/NGramTestCase.java
new file mode 100644
index 00000000000..912e6fcf030
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/schema/processing/NGramTestCase.java
@@ -0,0 +1,88 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.schema.processing;
+
+import com.yahoo.schema.Schema;
+import com.yahoo.schema.ApplicationBuilder;
+import com.yahoo.schema.AbstractSchemaTestCase;
+import com.yahoo.schema.document.MatchType;
+import com.yahoo.schema.document.SDField;
+import com.yahoo.schema.document.Stemming;
+import com.yahoo.schema.parser.ParseException;
+import org.junit.Test;
+
+import java.io.IOException;
+import java.util.List;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.fail;
+
+/**
+ * @author bratseth
+ */
+public class NGramTestCase extends AbstractSchemaTestCase {
+
+ @Test
+ public void testNGram() throws IOException, ParseException {
+ Schema schema = ApplicationBuilder.buildFromFile("src/test/examples/ngram.sd");
+ assertNotNull(schema);
+
+ SDField gram1 = schema.getConcreteField("gram_1");
+ assertEquals(MatchType.GRAM, gram1.getMatching().getType());
+ assertEquals(1, gram1.getMatching().getGramSize());
+
+ SDField gram2 = schema.getConcreteField("gram_2");
+ assertEquals(MatchType.GRAM, gram2.getMatching().getType());
+ assertEquals(-1, gram2.getMatching().getGramSize()); // Not set explicitly
+
+ SDField gram3= schema.getConcreteField("gram_3");
+ assertEquals(MatchType.GRAM,gram3.getMatching().getType());
+ assertEquals(3, gram3.getMatching().getGramSize());
+
+ assertEquals("input gram_1 | ngram 1 | index gram_1 | summary gram_1", gram1.getIndexingScript().iterator().next().toString());
+ assertEquals("input gram_2 | ngram 2 | attribute gram_2 | index gram_2", gram2.getIndexingScript().iterator().next().toString());
+ assertEquals("input gram_3 | ngram 3 | index gram_3", gram3.getIndexingScript().iterator().next().toString());
+
+ assertFalse(gram1.getNormalizing().doRemoveAccents());
+ assertEquals(Stemming.NONE, gram1.getStemming());
+
+ List<String> queryCommands = gram1.getQueryCommands();
+ assertEquals(2, queryCommands.size());
+ assertEquals("ngram 1", queryCommands.get(1));
+ }
+
+ @Test
+ public void testInvalidNGramSetting1() throws IOException, ParseException {
+ try {
+ Schema schema = ApplicationBuilder.buildFromFile("src/test/examples/invalidngram1.sd");
+ fail("Should cause an exception");
+ }
+ catch (IllegalArgumentException e) {
+ assertEquals("gram-size can only be set when the matching mode is 'gram'", e.getMessage());
+ }
+ }
+
+ @Test
+ public void testInvalidNGramSetting2() throws IOException, ParseException {
+ try {
+ Schema schema = ApplicationBuilder.buildFromFile("src/test/examples/invalidngram2.sd");
+ fail("Should cause an exception");
+ }
+ catch (IllegalArgumentException e) {
+ assertEquals("gram-size can only be set when the matching mode is 'gram'", e.getMessage());
+ }
+ }
+
+ @Test
+ public void testInvalidNGramSetting3() throws IOException, ParseException {
+ try {
+ Schema schema = ApplicationBuilder.buildFromFile("src/test/examples/invalidngram3.sd");
+ fail("Should cause an exception");
+ }
+ catch (IllegalArgumentException e) {
+ assertEquals("gram matching is not supported with attributes, use 'index' in indexing", e.getMessage());
+ }
+ }
+
+}
diff --git a/config-model/src/test/java/com/yahoo/schema/processing/PagedAttributeValidatorTestCase.java b/config-model/src/test/java/com/yahoo/schema/processing/PagedAttributeValidatorTestCase.java
new file mode 100644
index 00000000000..a291dda24b9
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/schema/processing/PagedAttributeValidatorTestCase.java
@@ -0,0 +1,119 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.schema.processing;
+
+import com.yahoo.config.model.application.provider.BaseDeployLogger;
+import com.yahoo.schema.parser.ParseException;
+import org.junit.Test;
+
+import java.util.Optional;
+
+import static com.yahoo.config.model.test.TestUtil.joinLines;
+import static com.yahoo.schema.ApplicationBuilder.createFromString;
+import static com.yahoo.schema.ApplicationBuilder.createFromStrings;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+public class PagedAttributeValidatorTestCase {
+
+ @Test
+ public void dense_tensor_attribute_supports_paged_setting() throws ParseException {
+ assertPagedSupported("tensor(x[2],y[2])");
+ }
+
+ @Test
+ public void primitive_attribute_types_support_paged_setting() throws ParseException {
+ assertPagedSupported("int");
+ assertPagedSupported("array<int>");
+ assertPagedSupported("weightedset<int>");
+
+ assertPagedSupported("string");
+ assertPagedSupported("array<string>");
+ assertPagedSupported("weightedset<string>");
+ }
+
+ @Test
+ public void struct_field_attributes_support_paged_setting() throws ParseException {
+ var sd = joinLines("schema test {",
+ " document test {",
+ " struct elem {",
+ " field first type int {}",
+ " field second type string {}",
+ " }",
+ " field foo type array<elem> {",
+ " indexing: summary",
+ " struct-field first {",
+ " indexing: attribute",
+ " attribute: paged",
+ " }",
+ " struct-field second {",
+ " indexing: attribute",
+ " attribute: paged",
+ " }",
+ " }",
+ " }",
+ "}");
+
+ var appBuilder = createFromString(sd);
+ var field = appBuilder.getSchema().getField("foo");
+ assertTrue(field.getStructField("first").getAttribute().isPaged());
+ assertTrue(field.getStructField("second").getAttribute().isPaged());
+ }
+
+ private void assertPagedSupported(String fieldType) throws ParseException {
+ var appBuilder = createFromString(getSd(fieldType));
+ var attribute = appBuilder.getSchema().getAttribute("foo");
+ assertTrue(attribute.isPaged());
+ }
+
+ @Test
+ public void non_dense_tensor_attribute_does_not_support_paged_setting() throws ParseException {
+ assertPagedSettingNotSupported("tensor(x{},y[2])");
+ }
+
+ @Test
+ public void predicate_attribute_does_not_support_paged_setting() throws ParseException {
+ assertPagedSettingNotSupported("predicate");
+ }
+
+ @Test
+ public void reference_attribute_does_not_support_paged_setting() throws ParseException {
+ assertPagedSettingNotSupported("reference<parent>", Optional.of(getSd("parent", "int")));
+ }
+
+ private void assertPagedSettingNotSupported(String fieldType) throws ParseException {
+ assertPagedSettingNotSupported(fieldType, Optional.empty());
+ }
+
+ private void assertPagedSettingNotSupported(String fieldType, Optional<String> parentSd) throws ParseException {
+ try {
+ if (parentSd.isPresent()) {
+ createFromStrings(new BaseDeployLogger(), parentSd.get(), getSd(fieldType));
+ } else {
+ createFromString(getSd(fieldType));
+ }
+ fail("Expected exception");
+ } catch (IllegalArgumentException e) {
+ assertEquals("For schema 'test', field 'foo': The 'paged' attribute setting is not supported for non-dense tensor, predicate and reference types",
+ e.getMessage());
+ }
+ }
+
+ private String getSd(String fieldType) {
+ return getSd("test", fieldType);
+ }
+
+ private String getSd(String docType, String fieldType) {
+ return joinLines(
+ "schema " + docType + " {",
+ " document " + docType + " {",
+ " field foo type " + fieldType + "{",
+ " indexing: attribute",
+ " attribute: paged",
+ " }",
+ " }",
+ "}");
+ }
+
+}
diff --git a/config-model/src/test/java/com/yahoo/schema/processing/ParentChildSearchModel.java b/config-model/src/test/java/com/yahoo/schema/processing/ParentChildSearchModel.java
new file mode 100644
index 00000000000..e5636da57a0
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/schema/processing/ParentChildSearchModel.java
@@ -0,0 +1,64 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.schema.processing;
+
+import com.google.common.collect.ImmutableMap;
+import com.yahoo.config.model.application.provider.MockFileRegistry;
+import com.yahoo.config.model.deploy.TestProperties;
+import com.yahoo.config.model.test.MockApplicationPackage;
+import com.yahoo.document.DataType;
+import com.yahoo.documentmodel.NewDocumentReferenceDataType;
+import com.yahoo.schema.DocumentReference;
+import com.yahoo.schema.DocumentReferences;
+import com.yahoo.schema.Schema;
+import com.yahoo.schema.derived.TestableDeployLogger;
+import com.yahoo.schema.document.SDDocumentType;
+import com.yahoo.schema.document.SDField;
+import com.yahoo.schema.document.TemporaryImportedField;
+import com.yahoo.schema.document.TemporarySDField;
+
+/*
+ * Fixture class used for ImportedFieldsResolverTestCase and AdjustPositionSummaryFieldsTestCase.
+ */
+public class ParentChildSearchModel {
+
+ public Schema parentSchema;
+ public Schema childSchema;
+
+ ParentChildSearchModel() {
+ parentSchema = createSearch("parent");
+ childSchema = createSearch("child");
+ }
+
+ protected Schema createSearch(String name) {
+ Schema result = new Schema(name, MockApplicationPackage.createEmpty(), new MockFileRegistry(), new TestableDeployLogger(), new TestProperties());
+ result.addDocument(new SDDocumentType(name));
+ return result;
+ }
+
+ protected static TemporarySDField createField(SDDocumentType repo, String name, DataType dataType, String indexingScript) {
+ TemporarySDField result = new TemporarySDField(repo, name, dataType);
+ result.parseIndexingScript(indexingScript);
+ return result;
+ }
+
+ @SuppressWarnings("deprecation")
+ protected static SDField createRefField(SDDocumentType repo, String parentType, String fieldName) {
+ return new TemporarySDField(repo, fieldName, NewDocumentReferenceDataType.forDocumentName(parentType));
+ }
+
+ protected static void addRefField(Schema child, Schema parent, String fieldName) {
+ SDField refField = createRefField(child.getDocument(), parent.getName(), fieldName);
+ child.getDocument().addField(refField);
+ child.getDocument().setDocumentReferences(new DocumentReferences(ImmutableMap.of(refField.getName(),
+ new DocumentReference(refField, parent))));
+ }
+
+ protected ParentChildSearchModel addImportedField(String fieldName, String referenceFieldName, String targetFieldName) {
+ return addImportedField(childSchema, fieldName, referenceFieldName, targetFieldName);
+ }
+
+ protected ParentChildSearchModel addImportedField(Schema schema, String fieldName, String referenceFieldName, String targetFieldName) {
+ schema.temporaryImportedFields().get().add(new TemporaryImportedField(fieldName, referenceFieldName, targetFieldName));
+ return this;
+ }
+}
diff --git a/config-model/src/test/java/com/yahoo/schema/processing/PositionTestCase.java b/config-model/src/test/java/com/yahoo/schema/processing/PositionTestCase.java
new file mode 100644
index 00000000000..6f0facf9541
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/schema/processing/PositionTestCase.java
@@ -0,0 +1,130 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.schema.processing;
+
+import com.yahoo.document.DataType;
+import com.yahoo.document.DocumentType;
+import com.yahoo.document.PositionDataType;
+import com.yahoo.schema.Schema;
+import com.yahoo.schema.ApplicationBuilder;
+import com.yahoo.schema.document.Attribute;
+import com.yahoo.schema.document.FieldSet;
+import com.yahoo.vespa.documentmodel.SummaryField;
+import com.yahoo.vespa.documentmodel.SummaryTransform;
+
+import org.junit.Test;
+
+import java.util.Arrays;
+import java.util.Iterator;
+
+import static org.junit.Assert.*;
+
+/**
+ * Test Position processor.
+ *
+ * @author hmusum
+ */
+public class PositionTestCase {
+
+ @Test
+ public void inherited_position_zcurve_field_is_not_added_to_document_fieldset() throws Exception {
+ ApplicationBuilder sb = ApplicationBuilder.createFromFiles(Arrays.asList(
+ "src/test/examples/position_base.sd",
+ "src/test/examples/position_inherited.sd"));
+
+ Schema schema = sb.getSchema("position_inherited");
+ FieldSet fieldSet = schema.getDocument().getFieldSets().builtInFieldSets().get(DocumentType.DOCUMENT);
+ assertFalse(fieldSet.getFieldNames().contains(PositionDataType.getZCurveFieldName("pos")));
+ }
+
+ @Test
+ public void requireThatPositionCanBeAttribute() throws Exception {
+ Schema schema = ApplicationBuilder.buildFromFile("src/test/examples/position_attribute.sd");
+ assertNull(schema.getAttribute("pos"));
+ assertNull(schema.getAttribute("pos.x"));
+ assertNull(schema.getAttribute("pos.y"));
+
+ assertPositionAttribute(schema, "pos", Attribute.CollectionType.SINGLE);
+ assertPositionSummary(schema, "pos", false);
+ }
+
+ @Test
+ public void requireThatPositionCanNotBeIndex() throws Exception {
+ try {
+ ApplicationBuilder.buildFromFile("src/test/examples/position_index.sd");
+ fail();
+ } catch (IllegalArgumentException e) {
+ assertEquals("For schema 'position_index', field 'pos': Indexing of data type 'position' is not " +
+ "supported, replace 'index' statement with 'attribute'.", e.getMessage());
+ }
+ }
+
+ @Test
+ public void requireThatSummaryAloneDoesNotCreateZCurve() throws Exception {
+ Schema schema = ApplicationBuilder.buildFromFile("src/test/examples/position_summary.sd");
+ assertNull(schema.getAttribute("pos"));
+ assertNull(schema.getAttribute("pos.x"));
+ assertNull(schema.getAttribute("pos.y"));
+ assertNull(schema.getAttribute("pos.zcurve"));
+
+ SummaryField summary = schema.getSummaryField("pos");
+ assertNotNull(summary);
+ assertEquals(2, summary.getSourceCount());
+ Iterator<SummaryField.Source> it = summary.getSources().iterator();
+ assertEquals("pos.x", it.next().getName());
+ assertEquals("pos.y", it.next().getName());
+ assertEquals(SummaryTransform.NONE, summary.getTransform());
+
+ assertNull(schema.getSummaryField("pos_ext.distance"));
+ }
+
+ @Test
+ public void requireThatExtraFieldCanBePositionAttribute() throws Exception {
+ Schema schema = ApplicationBuilder.buildFromFile("src/test/examples/position_extra.sd");
+ assertNull(schema.getAttribute("pos_ext"));
+ assertNull(schema.getAttribute("pos_ext.x"));
+ assertNull(schema.getAttribute("pos_ext.y"));
+
+ assertPositionAttribute(schema, "pos_ext", Attribute.CollectionType.SINGLE);
+ assertPositionSummary(schema, "pos_ext", false);
+ }
+
+ @Test
+ public void requireThatPositionArrayIsSupported() throws Exception {
+ Schema schema = ApplicationBuilder.buildFromFile("src/test/examples/position_array.sd");
+ assertNull(schema.getAttribute("pos"));
+ assertNull(schema.getAttribute("pos.x"));
+ assertNull(schema.getAttribute("pos.y"));
+
+ assertPositionAttribute(schema, "pos", Attribute.CollectionType.ARRAY);
+ assertPositionSummary(schema, "pos", true);
+ }
+
+ private static void assertPositionAttribute(Schema schema, String fieldName, Attribute.CollectionType type) {
+ Attribute attribute = schema.getAttribute(PositionDataType.getZCurveFieldName(fieldName));
+ assertNotNull(attribute);
+ assertTrue(attribute.isPosition());
+ assertEquals(attribute.getCollectionType(), type);
+ assertEquals(attribute.getType(), Attribute.Type.LONG);
+ }
+
+ private static void assertPositionSummary(Schema schema, String fieldName, boolean isArray) {
+ assertSummaryField(schema,
+ fieldName,
+ PositionDataType.getZCurveFieldName(fieldName),
+ (isArray ? DataType.getArray(PositionDataType.INSTANCE) : PositionDataType.INSTANCE),
+ SummaryTransform.GEOPOS);
+ assertNull(schema.getSummaryField(PositionDataType.getDistanceSummaryFieldName(fieldName)));
+ assertNull(schema.getSummaryField(PositionDataType.getPositionSummaryFieldName(fieldName)));
+ }
+
+ private static void assertSummaryField(Schema schema, String fieldName, String sourceName, DataType dataType,
+ SummaryTransform transform)
+ {
+ SummaryField summary = schema.getSummaryField(fieldName);
+ assertNotNull(summary);
+ assertEquals(1, summary.getSourceCount());
+ assertEquals(sourceName, summary.getSingleSource());
+ assertEquals(dataType, summary.getDataType());
+ assertEquals(transform, summary.getTransform());
+ }
+}
diff --git a/config-model/src/test/java/com/yahoo/schema/processing/RankModifierTestCase.java b/config-model/src/test/java/com/yahoo/schema/processing/RankModifierTestCase.java
new file mode 100644
index 00000000000..69bf62be84b
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/schema/processing/RankModifierTestCase.java
@@ -0,0 +1,22 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.schema.processing;
+
+import com.yahoo.schema.Schema;
+import com.yahoo.schema.ApplicationBuilder;
+import com.yahoo.schema.AbstractSchemaTestCase;
+import com.yahoo.schema.parser.ParseException;
+import org.junit.Test;
+
+import java.io.IOException;
+
+/**
+ * Tests for the field "rank {" shortcut
+ * @author vegardh
+ *
+ */
+public class RankModifierTestCase extends AbstractSchemaTestCase {
+ @Test
+ public void testLiteral() throws IOException, ParseException {
+ Schema schema = ApplicationBuilder.buildFromFile("src/test/examples/rankmodifier/literal.sd");
+ }
+}
diff --git a/config-model/src/test/java/com/yahoo/schema/processing/RankProfileSearchFixture.java b/config-model/src/test/java/com/yahoo/schema/processing/RankProfileSearchFixture.java
new file mode 100644
index 00000000000..e380b1ab9af
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/schema/processing/RankProfileSearchFixture.java
@@ -0,0 +1,128 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.schema.processing;
+
+import com.google.common.collect.ImmutableList;
+import com.yahoo.config.application.api.ApplicationPackage;
+import ai.vespa.rankingexpression.importer.configmodelview.MlModelImporter;
+import com.yahoo.config.model.application.provider.BaseDeployLogger;
+import com.yahoo.config.model.application.provider.MockFileRegistry;
+import com.yahoo.config.model.deploy.TestProperties;
+import com.yahoo.config.model.test.MockApplicationPackage;
+import com.yahoo.path.Path;
+import com.yahoo.search.query.profile.QueryProfileRegistry;
+import com.yahoo.schema.RankProfile;
+import com.yahoo.schema.RankProfileRegistry;
+import com.yahoo.schema.Schema;
+import com.yahoo.schema.ApplicationBuilder;
+import com.yahoo.schema.parser.ParseException;
+import ai.vespa.rankingexpression.importer.configmodelview.ImportedMlModels;
+import ai.vespa.rankingexpression.importer.onnx.OnnxImporter;
+import ai.vespa.rankingexpression.importer.tensorflow.TensorFlowImporter;
+import ai.vespa.rankingexpression.importer.lightgbm.LightGBMImporter;
+import ai.vespa.rankingexpression.importer.xgboost.XGBoostImporter;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+import static org.junit.Assert.assertEquals;
+
+/**
+ * Helper class for setting up and asserting over a Search instance with a rank profile given literally
+ * in the search definition language.
+ *
+ * @author geirst
+ */
+class RankProfileSearchFixture {
+
+ private final ImmutableList<MlModelImporter> importers = ImmutableList.of(new TensorFlowImporter(),
+ new OnnxImporter(),
+ new LightGBMImporter(),
+ new XGBoostImporter());
+ private final RankProfileRegistry rankProfileRegistry = new RankProfileRegistry();
+ private final QueryProfileRegistry queryProfileRegistry;
+ private final Schema schema;
+ private final Map<String, RankProfile> compiledRankProfiles = new HashMap<>();
+ private final ExecutorService executor = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());
+
+ public RankProfileRegistry getRankProfileRegistry() {
+ return rankProfileRegistry;
+ }
+
+ public QueryProfileRegistry getQueryProfileRegistry() {
+ return queryProfileRegistry;
+ }
+
+ RankProfileSearchFixture(String rankProfiles) throws ParseException {
+ this(MockApplicationPackage.createEmpty(), new QueryProfileRegistry(), rankProfiles);
+ }
+
+ RankProfileSearchFixture(ApplicationPackage applicationpackage, QueryProfileRegistry queryProfileRegistry,
+ String rankProfiles) throws ParseException {
+ this(applicationpackage, queryProfileRegistry, rankProfiles, null, null);
+ }
+
+ RankProfileSearchFixture(ApplicationPackage applicationpackage, QueryProfileRegistry queryProfileRegistry,
+ String rankProfiles, String constant, String field)
+ throws ParseException {
+ this.queryProfileRegistry = queryProfileRegistry;
+ ApplicationBuilder builder = new ApplicationBuilder(applicationpackage, new MockFileRegistry(), new BaseDeployLogger(), new TestProperties(), rankProfileRegistry, queryProfileRegistry);
+ String sdContent = "search test {\n" +
+ " " + (constant != null ? constant : "") + "\n" +
+ " document test {\n" +
+ " " + (field != null ? field : "") + "\n" +
+ " }\n" +
+ rankProfiles +
+ "\n" +
+ "}";
+ builder.addSchema(sdContent);
+ builder.build(true);
+ schema = builder.getSchema();
+ }
+
+ public void assertFirstPhaseExpression(String expExpression, String rankProfile) {
+ assertEquals(expExpression, compiledRankProfile(rankProfile).getFirstPhaseRanking().getRoot().toString());
+ }
+
+ public void assertSecondPhaseExpression(String expExpression, String rankProfile) {
+ assertEquals(expExpression, compiledRankProfile(rankProfile).getSecondPhaseRanking().getRoot().toString());
+ }
+
+ public void assertRankProperty(String expValue, String name, String rankProfile) {
+ List<RankProfile.RankProperty> rankPropertyList = compiledRankProfile(rankProfile).getRankPropertyMap().get(name);
+ assertEquals(1, rankPropertyList.size());
+ assertEquals(expValue, rankPropertyList.get(0).getValue());
+ }
+
+ public void assertFunction(String expexctedExpression, String functionName, String rankProfile) {
+ assertEquals(expexctedExpression,
+ compiledRankProfile(rankProfile).getFunctions().get(functionName).function().getBody().getRoot().toString());
+ }
+
+ public RankProfile compileRankProfile(String rankProfile) {
+ return compileRankProfile(rankProfile, Path.fromString("nonexistinng"));
+ }
+
+ public RankProfile compileRankProfile(String rankProfile, Path applicationDir) {
+ RankProfile compiled = rankProfileRegistry.get(schema, rankProfile)
+ .compile(queryProfileRegistry,
+ new ImportedMlModels(applicationDir.toFile(), executor, importers));
+ compiledRankProfiles.put(rankProfile, compiled);
+ return compiled;
+ }
+
+ /** Returns the given uncompiled profile */
+ public RankProfile rankProfile(String rankProfile) {
+ return rankProfileRegistry.get(schema, rankProfile);
+ }
+
+ /** Returns the given compiled profile, or null if not compiled yet or not present at all */
+ public RankProfile compiledRankProfile(String rankProfile) {
+ return compiledRankProfiles.get(rankProfile);
+ }
+
+ public Schema search() { return schema; }
+
+}
diff --git a/config-model/src/test/java/com/yahoo/schema/processing/RankPropertyVariablesTestCase.java b/config-model/src/test/java/com/yahoo/schema/processing/RankPropertyVariablesTestCase.java
new file mode 100644
index 00000000000..dab1d9e6e95
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/schema/processing/RankPropertyVariablesTestCase.java
@@ -0,0 +1,47 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.schema.processing;
+
+import com.yahoo.config.model.application.provider.BaseDeployLogger;
+import com.yahoo.search.query.profile.QueryProfileRegistry;
+import com.yahoo.schema.RankProfile.RankProperty;
+import com.yahoo.schema.RankProfileRegistry;
+import com.yahoo.schema.Schema;
+import com.yahoo.schema.ApplicationBuilder;
+import com.yahoo.schema.AbstractSchemaTestCase;
+import com.yahoo.schema.parser.ParseException;
+import org.junit.Test;
+
+import java.io.IOException;
+import java.util.List;
+
+import static org.junit.Assert.fail;
+
+public class RankPropertyVariablesTestCase extends AbstractSchemaTestCase {
+
+ @Test
+ public void testRankPropVariables() throws IOException, ParseException {
+ RankProfileRegistry rankProfileRegistry = new RankProfileRegistry();
+ Schema schema = ApplicationBuilder.buildFromFile("src/test/examples/rankpropvars.sd",
+ new BaseDeployLogger(),
+ rankProfileRegistry,
+ new QueryProfileRegistry());
+ assertRankPropEquals(rankProfileRegistry.get(schema, "other").getRankProperties(), "$testvar1", "foo");
+ assertRankPropEquals(rankProfileRegistry.get(schema, "other").getRankProperties(), "$testvar_2", "bar");
+ assertRankPropEquals(rankProfileRegistry.get(schema, "other").getRankProperties(), "$testvarOne23", "baz");
+ assertRankPropEquals(rankProfileRegistry.get(schema, "another").getRankProperties(), "$Testvar1", "1");
+ assertRankPropEquals(rankProfileRegistry.get(schema, "another").getRankProperties(), "$Testvar_4", "4");
+ assertRankPropEquals(rankProfileRegistry.get(schema, "another").getRankProperties(), "$testvarFour23", "234234.234");
+ }
+
+ private void assertRankPropEquals(List<RankProperty> props, String key, String val) {
+ for (RankProperty prop : props) {
+ if (prop.getName().equals(key)) {
+ if (prop.getValue().equals(val)) {
+ return;
+ }
+ }
+ }
+ fail(key+":"+val+ " not found in rank properties.");
+ }
+
+}
diff --git a/config-model/src/test/java/com/yahoo/schema/processing/RankingExpressionTypeResolverTestCase.java b/config-model/src/test/java/com/yahoo/schema/processing/RankingExpressionTypeResolverTestCase.java
new file mode 100644
index 00000000000..4b6a22fc81a
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/schema/processing/RankingExpressionTypeResolverTestCase.java
@@ -0,0 +1,521 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.schema.processing;
+
+import com.yahoo.config.application.api.DeployLogger;
+import com.yahoo.search.query.profile.types.FieldDescription;
+import com.yahoo.search.query.profile.types.QueryProfileType;
+import com.yahoo.search.query.profile.types.TensorFieldType;
+import com.yahoo.schema.RankProfile;
+import com.yahoo.schema.RankProfileRegistry;
+import com.yahoo.schema.ApplicationBuilder;
+import com.yahoo.searchlib.rankingexpression.rule.ReferenceNode;
+import com.yahoo.tensor.TensorType;
+import com.yahoo.yolean.Exceptions;
+import org.junit.Test;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.logging.Level;
+import java.util.stream.Collectors;
+
+import static com.yahoo.config.model.test.TestUtil.joinLines;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.fail;
+
+/**
+ * @author bratseth
+ */
+public class RankingExpressionTypeResolverTestCase {
+
+ @Test
+ public void tensorFirstPhaseMustProduceDouble() throws Exception {
+ try {
+ ApplicationBuilder builder = new ApplicationBuilder();
+ builder.addSchema(joinLines(
+ "search test {",
+ " document test { ",
+ " field a type tensor(x[10],y[3]) {",
+ " indexing: attribute",
+ " }",
+ " }",
+ " rank-profile my_rank_profile {",
+ " first-phase {",
+ " expression: attribute(a)",
+ " }",
+ " }",
+ "}"
+ ));
+ builder.build(true);
+ fail("Expected exception");
+ }
+ catch (IllegalArgumentException expected) {
+ assertEquals("In schema 'test', rank profile 'my_rank_profile': The first-phase expression must produce a double (a tensor with no dimensions), but produces tensor(x[10],y[3])",
+ Exceptions.toMessageString(expected));
+ }
+ }
+
+
+ @Test
+ public void tensorFirstPhaseFromConstantMustProduceDouble() throws Exception {
+ try {
+ ApplicationBuilder builder = new ApplicationBuilder();
+ builder.addSchema(joinLines(
+ "schema test {",
+ " document test { ",
+ " field a type tensor(d0[3]) {",
+ " indexing: attribute",
+ " }",
+ " }",
+ " rank-profile my_rank_profile {",
+ " function my_func() {",
+ " expression: x_tensor*2.0",
+ " }",
+ " function inline other_func() {",
+ " expression: z_tensor+3.0",
+ " }",
+ " first-phase {",
+ " expression: reduce(attribute(a),sum,d0)+y_tensor+my_func+other_func",
+ " }",
+ " constants {",
+ " x_tensor {", // legacy form
+ " type: tensor(x{})",
+ " value: { {x:bar}:17 }",
+ " }",
+ " y_tensor tensor(y{}):{{y:foo}:42 }",
+ " z_tensor tensor(z{}):{qux:666}",
+ " }",
+ " }",
+ "}"
+ ));
+ builder.build(true);
+ fail("Expected exception");
+ }
+ catch (IllegalArgumentException expected) {
+ assertEquals("In schema 'test', rank profile 'my_rank_profile': The first-phase expression must produce a double (a tensor with no dimensions), but produces tensor(x{},y{},z{})",
+ Exceptions.toMessageString(expected));
+ }
+ }
+
+
+
+ @Test
+ public void tensorSecondPhaseMustProduceDouble() throws Exception {
+ try {
+ ApplicationBuilder builder = new ApplicationBuilder();
+ builder.addSchema(joinLines(
+ "search test {",
+ " document test { ",
+ " field a type tensor(x[10],y[3]) {",
+ " indexing: attribute",
+ " }",
+ " }",
+ " rank-profile my_rank_profile {",
+ " first-phase {",
+ " expression: sum(attribute(a))",
+ " }",
+ " second-phase {",
+ " expression: attribute(a)",
+ " }",
+ " }",
+ "}"
+ ));
+ builder.build(true);
+ fail("Expected exception");
+ }
+ catch (IllegalArgumentException expected) {
+ assertEquals("In schema 'test', rank profile 'my_rank_profile': The second-phase expression must produce a double (a tensor with no dimensions), but produces tensor(x[10],y[3])",
+ Exceptions.toMessageString(expected));
+ }
+ }
+
+ @Test
+ public void tensorConditionsMustHaveTypeCompatibleBranches() throws Exception {
+ try {
+ ApplicationBuilder schemaBuilder = new ApplicationBuilder();
+ schemaBuilder.addSchema(joinLines(
+ "search test {",
+ " document test { ",
+ " field a type tensor(x[10],y[5]) {",
+ " indexing: attribute",
+ " }",
+ " field b type tensor(z[10]) {",
+ " indexing: attribute",
+ " }",
+ " }",
+ " rank-profile my_rank_profile {",
+ " first-phase {",
+ " expression: sum(if(1>0, attribute(a), attribute(b)))",
+ " }",
+ " }",
+ "}"
+ ));
+ schemaBuilder.build(true);
+ fail("Expected exception");
+ }
+ catch (IllegalArgumentException expected) {
+ assertEquals("In schema 'test', rank profile 'my_rank_profile': The first-phase expression is invalid: An if expression must produce compatible types in both alternatives, but the 'true' type is tensor(x[10],y[5]) while the 'false' type is tensor(z[10])" +
+ "\n'true' branch: attribute(a)" +
+ "\n'false' branch: attribute(b)",
+ Exceptions.toMessageString(expected));
+ }
+ }
+
+ @Test
+ public void testFunctionInvocationTypes() throws Exception {
+ RankProfileRegistry rankProfileRegistry = new RankProfileRegistry();
+ ApplicationBuilder builder = new ApplicationBuilder(rankProfileRegistry);
+ builder.addSchema(joinLines(
+ "search test {",
+ " document test { ",
+ " field a type tensor(x[10],y[3]) {",
+ " indexing: attribute",
+ " }",
+ " field b type tensor(z[10]) {",
+ " indexing: attribute",
+ " }",
+ " }",
+ " rank-profile my_rank_profile {",
+ " function macro1(attribute_to_use) {",
+ " expression: attribute(attribute_to_use)",
+ " }",
+ " summary-features {",
+ " macro1(a)",
+ " macro1(b)",
+ " }",
+ " }",
+ "}"
+ ));
+ builder.build(true);
+ RankProfile profile =
+ builder.getRankProfileRegistry().get(builder.getSchema(), "my_rank_profile");
+ assertEquals(TensorType.fromSpec("tensor(x[10],y[3])"),
+ summaryFeatures(profile).get("macro1(a)").type(profile.typeContext(builder.getQueryProfileRegistry())));
+ assertEquals(TensorType.fromSpec("tensor(z[10])"),
+ summaryFeatures(profile).get("macro1(b)").type(profile.typeContext(builder.getQueryProfileRegistry())));
+ }
+
+ @Test
+ public void testTensorFunctionInvocationTypes_Nested() throws Exception {
+ ApplicationBuilder builder = new ApplicationBuilder();
+ builder.addSchema(joinLines(
+ "search test {",
+ " document test { ",
+ " field a type tensor(x[10],y[1]) {",
+ " indexing: attribute",
+ " }",
+ " field b type tensor(z[10]) {",
+ " indexing: attribute",
+ " }",
+ " }",
+ " rank-profile my_rank_profile {",
+ " function return_a() {",
+ " expression: return_first(attribute(a), attribute(b))",
+ " }",
+ " function return_b() {",
+ " expression: return_second(attribute(a), attribute(b))",
+ " }",
+ " function return_first(e1, e2) {",
+ " expression: e1",
+ " }",
+ " function return_second(e1, e2) {",
+ " expression: return_first(e2, e1)",
+ " }",
+ " summary-features {",
+ " return_a",
+ " return_b",
+ " }",
+ " }",
+ "}"
+ ));
+ builder.build(true);
+ RankProfile profile =
+ builder.getRankProfileRegistry().get(builder.getSchema(), "my_rank_profile");
+ assertEquals(TensorType.fromSpec("tensor(x[10],y[1])"),
+ summaryFeatures(profile).get("return_a").type(profile.typeContext(builder.getQueryProfileRegistry())));
+ assertEquals(TensorType.fromSpec("tensor(z[10])"),
+ summaryFeatures(profile).get("return_b").type(profile.typeContext(builder.getQueryProfileRegistry())));
+ }
+
+ @Test
+ public void testAttributeInvocationViaBoundIdentifier() throws Exception {
+ ApplicationBuilder builder = new ApplicationBuilder();
+ builder.addSchema(joinLines(
+ "search newsarticle {",
+ " document newsarticle {",
+ " field title type string {",
+ " indexing {",
+ " input title | index",
+ " }",
+ " weight: 30",
+ " }",
+ " field usstaticrank type int {",
+ " indexing: summary | attribute",
+ " }",
+ " field eustaticrank type int {",
+ " indexing: summary | attribute",
+ " }",
+ " }",
+ " rank-profile default {",
+ " macro newsboost() { ",
+ " expression: 200 * matches(title)",
+ " }",
+ " macro commonboost(mystaticrank) { ",
+ " expression: attribute(mystaticrank) + newsboost",
+ " }",
+ " macro commonfirstphase(mystaticrank) { ",
+ " expression: nativeFieldMatch(title) + commonboost(mystaticrank) ",
+ " }",
+ " first-phase { expression: commonfirstphase(usstaticrank) }",
+ " }",
+ " rank-profile eurank inherits default {",
+ " first-phase { expression: commonfirstphase(eustaticrank) }",
+ " }",
+ "}"));
+ builder.build(true);
+ RankProfile profile = builder.getRankProfileRegistry().get(builder.getSchema(), "eurank");
+ }
+
+ @Test
+ public void testTensorFunctionInvocationTypes_NestedSameName() throws Exception {
+ ApplicationBuilder builder = new ApplicationBuilder();
+ builder.addSchema(joinLines(
+ "search test {",
+ " document test { ",
+ " field a type tensor(x[10],y[1]) {",
+ " indexing: attribute",
+ " }",
+ " field b type tensor(z[10]) {",
+ " indexing: attribute",
+ " }",
+ " }",
+ " rank-profile my_rank_profile {",
+ " function return_a() {",
+ " expression: return_first(attribute(a), attribute(b))",
+ " }",
+ " function return_b() {",
+ " expression: return_second(attribute(a), attribute(b))",
+ " }",
+ " function return_first(e1, e2) {",
+ " expression: just_return(e1)",
+ " }",
+ " function just_return(e1) {",
+ " expression: e1",
+ " }",
+ " function return_second(e1, e2) {",
+ " expression: return_first(e2+0, e1)",
+ " }",
+ " summary-features {",
+ " return_a",
+ " return_b",
+ " }",
+ " }",
+ "}"
+ ));
+ builder.build(true);
+ RankProfile profile =
+ builder.getRankProfileRegistry().get(builder.getSchema(), "my_rank_profile");
+ assertEquals(TensorType.fromSpec("tensor(x[10],y[1])"),
+ summaryFeatures(profile).get("return_a").type(profile.typeContext(builder.getQueryProfileRegistry())));
+ assertEquals(TensorType.fromSpec("tensor(z[10])"),
+ summaryFeatures(profile).get("return_b").type(profile.typeContext(builder.getQueryProfileRegistry())));
+ }
+
+ @Test
+ public void testTensorFunctionInvocationTypes_viaFuncWithExpr() throws Exception {
+ ApplicationBuilder builder = new ApplicationBuilder();
+ builder.addSchema(joinLines(
+ "search test {",
+ " document test {",
+ " field t1 type tensor<float>(y{}) { indexing: attribute | summary }",
+ " field t2 type tensor<float>(x{}) { indexing: attribute | summary }",
+ " }",
+ " rank-profile test {",
+ " function my_func(t) { expression: sum(t, x) + 1 }",
+ " function test_func_via_func_with_expr() { expression: call_func_with_expr( attribute(t1), attribute(t2) ) }",
+ " function call_func_with_expr(a, b) { expression: my_func( a * b ) }",
+ " summary-features { test_func_via_func_with_expr }",
+ " }",
+ "}"));
+ builder.build(true);
+ RankProfile profile = builder.getRankProfileRegistry().get(builder.getSchema(), "test");
+ assertEquals(TensorType.fromSpec("tensor<float>(y{})"),
+ summaryFeatures(profile).get("test_func_via_func_with_expr").type(profile.typeContext(builder.getQueryProfileRegistry())));
+ }
+
+ @Test
+ public void importedFieldsAreAvailable() throws Exception {
+ ApplicationBuilder builder = new ApplicationBuilder();
+ builder.addSchema(joinLines(
+ "search parent {",
+ " document parent {",
+ " field a type tensor(x[5],y[1000]) {",
+ " indexing: attribute",
+ " }",
+ " }",
+ "}"
+ ));
+ builder.addSchema(joinLines(
+ "search child {",
+ " document child { ",
+ " field ref type reference<parent> {",
+ "indexing: attribute | summary",
+ " }",
+ " }",
+ " import field ref.a as imported_a {}",
+ " rank-profile my_rank_profile {",
+ " first-phase {",
+ " expression: sum(attribute(imported_a))",
+ " }",
+ " }",
+ "}"
+ ));
+ builder.build(true);
+ }
+
+ @Test
+ public void undeclaredQueryFeaturesAreAccepted() throws Exception {
+ InspectableDeployLogger logger = new InspectableDeployLogger();
+ ApplicationBuilder builder = new ApplicationBuilder(logger);
+ builder.addSchema(joinLines(
+ "search test {",
+ " document test { ",
+ " field anyfield type double {" +
+ " indexing: attribute",
+ " }",
+ " }",
+ " rank-profile my_rank_profile {",
+ " first-phase {",
+ " expression: query(foo) + f() + sum(attribute(anyfield))",
+ " }",
+ " function f() {",
+ " expression: query(bar) + query(baz)",
+ " }",
+ " }",
+ "}"
+ ));
+ builder.build(true);
+ String message = logger.findMessage("The following query features");
+ assertNull(message);
+ }
+
+ @Test
+ public void undeclaredQueryFeaturesAreNotAcceptedWhenStrict() throws Exception {
+ try {
+ InspectableDeployLogger logger = new InspectableDeployLogger();
+ ApplicationBuilder builder = new ApplicationBuilder(logger);
+ builder.addSchema(joinLines(
+ "search test {",
+ " document test { ",
+ " field anyfield type double {" +
+ " indexing: attribute",
+ " }",
+ " }",
+ " rank-profile my_rank_profile {",
+ " strict: true" +
+ " first-phase {",
+ " expression: query(foo) + f() + sum(attribute(anyfield))",
+ " }",
+ " function f() {",
+ " expression: query(bar) + query(baz)",
+ " }",
+ " }",
+ "}"
+ ));
+ builder.build(true);
+ }
+ catch (IllegalArgumentException e) {
+ assertEquals("In schema 'test', rank profile 'my_rank_profile': rank profile 'my_rank_profile' is strict but is missing a query profile type declaration of features [query(bar), query(baz), query(foo)]",
+ Exceptions.toMessageString(e));
+ }
+ }
+
+ @Test
+ public void undeclaredQueryFeaturesAreAcceptedWithWarningWhenUsingTensors() throws Exception {
+ InspectableDeployLogger logger = new InspectableDeployLogger();
+ ApplicationBuilder builder = new ApplicationBuilder(logger);
+ builder.addSchema(joinLines(
+ "search test {",
+ " document test { ",
+ " field anyfield type tensor(d[2]) {",
+ " indexing: attribute",
+ " }",
+ " }",
+ " rank-profile my_rank_profile {",
+ " first-phase {",
+ " expression: query(foo) + f() + sum(attribute(anyfield))",
+ " }",
+ " function f() {",
+ " expression: query(bar) + query(baz)",
+ " }",
+ " }",
+ "}"
+ ));
+ builder.build(true);
+ String message = logger.findMessage("The following query features");
+ assertNotNull(message);
+ assertEquals("WARNING: The following query features used in rank profile 'my_rank_profile' are not declared in query profile types and " +
+ "will be interpreted as scalars, not tensors: [query(bar), query(baz), query(foo)]",
+ message);
+ }
+
+ @Test
+ public void noWarningWhenUsingTensorsWhenQueryFeaturesAreDeclared() throws Exception {
+ InspectableDeployLogger logger = new InspectableDeployLogger();
+ ApplicationBuilder builder = new ApplicationBuilder(logger);
+ QueryProfileType myType = new QueryProfileType("mytype");
+ myType.addField(new FieldDescription("rank.feature.query(foo)",
+ new TensorFieldType(TensorType.fromSpec("tensor(d[2])"))),
+ builder.getQueryProfileRegistry().getTypeRegistry());
+ myType.addField(new FieldDescription("rank.feature.query(bar)",
+ new TensorFieldType(TensorType.fromSpec("tensor(d[2])"))),
+ builder.getQueryProfileRegistry().getTypeRegistry());
+ myType.addField(new FieldDescription("rank.feature.query(baz)",
+ new TensorFieldType(TensorType.fromSpec("tensor(d[2])"))),
+ builder.getQueryProfileRegistry().getTypeRegistry());
+ builder.getQueryProfileRegistry().getTypeRegistry().register(myType);
+ builder.addSchema(joinLines(
+ "search test {",
+ " document test { ",
+ " field anyfield type tensor(d[2]) {",
+ " indexing: attribute",
+ " }",
+ " }",
+ " rank-profile my_rank_profile {",
+ " first-phase {",
+ " expression: sum(query(foo) + f() + sum(attribute(anyfield)))",
+ " }",
+ " function f() {",
+ " expression: query(bar) + query(baz)",
+ " }",
+ " }",
+ "}"
+ ));
+ builder.build(true);
+ String message = logger.findMessage("The following query features");
+ assertNull(message);
+ }
+
+ private Map<String, ReferenceNode> summaryFeatures(RankProfile profile) {
+ return profile.getSummaryFeatures().stream().collect(Collectors.toMap(f -> f.toString(), f -> f));
+ }
+
+ private static class InspectableDeployLogger implements DeployLogger {
+
+ private List<String> messages = new ArrayList<>();
+
+ @Override
+ public void log(Level level, String message) {
+ messages.add(level + ": " + message);
+ }
+
+ /** Returns the first message containing the given string, or null if none */
+ public String findMessage(String substring) {
+ return messages.stream().filter(message -> message.contains(substring)).findFirst().orElse(null);
+ }
+
+ }
+
+}
diff --git a/config-model/src/test/java/com/yahoo/schema/processing/RankingExpressionWithLightGBMTestCase.java b/config-model/src/test/java/com/yahoo/schema/processing/RankingExpressionWithLightGBMTestCase.java
new file mode 100644
index 00000000000..4df0a09ec2e
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/schema/processing/RankingExpressionWithLightGBMTestCase.java
@@ -0,0 +1,88 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.schema.processing;
+
+import com.yahoo.config.application.api.ApplicationPackage;
+import com.yahoo.io.IOUtils;
+import com.yahoo.path.Path;
+import com.yahoo.schema.parser.ParseException;
+import org.junit.After;
+import org.junit.Test;
+
+import java.io.IOException;
+
+/**
+ * @author lesters
+ */
+public class RankingExpressionWithLightGBMTestCase {
+
+ private final Path applicationDir = Path.fromString("src/test/integration/lightgbm/");
+
+ private final static String lightGBMExpression =
+ "if (!(numerical_2 >= 0.46643291586559305), 2.1594397038037663, if (categorical_2 in [\"k\", \"l\", \"m\"], 2.235297305276056, 2.1792953471546546)) + if (categorical_1 in [\"d\", \"e\"], 0.03070842919354316, if (!(numerical_1 >= 0.5102250691730842), -0.04439151147520909, 0.005117411709368601)) + if (!(numerical_2 >= 0.668665477622446), if (!(numerical_2 >= 0.008118820676863816), -0.15361238490967524, -0.01192330846157292), 0.03499044894987518) + if (!(numerical_1 >= 0.5201391072644542), -0.02141000620783247, if (categorical_1 in [\"a\", \"b\"], -0.004121485787596721, 0.04534090904886873)) + if (categorical_2 in [\"k\", \"l\", \"m\"], if (!(numerical_2 >= 0.27283279016959255), -0.01924803254356527, 0.03643772842347651), -0.02701711918923075)";
+
+ @After
+ public void removeGeneratedModelFiles() {
+ IOUtils.recursiveDeleteDir(applicationDir.append(ApplicationPackage.MODELS_GENERATED_DIR).toFile());
+ }
+
+ @Test
+ public void testLightGBMReference() {
+ RankProfileSearchFixture search = fixtureWith("lightgbm('regression.json')");
+ search.assertFirstPhaseExpression(lightGBMExpression, "my_profile");
+ }
+
+ @Test
+ public void testNestedLightGBMReference() {
+ RankProfileSearchFixture search = fixtureWith("5 + sum(lightgbm('regression.json'))");
+ search.assertFirstPhaseExpression("5 + reduce(" + lightGBMExpression + ", sum)", "my_profile");
+ }
+
+ @Test
+ public void testImportingFromStoredExpressions() throws IOException {
+ RankProfileSearchFixture search = fixtureWith("lightgbm('regression.json')");
+ search.assertFirstPhaseExpression(lightGBMExpression, "my_profile");
+
+ // At this point the expression is stored - copy application to another location which do not have a models dir
+ Path storedApplicationDirectory = applicationDir.getParentPath().append("copy");
+ try {
+ storedApplicationDirectory.toFile().mkdirs();
+ IOUtils.copyDirectory(applicationDir.append(ApplicationPackage.MODELS_GENERATED_DIR).toFile(),
+ storedApplicationDirectory.append(ApplicationPackage.MODELS_GENERATED_DIR).toFile());
+ RankingExpressionWithOnnxTestCase.StoringApplicationPackage storedApplication = new RankingExpressionWithOnnxTestCase.StoringApplicationPackage(storedApplicationDirectory);
+ RankProfileSearchFixture searchFromStored = fixtureWith("lightgbm('regression.json')");
+ searchFromStored.assertFirstPhaseExpression(lightGBMExpression, "my_profile");
+ }
+ finally {
+ IOUtils.recursiveDeleteDir(storedApplicationDirectory.toFile());
+ }
+ }
+
+ private RankProfileSearchFixture fixtureWith(String firstPhaseExpression) {
+ return fixtureWith(firstPhaseExpression, null, null,
+ new RankingExpressionWithOnnxTestCase.StoringApplicationPackage(applicationDir));
+ }
+
+ private RankProfileSearchFixture fixtureWith(String firstPhaseExpression,
+ String constant,
+ String field,
+ RankingExpressionWithOnnxTestCase.StoringApplicationPackage application) {
+ try {
+ RankProfileSearchFixture fixture = new RankProfileSearchFixture(
+ application,
+ application.getQueryProfiles(),
+ " rank-profile my_profile {\n" +
+ " first-phase {\n" +
+ " expression: " + firstPhaseExpression +
+ " }\n" +
+ " }",
+ constant,
+ field);
+ fixture.compileRankProfile("my_profile", applicationDir.append("models"));
+ return fixture;
+ } catch (ParseException e) {
+ throw new IllegalArgumentException(e);
+ }
+ }
+
+}
+
diff --git a/config-model/src/test/java/com/yahoo/schema/processing/RankingExpressionWithOnnxModelTestCase.java b/config-model/src/test/java/com/yahoo/schema/processing/RankingExpressionWithOnnxModelTestCase.java
new file mode 100644
index 00000000000..713e11fd608
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/schema/processing/RankingExpressionWithOnnxModelTestCase.java
@@ -0,0 +1,184 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.schema.processing;
+
+import com.yahoo.config.application.api.ApplicationPackage;
+import com.yahoo.config.model.application.provider.FilesApplicationPackage;
+import com.yahoo.config.model.deploy.DeployState;
+import com.yahoo.io.IOUtils;
+import com.yahoo.path.Path;
+import com.yahoo.vespa.config.search.RankProfilesConfig;
+import com.yahoo.vespa.config.search.core.OnnxModelsConfig;
+import com.yahoo.vespa.config.search.core.RankingConstantsConfig;
+import com.yahoo.vespa.model.VespaModel;
+import com.yahoo.vespa.model.search.DocumentDatabase;
+import com.yahoo.vespa.model.search.IndexedSearchCluster;
+import org.junit.After;
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+public class RankingExpressionWithOnnxModelTestCase {
+
+ private final Path applicationDir = Path.fromString("src/test/integration/onnx-model/");
+
+ @After
+ public void removeGeneratedModelFiles() {
+ IOUtils.recursiveDeleteDir(applicationDir.append(ApplicationPackage.MODELS_GENERATED_DIR).toFile());
+ }
+
+ @Test
+ public void testOnnxModelFeature() throws Exception {
+ VespaModel model = loadModel(applicationDir);
+ assertTransformedFeature(model);
+ assertGeneratedConfig(model);
+
+ Path storedApplicationDir = applicationDir.append("copy");
+ try {
+ storedApplicationDir.toFile().mkdirs();
+ IOUtils.copy(applicationDir.append("services.xml").toString(), storedApplicationDir.append("services.xml").toString());
+ IOUtils.copyDirectory(applicationDir.append("schemas").toFile(), storedApplicationDir.append("schemas").toFile());
+ IOUtils.copyDirectory(applicationDir.append(ApplicationPackage.MODELS_GENERATED_DIR).toFile(),
+ storedApplicationDir.append(ApplicationPackage.MODELS_GENERATED_DIR).toFile());
+
+ VespaModel storedModel = loadModel(storedApplicationDir);
+ assertTransformedFeature(storedModel);
+ assertGeneratedConfig(storedModel);
+ }
+ finally {
+ IOUtils.recursiveDeleteDir(storedApplicationDir.toFile());
+ }
+ }
+
+ private VespaModel loadModel(Path path) throws Exception {
+ FilesApplicationPackage applicationPackage = FilesApplicationPackage.fromFile(path.toFile());
+ DeployState state = new DeployState.Builder().applicationPackage(applicationPackage).build();
+ return new VespaModel(state);
+ }
+
+ private void assertGeneratedConfig(VespaModel vespaModel) {
+ DocumentDatabase db = ((IndexedSearchCluster)vespaModel.getSearchClusters().get(0)).getDocumentDbs().get(0);
+
+ RankingConstantsConfig.Builder rankingConstantsConfigBuilder = new RankingConstantsConfig.Builder();
+ db.getConfig(rankingConstantsConfigBuilder);
+ var rankingConstantsConfig = rankingConstantsConfigBuilder.build();
+ assertEquals(1, rankingConstantsConfig.constant().size());
+ assertEquals("my_constant", rankingConstantsConfig.constant(0).name());
+ assertEquals("tensor(d0[2])", rankingConstantsConfig.constant(0).type());
+ assertEquals("files/constant.json", rankingConstantsConfig.constant(0).fileref().value());
+
+ OnnxModelsConfig.Builder builder = new OnnxModelsConfig.Builder();
+ ((OnnxModelsConfig.Producer) db).getConfig(builder);
+ OnnxModelsConfig config = new OnnxModelsConfig(builder);
+ assertEquals(6, config.model().size());
+ for (OnnxModelsConfig.Model model : config.model()) {
+ assertTrue(model.dry_run_on_setup());
+ }
+
+ OnnxModelsConfig.Model model = config.model(0);
+ assertEquals("my_model", model.name());
+ assertEquals(3, model.input().size());
+ assertEquals("second/input:0", model.input(0).name());
+ assertEquals("constant(my_constant)", model.input(0).source());
+ assertEquals("first_input", model.input(1).name());
+ assertEquals("attribute(document_field)", model.input(1).source());
+ assertEquals("third_input", model.input(2).name());
+ assertEquals("rankingExpression(my_function)", model.input(2).source());
+ assertEquals(3, model.output().size());
+ assertEquals("path/to/output:0", model.output(0).name());
+ assertEquals("out", model.output(0).as());
+ assertEquals("path/to/output:1", model.output(1).name());
+ assertEquals("path_to_output_1", model.output(1).as());
+ assertEquals("path/to/output:2", model.output(2).name());
+ assertEquals("path_to_output_2", model.output(2).as());
+
+ model = config.model(1);
+ assertEquals("dynamic_model", model.name());
+ assertEquals(1, model.input().size());
+ assertEquals(1, model.output().size());
+ assertEquals("rankingExpression(my_function)", model.input(0).source());
+
+ model = config.model(2);
+ assertEquals("unbound_model", model.name());
+ assertEquals(1, model.input().size());
+ assertEquals(1, model.output().size());
+ assertEquals("rankingExpression(my_function)", model.input(0).source());
+
+ model = config.model(3);
+ assertEquals("files_model_onnx", model.name());
+ assertEquals(3, model.input().size());
+ assertEquals(3, model.output().size());
+ assertEquals("path/to/output:0", model.output(0).name());
+ assertEquals("path_to_output_0", model.output(0).as());
+ assertEquals("path/to/output:1", model.output(1).name());
+ assertEquals("path_to_output_1", model.output(1).as());
+ assertEquals("path/to/output:2", model.output(2).name());
+ assertEquals("path_to_output_2", model.output(2).as());
+ assertEquals("files_model_onnx", model.name());
+
+ model = config.model(4);
+ assertEquals("another_model", model.name());
+ assertEquals("third_input", model.input(2).name());
+ assertEquals("rankingExpression(another_function)", model.input(2).source());
+
+ model = config.model(5);
+ assertEquals("files_summary_model_onnx", model.name());
+ assertEquals(3, model.input().size());
+ assertEquals(3, model.output().size());
+ }
+
+ private void assertTransformedFeature(VespaModel model) {
+ DocumentDatabase db = ((IndexedSearchCluster)model.getSearchClusters().get(0)).getDocumentDbs().get(0);
+ RankProfilesConfig.Builder builder = new RankProfilesConfig.Builder();
+ ((RankProfilesConfig.Producer) db).getConfig(builder);
+ RankProfilesConfig config = new RankProfilesConfig(builder);
+ assertEquals(10, config.rankprofile().size());
+
+ assertEquals("test_model_config", config.rankprofile(2).name());
+ assertEquals("rankingExpression(my_function).rankingScript", config.rankprofile(2).fef().property(0).name());
+ assertEquals("vespa.rank.firstphase", config.rankprofile(2).fef().property(2).name());
+ assertEquals("rankingExpression(firstphase)", config.rankprofile(2).fef().property(2).value());
+ assertEquals("rankingExpression(firstphase).rankingScript", config.rankprofile(2).fef().property(3).name());
+ assertEquals("onnxModel(my_model).out{d0:1}", config.rankprofile(2).fef().property(3).value());
+
+ assertEquals("test_generated_model_config", config.rankprofile(3).name());
+ assertEquals("rankingExpression(my_function).rankingScript", config.rankprofile(3).fef().property(0).name());
+ assertEquals("rankingExpression(first_input).rankingScript", config.rankprofile(3).fef().property(2).name());
+ assertEquals("rankingExpression(second_input).rankingScript", config.rankprofile(3).fef().property(4).name());
+ assertEquals("rankingExpression(third_input).rankingScript", config.rankprofile(3).fef().property(6).name());
+ assertEquals("vespa.rank.firstphase", config.rankprofile(3).fef().property(8).name());
+ assertEquals("rankingExpression(firstphase)", config.rankprofile(3).fef().property(8).value());
+ assertEquals("rankingExpression(firstphase).rankingScript", config.rankprofile(3).fef().property(9).name());
+ assertEquals("onnxModel(files_model_onnx).path_to_output_1{d0:1}", config.rankprofile(3).fef().property(9).value());
+
+ assertEquals("test_summary_features", config.rankprofile(4).name());
+ assertEquals("rankingExpression(another_function).rankingScript", config.rankprofile(4).fef().property(0).name());
+ assertEquals("rankingExpression(firstphase).rankingScript", config.rankprofile(4).fef().property(3).name());
+ assertEquals("1", config.rankprofile(4).fef().property(3).value());
+ assertEquals("vespa.summary.feature", config.rankprofile(4).fef().property(4).name());
+ assertEquals("onnxModel(files_summary_model_onnx).path_to_output_2", config.rankprofile(4).fef().property(4).value());
+ assertEquals("vespa.summary.feature", config.rankprofile(4).fef().property(5).name());
+ assertEquals("onnxModel(another_model).out", config.rankprofile(4).fef().property(5).value());
+
+ assertEquals("test_dynamic_model", config.rankprofile(5).name());
+ assertEquals("rankingExpression(my_function).rankingScript", config.rankprofile(5).fef().property(0).name());
+ assertEquals("rankingExpression(firstphase).rankingScript", config.rankprofile(5).fef().property(3).name());
+ assertEquals("onnxModel(dynamic_model).my_output{d0:0, d1:1}", config.rankprofile(5).fef().property(3).value());
+
+ assertEquals("test_dynamic_model_2", config.rankprofile(6).name());
+ assertEquals("rankingExpression(firstphase).rankingScript", config.rankprofile(6).fef().property(5).name());
+ assertEquals("onnxModel(dynamic_model).my_output{d0:0, d1:2}", config.rankprofile(6).fef().property(5).value());
+
+ assertEquals("test_dynamic_model_with_transformer_tokens", config.rankprofile(7).name());
+ assertEquals("rankingExpression(my_function).rankingScript", config.rankprofile(7).fef().property(1).name());
+ assertEquals("tensor<float>(d0[1],d1[10])((if (d1 < 1.0 + rankingExpression(__token_length@-1993461420) + 1.0, 0.0, if (d1 < 1.0 + rankingExpression(__token_length@-1993461420) + 1.0 + rankingExpression(__token_length@-1993461420) + 1.0, 1.0, 0.0))))", config.rankprofile(7).fef().property(1).value());
+
+ assertEquals("test_unbound_model", config.rankprofile(8).name());
+ assertEquals("rankingExpression(my_function).rankingScript", config.rankprofile(8).fef().property(0).name());
+ assertEquals("rankingExpression(firstphase).rankingScript", config.rankprofile(8).fef().property(3).name());
+ assertEquals("onnxModel(unbound_model).my_output{d0:0, d1:1}", config.rankprofile(8).fef().property(3).value());
+
+
+ }
+
+}
diff --git a/config-model/src/test/java/com/yahoo/schema/processing/RankingExpressionWithOnnxTestCase.java b/config-model/src/test/java/com/yahoo/schema/processing/RankingExpressionWithOnnxTestCase.java
new file mode 100644
index 00000000000..94a51d25717
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/schema/processing/RankingExpressionWithOnnxTestCase.java
@@ -0,0 +1,417 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.schema.processing;
+
+import com.yahoo.config.application.api.ApplicationFile;
+import com.yahoo.config.application.api.ApplicationPackage;
+import com.yahoo.config.model.test.MockApplicationPackage;
+import com.yahoo.io.IOUtils;
+import com.yahoo.io.reader.NamedReader;
+import com.yahoo.path.Path;
+import com.yahoo.search.query.profile.QueryProfileRegistry;
+import com.yahoo.schema.FeatureNames;
+import com.yahoo.schema.parser.ParseException;
+import com.yahoo.tensor.TensorType;
+import com.yahoo.yolean.Exceptions;
+import org.junit.After;
+import org.junit.Test;
+
+import java.io.File;
+import java.io.FileReader;
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.fail;
+
+public class RankingExpressionWithOnnxTestCase {
+
+ private final Path applicationDir = Path.fromString("src/test/integration/onnx/");
+
+ private final static String name = "mnist_softmax";
+ private final static String vespaExpression = "join(reduce(join(rename(Placeholder, (d0, d1), (d0, d2)), constant(mnist_softmax_layer_Variable), f(a,b)(a * b)), sum, d2) * 1.0, constant(mnist_softmax_layer_Variable_1) * 1.0, f(a,b)(a + b))";
+
+ @After
+ public void removeGeneratedModelFiles() {
+ IOUtils.recursiveDeleteDir(applicationDir.append(ApplicationPackage.MODELS_GENERATED_DIR).toFile());
+ }
+
+ @Test
+ public void testOnnxReferenceWithConstantFeature() {
+ RankProfileSearchFixture search = fixtureWith("constant(mytensor)",
+ "onnx_vespa('mnist_softmax.onnx')",
+ "constant mytensor { file: ignored\ntype: tensor<float>(d0[1],d1[784]) }",
+ null);
+ search.assertFirstPhaseExpression(vespaExpression, "my_profile");
+ }
+
+ @Test
+ public void testOnnxReferenceWithQueryFeature() {
+ String queryProfile = "<query-profile id='default' type='root'/>";
+ String queryProfileType =
+ "<query-profile-type id='root'>" +
+ " <field name='query(mytensor)' type='tensor&lt;float&gt;(d0[1],d1[784])'/>" +
+ "</query-profile-type>";
+ StoringApplicationPackage application = new StoringApplicationPackage(applicationDir,
+ queryProfile,
+ queryProfileType);
+ RankProfileSearchFixture search = fixtureWith("query(mytensor)",
+ "onnx_vespa('mnist_softmax.onnx')",
+ null,
+ null,
+ "Placeholder",
+ application);
+ search.assertFirstPhaseExpression(vespaExpression, "my_profile");
+ }
+
+ @Test
+ public void testOnnxReferenceWithDocumentFeature() {
+ StoringApplicationPackage application = new StoringApplicationPackage(applicationDir);
+ RankProfileSearchFixture search = fixtureWith("attribute(mytensor)",
+ "onnx_vespa('mnist_softmax.onnx')",
+ null,
+ "field mytensor type tensor<float>(d0[1],d1[784]) { indexing: attribute }",
+ "Placeholder",
+ application);
+ search.assertFirstPhaseExpression(vespaExpression, "my_profile");
+ }
+
+
+ @Test
+ public void testOnnxReferenceWithFeatureCombination() {
+ String queryProfile = "<query-profile id='default' type='root'/>";
+ String queryProfileType =
+ "<query-profile-type id='root'>" +
+ " <field name='query(mytensor)' type='tensor&lt;float&gt;(d0[1],d1[784],d2[10])'/>" +
+ "</query-profile-type>";
+ StoringApplicationPackage application = new StoringApplicationPackage(applicationDir, queryProfile, queryProfileType);
+ RankProfileSearchFixture search = fixtureWith("sum(query(mytensor) * attribute(mytensor) * constant(mytensor),d2)",
+ "onnx_vespa('mnist_softmax.onnx')",
+ "constant mytensor { file: ignored\ntype: tensor<float>(d0[1],d1[784]) }",
+ "field mytensor type tensor<float>(d0[1],d1[784]) { indexing: attribute }",
+ "Placeholder",
+ application);
+ search.assertFirstPhaseExpression(vespaExpression, "my_profile");
+ }
+
+
+ @Test
+ public void testNestedOnnxReference() {
+ RankProfileSearchFixture search = fixtureWith("tensor<float>(d0[1],d1[784])(0.0)",
+ "5 + sum(onnx_vespa('mnist_softmax.onnx'))");
+ search.assertFirstPhaseExpression("5 + reduce(" + vespaExpression + ", sum)", "my_profile");
+ }
+
+ @Test
+ public void testOnnxReferenceWithSpecifiedOutput() {
+ RankProfileSearchFixture search = fixtureWith("tensor<float>(d0[1],d1[784])(0.0)",
+ "onnx_vespa('mnist_softmax.onnx', 'layer_add')");
+ search.assertFirstPhaseExpression(vespaExpression, "my_profile");
+ }
+
+ @Test
+ public void testOnnxReferenceWithSpecifiedOutputAndSignature() {
+ RankProfileSearchFixture search = fixtureWith("tensor<float>(d0[1],d1[784])(0.0)",
+ "onnx_vespa('mnist_softmax.onnx', 'default.layer_add')");
+ search.assertFirstPhaseExpression(vespaExpression, "my_profile");
+ }
+
+ @Test
+ public void testOnnxReferenceMissingFunction() throws ParseException {
+ try {
+ RankProfileSearchFixture search = new RankProfileSearchFixture(
+ new StoringApplicationPackage(applicationDir),
+ new QueryProfileRegistry(),
+ " rank-profile my_profile {\n" +
+ " first-phase {\n" +
+ " expression: onnx_vespa('mnist_softmax.onnx')" +
+ " }\n" +
+ " }");
+ search.compileRankProfile("my_profile", applicationDir.append("models"));
+ search.assertFirstPhaseExpression(vespaExpression, "my_profile");
+ fail("Expecting exception");
+ }
+ catch (IllegalArgumentException expected) {
+ assertEquals("Rank profile 'my_profile' is invalid: Could not use Onnx model from " +
+ "onnx_vespa(\"mnist_softmax.onnx\"): " +
+ "Model refers input 'Placeholder' of type tensor<float>(d0[1],d1[784]) but this function is " +
+ "not present in rank profile 'my_profile'",
+ Exceptions.toMessageString(expected));
+ }
+ }
+
+ @Test
+ public void testOnnxReferenceWithWrongFunctionType() {
+ try {
+ RankProfileSearchFixture search = fixtureWith("tensor(d0[1],d5[10])(0.0)",
+ "onnx_vespa('mnist_softmax.onnx')");
+ search.assertFirstPhaseExpression(vespaExpression, "my_profile");
+ fail("Expecting exception");
+ }
+ catch (IllegalArgumentException expected) {
+ assertEquals("Rank profile 'my_profile' is invalid: Could not use Onnx model from " +
+ "onnx_vespa(\"mnist_softmax.onnx\"): " +
+ "Model refers input 'Placeholder'. The required type of this is tensor<float>(d0[1],d1[784]), " +
+ "but this function returns tensor(d0[1],d5[10])",
+ Exceptions.toMessageString(expected));
+ }
+ }
+
+ @Test
+ public void testOnnxReferenceSpecifyingNonExistingOutput() {
+ try {
+ RankProfileSearchFixture search = fixtureWith("tensor<float>(d0[2],d1[784])(0.0)",
+ "onnx_vespa('mnist_softmax.onnx', 'y')");
+ search.assertFirstPhaseExpression(vespaExpression, "my_profile");
+ fail("Expecting exception");
+ }
+ catch (IllegalArgumentException expected) {
+ assertEquals("Rank profile 'my_profile' is invalid: Could not use Onnx model from " +
+ "onnx_vespa(\"mnist_softmax.onnx\",\"y\"): " +
+ "No expressions named 'y' in model 'mnist_softmax.onnx'. Available expressions: default.layer_add",
+ Exceptions.toMessageString(expected));
+ }
+ }
+
+ @Test
+ public void testImportingFromStoredExpressions() throws IOException {
+ RankProfileSearchFixture search = fixtureWith("tensor<float>(d0[1],d1[784])(0.0)",
+ "onnx_vespa(\"mnist_softmax.onnx\")");
+ search.assertFirstPhaseExpression(vespaExpression, "my_profile");
+
+ // At this point the expression is stored - copy application to another location which do not have a models dir
+ Path storedApplicationDirectory = applicationDir.getParentPath().append("copy");
+ try {
+ storedApplicationDirectory.toFile().mkdirs();
+ IOUtils.copyDirectory(applicationDir.append(ApplicationPackage.MODELS_GENERATED_DIR).toFile(),
+ storedApplicationDirectory.append(ApplicationPackage.MODELS_GENERATED_DIR).toFile());
+ StoringApplicationPackage storedApplication = new StoringApplicationPackage(storedApplicationDirectory);
+ RankProfileSearchFixture searchFromStored = fixtureWith("tensor<float>(d0[2],d1[784])(0.0)",
+ "onnx_vespa('mnist_softmax.onnx')",
+ null,
+ null,
+ "Placeholder",
+ storedApplication);
+ searchFromStored.assertFirstPhaseExpression(vespaExpression, "my_profile");
+ // Verify that the constants exists, but don't verify the content as we are not
+ // simulating file distribution in this test
+ }
+ finally {
+ IOUtils.recursiveDeleteDir(storedApplicationDirectory.toFile());
+ }
+ }
+
+ @Test
+ public void testImportingFromStoredExpressionsWithFunctionOverridingConstantAndInheritance() throws IOException {
+ String rankProfile =
+ " rank-profile my_profile {\n" +
+ " function Placeholder() {\n" +
+ " expression: tensor<float>(d0[1],d1[784])(0.0)\n" +
+ " }\n" +
+ " function " + name + "_layer_Variable() {\n" +
+ " expression: tensor<float>(d1[10],d2[784])(0.0)\n" +
+ " }\n" +
+ " first-phase {\n" +
+ " expression: onnx_vespa('mnist_softmax.onnx')" +
+ " }\n" +
+ " }" +
+ " rank-profile my_profile_child inherits my_profile {\n" +
+ " }";
+
+ String vespaExpressionWithoutConstant =
+ "join(reduce(join(rename(Placeholder, (d0, d1), (d0, d2)), " + name + "_layer_Variable, f(a,b)(a * b)), sum, d2) * 1.0, constant(" + name + "_layer_Variable_1) * 1.0, f(a,b)(a + b))";
+ RankProfileSearchFixture search = uncompiledFixtureWith(rankProfile, new StoringApplicationPackage(applicationDir));
+ search.compileRankProfile("my_profile", applicationDir.append("models"));
+ search.compileRankProfile("my_profile_child", applicationDir.append("models"));
+ search.assertFirstPhaseExpression(vespaExpressionWithoutConstant, "my_profile");
+ search.assertFirstPhaseExpression(vespaExpressionWithoutConstant, "my_profile_child");
+
+ assertNull("Constant overridden by function is not added",
+ search.search().constants().get(name + "_Variable"));
+
+ // At this point the expression is stored - copy application to another location which do not have a models dir
+ Path storedApplicationDirectory = applicationDir.getParentPath().append("copy");
+ try {
+ storedApplicationDirectory.toFile().mkdirs();
+ IOUtils.copyDirectory(applicationDir.append(ApplicationPackage.MODELS_GENERATED_DIR).toFile(),
+ storedApplicationDirectory.append(ApplicationPackage.MODELS_GENERATED_DIR).toFile());
+ StoringApplicationPackage storedApplication = new StoringApplicationPackage(storedApplicationDirectory);
+ RankProfileSearchFixture searchFromStored = uncompiledFixtureWith(rankProfile, storedApplication);
+ searchFromStored.compileRankProfile("my_profile", applicationDir.append("models"));
+ searchFromStored.compileRankProfile("my_profile_child", applicationDir.append("models"));
+ searchFromStored.assertFirstPhaseExpression(vespaExpressionWithoutConstant, "my_profile");
+ searchFromStored.assertFirstPhaseExpression(vespaExpressionWithoutConstant, "my_profile_child");
+ assertNull("Constant overridden by function is not added",
+ searchFromStored.search().constants().get(name + "_Variable"));
+ } finally {
+ IOUtils.recursiveDeleteDir(storedApplicationDirectory.toFile());
+ }
+ }
+
+ @Test
+ public void testFunctionGeneration() {
+ final String name = "small_constants_and_functions";
+ final String rankProfiles =
+ " rank-profile my_profile {\n" +
+ " function input() {\n" +
+ " expression: tensor<float>(d0[3])(0.0)\n" +
+ " }\n" +
+ " first-phase {\n" +
+ " expression: onnx_vespa('" + name + ".onnx')" +
+ " }\n" +
+ " }";
+ final String functionName = "imported_ml_function_" + name + "_exp_output";
+ final String expression = "join(" + functionName + ", reduce(join(join(reduce(" + functionName + ", sum, d0), tensor<float>(d0[1])(1.0), f(a,b)(a * b)), constant(" + name + "_epsilon), f(a,b)(a + b)), sum, d0), f(a,b)(a / b))";
+ final String functionExpression = "map(input, f(a)(exp(a)))";
+
+ RankProfileSearchFixture search = uncompiledFixtureWith(rankProfiles, new StoringApplicationPackage(applicationDir));
+ search.compileRankProfile("my_profile", applicationDir.append("models"));
+ search.assertFirstPhaseExpression(expression, "my_profile");
+ search.assertFunction(functionExpression, functionName, "my_profile");
+ }
+
+ @Test
+ public void testImportingFromStoredExpressionsWithSmallConstantsAndInheritance() throws IOException {
+ final String name = "small_constants_and_functions";
+ final String rankProfiles =
+ " rank-profile my_profile {\n" +
+ " function input() {\n" +
+ " expression: tensor<float>(d0[3])(0.0)\n" +
+ " }\n" +
+ " first-phase {\n" +
+ " expression: onnx_vespa('" + name + ".onnx')" +
+ " }\n" +
+ " }" +
+ " rank-profile my_profile_child inherits my_profile {\n" +
+ " }";
+ final String functionName = "imported_ml_function_" + name + "_exp_output";
+ final String expression = "join(" + functionName + ", reduce(join(join(reduce(" + functionName + ", sum, d0), tensor<float>(d0[1])(1.0), f(a,b)(a * b)), constant(" + name + "_epsilon), f(a,b)(a + b)), sum, d0), f(a,b)(a / b))";
+ final String functionExpression = "map(input, f(a)(exp(a)))";
+
+ RankProfileSearchFixture search = uncompiledFixtureWith(rankProfiles, new StoringApplicationPackage(applicationDir));
+ search.compileRankProfile("my_profile", applicationDir.append("models"));
+ search.compileRankProfile("my_profile_child", applicationDir.append("models"));
+ search.assertFirstPhaseExpression(expression, "my_profile");
+ search.assertFirstPhaseExpression(expression, "my_profile_child");
+ assertSmallConstant(name + "_epsilon", TensorType.fromSpec("tensor()"), search);
+ search.assertFunction(functionExpression, functionName, "my_profile");
+ search.assertFunction(functionExpression, functionName, "my_profile_child");
+
+ // At this point the expression is stored - copy application to another location which do not have a models dir
+ Path storedApplicationDirectory = applicationDir.getParentPath().append("copy");
+ try {
+ storedApplicationDirectory.toFile().mkdirs();
+ IOUtils.copyDirectory(applicationDir.append(ApplicationPackage.MODELS_GENERATED_DIR).toFile(),
+ storedApplicationDirectory.append(ApplicationPackage.MODELS_GENERATED_DIR).toFile());
+ StoringApplicationPackage storedApplication = new StoringApplicationPackage(storedApplicationDirectory);
+ RankProfileSearchFixture searchFromStored = uncompiledFixtureWith(rankProfiles, storedApplication);
+ searchFromStored.compileRankProfile("my_profile", applicationDir.append("models"));
+ searchFromStored.compileRankProfile("my_profile_child", applicationDir.append("models"));
+ searchFromStored.assertFirstPhaseExpression(expression, "my_profile");
+ searchFromStored.assertFirstPhaseExpression(expression, "my_profile_child");
+ assertSmallConstant(name + "_epsilon", TensorType.fromSpec("tensor()"), search);
+ searchFromStored.assertFunction(functionExpression, functionName, "my_profile");
+ searchFromStored.assertFunction(functionExpression, functionName, "my_profile_child");
+ }
+ finally {
+ IOUtils.recursiveDeleteDir(storedApplicationDirectory.toFile());
+ }
+ }
+
+ private void assertSmallConstant(String name, TensorType type, RankProfileSearchFixture search) {
+ var value = search.compiledRankProfile("my_profile").constants().get(FeatureNames.asConstantFeature(name));
+ assertNotNull(value);
+ assertEquals(type, value.type());
+ }
+
+ private RankProfileSearchFixture fixtureWith(String placeholderExpression, String firstPhaseExpression) {
+ return fixtureWith(placeholderExpression, firstPhaseExpression, null, null, "Placeholder",
+ new StoringApplicationPackage(applicationDir));
+ }
+
+ private RankProfileSearchFixture fixtureWith(String placeholderExpression, String firstPhaseExpression,
+ String constant, String field) {
+ return fixtureWith(placeholderExpression, firstPhaseExpression, constant, field, "Placeholder",
+ new StoringApplicationPackage(applicationDir));
+ }
+
+ private RankProfileSearchFixture uncompiledFixtureWith(String rankProfile, StoringApplicationPackage application) {
+ try {
+ return new RankProfileSearchFixture(application, application.getQueryProfiles(),
+ rankProfile, null, null);
+ }
+ catch (ParseException e) {
+ throw new IllegalArgumentException(e);
+ }
+ }
+
+ private RankProfileSearchFixture fixtureWith(String functionExpression,
+ String firstPhaseExpression,
+ String constant,
+ String field,
+ String functionName,
+ StoringApplicationPackage application) {
+ try {
+ RankProfileSearchFixture fixture = new RankProfileSearchFixture(
+ application,
+ application.getQueryProfiles(),
+ " rank-profile my_profile {\n" +
+ " function " + functionName + "() {\n" +
+ " expression: " + functionExpression +
+ " }\n" +
+ " first-phase {\n" +
+ " expression: " + firstPhaseExpression +
+ " }\n" +
+ " }",
+ constant,
+ field);
+ fixture.compileRankProfile("my_profile", applicationDir.append("models"));
+ return fixture;
+ }
+ catch (ParseException e) {
+ throw new IllegalArgumentException(e);
+ }
+ }
+
+ static class StoringApplicationPackage extends MockApplicationPackage {
+
+ StoringApplicationPackage(Path applicationPackageWritableRoot) {
+ this(applicationPackageWritableRoot, null, null);
+ }
+
+ StoringApplicationPackage(Path applicationPackageWritableRoot, String queryProfile, String queryProfileType) {
+ super(new File(applicationPackageWritableRoot.toString()),
+ null, null, List.of(), Map.of(), null,
+ null, null, false, queryProfile, queryProfileType);
+ }
+
+ @Override
+ public ApplicationFile getFile(Path file) {
+ return new MockApplicationFile(file, Path.fromString(root().toString()));
+ }
+
+ @Override
+ public List<NamedReader> getFiles(Path path, String suffix) {
+ File[] files = getFileReference(path).listFiles();
+ if (files == null) return List.of();
+ List<NamedReader> readers = new ArrayList<>();
+ for (File file : files) {
+ if ( ! file.getName().endsWith(suffix)) continue;
+ try {
+ readers.add(new NamedReader(file.getName(), new FileReader(file)));
+ }
+ catch (IOException e) {
+ throw new UncheckedIOException(e);
+ }
+ }
+ return readers;
+ }
+
+ }
+
+
+}
diff --git a/config-model/src/test/java/com/yahoo/schema/processing/RankingExpressionWithTensorTestCase.java b/config-model/src/test/java/com/yahoo/schema/processing/RankingExpressionWithTensorTestCase.java
new file mode 100644
index 00000000000..1f065bc7a20
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/schema/processing/RankingExpressionWithTensorTestCase.java
@@ -0,0 +1,202 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.schema.processing;
+
+import com.yahoo.schema.parser.ParseException;
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
+
+/**
+ * @author geirst
+ */
+public class RankingExpressionWithTensorTestCase {
+
+ @Test
+ public void requireThatSingleLineConstantMappedTensorCanBeParsed() throws ParseException {
+ RankProfileSearchFixture f = new RankProfileSearchFixture(
+ " rank-profile my_profile {\n" +
+ " first-phase {\n" +
+ " expression: sum(my_tensor)\n" +
+ " }\n" +
+ " constants {\n" +
+ " my_tensor tensor(x{},y{}):{ {x:1,y:2}:1, {x:2,y:1}:2 }\n" +
+ " }\n" +
+ " }");
+ f.compileRankProfile("my_profile");
+ f.assertFirstPhaseExpression("reduce(constant(my_tensor), sum)", "my_profile");
+ f.assertRankProperty("tensor(x{},y{}):{{x:1,y:2}:1.0, {x:2,y:1}:2.0}", "constant(my_tensor).value", "my_profile");
+ f.assertRankProperty("tensor(x{},y{})", "constant(my_tensor).type", "my_profile");
+ }
+
+ @Test
+ public void requireThatSingleLineConstantIndexedTensorCanBeParsed() throws ParseException {
+ RankProfileSearchFixture f = new RankProfileSearchFixture(
+ " rank-profile my_profile {\n" +
+ " first-phase {\n" +
+ " expression: sum(my_tensor)\n" +
+ " }\n" +
+ " constants {\n" +
+ " my_tensor tensor(x[3]):{ {x:0}:1, {x:1}:2, {x:2}:3 }\n" +
+ " }\n" +
+ " }");
+ f.compileRankProfile("my_profile");
+ f.assertFirstPhaseExpression("reduce(constant(my_tensor), sum)", "my_profile");
+ f.assertRankProperty("tensor(x[3]):[1.0, 2.0, 3.0]", "constant(my_tensor).value", "my_profile");
+ f.assertRankProperty("tensor(x[3])", "constant(my_tensor).type", "my_profile");
+ }
+
+ @Test
+ public void requireThatSingleLineConstantIndexedTensorShortFormCanBeParsed() throws ParseException {
+ RankProfileSearchFixture f = new RankProfileSearchFixture(
+ " rank-profile my_profile {\n" +
+ " first-phase {\n" +
+ " expression: sum(my_tensor)\n" +
+ " }\n" +
+ " constants {\n" +
+ " my_tensor tensor(x[3]):[1, 2, 3]\n" +
+ " }\n" +
+ " }");
+ f.compileRankProfile("my_profile");
+ f.assertFirstPhaseExpression("reduce(constant(my_tensor), sum)", "my_profile");
+ f.assertRankProperty("tensor(x[3]):[1.0, 2.0, 3.0]", "constant(my_tensor).value", "my_profile");
+ f.assertRankProperty("tensor(x[3])", "constant(my_tensor).type", "my_profile");
+ }
+
+ @Test
+ public void requireConstantTensorCanBeReferredViaConstantFeature() throws ParseException {
+ RankProfileSearchFixture f = new RankProfileSearchFixture(
+ " rank-profile my_profile {\n" +
+ " first-phase {\n" +
+ " expression: sum(constant(my_tensor))\n" +
+ " }\n" +
+ " constants {\n" +
+ " my_tensor tensor(x{},y{}):{{x:1,y:2}:1, {x:2,y:1}:2}\n" +
+ " }\n" +
+ " }");
+ f.compileRankProfile("my_profile");
+ f.assertFirstPhaseExpression("reduce(constant(my_tensor), sum)", "my_profile");
+ f.assertRankProperty("tensor(x{},y{}):{{x:1,y:2}:1.0, {x:2,y:1}:2.0}", "constant(my_tensor).value", "my_profile");
+ f.assertRankProperty("tensor(x{},y{})", "constant(my_tensor).type", "my_profile");
+ }
+
+ @Test
+ public void requireThatMultiLineConstantTensorAndTypeCanBeParsed() throws ParseException {
+ RankProfileSearchFixture f = new RankProfileSearchFixture(
+ " rank-profile my_profile {\n" +
+ " first-phase {\n" +
+ " expression: sum(my_tensor)\n" +
+ " }\n" +
+ " constants {\n" +
+ " my_tensor tensor(x{},y{}):\n" +
+ " { {x:1,y:2}:1,\n" +
+ " {x:2,y:1}:2 }\n" +
+ " }\n" +
+ " }");
+ f.compileRankProfile("my_profile");
+ f.assertFirstPhaseExpression("reduce(constant(my_tensor), sum)", "my_profile");
+ f.assertRankProperty("tensor(x{},y{}):{{x:1,y:2}:1.0, {x:2,y:1}:2.0}", "constant(my_tensor).value", "my_profile");
+ f.assertRankProperty("tensor(x{},y{})", "constant(my_tensor).type", "my_profile");
+ }
+
+ @Test
+ public void requireThatConstantTensorsCanBeUsedInSecondPhaseExpression() throws ParseException {
+ RankProfileSearchFixture f = new RankProfileSearchFixture(
+ " rank-profile my_profile {\n" +
+ " second-phase {\n" +
+ " expression: sum(my_tensor)\n" +
+ " }\n" +
+ " constants {\n" +
+ " my_tensor tensor(x{}):{ {x:1}:1 }\n" +
+ " }\n" +
+ " }");
+ f.compileRankProfile("my_profile");
+ f.assertSecondPhaseExpression("reduce(constant(my_tensor), sum)", "my_profile");
+ f.assertRankProperty("tensor(x{}):{1:1.0}", "constant(my_tensor).value", "my_profile");
+ f.assertRankProperty("tensor(x{})", "constant(my_tensor).type", "my_profile");
+ }
+
+ @Test
+ public void requireThatConstantTensorsCanBeUsedInInheritedRankProfile() throws ParseException {
+ RankProfileSearchFixture f = new RankProfileSearchFixture(
+ " rank-profile parent {\n" +
+ " constants {\n" +
+ " my_tensor {\n" +
+ " value: { {x:1}:1 }\n" +
+ " }\n" +
+ " }\n" +
+ " }\n" +
+ " rank-profile my_profile inherits parent {\n" +
+ " first-phase {\n" +
+ " expression: sum(my_tensor)\n" +
+ " }\n" +
+ " }");
+ f.compileRankProfile("my_profile");
+ f.assertFirstPhaseExpression("reduce(constant(my_tensor), sum)", "my_profile");
+ f.assertRankProperty("tensor(x{}):{1:1.0}", "constant(my_tensor).value", "my_profile");
+ f.assertRankProperty("tensor(x{})", "constant(my_tensor).type", "my_profile");
+ }
+
+ @Test
+ public void requireThatConstantTensorsCanBeUsedInFunction() throws ParseException {
+ RankProfileSearchFixture f = new RankProfileSearchFixture(
+ " rank-profile my_profile {\n" +
+ " function my_macro() {\n" +
+ " expression: sum(my_tensor)\n" +
+ " }\n" +
+ " first-phase {\n" +
+ " expression: 5.0 + my_macro\n" +
+ " }\n" +
+ " constants {\n" +
+ " my_tensor tensor(x{}):{ {x:1}:1 }\n" +
+ " }\n" +
+ " }");
+ f.compileRankProfile("my_profile");
+ f.assertFirstPhaseExpression("5.0 + my_macro", "my_profile");
+ f.assertFunction("reduce(constant(my_tensor), sum)", "my_macro", "my_profile");
+ f.assertRankProperty("tensor(x{}):{1:1.0}", "constant(my_tensor).value", "my_profile");
+ f.assertRankProperty("tensor(x{})", "constant(my_tensor).type", "my_profile");
+ }
+
+ @Test
+ public void requireThatCombinationOfConstantTensorsAndConstantValuesCanBeUsed() throws ParseException {
+ RankProfileSearchFixture f = new RankProfileSearchFixture(
+ " rank-profile my_profile {\n" +
+ " first-phase {\n" +
+ " expression: my_number_1 + sum(my_tensor) + my_number_2\n" +
+ " }\n" +
+ " constants {\n" +
+ " my_number_1 double: 3.0\n" +
+ " my_tensor tensor(x{}):{ {x:1}:1 }\n" +
+ " my_number_2 double: 5.0\n" +
+ " }\n" +
+ " }");
+ f.compileRankProfile("my_profile");
+ f.assertFirstPhaseExpression("3.0 + reduce(constant(my_tensor), sum) + 5.0", "my_profile");
+ f.assertRankProperty("tensor(x{}):{1:1.0}", "constant(my_tensor).value", "my_profile");
+ f.assertRankProperty("tensor(x{})", "constant(my_tensor).type", "my_profile");
+ }
+
+ @Test
+ public void requireThatInvalidTensorTypeSpecThrowsException() throws ParseException {
+ try {
+ RankProfileSearchFixture f = new RankProfileSearchFixture(
+ " rank-profile my_profile {\n" +
+ " constants {\n" +
+ " my_tensor tensor(x):{ {x:1}:1 }\n" +
+ " }\n" +
+ " }");
+ f.compileRankProfile("my_profile");
+ fail("Expected exception");
+ }
+ catch (IllegalArgumentException e) {
+ assertStartsWith("Type of constant(my_tensor): Illegal tensor type spec: A tensor type spec must be on the form",
+ e.getMessage());
+ }
+ }
+
+ private void assertStartsWith(String prefix, String string) {
+ assertEquals(prefix, string.substring(0, Math.min(prefix.length(), string.length())));
+ }
+
+}
diff --git a/config-model/src/test/java/com/yahoo/schema/processing/RankingExpressionWithTransformerTokensTestCase.java b/config-model/src/test/java/com/yahoo/schema/processing/RankingExpressionWithTransformerTokensTestCase.java
new file mode 100644
index 00000000000..f8086fb3bc6
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/schema/processing/RankingExpressionWithTransformerTokensTestCase.java
@@ -0,0 +1,98 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.schema.processing;
+
+import com.yahoo.config.model.application.provider.BaseDeployLogger;
+import com.yahoo.config.model.application.provider.MockFileRegistry;
+import com.yahoo.config.model.deploy.TestProperties;
+import com.yahoo.config.model.test.MockApplicationPackage;
+import com.yahoo.search.query.profile.QueryProfileRegistry;
+import com.yahoo.schema.RankProfile;
+import com.yahoo.schema.RankProfileRegistry;
+import com.yahoo.schema.Schema;
+import com.yahoo.schema.ApplicationBuilder;
+import com.yahoo.schema.expressiontransforms.RankProfileTransformContext;
+import com.yahoo.schema.expressiontransforms.TokenTransformer;
+import com.yahoo.schema.parser.ParseException;
+import com.yahoo.searchlib.rankingexpression.RankingExpression;
+import com.yahoo.searchlib.rankingexpression.evaluation.MapContext;
+import com.yahoo.searchlib.rankingexpression.evaluation.TensorValue;
+import com.yahoo.tensor.Tensor;
+import org.junit.Test;
+
+import java.util.Collections;
+
+import static org.junit.Assert.assertEquals;
+
+public class RankingExpressionWithTransformerTokensTestCase {
+
+ @Test
+ public void testTokenInputIds() throws Exception {
+ String expected = "tensor(d0[1],d1[12]):[101,1,2,102,3,4,5,102,6,7,102,0]";
+ String a = "tensor(d0[2]):[1,2]";
+ String b = "tensor(d0[3]):[3,4,5]";
+ String c = "tensor(d0[2]):[6,7]";
+ String expression = "tokenInputIds(12, a, b, c)";
+ Tensor result = evaluateExpression(expression, a, b, c);
+ assertEquals(Tensor.from(expected), result);
+ }
+
+ @Test
+ public void testTokenTypeIds() throws Exception {
+ String expected = "tensor(d0[1],d1[10]):[0,0,0,0,1,1,1,1,0,0]";
+ String a = "tensor(d0[2]):[1,2]";
+ String b = "tensor(d0[3]):[3,4,5]";
+ String expression = "tokenTypeIds(10, a, b)";
+ Tensor result = evaluateExpression(expression, a, b);
+ assertEquals(Tensor.from(expected), result);
+ }
+
+ @Test
+ public void testAttentionMask() throws Exception {
+ String expected = "tensor(d0[1],d1[10]):[1,1,1,1,1,1,1,1,0,0]";
+ String a = "tensor(d0[2]):[1,2]";
+ String b = "tensor(d0[3]):[3,4,5]";
+ String expression = "tokenAttentionMask(10, a, b)";
+ Tensor result = evaluateExpression(expression, a, b);
+ assertEquals(Tensor.from(expected), result);
+ }
+
+ private Tensor evaluateExpression(String expression, String a, String b) throws Exception {
+ return evaluateExpression(expression, a, b, null, null);
+ }
+
+ private Tensor evaluateExpression(String expression, String a, String b, String c) throws Exception {
+ return evaluateExpression(expression, a, b, c, null);
+ }
+
+ private Tensor evaluateExpression(String expression, String a, String b, String c, String d) throws Exception {
+ MapContext context = new MapContext();
+ if (a != null) context.put("a", new TensorValue(Tensor.from(a)));
+ if (b != null) context.put("b", new TensorValue(Tensor.from(b)));
+ if (c != null) context.put("c", new TensorValue(Tensor.from(c)));
+ if (d != null) context.put("d", new TensorValue(Tensor.from(d)));
+ var transformContext = createTransformContext();
+ var rankingExpression = new RankingExpression(expression);
+ var transformed = new TokenTransformer().transform(rankingExpression, transformContext);
+ for (var entry : transformContext.rankProfile().getFunctions().entrySet()) {
+ context.put(entry.getKey(), entry.getValue().function().getBody().evaluate(context).asDouble());
+ }
+ return transformed.evaluate(context).asTensor();
+ }
+
+ private RankProfileTransformContext createTransformContext() throws ParseException {
+ MockApplicationPackage application = (MockApplicationPackage) MockApplicationPackage.createEmpty();
+ RankProfileRegistry rankProfileRegistry = new RankProfileRegistry();
+ QueryProfileRegistry queryProfileRegistry = application.getQueryProfiles();
+ String sdContent = "search test {\n" +
+ " document test {}\n" +
+ " rank-profile my_profile inherits default {}\n" +
+ "}";
+ ApplicationBuilder schemaBuilder = new ApplicationBuilder(application, new MockFileRegistry(), new BaseDeployLogger(), new TestProperties(), rankProfileRegistry, queryProfileRegistry);
+ schemaBuilder.addSchema(sdContent);
+ schemaBuilder.build(true);
+ Schema schema = schemaBuilder.getSchema();
+ RankProfile rp = rankProfileRegistry.get(schema, "my_profile");
+ return new RankProfileTransformContext(rp, queryProfileRegistry, Collections.emptyMap(), null, Collections.emptyMap(), Collections.emptyMap());
+ }
+
+}
diff --git a/config-model/src/test/java/com/yahoo/schema/processing/RankingExpressionWithXGBoostTestCase.java b/config-model/src/test/java/com/yahoo/schema/processing/RankingExpressionWithXGBoostTestCase.java
new file mode 100644
index 00000000000..e1b1473a59a
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/schema/processing/RankingExpressionWithXGBoostTestCase.java
@@ -0,0 +1,90 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.schema.processing;
+
+import com.yahoo.config.application.api.ApplicationPackage;
+import com.yahoo.io.IOUtils;
+import com.yahoo.path.Path;
+import com.yahoo.schema.parser.ParseException;
+import org.junit.After;
+import org.junit.Test;
+
+import java.io.IOException;
+
+/**
+ * @author grace-lam
+ * @author bratseth
+ */
+public class RankingExpressionWithXGBoostTestCase {
+
+ private final Path applicationDir = Path.fromString("src/test/integration/xgboost/");
+
+ private final static String vespaExpression =
+ "if (f29 < -0.1234567, if (!(f56 >= -0.242398), 1.71218, -1.70044), if (f109 < 0.8723473, -1.94071, 1.85965)) + " +
+ "if (!(f60 >= -0.482947), if (f29 < -4.2387498, 0.784718, -0.96853), -6.23624)";
+
+ @After
+ public void removeGeneratedModelFiles() {
+ IOUtils.recursiveDeleteDir(applicationDir.append(ApplicationPackage.MODELS_GENERATED_DIR).toFile());
+ }
+
+ @Test
+ public void testXGBoostReference() {
+ RankProfileSearchFixture search = fixtureWith("xgboost('xgboost.2.2.json')");
+ search.assertFirstPhaseExpression(vespaExpression, "my_profile");
+ }
+
+ @Test
+ public void testNestedXGBoostReference() {
+ RankProfileSearchFixture search = fixtureWith("5 + sum(xgboost('xgboost.2.2.json'))");
+ search.assertFirstPhaseExpression("5 + reduce(" + vespaExpression + ", sum)", "my_profile");
+ }
+
+ @Test
+ public void testImportingFromStoredExpressions() throws IOException {
+ RankProfileSearchFixture search = fixtureWith("xgboost('xgboost.2.2.json')");
+ search.assertFirstPhaseExpression(vespaExpression, "my_profile");
+
+ // At this point the expression is stored - copy application to another location which do not have a models dir
+ Path storedApplicationDirectory = applicationDir.getParentPath().append("copy");
+ try {
+ storedApplicationDirectory.toFile().mkdirs();
+ IOUtils.copyDirectory(applicationDir.append(ApplicationPackage.MODELS_GENERATED_DIR).toFile(),
+ storedApplicationDirectory.append(ApplicationPackage.MODELS_GENERATED_DIR).toFile());
+ RankingExpressionWithOnnxTestCase.StoringApplicationPackage storedApplication = new RankingExpressionWithOnnxTestCase.StoringApplicationPackage(storedApplicationDirectory);
+ RankProfileSearchFixture searchFromStored = fixtureWith("xgboost('xgboost.2.2.json')");
+ searchFromStored.assertFirstPhaseExpression(vespaExpression, "my_profile");
+ }
+ finally {
+ IOUtils.recursiveDeleteDir(storedApplicationDirectory.toFile());
+ }
+ }
+
+ private RankProfileSearchFixture fixtureWith(String firstPhaseExpression) {
+ return fixtureWith(firstPhaseExpression, null, null,
+ new RankingExpressionWithOnnxTestCase.StoringApplicationPackage(applicationDir));
+ }
+
+ private RankProfileSearchFixture fixtureWith(String firstPhaseExpression,
+ String constant,
+ String field,
+ RankingExpressionWithOnnxTestCase.StoringApplicationPackage application) {
+ try {
+ RankProfileSearchFixture fixture = new RankProfileSearchFixture(
+ application,
+ application.getQueryProfiles(),
+ " rank-profile my_profile {\n" +
+ " first-phase {\n" +
+ " expression: " + firstPhaseExpression +
+ " }\n" +
+ " }",
+ constant,
+ field);
+ fixture.compileRankProfile("my_profile", applicationDir.append("models"));
+ return fixture;
+ } catch (ParseException e) {
+ throw new IllegalArgumentException(e);
+ }
+ }
+
+}
+
diff --git a/config-model/src/test/java/com/yahoo/schema/processing/RankingExpressionsTestCase.java b/config-model/src/test/java/com/yahoo/schema/processing/RankingExpressionsTestCase.java
new file mode 100644
index 00000000000..ace3788e49a
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/schema/processing/RankingExpressionsTestCase.java
@@ -0,0 +1,128 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.schema.processing;
+
+import com.yahoo.collections.Pair;
+import com.yahoo.config.model.api.ModelContext;
+import com.yahoo.config.model.application.provider.MockFileRegistry;
+import com.yahoo.config.model.deploy.TestProperties;
+import com.yahoo.search.query.profile.QueryProfileRegistry;
+import com.yahoo.schema.LargeRankExpressions;
+import com.yahoo.schema.RankProfile;
+import com.yahoo.schema.RankProfileRegistry;
+import com.yahoo.schema.Schema;
+import com.yahoo.schema.AbstractSchemaTestCase;
+import com.yahoo.schema.ApplicationBuilder;
+import com.yahoo.schema.derived.DerivedConfiguration;
+import com.yahoo.schema.derived.AttributeFields;
+import com.yahoo.schema.derived.RawRankProfile;
+import com.yahoo.schema.derived.TestableDeployLogger;
+import com.yahoo.schema.parser.ParseException;
+import ai.vespa.rankingexpression.importer.configmodelview.ImportedMlModels;
+import org.junit.Test;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+public class RankingExpressionsTestCase extends AbstractSchemaTestCase {
+
+ private static Schema createSearch(String dir, ModelContext.Properties deployProperties, RankProfileRegistry rankProfileRegistry) throws IOException, ParseException {
+ return ApplicationBuilder.createFromDirectory(dir, new MockFileRegistry(), new TestableDeployLogger(), deployProperties, rankProfileRegistry).getSchema();
+ }
+
+ @Test
+ public void testFunctions() throws IOException, ParseException {
+ ModelContext.Properties deployProperties = new TestProperties();
+ RankProfileRegistry rankProfileRegistry = new RankProfileRegistry();
+ Schema schema = createSearch("src/test/examples/rankingexpressionfunction", deployProperties, rankProfileRegistry);
+ RankProfile functionsRankProfile = rankProfileRegistry.get(schema, "macros");
+ Map<String, RankProfile.RankingExpressionFunction> functions = functionsRankProfile.getFunctions();
+ assertEquals(2, functions.get("titlematch$").function().arguments().size());
+ assertEquals("var1", functions.get("titlematch$").function().arguments().get(0));
+ assertEquals("var2", functions.get("titlematch$").function().arguments().get(1));
+ assertEquals("var1 * var2 + 890", functions.get("titlematch$").function().getBody().getRoot().toString());
+ assertEquals("0.8 + 0.2 * titlematch$(4,5) + 0.8 * titlematch$(7,8) * closeness(distance)",
+ functionsRankProfile.getFirstPhaseRanking().getRoot().toString());
+ assertEquals("78 + closeness(distance)",
+ functions.get("artistmatch").function().getBody().getRoot().toString());
+ assertEquals(0, functions.get("artistmatch").function().arguments().size());
+
+ RawRankProfile rawRankProfile = new RawRankProfile(functionsRankProfile, new LargeRankExpressions(new MockFileRegistry()), new QueryProfileRegistry(),
+ new ImportedMlModels(), new AttributeFields(schema), deployProperties);
+ List<Pair<String, String>> rankProperties = rawRankProfile.configProperties();
+ assertEquals(6, rankProperties.size());
+
+ assertEquals("rankingExpression(titlematch$).rankingScript", rankProperties.get(2).getFirst());
+ assertEquals("var1 * var2 + 890", rankProperties.get(2).getSecond());
+
+ assertEquals("rankingExpression(artistmatch).rankingScript", rankProperties.get(3).getFirst());
+ assertEquals("78 + closeness(distance)", rankProperties.get(3).getSecond());
+
+ assertEquals("rankingExpression(firstphase).rankingScript", rankProperties.get(5).getFirst());
+ assertEquals("0.8 + 0.2 * rankingExpression(titlematch$@126063073eb2deb.ab95cd69909927c) + 0.8 * rankingExpression(titlematch$@c7e4c2d0e6d9f2a1.1d4ed08e56cce2e6) * closeness(distance)", rankProperties.get(5).getSecond());
+
+ assertEquals("rankingExpression(titlematch$@c7e4c2d0e6d9f2a1.1d4ed08e56cce2e6).rankingScript", rankProperties.get(1).getFirst());
+ assertEquals("7 * 8 + 890", rankProperties.get(1).getSecond());
+
+ assertEquals("rankingExpression(titlematch$@126063073eb2deb.ab95cd69909927c).rankingScript", rankProperties.get(0).getFirst());
+ assertEquals("4 * 5 + 890", rankProperties.get(0).getSecond());
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testThatIncludingFileInSubdirFails() throws IOException, ParseException {
+ RankProfileRegistry registry = new RankProfileRegistry();
+ Schema schema = createSearch("src/test/examples/rankingexpressioninfile", new TestProperties(), registry);
+ new DerivedConfiguration(schema, registry); // rank profile parsing happens during deriving
+ }
+
+ private void verifyProfile(RankProfile profile, List<String> expectedFunctions, List<Pair<String, String>> rankProperties,
+ LargeRankExpressions largeExpressions, QueryProfileRegistry queryProfiles, ImportedMlModels models,
+ AttributeFields attributes, ModelContext.Properties properties) {
+ var functions = profile.getFunctions();
+ assertEquals(expectedFunctions.size(), functions.size());
+ for (String func : expectedFunctions) {
+ assertTrue(functions.containsKey(func));
+ }
+
+ RawRankProfile raw = new RawRankProfile(profile, largeExpressions, queryProfiles, models, attributes, properties);
+ assertEquals(rankProperties.size(), raw.configProperties().size());
+ for (int i = 0; i < rankProperties.size(); i++) {
+ assertEquals(rankProperties.get(i).getFirst(), raw.configProperties().get(i).getFirst());
+ assertEquals(rankProperties.get(i).getSecond(), raw.configProperties().get(i).getSecond());
+ }
+ }
+
+ private void verifySearch(Schema schema, RankProfileRegistry rankProfileRegistry, LargeRankExpressions largeExpressions,
+ QueryProfileRegistry queryProfiles, ImportedMlModels models, ModelContext.Properties properties)
+ {
+ AttributeFields attributes = new AttributeFields(schema);
+
+ verifyProfile(rankProfileRegistry.get(schema, "base"), Arrays.asList("large_f", "large_m"),
+ Arrays.asList(new Pair<>("rankingExpression(large_f).expressionName", "base.large_f"), new Pair<>("rankingExpression(large_m).expressionName", "base.large_m")),
+ largeExpressions, queryProfiles, models, attributes, properties);
+ for (String child : Arrays.asList("child_a", "child_b")) {
+ verifyProfile(rankProfileRegistry.get(schema, child), Arrays.asList("large_f", "large_m", "large_local_f", "large_local_m"),
+ Arrays.asList(new Pair<>("rankingExpression(large_f).expressionName", child + ".large_f"), new Pair<>("rankingExpression(large_m).expressionName", child + ".large_m"),
+ new Pair<>("rankingExpression(large_local_f).expressionName", child + ".large_local_f"), new Pair<>("rankingExpression(large_local_m).expressionName", child + ".large_local_m"),
+ new Pair<>("vespa.rank.firstphase", "rankingExpression(firstphase)"), new Pair<>("rankingExpression(firstphase).expressionName", child + ".firstphase")),
+ largeExpressions, queryProfiles, models, attributes, properties);
+ }
+ }
+
+ @Test
+ public void testLargeInheritedFunctions() throws IOException, ParseException {
+ ModelContext.Properties properties = new TestProperties();
+ RankProfileRegistry rankProfileRegistry = new RankProfileRegistry();
+ LargeRankExpressions largeExpressions = new LargeRankExpressions(new MockFileRegistry(), 50);
+ QueryProfileRegistry queryProfiles = new QueryProfileRegistry();
+ ImportedMlModels models = new ImportedMlModels();
+ Schema schema = createSearch("src/test/examples/largerankingexpressions", properties, rankProfileRegistry);
+ verifySearch(schema, rankProfileRegistry, largeExpressions, queryProfiles, models, properties);
+ // Need to verify that second derivation works as that will happen if same sd is used in multiple content clusters
+ verifySearch(schema, rankProfileRegistry, largeExpressions, queryProfiles, models, properties);
+ }
+}
diff --git a/config-model/src/test/java/com/yahoo/schema/processing/ReferenceFieldTestCase.java b/config-model/src/test/java/com/yahoo/schema/processing/ReferenceFieldTestCase.java
new file mode 100644
index 00000000000..57b4d928a52
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/schema/processing/ReferenceFieldTestCase.java
@@ -0,0 +1,92 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.schema.processing;
+
+import com.yahoo.config.model.deploy.TestProperties;
+import com.yahoo.document.DataType;
+import com.yahoo.document.Field;
+import com.yahoo.documentmodel.NewDocumentReferenceDataType;
+import com.yahoo.schema.Schema;
+import com.yahoo.schema.ApplicationBuilder;
+import com.yahoo.schema.document.SDDocumentType;
+import com.yahoo.schema.parser.ParseException;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * @author bjorncs
+ */
+public class ReferenceFieldTestCase {
+
+ @SuppressWarnings("deprecation")
+ @Rule
+ public final ExpectedException exceptionRule = ExpectedException.none();
+
+ @Test
+ public void reference_fields_are_parsed_from_search_definition() throws ParseException {
+ ApplicationBuilder builder = new ApplicationBuilder();
+ String campaignSdContent =
+ "schema campaign {\n" +
+ " document campaign {\n" +
+ " }\n" +
+ "}";
+ String salespersonSdContent =
+ "schema salesperson {\n" +
+ " document salesperson {\n" +
+ " }\n" +
+ "}";
+ String adSdContent =
+ "schema ad {\n" +
+ " document ad {\n" +
+ " field campaign_ref type reference<campaign> { indexing: attribute }\n" +
+ " field salesperson_ref type reference<salesperson> { indexing: attribute }\n" +
+ " }\n" +
+ "}";
+ builder.addSchema(campaignSdContent);
+ builder.addSchema(salespersonSdContent);
+ builder.addSchema(adSdContent);
+ builder.build(true);
+ Schema schema = builder.getSchema("ad");
+ assertSearchContainsReferenceField("campaign_ref", "campaign", schema.getDocument());
+ assertSearchContainsReferenceField("salesperson_ref", "salesperson", schema.getDocument());
+ }
+
+ @Test
+ public void cyclic_document_dependencies_are_detected() throws ParseException {
+ var builder = new ApplicationBuilder(new TestProperties());
+ String campaignSdContent =
+ "schema campaign {\n" +
+ " document campaign {\n" +
+ " field ad_ref type reference<ad> { indexing: attribute }\n" +
+ " }\n" +
+ "}";
+ String adSdContent =
+ "schema ad {\n" +
+ " document ad {\n" +
+ " field campaign_ref type reference<campaign> { indexing: attribute }\n" +
+ " }\n" +
+ "}";
+ builder.addSchema(campaignSdContent);
+ builder.addSchema(adSdContent);
+ exceptionRule.expect(IllegalArgumentException.class);
+ exceptionRule.expectMessage("reference cycle for documents");
+ builder.build(true);
+ }
+
+ private static void assertSearchContainsReferenceField(String expectedFieldname,
+ String referencedDocType,
+ SDDocumentType documentType) {
+ Field field = documentType.getDocumentType().getField(expectedFieldname);
+ assertNotNull("Field does not exist in document type: " + expectedFieldname, field);
+ DataType dataType = field.getDataType();
+ assertTrue(dataType instanceof NewDocumentReferenceDataType);
+ NewDocumentReferenceDataType refField = (NewDocumentReferenceDataType) dataType;
+ assertEquals(referencedDocType, refField.getTargetTypeName());
+ assertTrue(! refField.isTemporary());
+ }
+
+}
diff --git a/config-model/src/test/java/com/yahoo/schema/processing/ReservedDocumentNamesTestCase.java b/config-model/src/test/java/com/yahoo/schema/processing/ReservedDocumentNamesTestCase.java
new file mode 100644
index 00000000000..974d8c261ca
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/schema/processing/ReservedDocumentNamesTestCase.java
@@ -0,0 +1,27 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.schema.processing;
+
+import com.yahoo.schema.derived.AbstractExportingTestCase;
+import com.yahoo.schema.parser.ParseException;
+import org.junit.Test;
+
+import java.io.IOException;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
+
+/**
+ * @author Simon Thoresen Hult
+ */
+public class ReservedDocumentNamesTestCase extends AbstractExportingTestCase {
+
+ @Test
+ public void requireThatPositionIsAReservedDocumentName() throws IOException, ParseException {
+ try {
+ assertCorrectDeriving("reserved_position");
+ fail();
+ } catch (IllegalArgumentException e) {
+ assertEquals("For schema 'position': Document name 'position' is reserved.", e.getMessage());
+ }
+ }
+}
diff --git a/config-model/src/test/java/com/yahoo/schema/processing/ReservedRankingExpressionFunctionNamesTestCase.java b/config-model/src/test/java/com/yahoo/schema/processing/ReservedRankingExpressionFunctionNamesTestCase.java
new file mode 100644
index 00000000000..e405a105f3c
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/schema/processing/ReservedRankingExpressionFunctionNamesTestCase.java
@@ -0,0 +1,71 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.schema.processing;
+
+import com.yahoo.config.application.api.DeployLogger;
+import com.yahoo.schema.RankProfileRegistry;
+import com.yahoo.schema.ApplicationBuilder;
+import com.yahoo.schema.parser.ParseException;
+import org.junit.Test;
+
+import java.util.logging.Level;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * @author lesters
+ */
+public class ReservedRankingExpressionFunctionNamesTestCase {
+
+ @Test
+ public void requireThatFunctionsWithReservedNamesIssueAWarning() throws ParseException {
+ TestDeployLogger deployLogger = new TestDeployLogger();
+ RankProfileRegistry rankProfileRegistry = new RankProfileRegistry();
+ ApplicationBuilder builder = new ApplicationBuilder(deployLogger, rankProfileRegistry);
+ builder.addSchema(
+ "search test {\n" +
+ " document test { \n" +
+ " field a type string { \n" +
+ " indexing: index \n" +
+ " }\n" +
+ " }\n" +
+ " \n" +
+ " rank-profile test_rank_profile {\n" +
+ " function not_a_reserved_name(x) {\n" +
+ " expression: x + x\n" +
+ " }\n" +
+ " function sigmoid(x) {\n" +
+ " expression: x * x\n" +
+ " }\n" +
+ " first-phase {\n" +
+ " expression: sigmoid(2) + not_a_reserved_name(1)\n" +
+ " }\n" +
+ " }\n" +
+ " rank-profile test_rank_profile_2 inherits test_rank_profile {\n" +
+ " function sin(x) {\n" +
+ " expression: x * x\n" +
+ " }\n" +
+ " first-phase {\n" +
+ " expression: sigmoid(2) + sin(1)\n" +
+ " }\n" +
+ " }\n" +
+ "}\n");
+ builder.build(true);
+
+ assertTrue(deployLogger.log.contains("sigmoid") && deployLogger.log.contains("test_rank_profile"));
+ assertTrue(deployLogger.log.contains("sigmoid") && deployLogger.log.contains("test_rank_profile_2"));
+ assertTrue(deployLogger.log.contains("sin") && deployLogger.log.contains("test_rank_profile_2"));
+ assertFalse(deployLogger.log.contains("not_a_reserved_name") && deployLogger.log.contains("test_rank_profile"));
+ assertFalse(deployLogger.log.contains("not_a_reserved_name") && deployLogger.log.contains("test_rank_profile_2"));
+
+ }
+
+ public static class TestDeployLogger implements DeployLogger {
+ public String log = "";
+ @Override
+ public void log(Level level, String message) {
+ log += message;
+ }
+ }
+
+}
diff --git a/config-model/src/test/java/com/yahoo/schema/processing/SchemaMustHaveDocumentTest.java b/config-model/src/test/java/com/yahoo/schema/processing/SchemaMustHaveDocumentTest.java
new file mode 100644
index 00000000000..03f9d7c5960
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/schema/processing/SchemaMustHaveDocumentTest.java
@@ -0,0 +1,30 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.schema.processing;
+
+import com.yahoo.schema.ApplicationBuilder;
+import com.yahoo.schema.parser.ParseException;
+import org.junit.Test;
+
+import java.io.IOException;
+
+import static org.junit.Assert.fail;
+
+/**
+ * @author hmusum
+ */
+public class SchemaMustHaveDocumentTest {
+
+ @Test
+ public void requireErrorWhenMissingDocument() throws IOException, ParseException {
+ try {
+ ApplicationBuilder.buildFromFile("src/test/examples/invalid_sd_missing_document.sd");
+ fail("SD without document");
+ } catch (IllegalArgumentException e) {
+ if (!e.getMessage()
+ .contains("For schema 'imageconfig': A search specification must have an equally named document inside of it.")) {
+ throw e;
+ }
+ }
+ }
+
+}
diff --git a/config-model/src/test/java/com/yahoo/schema/processing/SummaryConsistencyTestCase.java b/config-model/src/test/java/com/yahoo/schema/processing/SummaryConsistencyTestCase.java
new file mode 100644
index 00000000000..76132a4d09f
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/schema/processing/SummaryConsistencyTestCase.java
@@ -0,0 +1,45 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.schema.processing;
+
+import com.yahoo.schema.Schema;
+import com.yahoo.schema.ApplicationBuilder;
+import com.yahoo.schema.parser.ParseException;
+import com.yahoo.vespa.documentmodel.SummaryTransform;
+import org.junit.Test;
+
+import static com.yahoo.config.model.test.TestUtil.joinLines;
+import static org.junit.Assert.assertEquals;
+
+public class SummaryConsistencyTestCase {
+
+ @Test
+ public void attribute_combiner_transform_is_set_when_source_is_array_of_struct_with_only_struct_field_attributes() throws ParseException {
+ String sd = joinLines(
+ "search structmemorysummary {",
+ " document structmemorysummary {",
+ " struct elem {",
+ " field name type string {}",
+ " field weight type int {}\n",
+ " }",
+ " field elem_array type array<elem> {",
+ " indexing: summary",
+ " struct-field name {",
+ " indexing: attribute",
+ " }",
+ " struct-field weight {",
+ " indexing: attribute",
+ " }",
+ " }",
+ " }",
+ " document-summary unfiltered {",
+ " summary elem_array_unfiltered type array<elem> {",
+ " source: elem_array",
+ " }",
+ " }",
+ "",
+ "}"
+ );
+ Schema schema = ApplicationBuilder.createFromString(sd).getSchema();
+ assertEquals(SummaryTransform.ATTRIBUTECOMBINER, schema.getSummaryField("elem_array_unfiltered").getTransform());
+ }
+}
diff --git a/config-model/src/test/java/com/yahoo/schema/processing/SummaryFieldsMustHaveValidSourceTestCase.java b/config-model/src/test/java/com/yahoo/schema/processing/SummaryFieldsMustHaveValidSourceTestCase.java
new file mode 100644
index 00000000000..d94815015d7
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/schema/processing/SummaryFieldsMustHaveValidSourceTestCase.java
@@ -0,0 +1,60 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.schema.processing;
+
+import com.yahoo.config.model.application.provider.BaseDeployLogger;
+
+import com.yahoo.schema.RankProfileRegistry;
+import com.yahoo.schema.Schema;
+import com.yahoo.schema.ApplicationBuilder;
+import com.yahoo.schema.AbstractSchemaTestCase;
+import com.yahoo.schema.parser.ParseException;
+import com.yahoo.vespa.model.container.search.QueryProfiles;
+import org.junit.Test;
+
+import java.io.IOException;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
+
+public class SummaryFieldsMustHaveValidSourceTestCase extends AbstractSchemaTestCase {
+
+ @Test
+ public void requireThatInvalidSourceIsCaught() throws IOException, ParseException {
+ try {
+ ApplicationBuilder.buildFromFile("src/test/examples/invalidsummarysource.sd");
+ fail("This should throw and never get here");
+ } catch (IllegalArgumentException e) {
+ assertEquals("For schema 'invalidsummarysource', summary class 'baz', summary field 'cox': there is no valid source 'nonexistingfield'.", e.getMessage());
+ }
+ }
+
+ @Test
+ public void requireThatInvalidImplicitSourceIsCaught() throws IOException, ParseException {
+ try {
+ ApplicationBuilder.buildFromFile("src/test/examples/invalidimplicitsummarysource.sd");
+ fail("This should throw and never get here");
+ } catch (IllegalArgumentException e) {
+ assertEquals("For schema 'invalidsummarysource', summary class 'baz', summary field 'cox': there is no valid source 'cox'.", e.getMessage());
+ }
+ }
+
+ @Test
+ public void requireThatInvalidSelfReferingSingleSource() throws IOException, ParseException {
+ try {
+ ApplicationBuilder.buildFromFile("src/test/examples/invalidselfreferringsummary.sd");
+ fail("This should throw and never get here");
+ } catch (IllegalArgumentException e) {
+ assertEquals("For schema 'invalidselfreferringsummary', summary class 'withid', summary field 'w': there is no valid source 'w'.", e.getMessage());
+ }
+ }
+
+ @Test
+ public void requireThatDocumentIdIsAllowedToPass() throws IOException, ParseException {
+ Schema schema = ApplicationBuilder.buildFromFile("src/test/examples/documentidinsummary.sd");
+ BaseDeployLogger deployLogger = new BaseDeployLogger();
+ RankProfileRegistry rankProfileRegistry = new RankProfileRegistry();
+ new SummaryFieldsMustHaveValidSource(schema, deployLogger, rankProfileRegistry, new QueryProfiles()).process(true, false);
+ assertEquals("documentid", schema.getSummary("withid").getSummaryField("w").getSingleSource());
+ }
+
+}
diff --git a/config-model/src/test/java/com/yahoo/schema/processing/TensorFieldTestCase.java b/config-model/src/test/java/com/yahoo/schema/processing/TensorFieldTestCase.java
new file mode 100644
index 00000000000..67c77508e3b
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/schema/processing/TensorFieldTestCase.java
@@ -0,0 +1,172 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.schema.processing;
+
+import com.yahoo.schema.document.Attribute;
+import com.yahoo.schema.parser.ParseException;
+import org.junit.Test;
+
+
+import static com.yahoo.schema.ApplicationBuilder.createFromString;
+import static com.yahoo.config.model.test.TestUtil.joinLines;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+/**
+ * @author geirst
+ */
+public class TensorFieldTestCase {
+
+ @Test
+ public void requireThatTensorFieldCannotBeOfCollectionType() throws ParseException {
+ try {
+ createFromString(getSd("field f1 type array<tensor(x{})> {}"));
+ fail("Expected exception");
+ }
+ catch (IllegalArgumentException e) {
+ assertEquals("For schema 'test', field 'f1': A field with collection type of tensor is not supported. Use simple type 'tensor' instead.",
+ e.getMessage());
+ }
+ }
+
+ @Test
+ public void requireThatTensorFieldCannotBeIndexField() throws ParseException {
+ try {
+ createFromString(getSd("field f1 type tensor(x{}) { indexing: index }"));
+ fail("Expected exception");
+ }
+ catch (IllegalArgumentException e) {
+ assertEquals("For schema 'test', field 'f1': A tensor of type 'tensor(x{})' does not support having an 'index'. " +
+ "Currently, only tensors with 1 indexed dimension supports that.",
+ e.getMessage());
+ }
+ }
+
+ @Test
+ public void requireThatIndexedTensorAttributeCannotBeFastSearch() throws ParseException {
+ try {
+ createFromString(getSd("field f1 type tensor(x[3]) { indexing: attribute \n attribute: fast-search }"));
+ fail("Expected exception");
+ }
+ catch (IllegalArgumentException e) {
+ assertEquals("For schema 'test', field 'f1': An attribute of type 'tensor' cannot be 'fast-search'.", e.getMessage());
+ }
+ }
+
+ @Test
+ public void requireThatIllegalTensorTypeSpecThrowsException() throws ParseException {
+ try {
+ createFromString(getSd("field f1 type tensor(invalid) { indexing: attribute }"));
+ fail("Expected exception");
+ }
+ catch (IllegalArgumentException e) {
+ assertStartsWith("Field type: Illegal tensor type spec:", e.getMessage());
+ }
+ }
+
+ @Test
+ public void hnsw_index_is_default_turned_off() throws ParseException {
+ var attr = getAttributeFromSd("field t1 type tensor(x[64]) { indexing: attribute }", "t1");
+ assertFalse(attr.hnswIndexParams().isPresent());
+ }
+
+ @Test
+ public void hnsw_index_gets_default_parameters_if_not_specified() throws ParseException {
+ assertHnswIndexParams("", 16, 200);
+ assertHnswIndexParams("index: hnsw", 16, 200);
+ }
+
+ @Test
+ public void hnsw_index_parameters_can_be_specified() throws ParseException {
+ assertHnswIndexParams("index { hnsw { max-links-per-node: 32 } }", 32, 200);
+ assertHnswIndexParams("index { hnsw { neighbors-to-explore-at-insert: 300 } }", 16, 300);
+ assertHnswIndexParams(joinLines("index {",
+ " hnsw {",
+ " max-links-per-node: 32",
+ " neighbors-to-explore-at-insert: 300",
+ " }",
+ "}"),
+ 32, 300);
+ }
+
+ @Test
+ public void tensor_with_hnsw_index_must_be_an_attribute() throws ParseException {
+ try {
+ createFromString(getSd("field t1 type tensor(x[64]) { indexing: index }"));
+ fail("Expected exception");
+ }
+ catch (IllegalArgumentException e) {
+ assertEquals("For schema 'test', field 't1': A tensor that has an index must also be an attribute.", e.getMessage());
+ }
+ }
+
+ @Test
+ public void tensor_with_hnsw_index_parameters_must_be_an_index() throws ParseException {
+ try {
+ createFromString(getSd(joinLines(
+ "field t1 type tensor(x[64]) {",
+ " indexing: attribute ",
+ " index {",
+ " hnsw { max-links-per-node: 32 }",
+ " }",
+ "}")));
+ fail("Expected exception");
+ }
+ catch (IllegalArgumentException e) {
+ assertEquals("For schema 'test', field 't1': " +
+ "A tensor that specifies hnsw index parameters must also specify 'index' in 'indexing'",
+ e.getMessage());
+ }
+ }
+
+ @Test
+ public void tensors_with_at_least_one_mapped_dimension_can_be_direct() throws ParseException {
+ assertTrue(getAttributeFromSd(
+ "field t1 type tensor(x{}) { indexing: attribute \n attribute: fast-search }", "t1").isFastSearch());
+ assertTrue(getAttributeFromSd(
+ "field t1 type tensor(x{},y{},z[4]) { indexing: attribute \n attribute: fast-search }", "t1").isFastSearch());
+ }
+
+ @Test
+ public void tensors_with_at_least_one_mapped_dimension_can_be_fast_rank() throws ParseException {
+ assertTrue(getAttributeFromSd(
+ "field t1 type tensor(x{}) { indexing: attribute \n attribute: fast-rank }", "t1").isFastRank());
+ assertTrue(getAttributeFromSd(
+ "field t1 type tensor(x{},y{},z[4]) { indexing: attribute \n attribute: fast-rank }", "t1").isFastRank());
+ }
+
+ private static String getSd(String field) {
+ return joinLines("search test {",
+ " document test {",
+ " " + field,
+ " }",
+ "}");
+ }
+
+ private Attribute getAttributeFromSd(String fieldSpec, String attrName) throws ParseException {
+ return createFromString(getSd(fieldSpec)).getSchema().getAttribute(attrName);
+ }
+
+ private void assertHnswIndexParams(String indexSpec, int maxLinksPerNode, int neighborsToExploreAtInsert) throws ParseException {
+ var sd = getSdWithIndexSpec(indexSpec);
+ var search = createFromString(sd).getSchema();
+ var attr = search.getAttribute("t1");
+ var params = attr.hnswIndexParams();
+ assertTrue(params.isPresent());
+ assertEquals(maxLinksPerNode, params.get().maxLinksPerNode());
+ assertEquals(neighborsToExploreAtInsert, params.get().neighborsToExploreAtInsert());
+ }
+
+ private String getSdWithIndexSpec(String indexSpec) {
+ return getSd(joinLines("field t1 type tensor(x[64]) {",
+ " indexing: attribute | index",
+ " " + indexSpec,
+ "}"));
+ }
+
+ private void assertStartsWith(String prefix, String string) {
+ assertEquals(prefix, string.substring(0, Math.min(prefix.length(), string.length())));
+ }
+
+}
diff --git a/config-model/src/test/java/com/yahoo/schema/processing/TensorTransformTestCase.java b/config-model/src/test/java/com/yahoo/schema/processing/TensorTransformTestCase.java
new file mode 100644
index 00000000000..aaf5f381c62
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/schema/processing/TensorTransformTestCase.java
@@ -0,0 +1,234 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.schema.processing;
+
+import com.yahoo.collections.Pair;
+import com.yahoo.component.ComponentId;
+import com.yahoo.config.model.application.provider.MockFileRegistry;
+import com.yahoo.config.model.deploy.TestProperties;
+import com.yahoo.search.query.profile.QueryProfileRegistry;
+import com.yahoo.search.query.profile.types.FieldDescription;
+import com.yahoo.search.query.profile.types.FieldType;
+import com.yahoo.search.query.profile.types.QueryProfileType;
+import com.yahoo.search.query.profile.types.QueryProfileTypeRegistry;
+import com.yahoo.schema.LargeRankExpressions;
+import com.yahoo.schema.RankProfile;
+import com.yahoo.schema.RankProfileRegistry;
+import com.yahoo.schema.Schema;
+import com.yahoo.schema.ApplicationBuilder;
+import com.yahoo.schema.AbstractSchemaTestCase;
+import com.yahoo.schema.derived.AttributeFields;
+import com.yahoo.schema.derived.RawRankProfile;
+import com.yahoo.schema.parser.ParseException;
+import ai.vespa.rankingexpression.importer.configmodelview.ImportedMlModels;
+import org.junit.Test;
+
+import java.util.List;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
+
+public class TensorTransformTestCase extends AbstractSchemaTestCase {
+
+ @Test
+ public void requireThatNormalMaxAndMinAreNotReplaced() throws ParseException {
+ assertTransformedExpression("max(1.0,2.0)",
+ "max(1.0,2.0)");
+ assertTransformedExpression("min(attribute(double_field),x)",
+ "min(attribute(double_field),x)");
+ assertTransformedExpression("max(attribute(double_field),attribute(double_array_field))",
+ "max(attribute(double_field),attribute(double_array_field))");
+ assertTransformedExpression("min(attribute(tensor_field_1),attribute(double_field))",
+ "min(attribute(tensor_field_1),attribute(double_field))");
+ assertTransformedExpression("reduce(max(attribute(tensor_field_1),attribute(tensor_field_2)),sum)",
+ "reduce(max(attribute(tensor_field_1),attribute(tensor_field_2)),sum)");
+ assertTransformedExpression("min(constant(test_constant_tensor),1.0)",
+ "min(test_constant_tensor,1.0)");
+ assertTransformedExpression("max(constant(base_constant_tensor),1.0)",
+ "max(base_constant_tensor,1.0)");
+ assertTransformedExpression("min(constant(file_constant_tensor),1.0)",
+ "min(constant(file_constant_tensor),1.0)");
+ assertTransformedExpression("max(query(q),1.0)",
+ "max(query(q),1.0)");
+ assertTransformedExpression("max(query(n),1.0)",
+ "max(query(n),1.0)");
+ }
+
+ @Test
+ public void requireThatMaxAndMinWithTensorAttributesAreReplaced() throws ParseException {
+ assertTransformedExpression("reduce(attribute(tensor_field_1),max,x)",
+ "max(attribute(tensor_field_1),x)");
+ assertTransformedExpression("1+reduce(attribute(tensor_field_1),max,x)",
+ "1 + max(attribute(tensor_field_1),x)");
+ assertTransformedExpression("if(attribute(double_field),1+reduce(attribute(tensor_field_1),max,x),reduce(attribute(tensor_field_1),sum,x))",
+ "if(attribute(double_field),1 + max(attribute(tensor_field_1),x),reduce(attribute(tensor_field_1), sum, x))");
+ assertTransformedExpression("reduce(max(attribute(tensor_field_1),attribute(tensor_field_2)),max,x)",
+ "max(max(attribute(tensor_field_1),attribute(tensor_field_2)),x)");
+ assertTransformedExpression("reduce(if(attribute(double_field),attribute(tensor_field_2),attribute(tensor_field_2)),max,x)",
+ "max(if(attribute(double_field),attribute(tensor_field_2),attribute(tensor_field_2)),x)");
+ assertTransformedExpression("max(reduce(attribute(tensor_field_1),max,x),x)",
+ "max(max(attribute(tensor_field_1),x),x)"); // will result in deploy error.
+ assertTransformedExpression("reduce(reduce(attribute(tensor_field_2),max,x),max,y)",
+ "max(max(attribute(tensor_field_2),x),y)");
+ }
+
+ @Test
+ public void requireThatMaxAndMinWithConstantTensorsAreReplaced() throws ParseException {
+ assertTransformedExpression("reduce(constant(test_constant_tensor),max,x)",
+ "max(test_constant_tensor,x)");
+ assertTransformedExpression("reduce(constant(base_constant_tensor),max,x)",
+ "max(base_constant_tensor,x)");
+ assertTransformedExpression("reduce(constant(file_constant_tensor),min,x)",
+ "min(constant(file_constant_tensor),x)");
+ }
+
+ @Test
+ public void requireThatMaxAndMinWithTensorExpressionsAreReplaced() throws ParseException {
+ assertTransformedExpression("reduce(attribute(double_field)+attribute(tensor_field_1),min,x)",
+ "min(attribute(double_field) + attribute(tensor_field_1),x)");
+ assertTransformedExpression("reduce(attribute(tensor_field_1)*attribute(tensor_field_2),min,x)",
+ "min(attribute(tensor_field_1) * attribute(tensor_field_2),x)");
+ assertTransformedExpression("reduce(join(attribute(tensor_field_1),attribute(tensor_field_2),f(x,y)(x*y)),min,x)",
+ "min(join(attribute(tensor_field_1),attribute(tensor_field_2),f(x,y)(x*y)),x)");
+ assertTransformedExpression("min(join(tensor_field_1,tensor_field_2,f(x,y)(x*y)),x)",
+ "min(join(tensor_field_1,tensor_field_2,f(x,y)(x*y)),x)"); // because tensor fields are not in attribute(...)
+ assertTransformedExpression("reduce(join(attribute(tensor_field_1),backend_rank_feature,f(x,y)(x*y)),min,x)",
+ "min(join(attribute(tensor_field_1),backend_rank_feature,f(x,y)(x*y)),x)");
+ }
+
+ @Test
+ public void requireThatMaxAndMinWithTensorFromIsReplaced() throws ParseException {
+ assertTransformedExpression("reduce(tensorFromLabels(attribute(double_array_field)),max,double_array_field)",
+ "max(tensorFromLabels(attribute(double_array_field)),double_array_field)");
+ assertTransformedExpression("reduce(tensorFromLabels(attribute(double_array_field),x),max,x)",
+ "max(tensorFromLabels(attribute(double_array_field),x),x)");
+ assertTransformedExpression("reduce(tensorFromWeightedSet(attribute(weightedset_field)),max,weightedset_field)",
+ "max(tensorFromWeightedSet(attribute(weightedset_field)),weightedset_field)");
+ assertTransformedExpression("reduce(tensorFromWeightedSet(attribute(weightedset_field),x),max,x)",
+ "max(tensorFromWeightedSet(attribute(weightedset_field),x),x)");
+ }
+
+ @Test
+ public void requireThatMaxAndMinWithTensorInQueryIsReplaced() throws ParseException {
+ assertTransformedExpression("reduce(query(q),max,x)", "max(query(q),x)");
+ assertTransformedExpression("max(query(n),x)", "max(query(n),x)");
+ }
+
+ @Test
+ public void requireThatMaxAndMinWithTensorsReturnedFromFunctionsAreReplaced() throws ParseException {
+ assertTransformedExpression("reduce(rankingExpression(returns_tensor),max,x)",
+ "max(returns_tensor,x)");
+ assertTransformedExpression("reduce(rankingExpression(wraps_returns_tensor),max,x)",
+ "max(wraps_returns_tensor,x)");
+ assertTransformedExpression("reduce(rankingExpression(tensor_inheriting),max,x)",
+ "max(tensor_inheriting,x)");
+ assertTransformedExpression("reduce(rankingExpression(returns_tensor_with_arg@),max,x)",
+ "max(returns_tensor_with_arg(attribute(tensor_field_1)),x)");
+ }
+
+ private void assertTransformedExpression(String expected, String original) throws ParseException {
+ for (Pair<String, String> rankPropertyExpression : buildSearch(original)) {
+ String rankProperty = rankPropertyExpression.getFirst();
+ if (rankProperty.equals("rankingExpression(testexpression).rankingScript")) {
+ String rankExpression = censorBindingHash(rankPropertyExpression.getSecond().replace(" ",""));
+ assertEquals(expected, rankExpression);
+ return;
+ }
+ }
+ fail("No 'rankingExpression(testexpression).rankingScript' property produced");
+ }
+
+ private List<Pair<String, String>> buildSearch(String expression) throws ParseException {
+ RankProfileRegistry rankProfileRegistry = new RankProfileRegistry();
+ QueryProfileRegistry queryProfiles = setupQueryProfileTypes();
+ ApplicationBuilder builder = new ApplicationBuilder(rankProfileRegistry, queryProfiles);
+ builder.addSchema(
+ "search test {\n" +
+ " document test { \n" +
+ " field double_field type double { \n" +
+ " indexing: summary | attribute \n" +
+ " }\n" +
+ " field double_array_field type array<double> { \n" +
+ " indexing: summary | attribute \n" +
+ " }\n" +
+ " field weightedset_field type weightedset<int> { \n" +
+ " indexing: summary | attribute \n" +
+ " }\n" +
+ " field tensor_field_1 type tensor(x{}) { \n" +
+ " indexing: summary | attribute \n" +
+ " }\n" +
+ " field tensor_field_2 type tensor(x[3],y[3]) { \n" +
+ " indexing: summary | attribute \n" +
+ " }\n" +
+ " }\n" +
+ " constant file_constant_tensor {\n" +
+ " file: constants/tensor.json\n" +
+ " type: tensor(x{})\n" +
+ " }\n" +
+ " rank-profile base {\n" +
+ " constants {\n" +
+ " base_constant_tensor tensor(x[1]):[0]\n"+
+ " }\n" +
+ " function base_tensor() {\n" +
+ " expression: constant(base_constant_tensor)\n" +
+ " }\n" +
+ " }\n" +
+ " rank-profile test inherits base {\n" +
+ " constants {\n" +
+ " test_constant_tensor tensor(x[1]):[1]" +
+ " }\n" +
+ " function returns_tensor_with_arg(arg1) {\n" +
+ " expression: 2.0 * arg1\n" +
+ " }\n" +
+ " function wraps_returns_tensor() {\n" +
+ " expression: returns_tensor\n" +
+ " }\n" +
+ " function returns_tensor() {\n" +
+ " expression: attribute(tensor_field_2)\n" +
+ " }\n" +
+ " function tensor_inheriting() {\n" +
+ " expression: base_tensor\n" +
+ " }\n" +
+ " function testexpression() {\n" +
+ " expression: " + expression + "\n" +
+ " }\n" +
+ " }\n" +
+ "}\n");
+ builder.build(true);
+ Schema s = builder.getSchema();
+ RankProfile test = rankProfileRegistry.get(s, "test").compile(queryProfiles, new ImportedMlModels());
+ List<Pair<String, String>> testRankProperties = new RawRankProfile(test,
+ new LargeRankExpressions(new MockFileRegistry()),
+ queryProfiles,
+ new ImportedMlModels(),
+ new AttributeFields(s), new TestProperties()).configProperties();
+ return testRankProperties;
+ }
+
+ private static QueryProfileRegistry setupQueryProfileTypes() {
+ QueryProfileRegistry registry = new QueryProfileRegistry();
+ QueryProfileTypeRegistry typeRegistry = registry.getTypeRegistry();
+ QueryProfileType type = new QueryProfileType(new ComponentId("testtype"));
+ type.addField(new FieldDescription("ranking.features.query(q)",
+ FieldType.fromString("tensor(x{})", typeRegistry)), typeRegistry);
+ type.addField(new FieldDescription("ranking.features.query(n)",
+ FieldType.fromString("integer", typeRegistry)), typeRegistry);
+ typeRegistry.register(type);
+ return registry;
+ }
+
+ private String censorBindingHash(String s) {
+ StringBuilder b = new StringBuilder();
+ boolean areInHash = false;
+ for (int i = 0; i < s.length() ; i++) {
+ char current = s.charAt(i);
+ if ( ! Character.isLetterOrDigit(current)) // end of hash
+ areInHash = false;
+ if ( ! areInHash)
+ b.append(current);
+ if (current == '@') // start of hash
+ areInHash = true;
+ }
+ return b.toString();
+ }
+
+}
diff --git a/config-model/src/test/java/com/yahoo/schema/processing/ValidateFieldTypesTest.java b/config-model/src/test/java/com/yahoo/schema/processing/ValidateFieldTypesTest.java
new file mode 100644
index 00000000000..87bb2e96042
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/schema/processing/ValidateFieldTypesTest.java
@@ -0,0 +1,80 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.schema.processing;
+
+import com.yahoo.config.model.application.provider.MockFileRegistry;
+import com.yahoo.config.model.deploy.TestProperties;
+import com.yahoo.config.model.test.MockApplicationPackage;
+import com.yahoo.document.DataType;
+import com.yahoo.document.Field;
+import com.yahoo.schema.DocumentReference;
+import com.yahoo.schema.Schema;
+import com.yahoo.schema.derived.TestableDeployLogger;
+import com.yahoo.schema.document.ImportedField;
+import com.yahoo.schema.document.ImportedFields;
+import com.yahoo.schema.document.ImportedSimpleField;
+import com.yahoo.schema.document.SDDocumentType;
+import com.yahoo.schema.document.SDField;
+import com.yahoo.vespa.documentmodel.DocumentSummary;
+import com.yahoo.vespa.documentmodel.SummaryField;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+
+import java.util.Collections;
+
+/**
+ * @author bjorncs
+ */
+public class ValidateFieldTypesTest {
+
+ private static final String IMPORTED_FIELD_NAME = "imported_myfield";
+ private static final String DOCUMENT_NAME = "my_doc";
+
+ @SuppressWarnings("deprecation")
+ @Rule
+ public final ExpectedException exceptionRule = ExpectedException.none();
+
+ @Test
+ public void throws_exception_if_type_of_document_field_does_not_match_summary_field() {
+ Schema schema = createSearchWithDocument(DOCUMENT_NAME);
+ schema.setImportedFields(createSingleImportedField(IMPORTED_FIELD_NAME, DataType.INT));
+ schema.addSummary(createDocumentSummary(IMPORTED_FIELD_NAME, DataType.STRING, schema));
+
+ ValidateFieldTypes validator = new ValidateFieldTypes(schema, null, null, null);
+ exceptionRule.expect(IllegalArgumentException.class);
+ exceptionRule.expectMessage(
+ "For schema '" + DOCUMENT_NAME + "', field '" + IMPORTED_FIELD_NAME + "': Incompatible types. " +
+ "Expected int for summary field '" + IMPORTED_FIELD_NAME + "', got string.");
+ validator.process(true, false);
+ }
+
+ private static Schema createSearch(String documentType) {
+ return new Schema(documentType,
+ MockApplicationPackage.createEmpty(),
+ new MockFileRegistry(),
+ new TestableDeployLogger(),
+ new TestProperties());
+ }
+
+ private static Schema createSearchWithDocument(String documentName) {
+ Schema schema = createSearch(documentName);
+ SDDocumentType document = new SDDocumentType(documentName, schema);
+ schema.addDocument(document);
+ return schema;
+ }
+
+ private static ImportedFields createSingleImportedField(String fieldName, DataType dataType) {
+ Schema targetSchema = createSearchWithDocument("target_doc");
+ SDField targetField = new SDField(targetSchema.getDocument(), "target_field", dataType);
+ DocumentReference documentReference = new DocumentReference(new Field("reference_field"), targetSchema);
+ ImportedField importedField = new ImportedSimpleField(fieldName, documentReference, targetField);
+ return new ImportedFields(Collections.singletonMap(fieldName, importedField));
+ }
+
+ private static DocumentSummary createDocumentSummary(String fieldName, DataType dataType, Schema schema) {
+ DocumentSummary summary = new DocumentSummary("mysummary", schema);
+ summary.add(new SummaryField(fieldName, dataType));
+ return summary;
+ }
+
+}
diff --git a/config-model/src/test/java/com/yahoo/schema/processing/VespaMlModelTestCase.java b/config-model/src/test/java/com/yahoo/schema/processing/VespaMlModelTestCase.java
new file mode 100644
index 00000000000..016e30e80af
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/schema/processing/VespaMlModelTestCase.java
@@ -0,0 +1,77 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.schema.processing;
+
+import com.yahoo.config.application.api.ApplicationPackage;
+import com.yahoo.io.IOUtils;
+import com.yahoo.path.Path;
+import com.yahoo.schema.derived.RawRankProfile;
+import com.yahoo.vespa.model.VespaModel;
+import com.yahoo.vespa.model.ml.ImportedModelTester;
+import org.junit.After;
+import org.junit.Test;
+
+import java.io.IOException;
+import java.util.Optional;
+
+import static org.junit.Assert.assertEquals;
+
+/**
+ * Tests adding Vespa ranking expression based models in the models/ dir
+ *
+ * @author bratseth
+ */
+public class VespaMlModelTestCase {
+
+ private final Path applicationDir = Path.fromString("src/test/integration/vespa/");
+
+ private final String expectedRankConfig =
+ "constant(constant1).type : tensor(x[3])\n" +
+ "constant(constant1).value : tensor(x[3]):[0.5, 1.5, 2.5]\n" +
+ "rankingExpression(foo1).rankingScript : reduce(reduce(input1 * input2, sum, name) * constant(constant1), max, x) * 3.0\n" +
+ "rankingExpression(foo1).input2.type : tensor(x[3])\n" +
+ "rankingExpression(foo1).input1.type : tensor(name{},x[3])\n" +
+ "rankingExpression(foo2).rankingScript : reduce(reduce(input1 * input2, sum, name) * constant(constant1asLarge), max, x) * 3.0\n" +
+ "rankingExpression(foo2).input2.type : tensor(x[3])\n" +
+ "rankingExpression(foo2).input1.type : tensor(name{},x[3])\n";
+
+ /** The model name */
+ private final String name = "example";
+
+ @After
+ public void removeGeneratedModelFiles() {
+ IOUtils.recursiveDeleteDir(applicationDir.append(ApplicationPackage.MODELS_GENERATED_DIR).toFile());
+ }
+
+ @Test
+ public void testGlobalVespaModel() throws IOException {
+ ImportedModelTester tester = new ImportedModelTester(name, applicationDir);
+ VespaModel model = tester.createVespaModel();
+ tester.assertLargeConstant("constant1asLarge", model, Optional.of(3L));
+ assertEquals(expectedRankConfig, rankConfigOf("example", model));
+
+ // At this point the expression is stored - copy application to another location which do not have a models dir
+ Path storedAppDir = applicationDir.append("copy");
+ try {
+ storedAppDir.toFile().mkdirs();
+ IOUtils.copy(applicationDir.append("services.xml").toString(), storedAppDir.append("services.xml").toString());
+ IOUtils.copyDirectory(applicationDir.append(ApplicationPackage.MODELS_GENERATED_DIR).toFile(),
+ storedAppDir.append(ApplicationPackage.MODELS_GENERATED_DIR).toFile());
+ ImportedModelTester storedTester = new ImportedModelTester(name, storedAppDir);
+ VespaModel storedModel = storedTester.createVespaModel();
+ storedTester.assertLargeConstant("constant1asLarge", model, Optional.of(3L));
+ assertEquals(expectedRankConfig, rankConfigOf("example", storedModel));
+ }
+ finally {
+ IOUtils.recursiveDeleteDir(storedAppDir.toFile());
+ }
+ }
+
+ private String rankConfigOf(String rankProfileName, VespaModel model) {
+ StringBuilder b = new StringBuilder();
+ RawRankProfile profile = model.rankProfileList().getRankProfiles().get(rankProfileName);
+ for (var property : profile.configProperties())
+ b.append(property.getFirst()).append(" : ").append(property.getSecond()).append("\n");
+ return b.toString();
+ }
+
+}
diff --git a/config-model/src/test/java/com/yahoo/schema/processing/WeightedSetSummaryToTestCase.java b/config-model/src/test/java/com/yahoo/schema/processing/WeightedSetSummaryToTestCase.java
new file mode 100644
index 00000000000..2f62228cc3f
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/schema/processing/WeightedSetSummaryToTestCase.java
@@ -0,0 +1,23 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.schema.processing;
+
+import com.yahoo.schema.Schema;
+import com.yahoo.schema.ApplicationBuilder;
+import com.yahoo.schema.AbstractSchemaTestCase;
+import com.yahoo.schema.parser.ParseException;
+import org.junit.Test;
+
+import java.io.IOException;
+
+import static org.junit.Assert.assertNotNull;
+
+/** @author bratseth */
+public class WeightedSetSummaryToTestCase extends AbstractSchemaTestCase {
+
+ @Test
+ public void testRequireThatImplicitFieldsAreCreated() throws IOException, ParseException {
+ Schema schema = ApplicationBuilder.buildFromFile("src/test/examples/weightedset-summaryto.sd");
+ assertNotNull(schema);
+ }
+
+}