diff --git a/.github/workflows/golangci-lint.yaml b/.github/workflows/golangci-lint.yaml index c44c8f82..beec8961 100644 --- a/.github/workflows/golangci-lint.yaml +++ b/.github/workflows/golangci-lint.yaml @@ -23,4 +23,4 @@ jobs: go-version: stable - uses: golangci/golangci-lint-action@v9 with: - version: v2.8.0 + version: v2.9.0 diff --git a/api/proto/v1/base_search.proto b/api/proto/v1/base_search.proto index 55732017..28a0a992 100644 --- a/api/proto/v1/base_search.proto +++ b/api/proto/v1/base_search.proto @@ -50,7 +50,7 @@ message Hybrid { repeated string properties = 2; // protolint:disable:next REPEATED_FIELD_NAMES_PLURALIZED repeated float vector = 3 [deprecated = true]; // will be removed in the future, use vectors - float alpha = 4; + float alpha = 4 [deprecated = true]; // deprecated in 1.36.0 - use alpha_param enum FusionType { FUSION_TYPE_UNSPECIFIED = 0; FUSION_TYPE_RANKED = 1; @@ -63,6 +63,7 @@ message Hybrid { NearVector near_vector = 9; // same as above. Use the target vector in the hybrid message Targets targets = 10; optional SearchOperatorOptions bm25_search_operator = 11; + optional float alpha_param = 12; // only vector distance, but keep it extendable oneof threshold { diff --git a/api/proto/v1/batch.proto b/api/proto/v1/batch.proto index d53cd6f9..f18bf9c9 100644 --- a/api/proto/v1/batch.proto +++ b/api/proto/v1/batch.proto @@ -47,11 +47,12 @@ message BatchStreamReply { } message ShuttingDown { } - message Shutdown { - } message OutOfMemory{ repeated string uuids = 1; repeated string beacons = 2; + // How long to wait until ShuttingDown is sent, in seconds + // If ShuttingDown is not set by this time, the client should exit the stream + int32 wait_time = 3; } message Backoff { int32 batch_size = 1; @@ -80,12 +81,13 @@ message BatchStreamReply { oneof message { Results results = 1; ShuttingDown shutting_down = 2; - Shutdown shutdown = 3; Started started = 4; Backoff backoff = 5; Acks acks = 6; OutOfMemory out_of_memory = 7; } + reserved 3; // was shutdown + reserved "shutdown"; } message BatchObject { diff --git a/api/rest/schema.check.json b/api/rest/schema.check.json index 53393986..71f53a2e 100644 --- a/api/rest/schema.check.json +++ b/api/rest/schema.check.json @@ -618,6 +618,154 @@ }, "type": "object" }, + "ExportCreateRequest": { + "description": "Request to create a new export operation", + "type": "object", + "required": ["id"], + "properties": { + "id": { + "type": "string", + "description": "Unique identifier for this export. Must be URL-safe." + }, + "include": { + "type": "array", + "items": { + "type": "string" + }, + "description": "List of collection names to include in the export. Cannot be used with 'exclude'." + }, + "exclude": { + "type": "array", + "items": { + "type": "string" + }, + "description": "List of collection names to exclude from the export. Cannot be used with 'include'." + }, + "config": { + "type": "object", + "description": "Backend-specific configuration", + "properties": { + "bucket": { + "type": "string", + "description": "Bucket, container, or volume name for cloud storage backends" + }, + "path": { + "type": "string", + "description": "Path prefix within the bucket or filesystem" + } + } + } + } + }, + "ExportCreateResponse": { + "description": "Response from creating an export operation", + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "Unique identifier for this export" + }, + "backend": { + "type": "string", + "description": "The backend storage system used" + }, + "path": { + "type": "string", + "description": "Full path where the export is being written" + }, + "status": { + "type": "string", + "description": "Current status of the export", + "enum": ["STARTED"] + }, + "startedAt": { + "type": "string", + "format": "date-time", + "description": "When the export started" + }, + "classes": { + "type": "array", + "items": { + "type": "string" + }, + "description": "List of collections being exported" + } + } + }, + "ExportStatusResponse": { + "description": "Current status of an export operation", + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "Unique identifier for this export" + }, + "backend": { + "type": "string", + "description": "The backend storage system used" + }, + "path": { + "type": "string", + "description": "Full path where the export is stored" + }, + "status": { + "type": "string", + "description": "Current status of the export", + "enum": ["STARTED", "TRANSFERRING", "SUCCESS", "FAILED"] + }, + "startedAt": { + "type": "string", + "format": "date-time", + "description": "When the export started" + }, + "tookInMs": { + "type": "integer", + "format": "int64", + "description": "Duration of the export in milliseconds" + }, + "classes": { + "type": "array", + "items": { + "type": "string" + }, + "description": "List of collections in this export" + }, + "shardStatus": { + "type": "object", + "additionalProperties": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/ShardProgress" + } + }, + "description": "Per-shard progress: className -> shardName -> status" + }, + "error": { + "type": "string", + "description": "Error message if export failed" + } + } + }, + "ShardProgress": { + "description": "Progress information for exporting a single shard", + "type": "object", + "properties": { + "status": { + "type": "string", + "description": "Status of this shard's export", + "enum": ["STARTED", "TRANSFERRING", "SUCCESS", "FAILED"] + }, + "objectsExported": { + "type": "integer", + "format": "int64", + "description": "Number of objects exported from this shard" + }, + "error": { + "type": "string", + "description": "Error message if this shard's export failed" + } + } + }, "GraphQLError": { "description": "An error response caused by a GraphQL query.", "properties": { @@ -1589,6 +1737,12 @@ }, "type": "array", "x-omitempty": true + }, + "disableDuplicatedReferences": { + "description": "If set to false, allows multiple references to the same target object within this property. Setting it to true will enforce uniqueness of references within this property. By default, this is set to true.", + "type": "boolean", + "x-nullable": true, + "default": true } }, "type": "object" @@ -1722,8 +1876,10 @@ "STARTED", "TRANSFERRING", "TRANSFERRED", + "FINALIZING", "SUCCESS", "FAILED", + "CANCELLING", "CANCELED" ] }, @@ -1771,8 +1927,10 @@ "STARTED", "TRANSFERRING", "TRANSFERRED", + "FINALIZING", "SUCCESS", "FAILED", + "CANCELLING", "CANCELED" ] } @@ -1894,6 +2052,12 @@ "items": { "type": "string" } + }, + "incremental_base_backup_id": { + "description": "The ID of an existing backup to use as the base for a file-based incremental backup. If set, only files that have changed since the base backup will be included in the new backup.", + "type": "string", + "x-nullable": true, + "default": null } } }, @@ -1935,8 +2099,10 @@ "STARTED", "TRANSFERRING", "TRANSFERRED", + "FINALIZING", "SUCCESS", "FAILED", + "CANCELLING", "CANCELED" ] } @@ -1966,8 +2132,10 @@ "STARTED", "TRANSFERRING", "TRANSFERRED", + "FINALIZING", "SUCCESS", "FAILED", + "CANCELLING", "CANCELED" ] }, @@ -2058,8 +2226,10 @@ "STARTED", "TRANSFERRING", "TRANSFERRED", + "FINALIZING", "SUCCESS", "FAILED", + "CANCELLING", "CANCELED" ] } @@ -3339,7 +3509,7 @@ }, "description": "# Introduction
Weaviate is an open source, AI-native vector database that helps developers create intuitive and reliable AI-powered applications.
### Base Path
The base path for the Weaviate server is structured as `[YOUR-WEAVIATE-HOST]:[PORT]/v1`. As an example, if you wish to access the `schema` endpoint on a local instance, you would navigate to `http://localhost:8080/v1/schema`. Ensure you replace `[YOUR-WEAVIATE-HOST]` and `[PORT]` with your actual server host and port number respectively.
### Questions?
If you have any comments or questions, please feel free to reach out to us at the community forum [https://forum.weaviate.io/](https://forum.weaviate.io/).
### Issues?
If you find a bug or want to file a feature request, please open an issue on our GitHub repository for [Weaviate](https://github.com/weaviate/weaviate).
### Need more documentation?
For a quickstart, code examples, concepts and more, please visit our [documentation page](https://docs.weaviate.io/weaviate).", "title": "Weaviate REST API", - "version": "1.36.0-dev" + "version": "1.37.0-dev" }, "parameters": { "CommonAfterParameterQuery": { @@ -7895,6 +8065,73 @@ } } }, + "/schema/{className}/properties/{propertyName}/index/{indexName}": { + "delete": { + "summary": "Delete a property's inverted index", + "description": "Deletes an inverted index of a specific property within a collection (`className`). The index to delete is identified by `indexName` and must be one of `filterable`, `searchable`, or `rangeFilters`.", + "operationId": "schema.objects.properties.delete", + "x-serviceIds": [ + "weaviate.local.manipulate.meta" + ], + "tags": [ + "schema" + ], + "parameters": [ + { + "name": "className", + "description": "The name of the collection (class) containing the property.", + "in": "path", + "required": true, + "type": "string" + }, + { + "name": "propertyName", + "description": "The name of the property whose inverted index should be deleted.", + "in": "path", + "required": true, + "type": "string" + }, + { + "name": "indexName", + "description": "The name of the inverted index to delete from the property.", + "in": "path", + "required": true, + "type": "string", + "enum": [ + "filterable", + "searchable", + "rangeFilters" + ] + } + ], + "responses": { + "200": { + "description": "Index deleted successfully." + }, + "401": { + "description": "Unauthorized or invalid credentials." + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/ErrorResponse" + } + }, + "422": { + "description": "Invalid index, property or collection provided.", + "schema": { + "$ref": "#/definitions/ErrorResponse" + } + }, + "500": { + "description": "An error occurred while deleting the index. Check the ErrorResponse for details.", + "schema": { + "$ref": "#/definitions/ErrorResponse" + } + } + } + } + }, "/schema/{className}/shards": { "get": { "summary": "Get the shards status of a collection", @@ -9126,6 +9363,137 @@ } } }, + "/export/{backend}": { + "post": { + "summary": "Start a new export", + "description": "Initiates an export operation that writes collections to Parquet files on the specified backend storage (S3, GCS, Azure, or filesystem). Each collection is exported to a separate Parquet file.", + "operationId": "export.create", + "x-serviceIds": [ + "weaviate.local.export.create" + ], + "tags": [ + "export" + ], + "parameters": [ + { + "name": "backend", + "in": "path", + "required": true, + "type": "string", + "description": "The backend storage system to use for the export (e.g., `filesystem`, `gcs`, `s3`, `azure`)." + }, + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/ExportCreateRequest" + } + } + ], + "responses": { + "200": { + "description": "Successfully started export operation", + "schema": { + "$ref": "#/definitions/ExportCreateResponse" + } + }, + "401": { + "description": "Unauthorized or invalid credentials" + }, + "403": { + "description": "Forbidden - insufficient permissions", + "schema": { + "$ref": "#/definitions/ErrorResponse" + } + }, + "422": { + "description": "Invalid export request", + "schema": { + "$ref": "#/definitions/ErrorResponse" + } + }, + "500": { + "description": "Internal server error occurred while starting export", + "schema": { + "$ref": "#/definitions/ErrorResponse" + } + } + } + } + }, + "/export/{backend}/{id}": { + "get": { + "summary": "Get export status", + "description": "Retrieves the current status of an export operation, including progress for each collection being exported.", + "operationId": "export.status", + "x-serviceIds": [ + "weaviate.local.export.status" + ], + "tags": [ + "export" + ], + "parameters": [ + { + "name": "backend", + "in": "path", + "required": true, + "type": "string", + "description": "The backend storage system where the export is stored." + }, + { + "name": "id", + "in": "path", + "required": true, + "type": "string", + "description": "The unique identifier of the export." + }, + { + "name": "bucket", + "in": "query", + "required": false, + "type": "string", + "description": "Optional bucket name where the export is stored. If not specified, uses the backend's default bucket." + }, + { + "name": "path", + "in": "query", + "required": false, + "type": "string", + "description": "Optional path prefix within the bucket. If not specified, uses the backend's default path." + } + ], + "responses": { + "200": { + "description": "Successfully retrieved export status", + "schema": { + "$ref": "#/definitions/ExportStatusResponse" + } + }, + "401": { + "description": "Unauthorized or invalid credentials" + }, + "403": { + "description": "Forbidden - insufficient permissions", + "schema": { + "$ref": "#/definitions/ErrorResponse" + } + }, + "404": { + "description": "Export not found", + "schema": { + "$ref": "#/definitions/ErrorResponse" + } + }, + "500": { + "description": "Internal server error occurred while retrieving export status", + "schema": { + "$ref": "#/definitions/ErrorResponse" + } + } + } + } + }, "/cluster/statistics": { "get": { "summary": "Get cluster statistics", @@ -9465,6 +9833,10 @@ "name": "backups", "description": "Operations related to creating and managing backups of Weaviate data. This feature allows you to create snapshots of your collections and store them on external storage backends such as cloud object storage (S3, GCS, Azure) or a shared filesystem. These endpoints enable you to initiate backup and restore processes, monitor their status, list available backups on a backend, and delete unwanted backups. Backups are essential for disaster recovery, data migration, and maintaining point-in-time copies of your vector database." }, + { + "name": "exports", + "description": "Operations for exporting Weaviate data to Parquet format on external storage backends (S3, GCS, Azure, or filesystem). Exports provide a way to extract your vector data and object properties into a standardized columnar format for data analysis, archival, or migration. Each collection is exported to a separate Parquet file containing object IDs, vectors, properties, and metadata." + }, { "name": "users", "description": "Endpoints for user account management in Weaviate. This includes operations specific to Weaviate-managed database users (`db` users), such as creation (which generates an API key), listing, deletion, activation/deactivation, and API key rotation. It also provides operations applicable to any authenticated user (`db` or `oidc`), like retrieving their own information (username and assigned roles).

**User Types:**
* **`db` users:** Managed entirely within Weaviate (creation, deletion, API keys). Use these endpoints for full lifecycle management.
* **`oidc` users:** Authenticated via an external OpenID Connect provider. Their lifecycle (creation, credentials) is managed externally, but their role assignments *within Weaviate* are managed via the `authz` endpoints." diff --git a/api/rest/schema.v3.yaml b/api/rest/schema.v3.yaml index 0ca369c6..3e549c54 100644 --- a/api/rest/schema.v3.yaml +++ b/api/rest/schema.v3.yaml @@ -181,6 +181,11 @@ components: items: type: string type: array + incremental_base_backup_id: + default: "null" + description: The ID of an existing backup to use as the base for a file-based incremental backup. If set, only files that have changed since the base backup will be included in the new backup. + nullable: true + type: string type: object BackupCreateResponse: description: The definition of a backup create response body @@ -212,8 +217,10 @@ components: - STARTED - TRANSFERRING - TRANSFERRED + - FINALIZING - SUCCESS - FAILED + - CANCELLING - CANCELED type: string type: object @@ -251,8 +258,10 @@ components: - STARTED - TRANSFERRING - TRANSFERRED + - FINALIZING - SUCCESS - FAILED + - CANCELLING - CANCELED type: string type: object @@ -286,8 +295,10 @@ components: - STARTED - TRANSFERRING - TRANSFERRED + - FINALIZING - SUCCESS - FAILED + - CANCELLING - CANCELED type: string type: object @@ -343,8 +354,10 @@ components: - STARTED - TRANSFERRING - TRANSFERRED + - FINALIZING - SUCCESS - FAILED + - CANCELLING - CANCELED type: string type: object @@ -370,8 +383,10 @@ components: - STARTED - TRANSFERRING - TRANSFERRED + - FINALIZING - SUCCESS - FAILED + - CANCELLING - CANCELED type: string type: object @@ -878,6 +893,106 @@ components: type: object type: array type: object + ExportCreateRequest: + description: Request to create a new export operation + properties: + config: + description: Backend-specific configuration + properties: + bucket: + description: Bucket, container, or volume name for cloud storage backends + type: string + path: + description: Path prefix within the bucket or filesystem + type: string + type: object + exclude: + description: List of collection names to exclude from the export. Cannot be used with 'include'. + items: + type: string + type: array + id: + description: Unique identifier for this export. Must be URL-safe. + type: string + include: + description: List of collection names to include in the export. Cannot be used with 'exclude'. + items: + type: string + type: array + required: + - id + type: object + ExportCreateResponse: + description: Response from creating an export operation + properties: + backend: + description: The backend storage system used + type: string + classes: + description: List of collections being exported + items: + type: string + type: array + id: + description: Unique identifier for this export + type: string + path: + description: Full path where the export is being written + type: string + startedAt: + description: When the export started + format: date-time + type: string + status: + description: Current status of the export + enum: + - STARTED + type: string + type: object + ExportStatusResponse: + description: Current status of an export operation + properties: + backend: + description: The backend storage system used + type: string + classes: + description: List of collections in this export + items: + type: string + type: array + error: + description: Error message if export failed + type: string + id: + description: Unique identifier for this export + type: string + path: + description: Full path where the export is stored + type: string + shardStatus: + additionalProperties: + additionalProperties: + $ref: '#/components/schemas/ShardProgress' + type: object + description: 'Per-shard progress: className -> shardName -> status' + type: object + startedAt: + description: When the export started + format: date-time + type: string + status: + description: Current status of the export + enum: + - STARTED + - TRANSFERRING + - SUCCESS + - FAILED + type: string + tookInMs: + description: Duration of the export in milliseconds + format: int64 + type: integer + type: object GeoCoordinates: properties: latitude: @@ -1563,6 +1678,11 @@ components: description: description: Description of the property. type: string + disableDuplicatedReferences: + default: true + description: If set to false, allows multiple references to the same target object within this property. Setting it to true will enforce uniqueness of references within this property. By default, this is set to true. + nullable: true + type: boolean indexFilterable: description: 'Whether to include this property in the filterable, Roaring Bitmap index. If `false`, this property cannot be used in `where` filters.

Note: Unrelated to vectorization behavior.' nullable: true @@ -2179,6 +2299,25 @@ components: SchemaHistory: description: This is an open object, with OpenAPI Specification 3.0 this will be more detailed. See Weaviate docs for more info. In the future this will become a key/value OR a SingleRef definition. type: object + ShardProgress: + description: Progress information for exporting a single shard + properties: + error: + description: Error message if this shard's export failed + type: string + objectsExported: + description: Number of objects exported from this shard + format: int64 + type: integer + status: + description: Status of this shard's export + enum: + - STARTED + - TRANSFERRING + - SUCCESS + - FAILED + type: string + type: object ShardStatus: description: The status of a single shard properties: @@ -2547,7 +2686,7 @@ info: url: https://github.com/weaviate description: '# Introduction
Weaviate is an open source, AI-native vector database that helps developers create intuitive and reliable AI-powered applications.
### Base Path
The base path for the Weaviate server is structured as `[YOUR-WEAVIATE-HOST]:[PORT]/v1`. As an example, if you wish to access the `schema` endpoint on a local instance, you would navigate to `http://localhost:8080/v1/schema`. Ensure you replace `[YOUR-WEAVIATE-HOST]` and `[PORT]` with your actual server host and port number respectively.
### Questions?
If you have any comments or questions, please feel free to reach out to us at the community forum [https://forum.weaviate.io/](https://forum.weaviate.io/).
### Issues?
If you find a bug or want to file a feature request, please open an issue on our GitHub repository for [Weaviate](https://github.com/weaviate/weaviate).
### Need more documentation?
For a quickstart, code examples, concepts and more, please visit our [documentation page](https://docs.weaviate.io/weaviate).' title: Weaviate REST API - version: 1.36.0-dev + version: 1.37.0-dev openapi: 3.0.1 paths: /: @@ -4723,6 +4862,120 @@ paths: - cluster x-serviceIds: - weaviate.cluster.statistics.get + /export/{backend}: + post: + description: Initiates an export operation that writes collections to Parquet files on the specified backend storage (S3, GCS, Azure, or filesystem). Each collection is exported to a separate Parquet file. + operationId: export.create + parameters: + - description: The backend storage system to use for the export (e.g., `filesystem`, `gcs`, `s3`, `azure`). + in: path + name: backend + required: true + schema: + type: string + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/ExportCreateRequest' + application/yaml: + schema: + $ref: '#/components/schemas/ExportCreateRequest' + required: true + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/ExportCreateResponse' + description: Successfully started export operation + "401": + content: {} + description: Unauthorized or invalid credentials + "403": + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + description: Forbidden - insufficient permissions + "422": + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + description: Invalid export request + "500": + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + description: Internal server error occurred while starting export + summary: Start a new export + tags: + - export + x-codegen-request-body-name: body + x-serviceIds: + - weaviate.local.export.create + /export/{backend}/{id}: + get: + description: Retrieves the current status of an export operation, including progress for each collection being exported. + operationId: export.status + parameters: + - description: The backend storage system where the export is stored. + in: path + name: backend + required: true + schema: + type: string + - description: The unique identifier of the export. + in: path + name: id + required: true + schema: + type: string + - description: Optional bucket name where the export is stored. If not specified, uses the backend's default bucket. + in: query + name: bucket + schema: + type: string + - description: Optional path prefix within the bucket. If not specified, uses the backend's default path. + in: query + name: path + schema: + type: string + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/ExportStatusResponse' + description: Successfully retrieved export status + "401": + content: {} + description: Unauthorized or invalid credentials + "403": + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + description: Forbidden - insufficient permissions + "404": + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + description: Export not found + "500": + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + description: Internal server error occurred while retrieving export status + summary: Get export status + tags: + - export + x-serviceIds: + - weaviate.local.export.status /graphql: post: description: Executes a single GraphQL query provided in the request body. Use this endpoint for all Weaviate data queries and exploration. @@ -7128,6 +7381,63 @@ paths: x-codegen-request-body-name: body x-serviceIds: - weaviate.local.manipulate.meta + /schema/{className}/properties/{propertyName}/index/{indexName}: + delete: + description: Deletes an inverted index of a specific property within a collection (`className`). The index to delete is identified by `indexName` and must be one of `filterable`, `searchable`, or `rangeFilters`. + operationId: schema.objects.properties.delete + parameters: + - description: The name of the collection (class) containing the property. + in: path + name: className + required: true + schema: + type: string + - description: The name of the property whose inverted index should be deleted. + in: path + name: propertyName + required: true + schema: + type: string + - description: The name of the inverted index to delete from the property. + in: path + name: indexName + required: true + schema: + enum: + - filterable + - searchable + - rangeFilters + type: string + responses: + "200": + content: {} + description: Index deleted successfully. + "401": + content: {} + description: Unauthorized or invalid credentials. + "403": + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + description: Forbidden + "422": + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + description: Invalid index, property or collection provided. + "500": + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + description: An error occurred while deleting the index. Check the ErrorResponse for details. + summary: Delete a property's inverted index + tags: + - schema + x-serviceIds: + - weaviate.local.manipulate.meta /schema/{className}/shards: get: description: Retrieves the status of all shards associated with the specified collection (`className`). For multi-tenant collections, use the `tenant` query parameter to retrieve status for a specific tenant's shards. @@ -8059,6 +8369,8 @@ tags: name: schema - description: Operations related to creating and managing backups of Weaviate data. This feature allows you to create snapshots of your collections and store them on external storage backends such as cloud object storage (S3, GCS, Azure) or a shared filesystem. These endpoints enable you to initiate backup and restore processes, monitor their status, list available backups on a backend, and delete unwanted backups. Backups are essential for disaster recovery, data migration, and maintaining point-in-time copies of your vector database. name: backups + - description: Operations for exporting Weaviate data to Parquet format on external storage backends (S3, GCS, Azure, or filesystem). Exports provide a way to extract your vector data and object properties into a standardized columnar format for data analysis, archival, or migration. Each collection is exported to a separate Parquet file containing object IDs, vectors, properties, and metadata. + name: exports - description: Endpoints for user account management in Weaviate. This includes operations specific to Weaviate-managed database users (`db` users), such as creation (which generates an API key), listing, deletion, activation/deactivation, and API key rotation. It also provides operations applicable to any authenticated user (`db` or `oidc`), like retrieving their own information (username and assigned roles).

**User Types:**
* **`db` users:** Managed entirely within Weaviate (creation, deletion, API keys). Use these endpoints for full lifecycle management.
* **`oidc` users:** Authenticated via an external OpenID Connect provider. Their lifecycle (creation, credentials) is managed externally, but their role assignments *within Weaviate* are managed via the `authz` endpoints. name: users - description: 'Endpoints for managing Weaviate''s Role-Based Access Control (RBAC) system. Access to Weaviate resources is granted through roles, which are collections of fine-grained permissions.

**Permissions:** Define allowed actions (e.g., `read_data`, `create_collections`, `delete_users`) on specific resources. Resources can be specified broadly (e.g., all collections: `*`) or narrowly (e.g., a specific collection name, tenant pattern, user name, or role name).

**Roles:** Are named sets of permissions. Managing roles involves creating roles with specific permissions, retrieving role definitions, deleting roles, and adding or removing permissions from existing roles.

**Role assignment:** Roles grant their contained permissions to users or groups. These endpoints allow assigning roles to:
* `db` users: Users managed directly by Weaviate via API or environment variables, authenticating with API keys.
* `oidc` users: Users authenticated via an external OpenID Connect provider, managed externally but assigned roles within Weaviate.
* OIDC `groups`: Users authenticated via OIDC who belong to a group automatically inherit roles assigned to that group.

Operations also include revoking roles, checking if a role has a specific permission, listing roles assigned to a user, and listing users/groups assigned to a role. The authorization framework applies universally to both `db` and `oidc` users based on their assigned roles.' diff --git a/client.go b/client.go new file mode 100644 index 00000000..b45f5459 --- /dev/null +++ b/client.go @@ -0,0 +1,213 @@ +package weaviate + +import ( + "context" + "fmt" + "net/http" + "strconv" + "strings" + "time" + + "github.com/weaviate/weaviate-go-client/v6/collections" + "github.com/weaviate/weaviate-go-client/v6/internal/api/transport" +) + +type Client struct { + Collections *collections.Client +} + +// NewClient returns a new client. Nothing is configured by default and +// the following must be provided: +// - schema +// - HTTP host and port +// - gRPC host and port +func NewClient(ctx context.Context, options ...Option) (*Client, error) { + return newClient(ctx, options) +} + +// NewLocal sets default options for connecting to an locally running instance. +// +// Example: +// +// // Use default local configuration +// c, err := weaviate.NewLocal(ctx) +// +// // Change HTTP port +// c, err := weaviate.NewLocal(ctx, weaviate.WithHTTPPort(8081)) +func NewLocal(ctx context.Context, options ...Option) (*Client, error) { + return newClient(ctx, append([]Option{ + WithScheme("http"), + WithHost("localhost"), + WithHTTPPort(8080), + WithGRPCPort(50051), + }, options...)) +} + +// NewWeaviateCloud sets default options for connecting to a Weaviate Cloud instance. +// +// Example: +// +// // Use default connection to Weaviate Cloud instance +// c, err := weaviate.NewWeaviateCloud(ctx, "my.weaviate.io") +// +// // Set additional headers +// c, err := weaviate.NewWeaviateCloud(ctx, "my.weaviate.io", +// weaviate.WithHeader(http.Header{ +// "Custom-X-Value": {"my-header"} +// }), +// ) +func NewWeaviateCloud(ctx context.Context, host string, apiKey string, options ...Option) (*Client, error) { + return newClient(ctx, append([]Option{ + WithScheme("https"), + WithHTTPHost(host), + WithGRPCHost("grpc-" + host), + WithHTTPPort(443), + WithGRPCPort(443), + }, options...)) +} + +const ( + headerWeaviateClient = "X-Weaviate-Client" + clientName = "weaviate-client-go" + + headerWeaviateClusterURL = "X-Weaviate-Cluster-URL" + domainWeaviateIO = "weaviate.io" + domainWeaviateCloud = "weaviate.cloud" + domainSemiTechnology = "semi.technology" +) + +func newDefaultConfig(options ...Option) config { + c := config{ + Header: http.Header{ + headerWeaviateClient: {clientName + "/" + Version()}, + }, + Timeout: transport.Timeout{ + Read: 30 * time.Second, + Write: 90 * time.Second, + }, + } + for _, opt := range options { + opt(&c) + } + return c +} + +func newClient(_ context.Context, options []Option) (*Client, error) { + c := newDefaultConfig(options...) + + if strings.Contains(c.RESTHost, domainWeaviateIO) || + strings.Contains(c.RESTHost, domainWeaviateCloud) || + strings.Contains(c.RESTHost, domainSemiTechnology) { + clusterURL := c.Scheme + "://" + c.RESTHost + ":" + strconv.Itoa(c.RESTPort) + c.Header.Add(headerWeaviateClusterURL, clusterURL) + } + + t, err := transport.New(transport.Config{ + Scheme: c.Scheme, + RESTHost: c.RESTHost, + RESTPort: c.RESTPort, + GRPCHost: c.GRPCHost, + GRPCPort: c.GRPCPort, + Header: c.Header, + Timeout: c.Timeout, + }) + if err != nil { + return nil, fmt.Errorf("weaviate: new client: %w", err) + } + + return &Client{ + Collections: collections.NewClient(t), + }, nil +} + +type ( + config transport.Config + Option func(*config) +) + +// Scheme for request URLs, "http" or "https". +func WithScheme(scheme string) Option { + return func(c *config) { + c.Scheme = scheme + } +} + +// Set HTTPHost and GRPCHost to the same value. +func WithHost(host string) Option { + return func(c *config) { + c.RESTHost = host + c.GRPCHost = host + } +} + +// Hostname of the HTTP host. +func WithHTTPHost(host string) Option { + return func(c *config) { + c.RESTHost = host + } +} + +// Hostname of the gRPC host. +func WithGRPCHost(host string) Option { + return func(c *config) { + c.GRPCHost = host + } +} + +// Port number of the HTTP host. +func WithHTTPPort(port int) Option { + return func(c *config) { + c.RESTPort = port + } +} + +// Port number of the gRPC host. +func WithGRPCPort(port int) Option { + return func(c *config) { + c.GRPCPort = port + } +} + +// Add request headers. +func WithHeader(h http.Header) Option { + return func(c *config) { + if c.Header == nil { + c.Header = make(http.Header) + } + for k, v := range h { + for i := range v { + c.Header.Add(k, v[i]) + } + } + } +} + +// Set read, write, and batch timeouts. +func WithTimeout(d time.Duration) Option { + return func(c *config) { + c.Timeout.Read = d + c.Timeout.Write = d + c.Timeout.Batch = d + } +} + +// Client-side timeout for read operations. Default: 30s. +func WithReadTimeout(d time.Duration) Option { + return func(c *config) { + c.Timeout.Read = d + } +} + +// Client-side timeout for write operations. Default: 90s. +func WithWriteTimeout(d time.Duration) Option { + return func(c *config) { + c.Timeout.Write = d + } +} + +// Client-side timeout for SSB (Server-Side Batching) insert requests. Not set by default. +func WithBatchTimeout(d time.Duration) Option { + return func(c *config) { + c.Timeout.Batch = d + } +} diff --git a/client_test.go b/client_test.go new file mode 100644 index 00000000..7eb87475 --- /dev/null +++ b/client_test.go @@ -0,0 +1,182 @@ +package weaviate_test + +import ( + "net/http" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/weaviate/weaviate-go-client/v6" + "github.com/weaviate/weaviate-go-client/v6/internal" + "github.com/weaviate/weaviate-go-client/v6/internal/api/transport" + "github.com/weaviate/weaviate-go-client/v6/internal/testkit" +) + +// DO NOT enable t.Parallel() for this test as it messes with the global state. +func TestNewLocal(t *testing.T) { + newFunc := transport.New + t.Cleanup(func() { transport.New = newFunc }) + + t.Run("default", func(t *testing.T) { + var got transport.Config + transport.New = func(cfg transport.Config) (internal.Transport, error) { + got = cfg + return testkit.NopTransport, nil + } + + c, err := weaviate.NewLocal(t.Context()) + assert.NotNil(t, c, "nil client") + assert.NoError(t, err) + + assert.Equal(t, transport.Config{ + Scheme: "http", + RESTHost: "localhost", + GRPCHost: "localhost", + RESTPort: 8080, + GRPCPort: 50051, + Header: http.Header{ + "X-Weaviate-Client": {"weaviate-client-go" + "/" + weaviate.Version()}, + }, + Timeout: transport.Timeout{ + Read: 30 * time.Second, + Write: 90 * time.Second, + }, + }, got) + }) + + t.Run("with options", func(t *testing.T) { + var got transport.Config + transport.New = func(cfg transport.Config) (internal.Transport, error) { + got = cfg + return testkit.NopTransport, nil + } + + c, err := weaviate.NewLocal(t.Context(), + weaviate.WithScheme("https"), + weaviate.WithHTTPPort(7070), + weaviate.WithGRPCPort(54321), + weaviate.WithHeader(http.Header{ + "X-Test": {"heads", "up"}, + }), + weaviate.WithReadTimeout(20*time.Second), + weaviate.WithBatchTimeout(100*time.Millisecond), + ) + assert.NotNil(t, c, "nil client") + assert.NoError(t, err) + + assert.Equal(t, transport.Config{ + // Defaults + RESTHost: "localhost", + GRPCHost: "localhost", + + // Custom + Scheme: "https", + RESTPort: 7070, + GRPCPort: 54321, + Header: http.Header{ + "X-Test": {"heads", "up"}, + "X-Weaviate-Client": {"weaviate-client-go" + "/" + weaviate.Version()}, + }, + Timeout: transport.Timeout{ + Read: 20 * time.Second, + Write: 90 * time.Second, + Batch: 100 * time.Millisecond, + }, + }, got) + }) +} + +// DO NOT enable t.Parallel() for this test as it messes with the global state. +func TestNewWeaviateCloud(t *testing.T) { + newFunc := transport.New + t.Cleanup(func() { transport.New = newFunc }) + + t.Run("default", func(t *testing.T) { + var got transport.Config + transport.New = func(cfg transport.Config) (internal.Transport, error) { + got = cfg + return testkit.NopTransport, nil + } + + c, err := weaviate.NewWeaviateCloud(t.Context(), "example.com", "api-key") + assert.NotNil(t, c, "nil client") + assert.NoError(t, err) + + assert.Equal(t, transport.Config{ + Scheme: "https", + RESTHost: "example.com", + GRPCHost: "grpc-example.com", + RESTPort: 443, + GRPCPort: 443, + Header: http.Header{ + "X-Weaviate-Client": {"weaviate-client-go" + "/" + weaviate.Version()}, + }, + Timeout: transport.Timeout{ + Read: 30 * time.Second, + Write: 90 * time.Second, + }, + }, got) + }) + + t.Run("weaviate domain", func(t *testing.T) { + for _, domain := range []string{ + "weaviate.io", + "weaviate.cloud", + "semi.technology", + } { + t.Run(domain, func(t *testing.T) { + var got transport.Config + transport.New = func(cfg transport.Config) (internal.Transport, error) { + got = cfg + return testkit.NopTransport, nil + } + + c, err := weaviate.NewWeaviateCloud(t.Context(), "my."+domain, "api-key") + assert.NotNil(t, c, "nil client") + assert.NoError(t, err) + + assert.Equal(t, got.Header, http.Header{ + "X-Weaviate-Client": {"weaviate-client-go" + "/" + weaviate.Version()}, + "X-Weaviate-Cluster-Url": {"https://my." + domain + ":443"}, + }) + }) + } + }) + + t.Run("with options", func(t *testing.T) { + var got transport.Config + transport.New = func(cfg transport.Config) (internal.Transport, error) { + got = cfg + return testkit.NopTransport, nil + } + + c, err := weaviate.NewWeaviateCloud(t.Context(), "example.com", "api-key", + weaviate.WithHTTPPort(7070), + weaviate.WithGRPCPort(54321), + weaviate.WithHeader(http.Header{ + "X-Test": {"heads", "up"}, + }), + weaviate.WithReadTimeout(20*time.Second), + weaviate.WithBatchTimeout(100*time.Millisecond), + ) + assert.NotNil(t, c, "nil client") + assert.NoError(t, err) + + assert.Equal(t, transport.Config{ + Scheme: "https", + RESTHost: "example.com", + GRPCHost: "grpc-example.com", + RESTPort: 7070, + GRPCPort: 54321, + Header: http.Header{ + "X-Test": {"heads", "up"}, + "X-Weaviate-Client": {"weaviate-client-go" + "/" + weaviate.Version()}, + }, + Timeout: transport.Timeout{ + Read: 20 * time.Second, + Write: 90 * time.Second, + Batch: 100 * time.Millisecond, + }, + }, got) + }) +} diff --git a/collections/client.go b/collections/client.go new file mode 100644 index 00000000..7beeae46 --- /dev/null +++ b/collections/client.go @@ -0,0 +1,166 @@ +package collections + +import ( + "context" + "fmt" + + "github.com/weaviate/weaviate-go-client/v6/data" + "github.com/weaviate/weaviate-go-client/v6/internal" + "github.com/weaviate/weaviate-go-client/v6/internal/api" + "github.com/weaviate/weaviate-go-client/v6/internal/dev" + "github.com/weaviate/weaviate-go-client/v6/query" + "github.com/weaviate/weaviate-go-client/v6/types" +) + +func NewClient(t internal.Transport) *Client { + dev.AssertNotNil(t, "t") + return &Client{t: t} +} + +type Client struct { + t internal.Transport +} + +// WithConsistencyLevel default consistency level for all read / write requests made with this collection handle. +func WithConsistencyLevel(cl types.ConsistencyLevel) HandleOption { + return func(rd *api.RequestDefaults) { + dev.AssertNotNil(rd, "rd") + rd.ConsistencyLevel = api.ConsistencyLevel(cl) + } +} + +// WithConsistencyLevel default tenant for all read / write requests made with this collection handle. +func WithTenant(tenant string) HandleOption { + return func(rd *api.RequestDefaults) { + dev.AssertNotNil(rd, "rd") + rd.Tenant = tenant + } +} + +func (c *Client) Use(collectionName string, options ...HandleOption) *Handle { + rd := api.RequestDefaults{CollectionName: collectionName} + for _, opt := range options { + opt(&rd) + } + return newHandle(c.t, rd) +} + +type Handle struct { + transport internal.Transport + defaults api.RequestDefaults + + Data *data.Client + Query *query.Client +} + +func newHandle(t internal.Transport, rd api.RequestDefaults) *Handle { + dev.AssertNotNil(t, "t") + + return &Handle{ + transport: t, + defaults: rd, + + Data: data.NewClient(t, rd), + Query: query.NewClient(t, rd), + } +} + +func (h *Handle) CollectionName() string { + return h.defaults.CollectionName +} + +func (h *Handle) ConsistencyLevel() types.ConsistencyLevel { + return types.ConsistencyLevel(h.defaults.ConsistencyLevel) +} + +func (h *Handle) Tenant() string { + return h.defaults.Tenant +} + +// HandleOption configures request defaults for collection handle. +type HandleOption func(*api.RequestDefaults) + +// WithOptions returns a new handle with different defaults. +func (h *Handle) WithOptions(options ...HandleOption) *Handle { + defaults := h.defaults + for _, opt := range options { + opt(&defaults) + } + return newHandle(h.transport, defaults) +} + +// Create new collection in the schema. A collection can be created with just the name. +// To configure the new collection, provide a single instance of CreateOptions as the options argument. +// +// Avoid passing multiple options arguments at once -- only the last one will be applied. +func (c *Client) Create(ctx context.Context, collection Collection) (*Handle, error) { + req := &api.CreateCollectionRequest{Collection: collectionToAPI(&collection)} + + // No need to read the result of the request, we only need the name to create a handle. + if err := c.t.Do(ctx, req, nil); err != nil { + return nil, fmt.Errorf("create collection: %w", err) + } + return c.Use(collection.Name), nil +} + +// GetConfig returns configuration for the collection. +// Returns nil with nil error if collections does not exist. +func (c *Client) GetConfig(ctx context.Context, collectionName string) (*Collection, error) { + var resp api.Collection + if err := c.t.Do(ctx, api.GetCollectionRequest(collectionName), &resp); err != nil { + return nil, fmt.Errorf("get collection config: %w", err) + } + collection := collectionFromAPI(&resp) + return &collection, nil +} + +// List returns configurations for all collections defined in the schema. +func (c *Client) List(ctx context.Context) ([]Collection, error) { + var resp api.ListCollectionsResponse + if err := c.t.Do(ctx, api.ListCollectionsRequest, &resp); err != nil { + return nil, fmt.Errorf("list collections: %w", err) + } + + if len(resp) == 0 { + return nil, nil + } + + out := make([]Collection, len(resp)) + for i, c := range resp { + out[i] = collectionFromAPI(&c) + } + return out, nil +} + +// Exists check if collection with this name exists. Always check the returned error, +// as Exists may return false with both nil (collection does not exist) and non-nil +// errors (request failed en route). +func (c *Client) Exists(ctx context.Context, collectionName string) (bool, error) { + var exists api.ResourceExistsResponse + if err := c.t.Do(ctx, api.GetCollectionRequest(collectionName), &exists); err != nil { + return false, fmt.Errorf("check collection exists: %w", err) + } + return exists.Bool(), nil +} + +// Delete collection by name. Returns an error if no collection with this name exist. +func (c *Client) Delete(ctx context.Context, collectionName string) error { + if err := c.t.Do(ctx, api.DeleteCollectionRequest(collectionName), nil); err != nil { + return fmt.Errorf("delete collection: %w", err) + } + return nil +} + +// DeleteAll collections in the schema. +func (c *Client) DeleteAll(ctx context.Context) error { + all, err := c.List(ctx) + if err != nil { + return fmt.Errorf("delete all collections: %w", err) + } + for _, collection := range all { + if err := c.Delete(ctx, collection.Name); err != nil { + return err + } + } + return nil +} diff --git a/collections/client_test.go b/collections/client_test.go new file mode 100644 index 00000000..7411e1c1 --- /dev/null +++ b/collections/client_test.go @@ -0,0 +1,632 @@ +package collections_test + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/weaviate/weaviate-go-client/v6/collections" + "github.com/weaviate/weaviate-go-client/v6/internal/api" + "github.com/weaviate/weaviate-go-client/v6/internal/testkit" + "github.com/weaviate/weaviate-go-client/v6/types" +) + +func TestNewClient(t *testing.T) { + require.Panics(t, func() { + collections.NewClient(nil) + }, "nil transport") +} + +func TestClient_Use(t *testing.T) { + c := collections.NewClient(testkit.NopTransport) + require.NotNil(t, c, "nil client") + + checkNamespaces := func(t *testing.T, h *collections.Handle) { + t.Helper() + assert.NotNil(t, h.Data, "nil data namespace") + assert.NotNil(t, h.Query, "nil query namespace") + } + + t.Run("default handle", func(t *testing.T) { + handle := c.Use("Songs") + + assert.Equal(t, "Songs", handle.CollectionName(), "collection name") + assert.EqualValues(t, "", handle.ConsistencyLevel(), "consistency level") + assert.Equal(t, "", handle.Tenant(), "tenant") + checkNamespaces(t, handle) + }) + + t.Run("use options", func(t *testing.T) { + handle := c.Use("Songs", + collections.WithConsistencyLevel(types.ConsistencyLevelQuorum), + collections.WithTenant("john_doe")) + + assert.Equal(t, "Songs", handle.CollectionName(), "collection name") + assert.EqualValues(t, types.ConsistencyLevelQuorum, handle.ConsistencyLevel(), "consistency level") + assert.Equal(t, "john_doe", handle.Tenant(), "tenant") + checkNamespaces(t, handle) + }) + + t.Run("derive new handle", func(t *testing.T) { + handle := c.Use("Songs") + + derived := handle.WithOptions( + collections.WithConsistencyLevel(types.ConsistencyLevelQuorum), + collections.WithTenant("john_doe")) + + assert.Equal(t, handle.CollectionName(), derived.CollectionName(), "collection name") + assert.EqualValues(t, types.ConsistencyLevelQuorum, derived.ConsistencyLevel(), "consistency level") + assert.Equal(t, "john_doe", derived.Tenant(), "tenant") + checkNamespaces(t, derived) + + assert.EqualValues(t, "", handle.ConsistencyLevel(), "consistency level was modified") + assert.Equal(t, "", handle.Tenant(), "tenant was modified") + }) +} + +func TestClient_Create(t *testing.T) { + for _, tt := range []struct { + name string + collection collections.Collection // Collection to be created. + stubs []testkit.Stub[api.CreateCollectionRequest, api.Collection] + err testkit.Error + }{ + { + name: "full config", + collection: collections.Collection{ + Name: "Songs", + Description: "My favorite songs", + Properties: []collections.Property{ + {Name: "title", DataType: collections.DataTypeText}, + {Name: "genres", DataType: collections.DataTypeTextArray}, + {Name: "single", DataType: collections.DataTypeBool}, + {Name: "year", DataType: collections.DataTypeInt}, + { + Name: "lyrics", + DataType: collections.DataTypeInt, + Tokenization: collections.TokenizationTrigram, + IndexFilterable: true, + IndexRangeFilters: true, + IndexSearchable: true, + }, + { + Name: "metadata", DataType: collections.DataTypeObject, + NestedProperties: []collections.Property{ + {Name: "duration", DataType: collections.DataTypeNumber}, + {Name: "uploadedTime", DataType: collections.DataTypeDate}, + }, + Tokenization: collections.TokenizationWhitespace, + IndexFilterable: true, + IndexRangeFilters: true, + IndexSearchable: true, + }, + }, + References: []collections.Reference{ + { + Name: "artist", + Collections: []string{"Singers", "Bands"}, + }, + }, + Sharding: &collections.ShardingConfig{ + DesiredCount: 3, + DesiredVirtualCount: 150, + VirtualPerPhysical: 50, + }, + Replication: &collections.ReplicationConfig{ + AsyncEnabled: false, + Factor: 6, + DeletionStrategy: collections.TimeBasedResolution, + AsyncReplication: &collections.AsyncReplicationConfig{ + DiffBatchSize: 1, + DiffPerNodeTimeout: 2 * time.Second, + ReplicationConcurrency: 3, + ReplicationFrequency: 4 * time.Millisecond, + ReplicationFrequencyPropagating: 5 * time.Millisecond, + PrePropagationTimeout: 6 * time.Second, + PropagationConcurrency: 7, + PropagationBatchSize: 8, + PropagationLimit: 9, + PropagationTimeout: 10 * time.Second, + PropagationDelay: 11 * time.Millisecond, + HashTreeHeight: 12, + NodePingFrequency: 13 * time.Millisecond, + LoggingFrequency: 14 * time.Second, + }, + }, + InvertedIndex: &collections.InvertedIndexConfig{ + IndexNullState: true, + IndexPropertyLength: true, + IndexTimestamps: true, + UsingBlockMaxWAND: true, + CleanupIntervalSeconds: 92, + BM25: &collections.BM25Config{ + B: 25, + K1: 1, + }, + Stopwords: &collections.StopwordConfig{ + Preset: "standard-please-stop", + Additions: []string{"end"}, + Removals: []string{"terminate"}, + }, + }, + MultiTenancy: &collections.MultiTenancyConfig{ + Enabled: true, + AutoTenantActivation: true, + AutoTenantCreation: false, + }, + }, + stubs: []testkit.Stub[api.CreateCollectionRequest, api.Collection]{ + { + Request: &api.CreateCollectionRequest{ + Collection: api.Collection{ + Name: "Songs", + Description: "My favorite songs", + Properties: []api.Property{ + {Name: "title", DataType: api.DataTypeText}, + {Name: "genres", DataType: api.DataTypeTextArray}, + {Name: "single", DataType: api.DataTypeBool}, + {Name: "year", DataType: api.DataTypeInt}, + { + Name: "lyrics", + DataType: api.DataTypeInt, + Tokenization: api.TokenizationTrigram, + IndexFilterable: true, + IndexRangeFilters: true, + IndexSearchable: true, + }, + { + Name: "metadata", DataType: api.DataTypeObject, + NestedProperties: []api.Property{ + {Name: "duration", DataType: api.DataTypeNumber}, + {Name: "uploadedTime", DataType: api.DataTypeDate}, + }, + Tokenization: api.TokenizationWhitespace, + IndexFilterable: true, + IndexRangeFilters: true, + IndexSearchable: true, + }, + }, + References: []api.ReferenceProperty{ + { + Name: "artist", + Collections: []string{"Singers", "Bands"}, + }, + }, + Sharding: &api.ShardingConfig{ + DesiredCount: 3, + DesiredVirtualCount: 150, + VirtualPerPhysical: 50, + }, + Replication: &api.ReplicationConfig{ + AsyncEnabled: false, + Factor: 6, + DeletionStrategy: api.TimeBasedResolution, + AsyncReplication: &api.AsyncReplicationConfig{ + DiffBatchSize: 1, + DiffPerNodeTimeout: 2 * time.Second, + ReplicationConcurrency: 3, + ReplicationFrequency: 4 * time.Millisecond, + ReplicationFrequencyPropagating: 5 * time.Millisecond, + PrePropagationTimeout: 6 * time.Second, + PropagationConcurrency: 7, + PropagationBatchSize: 8, + PropagationLimit: 9, + PropagationTimeout: 10 * time.Second, + PropagationDelay: 11 * time.Millisecond, + HashTreeHeight: 12, + NodePingFrequency: 13 * time.Millisecond, + LoggingFrequency: 14 * time.Second, + }, + }, + InvertedIndex: &api.InvertedIndexConfig{ + IndexNullState: true, + IndexPropertyLength: true, + IndexTimestamps: true, + UsingBlockMaxWAND: true, + CleanupIntervalSeconds: 92, + BM25: &api.BM25Config{ + B: 25, + K1: 1, + }, + Stopwords: &api.StopwordConfig{ + Preset: "standard-please-stop", + Additions: []string{"end"}, + Removals: []string{"terminate"}, + }, + }, + MultiTenancy: &api.MultiTenancyConfig{ + Enabled: true, + AutoTenantActivation: true, + AutoTenantCreation: false, + }, + }, + }, + }, + }, + }, + { + name: "partial config", + collection: collections.Collection{ + Name: "Songs", + Description: "My favorite songs", + }, + stubs: []testkit.Stub[api.CreateCollectionRequest, api.Collection]{ + { + Request: &api.CreateCollectionRequest{ + Collection: api.Collection{ + Name: "Songs", + Description: "My favorite songs", + }, + }, + }, + }, + }, + { + name: "with error", + stubs: []testkit.Stub[api.CreateCollectionRequest, api.Collection]{ + {Err: testkit.ErrWhaam}, + }, + err: testkit.ExpectError, + }, + } { + t.Run(tt.name, func(t *testing.T) { + // Return the exact collection that was passed + // in the request to simplify the test cases. + for i, stub := range tt.stubs { + if stub.Request != nil { + tt.stubs[i].Response = stub.Request.Collection + } + } + + transport := testkit.NewTransport(t, tt.stubs) + c := collections.NewClient(transport) + require.NotNil(t, c, "nil client") + + handle, err := c.Create(t.Context(), tt.collection) + tt.err.Require(t, err, "create error") + + if tt.err == nil { + require.Equal(t, tt.collection.Name, handle.CollectionName(), "collection handle name") + } else { + require.Nil(t, handle, "handle on error") + } + }) + } +} + +func TestClient_GetConfig(t *testing.T) { + for _, tt := range []struct { + name string + collection string + stubs []testkit.Stub[any, api.Collection] + want *collections.Collection + err testkit.Error + }{ + { + name: "ok", + collection: "Songs", + stubs: []testkit.Stub[any, api.Collection]{ + { + Request: testkit.Ptr(api.GetCollectionRequest("Songs")), + Response: api.Collection{ + Name: "Songs", + Description: "My favorite songs", + Properties: []api.Property{ + {Name: "title", DataType: api.DataTypeText}, + {Name: "genres", DataType: api.DataTypeTextArray}, + {Name: "single", DataType: api.DataTypeBool}, + {Name: "year", DataType: api.DataTypeInt}, + { + Name: "lyrics", + DataType: api.DataTypeInt, + Tokenization: api.TokenizationTrigram, + IndexFilterable: true, + IndexRangeFilters: true, + IndexSearchable: true, + }, + { + Name: "metadata", DataType: api.DataTypeObject, + NestedProperties: []api.Property{ + {Name: "duration", DataType: api.DataTypeNumber}, + {Name: "uploadedTime", DataType: api.DataTypeDate}, + }, + Tokenization: api.TokenizationWhitespace, + IndexFilterable: true, + IndexRangeFilters: true, + IndexSearchable: true, + }, + }, + References: []api.ReferenceProperty{ + { + Name: "artist", + Collections: []string{"Singers", "Bands"}, + }, + }, + Sharding: &api.ShardingConfig{ + DesiredCount: 3, + DesiredVirtualCount: 150, + VirtualPerPhysical: 50, + }, + Replication: &api.ReplicationConfig{ + AsyncEnabled: false, + Factor: 6, + DeletionStrategy: api.TimeBasedResolution, + AsyncReplication: &api.AsyncReplicationConfig{ + DiffBatchSize: 1, + DiffPerNodeTimeout: 2 * time.Second, + ReplicationConcurrency: 3, + ReplicationFrequency: 4 * time.Millisecond, + ReplicationFrequencyPropagating: 5 * time.Millisecond, + PrePropagationTimeout: 6 * time.Second, + PropagationConcurrency: 7, + PropagationBatchSize: 8, + PropagationLimit: 9, + PropagationTimeout: 10 * time.Second, + PropagationDelay: 11 * time.Millisecond, + HashTreeHeight: 12, + NodePingFrequency: 13 * time.Millisecond, + LoggingFrequency: 14 * time.Second, + }, + }, + InvertedIndex: &api.InvertedIndexConfig{ + IndexNullState: true, + IndexPropertyLength: true, + IndexTimestamps: true, + UsingBlockMaxWAND: true, + CleanupIntervalSeconds: 92, + BM25: &api.BM25Config{ + B: 25, + K1: 1, + }, + Stopwords: &api.StopwordConfig{ + Preset: "standard-please-stop", + Additions: []string{"end"}, + Removals: []string{"terminate"}, + }, + }, + MultiTenancy: &api.MultiTenancyConfig{ + Enabled: true, + AutoTenantActivation: true, + AutoTenantCreation: false, + }, + }, + }, + }, + want: &collections.Collection{ + Name: "Songs", + Description: "My favorite songs", + Properties: []collections.Property{ + {Name: "title", DataType: collections.DataTypeText}, + {Name: "genres", DataType: collections.DataTypeTextArray}, + {Name: "single", DataType: collections.DataTypeBool}, + {Name: "year", DataType: collections.DataTypeInt}, + { + Name: "lyrics", + DataType: collections.DataTypeInt, + Tokenization: collections.TokenizationTrigram, + IndexFilterable: true, + IndexRangeFilters: true, + IndexSearchable: true, + }, + { + Name: "metadata", DataType: collections.DataTypeObject, + NestedProperties: []collections.Property{ + {Name: "duration", DataType: collections.DataTypeNumber}, + {Name: "uploadedTime", DataType: collections.DataTypeDate}, + }, + Tokenization: collections.TokenizationWhitespace, + IndexFilterable: true, + IndexRangeFilters: true, + IndexSearchable: true, + }, + }, + References: []collections.Reference{ + { + Name: "artist", + Collections: []string{"Singers", "Bands"}, + }, + }, + Sharding: &collections.ShardingConfig{ + DesiredCount: 3, + DesiredVirtualCount: 150, + VirtualPerPhysical: 50, + }, + Replication: &collections.ReplicationConfig{ + AsyncEnabled: false, + Factor: 6, + DeletionStrategy: collections.TimeBasedResolution, + AsyncReplication: &collections.AsyncReplicationConfig{ + DiffBatchSize: 1, + DiffPerNodeTimeout: 2 * time.Second, + ReplicationConcurrency: 3, + ReplicationFrequency: 4 * time.Millisecond, + ReplicationFrequencyPropagating: 5 * time.Millisecond, + PrePropagationTimeout: 6 * time.Second, + PropagationConcurrency: 7, + PropagationBatchSize: 8, + PropagationLimit: 9, + PropagationTimeout: 10 * time.Second, + PropagationDelay: 11 * time.Millisecond, + HashTreeHeight: 12, + NodePingFrequency: 13 * time.Millisecond, + LoggingFrequency: 14 * time.Second, + }, + }, + InvertedIndex: &collections.InvertedIndexConfig{ + IndexNullState: true, + IndexPropertyLength: true, + IndexTimestamps: true, + UsingBlockMaxWAND: true, + CleanupIntervalSeconds: 92, + BM25: &collections.BM25Config{ + B: 25, + K1: 1, + }, + Stopwords: &collections.StopwordConfig{ + Preset: "standard-please-stop", + Additions: []string{"end"}, + Removals: []string{"terminate"}, + }, + }, + MultiTenancy: &collections.MultiTenancyConfig{ + Enabled: true, + AutoTenantActivation: true, + AutoTenantCreation: false, + }, + }, + }, + { + name: "with error", + stubs: []testkit.Stub[any, api.Collection]{ + {Err: testkit.ErrWhaam}, + }, + err: testkit.ExpectError, + }, + } { + t.Run(tt.name, func(t *testing.T) { + transport := testkit.NewTransport(t, tt.stubs) + c := collections.NewClient(transport) + require.NotNil(t, c, "nil client") + + got, err := c.GetConfig(t.Context(), tt.collection) + tt.err.Require(t, err, "list error") + + require.Equal(t, tt.want, got, "collection config") + }) + } +} + +func TestClient_List(t *testing.T) { + for _, tt := range []struct { + name string + stubs []testkit.Stub[any, api.ListCollectionsResponse] + want []collections.Collection + err testkit.Error + }{ + { + name: "empty response", + stubs: []testkit.Stub[any, api.ListCollectionsResponse]{ + { + Request: testkit.Ptr[any](api.ListCollectionsRequest), + }, + }, + }, + { + name: "several collections", + stubs: []testkit.Stub[any, api.ListCollectionsResponse]{ + { + Request: testkit.Ptr[any](api.ListCollectionsRequest), + Response: api.ListCollectionsResponse{ + {Name: "Songs"}, + {Name: "Artists"}, + {Name: "Albums"}, + }, + }, + }, + want: []collections.Collection{ + {Name: "Songs"}, + {Name: "Artists"}, + {Name: "Albums"}, + }, + }, + { + name: "with error", + stubs: []testkit.Stub[any, api.ListCollectionsResponse]{ + {Err: testkit.ErrWhaam}, + }, + err: testkit.ExpectError, + }, + } { + t.Run(tt.name, func(t *testing.T) { + transport := testkit.NewTransport(t, tt.stubs) + c := collections.NewClient(transport) + require.NotNil(t, c, "nil client") + + got, err := c.List(t.Context()) + tt.err.Require(t, err, "list error") + + require.Equal(t, tt.want, got, "collection config") + }) + } +} + +func TestClient_Delete(t *testing.T) { + transport := testkit.NewTransport(t, []testkit.Stub[any, any]{ + {Request: testkit.Ptr(api.DeleteCollectionRequest("Songs"))}, + }) + c := collections.NewClient(transport) + require.NotNil(t, c, "nil client") + + err := c.Delete(t.Context(), "Songs") + require.NoError(t, err, "delete error") +} + +func TestClient_DeleteAll(t *testing.T) { + transport := testkit.NewTransport(t, []testkit.Stub[any, any]{ + { + Request: testkit.Ptr[any](api.ListCollectionsRequest), + Response: api.ListCollectionsResponse{ + {Name: "Songs"}, + {Name: "Artists"}, + {Name: "Albums"}, + }, + }, + {Request: testkit.Ptr(api.DeleteCollectionRequest("Songs"))}, + {Request: testkit.Ptr(api.DeleteCollectionRequest("Artists"))}, + {Request: testkit.Ptr(api.DeleteCollectionRequest("Albums"))}, + }) + c := collections.NewClient(transport) + require.NotNil(t, c, "nil client") + + err := c.DeleteAll(t.Context()) + require.NoError(t, err, "delete all error") +} + +func TestClient_Exists(t *testing.T) { + for _, tt := range []struct { + name string + stubs []testkit.Stub[any, api.ResourceExistsResponse] + want bool + err testkit.Error + }{ + { + name: "exists", + stubs: []testkit.Stub[any, api.ResourceExistsResponse]{ + { + Request: testkit.Ptr(api.GetCollectionRequest("Songs")), + Response: true, + }, + }, + want: true, + }, + { + name: "not exists", + stubs: []testkit.Stub[any, api.ResourceExistsResponse]{ + { + Request: testkit.Ptr(api.GetCollectionRequest("Songs")), + Response: false, + }, + }, + want: false, + }, + { + name: "with error", + stubs: []testkit.Stub[any, api.ResourceExistsResponse]{ + {Err: testkit.ErrWhaam}, + }, + err: testkit.ExpectError, + }, + } { + t.Run(tt.name, func(t *testing.T) { + transport := testkit.NewTransport(t, tt.stubs) + c := collections.NewClient(transport) + require.NotNil(t, c, "nil client") + + got, err := c.Exists(t.Context(), "Songs") + tt.err.Require(t, err, "exists error") + + require.Equal(t, tt.want, got, "exists") + }) + } +} diff --git a/collections/collection.go b/collections/collection.go new file mode 100644 index 00000000..62982e80 --- /dev/null +++ b/collections/collection.go @@ -0,0 +1,285 @@ +package collections + +import ( + "github.com/weaviate/weaviate-go-client/v6/internal/api" + "github.com/weaviate/weaviate-go-client/v6/internal/dev" +) + +type ( + Collection struct { + Name string + Description string + Properties []Property + References []Reference + Sharding *ShardingConfig + Replication *ReplicationConfig + InvertedIndex *InvertedIndexConfig + MultiTenancy *MultiTenancyConfig + } + Property struct { + Name string + Description string + DataType DataType + NestedProperties []Property + Tokenization Tokenization + IndexFilterable bool + IndexRangeFilters bool + IndexSearchable bool + } + Reference struct { + Name string + Collections []string + } + ShardingConfig struct { + VirtualPerPhysical int + DesiredCount int + DesiredVirtualCount int + } + ReplicationConfig struct { + AsyncEnabled bool // Enable asynchronous replication. + AsyncReplication *AsyncReplicationConfig // Fine-tune asynchronous replication. + DeletionStrategy DeletionStrategy // Conflict resolution strategy for deleted objects. + Factor int // Number of times a collection is replicated. + } + AsyncReplicationConfig api.AsyncReplicationConfig + InvertedIndexConfig struct { + IndexNullState bool // Index each object with the null state. + IndexPropertyLength bool // Index length of properties. + IndexTimestamps bool // Index each object by its internal timestamps. + UsingBlockMaxWAND bool // Toggle UsingBlockMaxWAND usage for BM25 search. + CleanupIntervalSeconds int32 // Asynchronous index cleanup internal. + BM25 *BM25Config // Tuning parameters for the BM25 algorithm. + Stopwords *StopwordConfig // Fine-grained control over stopword list usage. + } + BM25Config api.BM25Config + StopwordConfig api.StopwordConfig + MultiTenancyConfig api.MultiTenancyConfig +) + +// DataType defines supported property data types. +type DataType api.DataType + +const ( + DataTypeText DataType = DataType(api.DataTypeText) + DataTypeBool DataType = DataType(api.DataTypeBool) + DataTypeInt DataType = DataType(api.DataTypeInt) + DataTypeNumber DataType = DataType(api.DataTypeNumber) + DataTypeDate DataType = DataType(api.DataTypeDate) + DataTypeObject DataType = DataType(api.DataTypeObject) + DataTypeGeoCoordinates DataType = DataType(api.DataTypeGeoCoordinates) + DataTypeTextArray DataType = DataType(api.DataTypeTextArray) + DataTypeBoolArray DataType = DataType(api.DataTypeBoolArray) + DataTypeIntArray DataType = DataType(api.DataTypeIntArray) + DataTypeNumberArray DataType = DataType(api.DataTypeNumberArray) + DataTypeDateArray DataType = DataType(api.DataTypeDateArray) + DataTypeObjectArray DataType = DataType(api.DataTypeObjectArray) +) + +type Tokenization api.Tokenization + +const ( + TokenizationWord Tokenization = Tokenization(api.TokenizationWord) + TokenizationWhitespace Tokenization = Tokenization(api.TokenizationWhitespace) + TokenizationLowercase Tokenization = Tokenization(api.TokenizationLowercase) + TokenizationField Tokenization = Tokenization(api.TokenizationField) + TokenizationGSE Tokenization = Tokenization(api.TokenizationGSE) + TokenizationGSE_CH Tokenization = Tokenization(api.TokenizationGSE_CH) + TokenizationTrigram Tokenization = Tokenization(api.TokenizationTrigram) + TokenizationKagomeJA Tokenization = Tokenization(api.TokenizationKagomeJA) + TokenizationKagomeKR Tokenization = Tokenization(api.TokenizationKagomeKR) +) + +type DeletionStrategy string + +const ( + DeleteOnConflict DeletionStrategy = DeletionStrategy(api.DeleteOnConflict) + NoAutomatedResolution DeletionStrategy = DeletionStrategy(api.NoAutomatedResolution) + TimeBasedResolution DeletionStrategy = DeletionStrategy(api.TimeBasedResolution) +) + +func collectionToAPI(c *Collection) api.Collection { + var properties []api.Property + if len(c.Properties) > 0 { + properties = make([]api.Property, len(c.Properties)) + for i, p := range c.Properties { + properties[i] = api.Property{ + Name: p.Name, + Description: p.Description, + DataType: api.DataType(p.DataType), + NestedProperties: nestedPropertiesToAPI(p.NestedProperties), + Tokenization: api.Tokenization(p.Tokenization), + IndexFilterable: p.IndexFilterable, + IndexRangeFilters: p.IndexRangeFilters, + IndexSearchable: p.IndexSearchable, + } + } + } + + var references []api.ReferenceProperty + if len(c.References) > 0 { + references = make([]api.ReferenceProperty, len(c.References)) + for i, ref := range c.References { + references[i] = api.ReferenceProperty{ + Name: ref.Name, + Collections: ref.Collections, + } + } + } + + out := api.Collection{ + Name: c.Name, + Description: c.Description, + Properties: properties, + References: references, + MultiTenancy: (*api.MultiTenancyConfig)(c.MultiTenancy), + } + + if c.Sharding != nil { + out.Sharding = &api.ShardingConfig{ + DesiredCount: c.Sharding.DesiredCount, + DesiredVirtualCount: c.Sharding.DesiredVirtualCount, + VirtualPerPhysical: c.Sharding.VirtualPerPhysical, + } + } + + if c.Replication != nil { + out.Replication = &api.ReplicationConfig{ + AsyncEnabled: c.Replication.AsyncEnabled, + Factor: c.Replication.Factor, + DeletionStrategy: api.DeletionStrategy(c.Replication.DeletionStrategy), + AsyncReplication: (*api.AsyncReplicationConfig)(c.Replication.AsyncReplication), + } + } + + if c.InvertedIndex != nil { + out.InvertedIndex = &api.InvertedIndexConfig{ + IndexNullState: c.InvertedIndex.IndexNullState, + IndexPropertyLength: c.InvertedIndex.IndexPropertyLength, + IndexTimestamps: c.InvertedIndex.IndexTimestamps, + UsingBlockMaxWAND: c.InvertedIndex.UsingBlockMaxWAND, + CleanupIntervalSeconds: c.InvertedIndex.CleanupIntervalSeconds, + Stopwords: (*api.StopwordConfig)(c.InvertedIndex.Stopwords), + BM25: (*api.BM25Config)(c.InvertedIndex.BM25), + } + } + + return out +} + +// collectionFromAPI converts api.Collection into Collection. +func collectionFromAPI(c *api.Collection) Collection { + dev.AssertNotNil(c, "c") + + var properties []Property + if len(c.Properties) > 0 { + properties = make([]Property, len(c.Properties)) + for i, p := range c.Properties { + properties[i] = Property{ + Name: p.Name, + Description: p.Description, + DataType: DataType(p.DataType), + NestedProperties: nestedPropertiesFromAPI(p.NestedProperties), + Tokenization: Tokenization(p.Tokenization), + IndexFilterable: p.IndexFilterable, + IndexRangeFilters: p.IndexRangeFilters, + IndexSearchable: p.IndexSearchable, + } + } + } + + var references []Reference + if len(c.References) > 0 { + references = make([]Reference, len(c.References)) + for i, ref := range c.References { + references[i] = Reference{ + Name: ref.Name, + Collections: ref.Collections, + } + } + } + + var sharding *ShardingConfig + if c.Sharding != nil { + sharding = &ShardingConfig{ + DesiredCount: c.Sharding.DesiredCount, + DesiredVirtualCount: c.Sharding.DesiredVirtualCount, + VirtualPerPhysical: c.Sharding.VirtualPerPhysical, + } + } + + var replication *ReplicationConfig + if c.Replication != nil { + replication = &ReplicationConfig{ + AsyncEnabled: c.Replication.AsyncEnabled, + Factor: c.Replication.Factor, + DeletionStrategy: DeletionStrategy(c.Replication.DeletionStrategy), + AsyncReplication: (*AsyncReplicationConfig)(c.Replication.AsyncReplication), + } + } + + var invertedIndex *InvertedIndexConfig + if c.InvertedIndex != nil { + invertedIndex = &InvertedIndexConfig{ + IndexNullState: c.InvertedIndex.IndexNullState, + IndexPropertyLength: c.InvertedIndex.IndexPropertyLength, + IndexTimestamps: c.InvertedIndex.IndexTimestamps, + UsingBlockMaxWAND: c.InvertedIndex.UsingBlockMaxWAND, + CleanupIntervalSeconds: c.InvertedIndex.CleanupIntervalSeconds, + Stopwords: (*StopwordConfig)(c.InvertedIndex.Stopwords), + BM25: (*BM25Config)(c.InvertedIndex.BM25), + } + } + + return Collection{ + Name: c.Name, + Description: c.Description, + Properties: properties, + References: references, + Sharding: sharding, + Replication: replication, + InvertedIndex: invertedIndex, + MultiTenancy: (*MultiTenancyConfig)(c.MultiTenancy), + } +} + +func nestedPropertiesFromAPI(nested []api.Property) []Property { + if len(nested) == 0 { + return nil + } + + out := make([]Property, len(nested)) + for i, np := range nested { + out[i] = Property{ + Name: np.Name, + Description: np.Description, + DataType: DataType(np.DataType), + NestedProperties: nestedPropertiesFromAPI(np.NestedProperties), + Tokenization: Tokenization(np.Tokenization), + IndexFilterable: np.IndexFilterable, + IndexRangeFilters: np.IndexRangeFilters, + IndexSearchable: np.IndexSearchable, + } + } + return out +} + +func nestedPropertiesToAPI(nested []Property) []api.Property { + if len(nested) == 0 { + return nil + } + + out := make([]api.Property, len(nested)) + for i, p := range nested { + out[i] = api.Property{ + Name: p.Name, + Description: p.Description, + DataType: api.DataType(p.DataType), + NestedProperties: nestedPropertiesToAPI(p.NestedProperties), + Tokenization: api.Tokenization(p.Tokenization), + IndexFilterable: p.IndexFilterable, + IndexRangeFilters: p.IndexRangeFilters, + IndexSearchable: p.IndexSearchable, + } + } + return out +} diff --git a/internal/api/collections.go b/internal/api/collections.go new file mode 100644 index 00000000..5da5c521 --- /dev/null +++ b/internal/api/collections.go @@ -0,0 +1,423 @@ +package api + +import ( + "encoding/json" + "net/http" + "time" + + "github.com/weaviate/weaviate-go-client/v6/internal/api/internal/gen/rest" + "github.com/weaviate/weaviate-go-client/v6/internal/transports" +) + +type ( + Collection struct { + Name string + Description string + Properties []Property + References []ReferenceProperty + Sharding *ShardingConfig + Replication *ReplicationConfig + InvertedIndex *InvertedIndexConfig + MultiTenancy *MultiTenancyConfig + } + Property struct { + Name string + Description string + DataType DataType + NestedProperties []Property + Tokenization Tokenization + IndexFilterable bool + IndexRangeFilters bool + IndexSearchable bool + } + ReferenceProperty struct { + Name string + Collections []string // Collections that can be referenced. + } + ShardingConfig struct { + DesiredCount int + DesiredVirtualCount int + VirtualPerPhysical int + } + ReplicationConfig struct { + AsyncEnabled bool // Enable asynchronous replication. + AsyncReplication *AsyncReplicationConfig // Fine-tuning parameters for async replication. + Factor int // Number of times a collection is replicated. + DeletionStrategy DeletionStrategy // Conflict resolution strategy for deleted objects. + } + AsyncReplicationConfig struct { + DiffBatchSize int64 // Maximum number of keys in a diff batch. + DiffPerNodeTimeout time.Duration // Timeout for computing a diff against a single node. Recommended unit: seconds. + ReplicationConcurrency int64 // Maximum number of concurrent replication workers. + ReplicationFrequency time.Duration // Frequency at which diff calculations are run. Recommended unit: milliseconds. + ReplicationFrequencyPropagating time.Duration // Replication frequency during the propagating phase. Recommended unit: milliseconds. + PrePropagationTimeout time.Duration // Total timeout for the pre-propagation phase. Recommended unit: seconds. + PropagationConcurrency int64 // Maximum number of concurrent propagation workers. + PropagationBatchSize int64 // Maximum number of objects in a single propagation batch. + PropagationLimit int64 // Maximum number of objects propagated in a single replication round. + PropagationTimeout time.Duration // Timeout for a single propagation batch request. Recommended unit: seconds. + PropagationDelay time.Duration // Delay before newly added / updated objects are propagated. Recommended unit: milliseconds. + HashTreeHeight int64 // Height of the hash tree used to compute the diff. + NodePingFrequency time.Duration // Frequency at which liveness of the target nodes is checked. Recommended unit: milliseconds. + LoggingFrequency time.Duration // Frequency at which replication status is logged. Recommended unit: seconds. + } + InvertedIndexConfig struct { + IndexNullState bool // Index each object with the null state. + IndexPropertyLength bool // Index length of properties. + IndexTimestamps bool // Index each object by its internal timestamps. + UsingBlockMaxWAND bool // Toggle UsingBlockMaxWAND usage for BM25 search. + CleanupIntervalSeconds int32 // Asynchronous index cleanup internal. + BM25 *BM25Config // Tuning parameters for the BM25 algorithm. + Stopwords *StopwordConfig // Fine-grained control over stopword list usage. + } + BM25Config rest.BM25Config + StopwordConfig rest.StopwordConfig + MultiTenancyConfig rest.MultiTenancyConfig +) + +// DataType defines supported property data types. +type DataType string + +const ( + DataTypeText DataType = "text" + DataTypeBool DataType = "boolean" + DataTypeInt DataType = "int" + DataTypeNumber DataType = "number" + DataTypeDate DataType = "date" + DataTypeObject DataType = "object" + DataTypeGeoCoordinates DataType = "geoCoordinates" + DataTypeTextArray DataType = "text[]" + DataTypeBoolArray DataType = "boolean[]" + DataTypeIntArray DataType = "number[]" + DataTypeNumberArray DataType = "date[]" + DataTypeDateArray DataType = "object[]" + DataTypeObjectArray DataType = "geoCoordinates[]" +) + +// knownDataTypes are a set of all data types defined in the Weaviate server. +// A property whose data type is not in knownDataTypes is assumed to be a reference. +var knownDataTypes = newSet([]DataType{ + DataTypeText, + DataTypeBool, + DataTypeInt, + DataTypeNumber, + DataTypeDate, + DataTypeObject, + DataTypeGeoCoordinates, + DataTypeTextArray, + DataTypeBoolArray, + DataTypeIntArray, + DataTypeNumberArray, + DataTypeDateArray, + DataTypeObjectArray, +}) + +type Tokenization string + +const ( + TokenizationWord Tokenization = Tokenization(rest.PropertyTokenizationWord) + TokenizationWhitespace Tokenization = Tokenization(rest.PropertyTokenizationWhitespace) + TokenizationLowercase Tokenization = Tokenization(rest.PropertyTokenizationLowercase) + TokenizationField Tokenization = Tokenization(rest.PropertyTokenizationField) + TokenizationGSE Tokenization = Tokenization(rest.PropertyTokenizationGse) + TokenizationGSE_CH Tokenization = Tokenization(rest.PropertyTokenizationGseCh) + TokenizationTrigram Tokenization = Tokenization(rest.PropertyTokenizationTrigram) + TokenizationKagomeJA Tokenization = Tokenization(rest.PropertyTokenizationKagomeJa) + TokenizationKagomeKR Tokenization = Tokenization(rest.PropertyTokenizationKagomeKr) +) + +type DeletionStrategy string + +const ( + DeleteOnConflict DeletionStrategy = DeletionStrategy(rest.DeleteOnConflict) + NoAutomatedResolution DeletionStrategy = DeletionStrategy(rest.NoAutomatedResolution) + TimeBasedResolution DeletionStrategy = DeletionStrategy(rest.TimeBasedResolution) +) + +// CreateCollectionsRequest creates a new collection in the schema. +type CreateCollectionRequest struct { + transports.BaseEndpoint + Collection +} + +var _ transports.Endpoint = (*CreateCollectionRequest)(nil) + +func (*CreateCollectionRequest) Method() string { return http.MethodPost } +func (*CreateCollectionRequest) Path() string { return "/schema" } +func (r *CreateCollectionRequest) Body() any { return &r.Collection } + +// GetCollectionRequest by collection name. +var GetCollectionRequest = transports.IdentityEndpoint[string](http.MethodGet, "/schema/%s") + +// ListCollectionsRequest fetches definitions for all collections in the schema. +var ListCollectionsRequest transports.Endpoint = transports.StaticEndpoint(http.MethodGet, "/schema") + +type ListCollectionsResponse []Collection + +var _ json.Unmarshaler = (*ListCollectionsResponse)(nil) + +func (r *ListCollectionsResponse) UnmarshalJSON(data []byte) error { + var schema struct { + Collections []Collection `json:"classes"` + } + if err := json.Unmarshal(data, &schema); err != nil { + return err + } + *r = schema.Collections + return nil +} + +// DeleteCollectionRequest by collection name. +var DeleteCollectionRequest = transports.IdentityEndpoint[string](http.MethodDelete, "/schema/%s") + +var ( + _ json.Marshaler = (*Collection)(nil) + _ json.Unmarshaler = (*Collection)(nil) +) + +// MarshaJSON marshals Collection via [rest.Class]. +func (c *Collection) MarshalJSON() ([]byte, error) { + properties := make([]rest.Property, len(c.Properties)+len(c.References)) + for i, p := range c.Properties { + properties[i] = rest.Property{ + Name: p.Name, + Description: p.Description, + DataType: []string{string(p.DataType)}, + NestedProperties: nestedPropertiesToREST(p.NestedProperties), + Tokenization: rest.PropertyTokenization(p.Tokenization), + IndexFilterable: p.IndexFilterable, + IndexRangeFilters: p.IndexRangeFilters, + IndexSearchable: p.IndexSearchable, + } + } + for i, ref := range c.References { + properties[i+len(c.Properties)] = rest.Property{ + Name: ref.Name, + DataType: ref.Collections, + } + } + + out := &rest.Class{ + Class: c.Name, + Description: c.Description, + Properties: properties, + } + + if c.Sharding != nil { + out.ShardingConfig = map[string]any{ + "desiredCount": c.Sharding.DesiredCount, + "desiredVirtualCount": c.Sharding.DesiredVirtualCount, + "virtualPerPhysical": c.Sharding.VirtualPerPhysical, + } + } + + if c.Replication != nil { + out.ReplicationConfig = rest.ReplicationConfig{ + AsyncEnabled: c.Replication.AsyncEnabled, + Factor: c.Replication.Factor, + DeletionStrategy: rest.ReplicationConfigDeletionStrategy(c.Replication.DeletionStrategy), + } + + if async := c.Replication.AsyncReplication; async != nil { + out.ReplicationConfig.AsyncConfig = rest.ReplicationAsyncConfig{ + DiffBatchSize: async.DiffBatchSize, + DiffPerNodeTimeout: int64(async.DiffPerNodeTimeout.Seconds()), + MaxWorkers: async.ReplicationConcurrency, + Frequency: async.ReplicationFrequency.Milliseconds(), + FrequencyWhilePropagating: async.ReplicationFrequencyPropagating.Milliseconds(), + PrePropagationTimeout: int64(async.PrePropagationTimeout.Seconds()), + PropagationConcurrency: async.PropagationConcurrency, + PropagationBatchSize: async.PropagationBatchSize, + PropagationLimit: async.PropagationLimit, + PropagationTimeout: int64(async.PropagationTimeout.Seconds()), + PropagationDelay: async.PropagationDelay.Milliseconds(), + HashtreeHeight: async.HashTreeHeight, + AliveNodesCheckingFrequency: async.NodePingFrequency.Milliseconds(), + LoggingFrequency: int64(async.LoggingFrequency.Seconds()), + } + } + } + + if c.InvertedIndex != nil { + out.InvertedIndexConfig = rest.InvertedIndexConfig{ + IndexNullState: c.InvertedIndex.IndexNullState, + IndexPropertyLength: c.InvertedIndex.IndexPropertyLength, + IndexTimestamps: c.InvertedIndex.IndexTimestamps, + UsingBlockMaxWAND: c.InvertedIndex.UsingBlockMaxWAND, + CleanupIntervalSeconds: c.InvertedIndex.CleanupIntervalSeconds, + } + + if c.InvertedIndex.Stopwords != nil { + out.InvertedIndexConfig.Stopwords = rest.StopwordConfig(*c.InvertedIndex.Stopwords) + } + + if c.InvertedIndex.BM25 != nil { + out.InvertedIndexConfig.Bm25 = rest.BM25Config{ + B: c.InvertedIndex.BM25.B, + K1: c.InvertedIndex.BM25.K1, + } + } + } + + if c.MultiTenancy != nil { + out.MultiTenancyConfig = rest.MultiTenancyConfig(*c.MultiTenancy) + } + + return json.Marshal(&out) +} + +func nestedPropertiesToREST(nps []Property) []rest.NestedProperty { + if len(nps) == 0 { + return nil + } + + properties := make([]rest.NestedProperty, len(nps)) + for i, p := range nps { + properties[i] = rest.NestedProperty{ + Name: p.Name, + Description: p.Description, + DataType: []string{string(p.DataType)}, + NestedProperties: nestedPropertiesToREST(p.NestedProperties), + Tokenization: rest.NestedPropertyTokenization(p.Tokenization), + IndexFilterable: p.IndexFilterable, + IndexRangeFilters: p.IndexRangeFilters, + IndexSearchable: p.IndexSearchable, + } + } + + return properties +} + +func (c *Collection) UnmarshalJSON(data []byte) error { + var class rest.Class + if err := json.Unmarshal(data, &class); err != nil { + return err + } + + properties := make([]Property, 0, len(class.Properties)) + references := make([]ReferenceProperty, 0, len(class.Properties)) + for _, p := range class.Properties { + notReference := len(p.DataType) == 1 && knownDataTypes.Contains(DataType(p.DataType[0])) + if notReference { + properties = append(properties, Property{ + Name: p.Name, + Description: p.Description, + DataType: DataType(p.DataType[0]), + NestedProperties: nestedPropertiesFromREST(p.NestedProperties), + Tokenization: Tokenization(p.Tokenization), + IndexFilterable: p.IndexFilterable, + IndexRangeFilters: p.IndexRangeFilters, + IndexSearchable: p.IndexSearchable, + }) + } else { + references = append(references, ReferenceProperty{ + Name: p.Name, + Collections: p.DataType, + }) + } + } + + var sharding ShardingConfig + if len(class.ShardingConfig) > 0 { + // In case any of the fields are not ints, the cast will return a zero value. + // We explicitly ignore the checks _ to avoid runtime panics in such cases. + sharding.DesiredCount = int(class.ShardingConfig["desiredCount"].(float64)) + sharding.DesiredVirtualCount = int(class.ShardingConfig["desiredVirtualCount"].(float64)) + sharding.VirtualPerPhysical = int(class.ShardingConfig["virtualPerPhysical"].(float64)) + } + + *c = Collection{ + Name: class.Class, + Description: class.Description, + Properties: properties, + References: references, + Replication: &ReplicationConfig{ + AsyncEnabled: class.ReplicationConfig.AsyncEnabled, + Factor: class.ReplicationConfig.Factor, + DeletionStrategy: DeletionStrategy(class.ReplicationConfig.DeletionStrategy), + AsyncReplication: &AsyncReplicationConfig{ + DiffBatchSize: class.ReplicationConfig.AsyncConfig.DiffBatchSize, + DiffPerNodeTimeout: time.Duration(class.ReplicationConfig.AsyncConfig.DiffPerNodeTimeout) * time.Second, + ReplicationConcurrency: class.ReplicationConfig.AsyncConfig.MaxWorkers, + ReplicationFrequency: time.Duration(class.ReplicationConfig.AsyncConfig.Frequency) * time.Millisecond, + ReplicationFrequencyPropagating: time.Duration(class.ReplicationConfig.AsyncConfig.FrequencyWhilePropagating) * time.Millisecond, + PrePropagationTimeout: time.Duration(class.ReplicationConfig.AsyncConfig.PrePropagationTimeout) * time.Second, + PropagationConcurrency: class.ReplicationConfig.AsyncConfig.PropagationConcurrency, + PropagationBatchSize: class.ReplicationConfig.AsyncConfig.PropagationBatchSize, + PropagationLimit: class.ReplicationConfig.AsyncConfig.PropagationLimit, + PropagationTimeout: time.Duration(class.ReplicationConfig.AsyncConfig.PropagationTimeout) * time.Second, + PropagationDelay: time.Duration(class.ReplicationConfig.AsyncConfig.PropagationDelay) * time.Millisecond, + HashTreeHeight: class.ReplicationConfig.AsyncConfig.HashtreeHeight, + NodePingFrequency: time.Duration(class.ReplicationConfig.AsyncConfig.AliveNodesCheckingFrequency) * time.Millisecond, + LoggingFrequency: time.Duration(class.ReplicationConfig.AsyncConfig.LoggingFrequency) * time.Second, + }, + }, + InvertedIndex: &InvertedIndexConfig{ + IndexNullState: class.InvertedIndexConfig.IndexNullState, + IndexPropertyLength: class.InvertedIndexConfig.IndexPropertyLength, + IndexTimestamps: class.InvertedIndexConfig.IndexTimestamps, + UsingBlockMaxWAND: class.InvertedIndexConfig.UsingBlockMaxWAND, + CleanupIntervalSeconds: class.InvertedIndexConfig.CleanupIntervalSeconds, + Stopwords: (*StopwordConfig)(&class.InvertedIndexConfig.Stopwords), + BM25: &BM25Config{ + B: class.InvertedIndexConfig.Bm25.B, + K1: class.InvertedIndexConfig.Bm25.K1, + }, + }, + Sharding: &sharding, + MultiTenancy: &MultiTenancyConfig{ + Enabled: class.MultiTenancyConfig.Enabled, + AutoTenantCreation: class.MultiTenancyConfig.AutoTenantCreation, + AutoTenantActivation: class.MultiTenancyConfig.AutoTenantActivation, + }, + } + + return nil +} + +func nestedPropertiesFromREST(nested []rest.NestedProperty) []Property { + if len(nested) == 0 { + return nil + } + + nps := make([]Property, 0, len(nested)) + for _, np := range nested { + if len(np.DataType) != 1 { + // Invalid response -- nested property must have exactly 1 data type. + continue + } + + nps = append(nps, Property{ + Name: np.Name, + Description: np.Description, + DataType: DataType(np.DataType[0]), + NestedProperties: nestedPropertiesFromREST(np.NestedProperties), + Tokenization: Tokenization(np.Tokenization), + IndexFilterable: np.IndexFilterable, + IndexRangeFilters: np.IndexRangeFilters, + IndexSearchable: np.IndexSearchable, + }) + } + return nps +} + +func newSet[Slice ~[]E, E comparable](values Slice) set[E] { + set := make(set[E], len(values)) + for _, v := range values { + set.Add(v) + } + return set +} + +// set is a lightweight set implementation based on on map[string]struct{}. +// It uses struct{} as a value type, because empty structs do not use memory. +type set[E comparable] map[E]struct{} + +func (s set[E]) Add(v E) { + s[v] = struct{}{} +} + +func (s set[E]) Contains(v E) bool { + _, ok := s[v] + return ok +} diff --git a/internal/api/endpoint_test.go b/internal/api/endpoint_test.go index e2c340c7..5e8c3d66 100644 --- a/internal/api/endpoint_test.go +++ b/internal/api/endpoint_test.go @@ -5,6 +5,7 @@ import ( "net/http" "net/url" "testing" + "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -200,6 +201,220 @@ func TestRESTRequests(t *testing.T) { wantMethod: http.MethodDelete, wantPath: "/objects/Songs/" + testkit.UUID.String(), }, + { + name: "create collection (full config)", + req: &api.CreateCollectionRequest{ + Collection: api.Collection{ + Name: "Songs", + Description: "My favorite songs", + Properties: []api.Property{ + {Name: "title", DataType: api.DataTypeText}, + {Name: "genres", DataType: api.DataTypeTextArray}, + {Name: "single", DataType: api.DataTypeBool}, + {Name: "year", DataType: api.DataTypeInt}, + { + Name: "lyrics", + DataType: api.DataTypeInt, + Tokenization: api.TokenizationTrigram, + IndexFilterable: true, + IndexRangeFilters: true, + IndexSearchable: true, + }, + { + Name: "metadata", DataType: api.DataTypeObject, + NestedProperties: []api.Property{ + {Name: "duration", DataType: api.DataTypeNumber}, + {Name: "uploadedTime", DataType: api.DataTypeDate}, + }, + Tokenization: api.TokenizationWhitespace, + IndexFilterable: true, + IndexRangeFilters: true, + IndexSearchable: true, + }, + }, + References: []api.ReferenceProperty{ + { + Name: "artist", + Collections: []string{"Singers", "Bands"}, + }, + }, + Sharding: &api.ShardingConfig{ + DesiredCount: 3, + DesiredVirtualCount: 150, + VirtualPerPhysical: 50, + }, + Replication: &api.ReplicationConfig{ + AsyncEnabled: false, + Factor: 6, + DeletionStrategy: api.TimeBasedResolution, + AsyncReplication: &api.AsyncReplicationConfig{ + DiffBatchSize: 1, + DiffPerNodeTimeout: 2 * time.Second, + ReplicationConcurrency: 3, + ReplicationFrequency: 4 * time.Millisecond, + ReplicationFrequencyPropagating: 5 * time.Millisecond, + PrePropagationTimeout: 6 * time.Second, + PropagationConcurrency: 7, + PropagationBatchSize: 8, + PropagationLimit: 9, + PropagationTimeout: 10 * time.Second, + PropagationDelay: 11 * time.Millisecond, + HashTreeHeight: 12, + NodePingFrequency: 13 * time.Millisecond, + LoggingFrequency: 14 * time.Second, + }, + }, + InvertedIndex: &api.InvertedIndexConfig{ + IndexNullState: true, + IndexPropertyLength: true, + IndexTimestamps: true, + UsingBlockMaxWAND: true, + CleanupIntervalSeconds: 92, + BM25: &api.BM25Config{ + B: 25, + K1: 1, + }, + Stopwords: &api.StopwordConfig{ + Preset: "standard-please-stop", + Additions: []string{"end"}, + Removals: []string{"terminate"}, + }, + }, + MultiTenancy: &api.MultiTenancyConfig{ + Enabled: true, + AutoTenantActivation: true, + AutoTenantCreation: false, + }, + }, + }, + wantMethod: http.MethodPost, + wantPath: "/schema", + wantBody: &rest.Class{ + Class: "Songs", + Description: "My favorite songs", + Properties: []rest.Property{ + {Name: "title", DataType: []string{string(api.DataTypeText)}}, + {Name: "genres", DataType: []string{string(api.DataTypeTextArray)}}, + {Name: "single", DataType: []string{string(api.DataTypeBool)}}, + {Name: "year", DataType: []string{string(api.DataTypeInt)}}, + { + Name: "lyrics", + DataType: []string{string(api.DataTypeInt)}, + Tokenization: rest.PropertyTokenizationTrigram, + IndexFilterable: true, + IndexRangeFilters: true, + IndexSearchable: true, + }, + { + Name: "metadata", DataType: []string{string(api.DataTypeObject)}, + NestedProperties: []rest.NestedProperty{ + {Name: "duration", DataType: []string{string(api.DataTypeNumber)}}, + {Name: "uploadedTime", DataType: []string{string(api.DataTypeDate)}}, + }, + Tokenization: rest.PropertyTokenizationWhitespace, + IndexFilterable: true, + IndexRangeFilters: true, + IndexSearchable: true, + }, + { + Name: "artist", + DataType: []string{"Singers", "Bands"}, + }, + }, + ShardingConfig: map[string]any{ + "desiredCount": 3, + "desiredVirtualCount": 150, + "virtualPerPhysical": 50, + }, + ReplicationConfig: rest.ReplicationConfig{ + AsyncEnabled: false, + Factor: 6, + DeletionStrategy: rest.TimeBasedResolution, + AsyncConfig: rest.ReplicationAsyncConfig{ + DiffBatchSize: 1, + DiffPerNodeTimeout: 2, + MaxWorkers: 3, + Frequency: 4, + FrequencyWhilePropagating: 5, + PrePropagationTimeout: 6, + PropagationConcurrency: 7, + PropagationBatchSize: 8, + PropagationLimit: 9, + PropagationTimeout: 10, + PropagationDelay: 11, + HashtreeHeight: 12, + AliveNodesCheckingFrequency: 13, + LoggingFrequency: 14, + }, + }, + InvertedIndexConfig: rest.InvertedIndexConfig{ + IndexNullState: true, + IndexPropertyLength: true, + IndexTimestamps: true, + UsingBlockMaxWAND: true, + CleanupIntervalSeconds: 92, + Bm25: rest.BM25Config{ + B: 25, + K1: 1, + }, + Stopwords: rest.StopwordConfig{ + Preset: "standard-please-stop", + Additions: []string{"end"}, + Removals: []string{"terminate"}, + }, + }, + MultiTenancyConfig: rest.MultiTenancyConfig{ + Enabled: true, + AutoTenantActivation: true, + AutoTenantCreation: false, + }, + }, + }, + { + name: "create collection (partial config)", + req: &api.CreateCollectionRequest{ + Collection: api.Collection{ + Name: "Songs", + Description: "My favorite songs", + Properties: []api.Property{ + {Name: "title", DataType: api.DataTypeText}, + {Name: "genres", DataType: api.DataTypeTextArray}, + {Name: "single", DataType: api.DataTypeBool}, + {Name: "year", DataType: api.DataTypeInt}, + }, + }, + }, + wantMethod: http.MethodPost, + wantPath: "/schema", + wantBody: &rest.Class{ + Class: "Songs", + Description: "My favorite songs", + Properties: []rest.Property{ + {Name: "title", DataType: []string{string(api.DataTypeText)}}, + {Name: "genres", DataType: []string{string(api.DataTypeTextArray)}}, + {Name: "single", DataType: []string{string(api.DataTypeBool)}}, + {Name: "year", DataType: []string{string(api.DataTypeInt)}}, + }, + }, + }, + { + name: "get collection config", + req: api.GetCollectionRequest("Songs"), + wantMethod: http.MethodGet, + wantPath: "/schema/Songs", + }, + { + name: "list collections", + req: api.ListCollectionsRequest, + wantMethod: http.MethodGet, + wantPath: "/schema", + }, + { + name: "delete collection", + req: api.DeleteCollectionRequest("Songs"), + wantMethod: http.MethodDelete, + wantPath: "/schema/Songs", + }, } { t.Run(tt.name, func(t *testing.T) { require.Implements(t, (*transports.Endpoint)(nil), tt.req) @@ -330,11 +545,177 @@ func TestRESTResponses(t *testing.T) { }, }, }, + { + name: "collection config", + body: &rest.Class{ + Class: "Songs", + Description: "My favorite songs", + Properties: []rest.Property{ + {Name: "title", DataType: []string{string(api.DataTypeText)}}, + {Name: "genres", DataType: []string{string(api.DataTypeTextArray)}}, + {Name: "single", DataType: []string{string(api.DataTypeBool)}}, + {Name: "year", DataType: []string{string(api.DataTypeInt)}}, + { + Name: "lyrics", + DataType: []string{string(api.DataTypeInt)}, + Tokenization: rest.PropertyTokenizationTrigram, + IndexFilterable: true, + IndexRangeFilters: true, + IndexSearchable: true, + }, + { + Name: "metadata", DataType: []string{string(api.DataTypeObject)}, + NestedProperties: []rest.NestedProperty{ + {Name: "duration", DataType: []string{string(api.DataTypeNumber)}}, + {Name: "uploadedTime", DataType: []string{string(api.DataTypeDate)}}, + }, + Tokenization: rest.PropertyTokenizationWhitespace, + IndexFilterable: true, + IndexRangeFilters: true, + IndexSearchable: true, + }, + { + Name: "artist", + DataType: []string{"Singers", "Bands"}, + }, + }, + ShardingConfig: map[string]any{ + "desiredCount": 3, + "desiredVirtualCount": 150, + "virtualPerPhysical": 50, + }, + ReplicationConfig: rest.ReplicationConfig{ + AsyncEnabled: false, + Factor: 6, + DeletionStrategy: rest.TimeBasedResolution, + AsyncConfig: rest.ReplicationAsyncConfig{ + DiffBatchSize: 1, + DiffPerNodeTimeout: 2, + MaxWorkers: 3, + Frequency: 4, + FrequencyWhilePropagating: 5, + PrePropagationTimeout: 6, + PropagationConcurrency: 7, + PropagationBatchSize: 8, + PropagationLimit: 9, + PropagationTimeout: 10, + PropagationDelay: 11, + HashtreeHeight: 12, + AliveNodesCheckingFrequency: 13, + LoggingFrequency: 14, + }, + }, + InvertedIndexConfig: rest.InvertedIndexConfig{ + IndexNullState: true, + IndexPropertyLength: true, + IndexTimestamps: true, + UsingBlockMaxWAND: true, + CleanupIntervalSeconds: 92, + Bm25: rest.BM25Config{ + B: 25, + K1: 1, + }, + Stopwords: rest.StopwordConfig{ + Preset: "standard-please-stop", + Additions: []string{"end"}, + Removals: []string{"terminate"}, + }, + }, + MultiTenancyConfig: rest.MultiTenancyConfig{ + Enabled: true, + AutoTenantActivation: true, + AutoTenantCreation: false, + }, + }, + dest: new(api.Collection), + want: &api.Collection{ + Name: "Songs", + Description: "My favorite songs", + Properties: []api.Property{ + {Name: "title", DataType: api.DataTypeText}, + {Name: "genres", DataType: api.DataTypeTextArray}, + {Name: "single", DataType: api.DataTypeBool}, + {Name: "year", DataType: api.DataTypeInt}, + { + Name: "lyrics", + DataType: api.DataTypeInt, + Tokenization: api.TokenizationTrigram, + IndexFilterable: true, + IndexRangeFilters: true, + IndexSearchable: true, + }, + { + Name: "metadata", DataType: api.DataTypeObject, + NestedProperties: []api.Property{ + {Name: "duration", DataType: api.DataTypeNumber}, + {Name: "uploadedTime", DataType: api.DataTypeDate}, + }, + Tokenization: api.TokenizationWhitespace, + IndexFilterable: true, + IndexRangeFilters: true, + IndexSearchable: true, + }, + }, + References: []api.ReferenceProperty{ + { + Name: "artist", + Collections: []string{"Singers", "Bands"}, + }, + }, + Sharding: &api.ShardingConfig{ + DesiredCount: 3, + DesiredVirtualCount: 150, + VirtualPerPhysical: 50, + }, + Replication: &api.ReplicationConfig{ + AsyncEnabled: false, + Factor: 6, + DeletionStrategy: api.TimeBasedResolution, + AsyncReplication: &api.AsyncReplicationConfig{ + DiffBatchSize: 1, + DiffPerNodeTimeout: 2 * time.Second, + ReplicationConcurrency: 3, + ReplicationFrequency: 4 * time.Millisecond, + ReplicationFrequencyPropagating: 5 * time.Millisecond, + PrePropagationTimeout: 6 * time.Second, + PropagationConcurrency: 7, + PropagationBatchSize: 8, + PropagationLimit: 9, + PropagationTimeout: 10 * time.Second, + PropagationDelay: 11 * time.Millisecond, + HashTreeHeight: 12, + NodePingFrequency: 13 * time.Millisecond, + LoggingFrequency: 14 * time.Second, + }, + }, + InvertedIndex: &api.InvertedIndexConfig{ + IndexNullState: true, + IndexPropertyLength: true, + IndexTimestamps: true, + UsingBlockMaxWAND: true, + CleanupIntervalSeconds: 92, + BM25: &api.BM25Config{ + B: 25, + K1: 1, + }, + Stopwords: &api.StopwordConfig{ + Preset: "standard-please-stop", + Additions: []string{"end"}, + Removals: []string{"terminate"}, + }, + }, + MultiTenancy: &api.MultiTenancyConfig{ + Enabled: true, + AutoTenantActivation: true, + AutoTenantCreation: false, + }, + }, + }, } { t.Run(tt.name, func(t *testing.T) { require.NotNil(t, tt.body, "incomplete test case: body is nil") - testkit.IsPointer(t, tt.body, "body") - testkit.IsPointer(t, tt.dest, "dest") + testkit.RequirePointer(t, tt.body, "body") + testkit.RequirePointer(t, tt.dest, "dest") body, err := json.Marshal(tt.body) require.NoError(t, err, "marshal expected body") diff --git a/internal/api/exists.go b/internal/api/exists.go new file mode 100644 index 00000000..15d39288 --- /dev/null +++ b/internal/api/exists.go @@ -0,0 +1,54 @@ +package api + +import ( + "encoding/json" + "net/http" + + "github.com/weaviate/weaviate-go-client/v6/internal/dev" + "github.com/weaviate/weaviate-go-client/v6/internal/transports" +) + +// ResourceExistsResponse is true if the requested resource exists. +// +// Weaviate does not support HEAD requests for _certain_ resources, +// so, in order to check if a collection or an RBAC role exist, +// the client has to GET that resource instead. Unmarshaling the +// response body of that request is unnecessary, as we are only +// interested in a simple yes/no answer. +// A request that returns HTTP 404 is a "no" and any other +// successful response is a "yes". +// +// Example: +// +// func SongExists(ctx context.Context, resourceID string) (bool, error) { +// req := api.GetSongRequest(ctx, resourceID) +// var resp api.ResourceExistsResponse +// if err := transport.Do(ctx, req); err != nil { +// return false, err +// } +// return resp.Bool(), nil +// } +type ResourceExistsResponse bool + +// Bool returns bool value of ResourceExistsResponse. +func (exists ResourceExistsResponse) Bool() bool { + return bool(exists) +} + +var ( + _ json.Unmarshaler = (*ResourceExistsResponse)(nil) + _ transports.StatusAccepter = (*ResourceExistsResponse)(nil) +) + +// AcceptStatus implements transport.StatusAccepter. +func (exists ResourceExistsResponse) AcceptStatus(code int) bool { + return code == http.StatusNotFound +} + +// UnmarshalJSON implements json.Unmarshaler. +func (exists *ResourceExistsResponse) UnmarshalJSON(_ []byte) error { + dev.AssertNotNil(exists, "exists") + + *exists = true + return nil +} diff --git a/internal/api/exists_test.go b/internal/api/exists_test.go new file mode 100644 index 00000000..4c1a2ef1 --- /dev/null +++ b/internal/api/exists_test.go @@ -0,0 +1,55 @@ +package api_test + +import ( + "encoding/json" + "net/http" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/weaviate/weaviate-go-client/v6/internal/api" + "github.com/weaviate/weaviate-go-client/v6/internal/transports" +) + +func TestResourceExistsResponse(t *testing.T) { + t.Run("http response", func(t *testing.T) { + var exists api.ResourceExistsResponse + assert.False(t, exists.Bool(), "Bool() of zero value") + + if assert.Implements(t, (*transports.StatusAccepter)(nil), exists) { + acc := (any(exists)).(transports.StatusAccepter) + assert.True(t, acc.AcceptStatus(http.StatusNotFound), "must accept 404 Not Found") + + // Check all other error status codes, fail fast. + for status := 300; status < 600; status++ { + if status == http.StatusNotFound { + continue + } + ok := assert.Falsef(t, acc.AcceptStatus(status), "must not accept HTTP %d", status) + if !ok { + break + } + } + } + + assert.Implements(t, (*json.Unmarshaler)(nil), &exists) + for _, body := range []string{ + `{}`, `[]`, `[{}, {}]`, + `{"accept": "any", "valid": "json"}`, + } { + require.NoError(t, + json.Unmarshal([]byte(body), &exists), + "ResourceExistsResponse.UnmarshalJSON must accept any valid JSON", + ) + } + + assert.True(t, exists.Bool(), "Bool() after UnmarshalJSON") + }) + + t.Run("nil receiver", func(t *testing.T) { + var exists *api.ResourceExistsResponse + require.Panics(t, func() { + exists.UnmarshalJSON(nil) //nolint:errcheck + }) + }) +} diff --git a/internal/api/internal/gen/proto/v1/base_search.pb.go b/internal/api/internal/gen/proto/v1/base_search.pb.go index 00bbff2c..ba533926 100644 --- a/internal/api/internal/gen/proto/v1/base_search.pb.go +++ b/internal/api/internal/gen/proto/v1/base_search.pb.go @@ -410,8 +410,9 @@ type Hybrid struct { // protolint:disable:next REPEATED_FIELD_NAMES_PLURALIZED // // Deprecated: Marked as deprecated in v1/base_search.proto. - Vector []float32 `protobuf:"fixed32,3,rep,packed,name=vector,proto3" json:"vector,omitempty"` // will be removed in the future, use vectors - Alpha float32 `protobuf:"fixed32,4,opt,name=alpha,proto3" json:"alpha,omitempty"` + Vector []float32 `protobuf:"fixed32,3,rep,packed,name=vector,proto3" json:"vector,omitempty"` // will be removed in the future, use vectors + // Deprecated: Marked as deprecated in v1/base_search.proto. + Alpha float32 `protobuf:"fixed32,4,opt,name=alpha,proto3" json:"alpha,omitempty"` // deprecated in 1.36.0 - use alpha_param FusionType Hybrid_FusionType `protobuf:"varint,5,opt,name=fusion_type,json=fusionType,proto3,enum=weaviate.v1.Hybrid_FusionType" json:"fusion_type,omitempty"` // Deprecated: Marked as deprecated in v1/base_search.proto. VectorBytes []byte `protobuf:"bytes,6,opt,name=vector_bytes,json=vectorBytes,proto3" json:"vector_bytes,omitempty"` // deprecated in 1.29.0 - use vectors @@ -421,6 +422,7 @@ type Hybrid struct { NearVector *NearVector `protobuf:"bytes,9,opt,name=near_vector,json=nearVector,proto3" json:"near_vector,omitempty"` // same as above. Use the target vector in the hybrid message Targets *Targets `protobuf:"bytes,10,opt,name=targets,proto3" json:"targets,omitempty"` Bm25SearchOperator *SearchOperatorOptions `protobuf:"bytes,11,opt,name=bm25_search_operator,json=bm25SearchOperator,proto3,oneof" json:"bm25_search_operator,omitempty"` + AlphaParam *float32 `protobuf:"fixed32,12,opt,name=alpha_param,json=alphaParam,proto3,oneof" json:"alpha_param,omitempty"` // only vector distance, but keep it extendable // // Types that are valid to be assigned to Threshold: @@ -484,6 +486,7 @@ func (x *Hybrid) GetVector() []float32 { return nil } +// Deprecated: Marked as deprecated in v1/base_search.proto. func (x *Hybrid) GetAlpha() float32 { if x != nil { return x.Alpha @@ -542,6 +545,13 @@ func (x *Hybrid) GetBm25SearchOperator() *SearchOperatorOptions { return nil } +func (x *Hybrid) GetAlphaParam() float32 { + if x != nil && x.AlphaParam != nil { + return *x.AlphaParam + } + return 0 +} + func (x *Hybrid) GetThreshold() isHybrid_Threshold { if x != nil { return x.Threshold @@ -1477,14 +1487,14 @@ const file_v1_base_search_proto_rawDesc = "" + "\x14OPERATOR_UNSPECIFIED\x10\x00\x12\x0f\n" + "\vOPERATOR_OR\x10\x01\x12\x10\n" + "\fOPERATOR_AND\x10\x02B\x1a\n" + - "\x18_minimum_or_tokens_match\"\xe6\x05\n" + + "\x18_minimum_or_tokens_match\"\xa0\x06\n" + "\x06Hybrid\x12\x14\n" + "\x05query\x18\x01 \x01(\tR\x05query\x12\x1e\n" + "\n" + "properties\x18\x02 \x03(\tR\n" + "properties\x12\x1a\n" + - "\x06vector\x18\x03 \x03(\x02B\x02\x18\x01R\x06vector\x12\x14\n" + - "\x05alpha\x18\x04 \x01(\x02R\x05alpha\x12?\n" + + "\x06vector\x18\x03 \x03(\x02B\x02\x18\x01R\x06vector\x12\x18\n" + + "\x05alpha\x18\x04 \x01(\x02B\x02\x18\x01R\x05alpha\x12?\n" + "\vfusion_type\x18\x05 \x01(\x0e2\x1e.weaviate.v1.Hybrid.FusionTypeR\n" + "fusionType\x12%\n" + "\fvector_bytes\x18\x06 \x01(\fB\x02\x18\x01R\vvectorBytes\x12)\n" + @@ -1494,7 +1504,9 @@ const file_v1_base_search_proto_rawDesc = "" + "nearVector\x12.\n" + "\atargets\x18\n" + " \x01(\v2\x14.weaviate.v1.TargetsR\atargets\x12Y\n" + - "\x14bm25_search_operator\x18\v \x01(\v2\".weaviate.v1.SearchOperatorOptionsH\x01R\x12bm25SearchOperator\x88\x01\x01\x12)\n" + + "\x14bm25_search_operator\x18\v \x01(\v2\".weaviate.v1.SearchOperatorOptionsH\x01R\x12bm25SearchOperator\x88\x01\x01\x12$\n" + + "\valpha_param\x18\f \x01(\x02H\x02R\n" + + "alphaParam\x88\x01\x01\x12)\n" + "\x0fvector_distance\x18\x14 \x01(\x02H\x00R\x0evectorDistance\x12.\n" + "\avectors\x18\x15 \x03(\v2\x14.weaviate.v1.VectorsR\avectors\"a\n" + "\n" + @@ -1503,7 +1515,8 @@ const file_v1_base_search_proto_rawDesc = "" + "\x12FUSION_TYPE_RANKED\x10\x01\x12\x1e\n" + "\x1aFUSION_TYPE_RELATIVE_SCORE\x10\x02B\v\n" + "\tthresholdB\x17\n" + - "\x15_bm25_search_operator\"\xa7\x04\n" + + "\x15_bm25_search_operatorB\x0e\n" + + "\f_alpha_param\"\xa7\x04\n" + "\n" + "NearVector\x12\x1a\n" + "\x06vector\x18\x01 \x03(\x02B\x02\x18\x01R\x06vector\x12!\n" + diff --git a/internal/api/internal/gen/proto/v1/batch.pb.go b/internal/api/internal/gen/proto/v1/batch.pb.go index 39e78119..8dd711fe 100644 --- a/internal/api/internal/gen/proto/v1/batch.pb.go +++ b/internal/api/internal/gen/proto/v1/batch.pb.go @@ -230,7 +230,6 @@ type BatchStreamReply struct { // // *BatchStreamReply_Results_ // *BatchStreamReply_ShuttingDown_ - // *BatchStreamReply_Shutdown_ // *BatchStreamReply_Started_ // *BatchStreamReply_Backoff_ // *BatchStreamReply_Acks_ @@ -295,15 +294,6 @@ func (x *BatchStreamReply) GetShuttingDown() *BatchStreamReply_ShuttingDown { return nil } -func (x *BatchStreamReply) GetShutdown() *BatchStreamReply_Shutdown { - if x != nil { - if x, ok := x.Message.(*BatchStreamReply_Shutdown_); ok { - return x.Shutdown - } - } - return nil -} - func (x *BatchStreamReply) GetStarted() *BatchStreamReply_Started { if x != nil { if x, ok := x.Message.(*BatchStreamReply_Started_); ok { @@ -352,10 +342,6 @@ type BatchStreamReply_ShuttingDown_ struct { ShuttingDown *BatchStreamReply_ShuttingDown `protobuf:"bytes,2,opt,name=shutting_down,json=shuttingDown,proto3,oneof"` } -type BatchStreamReply_Shutdown_ struct { - Shutdown *BatchStreamReply_Shutdown `protobuf:"bytes,3,opt,name=shutdown,proto3,oneof"` -} - type BatchStreamReply_Started_ struct { Started *BatchStreamReply_Started `protobuf:"bytes,4,opt,name=started,proto3,oneof"` } @@ -376,8 +362,6 @@ func (*BatchStreamReply_Results_) isBatchStreamReply_Message() {} func (*BatchStreamReply_ShuttingDown_) isBatchStreamReply_Message() {} -func (*BatchStreamReply_Shutdown_) isBatchStreamReply_Message() {} - func (*BatchStreamReply_Started_) isBatchStreamReply_Message() {} func (*BatchStreamReply_Backoff_) isBatchStreamReply_Message() {} @@ -963,53 +947,20 @@ func (*BatchStreamReply_ShuttingDown) Descriptor() ([]byte, []int) { return file_v1_batch_proto_rawDescGZIP(), []int{3, 1} } -type BatchStreamReply_Shutdown struct { - state protoimpl.MessageState `protogen:"open.v1"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *BatchStreamReply_Shutdown) Reset() { - *x = BatchStreamReply_Shutdown{} - mi := &file_v1_batch_proto_msgTypes[15] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *BatchStreamReply_Shutdown) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*BatchStreamReply_Shutdown) ProtoMessage() {} - -func (x *BatchStreamReply_Shutdown) ProtoReflect() protoreflect.Message { - mi := &file_v1_batch_proto_msgTypes[15] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use BatchStreamReply_Shutdown.ProtoReflect.Descriptor instead. -func (*BatchStreamReply_Shutdown) Descriptor() ([]byte, []int) { - return file_v1_batch_proto_rawDescGZIP(), []int{3, 2} -} - type BatchStreamReply_OutOfMemory struct { - state protoimpl.MessageState `protogen:"open.v1"` - Uuids []string `protobuf:"bytes,1,rep,name=uuids,proto3" json:"uuids,omitempty"` - Beacons []string `protobuf:"bytes,2,rep,name=beacons,proto3" json:"beacons,omitempty"` + state protoimpl.MessageState `protogen:"open.v1"` + Uuids []string `protobuf:"bytes,1,rep,name=uuids,proto3" json:"uuids,omitempty"` + Beacons []string `protobuf:"bytes,2,rep,name=beacons,proto3" json:"beacons,omitempty"` + // How long to wait until ShuttingDown is sent, in seconds + // If ShuttingDown is not set by this time, the client should exit the stream + WaitTime int32 `protobuf:"varint,3,opt,name=wait_time,json=waitTime,proto3" json:"wait_time,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *BatchStreamReply_OutOfMemory) Reset() { *x = BatchStreamReply_OutOfMemory{} - mi := &file_v1_batch_proto_msgTypes[16] + mi := &file_v1_batch_proto_msgTypes[15] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1021,7 +972,7 @@ func (x *BatchStreamReply_OutOfMemory) String() string { func (*BatchStreamReply_OutOfMemory) ProtoMessage() {} func (x *BatchStreamReply_OutOfMemory) ProtoReflect() protoreflect.Message { - mi := &file_v1_batch_proto_msgTypes[16] + mi := &file_v1_batch_proto_msgTypes[15] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1034,7 +985,7 @@ func (x *BatchStreamReply_OutOfMemory) ProtoReflect() protoreflect.Message { // Deprecated: Use BatchStreamReply_OutOfMemory.ProtoReflect.Descriptor instead. func (*BatchStreamReply_OutOfMemory) Descriptor() ([]byte, []int) { - return file_v1_batch_proto_rawDescGZIP(), []int{3, 3} + return file_v1_batch_proto_rawDescGZIP(), []int{3, 2} } func (x *BatchStreamReply_OutOfMemory) GetUuids() []string { @@ -1051,6 +1002,13 @@ func (x *BatchStreamReply_OutOfMemory) GetBeacons() []string { return nil } +func (x *BatchStreamReply_OutOfMemory) GetWaitTime() int32 { + if x != nil { + return x.WaitTime + } + return 0 +} + type BatchStreamReply_Backoff struct { state protoimpl.MessageState `protogen:"open.v1"` BatchSize int32 `protobuf:"varint,1,opt,name=batch_size,json=batchSize,proto3" json:"batch_size,omitempty"` @@ -1060,7 +1018,7 @@ type BatchStreamReply_Backoff struct { func (x *BatchStreamReply_Backoff) Reset() { *x = BatchStreamReply_Backoff{} - mi := &file_v1_batch_proto_msgTypes[17] + mi := &file_v1_batch_proto_msgTypes[16] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1072,7 +1030,7 @@ func (x *BatchStreamReply_Backoff) String() string { func (*BatchStreamReply_Backoff) ProtoMessage() {} func (x *BatchStreamReply_Backoff) ProtoReflect() protoreflect.Message { - mi := &file_v1_batch_proto_msgTypes[17] + mi := &file_v1_batch_proto_msgTypes[16] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1085,7 +1043,7 @@ func (x *BatchStreamReply_Backoff) ProtoReflect() protoreflect.Message { // Deprecated: Use BatchStreamReply_Backoff.ProtoReflect.Descriptor instead. func (*BatchStreamReply_Backoff) Descriptor() ([]byte, []int) { - return file_v1_batch_proto_rawDescGZIP(), []int{3, 4} + return file_v1_batch_proto_rawDescGZIP(), []int{3, 3} } func (x *BatchStreamReply_Backoff) GetBatchSize() int32 { @@ -1105,7 +1063,7 @@ type BatchStreamReply_Acks struct { func (x *BatchStreamReply_Acks) Reset() { *x = BatchStreamReply_Acks{} - mi := &file_v1_batch_proto_msgTypes[18] + mi := &file_v1_batch_proto_msgTypes[17] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1117,7 +1075,7 @@ func (x *BatchStreamReply_Acks) String() string { func (*BatchStreamReply_Acks) ProtoMessage() {} func (x *BatchStreamReply_Acks) ProtoReflect() protoreflect.Message { - mi := &file_v1_batch_proto_msgTypes[18] + mi := &file_v1_batch_proto_msgTypes[17] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1130,7 +1088,7 @@ func (x *BatchStreamReply_Acks) ProtoReflect() protoreflect.Message { // Deprecated: Use BatchStreamReply_Acks.ProtoReflect.Descriptor instead. func (*BatchStreamReply_Acks) Descriptor() ([]byte, []int) { - return file_v1_batch_proto_rawDescGZIP(), []int{3, 5} + return file_v1_batch_proto_rawDescGZIP(), []int{3, 4} } func (x *BatchStreamReply_Acks) GetUuids() []string { @@ -1157,7 +1115,7 @@ type BatchStreamReply_Results struct { func (x *BatchStreamReply_Results) Reset() { *x = BatchStreamReply_Results{} - mi := &file_v1_batch_proto_msgTypes[19] + mi := &file_v1_batch_proto_msgTypes[18] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1169,7 +1127,7 @@ func (x *BatchStreamReply_Results) String() string { func (*BatchStreamReply_Results) ProtoMessage() {} func (x *BatchStreamReply_Results) ProtoReflect() protoreflect.Message { - mi := &file_v1_batch_proto_msgTypes[19] + mi := &file_v1_batch_proto_msgTypes[18] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1182,7 +1140,7 @@ func (x *BatchStreamReply_Results) ProtoReflect() protoreflect.Message { // Deprecated: Use BatchStreamReply_Results.ProtoReflect.Descriptor instead. func (*BatchStreamReply_Results) Descriptor() ([]byte, []int) { - return file_v1_batch_proto_rawDescGZIP(), []int{3, 6} + return file_v1_batch_proto_rawDescGZIP(), []int{3, 5} } func (x *BatchStreamReply_Results) GetErrors() []*BatchStreamReply_Results_Error { @@ -1213,7 +1171,7 @@ type BatchStreamReply_Results_Error struct { func (x *BatchStreamReply_Results_Error) Reset() { *x = BatchStreamReply_Results_Error{} - mi := &file_v1_batch_proto_msgTypes[20] + mi := &file_v1_batch_proto_msgTypes[19] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1225,7 +1183,7 @@ func (x *BatchStreamReply_Results_Error) String() string { func (*BatchStreamReply_Results_Error) ProtoMessage() {} func (x *BatchStreamReply_Results_Error) ProtoReflect() protoreflect.Message { - mi := &file_v1_batch_proto_msgTypes[20] + mi := &file_v1_batch_proto_msgTypes[19] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1238,7 +1196,7 @@ func (x *BatchStreamReply_Results_Error) ProtoReflect() protoreflect.Message { // Deprecated: Use BatchStreamReply_Results_Error.ProtoReflect.Descriptor instead. func (*BatchStreamReply_Results_Error) Descriptor() ([]byte, []int) { - return file_v1_batch_proto_rawDescGZIP(), []int{3, 6, 0} + return file_v1_batch_proto_rawDescGZIP(), []int{3, 5, 0} } func (x *BatchStreamReply_Results_Error) GetError() string { @@ -1302,7 +1260,7 @@ type BatchStreamReply_Results_Success struct { func (x *BatchStreamReply_Results_Success) Reset() { *x = BatchStreamReply_Results_Success{} - mi := &file_v1_batch_proto_msgTypes[21] + mi := &file_v1_batch_proto_msgTypes[20] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1314,7 +1272,7 @@ func (x *BatchStreamReply_Results_Success) String() string { func (*BatchStreamReply_Results_Success) ProtoMessage() {} func (x *BatchStreamReply_Results_Success) ProtoReflect() protoreflect.Message { - mi := &file_v1_batch_proto_msgTypes[21] + mi := &file_v1_batch_proto_msgTypes[20] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1327,7 +1285,7 @@ func (x *BatchStreamReply_Results_Success) ProtoReflect() protoreflect.Message { // Deprecated: Use BatchStreamReply_Results_Success.ProtoReflect.Descriptor instead. func (*BatchStreamReply_Results_Success) Descriptor() ([]byte, []int) { - return file_v1_batch_proto_rawDescGZIP(), []int{3, 6, 1} + return file_v1_batch_proto_rawDescGZIP(), []int{3, 5, 1} } func (x *BatchStreamReply_Results_Success) GetDetail() isBatchStreamReply_Results_Success_Detail { @@ -1391,7 +1349,7 @@ type BatchObject_Properties struct { func (x *BatchObject_Properties) Reset() { *x = BatchObject_Properties{} - mi := &file_v1_batch_proto_msgTypes[22] + mi := &file_v1_batch_proto_msgTypes[21] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1403,7 +1361,7 @@ func (x *BatchObject_Properties) String() string { func (*BatchObject_Properties) ProtoMessage() {} func (x *BatchObject_Properties) ProtoReflect() protoreflect.Message { - mi := &file_v1_batch_proto_msgTypes[22] + mi := &file_v1_batch_proto_msgTypes[21] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1499,7 +1457,7 @@ type BatchObject_SingleTargetRefProps struct { func (x *BatchObject_SingleTargetRefProps) Reset() { *x = BatchObject_SingleTargetRefProps{} - mi := &file_v1_batch_proto_msgTypes[23] + mi := &file_v1_batch_proto_msgTypes[22] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1511,7 +1469,7 @@ func (x *BatchObject_SingleTargetRefProps) String() string { func (*BatchObject_SingleTargetRefProps) ProtoMessage() {} func (x *BatchObject_SingleTargetRefProps) ProtoReflect() protoreflect.Message { - mi := &file_v1_batch_proto_msgTypes[23] + mi := &file_v1_batch_proto_msgTypes[22] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1552,7 +1510,7 @@ type BatchObject_MultiTargetRefProps struct { func (x *BatchObject_MultiTargetRefProps) Reset() { *x = BatchObject_MultiTargetRefProps{} - mi := &file_v1_batch_proto_msgTypes[24] + mi := &file_v1_batch_proto_msgTypes[23] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1564,7 +1522,7 @@ func (x *BatchObject_MultiTargetRefProps) String() string { func (*BatchObject_MultiTargetRefProps) ProtoMessage() {} func (x *BatchObject_MultiTargetRefProps) ProtoReflect() protoreflect.Message { - mi := &file_v1_batch_proto_msgTypes[24] + mi := &file_v1_batch_proto_msgTypes[23] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1611,7 +1569,7 @@ type BatchObjectsReply_BatchError struct { func (x *BatchObjectsReply_BatchError) Reset() { *x = BatchObjectsReply_BatchError{} - mi := &file_v1_batch_proto_msgTypes[25] + mi := &file_v1_batch_proto_msgTypes[24] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1623,7 +1581,7 @@ func (x *BatchObjectsReply_BatchError) String() string { func (*BatchObjectsReply_BatchError) ProtoMessage() {} func (x *BatchObjectsReply_BatchError) ProtoReflect() protoreflect.Message { - mi := &file_v1_batch_proto_msgTypes[25] + mi := &file_v1_batch_proto_msgTypes[24] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1663,7 +1621,7 @@ type BatchReferencesReply_BatchError struct { func (x *BatchReferencesReply_BatchError) Reset() { *x = BatchReferencesReply_BatchError{} - mi := &file_v1_batch_proto_msgTypes[26] + mi := &file_v1_batch_proto_msgTypes[25] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1675,7 +1633,7 @@ func (x *BatchReferencesReply_BatchError) String() string { func (*BatchReferencesReply_BatchError) ProtoMessage() {} func (x *BatchReferencesReply_BatchError) ProtoReflect() protoreflect.Message { - mi := &file_v1_batch_proto_msgTypes[26] + mi := &file_v1_batch_proto_msgTypes[25] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1738,22 +1696,20 @@ const file_v1_batch_proto_rawDesc = "" + "\n" + "References\x123\n" + "\x06values\x18\x01 \x03(\v2\x1b.weaviate.v1.BatchReferenceR\x06valuesB\t\n" + - "\amessage\"\x8e\b\n" + + "\amessage\"\xe9\a\n" + "\x10BatchStreamReply\x12A\n" + "\aresults\x18\x01 \x01(\v2%.weaviate.v1.BatchStreamReply.ResultsH\x00R\aresults\x12Q\n" + - "\rshutting_down\x18\x02 \x01(\v2*.weaviate.v1.BatchStreamReply.ShuttingDownH\x00R\fshuttingDown\x12D\n" + - "\bshutdown\x18\x03 \x01(\v2&.weaviate.v1.BatchStreamReply.ShutdownH\x00R\bshutdown\x12A\n" + + "\rshutting_down\x18\x02 \x01(\v2*.weaviate.v1.BatchStreamReply.ShuttingDownH\x00R\fshuttingDown\x12A\n" + "\astarted\x18\x04 \x01(\v2%.weaviate.v1.BatchStreamReply.StartedH\x00R\astarted\x12A\n" + "\abackoff\x18\x05 \x01(\v2%.weaviate.v1.BatchStreamReply.BackoffH\x00R\abackoff\x128\n" + "\x04acks\x18\x06 \x01(\v2\".weaviate.v1.BatchStreamReply.AcksH\x00R\x04acks\x12O\n" + "\rout_of_memory\x18\a \x01(\v2).weaviate.v1.BatchStreamReply.OutOfMemoryH\x00R\voutOfMemory\x1a\t\n" + "\aStarted\x1a\x0e\n" + - "\fShuttingDown\x1a\n" + - "\n" + - "\bShutdown\x1a=\n" + + "\fShuttingDown\x1aZ\n" + "\vOutOfMemory\x12\x14\n" + "\x05uuids\x18\x01 \x03(\tR\x05uuids\x12\x18\n" + - "\abeacons\x18\x02 \x03(\tR\abeacons\x1a(\n" + + "\abeacons\x18\x02 \x03(\tR\abeacons\x12\x1b\n" + + "\twait_time\x18\x03 \x01(\x05R\bwaitTime\x1a(\n" + "\aBackoff\x12\x1d\n" + "\n" + "batch_size\x18\x01 \x01(\x05R\tbatchSize\x1a6\n" + @@ -1772,7 +1728,7 @@ const file_v1_batch_proto_rawDesc = "" + "\x04uuid\x18\x02 \x01(\tH\x00R\x04uuid\x12\x18\n" + "\x06beacon\x18\x03 \x01(\tH\x00R\x06beaconB\b\n" + "\x06detailB\t\n" + - "\amessage\"\xa4\n" + + "\amessageJ\x04\b\x03\x10\x04R\bshutdown\"\xa4\n" + "\n" + "\vBatchObject\x12\x12\n" + "\x04uuid\x18\x01 \x01(\tR\x04uuid\x12\x1a\n" + @@ -1842,7 +1798,7 @@ func file_v1_batch_proto_rawDescGZIP() []byte { return file_v1_batch_proto_rawDescData } -var file_v1_batch_proto_msgTypes = make([]protoimpl.MessageInfo, 27) +var file_v1_batch_proto_msgTypes = make([]protoimpl.MessageInfo, 26) var file_v1_batch_proto_goTypes = []any{ (*BatchObjectsRequest)(nil), // 0: weaviate.v1.BatchObjectsRequest (*BatchReferencesRequest)(nil), // 1: weaviate.v1.BatchReferencesRequest @@ -1859,68 +1815,66 @@ var file_v1_batch_proto_goTypes = []any{ (*BatchStreamRequest_Data_References)(nil), // 12: weaviate.v1.BatchStreamRequest.Data.References (*BatchStreamReply_Started)(nil), // 13: weaviate.v1.BatchStreamReply.Started (*BatchStreamReply_ShuttingDown)(nil), // 14: weaviate.v1.BatchStreamReply.ShuttingDown - (*BatchStreamReply_Shutdown)(nil), // 15: weaviate.v1.BatchStreamReply.Shutdown - (*BatchStreamReply_OutOfMemory)(nil), // 16: weaviate.v1.BatchStreamReply.OutOfMemory - (*BatchStreamReply_Backoff)(nil), // 17: weaviate.v1.BatchStreamReply.Backoff - (*BatchStreamReply_Acks)(nil), // 18: weaviate.v1.BatchStreamReply.Acks - (*BatchStreamReply_Results)(nil), // 19: weaviate.v1.BatchStreamReply.Results - (*BatchStreamReply_Results_Error)(nil), // 20: weaviate.v1.BatchStreamReply.Results.Error - (*BatchStreamReply_Results_Success)(nil), // 21: weaviate.v1.BatchStreamReply.Results.Success - (*BatchObject_Properties)(nil), // 22: weaviate.v1.BatchObject.Properties - (*BatchObject_SingleTargetRefProps)(nil), // 23: weaviate.v1.BatchObject.SingleTargetRefProps - (*BatchObject_MultiTargetRefProps)(nil), // 24: weaviate.v1.BatchObject.MultiTargetRefProps - (*BatchObjectsReply_BatchError)(nil), // 25: weaviate.v1.BatchObjectsReply.BatchError - (*BatchReferencesReply_BatchError)(nil), // 26: weaviate.v1.BatchReferencesReply.BatchError - (ConsistencyLevel)(0), // 27: weaviate.v1.ConsistencyLevel - (*Vectors)(nil), // 28: weaviate.v1.Vectors - (*structpb.Struct)(nil), // 29: google.protobuf.Struct - (*NumberArrayProperties)(nil), // 30: weaviate.v1.NumberArrayProperties - (*IntArrayProperties)(nil), // 31: weaviate.v1.IntArrayProperties - (*TextArrayProperties)(nil), // 32: weaviate.v1.TextArrayProperties - (*BooleanArrayProperties)(nil), // 33: weaviate.v1.BooleanArrayProperties - (*ObjectProperties)(nil), // 34: weaviate.v1.ObjectProperties - (*ObjectArrayProperties)(nil), // 35: weaviate.v1.ObjectArrayProperties + (*BatchStreamReply_OutOfMemory)(nil), // 15: weaviate.v1.BatchStreamReply.OutOfMemory + (*BatchStreamReply_Backoff)(nil), // 16: weaviate.v1.BatchStreamReply.Backoff + (*BatchStreamReply_Acks)(nil), // 17: weaviate.v1.BatchStreamReply.Acks + (*BatchStreamReply_Results)(nil), // 18: weaviate.v1.BatchStreamReply.Results + (*BatchStreamReply_Results_Error)(nil), // 19: weaviate.v1.BatchStreamReply.Results.Error + (*BatchStreamReply_Results_Success)(nil), // 20: weaviate.v1.BatchStreamReply.Results.Success + (*BatchObject_Properties)(nil), // 21: weaviate.v1.BatchObject.Properties + (*BatchObject_SingleTargetRefProps)(nil), // 22: weaviate.v1.BatchObject.SingleTargetRefProps + (*BatchObject_MultiTargetRefProps)(nil), // 23: weaviate.v1.BatchObject.MultiTargetRefProps + (*BatchObjectsReply_BatchError)(nil), // 24: weaviate.v1.BatchObjectsReply.BatchError + (*BatchReferencesReply_BatchError)(nil), // 25: weaviate.v1.BatchReferencesReply.BatchError + (ConsistencyLevel)(0), // 26: weaviate.v1.ConsistencyLevel + (*Vectors)(nil), // 27: weaviate.v1.Vectors + (*structpb.Struct)(nil), // 28: google.protobuf.Struct + (*NumberArrayProperties)(nil), // 29: weaviate.v1.NumberArrayProperties + (*IntArrayProperties)(nil), // 30: weaviate.v1.IntArrayProperties + (*TextArrayProperties)(nil), // 31: weaviate.v1.TextArrayProperties + (*BooleanArrayProperties)(nil), // 32: weaviate.v1.BooleanArrayProperties + (*ObjectProperties)(nil), // 33: weaviate.v1.ObjectProperties + (*ObjectArrayProperties)(nil), // 34: weaviate.v1.ObjectArrayProperties } var file_v1_batch_proto_depIdxs = []int32{ 4, // 0: weaviate.v1.BatchObjectsRequest.objects:type_name -> weaviate.v1.BatchObject - 27, // 1: weaviate.v1.BatchObjectsRequest.consistency_level:type_name -> weaviate.v1.ConsistencyLevel + 26, // 1: weaviate.v1.BatchObjectsRequest.consistency_level:type_name -> weaviate.v1.ConsistencyLevel 5, // 2: weaviate.v1.BatchReferencesRequest.references:type_name -> weaviate.v1.BatchReference - 27, // 3: weaviate.v1.BatchReferencesRequest.consistency_level:type_name -> weaviate.v1.ConsistencyLevel + 26, // 3: weaviate.v1.BatchReferencesRequest.consistency_level:type_name -> weaviate.v1.ConsistencyLevel 8, // 4: weaviate.v1.BatchStreamRequest.start:type_name -> weaviate.v1.BatchStreamRequest.Start 10, // 5: weaviate.v1.BatchStreamRequest.data:type_name -> weaviate.v1.BatchStreamRequest.Data 9, // 6: weaviate.v1.BatchStreamRequest.stop:type_name -> weaviate.v1.BatchStreamRequest.Stop - 19, // 7: weaviate.v1.BatchStreamReply.results:type_name -> weaviate.v1.BatchStreamReply.Results + 18, // 7: weaviate.v1.BatchStreamReply.results:type_name -> weaviate.v1.BatchStreamReply.Results 14, // 8: weaviate.v1.BatchStreamReply.shutting_down:type_name -> weaviate.v1.BatchStreamReply.ShuttingDown - 15, // 9: weaviate.v1.BatchStreamReply.shutdown:type_name -> weaviate.v1.BatchStreamReply.Shutdown - 13, // 10: weaviate.v1.BatchStreamReply.started:type_name -> weaviate.v1.BatchStreamReply.Started - 17, // 11: weaviate.v1.BatchStreamReply.backoff:type_name -> weaviate.v1.BatchStreamReply.Backoff - 18, // 12: weaviate.v1.BatchStreamReply.acks:type_name -> weaviate.v1.BatchStreamReply.Acks - 16, // 13: weaviate.v1.BatchStreamReply.out_of_memory:type_name -> weaviate.v1.BatchStreamReply.OutOfMemory - 22, // 14: weaviate.v1.BatchObject.properties:type_name -> weaviate.v1.BatchObject.Properties - 28, // 15: weaviate.v1.BatchObject.vectors:type_name -> weaviate.v1.Vectors - 25, // 16: weaviate.v1.BatchObjectsReply.errors:type_name -> weaviate.v1.BatchObjectsReply.BatchError - 26, // 17: weaviate.v1.BatchReferencesReply.errors:type_name -> weaviate.v1.BatchReferencesReply.BatchError - 27, // 18: weaviate.v1.BatchStreamRequest.Start.consistency_level:type_name -> weaviate.v1.ConsistencyLevel - 11, // 19: weaviate.v1.BatchStreamRequest.Data.objects:type_name -> weaviate.v1.BatchStreamRequest.Data.Objects - 12, // 20: weaviate.v1.BatchStreamRequest.Data.references:type_name -> weaviate.v1.BatchStreamRequest.Data.References - 4, // 21: weaviate.v1.BatchStreamRequest.Data.Objects.values:type_name -> weaviate.v1.BatchObject - 5, // 22: weaviate.v1.BatchStreamRequest.Data.References.values:type_name -> weaviate.v1.BatchReference - 20, // 23: weaviate.v1.BatchStreamReply.Results.errors:type_name -> weaviate.v1.BatchStreamReply.Results.Error - 21, // 24: weaviate.v1.BatchStreamReply.Results.successes:type_name -> weaviate.v1.BatchStreamReply.Results.Success - 29, // 25: weaviate.v1.BatchObject.Properties.non_ref_properties:type_name -> google.protobuf.Struct - 23, // 26: weaviate.v1.BatchObject.Properties.single_target_ref_props:type_name -> weaviate.v1.BatchObject.SingleTargetRefProps - 24, // 27: weaviate.v1.BatchObject.Properties.multi_target_ref_props:type_name -> weaviate.v1.BatchObject.MultiTargetRefProps - 30, // 28: weaviate.v1.BatchObject.Properties.number_array_properties:type_name -> weaviate.v1.NumberArrayProperties - 31, // 29: weaviate.v1.BatchObject.Properties.int_array_properties:type_name -> weaviate.v1.IntArrayProperties - 32, // 30: weaviate.v1.BatchObject.Properties.text_array_properties:type_name -> weaviate.v1.TextArrayProperties - 33, // 31: weaviate.v1.BatchObject.Properties.boolean_array_properties:type_name -> weaviate.v1.BooleanArrayProperties - 34, // 32: weaviate.v1.BatchObject.Properties.object_properties:type_name -> weaviate.v1.ObjectProperties - 35, // 33: weaviate.v1.BatchObject.Properties.object_array_properties:type_name -> weaviate.v1.ObjectArrayProperties - 34, // [34:34] is the sub-list for method output_type - 34, // [34:34] is the sub-list for method input_type - 34, // [34:34] is the sub-list for extension type_name - 34, // [34:34] is the sub-list for extension extendee - 0, // [0:34] is the sub-list for field type_name + 13, // 9: weaviate.v1.BatchStreamReply.started:type_name -> weaviate.v1.BatchStreamReply.Started + 16, // 10: weaviate.v1.BatchStreamReply.backoff:type_name -> weaviate.v1.BatchStreamReply.Backoff + 17, // 11: weaviate.v1.BatchStreamReply.acks:type_name -> weaviate.v1.BatchStreamReply.Acks + 15, // 12: weaviate.v1.BatchStreamReply.out_of_memory:type_name -> weaviate.v1.BatchStreamReply.OutOfMemory + 21, // 13: weaviate.v1.BatchObject.properties:type_name -> weaviate.v1.BatchObject.Properties + 27, // 14: weaviate.v1.BatchObject.vectors:type_name -> weaviate.v1.Vectors + 24, // 15: weaviate.v1.BatchObjectsReply.errors:type_name -> weaviate.v1.BatchObjectsReply.BatchError + 25, // 16: weaviate.v1.BatchReferencesReply.errors:type_name -> weaviate.v1.BatchReferencesReply.BatchError + 26, // 17: weaviate.v1.BatchStreamRequest.Start.consistency_level:type_name -> weaviate.v1.ConsistencyLevel + 11, // 18: weaviate.v1.BatchStreamRequest.Data.objects:type_name -> weaviate.v1.BatchStreamRequest.Data.Objects + 12, // 19: weaviate.v1.BatchStreamRequest.Data.references:type_name -> weaviate.v1.BatchStreamRequest.Data.References + 4, // 20: weaviate.v1.BatchStreamRequest.Data.Objects.values:type_name -> weaviate.v1.BatchObject + 5, // 21: weaviate.v1.BatchStreamRequest.Data.References.values:type_name -> weaviate.v1.BatchReference + 19, // 22: weaviate.v1.BatchStreamReply.Results.errors:type_name -> weaviate.v1.BatchStreamReply.Results.Error + 20, // 23: weaviate.v1.BatchStreamReply.Results.successes:type_name -> weaviate.v1.BatchStreamReply.Results.Success + 28, // 24: weaviate.v1.BatchObject.Properties.non_ref_properties:type_name -> google.protobuf.Struct + 22, // 25: weaviate.v1.BatchObject.Properties.single_target_ref_props:type_name -> weaviate.v1.BatchObject.SingleTargetRefProps + 23, // 26: weaviate.v1.BatchObject.Properties.multi_target_ref_props:type_name -> weaviate.v1.BatchObject.MultiTargetRefProps + 29, // 27: weaviate.v1.BatchObject.Properties.number_array_properties:type_name -> weaviate.v1.NumberArrayProperties + 30, // 28: weaviate.v1.BatchObject.Properties.int_array_properties:type_name -> weaviate.v1.IntArrayProperties + 31, // 29: weaviate.v1.BatchObject.Properties.text_array_properties:type_name -> weaviate.v1.TextArrayProperties + 32, // 30: weaviate.v1.BatchObject.Properties.boolean_array_properties:type_name -> weaviate.v1.BooleanArrayProperties + 33, // 31: weaviate.v1.BatchObject.Properties.object_properties:type_name -> weaviate.v1.ObjectProperties + 34, // 32: weaviate.v1.BatchObject.Properties.object_array_properties:type_name -> weaviate.v1.ObjectArrayProperties + 33, // [33:33] is the sub-list for method output_type + 33, // [33:33] is the sub-list for method input_type + 33, // [33:33] is the sub-list for extension type_name + 33, // [33:33] is the sub-list for extension extendee + 0, // [0:33] is the sub-list for field type_name } func init() { file_v1_batch_proto_init() } @@ -1939,7 +1893,6 @@ func file_v1_batch_proto_init() { file_v1_batch_proto_msgTypes[3].OneofWrappers = []any{ (*BatchStreamReply_Results_)(nil), (*BatchStreamReply_ShuttingDown_)(nil), - (*BatchStreamReply_Shutdown_)(nil), (*BatchStreamReply_Started_)(nil), (*BatchStreamReply_Backoff_)(nil), (*BatchStreamReply_Acks_)(nil), @@ -1947,11 +1900,11 @@ func file_v1_batch_proto_init() { } file_v1_batch_proto_msgTypes[5].OneofWrappers = []any{} file_v1_batch_proto_msgTypes[8].OneofWrappers = []any{} - file_v1_batch_proto_msgTypes[20].OneofWrappers = []any{ + file_v1_batch_proto_msgTypes[19].OneofWrappers = []any{ (*BatchStreamReply_Results_Error_Uuid)(nil), (*BatchStreamReply_Results_Error_Beacon)(nil), } - file_v1_batch_proto_msgTypes[21].OneofWrappers = []any{ + file_v1_batch_proto_msgTypes[20].OneofWrappers = []any{ (*BatchStreamReply_Results_Success_Uuid)(nil), (*BatchStreamReply_Results_Success_Beacon)(nil), } @@ -1961,7 +1914,7 @@ func file_v1_batch_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_v1_batch_proto_rawDesc), len(file_v1_batch_proto_rawDesc)), NumEnums: 0, - NumMessages: 27, + NumMessages: 26, NumExtensions: 0, NumServices: 0, }, diff --git a/internal/api/internal/gen/rest/models.go b/internal/api/internal/gen/rest/models.go index f7337bab..57ca3ce4 100644 --- a/internal/api/internal/gen/rest/models.go +++ b/internal/api/internal/gen/rest/models.go @@ -27,7 +27,9 @@ const ( // Defines values for BackupCreateResponseStatus. const ( BackupCreateResponseStatusCANCELED BackupCreateResponseStatus = "CANCELED" + BackupCreateResponseStatusCANCELLING BackupCreateResponseStatus = "CANCELLING" BackupCreateResponseStatusFAILED BackupCreateResponseStatus = "FAILED" + BackupCreateResponseStatusFINALIZING BackupCreateResponseStatus = "FINALIZING" BackupCreateResponseStatusSTARTED BackupCreateResponseStatus = "STARTED" BackupCreateResponseStatusSUCCESS BackupCreateResponseStatus = "SUCCESS" BackupCreateResponseStatusTRANSFERRED BackupCreateResponseStatus = "TRANSFERRED" @@ -37,7 +39,9 @@ const ( // Defines values for BackupCreateStatusResponseStatus. const ( BackupCreateStatusResponseStatusCANCELED BackupCreateStatusResponseStatus = "CANCELED" + BackupCreateStatusResponseStatusCANCELLING BackupCreateStatusResponseStatus = "CANCELLING" BackupCreateStatusResponseStatusFAILED BackupCreateStatusResponseStatus = "FAILED" + BackupCreateStatusResponseStatusFINALIZING BackupCreateStatusResponseStatus = "FINALIZING" BackupCreateStatusResponseStatusSTARTED BackupCreateStatusResponseStatus = "STARTED" BackupCreateStatusResponseStatusSUCCESS BackupCreateStatusResponseStatus = "SUCCESS" BackupCreateStatusResponseStatusTRANSFERRED BackupCreateStatusResponseStatus = "TRANSFERRED" @@ -47,7 +51,9 @@ const ( // Defines values for BackupListResponseStatus. const ( BackupListResponseStatusCANCELED BackupListResponseStatus = "CANCELED" + BackupListResponseStatusCANCELLING BackupListResponseStatus = "CANCELLING" BackupListResponseStatusFAILED BackupListResponseStatus = "FAILED" + BackupListResponseStatusFINALIZING BackupListResponseStatus = "FINALIZING" BackupListResponseStatusSTARTED BackupListResponseStatus = "STARTED" BackupListResponseStatusSUCCESS BackupListResponseStatus = "SUCCESS" BackupListResponseStatusTRANSFERRED BackupListResponseStatus = "TRANSFERRED" @@ -57,7 +63,9 @@ const ( // Defines values for BackupRestoreResponseStatus. const ( BackupRestoreResponseStatusCANCELED BackupRestoreResponseStatus = "CANCELED" + BackupRestoreResponseStatusCANCELLING BackupRestoreResponseStatus = "CANCELLING" BackupRestoreResponseStatusFAILED BackupRestoreResponseStatus = "FAILED" + BackupRestoreResponseStatusFINALIZING BackupRestoreResponseStatus = "FINALIZING" BackupRestoreResponseStatusSTARTED BackupRestoreResponseStatus = "STARTED" BackupRestoreResponseStatusSUCCESS BackupRestoreResponseStatus = "SUCCESS" BackupRestoreResponseStatusTRANSFERRED BackupRestoreResponseStatus = "TRANSFERRED" @@ -67,7 +75,9 @@ const ( // Defines values for BackupRestoreStatusResponseStatus. const ( BackupRestoreStatusResponseStatusCANCELED BackupRestoreStatusResponseStatus = "CANCELED" + BackupRestoreStatusResponseStatusCANCELLING BackupRestoreStatusResponseStatus = "CANCELLING" BackupRestoreStatusResponseStatusFAILED BackupRestoreStatusResponseStatus = "FAILED" + BackupRestoreStatusResponseStatusFINALIZING BackupRestoreStatusResponseStatus = "FINALIZING" BackupRestoreStatusResponseStatusSTARTED BackupRestoreStatusResponseStatus = "STARTED" BackupRestoreStatusResponseStatusSUCCESS BackupRestoreStatusResponseStatus = "SUCCESS" BackupRestoreStatusResponseStatusTRANSFERRED BackupRestoreStatusResponseStatus = "TRANSFERRED" @@ -100,6 +110,19 @@ const ( DBUserInfoDbUserTypeDbUser DBUserInfoDbUserType = "db_user" ) +// Defines values for ExportCreateResponseStatus. +const ( + ExportCreateResponseStatusSTARTED ExportCreateResponseStatus = "STARTED" +) + +// Defines values for ExportStatusResponseStatus. +const ( + ExportStatusResponseStatusFAILED ExportStatusResponseStatus = "FAILED" + ExportStatusResponseStatusSTARTED ExportStatusResponseStatus = "STARTED" + ExportStatusResponseStatusSUCCESS ExportStatusResponseStatus = "SUCCESS" + ExportStatusResponseStatusTRANSFERRING ExportStatusResponseStatus = "TRANSFERRING" +) + // Defines values for GroupType. const ( GroupTypeOidc GroupType = "oidc" @@ -136,8 +159,8 @@ const ( // Defines values for ObjectsGetResponseResultStatus. const ( - ObjectsGetResponseResultStatusFAILED ObjectsGetResponseResultStatus = "FAILED" - ObjectsGetResponseResultStatusSUCCESS ObjectsGetResponseResultStatus = "SUCCESS" + FAILED ObjectsGetResponseResultStatus = "FAILED" + SUCCESS ObjectsGetResponseResultStatus = "SUCCESS" ) // Defines values for PermissionAction. @@ -244,6 +267,14 @@ const ( NoRestore RestoreConfigUsersOptions = "noRestore" ) +// Defines values for ShardProgressStatus. +const ( + ShardProgressStatusFAILED ShardProgressStatus = "FAILED" + ShardProgressStatusSTARTED ShardProgressStatus = "STARTED" + ShardProgressStatusSUCCESS ShardProgressStatus = "SUCCESS" + ShardProgressStatusTRANSFERRING ShardProgressStatus = "TRANSFERRING" +) + // Defines values for StatisticsStatus. const ( StatisticsStatusHEALTHY StatisticsStatus = "HEALTHY" @@ -329,6 +360,13 @@ const ( BatchObjectsCreateJSONBodyFieldsSchema BatchObjectsCreateJSONBodyFields = "schema" ) +// Defines values for SchemaObjectsPropertiesDeleteParamsIndexName. +const ( + Filterable SchemaObjectsPropertiesDeleteParamsIndexName = "filterable" + RangeFilters SchemaObjectsPropertiesDeleteParamsIndexName = "rangeFilters" + Searchable SchemaObjectsPropertiesDeleteParamsIndexName = "searchable" +) + // AdditionalProperties (Response only) Additional meta information about a single object. type AdditionalProperties map[string]map[string]interface{} @@ -405,6 +443,9 @@ type BackupCreateRequest struct { // Include List of collections to include in the backup creation process. If not set, all collections are included. Cannot be used together with `exclude`. Include []string `json:"include,omitempty"` + + // IncrementalBaseBackupId The ID of an existing backup to use as the base for a file-based incremental backup. If set, only files that have changed since the base backup will be included in the new backup. + IncrementalBaseBackupId string `json:"incremental_base_backup_id"` } // BackupCreateResponse The definition of a backup create response body @@ -865,6 +906,84 @@ type ErrorResponse struct { } `json:"error,omitempty"` } +// ExportCreateRequest Request to create a new export operation +type ExportCreateRequest struct { + // Config Backend-specific configuration + Config struct { + // Bucket Bucket, container, or volume name for cloud storage backends + Bucket string `json:"bucket,omitempty"` + + // Path Path prefix within the bucket or filesystem + Path string `json:"path,omitempty"` + } `json:"config,omitempty"` + + // Exclude List of collection names to exclude from the export. Cannot be used with 'include'. + Exclude []string `json:"exclude,omitempty"` + + // Id Unique identifier for this export. Must be URL-safe. + Id string `json:"id"` + + // Include List of collection names to include in the export. Cannot be used with 'exclude'. + Include []string `json:"include,omitempty"` +} + +// ExportCreateResponse Response from creating an export operation +type ExportCreateResponse struct { + // Backend The backend storage system used + Backend string `json:"backend,omitempty"` + + // Classes List of collections being exported + Classes []string `json:"classes,omitempty"` + + // Id Unique identifier for this export + Id string `json:"id,omitempty"` + + // Path Full path where the export is being written + Path string `json:"path,omitempty"` + + // StartedAt When the export started + StartedAt time.Time `json:"startedAt,omitempty"` + + // Status Current status of the export + Status ExportCreateResponseStatus `json:"status,omitempty"` +} + +// ExportCreateResponseStatus Current status of the export +type ExportCreateResponseStatus string + +// ExportStatusResponse Current status of an export operation +type ExportStatusResponse struct { + // Backend The backend storage system used + Backend string `json:"backend,omitempty"` + + // Classes List of collections in this export + Classes []string `json:"classes,omitempty"` + + // Error Error message if export failed + Error string `json:"error,omitempty"` + + // Id Unique identifier for this export + Id string `json:"id,omitempty"` + + // Path Full path where the export is stored + Path string `json:"path,omitempty"` + + // ShardStatus Per-shard progress: className -> shardName -> status + ShardStatus map[string]map[string]ShardProgress `json:"shardStatus,omitempty"` + + // StartedAt When the export started + StartedAt time.Time `json:"startedAt,omitempty"` + + // Status Current status of the export + Status ExportStatusResponseStatus `json:"status,omitempty"` + + // TookInMs Duration of the export in milliseconds + TookInMs int64 `json:"tookInMs,omitempty"` +} + +// ExportStatusResponseStatus Current status of the export +type ExportStatusResponseStatus string + // GeoCoordinates defines model for GeoCoordinates. type GeoCoordinates struct { // Latitude The latitude of the point on earth in decimal form. @@ -1290,6 +1409,9 @@ type Property struct { // Description Description of the property. Description string `json:"description,omitempty"` + // DisableDuplicatedReferences If set to false, allows multiple references to the same target object within this property. Setting it to true will enforce uniqueness of references within this property. By default, this is set to true. + DisableDuplicatedReferences bool `json:"disableDuplicatedReferences"` + // IndexFilterable Whether to include this property in the filterable, Roaring Bitmap index. If `false`, this property cannot be used in `where` filters.

Note: Unrelated to vectorization behavior. IndexFilterable bool `json:"indexFilterable"` @@ -1378,8 +1500,56 @@ type ReferenceMetaClassification struct { WinningDistance float32 `json:"winningDistance,omitempty"` } +// ReplicationAsyncConfig Configuration for asynchronous replication. +type ReplicationAsyncConfig struct { + // AliveNodesCheckingFrequency Interval in milliseconds at which liveness of target nodes is checked. + AliveNodesCheckingFrequency int64 `json:"aliveNodesCheckingFrequency,omitempty"` + + // DiffBatchSize Maximum number of object keys included in a single diff batch. + DiffBatchSize int64 `json:"diffBatchSize,omitempty"` + + // DiffPerNodeTimeout Timeout in seconds for computing a diff against a single node. + DiffPerNodeTimeout int64 `json:"diffPerNodeTimeout,omitempty"` + + // Frequency Base frequency in milliseconds at which async replication runs diff calculations. + Frequency int64 `json:"frequency,omitempty"` + + // FrequencyWhilePropagating Frequency in milliseconds at which async replication runs while propagation is active. + FrequencyWhilePropagating int64 `json:"frequencyWhilePropagating,omitempty"` + + // HashtreeHeight Height of the hashtree used for diffing. + HashtreeHeight int64 `json:"hashtreeHeight,omitempty"` + + // LoggingFrequency Interval in seconds at which async replication logs its status. + LoggingFrequency int64 `json:"loggingFrequency,omitempty"` + + // MaxWorkers Maximum number of async replication workers. + MaxWorkers int64 `json:"maxWorkers,omitempty"` + + // PrePropagationTimeout Overall timeout in seconds for the pre-propagation phase. + PrePropagationTimeout int64 `json:"prePropagationTimeout,omitempty"` + + // PropagationBatchSize Number of objects to include in a single propagation batch. + PropagationBatchSize int64 `json:"propagationBatchSize,omitempty"` + + // PropagationConcurrency Maximum number of concurrent propagation workers. + PropagationConcurrency int64 `json:"propagationConcurrency,omitempty"` + + // PropagationDelay Delay in milliseconds before newly added or updated objects are propagated. + PropagationDelay int64 `json:"propagationDelay,omitempty"` + + // PropagationLimit Maximum number of objects to propagate in a single async replication run. + PropagationLimit int64 `json:"propagationLimit,omitempty"` + + // PropagationTimeout Timeout in seconds for propagating batch of changes to a node. + PropagationTimeout int64 `json:"propagationTimeout,omitempty"` +} + // ReplicationConfig Configure how replication is executed in a cluster type ReplicationConfig struct { + // AsyncConfig Configuration for asynchronous replication. + AsyncConfig ReplicationAsyncConfig `json:"asyncConfig,omitempty"` + // AsyncEnabled Enable asynchronous replication (default: `false`). AsyncEnabled bool `json:"asyncEnabled"` @@ -1613,6 +1783,21 @@ type Schema struct { Name string `json:"name,omitempty"` } +// ShardProgress Progress information for exporting a single shard +type ShardProgress struct { + // Error Error message if this shard's export failed + Error string `json:"error,omitempty"` + + // ObjectsExported Number of objects exported from this shard + ObjectsExported int64 `json:"objectsExported,omitempty"` + + // Status Status of this shard's export + Status ShardProgressStatus `json:"status,omitempty"` +} + +// ShardProgressStatus Status of this shard's export +type ShardProgressStatus string + // ShardStatus The status of a single shard type ShardStatus struct { // Status Status of the shard @@ -1929,6 +2114,15 @@ type BackupsCreateStatusParams struct { Path string `form:"path,omitempty" json:"path,omitempty"` } +// BackupsRestoreCancelParams defines parameters for BackupsRestoreCancel. +type BackupsRestoreCancelParams struct { + // Bucket Optional: Specifies the bucket, container, or volume name if required by the backend. + Bucket string `form:"bucket,omitempty" json:"bucket,omitempty"` + + // Path Optional: Specifies the path within the bucket/container/volume if the backup is not at the root. + Path string `form:"path,omitempty" json:"path,omitempty"` +} + // BackupsRestoreStatusParams defines parameters for BackupsRestoreStatus. type BackupsRestoreStatusParams struct { // Bucket Optional: Specifies the bucket, container, or volume name if required by the backend. @@ -1974,6 +2168,15 @@ type BatchReferencesCreateParams struct { ConsistencyLevel string `form:"consistency_level,omitempty" json:"consistency_level,omitempty"` } +// ExportStatusParams defines parameters for ExportStatus. +type ExportStatusParams struct { + // Bucket Optional bucket name where the export is stored. If not specified, uses the backend's default bucket. + Bucket string `form:"bucket,omitempty" json:"bucket,omitempty"` + + // Path Optional path prefix within the bucket. If not specified, uses the backend's default path. + Path string `form:"path,omitempty" json:"path,omitempty"` +} + // NodesGetParams defines parameters for NodesGet. type NodesGetParams struct { // Output Controls the verbosity of the output, possible values are: `minimal`, `verbose`. Defaults to `minimal`. @@ -2189,6 +2392,9 @@ type SchemaObjectsGetParams struct { Consistency bool `json:"consistency,omitempty"` } +// SchemaObjectsPropertiesDeleteParamsIndexName defines parameters for SchemaObjectsPropertiesDelete. +type SchemaObjectsPropertiesDeleteParamsIndexName string + // SchemaObjectsShardsGetParams defines parameters for SchemaObjectsShardsGet. type SchemaObjectsShardsGetParams struct { // Tenant The name of the tenant for which to retrieve shard statuses (only applicable for multi-tenant collections). @@ -2297,6 +2503,9 @@ type BatchReferencesCreateJSONRequestBody = BatchReferencesCreateJSONBody // ClassificationsPostJSONRequestBody defines body for ClassificationsPost for application/json ContentType. type ClassificationsPostJSONRequestBody = Classification +// ExportCreateJSONRequestBody defines body for ExportCreate for application/json ContentType. +type ExportCreateJSONRequestBody = ExportCreateRequest + // GraphqlPostJSONRequestBody defines body for GraphqlPost for application/json ContentType. type GraphqlPostJSONRequestBody = GraphQLQuery diff --git a/internal/api/message_test.go b/internal/api/message_test.go index a9847b72..87b76f0b 100644 --- a/internal/api/message_test.go +++ b/internal/api/message_test.go @@ -8,26 +8,27 @@ import ( "github.com/stretchr/testify/require" "github.com/weaviate/weaviate-go-client/v6/internal/api" proto "github.com/weaviate/weaviate-go-client/v6/internal/api/internal/gen/proto/v1" + "github.com/weaviate/weaviate-go-client/v6/internal/api/transport" "github.com/weaviate/weaviate-go-client/v6/internal/testkit" "google.golang.org/protobuf/types/known/structpb" ) -// MessageMarshalerTest tests that [api.Message] produces a correct request message. +// MessageMarshalerTest tests that [transport.Message] produces a correct request message. // // We do not verify the [Message.Method] part of the request, because the specific // [api.MethodFunc] returned is an implementation detail and there's probably not // a lot of room for error. -type MessageMarshalerTest[In api.RequestMessage, Out api.ReplyMessage] struct { +type MessageMarshalerTest[In transport.RequestMessage, Out transport.ReplyMessage] struct { testkit.Only name string - req api.Message[In, Out] // Request struct. - want *In // Expected protobuf request message. + req transport.Message[In, Out] // Request struct. + want *In // Expected protobuf request message. err testkit.Error } // testMessageMarshaler runs [MessageMarshalerTest] test cases. -func testMessageMarshaler[In api.RequestMessage, Out api.ReplyMessage](t *testing.T, tests []MessageMarshalerTest[In, Out]) { +func testMessageMarshaler[In transport.RequestMessage, Out transport.ReplyMessage](t *testing.T, tests []MessageMarshalerTest[In, Out]) { t.Helper() for _, tt := range testkit.WithOnly(t, tests) { t.Run(tt.name, func(t *testing.T) { @@ -586,22 +587,22 @@ func TestSearchRequest_MarshalMessage(t *testing.T) { // ---------------------------------------------------------------------------- -type MessageUnmarshalerTest[Out api.ReplyMessage] struct { +type MessageUnmarshalerTest[Out transport.ReplyMessage] struct { testkit.Only name string - reply *Out // Protobuf message that needs to be unmarshaled. - dest api.MessageUnmarshaler[Out] // Set dest to a pointer to response struct. - want any // Expected response value (pointer). + reply *Out // Protobuf message that needs to be unmarshaled. + dest transport.MessageUnmarshaler[Out] // Set dest to a pointer to response struct. + want any // Expected response value (pointer). err testkit.Error } -// testMessageMarshaler runs test cases for [api.MessageUnmarshaler] implementations. -func testMessageUnmarshaler[Out api.ReplyMessage](t *testing.T, tests []MessageUnmarshalerTest[Out]) { +// testMessageMarshaler runs test cases for [transport.MessageUnmarshaler] implementations. +func testMessageUnmarshaler[Out transport.ReplyMessage](t *testing.T, tests []MessageUnmarshalerTest[Out]) { t.Helper() for _, tt := range testkit.WithOnly(t, tests) { t.Run(tt.name, func(t *testing.T) { - testkit.IsPointer(t, tt.want, "want") + testkit.RequirePointer(t, tt.want, "want") err := tt.dest.UnmarshalMessage(tt.reply) tt.err.Require(t, err, "unmarshal") diff --git a/internal/api/search.go b/internal/api/search.go index 0128f493..5da44618 100644 --- a/internal/api/search.go +++ b/internal/api/search.go @@ -7,6 +7,7 @@ import ( "github.com/google/uuid" proto "github.com/weaviate/weaviate-go-client/v6/internal/api/internal/gen/proto/v1" + "github.com/weaviate/weaviate-go-client/v6/internal/api/transport" "github.com/weaviate/weaviate-go-client/v6/internal/dev" ) @@ -27,14 +28,14 @@ type SearchRequest struct { } var ( - _ Message[proto.SearchRequest, proto.SearchReply] = (*SearchRequest)(nil) - _ MessageMarshaler[proto.SearchRequest] = (*SearchRequest)(nil) + _ transport.Message[proto.SearchRequest, proto.SearchReply] = (*SearchRequest)(nil) + _ transport.MessageMarshaler[proto.SearchRequest] = (*SearchRequest)(nil) ) -func (r *SearchRequest) Method() MethodFunc[proto.SearchRequest, proto.SearchReply] { +func (r *SearchRequest) Method() transport.MethodFunc[proto.SearchRequest, proto.SearchReply] { return proto.WeaviateClient.Search } -func (r *SearchRequest) Body() MessageMarshaler[proto.SearchRequest] { return r } +func (r *SearchRequest) Body() transport.MessageMarshaler[proto.SearchRequest] { return r } type ( ReturnMetadata struct { @@ -297,7 +298,7 @@ type SearchResponse struct { GroupByResults []Group } -var _ MessageUnmarshaler[proto.SearchReply] = (*SearchResponse)(nil) +var _ transport.MessageUnmarshaler[proto.SearchReply] = (*SearchResponse)(nil) type ( Object struct { diff --git a/internal/api/transport.go b/internal/api/transport/message.go similarity index 98% rename from internal/api/transport.go rename to internal/api/transport/message.go index ef59d786..39269ba2 100644 --- a/internal/api/transport.go +++ b/internal/api/transport/message.go @@ -1,4 +1,4 @@ -package api +package transport import ( "context" diff --git a/internal/api/transport/transport.go b/internal/api/transport/transport.go new file mode 100644 index 00000000..a4553813 --- /dev/null +++ b/internal/api/transport/transport.go @@ -0,0 +1,41 @@ +package transport + +import ( + "net/http" + "time" + + "github.com/weaviate/weaviate-go-client/v6/internal" +) + +type Config struct { + Scheme string // Scheme for request URLs, "http" or "https". + RESTHost string // Hostname of the REST host. + RESTPort int // Port number of the REST host + GRPCHost string // Hostname of the gRPC host. + GRPCPort int // Port number of the gRPC host. + Header http.Header // Request headers. + Timeout Timeout // Request timeout options. +} + +// Timeout sets client-side timeouts. +type Timeout struct { + // Timeout for REST requests using HTTP GET or HEAD methods, + // and gRPC requests using [WeaviateClient.Search], + // [WeaviateClient.Aggregate], or [WeaviateClient.TenantsGet] methods. + Read time.Duration + + // Timeout for REST requests using HTTP POST, PUT, PATCH, or DELETE methods, + // and gRPC requests using [WeaviateClient.BatchDelete], + // [WeaviateClient.BatchObjects] or [WeaviateClient.BatchReferences] methods. + Write time.Duration // Timeout for insert requests. + Batch time.Duration // Timeout for batch insert requests. +} + +// NewFunc returns an [internal.Transport] instance for [transport.Config]. +type NewFunc func(Config) (internal.Transport, error) + +var New NewFunc = newTransport + +func newTransport(Config) (internal.Transport, error) { + return nil, nil +} diff --git a/internal/dev/assert.go b/internal/dev/assert.go index 5d4fd6d5..f66f43cc 100644 --- a/internal/dev/assert.go +++ b/internal/dev/assert.go @@ -3,20 +3,30 @@ package dev import ( "flag" "fmt" + "os" "reflect" + "slices" + "strings" "sync" ) // ea is a flag that enables asserts. +// // We only ever want to enable asserts in test builds. // go test will register a number of command-line flags, // test.v is one of them. See: https://stackoverflow.com/a/36666114/14726116 +// In case some code uses asserts before the flags are registerred, +// we double-check the [os.Args] for any values that resemble test flags, +// i.e start with "-test.". // // While this is not part of the go test public contract, // the worst thing that can happen in case that flag is not set anymore // is that assertions will be _permanently disabled_. var ea = sync.OnceValue(func() bool { - return flag.Lookup("test.v") != nil + return flag.Lookup("test.v") != nil || + slices.ContainsFunc(os.Args, func(arg string) bool { + return strings.HasPrefix(arg, "-test.") + }) }) // Assert panics with a formated message if the check is false. @@ -52,6 +62,7 @@ func isNil(v any) bool { reflect.Func, reflect.Chan: return reflect.ValueOf(v).IsNil() + default: } return false } diff --git a/internal/testkit/testkit_test.go b/internal/testkit/testkit_test.go index 4ad56744..908c9131 100644 --- a/internal/testkit/testkit_test.go +++ b/internal/testkit/testkit_test.go @@ -61,6 +61,34 @@ func TestMockTransport(t *testing.T) { require.Equal(t, n, 0, "mistake in test code") require.True(t, transport.Done(), "done: all requests consumed") }) + + t.Run("heterogenous requests", func(t *testing.T) { + transport := testkit.NewTransport(t, []testkit.Stub[any, any]{ + { + Request: testkit.Ptr[any](testkit.Ptr(5)), + Response: 92, + }, + { + Request: testkit.Ptr[any](testkit.Ptr("hello, transport")), + Response: true, + }, + }) + + { + req := 5 + var dest int + err := transport.Do(t.Context(), &req, &dest) + require.NoError(t, err, "mock transport error") + require.Equal(t, 92, dest, "int dest") + } + { + req := "hello, transport" + var dest bool + err := transport.Do(t.Context(), &req, &dest) + require.NoError(t, err, "mock transport error") + require.Equal(t, true, dest, "bool dest") + } + }) } func TestWithOnly(t *testing.T) { diff --git a/internal/testkit/transport.go b/internal/testkit/transport.go index ec45c20d..3ac7098b 100644 --- a/internal/testkit/transport.go +++ b/internal/testkit/transport.go @@ -2,6 +2,7 @@ package testkit import ( "context" + "reflect" "testing" "github.com/stretchr/testify/assert" @@ -74,8 +75,17 @@ func (t *MockTransport[Req, Resp]) Do(ctx context.Context, req, dest any) error return ctx.Err() } - if stub.Request != nil && assert.IsType(t.t, (*Req)(nil), req, "bad request") { - assert.Equal(t.t, stub.Request, req, "bad request") + if stub.Request != nil { + if reflect.TypeFor[Req]().Kind() == reflect.Interface { + assert.Implements(t.t, (*Req)(nil), req, "bad request") + + // If Req is an interface, [Stub.Request] must be a pointer to satisfy *Req. + // We expect the _actual_ request to be the de-referenced value, so we compare + // *stub.Request instead of stub.Request. + assert.Equal(t.t, *stub.Request, req, "bad request") + } else if assert.IsType(t.t, (*Req)(nil), req, "bad request") { + assert.Equal(t.t, stub.Request, req, "bad request") + } } if stub.Err != nil { @@ -83,7 +93,17 @@ func (t *MockTransport[Req, Resp]) Do(ctx context.Context, req, dest any) error } if dest != nil { - if assert.IsType(t.t, (*Resp)(nil), dest, "bad dest") { + if reflect.TypeFor[Resp]().Kind() == reflect.Interface { + assert.Implements(t.t, (*Resp)(nil), dest, "bad dest") + + // To support heterogenous requests in the same transport, + // we need to handle Resp == interface{} correctly. Since + // a non-nil dest will have some concrete type, we cannot + // cast to to (*any) and dereference it safely, as we do + // with homogenous requests in the else-branch). + // We can leverage reflection to assign the response to dest. + reflect.ValueOf(dest).Elem().Set(reflect.ValueOf(stub.Response)) + } else if assert.IsType(t.t, (*Resp)(nil), dest, "bad dest") { *dest.(*Resp) = stub.Response } } diff --git a/internal/testkit/util.go b/internal/testkit/util.go index 335eee35..d36884f4 100644 --- a/internal/testkit/util.go +++ b/internal/testkit/util.go @@ -25,10 +25,10 @@ var UUID = uuid.New() // Ptr is a helper for passing pointers to constants. func Ptr[T any](v T) *T { return &v } -// IsPointer asserts that v is a pointer. If the assertion fails, -// the test t will fail immediately. Use IsPointer as a pre-condition +// RequirePointer asserts that v is a pointer. If the assertion fails, +// the test t will fail immediately. Use RequirePointer as a pre-condition // in unit tests to ensure the test cases are valid. -func IsPointer(t *testing.T, v any, name string) { +func RequirePointer(t *testing.T, v any, name string) { t.Helper() require.Equalf(t, reflect.Pointer, reflect.TypeOf(v).Kind(), "%q must be a pointer", name) } diff --git a/internal/transports/rest.go b/internal/transports/rest.go index 897dc6a8..5f0e25e4 100644 --- a/internal/transports/rest.go +++ b/internal/transports/rest.go @@ -1,6 +1,12 @@ package transports -import "net/url" +import ( + "fmt" + "net/url" + "strings" + + "github.com/weaviate/weaviate-go-client/v6/internal/dev" +) // Endpoint describes a REST request. type Endpoint interface { @@ -19,6 +25,16 @@ type Endpoint interface { Body() any } +// StatusAccepter is an interface that response types can implement to +// control which HTTP status codes > 299 should not result in an error. +// +// The [REST] transport always treats codes < 299 as successful +// and only calls AcceptStatus for codes > 299. +type StatusAccepter interface { + // AcceptStatus returns true if a status code is acceptable. + AcceptStatus(code int) bool +} + // BaseEndpoint implements [Endpoint] methods which may return nil. // These values are usually optional in the request. // BaseEndpoint can be embedded in a another request struct to reduce boilerplate. @@ -39,3 +55,71 @@ type BaseEndpoint struct{} func (*BaseEndpoint) Query() url.Values { return nil } func (*BaseEndpoint) Body() any { return nil } + +// IdentityEndpoint returns a factory for requests to identity endpoints. +// +// Example: +// +// // Assign request creator func to a variable. +// var DeleteSongRequest = transport.IdentityEndpoint[int](http.MethodDelete, "/songs/%v") +// +// // Use it to create new requests +// req := DeleteSongRequest(123) +// +// The pathFmt MUST ONLY contain a single formatting directive. Callers are free to use +// the formatting directive most appropriate to the identity type, e.g. %s for strings, +// but %d for numbers. +// +// IdentityEndpoint will panic on invalid pathFmt _before_ the factory is created. +// +// // BAD: panics because pathFmt contains 2 formatting directives. +// var DeleteSongsRequest = transport.IdentityEndpoint[uuid.UUID](http.MethodGet, "/albums/%v/songs/%v") +func IdentityEndpoint[I any](method, pathFmt string) func(I) any { + dev.Assert(strings.Count(pathFmt, "%") == 1, "%s must have a single formatting directive", pathFmt) + + return func(id I) any { + return &identityEndpoint[I]{ + method: method, + pathFmt: pathFmt, + id: id, + } + } +} + +// identityEndpoint implements [Endpoint] for endpoints that use some ID parameter +// in the request path, e.g. 'DELETE /users/' or 'GET /artist//songs'. +// See [IdentityEndpoint] for more details. +type identityEndpoint[I any] struct { + BaseEndpoint + method, pathFmt string + id I +} + +var _ Endpoint = (*identityEndpoint[any])(nil) + +func (r *identityEndpoint[T]) Method() string { return r.method } +func (r *identityEndpoint[T]) Path() string { return fmt.Sprintf(r.pathFmt, r.id) } + +// StaticEndpoint creates a new static endpoint with a method and a path. +func StaticEndpoint(method, path string) *staticEndpoint { + return &staticEndpoint{method: method, path: path} +} + +// staticEndpoint implements [Endpoint] for requests that has +// neither query or path parameters, nor the body. Each request +// to a static endpoint looks identical to all other ones, e.g. GET /live. +// Since such request is independent of its inputs (it has none of them), +// it can be created once and re-used throughout the program's lifetime. +// +// Example: +// +// var ListSongsRequest transport.Endpoint = transport.StaticEndpoint(http.MethodGet, "/songs") +type staticEndpoint struct { + BaseEndpoint + method, path string +} + +var _ Endpoint = (*staticEndpoint)(nil) + +func (e *staticEndpoint) Method() string { return e.method } +func (e *staticEndpoint) Path() string { return e.path } diff --git a/internal/transports/rest_test.go b/internal/transports/rest_test.go index 69dd5e7c..59248ed7 100644 --- a/internal/transports/rest_test.go +++ b/internal/transports/rest_test.go @@ -1,9 +1,14 @@ package transports_test import ( + "fmt" + "net/http" + "strings" "testing" + "github.com/google/uuid" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "github.com/weaviate/weaviate-go-client/v6/internal/transports" ) @@ -13,3 +18,77 @@ func TestBaseEndoint(t *testing.T) { assert.Nil(t, endpoint.Query(), "query") assert.Nil(t, endpoint.Body(), "body") } + +func TestIdentityEndpoint(t *testing.T) { + t.Run("string", func(t *testing.T) { + id := "test-id" + pathFmt := "/string/%s" + wantPath := fmt.Sprintf(pathFmt, id) + + factory := transports.IdentityEndpoint[string](http.MethodGet, pathFmt) + req := factory(id) + + if assert.Implements(t, (*transports.Endpoint)(nil), req, "factory returns valid requests") { + ep := req.(transports.Endpoint) + + assert.Equal(t, ep.Method(), http.MethodGet, "method") + assert.Equal(t, ep.Path(), wantPath, "path") + assert.Nil(t, ep.Query(), "query") + assert.Nil(t, ep.Body(), "body") + } + }) + + t.Run("int", func(t *testing.T) { + id := 123 + pathFmt := "/int/%d" + wantPath := fmt.Sprintf(pathFmt, id) + + factory := transports.IdentityEndpoint[int](http.MethodGet, pathFmt) + req := factory(id) + + if assert.Implements(t, (*transports.Endpoint)(nil), req, "factory returns valid requests") { + ep := req.(transports.Endpoint) + + assert.Equal(t, ep.Method(), http.MethodGet, "method") + assert.Equal(t, ep.Path(), wantPath, "path") + assert.Nil(t, ep.Query(), "query") + assert.Nil(t, ep.Body(), "body") + } + }) + + t.Run("uuid.UUID", func(t *testing.T) { + id := uuid.New() + pathFmt := "/uuid/%s" + wantPath := fmt.Sprintf(pathFmt, id) + + factory := transports.IdentityEndpoint[uuid.UUID](http.MethodGet, pathFmt) + req := factory(id) + + if assert.Implements(t, (*transports.Endpoint)(nil), req, "factory returns valid requests") { + ep := req.(transports.Endpoint) + + assert.Equal(t, ep.Method(), http.MethodGet, "method") + assert.Equal(t, ep.Path(), wantPath, "path") + assert.Nil(t, ep.Query(), "query") + assert.Nil(t, ep.Body(), "body") + } + }) + + t.Run("invalid pathFmt", func(t *testing.T) { + pathFmt := "/first/%v/second/%d" + require.Panics(t, func() { + transports.IdentityEndpoint[any](http.MethodGet, pathFmt) + }, "must validate pathFmt on creation (%q has %d formatting directives)", + pathFmt, strings.Count(pathFmt, "%"), + ) + }) +} + +func TestStaticEndpoint(t *testing.T) { + static := transports.StaticEndpoint(http.MethodGet, "/live") + + assert.Equal(t, static.Method(), http.MethodGet, "method") + assert.Equal(t, static.Path(), "/live", "path") + assert.Nil(t, static.Query(), "query") + assert.Nil(t, static.Body(), "body") +} diff --git a/types/enums.go b/types/enums.go new file mode 100644 index 00000000..795204b4 --- /dev/null +++ b/types/enums.go @@ -0,0 +1,14 @@ +package types + +import ( + "github.com/weaviate/weaviate-go-client/v6/internal/api" +) + +// ConsistencyLevel defines supported read / write consistency level. +type ConsistencyLevel api.ConsistencyLevel + +const ( + ConsistencyLevelOne ConsistencyLevel = ConsistencyLevel(api.ConsistencyLevelOne) + ConsistencyLevelQuorum ConsistencyLevel = ConsistencyLevel(api.ConsistencyLevelQuorum) + ConsistencyLevelAll ConsistencyLevel = ConsistencyLevel(api.ConsistencyLevelAll) +) diff --git a/version.go b/version.go new file mode 100644 index 00000000..2677a0ad --- /dev/null +++ b/version.go @@ -0,0 +1,9 @@ +package weaviate + +var version = "v6.0.0-alpha.1" + +// Version reports the version of the package. +// This is sent in the X-Weaviate-Client header for telemetry. +func Version() string { + return version +}