diff options
author | Jon Bratseth <bratseth@gmail.com> | 2022-05-19 12:03:06 +0200 |
---|---|---|
committer | Jon Bratseth <bratseth@gmail.com> | 2022-05-19 12:03:06 +0200 |
commit | 5c24dc5c9642a8d9ed70aee4c950fd0678a1ebec (patch) | |
tree | bd9b74bf00c832456f0b83c1b2cd7010be387d68 /config-model/src/test/java/com/yahoo/schema | |
parent | f17c4fe7de4c55f5c4ee61897eab8c2f588d8405 (diff) |
Rename the 'searchdefinition' package to 'schema'
Diffstat (limited to 'config-model/src/test/java/com/yahoo/schema')
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<float>(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<float>(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); + } + +} |