feat(collections): create from and read raw schema JSON - #598
feat(collections): create from and read raw schema JSON#598dudanogueira wants to merge 1 commit into
Conversation
Add a raw-JSON escape hatch to the collections API, in both directions,
on the sync and async clients:
client.collections.createFromJson(json) // POST /schema
client.collections.getConfigAsJson(name) // GET /schema/{name}
client.collections.listAsJson() // GET /schema
Neither direction maps the document onto CollectionConfig. On write the
body is forwarded byte-for-byte, so any option the server accepts works,
including ones this client version does not model yet. On read the
response body is returned untouched.
The read side matters because CollectionConfig deserialization is lossy
today. Most visibly, VectorConfig.CustomTypeAdapterFactory looks for the
bq/pq/sq/rq keys at the top of vectorIndexConfig, which is correct for
hnsw and flat but not for dynamic, where they sit one level deeper under
hnsw/flat. A collection with a dynamic index and RQ enabled reports
quantization = null. Property-level moduleConfig, moduleConfig entries
that are neither reranker-* nor generative-*, and the read-only
shardingConfig fields are dropped as well. getConfigAsJson gives callers
that need the complete picture -- rendering or diffing a schema -- a way
to get it without waiting for the mapping to be fixed.
createFromJson reads exactly one key, "class", which it needs to return
a collection handle. A document that is not a JSON object, or that has
no non-empty string "class", raises IllegalArgumentException before the
request is sent. getConfigAsJson validates nothing and maps 404 to
Optional.empty(), matching the typed getConfig.
There was a problem hiding this comment.
Orca Security Scan Summary
| Status | Check | Issues by priority | |
|---|---|---|---|
| Infrastructure as Code | View in Orca | ||
| SAST | View in Orca | ||
| Secrets | View in Orca | ||
| Vulnerabilities | View in Orca |
|
I suggest a different approach, probably one that will result in better code-reuse / less duplication. The only difference between public record GetConfigRequest(String collectionName) {
public static final <T> Endpoint<GetConfigRequest, Optional<T>> endpoint(Class<T> cls) {
return OptionalEndpoint
.<GetConfigRequest, T>noBodyOptional(
request -> "GET",
request -> "/schema/" + request.collectionName,
request -> Collections.emptyMap(),
(statusCode, response) -> JSON.deserialize(response, cls));
}
}This way the same GetConfigRequest can be used to return:
Same for list and create endpoints. Would you like to take a stab at that? I'm happy to take this over, this seems like a small enough change. Wrt to the "Not addressed here" section: if you find any bugs while working on a PR, please open a ticket for those. A PR description is not the right place to document bugs. |
What
Adds a raw-JSON escape hatch to the collections API, in both directions, on the sync and async clients:
Neither direction maps the document onto
CollectionConfig. On write the body is forwarded byte-for-byte, so any option the server accepts works — including ones this client version does not model yet. On read the response body is returned untouched.Why the read side is here too
This started as write-only, but the read path turned out to need the same escape hatch:
CollectionConfigdeserialization is lossy today, so there is currently no way to render or diff a real collection faithfully using the client alone.The most visible case:
VectorConfig.CustomTypeAdapterFactory.readlooks for thebq/pq/sq/rqkeys at the top ofvectorIndexConfig. That is correct forhnswandflat, but adynamicindex nests them one level deeper underhnsw/flat. Verified against a live 1.38.0 collection with a dynamic index and RQ enabled:Same root cause loses
Dynamic.flat's own quantizers.Also dropped on read: property-level
moduleConfig(the server's actual home forskip/vectorizePropertyName),moduleConfigentries that are neitherreranker-*norgenerative-*, the read-onlyshardingConfigfields (actualCount,function,key,strategy), andvectorIndexConfig.distanceat the dynamic level.getConfigAsJsonunblocks callers who need the complete picture without waiting for the mapping to be fixed.Design notes
createFromJsonreads exactly one key,"class", which it needs in order to return a collection handle. A document that is not a JSON object, or that has no non-empty string"class", raisesIllegalArgumentExceptionbefore the request is sent rather than after a round-trip.getConfigAsJsonvalidates nothing — there is no name to extract — and maps 404 toOptional.empty(), matching the typedgetConfig.create(String)already means "create by collection name", so the new method could not be an overload; hence the distinct names.Not addressed here
Two parsing bugs found while investigating, left for follow-ups so this PR stays additive:
dynamic,hnswandflatcan carry different quantizers, butVectorConfighas a singlequantization()slot.baseURLvsbaseUrl. 28 classes use@SerializedName("baseURL"). Probing the server directly, sendingbaseURLgets echoed back as an unrecognized passthrough key alongside the module's realbaseUrldefault, while sendingbaseUrlactually takes effect — so the base URL is neither written effectively nor read back.rerankers/NvidiaReranker.javaalready usesbaseUrl. Onlytext2vec-coherewas verified empirically; the canonical key should be confirmed per module before any blanket rename.Testing
createFromJsonvalidation path.CollectionsITestexercising create-from-JSON →getConfigAsJson→listAsJson→ delete, using a dynamic-index-with-RQ payload.Full unit suite passes. The integration tests could not run locally (testcontainers fails to initialize in my environment — all pre-existing ITs error the same way), so the new integration test logic was additionally verified by running an identical copy against a live Weaviate 1.38.0: the collection was created from raw JSON,
getConfigAsJsonreturnedhnsw.rq.enabled = trueintact,listAsJsonsaw it, and the post-delete lookup returned empty. CI will be the real check on the ITs.