From 89aedca29bd08415eb960a08ffb363744d57acc9 Mon Sep 17 00:00:00 2001 From: Arnaud Gourlay Date: Thu, 23 Apr 2026 18:34:50 +0200 Subject: [PATCH] Fix Openapi spec for new vector crud ops (#8779) * Fix Openapi spec for new vector crud ops * validate spec conformance in tests --- docs/redoc/master/openapi.json | 4 +- openapi/openapi-collections.ytt.yaml | 4 +- tests/openapi/test_named_vector_crud.py | 113 +++++++++++++++++------- 3 files changed, 84 insertions(+), 37 deletions(-) diff --git a/docs/redoc/master/openapi.json b/docs/redoc/master/openapi.json index f2a150f29f..c0b53ae50d 100644 --- a/docs/redoc/master/openapi.json +++ b/docs/redoc/master/openapi.json @@ -1893,7 +1893,7 @@ "example": "ok" }, "result": { - "type": "boolean" + "$ref": "#/components/schemas/UpdateResult" } } } @@ -2007,7 +2007,7 @@ "example": "ok" }, "result": { - "type": "boolean" + "$ref": "#/components/schemas/UpdateResult" } } } diff --git a/openapi/openapi-collections.ytt.yaml b/openapi/openapi-collections.ytt.yaml index 991281c565..0c80255e25 100644 --- a/openapi/openapi-collections.ytt.yaml +++ b/openapi/openapi-collections.ytt.yaml @@ -276,7 +276,7 @@ paths: application/json: schema: $ref: "#/components/schemas/VectorNameConfig" - responses: #@ response(type("boolean")) + responses: #@ response(reference("UpdateResult")) delete: tags: @@ -316,7 +316,7 @@ paths: schema: type: integer minimum: 1 - responses: #@ response(type("boolean")) + responses: #@ response(reference("UpdateResult")) /collections/{collection_name}/cluster: get: diff --git a/tests/openapi/test_named_vector_crud.py b/tests/openapi/test_named_vector_crud.py index f3d8c7117c..386ebff759 100644 --- a/tests/openapi/test_named_vector_crud.py +++ b/tests/openapi/test_named_vector_crud.py @@ -13,7 +13,11 @@ VECTOR_SIZE2 = 8 @pytest.fixture(autouse=True, scope="module") def setup(collection_name): # Drop if exists - requests.delete(f"{QDRANT_HOST}/collections/{collection_name}") + request_with_validation( + api='/collections/{collection_name}', + method="DELETE", + path_params={'collection_name': collection_name}, + ) # Create collection with two named vectors response = request_with_validation( @@ -32,7 +36,11 @@ def setup(collection_name): ) assert response.ok yield - requests.delete(f"{QDRANT_HOST}/collections/{collection_name}") + request_with_validation( + api='/collections/{collection_name}', + method="DELETE", + path_params={'collection_name': collection_name}, + ) def test_delete_recreate_vector_scroll(collection_name): @@ -64,18 +72,22 @@ def test_delete_recreate_vector_scroll(collection_name): ) assert response.ok - # Delete vec_a (use plain requests — OpenAPI response schema is mismatched) - response = requests.delete( - f"{QDRANT_HOST}/collections/{collection_name}/vectors/vec_a", - params={'wait': 'true'}, + # Delete vec_a + response = request_with_validation( + api='/collections/{collection_name}/vectors/{vector_name}', + method="DELETE", + path_params={'collection_name': collection_name, 'vector_name': 'vec_a'}, + query_params={'wait': 'true'}, ) assert response.ok # Recreate vec_a with different dimensions - response = requests.put( - f"{QDRANT_HOST}/collections/{collection_name}/vectors/vec_a", - params={'wait': 'true'}, - json={"dense": {"size": 6, "distance": "Dot"}}, + response = request_with_validation( + api='/collections/{collection_name}/vectors/{vector_name}', + method="PUT", + path_params={'collection_name': collection_name, 'vector_name': 'vec_a'}, + query_params={'wait': 'true'}, + body={"dense": {"size": 6, "distance": "Dot"}}, ) assert response.ok @@ -95,6 +107,11 @@ def test_delete_recreate_vector_scroll(collection_name): assert len(point['vector']['vec_b']) == VECTOR_SIZE2 +# Input-validation tests below intentionally use plain `requests` because +# `request_with_validation` would reject the body client-side (size violates +# the spec's minimum/maximum), preventing the server-side rejection from +# being exercised. + def test_create_vector_rejects_zero_size(collection_name): """size: 0 must be rejected at the API boundary, not reach the storage layer.""" response = requests.put( @@ -141,10 +158,16 @@ def _run_delete_recreate_scroll(wait_for_indexing: bool): num_points = 200 # Setup - requests.delete(f"{QDRANT_HOST}/collections/{coll}") - response = requests.put( - f"{QDRANT_HOST}/collections/{coll}", - json={ + request_with_validation( + api='/collections/{collection_name}', + method="DELETE", + path_params={'collection_name': coll}, + ) + response = request_with_validation( + api='/collections/{collection_name}', + method="PUT", + path_params={'collection_name': coll}, + body={ "vectors": { "vec_a": {"size": dim_a, "distance": "Cosine"}, "vec_b": {"size": dim_b, "distance": "Cosine"}, @@ -167,9 +190,12 @@ def _run_delete_recreate_scroll(wait_for_indexing: bool): } for i in range(num_points) ] - response = requests.put( - f"{QDRANT_HOST}/collections/{coll}/points?wait=true", - json={"points": points}, + response = request_with_validation( + api='/collections/{collection_name}/points', + method="PUT", + path_params={'collection_name': coll}, + query_params={'wait': 'true'}, + body={"points": points}, ) assert response.ok @@ -177,22 +203,30 @@ def _run_delete_recreate_scroll(wait_for_indexing: bool): wait_collection_green(coll) # Delete vec_a - response = requests.delete( - f"{QDRANT_HOST}/collections/{coll}/vectors/vec_a?wait=true", + response = request_with_validation( + api='/collections/{collection_name}/vectors/{vector_name}', + method="DELETE", + path_params={'collection_name': coll, 'vector_name': 'vec_a'}, + query_params={'wait': 'true'}, ) assert response.ok # Recreate vec_a with different dimensions - response = requests.put( - f"{QDRANT_HOST}/collections/{coll}/vectors/vec_a?wait=true", - json={"dense": {"size": dim_a_new, "distance": "Dot"}}, + response = request_with_validation( + api='/collections/{collection_name}/vectors/{vector_name}', + method="PUT", + path_params={'collection_name': coll, 'vector_name': 'vec_a'}, + query_params={'wait': 'true'}, + body={"dense": {"size": dim_a_new, "distance": "Dot"}}, ) assert response.ok # First scroll — must not panic - response = requests.post( - f"{QDRANT_HOST}/collections/{coll}/points/scroll", - json={"limit": 10, "with_vector": True}, + response = request_with_validation( + api='/collections/{collection_name}/points/scroll', + method="POST", + path_params={'collection_name': coll}, + body={"limit": 10, "with_vector": True}, ) assert response.ok result = response.json()['result'] @@ -213,16 +247,21 @@ def _run_delete_recreate_scroll(wait_for_indexing: bool): } for i in range(50) ] - response = requests.put( - f"{QDRANT_HOST}/collections/{coll}/points?wait=true", - json={"points": updated_points}, + response = request_with_validation( + api='/collections/{collection_name}/points', + method="PUT", + path_params={'collection_name': coll}, + query_params={'wait': 'true'}, + body={"points": updated_points}, ) assert response.ok # Second scroll — verify updated points have vec_a data - response = requests.post( - f"{QDRANT_HOST}/collections/{coll}/points/scroll", - json={"limit": num_points, "with_vector": True}, + response = request_with_validation( + api='/collections/{collection_name}/points/scroll', + method="POST", + path_params={'collection_name': coll}, + body={"limit": num_points, "with_vector": True}, ) assert response.ok result = response.json()['result'] @@ -238,14 +277,22 @@ def _run_delete_recreate_scroll(wait_for_indexing: bool): assert len(point['vector']['vec_a']) == dim_a_new # Cleanup - requests.delete(f"{QDRANT_HOST}/collections/{coll}") + request_with_validation( + api='/collections/{collection_name}', + method="DELETE", + path_params={'collection_name': coll}, + ) def wait_collection_green(collection_name, timeout=30): """Poll collection status until optimizer is idle.""" start = time.time() while time.time() - start < timeout: - r = requests.get(f"{QDRANT_HOST}/collections/{collection_name}") + r = request_with_validation( + api='/collections/{collection_name}', + method="GET", + path_params={'collection_name': collection_name}, + ) assert r.ok if r.json()['result']['status'] == 'green': return