diff --git a/README.md b/README.md index 1ba9b291d..5a77b0020 100644 --- a/README.md +++ b/README.md @@ -668,10 +668,22 @@ for (var object : result.objects()) { } ``` +When _ingetsting_ data, Java records can be used to represent nested object properties: + +```java +record MusicVideo(@Property("link") String url, long runtimeSeconds) {} + +songs.data.insert(Map.of( + "title", "Billie Jean", + "musicVideo", new MusicVideo("https://youtube.com/billijean", 294L), +)); +``` + We want to stress that this ORM's focus is on improving type-safety around object properties and simplifying serialization/deserialization. It is intentionally kept minimal and as such has the following limitations: - **Does not support BLOB properties.** On the wire, blob properties are represented as base64-encoded strings, and both logically map to the Java's `String`. Presently there isn't a good way for the client to deduce which property type should be created, so it always maps `Sting -> TEXT`. - **Limited configuration options.** Vector indices, replication, multi-tenancy, and such need to be configured via a tucked builder in `.create(..., here -> here)`. +- **Cannot include nested objects.** Java records can be used as nested properties in a `Map`, but cannot include nested properties themselves. - **Does not support cross-references.** Properties and Cross-References are conceptually and "physically" separated in Weaviate' client libraries, so doing something like in the snippet below is not supported. ```java diff --git a/src/it/java/io/weaviate/containers/Weaviate.java b/src/it/java/io/weaviate/containers/Weaviate.java index 88cd23ee0..661273d74 100644 --- a/src/it/java/io/weaviate/containers/Weaviate.java +++ b/src/it/java/io/weaviate/containers/Weaviate.java @@ -15,7 +15,7 @@ import io.weaviate.client6.v1.internal.ObjectBuilder; public class Weaviate extends WeaviateContainer { - public static final String VERSION = "1.32.3"; + public static final String VERSION = "1.33.0"; public static final String DOCKER_IMAGE = "semitechnologies/weaviate"; private volatile SharedClient clientInstance; diff --git a/src/it/java/io/weaviate/integration/CollectionsITest.java b/src/it/java/io/weaviate/integration/CollectionsITest.java index 7e21784e6..975ebba76 100644 --- a/src/it/java/io/weaviate/integration/CollectionsITest.java +++ b/src/it/java/io/weaviate/integration/CollectionsITest.java @@ -10,6 +10,7 @@ import io.weaviate.client6.v1.api.WeaviateApiException; import io.weaviate.client6.v1.api.WeaviateClient; import io.weaviate.client6.v1.api.collections.CollectionConfig; +import io.weaviate.client6.v1.api.collections.DataType; import io.weaviate.client6.v1.api.collections.InvertedIndex; import io.weaviate.client6.v1.api.collections.Property; import io.weaviate.client6.v1.api.collections.ReferenceProperty; @@ -191,4 +192,39 @@ public void testShards() throws IOException { public void testInvalidCollectionName() throws IOException { client.collections.create("^collection@weaviate.io$"); } + + @Test + public void testNestedProperties() throws IOException, Exception { + var nsBuildings = ns("Buildings"); + + client.collections.create( + nsBuildings, c -> c.properties( + Property.object("address", p -> p.nestedProperties( + Property.text("street"), + Property.integer("building_nr"), + Property.bool("isOneWay"))), + Property.objectArray("apartments", p -> p.nestedProperties( + Property.integer("door_nr"), + Property.number("area"))))); + + var config = client.collections.getConfig(nsBuildings); + + var properties = Assertions.assertThat(config).get() + .extracting(CollectionConfig::properties, InstanceOfAssertFactories.list(Property.class)) + .hasSize(2).actual(); + + Assertions.assertThat(properties.get(0)) + .returns("address", Property::propertyName) + .returns(DataType.OBJECT, p -> p.dataTypes().get(0)) + .extracting(Property::nestedProperties, InstanceOfAssertFactories.list(Property.class)) + .extracting(Property::dataTypes).extracting(types -> types.get(0)) + .containsExactly(DataType.TEXT, DataType.INT, DataType.BOOL); + + Assertions.assertThat(properties.get(1)) + .returns("apartments", Property::propertyName) + .returns(DataType.OBJECT_ARRAY, p -> p.dataTypes().get(0)) + .extracting(Property::nestedProperties, InstanceOfAssertFactories.list(Property.class)) + .extracting(Property::dataTypes).extracting(types -> types.get(0)) + .containsExactly(DataType.INT, DataType.NUMBER); + } } diff --git a/src/it/java/io/weaviate/integration/DataITest.java b/src/it/java/io/weaviate/integration/DataITest.java index c679f7a7a..693eccea4 100644 --- a/src/it/java/io/weaviate/integration/DataITest.java +++ b/src/it/java/io/weaviate/integration/DataITest.java @@ -431,7 +431,13 @@ public void testDataTypes() throws IOException { Property.boolArray("prop_bool_array"), Property.dateArray("prop_date_array"), Property.uuidArray("prop_uuid_array"), - Property.textArray("prop_text_array"))); + Property.textArray("prop_text_array"), + Property.object("prop_object", + p -> p.nestedProperties( + Property.text("marco"))), + Property.objectArray("prop_object_array", + p -> p.nestedProperties( + Property.text("marco"))))); var types = client.collections.use(nsDataTypes); @@ -450,13 +456,13 @@ public void testDataTypes() throws IOException { Map.entry("prop_bool_array", List.of(true, false)), Map.entry("prop_date_array", List.of(now, now)), Map.entry("prop_uuid_array", List.of(uuid, uuid)), - Map.entry("prop_text_array", List.of("a", "b", "c"))); - var returnProperties = want.keySet().toArray(String[]::new); + Map.entry("prop_text_array", List.of("a", "b", "c")), + Map.entry("prop_object", Map.of("marco", "polo")), + Map.entry("prop_object_array", List.of(Map.of("marco", "polo")))); // Act var object = types.data.insert(want); - var got = types.query.byId(object.uuid(), - q -> q.returnProperties(returnProperties)); + var got = types.query.byId(object.uuid()); // return all properties // Assert Assertions.assertThat(got).get() @@ -465,4 +471,52 @@ public void testDataTypes() throws IOException { .containsAllEntriesOf(want); } + + record Address( + String street, + @io.weaviate.client6.v1.api.collections.annotations.Property("building_nr") int buildingNr, + @io.weaviate.client6.v1.api.collections.annotations.Property("isOneWay") boolean oneWay) { + } + + @Test + public void testNestedProperties_insertMany() throws IOException { + // Arrange + var nsBuildings = ns("Buildings"); + + client.collections.create( + nsBuildings, c -> c.properties( + Property.object("address", p -> p.nestedProperties( + Property.text("street"), + Property.integer("building_nr"), + Property.bool("isOneWay"))), + Property.objectArray("apartments", p -> p.nestedProperties( + Property.integer("door_nr"), + Property.number("area"))))); + + var buildings = client.collections.use(nsBuildings); + + Map house_1 = Map.of( + "address", Map.of( + "street", "Burggasse", + "building_nr", 51, + "isOneWay", true), + "apartments", List.of( + Map.of("door_nr", 11, "area", 42.2), + Map.of("door_nr", 12, "area", 26.7))); + Map house_2 = Map.of( + "address", new Address( + "Port Mariland St.", + 111, + false), + "apartments", new Map[] { + Map.of("door_nr", 21, "area", 42.2), + Map.of("door_nr", 22, "area", 26.7), + }); + + // Act + var result = buildings.data.insertMany(house_1, house_2); + + // Assert + Assertions.assertThat(result.errors()).isEmpty(); + } } diff --git a/src/it/java/io/weaviate/integration/ORMITest.java b/src/it/java/io/weaviate/integration/ORMITest.java index 16b5b3717..e732738fb 100644 --- a/src/it/java/io/weaviate/integration/ORMITest.java +++ b/src/it/java/io/weaviate/integration/ORMITest.java @@ -350,4 +350,8 @@ public void test_partialScan() throws IOException { .returns(true, Song::hasAward) .returns(null, Song::monthlyListeners); } + + @Test + public void test_nestedProperties() throws IOException { + } } diff --git a/src/main/java/io/weaviate/client6/v1/api/collections/DataType.java b/src/main/java/io/weaviate/client6/v1/api/collections/DataType.java index 91584a797..67044e22c 100644 --- a/src/main/java/io/weaviate/client6/v1/api/collections/DataType.java +++ b/src/main/java/io/weaviate/client6/v1/api/collections/DataType.java @@ -18,6 +18,8 @@ public interface DataType { public static final String DATE_ARRAY = "date[]"; public static final String UUID = "uuid"; public static final String UUID_ARRAY = "uuid[]"; + public static final String OBJECT = "object"; + public static final String OBJECT_ARRAY = "object[]"; /** * Scalar/array types which Weaviate and WeaviateClient recognize. @@ -31,6 +33,6 @@ public interface DataType { * using {@link Property}'s factory classes. */ public static final Set KNOWN_TYPES = ImmutableSet.of( - TEXT, INT, BLOB, BOOL, DATE, UUID, NUMBER, - TEXT_ARRAY, INT_ARRAY, NUMBER_ARRAY, BOOL_ARRAY, DATE_ARRAY, UUID_ARRAY); + TEXT, INT, BLOB, BOOL, DATE, UUID, NUMBER, OBJECT, + TEXT_ARRAY, INT_ARRAY, NUMBER_ARRAY, BOOL_ARRAY, DATE_ARRAY, UUID_ARRAY, OBJECT_ARRAY); } diff --git a/src/main/java/io/weaviate/client6/v1/api/collections/Property.java b/src/main/java/io/weaviate/client6/v1/api/collections/Property.java index 66121675c..f3e69cf3a 100644 --- a/src/main/java/io/weaviate/client6/v1/api/collections/Property.java +++ b/src/main/java/io/weaviate/client6/v1/api/collections/Property.java @@ -1,5 +1,7 @@ package io.weaviate.client6.v1.api.collections; +import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import java.util.function.Function; @@ -17,7 +19,8 @@ public record Property( @SerializedName("indexSearchable") Boolean indexSearchable, @SerializedName("tokenization") Tokenization tokenization, @SerializedName("skipVectorization") Boolean skipVectorization, - @SerializedName("vectorizePropertyName") Boolean vectorizePropertyName) { + @SerializedName("vectorizePropertyName") Boolean vectorizePropertyName, + @SerializedName("nestedProperties") List nestedProperties) { /** * Create a {@code text} property. @@ -96,7 +99,7 @@ public static Property integerArray(String name, Function> fn) { + return newProperty(name, DataType.OBJECT, fn); + } + + /** + * Create a {@code object[]} property. + * + * @param name Property name. + */ + public static Property objectArray(String name) { + return objectArray(name, ObjectBuilder.identity()); + } + + /** + * Create a {@code objectArray[]} property with additional configuration. + * + * @param name Property name. + * @param fn Lambda expression for optional parameters. + */ + public static Property objectArray(String name, Function> fn) { + return newProperty(name, DataType.OBJECT_ARRAY, fn); + } + private static Property newProperty(String name, String dataType, Function> fn) { return fn.apply(new Builder(name, dataType)).build(); } @@ -329,7 +370,8 @@ public Property(Builder builder) { builder.indexSearchable, builder.tokenization, builder.skipVectorization, - builder.vectorizePropertyName); + builder.vectorizePropertyName, + builder.nestedProperties.isEmpty() ? null : builder.nestedProperties); } // All methods accepting a `boolean` should have a boxed overload @@ -346,9 +388,9 @@ public Property(Builder builder) { public static class Builder implements ObjectBuilder { // Required parameters. private final String propertyName; + private final List dataTypes = new ArrayList<>(); // Optional parameters. - private List dataTypes; private String description; private Boolean indexInverted; private Boolean indexFilterable; @@ -357,6 +399,7 @@ public static class Builder implements ObjectBuilder { private Tokenization tokenization; private Boolean skipVectorization; private Boolean vectorizePropertyName; + private List nestedProperties = new ArrayList<>(); /** * Create a scalar / array type property. @@ -365,7 +408,7 @@ public static class Builder implements ObjectBuilder { */ public Builder(String propertyName, String dataType) { this.propertyName = propertyName; - this.dataTypes = List.of(dataType); + this.dataTypes.add(dataType); } /** @@ -375,7 +418,7 @@ public Builder(String propertyName, String dataType) { */ public Builder(String propertyName, List dataTypes) { this.propertyName = propertyName; - this.dataTypes = List.copyOf(dataTypes); + this.dataTypes.addAll(dataTypes); } /** Add property description. */ @@ -491,6 +534,32 @@ public Builder vectorizePropertyName(boolean vectorizePropertyName) { return this; } + /** + * Defined nested properties. This configuration is only applicable to a + * property of type {@code object} and {@code object[]}. + * + *
{@code
+     * Property.object("address",
+     *     p -> p.nestedProperties(
+     *         Property.text("street"),
+     *         Property.integer("building_nr")))
+     * }
+ */ + public Builder nestedProperties(Property... properties) { + return nestedProperties(Arrays.asList(properties)); + } + + /** + * Defined nested properties. This configuration is only applicable to a + * property of type {@code object} and {@code object[]}. + * + * @see Builder#nestedProperties(Property...) + */ + public Builder nestedProperties(List properties) { + this.nestedProperties.addAll(properties); + return this; + } + /** Convenience method to be used by {@link Property#edit}. */ Builder vectorizePropertyName(Boolean vectorizePropertyName) { this.vectorizePropertyName = vectorizePropertyName; diff --git a/src/main/java/io/weaviate/client6/v1/api/collections/data/InsertManyRequest.java b/src/main/java/io/weaviate/client6/v1/api/collections/data/InsertManyRequest.java index b4d86cfbc..4475f178d 100644 --- a/src/main/java/io/weaviate/client6/v1/api/collections/data/InsertManyRequest.java +++ b/src/main/java/io/weaviate/client6/v1/api/collections/data/InsertManyRequest.java @@ -4,11 +4,13 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.Map; import java.util.UUID; import io.weaviate.client6.v1.api.collections.CollectionHandleDefaults; import io.weaviate.client6.v1.api.collections.ObjectMetadata; import io.weaviate.client6.v1.api.collections.WeaviateObject; +import io.weaviate.client6.v1.internal.Debug; import io.weaviate.client6.v1.internal.MapUtil; import io.weaviate.client6.v1.internal.grpc.ByteStringUtil; import io.weaviate.client6.v1.internal.grpc.Rpc; @@ -128,120 +130,9 @@ public static void buildObject(WeaviateProtoBatch.BatchObject.Builder object } } - var properties = WeaviateProtoBatch.BatchObject.Properties.newBuilder(); - var nonRef = com.google.protobuf.Struct.newBuilder(); var singleRef = new ArrayList(); var multiRef = new ArrayList(); - collection - .propertiesReader(insert.properties()).readProperties() - .entrySet().stream().forEach(entry -> { - var value = entry.getValue(); - var protoValue = com.google.protobuf.Value.newBuilder(); - - if (value == null) { - return; - } - - if (value instanceof String v) { - protoValue.setStringValue(v); - } else if (value instanceof UUID v) { - protoValue.setStringValue(v.toString()); - } else if (value instanceof OffsetDateTime v) { - protoValue.setStringValue(v.toString()); - } else if (value instanceof Boolean v) { - protoValue.setBoolValue(v.booleanValue()); - } else if (value instanceof Number v) { - protoValue.setNumberValue(v.doubleValue()); - } else if (value instanceof List v) { - protoValue.setListValue( - com.google.protobuf.ListValue.newBuilder() - .addAllValues(v.stream() - .map(listValue -> { - var protoListValue = com.google.protobuf.Value.newBuilder(); - if (listValue instanceof String lv) { - protoListValue.setStringValue(lv); - } else if (listValue instanceof UUID lv) { - protoListValue.setStringValue(lv.toString()); - } else if (listValue instanceof OffsetDateTime lv) { - protoListValue.setStringValue(lv.toString()); - } else if (listValue instanceof Boolean lv) { - protoListValue.setBoolValue(lv); - } else if (listValue instanceof Number lv) { - protoListValue.setNumberValue(lv.doubleValue()); - } - return protoListValue.build(); - }) - .toList())); - - } else if (value.getClass().isArray()) { - List values; - - if (value instanceof String[] v) { - values = Arrays.stream(v) - .map(lv -> com.google.protobuf.Value.newBuilder().setStringValue(lv).build()).toList(); - } else if (value instanceof UUID[] v) { - values = Arrays.stream(v) - .map(lv -> com.google.protobuf.Value.newBuilder().setStringValue(lv.toString()).build()).toList(); - } else if (value instanceof OffsetDateTime[] v) { - values = Arrays.stream(v) - .map(lv -> com.google.protobuf.Value.newBuilder().setStringValue(lv.toString()).build()).toList(); - } else if (value instanceof Boolean[] v) { - values = Arrays.stream(v) - .map(lv -> com.google.protobuf.Value.newBuilder().setBoolValue(lv).build()).toList(); - } else if (value instanceof boolean[] v) { - values = new ArrayList<>(); - for (boolean b : v) { - values.add(com.google.protobuf.Value.newBuilder().setBoolValue(b).build()); - } - } else if (value instanceof short[] v) { - values = new ArrayList<>(); - for (short s : v) { - values.add(com.google.protobuf.Value.newBuilder().setNumberValue(s).build()); - } - } else if (value instanceof int[] v) { - values = Arrays.stream(v).boxed() - .map(lv -> com.google.protobuf.Value.newBuilder().setNumberValue(lv).build()).toList(); - } else if (value instanceof long[] v) { - values = Arrays.stream(v).boxed() - .map(lv -> com.google.protobuf.Value.newBuilder().setNumberValue(lv).build()).toList(); - } else if (value instanceof float[] v) { - values = new ArrayList<>(); - for (float s : v) { - values.add(com.google.protobuf.Value.newBuilder().setNumberValue(s).build()); - } - } else if (value instanceof double[] v) { - values = Arrays.stream(v).boxed() - .map(lv -> com.google.protobuf.Value.newBuilder().setNumberValue(lv).build()).toList(); - } else if (value instanceof Short[] v) { - values = Arrays.stream(v) - .map(lv -> com.google.protobuf.Value.newBuilder().setNumberValue(lv).build()).toList(); - } else if (value instanceof Integer[] v) { - values = Arrays.stream(v) - .map(lv -> com.google.protobuf.Value.newBuilder().setNumberValue(lv).build()).toList(); - } else if (value instanceof Long[] v) { - values = Arrays.stream(v) - .map(lv -> com.google.protobuf.Value.newBuilder().setNumberValue(lv).build()).toList(); - } else if (value instanceof Float[] v) { - values = Arrays.stream(v) - .map(lv -> com.google.protobuf.Value.newBuilder().setNumberValue(lv).build()).toList(); - } else if (value instanceof Double[] v) { - values = Arrays.stream(v) - .map(lv -> com.google.protobuf.Value.newBuilder().setNumberValue(lv).build()).toList(); - } else { - throw new AssertionError("(insertMany) branch not covered " + value.getClass()); - } - - protoValue.setListValue(com.google.protobuf.ListValue.newBuilder() - .addAllValues(values) - .build()); - } else { - throw new AssertionError("(insertMany) branch not covered " + value.getClass()); - } - - nonRef.putFields(entry.getKey(), protoValue.build()); - }); - insert.references() .entrySet().stream().forEach(entry -> { var references = entry.getValue(); @@ -266,11 +157,166 @@ public static void buildObject(WeaviateProtoBatch.BatchObject.Builder object } }); - properties + var nonRef = marshalStruct(collection.propertiesReader(insert.properties()).readProperties()); + object.setProperties(WeaviateProtoBatch.BatchObject.Properties.newBuilder() .setNonRefProperties(nonRef) .addAllSingleTargetRefProps(singleRef) - .addAllMultiTargetRefProps(multiRef); + .addAllMultiTargetRefProps(multiRef)); - object.setProperties(properties); + Debug.printProto(object); + } + + @SuppressWarnings("unchecked") + private static com.google.protobuf.Value marshalValue(Object value) { + var protoValue = com.google.protobuf.Value.newBuilder(); + + if (value == null) { + return null; + } + + if (value instanceof String v) { + protoValue.setStringValue(v); + } else if (value instanceof UUID v) { + protoValue.setStringValue(v.toString()); + } else if (value instanceof OffsetDateTime v) { + protoValue.setStringValue(v.toString()); + } else if (value instanceof Boolean v) { + protoValue.setBoolValue(v.booleanValue()); + } else if (value instanceof Number v) { + protoValue.setNumberValue(v.doubleValue()); + } else if (value instanceof List v) { + protoValue.setListValue( + com.google.protobuf.ListValue.newBuilder() + .addAllValues(v.stream() + .map(listValue -> { + var protoListValue = com.google.protobuf.Value.newBuilder(); + if (listValue instanceof String lv) { + protoListValue.setStringValue(lv); + } else if (listValue instanceof UUID lv) { + protoListValue.setStringValue(lv.toString()); + } else if (listValue instanceof OffsetDateTime lv) { + protoListValue.setStringValue(lv.toString()); + } else if (listValue instanceof Boolean lv) { + protoListValue.setBoolValue(lv); + } else if (listValue instanceof Number lv) { + protoListValue.setNumberValue(lv.doubleValue()); + } else if (listValue instanceof Map properties) { + protoListValue.setStructValue(marshalStruct((Map) properties)); + } else if (listValue instanceof Record r) { + CollectionDescriptor recordDescriptor = (CollectionDescriptor) CollectionDescriptor + .ofClass(r.getClass()); + var properties = recordDescriptor.propertiesReader(r).readProperties(); + protoListValue.setStructValue(marshalStruct(properties)); + } else { + throw new IllegalArgumentException("data type " + value.getClass() + " is not supported"); + } + return protoListValue.build(); + }) + .toList())); + + } else if (value.getClass().isArray()) { + List values; + + if (value instanceof String[] v) { + values = Arrays.stream(v) + .map(lv -> com.google.protobuf.Value.newBuilder().setStringValue(lv).build()).toList(); + } else if (value instanceof UUID[] v) { + values = Arrays.stream(v) + .map(lv -> com.google.protobuf.Value.newBuilder().setStringValue(lv.toString()).build()).toList(); + } else if (value instanceof OffsetDateTime[] v) { + values = Arrays.stream(v) + .map(lv -> com.google.protobuf.Value.newBuilder().setStringValue(lv.toString()).build()).toList(); + } else if (value instanceof Boolean[] v) { + values = Arrays.stream(v) + .map(lv -> com.google.protobuf.Value.newBuilder().setBoolValue(lv).build()).toList(); + } else if (value instanceof boolean[] v) { + values = new ArrayList<>(); + for (boolean b : v) { + values.add(com.google.protobuf.Value.newBuilder().setBoolValue(b).build()); + } + } else if (value instanceof short[] v) { + values = new ArrayList<>(); + for (short s : v) { + values.add(com.google.protobuf.Value.newBuilder().setNumberValue(s).build()); + } + } else if (value instanceof int[] v) { + values = Arrays.stream(v).boxed() + .map(lv -> com.google.protobuf.Value.newBuilder().setNumberValue(lv).build()).toList(); + } else if (value instanceof long[] v) { + values = Arrays.stream(v).boxed() + .map(lv -> com.google.protobuf.Value.newBuilder().setNumberValue(lv).build()).toList(); + } else if (value instanceof float[] v) { + values = new ArrayList<>(); + for (float s : v) { + values.add(com.google.protobuf.Value.newBuilder().setNumberValue(s).build()); + } + } else if (value instanceof double[] v) { + values = Arrays.stream(v).boxed() + .map(lv -> com.google.protobuf.Value.newBuilder().setNumberValue(lv).build()).toList(); + } else if (value instanceof Short[] v) { + values = Arrays.stream(v) + .map(lv -> com.google.protobuf.Value.newBuilder().setNumberValue(lv).build()).toList(); + } else if (value instanceof Integer[] v) { + values = Arrays.stream(v) + .map(lv -> com.google.protobuf.Value.newBuilder().setNumberValue(lv).build()).toList(); + } else if (value instanceof Long[] v) { + values = Arrays.stream(v) + .map(lv -> com.google.protobuf.Value.newBuilder().setNumberValue(lv).build()).toList(); + } else if (value instanceof Float[] v) { + values = Arrays.stream(v) + .map(lv -> com.google.protobuf.Value.newBuilder().setNumberValue(lv).build()).toList(); + } else if (value instanceof Double[] v) { + values = Arrays.stream(v) + .map(lv -> com.google.protobuf.Value.newBuilder().setNumberValue(lv).build()).toList(); + } else if (value instanceof Map[] v) { + values = Arrays.stream(v) + .map(lv -> com.google.protobuf.Value.newBuilder() + .setStructValue(marshalStruct((Map) lv)) + .build()) + .toList(); + } else if (value instanceof Record[] v) { + values = Arrays.stream(v) + .map(lv -> { + // Get the descriptor for each iteration in case the array is heterogenous. + final CollectionDescriptor recordDescriptor = (CollectionDescriptor) CollectionDescriptor + .ofClass(lv.getClass()); + var properties = recordDescriptor.propertiesReader(lv).readProperties(); + return com.google.protobuf.Value.newBuilder() + .setStructValue(marshalStruct(properties)) + .build(); + }) + .toList(); + } else { + throw new IllegalArgumentException("array type " + value.getClass() + " is not supported"); + } + + protoValue.setListValue(com.google.protobuf.ListValue.newBuilder() + .addAllValues(values) + .build()); + } else if (value instanceof Map properties) { + protoValue.setStructValue(marshalStruct((Map) properties)); + } else if (value instanceof Record r) { + CollectionDescriptor recordDescriptor = (CollectionDescriptor) CollectionDescriptor + .ofClass(r.getClass()); + var properties = recordDescriptor.propertiesReader(r).readProperties(); + protoValue.setStructValue(marshalStruct(properties)); + } else { + throw new IllegalArgumentException("data type " + value.getClass() + " is not supported"); + } + + return protoValue.build(); + } + + private static com.google.protobuf.Struct marshalStruct(Map properties) { + var struct = com.google.protobuf.Struct.newBuilder(); + properties.entrySet().stream() + .forEach(entry -> { + var nestedValue = marshalValue(entry.getValue()); + if (nestedValue == null) { + return; + } + struct.putFields((String) entry.getKey(), nestedValue); + }); + return struct.build(); } } diff --git a/src/main/java/io/weaviate/client6/v1/api/collections/query/QueryRequest.java b/src/main/java/io/weaviate/client6/v1/api/collections/query/QueryRequest.java index fa07d67d5..11d31b4e5 100644 --- a/src/main/java/io/weaviate/client6/v1/api/collections/query/QueryRequest.java +++ b/src/main/java/io/weaviate/client6/v1/api/collections/query/QueryRequest.java @@ -136,8 +136,8 @@ private static WeaviateObject unmarshalWithRefere WeaviateProtoSearchGet.MetadataResult metadataResult, CollectionDescriptor descriptor) { var properties = descriptor.propertiesBuilder(); - propertiesResult.getNonRefProps().getFieldsMap() - .entrySet().stream().forEach(entry -> setProperty(entry.getKey(), entry.getValue(), properties)); + propertiesResult.getNonRefProps().getFieldsMap().entrySet().stream() + .forEach(entry -> setProperty(entry.getKey(), entry.getValue(), properties, descriptor)); // In case a reference is multi-target, there will be a separate // "reference property" for each of the targets, so instead of @@ -213,7 +213,7 @@ private static WeaviateObject unmarshalWithRefere } private static void setProperty(String property, WeaviateProtoProperties.Value value, - PropertiesBuilder builder) { + PropertiesBuilder builder, CollectionDescriptor descriptor) { if (value.hasNullValue()) { builder.setNull(property); } else if (value.hasTextValue()) { @@ -254,7 +254,22 @@ private static void setProperty(String property, WeaviateProtoProperties.Val var dates = list.getDateValues().getValuesList().stream() .map(DateUtil::fromISO8601).toList(); builder.setOffsetDateTimeArray(property, dates); + } else if (list.hasObjectValues()) { + List objects = list.getObjectValues().getValuesList().stream() + .map(object -> { + var properties = descriptor.propertiesBuilder(); + object.getFieldsMap().entrySet().stream() + .forEach(entry -> setProperty(entry.getKey(), entry.getValue(), properties, descriptor)); + return properties.build(); + }).toList(); + builder.setNestedObjectArray(property, objects); } + } else if (value.hasObjectValue()) { + var object = value.getObjectValue(); + var properties = descriptor.propertiesBuilder(); + object.getFieldsMap().entrySet().stream() + .forEach(entry -> setProperty(entry.getKey(), entry.getValue(), properties, descriptor)); + builder.setNestedObject(property, properties.build()); } else { throw new IllegalArgumentException(property + " data type is not supported"); } diff --git a/src/main/java/io/weaviate/client6/v1/internal/json/JSON.java b/src/main/java/io/weaviate/client6/v1/internal/json/JSON.java index 3d2e00089..e1f0040f2 100644 --- a/src/main/java/io/weaviate/client6/v1/internal/json/JSON.java +++ b/src/main/java/io/weaviate/client6/v1/internal/json/JSON.java @@ -2,6 +2,7 @@ import com.google.gson.Gson; import com.google.gson.GsonBuilder; +import com.google.gson.ToNumberPolicy; import com.google.gson.reflect.TypeToken; import io.weaviate.client6.v1.internal.orm.PropertyFieldNamingStrategy; @@ -49,6 +50,8 @@ public final class JSON { // ORM FieldNaminsStrategy ------------------------------------------------ gsonBuilder.setFieldNamingStrategy(PropertyFieldNamingStrategy.INSTANCE); + + gsonBuilder.setObjectToNumberStrategy(ToNumberPolicy.LONG_OR_DOUBLE); gson = gsonBuilder.create(); } diff --git a/src/main/java/io/weaviate/client6/v1/internal/orm/MapBuilder.java b/src/main/java/io/weaviate/client6/v1/internal/orm/MapBuilder.java index 6d1383160..0c044e3c9 100644 --- a/src/main/java/io/weaviate/client6/v1/internal/orm/MapBuilder.java +++ b/src/main/java/io/weaviate/client6/v1/internal/orm/MapBuilder.java @@ -79,6 +79,16 @@ public void setOffsetDateTimeArray(String property, List value) properties.put(property, value); } + @Override + public void setNestedObject(String property, Object value) { + properties.put(property, value); + } + + @Override + public void setNestedObjectArray(String property, List value) { + properties.put(property, value); + } + @Override public Map build() { return properties; diff --git a/src/main/java/io/weaviate/client6/v1/internal/orm/PojoBuilder.java b/src/main/java/io/weaviate/client6/v1/internal/orm/PojoBuilder.java index 223fd0f53..085811565 100644 --- a/src/main/java/io/weaviate/client6/v1/internal/orm/PojoBuilder.java +++ b/src/main/java/io/weaviate/client6/v1/internal/orm/PojoBuilder.java @@ -227,6 +227,16 @@ public void setOffsetDateTimeArray(String propertyName, List val : value); } + @Override + public void setNestedObject(String propertyName, Object value) { + throw new UnsupportedOperationException("Unimplemented method 'setNestedObjectArray'"); + } + + @Override + public void setNestedObjectArray(String property, List value) { + throw new UnsupportedOperationException("Unimplemented method 'setNestedObjectArray'"); + } + @Override public PropertiesT build() { Object[] args = ctorArgs.values().stream().map(Arg::value).toArray(); diff --git a/src/main/java/io/weaviate/client6/v1/internal/orm/PojoDescriptor.java b/src/main/java/io/weaviate/client6/v1/internal/orm/PojoDescriptor.java index d0a1f218b..0b5724153 100644 --- a/src/main/java/io/weaviate/client6/v1/internal/orm/PojoDescriptor.java +++ b/src/main/java/io/weaviate/client6/v1/internal/orm/PojoDescriptor.java @@ -63,6 +63,8 @@ final class PojoDescriptor implements CollectionDescriptor put(Float[].class, Property::numberArray); put(double[].class, Property::numberArray); put(Double[].class, Property::numberArray); + + put(Map.class, Property::object); } }; CTORS = Collections.unmodifiableMap(ctors); diff --git a/src/main/java/io/weaviate/client6/v1/internal/orm/PropertiesBuilder.java b/src/main/java/io/weaviate/client6/v1/internal/orm/PropertiesBuilder.java index 797831640..81e8ec5e4 100644 --- a/src/main/java/io/weaviate/client6/v1/internal/orm/PropertiesBuilder.java +++ b/src/main/java/io/weaviate/client6/v1/internal/orm/PropertiesBuilder.java @@ -33,5 +33,9 @@ public interface PropertiesBuilder { void setOffsetDateTimeArray(String property, List value); + void setNestedObject(String property, Object value); + + void setNestedObjectArray(String property, List value); + T build(); } diff --git a/src/test/java/io/weaviate/client6/v1/api/WeaviateClientAsyncTest.java b/src/test/java/io/weaviate/client6/v1/api/WeaviateClientAsyncTest.java index e538de7a0..60e46de46 100644 --- a/src/test/java/io/weaviate/client6/v1/api/WeaviateClientAsyncTest.java +++ b/src/test/java/io/weaviate/client6/v1/api/WeaviateClientAsyncTest.java @@ -14,7 +14,7 @@ public void testFailedConnection() { @Test(expected = WeaviateConnectException.class) public void testFailedConnection_Local() { - WeaviateClientAsync.connectToLocal(); + WeaviateClientAsync.connectToLocal(conn -> conn.port(1234)); } @Test(expected = WeaviateConnectException.class)