Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 62 additions & 0 deletions src/it/java/io/weaviate/integration/CollectionsITest.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
import org.assertj.core.api.InstanceOfAssertFactories;
import org.junit.Test;

import com.google.gson.JsonParser;

import io.weaviate.ConcurrentTest;
import io.weaviate.client6.v1.api.WeaviateApiException;
import io.weaviate.client6.v1.api.WeaviateClient;
Expand Down Expand Up @@ -63,6 +65,66 @@ public void testCreateGetDelete() throws IOException {
Assertions.assertThat(noCollection).as("after delete").isEmpty();
}

@Test
public void testCreateFromJsonAndGetConfigAsJson() throws IOException {
var collectionName = ns("Things");

// Act: create from a raw schema document, including a quantizer nested inside a
// "dynamic" index -- configuration CollectionConfig cannot currently round-trip.
client.collections.createFromJson("""
{
"class": "%s",
"description": "created from raw JSON",
"properties": [
{ "name": "username", "dataType": ["text"] },
{ "name": "age", "dataType": ["int"] }
],
"vectorConfig": {
"default": {
"vectorIndexType": "dynamic",
"vectorizer": { "none": {} },
"vectorIndexConfig": {
"threshold": 10000,
"hnsw": { "rq": { "enabled": true, "bits": 8 } }
}
}
}
}
""".formatted(collectionName));

// Assert: the server accepted every option, including the ones the typed API
// would have dropped on the way out.
var raw = client.collections.getConfigAsJson(collectionName);
Assertions.assertThat(raw).as("raw schema").get(InstanceOfAssertFactories.STRING)
.contains(collectionName)
.contains("created from raw JSON");

var document = JsonParser.parseString(raw.get()).getAsJsonObject();
Assertions.assertThat(document.get("class").getAsString()).isEqualTo(collectionName);
Assertions.assertThat(document.getAsJsonObject("vectorConfig")
.getAsJsonObject("default").get("vectorIndexType").getAsString())
.as("dynamic index").isEqualTo("dynamic");
Assertions.assertThat(document.getAsJsonObject("vectorConfig")
.getAsJsonObject("default").getAsJsonObject("vectorIndexConfig")
.getAsJsonObject("hnsw").getAsJsonObject("rq").get("enabled").getAsBoolean())
.as("rq survives the raw round-trip").isTrue();

// Assert: listAsJson sees it too.
Assertions.assertThat(client.collections.listAsJson())
.as("full schema").contains(collectionName);

// Assert: a missing collection is empty rather than an error.
client.collections.delete(collectionName);
Assertions.assertThat(client.collections.getConfigAsJson(collectionName))
.as("after delete").isEmpty();
}

@Test
public void testCreateFromJsonRejectsDocumentWithoutClass() {
Assertions.assertThatThrownBy(() -> client.collections.createFromJson("{ \"description\": \"no name\" }"))
.as("no \"class\" key").isInstanceOf(IllegalArgumentException.class);
}

