diff --git a/src/main/java/io/weaviate/client/v1/graphql/query/Get.java b/src/main/java/io/weaviate/client/v1/graphql/query/Get.java index a9250602d..3f3e94bdd 100644 --- a/src/main/java/io/weaviate/client/v1/graphql/query/Get.java +++ b/src/main/java/io/weaviate/client/v1/graphql/query/Get.java @@ -118,6 +118,11 @@ public Get withGenerativeSearch(GenerativeSearchBuilder generativeSearch) { return this; } + public Get withConsistencyLevel(String level) { + getBuilder.withConsistencyLevel(level); + return this; + } + @Override public Result run() { String getQuery = getBuilder.build().buildQuery(); diff --git a/src/main/java/io/weaviate/client/v1/graphql/query/builder/GetBuilder.java b/src/main/java/io/weaviate/client/v1/graphql/query/builder/GetBuilder.java index 46cf41fda..8ce3e98a0 100644 --- a/src/main/java/io/weaviate/client/v1/graphql/query/builder/GetBuilder.java +++ b/src/main/java/io/weaviate/client/v1/graphql/query/builder/GetBuilder.java @@ -41,6 +41,7 @@ public class GetBuilder implements Query { Integer offset; Integer limit; String after; + String withConsistencyLevel; WhereArgument withWhereFilter; NearTextArgument withNearTextFilter; Bm25Argument withBm25Filter; @@ -56,7 +57,7 @@ public class GetBuilder implements Query { private boolean includesFilterClause() { return ObjectUtils.anyNotNull(withWhereFilter, withNearTextFilter, withNearObjectFilter, withNearVectorFilter, withNearImageFilter, withGroupArgument, withAskArgument, withBm25Filter, withHybridFilter, limit, offset, - withSortArguments); + withSortArguments, withConsistencyLevel); } private String createFilterClause() { @@ -102,6 +103,9 @@ private String createFilterClause() { if (withSortArguments != null) { filters.add(withSortArguments.build()); } + if (withConsistencyLevel != null) { + filters.add(String.format("consistencyLevel:%s", withConsistencyLevel)); + } return String.format("(%s)", String.join(",", filters)); } diff --git a/src/test/java/io/weaviate/client/v1/graphql/query/builder/GetBuilderTest.java b/src/test/java/io/weaviate/client/v1/graphql/query/builder/GetBuilderTest.java index 19067a15d..b11fdeb30 100644 --- a/src/test/java/io/weaviate/client/v1/graphql/query/builder/GetBuilderTest.java +++ b/src/test/java/io/weaviate/client/v1/graphql/query/builder/GetBuilderTest.java @@ -1,5 +1,6 @@ package io.weaviate.client.v1.graphql.query.builder; +import io.weaviate.client.v1.data.replication.model.ConsistencyLevel; import io.weaviate.client.v1.graphql.query.argument.WhereArgument; import org.junit.Test; import io.weaviate.client.v1.filters.Operator; @@ -431,6 +432,28 @@ public void testBuildGetWithSort() { assertEquals("{Get{Pizza(sort:[{path:[\"property1\"]},{path:[\"property2\"] order:desc},{path:[\"property3\"] order:asc}]){name}}}", query3); } + @Test + public void testBuildGetWithConsistencyLevel() { + // given + Fields fields = Fields.builder() + .fields(new Field[]{ Field.builder().name("name").build() }) + .build(); + // when + String withAll = GetBuilder.builder().className("Pizza").fields(fields) + .withConsistencyLevel(ConsistencyLevel.ALL) + .build().buildQuery(); + String withQuorum = GetBuilder.builder().className("Pizza").fields(fields) + .withConsistencyLevel(ConsistencyLevel.QUORUM) + .build().buildQuery(); + String withOne = GetBuilder.builder().className("Pizza").fields(fields) + .withConsistencyLevel(ConsistencyLevel.ONE) + .build().buildQuery(); + // then + assertEquals("{Get{Pizza(consistencyLevel:ALL){name}}}", withAll); + assertEquals("{Get{Pizza(consistencyLevel:QUORUM){name}}}", withQuorum); + assertEquals("{Get{Pizza(consistencyLevel:ONE){name}}}", withOne); + } + @Test public void shouldBuildGetWithGenerativeSearchAndMultipleFieldsIncludingAdditional() { // given diff --git a/src/test/java/io/weaviate/integration/client/WeaviateTestGenerics.java b/src/test/java/io/weaviate/integration/client/WeaviateTestGenerics.java index de6554cc7..23de462c1 100644 --- a/src/test/java/io/weaviate/integration/client/WeaviateTestGenerics.java +++ b/src/test/java/io/weaviate/integration/client/WeaviateTestGenerics.java @@ -5,6 +5,7 @@ import io.weaviate.client.v1.batch.model.ObjectGetResponse; import io.weaviate.client.v1.data.model.WeaviateObject; import io.weaviate.client.v1.misc.model.InvertedIndexConfig; +import io.weaviate.client.v1.misc.model.ReplicationConfig; import io.weaviate.client.v1.schema.model.DataType; import io.weaviate.client.v1.schema.model.Property; import io.weaviate.client.v1.schema.model.Tokenization; @@ -164,6 +165,127 @@ public void createTestSchemaAndData(WeaviateClient client) { assertEquals(6, insertStatus.getResult().length); } + public void createReplicatedTestSchemaAndData(WeaviateClient client) { + createWeaviateReplicatedTestSchemaFood(client); + + // Create pizzas + WeaviateObject[] menuPizza = new WeaviateObject[]{ + createObject(PIZZA_QUATTRO_FORMAGGI_ID, "Pizza", "Quattro Formaggi", + "Pizza quattro formaggi Italian: [ˈkwattro forˈmaddʒi] (four cheese pizza) is a variety of pizza in Italian cuisine that is topped with a combination of four kinds of cheese, usually melted together, with (rossa, red) or without (bianca, white) tomato sauce. It is popular worldwide, including in Italy,[1] and is one of the iconic items from pizzerias's menus.", + 1.4f, "2022-01-02T03:04:05+01:00"), + createObject(PIZZA_FRUTTI_DI_MARE_ID, "Pizza", "Frutti di Mare", + "Frutti di Mare is an Italian type of pizza that may be served with scampi, mussels or squid. It typically lacks cheese, with the seafood being served atop a tomato sauce.", + 2.5f, "2022-02-03T04:05:06+02:00"), + createObject(PIZZA_HAWAII_ID, "Pizza", "Hawaii", + "Universally accepted to be the best pizza ever created.", + 1.1f, "2022-03-04T05:06:07+03:00"), + createObject(PIZZA_DOENER_ID, "Pizza", "Doener", + "A innovation, some say revolution, in the pizza industry.", + 1.2f, "2022-04-05T06:07:08+04:00"), + }; + // Create soups + WeaviateObject[] menuSoup = new WeaviateObject[]{ + createObject(SOUP_CHICKENSOUP_ID, "Soup", "ChickenSoup", + "Used by humans when their inferior genetics are attacked by microscopic organisms.", + 2.0f, "2022-05-06T07:08:09+05:00"), + createObject(SOUP_BEAUTIFUL_ID, "Soup", "Beautiful", + "Putting the game of letter soups to a whole new level.", + 3f, "2022-06-07T08:09:10+06:00"), + }; + Result insertStatus = client.batch().objectsBatcher() + .withObjects(menuPizza) + .withObjects(menuSoup) + .run(); + assertNotNull(insertStatus); + assertNotNull(insertStatus.getResult()); + assertEquals(6, insertStatus.getResult().length); + } + + public void createWeaviateReplicatedTestSchemaFood(WeaviateClient client) { + // classes + WeaviateClass pizza = WeaviateClass.builder() + .className("Pizza") + .description("A delicious religion like food and arguably the best export of Italy.") + .invertedIndexConfig(InvertedIndexConfig.builder().indexTimestamps(true).build()) + .replicationConfig(ReplicationConfig.builder().factor(2).build()) + .build(); + WeaviateClass soup = WeaviateClass.builder() + .className("Soup") + .description("Mostly water based brew of sustenance for humans.") + .replicationConfig(ReplicationConfig.builder().factor(2).build()) + .build(); + // create Pizza class + Result pizzaCreateStatus = client.schema().classCreator().withClass(pizza).run(); + assertNotNull(pizzaCreateStatus); + assertTrue(pizzaCreateStatus.getResult()); + // create Soup class + Result soupCreateStatus = client.schema().classCreator().withClass(soup).run(); + assertNotNull(soupCreateStatus); + assertTrue(soupCreateStatus.getResult()); + // properties + Property nameProperty = Property.builder() + .dataType(Arrays.asList(DataType.STRING)) + .description("name") + .name("name") + .tokenization(Tokenization.FIELD) + .build(); + Property descriptionProperty = Property.builder() + .dataType(Arrays.asList(DataType.TEXT)) + .description("description") + .name("description") + .tokenization(Tokenization.WORD) + .build(); + Property bestBeforeProperty = Property.builder() + .dataType(Arrays.asList(DataType.DATE)) + .description("best before") + .name("bestBefore") + .build(); + Map text2vecContextionary = new HashMap<>(); + text2vecContextionary.put("skip", true); + Map moduleConfig = new HashMap<>(); + moduleConfig.put("text2vec-contextionary", text2vecContextionary); + Property priceProperty = Property.builder() + .dataType(Arrays.asList(DataType.NUMBER)) + .description("price") + .name("price") + .moduleConfig(moduleConfig) + .build(); + // Add name and description properties to Pizza + Result pizzaPropertyNameStatus = client.schema().propertyCreator() + .withProperty(nameProperty).withClassName(pizza.getClassName()).run(); + assertNotNull(pizzaPropertyNameStatus); + assertTrue(pizzaPropertyNameStatus.getResult()); + Result pizzaPropertyDescriptionStatus = client.schema().propertyCreator() + .withProperty(descriptionProperty).withClassName(pizza.getClassName()).run(); + assertNotNull(pizzaPropertyDescriptionStatus); + assertTrue(pizzaPropertyDescriptionStatus.getResult()); + Result pizzaPropertyBestBeforeStatus = client.schema().propertyCreator() + .withProperty(bestBeforeProperty).withClassName(pizza.getClassName()).run(); + assertNotNull(pizzaPropertyBestBeforeStatus); + assertTrue(pizzaPropertyBestBeforeStatus.getResult()); + Result pizzaPropertyPriceStatus = client.schema().propertyCreator() + .withProperty(priceProperty).withClassName(pizza.getClassName()).run(); + assertNotNull(pizzaPropertyPriceStatus); + assertTrue(pizzaPropertyPriceStatus.getResult()); + // Add name and description properties to Soup + Result soupPropertyNameStatus = client.schema().propertyCreator() + .withProperty(nameProperty).withClassName(soup.getClassName()).run(); + assertNotNull(soupPropertyNameStatus); + assertTrue(soupPropertyNameStatus.getResult()); + Result soupPropertyDescriptionStatus = client.schema().propertyCreator() + .withProperty(descriptionProperty).withClassName(soup.getClassName()).run(); + assertNotNull(soupPropertyDescriptionStatus); + assertTrue(soupPropertyDescriptionStatus.getResult()); + Result soupPropertyBestBeforeStatus = client.schema().propertyCreator() + .withProperty(bestBeforeProperty).withClassName(soup.getClassName()).run(); + assertNotNull(soupPropertyBestBeforeStatus); + assertTrue(soupPropertyBestBeforeStatus.getResult()); + Result soupPropertyPriceStatus = client.schema().propertyCreator() + .withProperty(priceProperty).withClassName(soup.getClassName()).run(); + assertNotNull(soupPropertyPriceStatus); + assertTrue(soupPropertyPriceStatus.getResult()); + } + private WeaviateObject createObject(String id, String className, String name, String description, Float price, String bestBeforeRfc3339) { return WeaviateObject.builder() .id(id) diff --git a/src/test/java/io/weaviate/integration/client/graphql/ClusterGraphQLTest.java b/src/test/java/io/weaviate/integration/client/graphql/ClusterGraphQLTest.java new file mode 100644 index 000000000..136d8a952 --- /dev/null +++ b/src/test/java/io/weaviate/integration/client/graphql/ClusterGraphQLTest.java @@ -0,0 +1,159 @@ +package io.weaviate.integration.client.graphql; + +import com.google.gson.internal.LinkedTreeMap; +import io.weaviate.client.Config; +import io.weaviate.client.WeaviateClient; +import io.weaviate.client.base.Result; +import io.weaviate.client.v1.data.replication.model.ConsistencyLevel; +import io.weaviate.client.v1.graphql.model.GraphQLResponse; +import io.weaviate.client.v1.graphql.query.fields.Field; +import io.weaviate.integration.client.WeaviateTestGenerics; +import org.junit.Before; +import org.junit.ClassRule; +import org.junit.Test; +import org.testcontainers.containers.DockerComposeContainer; +import org.testcontainers.containers.Network; +import org.testcontainers.containers.wait.strategy.Wait; + +import java.io.File; +import java.util.List; +import java.util.Map; + +import static org.junit.Assert.*; +import static org.junit.Assert.assertEquals; + +public class ClusterGraphQLTest { + private String address; + + @ClassRule + public static DockerComposeContainer compose = new DockerComposeContainer( + new File("src/test/resources/docker-compose-cluster.yaml") + ).withExposedService("weaviate-node-1_1", 8087, Wait.forHttp("/v1/.well-known/ready").forStatusCode(200)); + + @Before + public void before() { + String host = compose.getServiceHost("weaviate-node-1_1", 8087); + Integer port = compose.getServicePort("weaviate-node-1_1", 8087); + address = host + ":" + port; + } + + @Test + public void testGraphQLGetUsingConsistencyLevelAll() { + // given + Config config = new Config("http", address); + WeaviateClient client = new WeaviateClient(config); + WeaviateTestGenerics testGenerics = new WeaviateTestGenerics(); + Field name = Field.builder().name("name").build(); + Field _additional = Field.builder() + .name("_additional") + .fields(new Field[]{Field.builder().name("isConsistent").build()}) + .build(); + // when + testGenerics.createReplicatedTestSchemaAndData(client); + Result result = client.graphQL().get() + .withClassName("Pizza").withConsistencyLevel(ConsistencyLevel.ALL) + .withFields(name, _additional) + .run(); + // then + assertNotNull(result); + assertFalse(result.hasErrors()); + GraphQLResponse resp = result.getResult(); + assertNotNull(resp); + assertNotNull(resp.getData()); + assertTrue(resp.getData() instanceof Map); + Map data = (Map) resp.getData(); + assertNotNull(data.get("Get")); + assertTrue(data.get("Get") instanceof Map); + Map get = (Map) data.get("Get"); + assertNotNull(get.get("Pizza")); + assertTrue(get.get("Pizza") instanceof List); + List getPizza = (List) get.get("Pizza"); + for (Object pizza : getPizza) { + LinkedTreeMap pizzaMap = (LinkedTreeMap) pizza; + LinkedTreeMap additional = (LinkedTreeMap) pizzaMap.get("_additional"); + assertTrue((boolean) additional.get("isConsistent")); + } + + testGenerics.cleanupWeaviate(client); + } + + @Test + public void testGraphQLGetUsingConsistencyLevelQuorum() { + // given + Config config = new Config("http", address); + WeaviateClient client = new WeaviateClient(config); + WeaviateTestGenerics testGenerics = new WeaviateTestGenerics(); + Field name = Field.builder().name("name").build(); + Field _additional = Field.builder() + .name("_additional") + .fields(new Field[]{Field.builder().name("isConsistent").build()}) + .build(); + // when + testGenerics.createReplicatedTestSchemaAndData(client); + Result result = client.graphQL().get() + .withClassName("Pizza").withConsistencyLevel(ConsistencyLevel.QUORUM) + .withFields(name, _additional) + .run(); + // then + assertNotNull(result); + assertFalse(result.hasErrors()); + GraphQLResponse resp = result.getResult(); + assertNotNull(resp); + assertNotNull(resp.getData()); + assertTrue(resp.getData() instanceof Map); + Map data = (Map) resp.getData(); + assertNotNull(data.get("Get")); + assertTrue(data.get("Get") instanceof Map); + Map get = (Map) data.get("Get"); + assertNotNull(get.get("Pizza")); + assertTrue(get.get("Pizza") instanceof List); + List getPizza = (List) get.get("Pizza"); + for (Object pizza : getPizza) { + LinkedTreeMap pizzaMap = (LinkedTreeMap) pizza; + LinkedTreeMap additional = (LinkedTreeMap) pizzaMap.get("_additional"); + assertTrue((boolean) additional.get("isConsistent")); + } + + testGenerics.cleanupWeaviate(client); + } + + @Test + public void testGraphQLGetUsingConsistencyLevelOne() { + // given + Config config = new Config("http", address); + WeaviateClient client = new WeaviateClient(config); + WeaviateTestGenerics testGenerics = new WeaviateTestGenerics(); + Field name = Field.builder().name("name").build(); + Field _additional = Field.builder() + .name("_additional") + .fields(new Field[]{Field.builder().name("isConsistent").build()}) + .build(); + // when + testGenerics.createReplicatedTestSchemaAndData(client); + Result result = client.graphQL().get() + .withClassName("Pizza").withConsistencyLevel(ConsistencyLevel.ONE) + .withFields(name, _additional) + .run(); + // then + assertNotNull(result); + assertFalse(result.hasErrors()); + GraphQLResponse resp = result.getResult(); + assertNotNull(resp); + assertNotNull(resp.getData()); + assertTrue(resp.getData() instanceof Map); + Map data = (Map) resp.getData(); + assertNotNull(data.get("Get")); + assertTrue(data.get("Get") instanceof Map); + Map get = (Map) data.get("Get"); + assertNotNull(get.get("Pizza")); + assertTrue(get.get("Pizza") instanceof List); + List getPizza = (List) get.get("Pizza"); + for (Object pizza : getPizza) { + LinkedTreeMap pizzaMap = (LinkedTreeMap) pizza; + LinkedTreeMap additional = (LinkedTreeMap) pizzaMap.get("_additional"); + assertTrue((boolean) additional.get("isConsistent")); + } + + testGenerics.cleanupWeaviate(client); + } +} diff --git a/src/test/resources/docker-compose-cluster.yaml b/src/test/resources/docker-compose-cluster.yaml new file mode 100644 index 000000000..0e79f9181 --- /dev/null +++ b/src/test/resources/docker-compose-cluster.yaml @@ -0,0 +1,67 @@ +--- +version: '3.4' +services: + weaviate-node-1: + command: + - --host + - 0.0.0.0 + - --port + - '8087' + - --scheme + - http + - --write-timeout=600s + image: semitechnologies/weaviate:preview-gql-handler-consistency-level-integration-4a12f55 + restart: on-failure:0 + environment: + LOG_LEVEL: 'debug' + QUERY_DEFAULTS_LIMIT: 20 + AUTHENTICATION_ANONYMOUS_ACCESS_ENABLED: 'true' + PERSISTENCE_DATA_PATH: '/var/lib/weaviate' + CLUSTER_GOSSIP_BIND_PORT: "7110" + CLUSTER_DATA_BIND_PORT: "7111" + DEFAULT_VECTORIZER_MODULE: text2vec-contextionary + ENABLE_MODULES: text2vec-contextionary + CONTEXTIONARY_URL: contextionary:9999 + networks: + cluster-net: + + contextionary: + image: semitechnologies/contextionary:en0.16.0-v1.2.0 + environment: + OCCURRENCE_WEIGHT_LINEAR_FACTOR: 0.75 + EXTENSIONS_STORAGE_MODE: weaviate + EXTENSIONS_STORAGE_ORIGIN: http://weaviate-node-1:8087 + NEIGHBOR_OCCURRENCE_IGNORE_PERCENTILE: 5 + ENABLE_COMPOUND_SPLITTING: 'false' + networks: + cluster-net: + + weaviate-node-2: + init: true + command: + - --host + - 0.0.0.0 + - --port + - '8088' + - --scheme + - http + image: semitechnologies/weaviate:preview-gql-handler-consistency-level-integration-4a12f55 + restart: on-failure:0 + environment: + LOG_LEVEL: 'debug' + QUERY_DEFAULTS_LIMIT: 20 + AUTHENTICATION_ANONYMOUS_ACCESS_ENABLED: 'true' + PERSISTENCE_DATA_PATH: './weaviate-node-2' + CLUSTER_HOSTNAME: 'node2' + CLUSTER_GOSSIP_BIND_PORT: '7112' + CLUSTER_DATA_BIND_PORT: '7113' + CLUSTER_JOIN: 'weaviate-node-1:7110' + DEFAULT_VECTORIZER_MODULE: text2vec-contextionary + ENABLE_MODULES: text2vec-contextionary + CONTEXTIONARY_URL: contextionary:9999 + networks: + cluster-net: + +networks: + cluster-net: +...