@Test
public void testCrossReferences() throws IOException {
// Arrange: Create Owners collection
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,82 @@

import java.util.Collections;

import com.google.gson.JsonElement;
import com.google.gson.JsonParseException;

import io.weaviate.client6.v1.internal.json.JSON;
import io.weaviate.client6.v1.internal.rest.Endpoint;
import io.weaviate.client6.v1.internal.rest.SimpleEndpoint;

public record CreateCollectionRequest(CollectionConfig collection) {
public static final Endpoint<CreateCollectionRequest, CollectionConfig> _ENDPOINT = new SimpleEndpoint<>(
request -> "POST",
request -> "/schema/",
request -> Collections.emptyMap(),
request -> JSON.serialize(request.collection),
(statusCode, response) -> JSON.deserialize(response, CollectionConfig.class));
/**
* Create a collection from a {@link CollectionConfig} or from a raw JSON schema
* definition.
*
* @param <T> Type of the payload. A {@code String} is sent verbatim, anything
* else is serialized first.
*/
public record CreateCollectionRequest<T>(T collection) {
/**
* Endpoint which sends the payload to {@code POST /schema}.
*
* <p>
* A {@code String} payload is forwarded byte-for-byte, so it may use any
* option the server accepts &mdash; including ones this client version does not
* model. Nothing is validated client-side either: a typo in a key surfaces as a
* server error rather than a compile error.
*
* <p>
* The server echoes the stored configuration back, which this endpoint discards
* &mdash; a raw payload may well describe a collection that
* {@link CollectionConfig} cannot represent.
*/
public static <T> Endpoint<CreateCollectionRequest<T>, Void> endpoint() {
return SimpleEndpoint.sideEffect(
request -> "POST",
request -> "/schema/",
request -> Collections.emptyMap(),
request -> request.collection instanceof String json
? json
: JSON.serialize(request.collection));
}

/**
* Name of the collection defined by a raw JSON document (its {@code "class"}
* key), which the client needs in order to return a handle for it.
*
* <p>
* This is the only part of the document the client reads. Calling it before
* sending doubles as validation: a document that is not usable as a
* {@code POST /schema} payload fails before the request leaves the process
* rather than after a round-trip.
*
* @throws IllegalArgumentException in case the string is not a JSON object or
* does not carry a {@code "class"} name.
*/
public static String collectionNameFromJson(String json) {
if (json == null || json.isBlank()) {
throw new IllegalArgumentException("collection JSON must not be null or blank");
}

JsonElement document;
try {
document = JSON.toJsonElement(json);
} catch (JsonParseException e) {
throw new IllegalArgumentException("collection JSON is not valid JSON", e);
}

if (!document.isJsonObject()) {
throw new IllegalArgumentException("collection JSON must be a JSON object");
}

var collectionName = document.getAsJsonObject().get("class");
if (collectionName == null
|| !collectionName.isJsonPrimitive()
|| !collectionName.getAsJsonPrimitive().isString()
|| collectionName.getAsString().isBlank()) {
throw new IllegalArgumentException(
"collection JSON must have a non-empty string \"class\" key with the collection name");
}
return collectionName.getAsString();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,24 @@
import java.util.Collections;
import java.util.Optional;

import io.weaviate.client6.v1.internal.json.JSON;
import io.weaviate.client6.v1.internal.rest.Endpoint;
import io.weaviate.client6.v1.internal.rest.OptionalEndpoint;

public record GetConfigRequest(String collectionName) {
public static final Endpoint<GetConfigRequest, Optional<CollectionConfig>> _ENDPOINT = OptionalEndpoint
.noBodyOptional(
request -> "GET",
request -> "/schema/" + request.collectionName,
request -> Collections.emptyMap(),
(statusCode, response) -> JSON.deserialize(response, CollectionConfig.class));
/**
* Endpoint which deserializes the schema document into {@code cls}.
*
* <p>
* Pass {@code String.class} to get the response body exactly as the server sent
* it. {@link CollectionConfig} only carries what this client version models, so
* the raw document is what you want when rendering or diffing a schema rather
* than reading individual settings.
*/
public static <T> Endpoint<GetConfigRequest, Optional<T>> endpoint(Class<T> cls) {
return OptionalEndpoint.noBodyOptional(
request -> "GET",
request -> "/schema/" + request.collectionName,
request -> Collections.emptyMap(),
cls);
}
}
Original file line number Diff line number Diff line change
@@ -1,16 +1,26 @@
package io.weaviate.client6.v1.api.collections;

import java.util.Collections;
import java.util.List;

import io.weaviate.client6.v1.internal.json.JSON;
import io.weaviate.client6.v1.internal.rest.Endpoint;
import io.weaviate.client6.v1.internal.rest.SimpleEndpoint;

public record ListCollectionRequest() {
public static final Endpoint<ListCollectionRequest, List<CollectionConfig>> _ENDPOINT = SimpleEndpoint.noBody(
request -> "GET",
request -> "/schema",
request -> Collections.emptyMap(),
(gson, response) -> JSON.deserialize(response, ListCollectionResponse.class).collections());
/**
* Endpoint which deserializes the schema document into {@code cls}.
*
* <p>
* Pass {@link ListCollectionResponse} to get the collections mapped onto
* {@link CollectionConfig}, or {@code String.class} for the response body
* exactly as the server sent it.
*
* @see GetConfigRequest#endpoint(Class)
*/
public static <T> Endpoint<ListCollectionRequest, T> endpoint(Class<T> cls) {
return SimpleEndpoint.noBody(
request -> "GET",
request -> "/schema",
request -> Collections.emptyMap(),
cls);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -179,11 +179,59 @@ public CollectionHandle<Map<String, Object>> create(String collectionName,
* or the server being unavailable.
*/
public CollectionHandle<Map<String, Object>> create(CollectionConfig collection) throws IOException {
this.restTransport.performRequest(new CreateCollectionRequest(collection),
CreateCollectionRequest._ENDPOINT);
this.restTransport.performRequest(new CreateCollectionRequest<>(collection),
CreateCollectionRequest.<CollectionConfig>endpoint());
return use(collection.collectionName());
}

/**
* Create a new Weaviate collection from a raw JSON schema definition.
*
* <p>
* The document is forwarded to the server verbatim, so it may use any option
* the server accepts &mdash; including ones this client version does not model.
* Nothing is validated client-side: invalid configuration surfaces as a
* {@link WeaviateApiException}. Use {@link #create(CollectionConfig)} when you
* want the type-checked builder API instead.
*
* <pre>{@code
* client.collections.createFromJson("""
* {
* "class": "Song",
* "properties": [
* { "name": "title", "dataType": ["text"] },
* { "name": "yearReleased", "dataType": ["int"] }
* ],
* "vectorConfig": {
* "default": {
* "vectorizer": { "none": {} },
* "vectorIndexType": "hnsw"
* }
* }
* }
* """);
* }</pre>
*
* @param json JSON object describing the collection. Must contain a
* {@code "class"} key with the collection name.
* @return Handle for the created collection.
* @throws IllegalArgumentException in case the string is not a JSON object or
* does not carry a {@code "class"} name.
* @throws WeaviateApiException in case the server returned with an
* error status code.
* @throws IOException in case the request was not sent
* successfully due to a malformed request, a
* networking error or the server being
* unavailable.
*/
public CollectionHandle<Map<String, Object>> createFromJson(String json) throws IOException {
// Read the name -- and reject an unusable document -- before sending anything.
var collectionName = CreateCollectionRequest.collectionNameFromJson(json);
this.restTransport.performRequest(new CreateCollectionRequest<>(json),
CreateCollectionRequest.<String>endpoint());
return use(collectionName);
}

/**
* Fetch Weaviate collection configuration.
*
Expand All @@ -196,7 +244,48 @@ public CollectionHandle<Map<String, Object>> create(CollectionConfig collection)
* or the server being unavailable.
*/
public Optional<CollectionConfig> getConfig(String collectionName) throws IOException {
return this.restTransport.performRequest(new GetConfigRequest(collectionName), GetConfigRequest._ENDPOINT);
return this.restTransport.performRequest(new GetConfigRequest(collectionName),
GetConfigRequest.endpoint(CollectionConfig.class));
}

/**
* Fetch a collection's schema exactly as the server returned it.
*
* <p>
* {@link #getConfig(String)} maps the response onto {@link CollectionConfig}
* and therefore drops anything this client version does not model. Use this
* method when you need the complete picture &mdash; rendering a schema,
* diffing two collections, or reading a module option that has no typed
* accessor yet.
*
* @param collectionName Collection name.
* @return the raw JSON body if a collection with this name exists.
* @throws WeaviateApiException in case the server returned with an
* error status code.
* @throws IOException in case the request was not sent successfully
* due to a malformed request, a networking error
* or the server being unavailable.
* @see #createFromJson(String)
*/
public Optional<String> getConfigAsJson(String collectionName) throws IOException {
return this.restTransport.performRequest(new GetConfigRequest(collectionName),
GetConfigRequest.endpoint(String.class));
}

/**
* Fetch the schema of all collections exactly as the server returned it.
*
* @return the raw JSON body, an object with a {@code "classes"} array.
* @throws WeaviateApiException in case the server returned with an
* error status code.
* @throws IOException in case the request was not sent successfully
* due to a malformed request, a networking error
* or the server being unavailable.
* @see #getConfigAsJson(String)
*/
public String listAsJson() throws IOException {
return this.restTransport.performRequest(new ListCollectionRequest(),
ListCollectionRequest.endpoint(String.class));
}

/**
Expand All @@ -210,7 +299,8 @@ public Optional<CollectionConfig> getConfig(String collectionName) throws IOExce
* or the server being unavailable.
*/
public List<CollectionConfig> list() throws IOException {
return this.restTransport.performRequest(new ListCollectionRequest(), ListCollectionRequest._ENDPOINT);
return this.restTransport.performRequest(new ListCollectionRequest(),
ListCollectionRequest.endpoint(ListCollectionResponse.class)).collections();
}

/**
Expand Down
Loading
Loading