From 2c1bbfcca22a4ddfb40bcb28e8ec8cbca865a9a5 Mon Sep 17 00:00:00 2001 From: David Miguel Lozano Date: Tue, 15 Aug 2023 11:13:34 +0200 Subject: [PATCH] feat(vertex_a): Add GCP Vertex AI Matching Engine client (#116) --- packages/vertex_ai/README.md | 244 ++++- .../lib/src/gen_ai/gen_ai_client.dart | 7 +- .../lib/src/matching_engine/apis/apis.dart | 4 + .../matching_engine/apis/index_endpoints.dart | 254 +++++ .../apis/index_endpoints_operations.dart | 94 ++ .../lib/src/matching_engine/apis/indexes.dart | 116 +++ .../apis/indexes_operations.dart | 92 ++ .../src/matching_engine/machine_engine.dart | 3 + .../mappers/index_endpoints.dart | 217 ++++ .../src/matching_engine/mappers/indexes.dart | 103 ++ .../src/matching_engine/mappers/mappers.dart | 3 + .../matching_engine/mappers/operation.dart | 29 + .../matching_engine_client.dart | 115 +++ .../models/index_endpoints.dart | 942 ++++++++++++++++++ .../src/matching_engine/models/indexes.dart | 760 ++++++++++++++ .../src/matching_engine/models/models.dart | 3 + .../src/matching_engine/models/operation.dart | 122 +++ packages/vertex_ai/lib/vertex_ai.dart | 1 + .../maching_engine_client_test.dart | 938 +++++++++++++++++ .../mappers/index_endpoints_test.dart | 273 +++++ .../matching_engine/mappers/indexes_test.dart | 177 ++++ .../mappers/operation_test.dart | 37 + 22 files changed, 4530 insertions(+), 4 deletions(-) create mode 100644 packages/vertex_ai/lib/src/matching_engine/apis/apis.dart create mode 100644 packages/vertex_ai/lib/src/matching_engine/apis/index_endpoints.dart create mode 100644 packages/vertex_ai/lib/src/matching_engine/apis/index_endpoints_operations.dart create mode 100644 packages/vertex_ai/lib/src/matching_engine/apis/indexes.dart create mode 100644 packages/vertex_ai/lib/src/matching_engine/apis/indexes_operations.dart create mode 100644 packages/vertex_ai/lib/src/matching_engine/machine_engine.dart create mode 100644 packages/vertex_ai/lib/src/matching_engine/mappers/index_endpoints.dart create mode 100644 packages/vertex_ai/lib/src/matching_engine/mappers/indexes.dart create mode 100644 packages/vertex_ai/lib/src/matching_engine/mappers/mappers.dart create mode 100644 packages/vertex_ai/lib/src/matching_engine/mappers/operation.dart create mode 100644 packages/vertex_ai/lib/src/matching_engine/matching_engine_client.dart create mode 100644 packages/vertex_ai/lib/src/matching_engine/models/index_endpoints.dart create mode 100644 packages/vertex_ai/lib/src/matching_engine/models/indexes.dart create mode 100644 packages/vertex_ai/lib/src/matching_engine/models/models.dart create mode 100644 packages/vertex_ai/lib/src/matching_engine/models/operation.dart create mode 100644 packages/vertex_ai/test/matching_engine/maching_engine_client_test.dart create mode 100644 packages/vertex_ai/test/matching_engine/mappers/index_endpoints_test.dart create mode 100644 packages/vertex_ai/test/matching_engine/mappers/indexes_test.dart create mode 100644 packages/vertex_ai/test/matching_engine/mappers/operation_test.dart diff --git a/packages/vertex_ai/README.md b/packages/vertex_ai/README.md index 118a0a04..f9320b3e 100644 --- a/packages/vertex_ai/README.md +++ b/packages/vertex_ai/README.md @@ -70,7 +70,7 @@ final res = await vertexAi.text.predict( ); ``` -#### Chat models +### Chat models [PaLM API for chat](https://cloud.google.com/vertex-ai/docs/generative-ai/model-reference/text-chat) is fine-tuned for multi-turn chat, where the model keeps track of previous @@ -88,7 +88,7 @@ final res = await vertexAi.chat.predict( ); ``` -#### Text embeddings +### Text embeddings The [Text Embedding API](https://cloud.google.com/vertex-ai/docs/generative-ai/model-reference/text-embeddings) generates vector embeddings for input text. You can use embeddings for tasks @@ -100,6 +100,246 @@ final res = await vertexAi.textEmbeddings.predict( ); ``` +## Matching Engine + +Vertex AI Matching Engine provides the industry's leading high-scale low +latency vector database. These vector databases are commonly referred to as +vector similarity-matching or an approximate nearest neighbor (ANN) service. + +Matching Engine provides tooling to build use cases that match semantically +similar items. More specifically, given a query item, Matching Engine finds the +most semantically similar items to it from a large corpus of candidate items. + +### Authentication + +The `VertexAIMatchingEngineClient` delegates authentication to the +[googleapis_auth](https://pub.dev/packages/googleapis_auth) package. + +To create an instance of `VertexAIMatchingEngineClient` you need to provide +an [`AuthClient`](https://pub.dev/documentation/googleapis_auth/latest/googleapis_auth/AuthClient-class.html) +instance. + +There are several ways to obtain an `AuthClient` depending on your use case. +Check out the [googleapis_auth](https://pub.dev/packages/googleapis_auth) +package documentation for more details. + +Example using a service account JSON: +```dart +final serviceAccountCredentials = ServiceAccountCredentials.fromJson( + json.decode(serviceAccountJson), +); +final authClient = await clientViaServiceAccount( + serviceAccountCredentials, + [VertexAIGenAIClient.cloudPlatformScope], +); +final vertexAi = VertexAIMatchingEngineClient( + authHttpClient: authClient, + project: 'your-project-id', + location: 'europe-west1', +); +``` + +To be able to create and manage indexes and index endpoints, the service +account should have the following [permissions](https://cloud.google.com/vertex-ai/docs/general/iam-permissions): +- `aiplatform.indexes.create` +- `aiplatform.indexes.get` +- `aiplatform.indexes.list` +- `aiplatform.indexes.update` +- `aiplatform.indexes.delete` +- `aiplatform.indexEndpoints.create` +- `aiplatform.indexEndpoints.get` +- `aiplatform.indexEndpoints.list` +- `aiplatform.indexEndpoints.update` +- `aiplatform.indexEndpoints.delete` +- `aiplatform.indexEndpoints.deploy` +- `aiplatform.indexEndpoints.undeploy` + +If you just want to query an index endpoint, the service account only needs: +- `aiplatform.indexEndpoints.queryVectors` + +The required[OAuth2 scope](https://developers.google.com/identity/protocols/oauth2/scopes) + is: +- `https://www.googleapis.com/auth/cloud-platform` (you can use the constant + `VertexAIMatchingEngineClient.cloudPlatformScope`) + See: https://cloud.google.com/vertex-ai/docs/generative-ai/access-control + +### Create an index + +1. Generate embeddings for your data and save them to a file (see supported + formats [here](https://cloud.google.com/vertex-ai/docs/matching-engine/match-eng-setup/format-structure)). +2. Create a Cloud Storage bucket and upload the embeddings file. +3. Create the index: + +```dart +final operation = await marchingEngine.indexes.create( + displayName: 'test-index', + description: 'This is a test index', + metadata: const VertexAINearestNeighborSearch( + contentsDeltaUri: 'gs://bucket-name/path-to-index-dir', + config: VertexAINearestNeighborSearchConfig( + dimensions: 768, + algorithmConfig: VertexAITreeAhAlgorithmConfig(), + ), + ), +); +``` + +To check the status of the operation: + +```dart +final operation = await marchingEngine.indexes.operations.get( + name: operation.name, +); +print(operation.done); +``` + +### Get index information + +```dart +final index = await marchingEngine.indexes.get(id: '5086059315115065344'); +``` + +You can also list all indexes: + +```dart +final indexes = await marchingEngine.indexes.list(); +``` + +### Create an index endpoint + +```dart +final operation = await marchingEngine.indexEndpoints.create( + displayName: 'test-index-endpoint', + description: 'This is a test index endpoint', + publicEndpointEnabled: true, +); +``` + +To check the status of the operation: + +```dart +final operation = await marchingEngine.indexEndpoints.operations.get( + name: operation.name, +); +print(operation.done); +``` + +### Deploy an index to an index endpoint + +```dart +final operation = await marchingEngine.indexEndpoints.deployIndex( + indexId: '5086059315115065344', + indexEndpointId: '8572232454792807200', + deployedIndexId: 'deployment1', + deployedIndexDisplayName: 'test-deployed-index', +); +``` + +You can check the status of the operation as shown above. + +If you want to enable autoscaling: + +```dart +final operation = await marchingEngine.indexEndpoints.deployIndex( + indexId: '5086059315115065344', + indexEndpointId: '8572232454792807200', + deployedIndexId: 'deployment1', + deployedIndexDisplayName: 'test-deployed-index', + automaticResources: const VertexAIAutomaticResources( + minReplicaCount: 2, + maxReplicaCount: 10, + ), +); +``` + +### Get index endpoint information + +```dart +final ie = await marchingEngine.indexEndpoints.get(id: '8572232454792807200'); +``` + +You can also list all index endpoints: + +```dart +final indexEndpoints = await marchingEngine.indexEndpoints.list(); +``` + +### Mutate index endpoint + +```dart +final operation = await marchingEngine.indexEndpoints.mutateDeployedIndex( + indexEndpointId: '8572232454792807200', + deployedIndexId: 'deployment1', + automaticResources: const VertexAIAutomaticResources( + minReplicaCount: 2, + maxReplicaCount: 20, + ), +); +``` + +### Undeploy an index from an index endpoint + +```dart +final operation = await marchingEngine.indexEndpoints.undeployIndex( + indexEndpointId: '8572232454792807200', + deployedIndexId: 'deployment1', +); +``` + +### Delete an index endpoint + +```dart +final operation = await marchingEngine.indexEndpoints.delete( + id: '8572232454792807200', +); +``` + +### Delete an index + +```dart +final operation = await marchingEngine.indexes.delete( + id: '5086059315115065344', +); +``` + +### Query an index using the index endpoint + +Once you've created the index, you can run queries to get its nearest neighbors. + +Mind that you will need a different `VertexAIMatchingEngineClient` for +calling this method, as the public query endpoint has a different `rootUrl` +than the rest of the API (e.g. https://xxxxxxxxxx.europe-west1-xxxxxxxxxxxx.vdb.vertexai.goog). + +Check the `VertexAIIndexEndpoint.publicEndpointDomainName` of your index +endpoint by calling `VertexAIMatchingEngineClient.indexEndpoints.get`. Then +create a new client setting the [VertexAIMatchingEngineClient.rootUrl] to that +value (mind that you need to add `https://` to the beginning of the domain +name). + +```dart +final machineEngineQuery = VertexAIMatchingEngineClient( + authHttpClient: authClient, + project: Platform.environment['VERTEX_AI_PROJECT_ID']!, + rootUrl: + 'https://1451028333.europe-west1-706285145444.vdb.vertexai.goog/', +); +final res = await machineEngineQuery.indexEndpoints.findNeighbors( + indexEndpointId: '8572232454792807200', + deployedIndexId: 'deployment1', + queries: const [ + VertexAIFindNeighborsRequestQuery( + datapoint: VertexAIIndexDatapoint( + datapointId: 'your-datapoint-id', + featureVector: [-0.0024800552055239677, 0.011974085122346878, ...], + ), + neighborCount: 3, + ), + ], +); +``` + +Docs: https://cloud.google.com/vertex-ai/docs/matching-engine/query-index-public-endpoint + ## License Vertex AI API Client is licensed under the [MIT License](https://github.com/davidmigloz/langchain_dart/blob/main/LICENSE). diff --git a/packages/vertex_ai/lib/src/gen_ai/gen_ai_client.dart b/packages/vertex_ai/lib/src/gen_ai/gen_ai_client.dart index 166d31b7..83a4ce2c 100644 --- a/packages/vertex_ai/lib/src/gen_ai/gen_ai_client.dart +++ b/packages/vertex_ai/lib/src/gen_ai/gen_ai_client.dart @@ -78,8 +78,11 @@ class VertexAIGenAIClient { required final AuthClient authHttpClient, required this.project, this.location = 'us-central1', - final String rootUrl = 'https://us-central1-aiplatform.googleapis.com/', - }) : _vertexAiApi = AiplatformApi(authHttpClient, rootUrl: rootUrl); + final String? rootUrl, + }) : _vertexAiApi = AiplatformApi( + authHttpClient, + rootUrl: rootUrl ?? 'https://$location-aiplatform.googleapis.com/', + ); /// The Google Cloud project to use for interacting with Vertex AI. final String project; diff --git a/packages/vertex_ai/lib/src/matching_engine/apis/apis.dart b/packages/vertex_ai/lib/src/matching_engine/apis/apis.dart new file mode 100644 index 00000000..40869914 --- /dev/null +++ b/packages/vertex_ai/lib/src/matching_engine/apis/apis.dart @@ -0,0 +1,4 @@ +export 'index_endpoints.dart'; +export 'index_endpoints_operations.dart'; +export 'indexes.dart'; +export 'indexes_operations.dart'; diff --git a/packages/vertex_ai/lib/src/matching_engine/apis/index_endpoints.dart b/packages/vertex_ai/lib/src/matching_engine/apis/index_endpoints.dart new file mode 100644 index 00000000..2546091f --- /dev/null +++ b/packages/vertex_ai/lib/src/matching_engine/apis/index_endpoints.dart @@ -0,0 +1,254 @@ +import 'package:googleapis/aiplatform/v1.dart'; + +import '../mappers/mappers.dart'; +import '../models/models.dart'; +import 'index_endpoints_operations.dart'; + +/// {@template vertex_ai_index_endpoints_api} +/// Vertex AI Index Endpoints API. +/// +/// Documentation: +/// https://cloud.google.com/vertex-ai/docs/reference/rest/v1/projects.locations.indexEndpoints +/// {@endtemplate} +class VertexAIIndexEndpointsApi { + /// {@macro vertex_ai_index_endpoints_api} + VertexAIIndexEndpointsApi({ + required final ProjectsLocationsIndexEndpointsResource indexEndpointsApi, + required this.project, + required this.location, + }) : _indexEndpointsApi = indexEndpointsApi; + + /// The Google Cloud project to use for interacting with Vertex AI. + final String project; + + /// The Google Cloud location to use for interacting with Vertex AI. + final String location; + + /// Index Endpoints API client. + final ProjectsLocationsIndexEndpointsResource _indexEndpointsApi; + + /// Index Endpoints Operations API client. + VertexAIIndexEndpointsOperationsApi get operations => + VertexAIIndexEndpointsOperationsApi( + indexEndpointsOperationsApi: _indexEndpointsApi.operations, + project: project, + location: location, + ); + + /// Creates an Index Endpoint. + /// + /// Documentation: + /// https://cloud.google.com/vertex-ai/docs/matching-engine/deploy-index-public + /// + /// - [displayName] The display name of the Index Endpoint. + /// - [description] The description of the Index Endpoint. + /// - [network] The full name of the Google Compute Engine network to which + /// the IndexEndpoint should be peered. + /// - [privateServiceConnectConfig] Configuration for private service connect. + /// [network] and [privateServiceConnectConfig] are mutually exclusive. + /// - [publicEndpointEnabled] If true, the deployed index will be accessible + /// through public endpoint. + /// - [labels] The labels with user-defined metadata to organize your + /// IndexEndpoints. + /// - [etag] Used to perform consistent read-modify-write updates. If not + /// set, a blind "overwrite" update happens. + Future create({ + required final String displayName, + required final String description, + final String? network, + final VertexAIPrivateServiceConnectConfig? privateServiceConnectConfig, + final bool publicEndpointEnabled = false, + final Map? labels, + final String? etag, + }) async { + final res = await _indexEndpointsApi.create( + GoogleCloudAiplatformV1IndexEndpoint( + displayName: displayName, + description: description, + network: network, + privateServiceConnectConfig: privateServiceConnectConfig != null + ? VertexAIIndexEndpointsGoogleApisMapper + .mapPrivateServiceConnectConfig( + privateServiceConnectConfig, + ) + : null, + publicEndpointEnabled: publicEndpointEnabled, + labels: labels, + etag: etag, + ), + 'projects/$project/locations/$location', + ); + return VertexAIOperationGoogleApisMapper.mapOperation(res); + } + + /// Returns the list of Index Endpoints in the same project and location. + Future> list() async { + final indexEndpoints = []; + String? pageToken; + do { + final res = await _indexEndpointsApi.list( + 'projects/$project/locations/$location', + ); + indexEndpoints.addAll( + res.indexEndpoints?.map( + VertexAIIndexEndpointsGoogleApisMapper.mapIndexEndpoint, + ) ?? + [], + ); + pageToken = res.nextPageToken; + } while (pageToken != null); + return indexEndpoints; + } + + /// Gets an Index Endpoint. + /// + /// - [id] The id of the index endpoint + Future get({ + required final String id, + }) async { + final response = await _indexEndpointsApi.get( + 'projects/$project/locations/$location/indexEndpoints/$id', + ); + return VertexAIIndexEndpointsGoogleApisMapper.mapIndexEndpoint(response); + } + + /// Deletes an Index Endpoint. + /// + /// - [id] The id of the index endpoint + Future delete({ + required final String id, + }) async { + final res = await _indexEndpointsApi.delete( + 'projects/$project/locations/$location/indexEndpoints/$id', + ); + return VertexAIOperationGoogleApisMapper.mapOperation(res); + } + + /// Deploys an Index into this IndexEndpoint, creating a DeployedIndex within + /// it. + /// + /// Only non-empty Indexes can be deployed. + /// + /// - [indexId] The ID of the Index we want to deploy. + /// - [indexEndpointId] The ID of the IndexEndpoint we want to deploy the + /// Index to. + /// - [deployedIndexId] A user specified string to uniquely identify the + /// deployed index. It must start with a letter and contain only letters, + /// numbers or underscores. + /// - [deployedIndexDisplayName] The display name of the DeployedIndex. + /// - [automaticResources] The resources that the IndexEndpoint should + /// automatically allocate for this DeployedIndex. + Future deployIndex({ + required final String indexId, + required final String indexEndpointId, + required final String deployedIndexId, + required final String deployedIndexDisplayName, + final VertexAIAutomaticResources? automaticResources, + }) async { + final res = await _indexEndpointsApi.deployIndex( + GoogleCloudAiplatformV1DeployIndexRequest( + deployedIndex: GoogleCloudAiplatformV1DeployedIndex( + id: deployedIndexId, + displayName: deployedIndexDisplayName, + index: 'projects/$project/locations/$location/indexes/$indexId', + automaticResources: automaticResources != null + ? VertexAIIndexEndpointsGoogleApisMapper.mapAutomaticResources( + automaticResources, + ) + : null, + ), + ), + 'projects/$project/locations/$location/indexEndpoints/$indexEndpointId', + ); + return VertexAIOperationGoogleApisMapper.mapOperation(res); + } + + /// Update an existing DeployedIndex under an IndexEndpoint. + /// + /// - [indexEndpointId] The ID of the IndexEndpoint we want to deploy the + /// Index to. + /// - [deployedIndexId] A user specified string to uniquely identify the + /// deployed index. It must start with a letter and contain only letters, + /// numbers or underscores. + /// - [deployedIndexDisplayName] The display name of the DeployedIndex. + /// - [automaticResources] The resources that the IndexEndpoint should + /// automatically allocate for this DeployedIndex. + Future mutateDeployedIndex({ + required final String indexEndpointId, + required final String deployedIndexId, + final String? deployedIndexDisplayName, + final VertexAIAutomaticResources? automaticResources, + }) async { + final res = await _indexEndpointsApi.mutateDeployedIndex( + GoogleCloudAiplatformV1DeployedIndex( + id: deployedIndexId, + displayName: deployedIndexDisplayName, + automaticResources: automaticResources != null + ? VertexAIIndexEndpointsGoogleApisMapper.mapAutomaticResources( + automaticResources, + ) + : null, + ), + 'projects/$project/locations/$location/indexEndpoints/$indexEndpointId', + ); + return VertexAIOperationGoogleApisMapper.mapOperation(res); + } + + /// Undeploy an Index from an IndexEndpoint, removing a DeployedIndex from + /// it, and freeing all resources it's using. + /// + /// - [indexEndpointId] The ID of the IndexEndpoint we want to deploy the + /// Index to. + /// - [deployedIndexId] A user specified string to uniquely identify the + /// deployed index. It must start with a letter and contain only letters, + /// numbers or underscores. + Future undeployIndex({ + required final String indexEndpointId, + required final String deployedIndexId, + }) async { + final res = await _indexEndpointsApi.undeployIndex( + GoogleCloudAiplatformV1UndeployIndexRequest( + deployedIndexId: deployedIndexId, + ), + 'projects/$project/locations/$location/indexEndpoints/$indexEndpointId', + ); + return VertexAIOperationGoogleApisMapper.mapOperation(res); + } + + /// Finds the nearest neighbors of each vector within the request. + /// + /// Mind that you probably need a different client for calling this method, + /// as the public query endpoint has a different rootUrl than the rest of the + /// API (e.g. https://xxxxxxxxxx.europe-west1-xxxxxxxxxxxx.vdb.vertexai.goog). + /// + /// Check the [VertexAIIndexEndpoint.publicEndpointDomainName] of your index + /// endpoint by calling [get]. Then create a new client setting the + /// [VertexAIMatchingEngineClient.rootUrl] to that value (mind that you need + /// to add `https://` to the beginning of the domain name). + /// + /// - [indexEndpointId] The id of the index endpoint. + /// - [deployedIndexId] The id of the deployed index. + /// - [queries] The search queries. + /// - [returnFullDatapoint] If set to true, the response will contain the + /// full datapoint for each nearest neighbor result. + Future findNeighbors({ + required final String indexEndpointId, + required final String deployedIndexId, + required final List queries, + final bool returnFullDatapoint = false, + }) async { + final res = await _indexEndpointsApi.findNeighbors( + GoogleCloudAiplatformV1FindNeighborsRequest( + deployedIndexId: deployedIndexId, + queries: queries + .map( + VertexAIIndexEndpointsGoogleApisMapper.mapRequestQuery, + ) + .toList(growable: false), + returnFullDatapoint: returnFullDatapoint, + ), + 'projects/$project/locations/$location/indexEndpoints/$indexEndpointId', + ); + return VertexAIIndexEndpointsGoogleApisMapper.mapFindNeighborsResponse(res); + } +} diff --git a/packages/vertex_ai/lib/src/matching_engine/apis/index_endpoints_operations.dart b/packages/vertex_ai/lib/src/matching_engine/apis/index_endpoints_operations.dart new file mode 100644 index 00000000..b518773c --- /dev/null +++ b/packages/vertex_ai/lib/src/matching_engine/apis/index_endpoints_operations.dart @@ -0,0 +1,94 @@ +import 'package:googleapis/aiplatform/v1.dart'; + +import '../mappers/mappers.dart'; +import '../models/models.dart'; + +/// {@template vertex_ai_index_endpoints_operations_api} +/// Vertex AI Index Endpoints Operations API. +/// +/// Documentation: +/// https://cloud.google.com/vertex-ai/docs/reference/rest/v1/projects.locations.indexEndpoints +/// {@endtemplate} +class VertexAIIndexEndpointsOperationsApi { + /// {@macro vertex_ai_index_endpoints_operations_api} + VertexAIIndexEndpointsOperationsApi({ + required final ProjectsLocationsIndexEndpointsOperationsResource + indexEndpointsOperationsApi, + required this.project, + required this.location, + }) : _indexEndpointsOperationsApi = indexEndpointsOperationsApi; + + /// The Google Cloud project to use for interacting with Vertex AI. + final String project; + + /// The Google Cloud location to use for interacting with Vertex AI. + final String location; + + /// Index Endpoints Operations API client. + final ProjectsLocationsIndexEndpointsOperationsResource + _indexEndpointsOperationsApi; + + /// Returns the list of operations for the specified index endpoint. + /// + /// - [indexEndpointId] - The ID of the index endpoint whose operations you + /// want to list. + Future> list({ + required final String indexEndpointId, + }) async { + final operations = []; + String? pageToken; + do { + final res = await _indexEndpointsOperationsApi.list( + 'projects/$project/locations/$location/indexEndpoints/$indexEndpointId', + ); + operations.addAll( + res.operations?.map(VertexAIOperationGoogleApisMapper.mapOperation) ?? + [], + ); + pageToken = res.nextPageToken; + } while (pageToken != null); + return operations; + } + + /// Gets the latest state of a long-running operation. + /// + /// Clients can use this method to poll the operation result at intervals as + /// recommended by the API service. + /// + /// - [name] - The name of the operation resource (e.g. + /// `projects/{project}/locations/{location}/indexEndpoints/{indexEndpoints}/operations/{operation}`). + Future get({ + required final String name, + }) async { + final res = await _indexEndpointsOperationsApi.get(name); + return VertexAIOperationGoogleApisMapper.mapOperation(res); + } + + /// Starts asynchronous cancellation on a long-running operation. + /// + /// The server makes a best effort to cancel the operation, but success is not + /// guaranteed. Clients can use [get] method to check whether the + /// cancellation succeeded or whether the operation completed despite + /// cancellation. + /// + /// - [name] - The name of the operation resource (e.g. + /// `projects/{project}/locations/{location}/indexEndpoints/{indexEndpoints}/operations/{operation}`). + Future cancel({ + required final String name, + }) async { + await _indexEndpointsOperationsApi.cancel(name); + } + + /// Deletes a long-running operation. + /// + /// This method indicates that the client is no longer interested in the + /// operation result. It does not cancel the operation. + /// + /// - [name] - The name of the operation resource (e.g. + /// `projects/{project}/locations/{location}/indexEndpoints/{indexEndpoints}/operations/{operation}`). + Future delete({ + required final String name, + }) async { + await _indexEndpointsOperationsApi.delete(name); + } +} diff --git a/packages/vertex_ai/lib/src/matching_engine/apis/indexes.dart b/packages/vertex_ai/lib/src/matching_engine/apis/indexes.dart new file mode 100644 index 00000000..bc75d6dc --- /dev/null +++ b/packages/vertex_ai/lib/src/matching_engine/apis/indexes.dart @@ -0,0 +1,116 @@ +import 'package:googleapis/aiplatform/v1.dart'; + +import '../mappers/mappers.dart'; +import '../models/models.dart'; +import 'indexes_operations.dart'; + +/// {@template vertex_ai_indexes_api} +/// Vertex AI Indexes API. +/// +/// Documentation: +/// https://cloud.google.com/vertex-ai/docs/reference/rest/v1/projects.locations.indexes +/// {@endtemplate} +class VertexAIIndexesApi { + /// {@macro vertex_ai_indexes_api} + VertexAIIndexesApi({ + required final ProjectsLocationsIndexesResource indexesApi, + required this.project, + required this.location, + }) : _indexesApi = indexesApi; + + /// The Google Cloud project to use for interacting with Vertex AI. + final String project; + + /// The Google Cloud location to use for interacting with Vertex AI. + final String location; + + /// Indexes API client. + final ProjectsLocationsIndexesResource _indexesApi; + + /// Indexes Operations API client. + VertexAIIndexesOperationsApi get operations => VertexAIIndexesOperationsApi( + indexesOperationsApi: _indexesApi.operations, + project: project, + location: location, + ); + + /// Creates an Index. + /// + /// Documentation: + /// https://cloud.google.com/vertex-ai/docs/matching-engine/create-manage-index + /// + /// - [displayName] The display name of the Index. The name can be up to 128 + /// characters long and can consist of any UTF-8 characters. + /// - [description] The description of the Index. + /// - [metadata] additional information about the Index. + /// - [indexUpdateMethod] The index update method. + /// - [labels] The labels with user-defined metadata to organize your Indexes. + /// - [etag] Used to perform consistent read-modify-write updates. If not + /// set, a blind "overwrite" update happens. + Future create({ + required final String displayName, + required final String description, + required final VertexAIIndexCreationMetadata metadata, + final VertexAIIndexUpdateMethod indexUpdateMethod = + VertexAIIndexUpdateMethod.batchUpdate, + final Map? labels, + final String? etag, + }) async { + final metadataMap = metadata.toMap(); + final res = await _indexesApi.create( + GoogleCloudAiplatformV1Index( + displayName: displayName, + description: description, + metadata: metadataMap, + indexUpdateMethod: indexUpdateMethod.id, + labels: labels, + etag: etag, + ), + 'projects/$project/locations/$location', + ); + return VertexAIOperationGoogleApisMapper.mapOperation(res); + } + + /// Returns the list of Indexes in the same project and location. + Future> list() async { + final indexes = []; + String? pageToken; + do { + final res = await _indexesApi.list( + 'projects/$project/locations/$location', + ); + indexes.addAll( + res.indexes?.map(VertexAIIndexGoogleApisMapper.mapIndex) ?? [], + ); + pageToken = res.nextPageToken; + } while (pageToken != null); + return indexes; + } + + /// Gets an Index. + /// + /// - [id] The id of the index + Future get({ + required final String id, + }) async { + final response = await _indexesApi.get( + 'projects/$project/locations/$location/indexes/$id', + ); + return VertexAIIndexGoogleApisMapper.mapIndex(response); + } + + /// Deletes an Index. + /// + /// An Index can only be deleted when all its DeployedIndexes had been + /// undeployed. + /// + /// - [id] The id of the index + Future delete({ + required final String id, + }) async { + final res = await _indexesApi.delete( + 'projects/$project/locations/$location/indexes/$id', + ); + return VertexAIOperationGoogleApisMapper.mapOperation(res); + } +} diff --git a/packages/vertex_ai/lib/src/matching_engine/apis/indexes_operations.dart b/packages/vertex_ai/lib/src/matching_engine/apis/indexes_operations.dart new file mode 100644 index 00000000..156a9041 --- /dev/null +++ b/packages/vertex_ai/lib/src/matching_engine/apis/indexes_operations.dart @@ -0,0 +1,92 @@ +import 'package:googleapis/aiplatform/v1.dart'; + +import '../mappers/mappers.dart'; +import '../models/models.dart'; + +/// {@template vertex_ai_indexes_operations_api} +/// Vertex AI Indexes Operations API. +/// +/// Documentation: +/// https://cloud.google.com/vertex-ai/docs/reference/rest/v1/projects.locations.indexes +/// {@endtemplate} +class VertexAIIndexesOperationsApi { + /// {@macro vertex_ai_indexes_operations_api} + VertexAIIndexesOperationsApi({ + required final ProjectsLocationsIndexesOperationsResource + indexesOperationsApi, + required this.project, + required this.location, + }) : _indexesOperationsApi = indexesOperationsApi; + + /// The Google Cloud project to use for interacting with Vertex AI. + final String project; + + /// The Google Cloud location to use for interacting with Vertex AI. + final String location; + + /// Indexes Operations API client. + final ProjectsLocationsIndexesOperationsResource _indexesOperationsApi; + + /// Returns the list of operations for the specified index. + /// + /// - [indexId] The ID of the index whose operations you want to list. + Future> list({ + required final String indexId, + }) async { + final operations = []; + String? pageToken; + do { + final res = await _indexesOperationsApi.list( + 'projects/$project/locations/$location/indexes/$indexId', + ); + operations.addAll( + res.operations?.map(VertexAIOperationGoogleApisMapper.mapOperation) ?? + [], + ); + pageToken = res.nextPageToken; + } while (pageToken != null); + return operations; + } + + /// Gets the latest state of a long-running operation. + /// + /// Clients can use this method to poll the operation result at intervals as + /// recommended by the API service. + /// + /// - [name] The name of the operation resource (e.g. + /// `projects/{project}/locations/{location}/indexes/{index}/operations/{operation}`). + Future get({ + required final String name, + }) async { + final res = await _indexesOperationsApi.get(name); + return VertexAIOperationGoogleApisMapper.mapOperation(res); + } + + /// Starts asynchronous cancellation on a long-running operation. + /// + /// The server makes a best effort to cancel the operation, but success is not + /// guaranteed. Clients can use [get] method to check whether the + /// cancellation succeeded or whether the operation completed despite + /// cancellation. + /// + /// - [name] The name of the operation resource (e.g. + /// `projects/{project}/locations/{location}/indexes/{index}/operations/{operation}`). + Future cancel({ + required final String name, + }) async { + await _indexesOperationsApi.cancel(name); + } + + /// Deletes a long-running operation. + /// + /// This method indicates that the client is no longer interested in the + /// operation result. It does not cancel the operation. + /// + /// - [name] The name of the operation resource (e.g. + /// `projects/{project}/locations/{location}/indexes/{index}/operations/{operation}`). + Future delete({ + required final String name, + }) async { + await _indexesOperationsApi.delete(name); + } +} diff --git a/packages/vertex_ai/lib/src/matching_engine/machine_engine.dart b/packages/vertex_ai/lib/src/matching_engine/machine_engine.dart new file mode 100644 index 00000000..d852c0aa --- /dev/null +++ b/packages/vertex_ai/lib/src/matching_engine/machine_engine.dart @@ -0,0 +1,3 @@ +export 'apis/apis.dart'; +export 'matching_engine_client.dart'; +export 'models/models.dart'; diff --git a/packages/vertex_ai/lib/src/matching_engine/mappers/index_endpoints.dart b/packages/vertex_ai/lib/src/matching_engine/mappers/index_endpoints.dart new file mode 100644 index 00000000..51581914 --- /dev/null +++ b/packages/vertex_ai/lib/src/matching_engine/mappers/index_endpoints.dart @@ -0,0 +1,217 @@ +import 'package:googleapis/aiplatform/v1.dart'; + +import '../models/index_endpoints.dart'; +import 'indexes.dart'; + +/// Vertex AI Index Endpoints to googleapis models. +class VertexAIIndexEndpointsGoogleApisMapper { + static GoogleCloudAiplatformV1PrivateServiceConnectConfig + mapPrivateServiceConnectConfig( + final VertexAIPrivateServiceConnectConfig privateServiceConnectConfig, + ) { + return GoogleCloudAiplatformV1PrivateServiceConnectConfig( + enablePrivateServiceConnect: + privateServiceConnectConfig.enablePrivateServiceConnect, + projectAllowlist: privateServiceConnectConfig.projectAllowlist, + ); + } + + static GoogleCloudAiplatformV1AutomaticResources mapAutomaticResources( + final VertexAIAutomaticResources automaticResources, + ) { + return GoogleCloudAiplatformV1AutomaticResources( + maxReplicaCount: automaticResources.maxReplicaCount, + minReplicaCount: automaticResources.minReplicaCount, + ); + } + + static GoogleCloudAiplatformV1FindNeighborsRequestQuery mapRequestQuery( + final VertexAIFindNeighborsRequestQuery query, + ) { + return GoogleCloudAiplatformV1FindNeighborsRequestQuery( + datapoint: + VertexAIIndexGoogleApisMapper.mapIndexDatapoint(query.datapoint), + neighborCount: query.neighborCount, + approximateNeighborCount: query.approximateNeighborCount, + fractionLeafNodesToSearchOverride: + query.fractionLeafNodesToSearchOverride, + perCrowdingAttributeNeighborCount: + query.perCrowdingAttributeNeighborCount, + ); + } + + static VertexAIIndexEndpoint mapIndexEndpoint( + final GoogleCloudAiplatformV1IndexEndpoint indexEndpoint, + ) { + return VertexAIIndexEndpoint( + name: indexEndpoint.name ?? '', + displayName: indexEndpoint.displayName ?? '', + description: indexEndpoint.description ?? '', + network: indexEndpoint.network, + privateServiceConnectConfig: + indexEndpoint.privateServiceConnectConfig != null + ? _mapPrivateServiceConnectConfig( + indexEndpoint.privateServiceConnectConfig!, + ) + : null, + publicEndpointEnabled: indexEndpoint.publicEndpointEnabled ?? false, + publicEndpointDomainName: indexEndpoint.publicEndpointDomainName, + deployedIndexes: indexEndpoint.deployedIndexes != null + ? indexEndpoint.deployedIndexes! + .map(_mapDeployedIndex) + .toList(growable: false) + : [], + labels: indexEndpoint.labels, + etag: indexEndpoint.etag, + createTime: + DateTime.tryParse(indexEndpoint.createTime ?? '') ?? DateTime.now(), + updateTime: + DateTime.tryParse(indexEndpoint.updateTime ?? '') ?? DateTime.now(), + ); + } + + static VertexAIPrivateServiceConnectConfig _mapPrivateServiceConnectConfig( + final GoogleCloudAiplatformV1PrivateServiceConnectConfig + privateServiceConnectConfig, + ) { + return VertexAIPrivateServiceConnectConfig( + enablePrivateServiceConnect: + privateServiceConnectConfig.enablePrivateServiceConnect ?? false, + projectAllowlist: privateServiceConnectConfig.projectAllowlist, + ); + } + + static VertexAIDeployedIndex _mapDeployedIndex( + final GoogleCloudAiplatformV1DeployedIndex deployedIndex, + ) { + return VertexAIDeployedIndex( + id: deployedIndex.id ?? '', + index: deployedIndex.index ?? '', + displayName: deployedIndex.displayName ?? '', + createTime: + DateTime.tryParse(deployedIndex.createTime ?? '') ?? DateTime.now(), + indexSyncTime: DateTime.tryParse(deployedIndex.indexSyncTime ?? '') ?? + DateTime.now(), + automaticResources: + _mapAutomaticResources(deployedIndex.automaticResources), + dedicatedResources: deployedIndex.dedicatedResources != null + ? _mapDedicatedResources(deployedIndex.dedicatedResources) + : null, + deployedIndexAuthConfig: deployedIndex.deployedIndexAuthConfig != null + ? _mapDeployedIndexAuthConfig(deployedIndex.deployedIndexAuthConfig!) + : null, + privateEndpoints: deployedIndex.privateEndpoints != null + ? _mapPrivateEndpoints(deployedIndex.privateEndpoints!) + : null, + reservedIpRanges: deployedIndex.reservedIpRanges ?? [], + deploymentGroup: deployedIndex.deploymentGroup ?? '', + enableAccessLogging: deployedIndex.enableAccessLogging ?? false, + ); + } + + static VertexAIAutomaticResources _mapAutomaticResources( + final GoogleCloudAiplatformV1AutomaticResources? automaticResources, + ) { + return VertexAIAutomaticResources( + maxReplicaCount: automaticResources?.maxReplicaCount ?? 0, + minReplicaCount: automaticResources?.minReplicaCount ?? 0, + ); + } + + static VertexAIDedicatedResources _mapDedicatedResources( + final GoogleCloudAiplatformV1DedicatedResources? dedicatedResources, + ) { + return VertexAIDedicatedResources( + autoscalingMetricSpecs: dedicatedResources?.autoscalingMetricSpecs + ?.map(_mapAutoscalingMetricSpec) + .toList(growable: false) ?? + [], + machineSpec: _mapMachineSpec(dedicatedResources!.machineSpec), + minReplicaCount: dedicatedResources.minReplicaCount ?? 0, + maxReplicaCount: dedicatedResources.maxReplicaCount ?? 0, + ); + } + + static VertexAIAutoscalingMetricSpec _mapAutoscalingMetricSpec( + final GoogleCloudAiplatformV1AutoscalingMetricSpec autoscalingMetricSpec, + ) { + return VertexAIAutoscalingMetricSpec( + metricName: autoscalingMetricSpec.metricName ?? '', + target: autoscalingMetricSpec.target ?? 0, + ); + } + + static VertexAIMachineSpec _mapMachineSpec( + final GoogleCloudAiplatformV1MachineSpec? machineSpec, + ) { + return VertexAIMachineSpec( + machineType: machineSpec?.machineType ?? '', + acceleratorType: machineSpec?.acceleratorType ?? '', + acceleratorCount: machineSpec?.acceleratorCount ?? 0, + ); + } + + static VertexAIDeployedIndexAuthConfig _mapDeployedIndexAuthConfig( + final GoogleCloudAiplatformV1DeployedIndexAuthConfig authConfig, + ) { + return VertexAIDeployedIndexAuthConfig( + authProvider: _mapAuthProvider(authConfig.authProvider), + ); + } + + static VertexAIDeployedIndexAuthConfigAuthProvider _mapAuthProvider( + final GoogleCloudAiplatformV1DeployedIndexAuthConfigAuthProvider? + authProvider, + ) { + return VertexAIDeployedIndexAuthConfigAuthProvider( + allowedIssuers: authProvider?.allowedIssuers ?? [], + audiences: authProvider?.audiences ?? [], + ); + } + + static VertexAIIndexPrivateEndpoints _mapPrivateEndpoints( + final GoogleCloudAiplatformV1IndexPrivateEndpoints privateEndpoints, + ) { + return VertexAIIndexPrivateEndpoints( + matchGrpcAddress: privateEndpoints.matchGrpcAddress ?? '', + serviceAttachment: privateEndpoints.serviceAttachment ?? '', + ); + } + + static VertexAIFindNeighborsResponse mapFindNeighborsResponse( + final GoogleCloudAiplatformV1FindNeighborsResponse response, + ) { + return VertexAIFindNeighborsResponse( + nearestNeighbors: response.nearestNeighbors != null + ? response.nearestNeighbors! + .map(_mapNearestNeighbors) + .toList(growable: false) + : [], + ); + } + + static VertexAIFindNeighborsResponseNearestNeighbors _mapNearestNeighbors( + final GoogleCloudAiplatformV1FindNeighborsResponseNearestNeighbors + nearestNeighbors, + ) { + return VertexAIFindNeighborsResponseNearestNeighbors( + id: nearestNeighbors.id ?? '', + neighbors: nearestNeighbors.neighbors != null + ? nearestNeighbors.neighbors! + .map(_mapNeighbor) + .toList(growable: false) + : [], + ); + } + + static VertexAIFindNeighborsResponseNeighbor _mapNeighbor( + final GoogleCloudAiplatformV1FindNeighborsResponseNeighbor neighbor, + ) { + return VertexAIFindNeighborsResponseNeighbor( + datapoint: VertexAIIndexGoogleApisMapper.mapIndexDatapointDto( + neighbor.datapoint!, + ), + distance: neighbor.distance ?? 0.0, + ); + } +} diff --git a/packages/vertex_ai/lib/src/matching_engine/mappers/indexes.dart b/packages/vertex_ai/lib/src/matching_engine/mappers/indexes.dart new file mode 100644 index 00000000..10a49e3c --- /dev/null +++ b/packages/vertex_ai/lib/src/matching_engine/mappers/indexes.dart @@ -0,0 +1,103 @@ +import 'package:collection/collection.dart'; +import 'package:googleapis/aiplatform/v1.dart'; + +import '../models/indexes.dart'; + +/// Vertex AI Index Mapper to googleapis models. +class VertexAIIndexGoogleApisMapper { + static VertexAIIndex mapIndex(final GoogleCloudAiplatformV1Index index) { + return VertexAIIndex( + name: index.name ?? '', + displayName: index.displayName ?? '', + description: index.description ?? '', + metadataSchemaUri: index.metadataSchemaUri ?? '', + metadata: VertexAIIndexMetadata.fromMap( + index.metadata as Map? ?? const {}, + ), + indexUpdateMethod: VertexAIIndexUpdateMethod.values + .firstWhereOrNull((final m) => m.id == index.indexUpdateMethod) ?? + VertexAIIndexUpdateMethod.batchUpdate, + indexStats: index.indexStats != null ? mapStats(index.indexStats!) : null, + labels: index.labels, + etag: index.etag ?? '', + createTime: DateTime.tryParse(index.createTime ?? '') ?? DateTime.now(), + updateTime: DateTime.tryParse(index.updateTime ?? '') ?? DateTime.now(), + ); + } + + static VertexAIIndexStats mapStats( + final GoogleCloudAiplatformV1IndexStats stats, + ) { + return VertexAIIndexStats( + shardsCount: stats.shardsCount ?? 0, + // the API returns a string instead of an int + vectorsCount: int.tryParse(stats.vectorsCount.toString()) ?? 0, + ); + } + + static GoogleCloudAiplatformV1IndexDatapoint mapIndexDatapoint( + final VertexAIIndexDatapoint datapoint, + ) { + return GoogleCloudAiplatformV1IndexDatapoint( + datapointId: datapoint.datapointId, + featureVector: datapoint.featureVector, + crowdingTag: datapoint.crowdingTag != null + ? _mapCrowdingTag(datapoint.crowdingTag!) + : null, + restricts: datapoint.restricts != null + ? datapoint.restricts!.map(_mapRestriction).toList(growable: false) + : null, + ); + } + + static GoogleCloudAiplatformV1IndexDatapointCrowdingTag _mapCrowdingTag( + final VertexAIIndexDatapointCrowdingTag crowdingTag, + ) { + return GoogleCloudAiplatformV1IndexDatapointCrowdingTag( + crowdingAttribute: crowdingTag.crowdingAttribute, + ); + } + + static GoogleCloudAiplatformV1IndexDatapointRestriction _mapRestriction( + final VertexAIIndexDatapointRestriction restriction, + ) { + return GoogleCloudAiplatformV1IndexDatapointRestriction( + namespace: restriction.namespace, + allowList: restriction.allowList, + denyList: restriction.denyList, + ); + } + + static VertexAIIndexDatapoint mapIndexDatapointDto( + final GoogleCloudAiplatformV1IndexDatapoint datapoint, + ) { + return VertexAIIndexDatapoint( + datapointId: datapoint.datapointId ?? '', + featureVector: datapoint.featureVector ?? [], + crowdingTag: datapoint.crowdingTag != null + ? _mapCrowdingTagDto(datapoint.crowdingTag!) + : null, + restricts: datapoint.restricts != null + ? datapoint.restricts!.map(_mapRestrictionDto).toList(growable: false) + : null, + ); + } + + static VertexAIIndexDatapointCrowdingTag _mapCrowdingTagDto( + final GoogleCloudAiplatformV1IndexDatapointCrowdingTag crowdingTag, + ) { + return VertexAIIndexDatapointCrowdingTag( + crowdingAttribute: crowdingTag.crowdingAttribute ?? '', + ); + } + + static VertexAIIndexDatapointRestriction _mapRestrictionDto( + final GoogleCloudAiplatformV1IndexDatapointRestriction restriction, + ) { + return VertexAIIndexDatapointRestriction( + namespace: restriction.namespace ?? '', + allowList: restriction.allowList ?? [], + denyList: restriction.denyList ?? [], + ); + } +} diff --git a/packages/vertex_ai/lib/src/matching_engine/mappers/mappers.dart b/packages/vertex_ai/lib/src/matching_engine/mappers/mappers.dart new file mode 100644 index 00000000..a5fa1920 --- /dev/null +++ b/packages/vertex_ai/lib/src/matching_engine/mappers/mappers.dart @@ -0,0 +1,3 @@ +export 'index_endpoints.dart'; +export 'indexes.dart'; +export 'operation.dart'; diff --git a/packages/vertex_ai/lib/src/matching_engine/mappers/operation.dart b/packages/vertex_ai/lib/src/matching_engine/mappers/operation.dart new file mode 100644 index 00000000..f7b01901 --- /dev/null +++ b/packages/vertex_ai/lib/src/matching_engine/mappers/operation.dart @@ -0,0 +1,29 @@ +import 'package:googleapis/aiplatform/v1.dart'; + +import '../models/operation.dart'; + +/// Vertex AI Operation to googleapis models. +class VertexAIOperationGoogleApisMapper { + static VertexAIOperation mapOperation( + final GoogleLongrunningOperation operation, + ) { + return VertexAIOperation( + name: operation.name ?? '', + done: operation.done ?? false, + response: operation.response, + error: + operation.error != null ? _mapOperationError(operation.error!) : null, + metadata: operation.metadata, + ); + } + + static VertexAIOperationError _mapOperationError( + final GoogleRpcStatus error, + ) { + return VertexAIOperationError( + code: error.code ?? 0, + details: error.details ?? const [], + message: error.message ?? '', + ); + } +} diff --git a/packages/vertex_ai/lib/src/matching_engine/matching_engine_client.dart b/packages/vertex_ai/lib/src/matching_engine/matching_engine_client.dart new file mode 100644 index 00000000..649fa432 --- /dev/null +++ b/packages/vertex_ai/lib/src/matching_engine/matching_engine_client.dart @@ -0,0 +1,115 @@ +import 'package:googleapis/aiplatform/v1.dart'; +import 'package:googleapis_auth/googleapis_auth.dart'; + +import 'apis/apis.dart'; + +/// {@template vertex_ai_matching_engine_client} +/// A client for interacting with Vertex AI's Matching Engine vector database. +/// +/// APIs available: +/// - [indexes] API: to create and manage vector indexes. +/// - [indexEndpoints] API: to create and manage vector index endpoints. +/// +/// Vertex AI Matching Engine documentation: +/// https://cloud.google.com/vertex-ai/docs/matching-engine/overview +/// +/// ### Set up your Google Cloud project +/// +/// 1. [Select or create a Google Cloud project](https://console.cloud.google.com/cloud-resource-manager). +/// 2. [Make sure that billing is enabled for your project](https://cloud.google.com/billing/docs/how-to/modify-project). +/// 3. [Enable the Vertex AI API](https://console.cloud.google.com/flows/enableapi?apiid=aiplatform.googleapis.com). +/// 4. [Configure the Vertex AI location](https://cloud.google.com/vertex-ai/docs/general/locations). +/// +/// ### Authentication +/// +/// The `VertexAIMatchingEngineClient` delegates authentication to the +/// [googleapis_auth](https://pub.dev/packages/googleapis_auth) package. +/// +/// To create an instance of `VertexAIMatchingEngineClient` you need to provide +/// an [`AuthClient`](https://pub.dev/documentation/googleapis_auth/latest/googleapis_auth/AuthClient-class.html) +/// instance. +/// +/// There are several ways to obtain an `AuthClient` depending on your use case. +/// Check out the [googleapis_auth](https://pub.dev/packages/googleapis_auth) +/// package documentation for more details. +/// +/// Example using a service account JSON: +/// +/// ```dart +/// final serviceAccountCredentials = ServiceAccountCredentials.fromJson( +/// json.decode(serviceAccountJson), +/// ); +/// final authClient = await clientViaServiceAccount( +/// serviceAccountCredentials, +/// [VertexAIGenAIClient.cloudPlatformScope], +/// ); +/// final vertexAi = VertexAIMatchingEngineClient( +/// authHttpClient: authClient, +/// project: 'your-project-id', +/// ); +/// ``` +/// +/// To be able to create and manage indexes and index endpoints, the service +/// account should have the following [permissions](https://cloud.google.com/vertex-ai/docs/general/iam-permissions): +/// - `aiplatform.indexes.create` +/// - `aiplatform.indexes.get` +/// - `aiplatform.indexes.list` +/// - `aiplatform.indexes.update` +/// - `aiplatform.indexes.delete` +/// - `aiplatform.indexEndpoints.create` +/// - `aiplatform.indexEndpoints.get` +/// - `aiplatform.indexEndpoints.list` +/// - `aiplatform.indexEndpoints.update` +/// - `aiplatform.indexEndpoints.delete` +/// - `aiplatform.indexEndpoints.deploy` +/// - `aiplatform.indexEndpoints.undeploy` +/// +/// If you just want to query an index endpoint, the service account only needs: +/// - `aiplatform.indexEndpoints.queryVectors` +/// +/// The required[OAuth2 scope](https://developers.google.com/identity/protocols/oauth2/scopes) +/// is: +/// - `https://www.googleapis.com/auth/cloud-platform` (you can use the constant +/// `VertexAIMatchingEngineClient.cloudPlatformScope`) +/// +/// See: https://cloud.google.com/vertex-ai/docs/generative-ai/access-control +/// {@endtemplate} +class VertexAIMatchingEngineClient { + VertexAIMatchingEngineClient({ + required final AuthClient authHttpClient, + required this.project, + this.location = 'us-central1', + final String? rootUrl, + }) : _vertexAiApi = AiplatformApi( + authHttpClient, + rootUrl: rootUrl ?? 'https://$location-aiplatform.googleapis.com/', + ); + + /// The Google Cloud project to use for interacting with Vertex AI. + final String project; + + /// The Google Cloud location to use for interacting with Vertex AI. + /// + /// See: https://cloud.google.com/vertex-ai/docs/general/locations + final String location; + + /// Vertex AI API client. + final AiplatformApi _vertexAiApi; + + /// Scope required for Vertex AI API calls. + static const cloudPlatformScope = AiplatformApi.cloudPlatformScope; + + /// Indexes API client. + VertexAIIndexesApi get indexes => VertexAIIndexesApi( + indexesApi: _vertexAiApi.projects.locations.indexes, + project: project, + location: location, + ); + + /// Index Endpoints API client. + VertexAIIndexEndpointsApi get indexEndpoints => VertexAIIndexEndpointsApi( + indexEndpointsApi: _vertexAiApi.projects.locations.indexEndpoints, + project: project, + location: location, + ); +} diff --git a/packages/vertex_ai/lib/src/matching_engine/models/index_endpoints.dart b/packages/vertex_ai/lib/src/matching_engine/models/index_endpoints.dart new file mode 100644 index 00000000..9be9afca --- /dev/null +++ b/packages/vertex_ai/lib/src/matching_engine/models/index_endpoints.dart @@ -0,0 +1,942 @@ +import 'package:collection/collection.dart'; +import 'package:meta/meta.dart'; + +import 'indexes.dart'; + +/// {@template vertex_ai_index_endpoint} +/// Indexes are deployed into it. +/// +/// An IndexEndpoint can have multiple DeployedIndexes. +/// {@endtemplate} +@immutable +class VertexAIIndexEndpoint { + /// {@macro vertex_ai_index_endpoint} + const VertexAIIndexEndpoint({ + required this.name, + required this.displayName, + required this.description, + this.network, + this.privateServiceConnectConfig, + required this.publicEndpointEnabled, + this.publicEndpointDomainName, + required this.deployedIndexes, + this.labels, + this.etag, + required this.createTime, + required this.updateTime, + }); + + /// The id of the IndexEndpoint. + String get id => name.split('/').last; + + /// The resource name of the IndexEndpoint. + final String name; + + /// The display name of the IndexEndpoint. + /// + /// The name can be up to 128 characters long and can consist of any UTF-8 + /// characters. + final String displayName; + + /// The description of the IndexEndpoint. + final String description; + + /// The full name of the Google Compute Engine + /// [network](https://cloud.google.com/compute/docs/networks-and-firewalls#networks) + /// to which the IndexEndpoint should be peered. + /// + /// Private services access must already be configured for the network. If + /// left unspecified, the Endpoint is not peered with any network. network and + /// private_service_connect_config are mutually exclusive. + /// [Format](https://cloud.google.com/compute/docs/reference/rest/v1/networks/insert): + /// `projects/{project}/global/networks/{network}`. Where {project} is a + /// project number, as in '12345', and {network} is network name. + final String? network; + + /// Configuration for private service connect. + /// + /// [network] and [privateServiceConnectConfig] are mutually exclusive. + final VertexAIPrivateServiceConnectConfig? privateServiceConnectConfig; + + /// If true, the deployed index will be accessible through public endpoint. + final bool publicEndpointEnabled; + + /// If [publicEndpointEnabled is true, this field will be populated with the + /// domain name to use for this index endpoint. + final String? publicEndpointDomainName; + + /// The indexes deployed in this endpoint. + final List deployedIndexes; + + /// The labels with user-defined metadata to organize your Indexes. + /// + /// Label keys and values can be no longer than 64 characters (Unicode + /// codepoints), can only contain lowercase letters, numeric characters, + /// underscores and dashes. International characters are allowed. See + /// https://goo.gl/xmQnxf for more information and examples of labels. + final Map? labels; + + /// Used to perform consistent read-modify-write updates. + /// + /// If not set, a blind "overwrite" update happens. + final String? etag; + + /// Timestamp when this IndexEndpoint was created. + final DateTime createTime; + + /// Timestamp when this IndexEndpoint was last updated. + /// + /// This timestamp is not updated when the endpoint's DeployedIndexes are + /// updated, e.g. due to updates of the original Indexes they are the + /// deployments of. + final DateTime updateTime; + + @override + bool operator ==(covariant final VertexAIIndexEndpoint other) { + const listEquals = ListEquality(); + const mapEquals = MapEquality(); + return identical(this, other) || + runtimeType == other.runtimeType && + name == other.name && + displayName == other.displayName && + description == other.description && + network == other.network && + privateServiceConnectConfig == other.privateServiceConnectConfig && + publicEndpointEnabled == other.publicEndpointEnabled && + publicEndpointDomainName == other.publicEndpointDomainName && + listEquals.equals(deployedIndexes, other.deployedIndexes) && + mapEquals.equals(labels, other.labels) && + etag == other.etag && + createTime == other.createTime && + updateTime == other.updateTime; + } + + @override + int get hashCode => + name.hashCode ^ + displayName.hashCode ^ + description.hashCode ^ + network.hashCode ^ + privateServiceConnectConfig.hashCode ^ + publicEndpointEnabled.hashCode ^ + publicEndpointDomainName.hashCode ^ + const ListEquality().hash(deployedIndexes) ^ + const MapEquality().hash(labels) ^ + etag.hashCode ^ + createTime.hashCode ^ + updateTime.hashCode; + + @override + String toString() { + return 'VertexAIIndexEndpoint{' + 'name: $name, ' + 'displayName: $displayName, ' + 'description: $description, ' + 'network: $network, ' + 'privateServiceConnectConfig: $privateServiceConnectConfig, ' + 'publicEndpointEnabled: $publicEndpointEnabled, ' + 'publicEndpointDomainName: $publicEndpointDomainName, ' + 'deployedIndexes: $deployedIndexes, ' + 'labels: $labels, ' + 'etag: $etag, ' + 'createTime: $createTime, ' + 'updateTime: $updateTime}'; + } +} + +/// {@template vertex_ai_private_service_connect_config} +/// Represents configuration for private service connect. +/// {@endtemplate} +@immutable +class VertexAIPrivateServiceConnectConfig { + /// {@macro vertex_ai_private_service_connect_config} + const VertexAIPrivateServiceConnectConfig({ + required this.enablePrivateServiceConnect, + this.projectAllowlist, + }); + + /// If true, expose the IndexEndpoint via private service connect. + final bool enablePrivateServiceConnect; + + /// A list of Projects from which the forwarding rule will target the service + /// attachment. + final List? projectAllowlist; + + @override + bool operator ==(covariant final VertexAIPrivateServiceConnectConfig other) { + const listEquals = ListEquality(); + return identical(this, other) || + runtimeType == other.runtimeType && + enablePrivateServiceConnect == other.enablePrivateServiceConnect && + listEquals.equals(projectAllowlist, other.projectAllowlist); + } + + @override + int get hashCode => + enablePrivateServiceConnect.hashCode ^ + const ListEquality().hash(projectAllowlist); + + @override + String toString() { + return 'VertexAIPrivateServiceConnectConfig{' + 'enablePrivateServiceConnect: $enablePrivateServiceConnect, ' + 'projectAllowlist: $projectAllowlist}'; + } +} + +/// {@template vertex_ai_deployed_index} +/// A deployment of an Index. +/// +/// IndexEndpoints contain one or more [VertexAIDeployedIndex]. +/// {@endtemplate} +@immutable +class VertexAIDeployedIndex { + /// {@macro vertex_ai_deployed_index} + const VertexAIDeployedIndex({ + required this.id, + required this.index, + required this.displayName, + required this.createTime, + required this.indexSyncTime, + this.automaticResources = const VertexAIAutomaticResources( + maxReplicaCount: 2, + minReplicaCount: 2, + ), + this.dedicatedResources, + this.deployedIndexAuthConfig, + this.privateEndpoints, + this.reservedIpRanges, + this.deploymentGroup = 'default', + this.enableAccessLogging = false, + }); + + /// The user specified ID of the DeployedIndex. + /// + /// The ID can be up to 128 characters long and must start with a letter and + /// only contain letters, numbers, and underscores. The ID must be unique + /// within the project it is created in. + final String id; + + /// The name of the Index this is the deployment of. + /// + /// We may refer to this Index as the DeployedIndex's "original" Index. + final String index; + + /// The display name of the DeployedIndex. + /// + /// If not provided upon creation, the Index's display_name is used. + final String displayName; + + /// Timestamp when the DeployedIndex was created. + final DateTime createTime; + + /// The DeployedIndex may depend on various data on its original Index. + /// + /// Additionally when certain changes to the original Index are being done + /// (e.g. when what the Index contains is being changed) the DeployedIndex may + /// be asynchronously updated in the background to reflect these changes. If + /// this timestamp's value is at least the Index.update_time of the original + /// Index, it means that this DeployedIndex and the original Index are in + /// sync. If this timestamp is older, then to see which updates this + /// DeployedIndex already contains (and which it does not), one must list the + /// operations that are running on the original Index. Only the successfully + /// completed Operations with update_time equal or before this sync time are + /// contained in this DeployedIndex. + final DateTime indexSyncTime; + + /// A description of resources that the DeployedIndex uses, which to large + /// degree are decided by Vertex AI, and optionally allows only a modest + /// additional configuration. + /// + /// If min_replica_count is not set, the default value is 2 (we don't provide + /// SLA when min_replica_count=1). If max_replica_count is not set, the + /// default value is min_replica_count. The max allowed replica count is 1000. + final VertexAIAutomaticResources automaticResources; + + /// A description of resources that are dedicated to the DeployedIndex, and + /// that need a higher degree of manual configuration. + /// + /// If min_replica_count is not set, the default value is 2 (we don't provide + /// SLA when min_replica_count=1). If max_replica_count is not set, the + /// default value is min_replica_count. The max allowed replica count is 1000. + /// Available machine types for SMALL shard: e2-standard-2 and all machine + /// types available for MEDIUM and LARGE shard. Available machine types for + /// MEDIUM shard: e2-standard-16 and all machine types available for LARGE + /// shard. Available machine types for LARGE shard: e2-highmem-16, + /// n2d-standard-32. n1-standard-16 and n1-standard-32 are still available, + /// but we recommend e2-standard-16 and e2-highmem-16 for cost efficiency. + final VertexAIDedicatedResources? dedicatedResources; + + /// If set, the authentication is enabled for the private endpoint. + final VertexAIDeployedIndexAuthConfig? deployedIndexAuthConfig; + + /// Provides paths for users to send requests directly to the deployed index + /// services running on Cloud via private services access. + /// + /// This field is populated if network is configured. + final VertexAIIndexPrivateEndpoints? privateEndpoints; + + /// A list of reserved ip ranges under the VPC network that can be used for + /// this DeployedIndex. + /// + /// If set, we will deploy the index within the provided ip ranges. Otherwise, + /// the index might be deployed to any ip ranges under the provided VPC + /// network. The value should be the name of the address + /// (https://cloud.google.com/compute/docs/reference/rest/v1/addresses) + /// Example: 'vertex-ai-ip-range'. + final List? reservedIpRanges; + + /// The deployment group can be no longer than 64 characters (eg: 'test', + /// 'prod'). + /// + /// If not set, we will use the 'default' deployment group. Creating + /// `deployment_groups` with `reserved_ip_ranges` is a recommended practice + /// when the peered network has multiple peering ranges. This creates your + /// deployments from predictable IP spaces for easier traffic administration. + /// Also, one deployment_group (except 'default') can only be used with the + /// same reserved_ip_ranges which means if the deployment_group has been used + /// with reserved_ip_ranges: \[a, b, c\], using it with \[a, b\] or \[d, e\] + /// is disallowed. Note: we only support up to 5 deployment groups(not + /// including 'default'). + final String deploymentGroup; + + /// If true, private endpoint's access logs are sent to Cloud Logging. + /// + /// These logs are like standard server access logs, containing information + /// like timestamp and latency for each MatchRequest. Note that logs may incur + /// a cost, especially if the deployed index receives a high queries per + /// second rate (QPS). Estimate your costs before enabling this option. + final bool enableAccessLogging; + + @override + bool operator ==(covariant final VertexAIDeployedIndex other) { + const listEquals = ListEquality(); + return identical(this, other) || + runtimeType == other.runtimeType && + id == other.id && + index == other.index && + displayName == other.displayName && + createTime == other.createTime && + indexSyncTime == other.indexSyncTime && + automaticResources == other.automaticResources && + dedicatedResources == other.dedicatedResources && + deployedIndexAuthConfig == other.deployedIndexAuthConfig && + privateEndpoints == other.privateEndpoints && + listEquals.equals(reservedIpRanges, other.reservedIpRanges) && + deploymentGroup == other.deploymentGroup && + enableAccessLogging == other.enableAccessLogging; + } + + @override + int get hashCode => + id.hashCode ^ + index.hashCode ^ + displayName.hashCode ^ + createTime.hashCode ^ + indexSyncTime.hashCode ^ + automaticResources.hashCode ^ + dedicatedResources.hashCode ^ + deployedIndexAuthConfig.hashCode ^ + privateEndpoints.hashCode ^ + const ListEquality().hash(reservedIpRanges) ^ + deploymentGroup.hashCode ^ + enableAccessLogging.hashCode; + + @override + String toString() { + return 'VertexAIDeployedIndex{' + 'id: $id, ' + 'index: $index, ' + 'displayName: $displayName, ' + 'createTime: $createTime, ' + 'indexSyncTime: $indexSyncTime, ' + 'automaticResources: $automaticResources, ' + 'dedicatedResources: $dedicatedResources, ' + 'deployedIndexAuthConfig: $deployedIndexAuthConfig, ' + 'privateEndpoints: $privateEndpoints, ' + 'reservedIpRanges: $reservedIpRanges, ' + 'deploymentGroup: $deploymentGroup, ' + 'enableAccessLogging: $enableAccessLogging}'; + } +} + +/// {@template vertex_ai_automatic_resources} +/// A description of resources that to large degree are decided by Vertex AI, +/// and require only a modest additional configuration. +/// +/// Each Model supporting these resources documents its specific guidelines. +/// {@endtemplate} +@immutable +class VertexAIAutomaticResources { + /// {@macro vertex_ai_automatic_resources} + const VertexAIAutomaticResources({ + required this.maxReplicaCount, + required this.minReplicaCount, + }); + + /// The maximum number of replicas this DeployedModel may be deployed on when + /// the traffic against it increases. + /// + /// If the requested value is too large, the deployment will error, but if + /// deployment succeeds then the ability to scale the model to that many + /// replicas is guaranteed (barring service outages). If traffic against the + /// DeployedModel increases beyond what its replicas at maximum may handle, a + /// portion of the traffic will be dropped. If this value is not provided, a + /// no upper bound for scaling under heavy traffic will be assume, though + /// Vertex AI may be unable to scale beyond certain replica number. + final int maxReplicaCount; + + /// The minimum number of replicas this DeployedModel will be always deployed + /// on. + /// + /// If traffic against it increases, it may dynamically be deployed onto more + /// replicas up to max_replica_count, and as traffic decreases, some of these + /// extra replicas may be freed. If the requested value is too large, the + /// deployment will error. + /// + /// Immutable. + final int minReplicaCount; + + @override + bool operator ==(covariant final VertexAIAutomaticResources other) { + return identical(this, other) || + runtimeType == other.runtimeType && + maxReplicaCount == other.maxReplicaCount && + minReplicaCount == other.minReplicaCount; + } + + @override + int get hashCode => maxReplicaCount.hashCode ^ minReplicaCount.hashCode; + + @override + String toString() { + return 'VertexAIAutomaticResources{' + 'maxReplicaCount: $maxReplicaCount, ' + 'minReplicaCount: $minReplicaCount}'; + } +} + +/// {@template vertex_ai_dedicated_resources} +/// A description of resources that are dedicated to a DeployedModel, and that +/// need a higher degree of manual configuration. +/// {@endtemplate} +@immutable +class VertexAIDedicatedResources { + /// {@macro vertex_ai_dedicated_resources} + const VertexAIDedicatedResources({ + required this.autoscalingMetricSpecs, + required this.machineSpec, + required this.minReplicaCount, + required this.maxReplicaCount, + }); + + /// The metric specifications that overrides a resource utilization metric + /// (CPU utilization, accelerator's duty cycle, and so on) target value + /// (default to 60 if not set). + /// + /// At most one entry is allowed per metric. If machine_spec.accelerator_count + /// is above 0, the autoscaling will be based on both CPU utilization and + /// accelerator's duty cycle metrics and scale up when either metrics exceeds + /// its target value while scale down if both metrics are under their target + /// value. The default target value is 60 for both metrics. If + /// machine_spec.accelerator_count is 0, the autoscaling will be based on CPU + /// utilization metric only with default target value 60 if not explicitly + /// set. For example, in the case of Online Prediction, if you want to + /// override target CPU utilization to 80, you should set + /// autoscaling_metric_specs.metric_name to + /// `aiplatform.googleapis.com/prediction/online/cpu/utilization` and + /// autoscaling_metric_specs.target to `80`. + final List autoscalingMetricSpecs; + + /// The specification of a single machine used by the prediction. + final VertexAIMachineSpec machineSpec; + + /// The maximum number of replicas this DeployedModel may be deployed on when + /// the traffic against it increases. + /// + /// If the requested value is too large, the deployment will error, but if + /// deployment succeeds then the ability to scale the model to that many + /// replicas is guaranteed (barring service outages). If traffic against the + /// DeployedModel increases beyond what its replicas at maximum may handle, a + /// portion of the traffic will be dropped. If this value is not provided, + /// will use min_replica_count as the default value. The value of this field + /// impacts the charge against Vertex CPU and GPU quotas. Specifically, you + /// will be charged for (max_replica_count * number of cores in the selected + /// machine type) and (max_replica_count * number of GPUs per replica in the + /// selected machine type). + final int maxReplicaCount; + + /// The minimum number of machine replicas this DeployedModel will be always + /// deployed on. + /// + /// This value must be greater than or equal to 1. If traffic against the + /// DeployedModel increases, it may dynamically be deployed onto more + /// replicas, and as traffic decreases, some of these extra replicas may be + /// freed. + final int minReplicaCount; + + @override + bool operator ==(covariant final VertexAIDedicatedResources other) { + const listEquals = ListEquality(); + return identical(this, other) || + runtimeType == other.runtimeType && + listEquals.equals( + autoscalingMetricSpecs, + other.autoscalingMetricSpecs, + ) && + machineSpec == other.machineSpec && + maxReplicaCount == other.maxReplicaCount && + minReplicaCount == other.minReplicaCount; + } + + @override + int get hashCode => + const ListEquality() + .hash(autoscalingMetricSpecs) ^ + machineSpec.hashCode ^ + maxReplicaCount.hashCode ^ + minReplicaCount.hashCode; + + @override + String toString() { + return 'VertexAIDedicatedResources{' + 'autoscalingMetricSpecs: $autoscalingMetricSpecs, ' + 'machineSpec: $machineSpec, ' + 'maxReplicaCount: $maxReplicaCount, ' + 'minReplicaCount: $minReplicaCount}'; + } +} + +/// {@template vertex_ai_autoscaling_metric_spec} +/// The metric specification that defines the target resource utilization (CPU +/// utilization, accelerator's duty cycle, and so on) for calculating the +/// desired replica count. +/// {@endtemplate} +@immutable +class VertexAIAutoscalingMetricSpec { + /// {@macro vertex_ai_autoscaling_metric_spec} + const VertexAIAutoscalingMetricSpec({ + required this.metricName, + this.target = 60, + }); + + /// The resource metric name. + /// + /// Supported metrics: * For Online Prediction: * + /// `aiplatform.googleapis.com/prediction/online/accelerator/duty_cycle` * + /// `aiplatform.googleapis.com/prediction/online/cpu/utilization` + final String metricName; + + /// The target resource utilization in percentage (1% - 100%) for the given + /// metric; once the real usage deviates from the target by a certain + /// percentage, the machine replicas change. + /// + /// The default value is 60 (representing 60%) if not provided. + final int target; + + @override + bool operator ==(covariant final VertexAIAutoscalingMetricSpec other) { + return identical(this, other) || + runtimeType == other.runtimeType && + metricName == other.metricName && + target == other.target; + } + + @override + int get hashCode => metricName.hashCode ^ target.hashCode; + + @override + String toString() { + return 'VertexAIAutoscalingMetricSpec{' + 'metricName: $metricName, ' + 'target: $target}'; + } +} + +/// {@template vertex_ai_machine_spec} +/// Specification of a single machine. +/// {@endtemplate} +@immutable +class VertexAIMachineSpec { + /// {@macro vertex_ai_machine_spec} + const VertexAIMachineSpec({ + required this.acceleratorCount, + required this.acceleratorType, + required this.machineType, + }); + + /// The number of accelerators to attach to the machine. + final int acceleratorCount; + + /// The type of accelerator(s) that may be attached to the machine as per + /// accelerator_count. + /// + /// Possible string values are: + /// - "ACCELERATOR_TYPE_UNSPECIFIED" : Unspecified accelerator type, which + /// means no accelerator. + /// - "NVIDIA_TESLA_K80" : Nvidia Tesla K80 GPU. + /// - "NVIDIA_TESLA_P100" : Nvidia Tesla P100 GPU. + /// - "NVIDIA_TESLA_V100" : Nvidia Tesla V100 GPU. + /// - "NVIDIA_TESLA_P4" : Nvidia Tesla P4 GPU. + /// - "NVIDIA_TESLA_T4" : Nvidia Tesla T4 GPU. + /// - "NVIDIA_TESLA_A100" : Nvidia Tesla A100 GPU. + /// - "NVIDIA_A100_80GB" : Nvidia A100 80GB GPU. + /// - "NVIDIA_L4" : Nvidia L4 GPU. + /// - "TPU_V2" : TPU v2. + /// - "TPU_V3" : TPU v3. + /// - "TPU_V4_POD" : TPU v4. + final String acceleratorType; + + /// The type of the machine. + /// + /// See the + /// [list of machine types supported for prediction](https://cloud.google.com/vertex-ai/docs/predictions/configure-compute#machine-types) + /// See the + /// [list of machine types supported for custom training](https://cloud.google.com/vertex-ai/docs/training/configure-compute#machine-types). + /// For DeployedModel this field is optional, and the default value is + /// `n1-standard-2`. For BatchPredictionJob or as part of WorkerPoolSpec this + /// field is required. + final String machineType; + + @override + bool operator ==(covariant final VertexAIMachineSpec other) { + return identical(this, other) || + runtimeType == other.runtimeType && + acceleratorCount == other.acceleratorCount && + acceleratorType == other.acceleratorType && + machineType == other.machineType; + } + + @override + int get hashCode => + acceleratorCount.hashCode ^ + acceleratorType.hashCode ^ + machineType.hashCode; + + @override + String toString() { + return 'VertexAIMachineSpec{' + 'acceleratorCount: $acceleratorCount, ' + 'acceleratorType: $acceleratorType, ' + 'machineType: $machineType}'; + } +} + +/// {@template vertex_ai_deployed_index_auth_config} +/// Used to set up the auth on the DeployedIndex's private endpoint. +/// {@endtemplate} +@immutable +class VertexAIDeployedIndexAuthConfig { + /// {@macro vertex_ai_deployed_index_auth_config} + const VertexAIDeployedIndexAuthConfig({ + required this.authProvider, + }); + + /// Defines the authentication provider that the DeployedIndex uses. + final VertexAIDeployedIndexAuthConfigAuthProvider authProvider; + + @override + bool operator ==(covariant final VertexAIDeployedIndexAuthConfig other) { + return identical(this, other) || + runtimeType == other.runtimeType && authProvider == other.authProvider; + } + + @override + int get hashCode => authProvider.hashCode; + + @override + String toString() { + return 'VertexAIDeployedIndexAuthConfig{' + 'authProvider: $authProvider}'; + } +} + +/// {@template vertex_ai_deployed_index_auth_config_auth_provider} +/// Configuration for an authentication provider, including support for \[JSON +/// Web Token +/// (JWT)\](https://tools.ietf.org/html/draft-ietf-oauth-json-web-token-32). +/// {@endtemplate} +@immutable +class VertexAIDeployedIndexAuthConfigAuthProvider { + /// {@macro vertex_ai_deployed_index_auth_config_auth_provider} + const VertexAIDeployedIndexAuthConfigAuthProvider({ + required this.allowedIssuers, + required this.audiences, + }); + + /// A list of allowed JWT issuers. + /// + /// Each entry must be a valid Google service account, in the following + /// format: `service-account-name@project-id.iam.gserviceaccount.com` + final List allowedIssuers; + + /// The list of JWT + /// [audiences](https://tools.ietf.org/html/draft-ietf-oauth-json-web-token-32#section-4.1.3). + /// that are allowed to access. A JWT containing any of these audiences will + /// be accepted. + final List audiences; + + @override + bool operator ==( + covariant final VertexAIDeployedIndexAuthConfigAuthProvider other, + ) { + const listEquals = ListEquality(); + return identical(this, other) || + runtimeType == other.runtimeType && + listEquals.equals(allowedIssuers, other.allowedIssuers) && + listEquals.equals(audiences, other.audiences); + } + + @override + int get hashCode => + const ListEquality().hash(allowedIssuers) ^ + const ListEquality().hash(audiences); + + @override + String toString() { + return 'VertexAIDeployedIndexAuthConfigAuthProvider{' + 'allowedIssuers: $allowedIssuers, ' + 'audiences: $audiences}'; + } +} + +/// {@template vertex_ai_index_private_endpoints} +/// IndexPrivateEndpoints proto is used to provide paths for users to send +/// requests via private endpoints (e.g. private service access, private service +/// connect). +/// +/// To send request via private service access, use match_grpc_address. To send +/// request via private service connect, use service_attachment. +/// {@endtemplate} +@immutable +class VertexAIIndexPrivateEndpoints { + /// {@macro vertex_ai_index_private_endpoints} + const VertexAIIndexPrivateEndpoints({ + required this.matchGrpcAddress, + required this.serviceAttachment, + }); + + /// The ip address used to send match gRPC requests. + final String matchGrpcAddress; + + /// The name of the service attachment resource. + /// + /// Populated if private service connect is enabled. + final String serviceAttachment; + + @override + bool operator ==(covariant final VertexAIIndexPrivateEndpoints other) { + return identical(this, other) || + runtimeType == other.runtimeType && + matchGrpcAddress == other.matchGrpcAddress && + serviceAttachment == other.serviceAttachment; + } + + @override + int get hashCode => matchGrpcAddress.hashCode ^ serviceAttachment.hashCode; + + @override + String toString() { + return 'VertexAIIndexPrivateEndpoints{' + 'matchGrpcAddress: $matchGrpcAddress, ' + 'serviceAttachment: $serviceAttachment}'; + } +} + +/// {@template vertex_ai_find_neighbors_request_query} +/// A query to find a number of the nearest neighbors (most similar vectors) of +/// a vector. +/// {@endtemplate} +@immutable +class VertexAIFindNeighborsRequestQuery { + const VertexAIFindNeighborsRequestQuery({ + required this.datapoint, + required this.neighborCount, + this.approximateNeighborCount, + this.fractionLeafNodesToSearchOverride, + this.perCrowdingAttributeNeighborCount, + }); + + /// The datapoint/vector whose nearest neighbors should be searched for. + final VertexAIIndexDatapoint datapoint; + + /// The number of nearest neighbors to be retrieved from database for each + /// query. + /// + /// If not set, will use the default from the service configuration + /// (https://cloud.google.com/vertex-ai/docs/matching-engine/configuring-indexes#nearest-neighbor-search-config). + final int? neighborCount; + + /// The number of neighbors to find via approximate search before exact + /// reordering is performed. + /// + /// If not set, the default value from scam config is used; if set, this value + /// must be \> 0. + final int? approximateNeighborCount; + + /// The fraction of the number of leaves to search, set at query time allows + /// user to tune search performance. + /// + /// This value increase result in both search accuracy and latency increase. + /// The value should be between 0.0 and 1.0. If not set or set to 0.0, query + /// uses the default value specified in + /// NearestNeighborSearchConfig.TreeAHConfig.fraction_leaf_nodes_to_search. + final double? fractionLeafNodesToSearchOverride; + + /// Crowding is a constraint on a neighbor list produced by nearest neighbor + /// search requiring that no more than some value k' of the k neighbors + /// returned have the same value of crowding_attribute. + /// + /// It's used for improving result diversity. This field is the maximum number + /// of matches with the same crowding tag. + final int? perCrowdingAttributeNeighborCount; + + @override + bool operator ==(covariant final VertexAIFindNeighborsRequestQuery other) { + return identical(this, other) || + runtimeType == other.runtimeType && + datapoint == other.datapoint && + neighborCount == other.neighborCount && + approximateNeighborCount == other.approximateNeighborCount && + fractionLeafNodesToSearchOverride == + other.fractionLeafNodesToSearchOverride && + perCrowdingAttributeNeighborCount == + other.perCrowdingAttributeNeighborCount; + } + + @override + int get hashCode => + datapoint.hashCode ^ + neighborCount.hashCode ^ + approximateNeighborCount.hashCode ^ + fractionLeafNodesToSearchOverride.hashCode ^ + perCrowdingAttributeNeighborCount.hashCode; + + @override + String toString() { + return 'VertexAIFindNeighborsRequestQuery{' + 'datapoint: $datapoint, ' + 'neighborCount: $neighborCount, ' + 'approximateNeighborCount: $approximateNeighborCount, ' + 'fractionLeafNodesToSearchOverride: $fractionLeafNodesToSearchOverride, ' + 'perCrowdingAttributeNeighborCount: $perCrowdingAttributeNeighborCount}'; + } +} + +/// {@template vertex_ai_find_neighbors_response} +/// The response message for MatchService.FindNeighbors. +/// {@endtemplate} +@immutable +class VertexAIFindNeighborsResponse { + /// {@macro vertex_ai_find_neighbors_response} + const VertexAIFindNeighborsResponse({ + required this.nearestNeighbors, + }); + + /// The nearest neighbors of the query datapoints. + final List nearestNeighbors; + + @override + bool operator ==(covariant final VertexAIFindNeighborsResponse other) { + const listEquals = + ListEquality(); + return identical(this, other) || + runtimeType == other.runtimeType && + listEquals.equals(nearestNeighbors, other.nearestNeighbors); + } + + @override + int get hashCode => + const ListEquality() + .hash(nearestNeighbors); + + @override + String toString() { + return 'VertexAIFindNeighborsResponse{' + 'nearestNeighbors: $nearestNeighbors}'; + } +} + +/// {@template vertex_ai_find_neighbors_response_nearest_neighbors} +/// Nearest neighbors for one query. +/// {@endtemplate} +@immutable +class VertexAIFindNeighborsResponseNearestNeighbors { + /// {@macro vertex_ai_find_neighbors_response_nearest_neighbors} + const VertexAIFindNeighborsResponseNearestNeighbors({ + required this.id, + required this.neighbors, + }); + + /// The ID of the query datapoint. + final String id; + + /// All its neighbors. + final List neighbors; + + @override + bool operator ==( + covariant final VertexAIFindNeighborsResponseNearestNeighbors other, + ) { + const listEquals = ListEquality(); + return identical(this, other) || + runtimeType == other.runtimeType && + id == other.id && + listEquals.equals(neighbors, other.neighbors); + } + + @override + int get hashCode => + id.hashCode ^ + const ListEquality() + .hash(neighbors); + + @override + String toString() { + return 'VertexAIFindNeighborsResponseNearestNeighbors{' + 'id: $id, ' + 'neighbors: $neighbors}'; + } +} + +/// {@template vertex_ai_find_neighbors_response_neighbor} +/// A neighbor of the query vector. +/// {@endtemplate} +@immutable +class VertexAIFindNeighborsResponseNeighbor { + /// {@macro vertex_ai_find_neighbors_response_neighbor} + const VertexAIFindNeighborsResponseNeighbor({ + required this.datapoint, + required this.distance, + }); + + /// The datapoint of the neighbor. + /// + /// Note that full datapoints are returned only when "return_full_datapoint" + /// is set to true. Otherwise, only the "datapoint_id" and "crowding_tag" + /// fields are populated. + final VertexAIIndexDatapoint datapoint; + + /// The distance between the neighbor and the query vector. + final double distance; + + @override + bool operator ==( + covariant final VertexAIFindNeighborsResponseNeighbor other, + ) { + return identical(this, other) || + runtimeType == other.runtimeType && + datapoint == other.datapoint && + distance == other.distance; + } + + @override + int get hashCode => datapoint.hashCode ^ distance.hashCode; + + @override + String toString() { + return 'VertexAIFindNeighborsResponseNeighbor{' + 'datapoint: $datapoint, ' + 'distance: $distance}'; + } +} diff --git a/packages/vertex_ai/lib/src/matching_engine/models/indexes.dart b/packages/vertex_ai/lib/src/matching_engine/models/indexes.dart new file mode 100644 index 00000000..5e18b0e3 --- /dev/null +++ b/packages/vertex_ai/lib/src/matching_engine/models/indexes.dart @@ -0,0 +1,760 @@ +// ignore_for_file: avoid_unused_constructor_parameters +import 'package:collection/collection.dart'; +import 'package:meta/meta.dart'; + +/// {@template vertex_ai_index} +/// A Vertex AI Index. +/// {@endtemplate} +@immutable +class VertexAIIndex { + /// {@macro vertex_ai_index} + const VertexAIIndex({ + required this.name, + required this.displayName, + required this.description, + required this.metadataSchemaUri, + required this.metadata, + required this.indexUpdateMethod, + required this.indexStats, + required this.labels, + required this.etag, + required this.createTime, + required this.updateTime, + }); + + /// The index ID. + String get id => name.split('/').last; + + /// The resource name of the Index. + final String name; + + /// The display name of the Index. + /// + /// The name can be up to 128 characters long and can consist of any UTF-8 + /// characters. + final String displayName; + + /// The description of the Index. + final String description; + + /// Points to a YAML file stored on Google Cloud Storage describing + /// additional information about the Index, that is specific to it. + final String metadataSchemaUri; + + /// An additional information about the Index. + final VertexAIIndexMetadata metadata; + + /// The update method to use with this Index. + final VertexAIIndexUpdateMethod indexUpdateMethod; + + /// The statistics of the Index. + final VertexAIIndexStats? indexStats; + + /// The labels with user-defined metadata to organize your Indexes. + /// + /// Label keys and values can be no longer than 64 characters (Unicode + /// codepoints), can only contain lowercase letters, numeric characters, + /// underscores and dashes. International characters are allowed. See + /// https://goo.gl/xmQnxf for more information and examples of labels. + final Map? labels; + + /// Used to perform consistent read-modify-write updates. + /// + /// If not set, a blind "overwrite" update happens. + final String? etag; + + /// Timestamp when this Index was created. + final DateTime createTime; + + /// Timestamp when this Index was most recently updated. + /// + /// This also includes any update to the contents of the Index. Note that + /// Operations working on this Index may have their + /// Operations.metadata.generic_metadata.update_time a little after the + /// value of this timestamp, yet that does not mean their results are not + /// already reflected in the Index. Result of any successfully completed + /// Operation on the Index is reflected in it. + final DateTime updateTime; + + @override + bool operator ==(covariant final VertexAIIndex other) { + final mapEquals = const MapEquality().equals; + return identical(this, other) || + runtimeType == other.runtimeType && + name == other.name && + displayName == other.displayName && + description == other.description && + metadataSchemaUri == other.metadataSchemaUri && + metadata == other.metadata && + indexUpdateMethod == other.indexUpdateMethod && + indexStats == other.indexStats && + mapEquals(labels, other.labels) && + etag == other.etag && + createTime == other.createTime && + updateTime == other.updateTime; + } + + @override + int get hashCode => + name.hashCode ^ + displayName.hashCode ^ + description.hashCode ^ + metadataSchemaUri.hashCode ^ + metadata.hashCode ^ + indexUpdateMethod.hashCode ^ + indexStats.hashCode ^ + const MapEquality().hash(labels) ^ + etag.hashCode ^ + createTime.hashCode ^ + updateTime.hashCode; + + @override + String toString() { + return 'VertexAIIndex{' + 'name: $name, ' + 'displayName: $displayName, ' + 'description: $description, ' + 'metadataSchemaUri: $metadataSchemaUri, ' + 'metadata: $metadata, ' + 'indexUpdateMethod: $indexUpdateMethod, ' + 'indexStats: $indexStats, ' + 'labels: $labels, ' + 'etag: $etag, ' + 'createTime: $createTime, ' + 'updateTime: $updateTime}'; + } +} + +/// {@template vertex_ai_index_creation_metadata} +/// Metadata required to create a Vertex AI Index. +/// {@endtemplate} +@immutable +class VertexAIIndexCreationMetadata { + /// {@macro vertex_ai_index_creation_metadata} + const VertexAIIndexCreationMetadata({ + required this.contentsDeltaUri, + this.isCompleteOverwrite = false, + required this.config, + }); + + /// Allows inserting, updating or deleting the contents of the Matching + /// Engine Index. The string must be a valid Cloud Storage directory path, + /// such as `gs://BUCKET_NAME/PATH_TO_INDEX_DIR/`. + /// + /// If you set this field when updating an index, then no other field can be + /// also updated as part of the same call. + final String contentsDeltaUri; + + /// If this field is set together with contentsDeltaUri when updating an + /// index, then existing content of the index will be replaced by the data + /// from the [contentsDeltaUri]. + final bool isCompleteOverwrite; + + /// The configuration of the Matching Engine Index. + final VertexAINearestNeighborSearchConfig config; + + /// Converts a [VertexAIIndexCreationMetadata] to a [Map]. + Map toMap() { + return { + 'contentsDeltaUri': contentsDeltaUri, + 'isCompleteOverwrite': isCompleteOverwrite, + 'config': config.toMap(), + }; + } + + @override + bool operator ==(covariant final VertexAIIndexCreationMetadata other) => + identical(this, other) || + runtimeType == other.runtimeType && + contentsDeltaUri == other.contentsDeltaUri && + isCompleteOverwrite == other.isCompleteOverwrite && + config == other.config; + + @override + int get hashCode => + contentsDeltaUri.hashCode ^ + isCompleteOverwrite.hashCode ^ + config.hashCode; + + @override + String toString() { + return 'VertexAIIndexCreationMetadata{' + 'contentsDeltaUri: $contentsDeltaUri, ' + 'isCompleteOverwrite: $isCompleteOverwrite, ' + 'config: $config}'; + } +} + +/// {@template vertex_ai_index_metadata} +/// A Vertex AI Nearest Neighbor Search Index Metadata. +/// {@endtemplate} +@immutable +class VertexAIIndexMetadata { + /// {@macro vertex_ai_index_metadata} + const VertexAIIndexMetadata({ + required this.config, + }); + + /// The configuration of the Matching Engine Index. + final VertexAINearestNeighborSearchConfig config; + + /// Factory method to create a [VertexAIIndexMetadata]. + factory VertexAIIndexMetadata.fromMap( + final Map map, + ) { + return VertexAIIndexMetadata( + config: VertexAINearestNeighborSearchConfig.fromMap( + map['config'] as Map? ?? const {}, + ), + ); + } + + @override + bool operator ==(covariant final VertexAIIndexMetadata other) => + identical(this, other) || + runtimeType == other.runtimeType && config == other.config; + + @override + int get hashCode => config.hashCode; + + @override + String toString() { + return 'VertexAIIndexMetadata{config: $config}'; + } +} + +/// {@template vertex_ai_nearest_neighbor_search_config} +/// The configuration of the Matching Engine Index. +/// {@endtemplate} +@immutable +class VertexAINearestNeighborSearchConfig { + /// {@macro vertex_ai_nearest_neighbor_search_config} + const VertexAINearestNeighborSearchConfig({ + required this.dimensions, + this.approximateNeighborsCount = 150, + this.distanceMeasureType = VertexAIDistanceMeasureType.dotProductDistance, + this.featureNormType = VertexAIFeatureNormType.none, + required this.algorithmConfig, + this.shardSize = VertexAIShardSize.medium, + }); + + /// The number of dimensions of the input vectors. + final int dimensions; + + /// Required if tree-AH algorithm is used. + /// + /// The default number of neighbors to find through approximate search before + /// exact reordering is performed. Exact reordering is a procedure where + /// results returned by an approximate search algorithm are reordered via a + /// more expensive distance computation. + final int approximateNeighborsCount; + + /// The distance measure used in nearest neighbor search. + final VertexAIDistanceMeasureType distanceMeasureType; + + /// Type of normalization to be carried out on each vector. + final VertexAIFeatureNormType featureNormType; + + /// The configuration for the algorithms that Matching Engine uses for + /// efficient search. + /// + /// - [VertexAITreeAhAlgorithmConfig] Configuration options for using the + /// tree-AH algorithm. Refer to this blog for more details: + /// https://cloud.google.com/blog/products/ai-machine-learning/scaling-deep-retrieval-tensorflow-two-towers-architecture + /// - [VertexAIBruteForceAlgorithmConfig] This option implements the standard + /// linear search in the database for each query. There are no fields to + /// configure for a brute force search. + final VertexAIAlgorithmConfig algorithmConfig; + + /// Machine type to use when you deploy your index. + final VertexAIShardSize shardSize; + + /// Factory method to create a [VertexAINearestNeighborSearchConfig]. + factory VertexAINearestNeighborSearchConfig.fromMap( + final Map map, + ) { + return VertexAINearestNeighborSearchConfig( + dimensions: map['dimensions'] as int, + approximateNeighborsCount: map['approximateNeighborsCount'] as int, + distanceMeasureType: VertexAIDistanceMeasureType.values.firstWhere( + (final t) => t.id == map['distanceMeasureType'] as String, + ), + featureNormType: VertexAIFeatureNormType.values.firstWhere( + (final t) => t.id == map['featureNormType'] as String, + ), + algorithmConfig: VertexAIAlgorithmConfig.fromMap( + map['algorithmConfig'] as Map? ?? const {}, + ), + shardSize: VertexAIShardSize.values.firstWhere( + (final t) => t.id == map['shardSize'] as String, + ), + ); + } + + /// Converts a [VertexAINearestNeighborSearchConfig] to a [Map]. + Map toMap() { + return { + 'dimensions': dimensions, + 'approximateNeighborsCount': approximateNeighborsCount, + 'distanceMeasureType': distanceMeasureType.id, + 'featureNormType': featureNormType.id, + 'algorithmConfig': algorithmConfig.toMap(), + 'shardSize': shardSize.id, + }; + } + + @override + bool operator ==(covariant final VertexAINearestNeighborSearchConfig other) => + identical(this, other) || + runtimeType == other.runtimeType && + dimensions == other.dimensions && + approximateNeighborsCount == other.approximateNeighborsCount && + distanceMeasureType == other.distanceMeasureType && + featureNormType == other.featureNormType && + algorithmConfig == other.algorithmConfig && + shardSize == other.shardSize; + + @override + int get hashCode => + dimensions.hashCode ^ + approximateNeighborsCount.hashCode ^ + distanceMeasureType.hashCode ^ + featureNormType.hashCode ^ + algorithmConfig.hashCode ^ + shardSize.hashCode; + + @override + String toString() { + return 'VertexAINearestNeighborSearchConfig{' + 'dimensions: $dimensions, ' + 'approximateNeighborsCount: $approximateNeighborsCount, ' + 'distanceMeasureType: $distanceMeasureType, ' + 'featureNormType: $featureNormType, ' + 'algorithmConfig: $algorithmConfig, ' + 'shardSize: $shardSize}'; + } +} + +/// {@template vertex_ai_distance_measure_type} +/// The distance measure used in nearest neighbor search. +/// {@endtemplate} +enum VertexAIDistanceMeasureType { + /// Euclidean (L2) Distance + squaredL2Distance('SQUARED_L2_DISTANCE'), + + /// Manhattan (L1) Distance + l1Distance('L1_DISTANCE'), + + /// Defined as a negative of the dot product (recommended) + dotProductDistance('DOT_PRODUCT_DISTANCE'), + + /// Cosine Distance. + cosineDistance('COSINE_DISTANCE'); + + /// {@macro vertex_ai_distance_measure_type} + const VertexAIDistanceMeasureType(this.id); + + /// The id of the distance measure type. + final String id; + + @override + String toString() => id; +} + +/// {@template vertex_ai_feature_norm_type} +/// Type of normalization to be carried out on each vector. +/// {@endtemplate} +enum VertexAIFeatureNormType { + /// Unit L2 normalization type. + unitL2Norm('UNIT_L2_NORM'), + + /// No normalization type is specified. + none('NONE'); + + /// {@macro vertex_ai_feature_norm_type} + const VertexAIFeatureNormType(this.id); + + /// The id of the normalization type. + final String id; + + @override + String toString() => id; +} + +/// {@template vertex_ai_shard_size} +/// Index data is split into equal parts to be processed. These are called +/// "shards". When you create an index you must specify the shard size. +/// Once you create the index, you can determine what machine type to use +/// when you deploy your index. +/// {@endtemplate} +enum VertexAIShardSize { + /// e2-standard-2 (2GB) + small('SHARD_SIZE_SMALL'), + + /// e2-standard-16 (20GB) + medium('SHARD_SIZE_MEDIUM'), + + /// e2-highmem-16 (50GB) + large('SHARD_SIZE_LARGE'); + + /// {@macro vertex_ai_feature_norm_type} + const VertexAIShardSize(this.id); + + /// The id of the shard size. + final String id; + + @override + String toString() => id; +} + +/// {@template vertex_ai_algorithm_config} +/// The configuration with regard to the algorithms used for efficient search. +/// {@endtemplate} +@immutable +sealed class VertexAIAlgorithmConfig { + /// {@macro vertex_ai_algorithm_config} + const VertexAIAlgorithmConfig({required this.type}); + + /// The type of algorithm used for efficient search. + final String type; + + /// Factory constructor for [VertexAIAlgorithmConfig]. + factory VertexAIAlgorithmConfig.fromMap(final Map map) { + final type = map.keys.first; + final config = map[type] as Map? ?? const {}; + switch (type) { + case 'treeAhConfig': + return VertexAITreeAhAlgorithmConfig.fromMap(config); + case 'bruteForceConfig': + return VertexAIBruteForceAlgorithmConfig.fromMap(config); + default: + throw ArgumentError.value( + type, + 'type', + 'Invalid algorithm type: $type', + ); + } + } + + /// Converts the [VertexAIAlgorithmConfig] to a [Map]. + Map toMap() { + return { + type: toConfigMap(), + }; + } + + /// Converts the actual config to a [Map]. + Map toConfigMap(); +} + +/// {@template vertex_ai_tree_ah_config} +/// Tree-AH efficient search algorithm. +/// {@endtemplate} +@immutable +class VertexAITreeAhAlgorithmConfig extends VertexAIAlgorithmConfig { + /// {@macro vertex_ai_tree_ah_config} + const VertexAITreeAhAlgorithmConfig({ + this.fractionLeafNodesToSearch = 0.05, + this.leafNodeEmbeddingCount = 1000, + this.leafNodesToSearchPercent = 10, + }) : super(type: 'treeAhConfig'); + + /// The default fraction of leaf nodes that any query may be searched. + /// + /// Must be in range (0.0, 1.0). + final double fractionLeafNodesToSearch; + + /// Number of embeddings on each leaf node. + final int leafNodeEmbeddingCount; + + /// The default percentage of leaf nodes that any query may be searched. + /// + /// Must be in the range [1, 100]. + final int leafNodesToSearchPercent; + + /// Factory constructor for creating a new [VertexAITreeAhAlgorithmConfig] + factory VertexAITreeAhAlgorithmConfig.fromMap( + final Map map, + ) { + return VertexAITreeAhAlgorithmConfig( + fractionLeafNodesToSearch: map['fractionLeafNodesToSearch'] as double, + leafNodeEmbeddingCount: int.parse( + // the API returns a string instead of an int + map['leafNodeEmbeddingCount'].toString(), + ), + leafNodesToSearchPercent: map['leafNodesToSearchPercent'] as int, + ); + } + + @override + Map toConfigMap() { + return { + 'fractionLeafNodesToSearch': fractionLeafNodesToSearch, + 'leafNodeEmbeddingCount': leafNodeEmbeddingCount, + 'leafNodesToSearchPercent': leafNodesToSearchPercent, + }; + } + + @override + bool operator ==(covariant final VertexAITreeAhAlgorithmConfig other) => + identical(this, other) || + runtimeType == other.runtimeType && + fractionLeafNodesToSearch == other.fractionLeafNodesToSearch && + leafNodeEmbeddingCount == other.leafNodeEmbeddingCount && + leafNodesToSearchPercent == other.leafNodesToSearchPercent; + + @override + int get hashCode => + fractionLeafNodesToSearch.hashCode ^ + leafNodeEmbeddingCount.hashCode ^ + leafNodesToSearchPercent.hashCode; + + @override + String toString() { + return 'VertexAITreeAhAlgorithmConfig{' + 'fractionLeafNodesToSearch: $fractionLeafNodesToSearch, ' + 'leafNodeEmbeddingCount: $leafNodeEmbeddingCount, ' + 'leafNodesToSearchPercent: $leafNodesToSearchPercent}'; + } +} + +/// {@template vertex_ai_brute_force_config} +/// Configuration options for using brute force search, which simply implements +/// the standard linear search in the database for each query. +/// {@endtemplate} +@immutable +class VertexAIBruteForceAlgorithmConfig extends VertexAIAlgorithmConfig { + /// {@macro vertex_ai_brute_force_config} + const VertexAIBruteForceAlgorithmConfig() : super(type: 'bruteForceConfig'); + + /// Factory constructor for creating a new [VertexAIBruteForceAlgorithmConfig] + factory VertexAIBruteForceAlgorithmConfig.fromMap( + final Map map, + ) { + return const VertexAIBruteForceAlgorithmConfig(); + } + + @override + Map toConfigMap() { + return const {}; + } + + @override + bool operator ==(covariant final VertexAIBruteForceAlgorithmConfig other) => + identical(this, other) || runtimeType == other.runtimeType; + + @override + int get hashCode => 0; + + @override + String toString() { + return 'VertexAIBruteForceAlgorithmConfig{}'; + } +} + +/// {@template vertex_ai_index_update_method} +/// The update method to use with this Index. +/// {@endtemplate} +enum VertexAIIndexUpdateMethod { + /// The user can update the index with files on Cloud Storage of datapoints + /// to update. + batchUpdate('BATCH_UPDATE'), + + /// The user can call UpsertDatapoints/DeleteDatapoints to update the Index + /// and the updates will be applied in corresponding DeployedIndexes in + /// nearly real-time. + streamUpdate('STREAM_UPDATE'); + + /// {@macro vertex_ai_index_update_method} + const VertexAIIndexUpdateMethod(this.id); + + /// The id of the update method. + final String id; + + @override + String toString() => id; +} + +/// {@template vertex_ai_index_stats} +/// The statistics of a Vertex AI Index. +/// {@endtemplate} +@immutable +class VertexAIIndexStats { + /// {@macro vertex_ai_index_stats} + const VertexAIIndexStats({ + required this.shardsCount, + required this.vectorsCount, + }); + + /// The number of shards in the index. + final int shardsCount; + + /// The number of vectors in the index. + final int vectorsCount; + + @override + bool operator ==(covariant final VertexAIIndexStats other) => + identical(this, other) || + runtimeType == other.runtimeType && + shardsCount == other.shardsCount && + vectorsCount == other.vectorsCount; + + @override + int get hashCode => shardsCount.hashCode ^ vectorsCount.hashCode; + + @override + String toString() { + return 'VertexAIIndexStats{' + 'shardsCount: $shardsCount, ' + 'vectorsCount: $vectorsCount}'; + } +} + +/// {@template vertex_ai_index_datapoint} +/// A datapoint of Index. +/// {@endtemplate} +@immutable +class VertexAIIndexDatapoint { + /// {@macro vertex_ai_index_datapoint} + const VertexAIIndexDatapoint({ + required this.datapointId, + required this.featureVector, + this.crowdingTag, + this.restricts, + }); + + /// Unique identifier of the datapoint. + final String datapointId; + + /// Feature embedding vector. + final List featureVector; + + /// CrowdingTag of the datapoint, the number of neighbors to return in each + /// crowding can be configured during query. + final VertexAIIndexDatapointCrowdingTag? crowdingTag; + + /// List of Restrict of the datapoint, used to perform "restricted searches" + /// where boolean rule are used to filter the subset of the database eligible + /// for matching. + /// + /// See: https://cloud.google.com/vertex-ai/docs/matching-engine/filtering + final List? restricts; + + @override + bool operator ==(covariant final VertexAIIndexDatapoint other) { + final listEquals = const ListEquality().equals; + return identical(this, other) || + runtimeType == other.runtimeType && + datapointId == other.datapointId && + listEquals(featureVector, other.featureVector) && + crowdingTag == other.crowdingTag && + const ListEquality() + .equals(restricts, other.restricts); + } + + @override + int get hashCode => + datapointId.hashCode ^ + const ListEquality().hash(featureVector) ^ + crowdingTag.hashCode ^ + const ListEquality().hash(restricts); + + @override + String toString() { + return 'VertexAIIndexDatapoint{' + 'datapointId: $datapointId, ' + 'featureVector: $featureVector, ' + 'crowdingTag: $crowdingTag, ' + 'restricts: $restricts}'; + } +} + +/// {@template vertex_ai_index_datapoint_crowding_tag} +/// Crowding tag is a constraint on a neighbor list produced by nearest neighbor +/// search requiring that no more than some value k' of the k neighbors returned +/// have the same value of crowding_attribute. +/// {@endtemplate} +@immutable +class VertexAIIndexDatapointCrowdingTag { + /// {@macro vertex_ai_index_datapoint_crowding_tag} + const VertexAIIndexDatapointCrowdingTag({ + required this.crowdingAttribute, + }); + + /// The attribute value used for crowding. + /// + /// The maximum number of neighbors to return per crowding attribute value + /// (per_crowding_attribute_num_neighbors) is configured per-query. This field + /// is ignored if per_crowding_attribute_num_neighbors is larger than the + /// total number of neighbors to return for a given query. + final String crowdingAttribute; + + @override + bool operator ==(covariant final VertexAIIndexDatapointCrowdingTag other) { + return identical(this, other) || + runtimeType == other.runtimeType && + crowdingAttribute == other.crowdingAttribute; + } + + @override + int get hashCode => crowdingAttribute.hashCode; + + @override + String toString() { + return 'VertexAIIndexDatapointCrowdingTag{' + 'crowdingAttribute: $crowdingAttribute}'; + } +} + +/// {@template vertex_ai_index_datapoint_restriction} +/// Restriction of a datapoint which describe its attributes(tokens) from each +/// of several attribute categories(namespaces). +/// {@endtemplate} +@immutable +class VertexAIIndexDatapointRestriction { + /// {@macro vertex_ai_index_datapoint_restriction} + const VertexAIIndexDatapointRestriction({ + required this.namespace, + required this.allowList, + required this.denyList, + }); + + /// The namespace of this restriction. + /// + /// eg: color. + final String namespace; + + /// The attributes to allow in this namespace. + /// + /// eg: 'red' + final List allowList; + + /// The attributes to deny in this namespace. + /// + /// eg: 'blue' + final List denyList; + + @override + bool operator ==(covariant final VertexAIIndexDatapointRestriction other) { + final listEquals = const ListEquality().equals; + return identical(this, other) || + runtimeType == other.runtimeType && + namespace == other.namespace && + listEquals(allowList, other.allowList) && + listEquals(denyList, other.denyList); + } + + @override + int get hashCode => + namespace.hashCode ^ + const ListEquality().hash(allowList) ^ + const ListEquality().hash(denyList); + + @override + String toString() { + return 'VertexAIIndexDatapointRestriction{' + 'namespace: $namespace, ' + 'allowList: $allowList, ' + 'denyList: $denyList}'; + } +} diff --git a/packages/vertex_ai/lib/src/matching_engine/models/models.dart b/packages/vertex_ai/lib/src/matching_engine/models/models.dart new file mode 100644 index 00000000..a5fa1920 --- /dev/null +++ b/packages/vertex_ai/lib/src/matching_engine/models/models.dart @@ -0,0 +1,3 @@ +export 'index_endpoints.dart'; +export 'indexes.dart'; +export 'operation.dart'; diff --git a/packages/vertex_ai/lib/src/matching_engine/models/operation.dart b/packages/vertex_ai/lib/src/matching_engine/models/operation.dart new file mode 100644 index 00000000..8c6596f4 --- /dev/null +++ b/packages/vertex_ai/lib/src/matching_engine/models/operation.dart @@ -0,0 +1,122 @@ +import 'package:collection/collection.dart'; +import 'package:meta/meta.dart'; + +/// {@template vertex_ai_operation} +/// A long-running operation that is the result of a network API call. +/// {@endtemplate} +@immutable +class VertexAIOperation { + /// {@macro vertex_ai_operation} + const VertexAIOperation({ + required this.name, + required this.done, + this.response, + this.error, + this.metadata, + }); + + /// The server-assigned name, which is only unique within the same service + /// that originally returns it. + /// + /// If you use the default HTTP mapping, the `name` should be a resource name + /// ending with `operations/{unique_id}`. + final String name; + + /// If the value is `false`, it means the operation is still in progress. + /// + /// If `true`, the operation is completed, and either `error` or `response` is + /// available. + final bool done; + + /// The normal response of the operation in case of success. + final Map? response; + + /// The error result of the operation in case of failure or cancellation. + final VertexAIOperationError? error; + + /// Service-specific metadata associated with the operation. + /// + /// It typically contains progress information and common metadata such as + /// create time. Some services might not provide such metadata. Any method + /// that returns a long-running operation should document the metadata type, + /// if any. + final Map? metadata; + + @override + bool operator ==(covariant final VertexAIOperation other) { + final mapEquals = const MapEquality().equals; + return identical(this, other) || + runtimeType == other.runtimeType && + name == other.name && + done == other.done && + mapEquals(response, other.response) && + error == other.error && + mapEquals(metadata, other.metadata); + } + + @override + int get hashCode => + name.hashCode ^ + done.hashCode ^ + const MapEquality().hash(response) ^ + error.hashCode ^ + const MapEquality().hash(metadata); + + @override + String toString() { + return 'VertexAIOperation{' + 'name: $name, ' + 'done: $done, ' + 'response: $response, ' + 'error: $error, ' + 'metadata: $metadata}'; + } +} + +/// {@template vertex_ai_operation_error} +/// The error result of the operation in case of failure. +/// {@endtemplate} +@immutable +class VertexAIOperationError { + /// {@macro vertex_ai_operation_error} + const VertexAIOperationError({ + this.code, + this.details, + this.message, + }); + + /// The status code + final int? code; + + /// A list of messages that carry the error details. + /// + /// There is a common set of message types for APIs to use. + final List>? details; + + /// A developer-facing error message, which should be in English. + final String? message; + + @override + bool operator ==(covariant final VertexAIOperationError other) { + final deepEquals = const DeepCollectionEquality().equals; + return identical(this, other) || + runtimeType == other.runtimeType && + code == other.code && + deepEquals(details, other.details) && + message == other.message; + } + + @override + int get hashCode => + code.hashCode ^ + const DeepCollectionEquality().hash(details) ^ + message.hashCode; + + @override + String toString() { + return 'VertexAIOperationError{' + 'code: $code, ' + 'details: $details, ' + 'message: $message}'; + } +} diff --git a/packages/vertex_ai/lib/vertex_ai.dart b/packages/vertex_ai/lib/vertex_ai.dart index 3233dd25..33315f78 100644 --- a/packages/vertex_ai/lib/vertex_ai.dart +++ b/packages/vertex_ai/lib/vertex_ai.dart @@ -2,3 +2,4 @@ library; export 'src/gen_ai/gen_ai.dart'; +export 'src/matching_engine/machine_engine.dart'; diff --git a/packages/vertex_ai/test/matching_engine/maching_engine_client_test.dart b/packages/vertex_ai/test/matching_engine/maching_engine_client_test.dart new file mode 100644 index 00000000..c483da4c --- /dev/null +++ b/packages/vertex_ai/test/matching_engine/maching_engine_client_test.dart @@ -0,0 +1,938 @@ +// ignore_for_file: avoid_print +@TestOn('vm') +library; // Uses dart:io + +import 'dart:convert'; +import 'dart:io'; + +import 'package:googleapis_auth/auth_io.dart'; +import 'package:test/test.dart'; +import 'package:vertex_ai/vertex_ai.dart'; + +void main() async { + final authClient = await _getAuthenticatedClient(); + final marchingEngine = VertexAIMatchingEngineClient( + authHttpClient: authClient, + project: Platform.environment['VERTEX_AI_PROJECT_ID']!, + location: 'europe-west1', + ); + + group('VertexAIMatchingEngineClient management tests', skip: true, () { + test('Test create index', () async { + // 1. Create index + final res = await marchingEngine.indexes.create( + displayName: 'test-index', + description: 'This is a test index', + metadata: const VertexAIIndexCreationMetadata( + contentsDeltaUri: 'gs://brxs-vertex-ai/index', + config: VertexAINearestNeighborSearchConfig( + dimensions: 768, + algorithmConfig: VertexAITreeAhAlgorithmConfig(), + ), + ), + ); + // 2. Poll for operation completion (takes around 30min) + VertexAIOperation operation = res; + while (!operation.done) { + print('Index creation operation not done yet...'); + await Future.delayed(const Duration(seconds: 10)); + operation = await marchingEngine.indexes.operations.get( + name: operation.name, + ); + } + expect(operation.error, isNull); + }); + + test('Test create index endpoint', () async { + // 1. Create index + final res = await marchingEngine.indexEndpoints.create( + displayName: 'test-index-endpoint', + description: 'This is a test index endpoint', + publicEndpointEnabled: true, + ); + // 2. Poll for operation completion (takes around 10s) + VertexAIOperation operation = res; + while (!operation.done) { + print('Index endpoint creation operation not done yet...'); + await Future.delayed(const Duration(seconds: 10)); + operation = await marchingEngine.indexEndpoints.operations.get( + name: operation.name, + ); + } + expect(operation.error, isNull); + }); + + test('Test deploy index to endpoint', () async { + // 1. Create index + final res = await marchingEngine.indexEndpoints.deployIndex( + indexId: '5086059315115065344', + indexEndpointId: '8572232454792806400', + deployedIndexId: 'deployment1', + deployedIndexDisplayName: 'test-deployed-index', + ); + // 2. Poll for operation completion (takes around 30min) + VertexAIOperation operation = res; + while (!operation.done) { + print('Index deployment operation not done yet...'); + await Future.delayed(const Duration(seconds: 10)); + operation = await marchingEngine.indexEndpoints.operations.get( + name: operation.name, + ); + } + expect(operation.error, isNull); + }); + + test('Test list indexes', () async { + final res = await marchingEngine.indexes.list(); + expect(res, isNotNull); + }); + + test('Test get index', () async { + final res = await marchingEngine.indexes.get(id: '5209908304867753984'); + expect(res, isNotNull); + }); + + test('Test delete index', () async { + final res = + await marchingEngine.indexes.delete(id: '5209908304867753984'); + expect(res, isNotNull); + }); + + test('Test list index operations', () async { + final res = await marchingEngine.indexes.operations + .list(indexId: '5209908304867753984'); + expect(res, isNotNull); + }); + + test('Test list index endpoints', () async { + final res = await marchingEngine.indexEndpoints.list(); + expect(res, isNotNull); + }); + + test('Test get index endpoint', () async { + final res = + await marchingEngine.indexEndpoints.get(id: '8572232454792806400'); + expect(res, isNotNull); + }); + + test('Test delete index endpoint', () async { + final res = + await marchingEngine.indexEndpoints.delete(id: '8572232454792806400'); + expect(res, isNotNull); + }); + + test('Test undeploy index from endpoint', () async { + final res = await marchingEngine.indexEndpoints.undeployIndex( + indexEndpointId: '8572232454792806400', + deployedIndexId: 'deployment1', + ); + expect(res, isNotNull); + }); + }); + + group('VertexAIMatchingEngineClient query tests', () { + test('Test query index', () async { + final machineEngineQuery = VertexAIMatchingEngineClient( + authHttpClient: authClient, + project: Platform.environment['VERTEX_AI_PROJECT_ID']!, + rootUrl: + 'https://1451028425.europe-west1-706285145183.vdb.vertexai.goog/', + ); + final res = await machineEngineQuery.indexEndpoints.findNeighbors( + indexEndpointId: '8572232454792806400', + deployedIndexId: 'deployment1', + queries: const [ + VertexAIFindNeighborsRequestQuery( + datapoint: VertexAIIndexDatapoint( + datapointId: '10', + featureVector: _queryVector, + ), + neighborCount: 3, + ), + ], + ); + expect(res, isNotNull); + }); + }); +} + +Future _getAuthenticatedClient() async { + final serviceAccountCredentials = ServiceAccountCredentials.fromJson( + json.decode(Platform.environment['VERTEX_AI_SERVICE_ACCOUNT']!), + ); + return clientViaServiceAccount( + serviceAccountCredentials, + [VertexAIGenAIClient.cloudPlatformScope], + ); +} + +const _queryVector = [ + -0.0024800552055239677, + 0.011974085122346878, + 0.027945270761847496, + 0.06089121848344803, + 0.01591639779508114, + 0.045949868857860565, + -0.012206366285681725, + 0.026397397741675377, + -0.02571168914437294, + -0.0022428107913583517, + 0.017314476892352104, + 0.05456288158893585, + 0.035699039697647095, + -0.02135152369737625, + -0.022606346756219864, + -0.004925490356981754, + -0.04399697855114937, + -0.024715086445212364, + -0.006850498262792826, + 0.01790747046470642, + -0.05940520390868187, + -0.006281573325395584, + -0.014405393972992897, + -0.026017308235168457, + 0.014548663049936295, + -0.0731135904788971, + 0.024497421458363533, + -0.027612458914518356, + -0.029020294547080994, + -0.06312020123004913, + -0.003921786323189735, + -0.017137227579951286, + 0.012081332504749298, + -0.02097455784678459, + 0.014852325432002544, + 0.053726162761449814, + -0.03733977675437927, + 0.02199450694024563, + -0.0018198131583631039, + 0.0499347485601902, + -0.01654892787337303, + -0.0491509735584259, + 0.020115764811635017, + 0.011543355882167816, + 0.010533302091062069, + -0.003705954644829035, + -0.021334081888198853, + 0.06394882500171661, + 0.06690078228712082, + -0.048335202038288116, + -0.01955878920853138, + 0.020723478868603706, + -0.00039762884262017906, + 0.048672866076231, + 0.024126645177602768, + 0.04419830068945885, + -0.06521899998188019, + -0.033290475606918335, + -0.01575658842921257, + 0.01642526686191559, + 0.02032945491373539, + -0.04161004722118378, + 0.01441210974007845, + -0.0410119853913784, + -0.06563021242618561, + 0.007922261953353882, + -0.028644349426031113, + 0.017991654574871063, + -0.0077970284037292, + -0.025995980948209763, + 0.011249588802456856, + -0.00045249194954521954, + -0.02123924158513546, + 0.021377315744757652, + 0.05281722545623779, + -0.02869519591331482, + 0.014597243629395962, + 0.01738382689654827, + 0.017890606075525284, + -0.07110098749399185, + -0.01999991573393345, + -0.00877347495406866, + -0.05301406607031822, + -0.11461994796991348, + -0.06401617079973221, + 0.06658251583576202, + -0.001995210302993655, + 0.013316542841494083, + -0.020424021407961845, + 0.037054046988487244, + -0.040274158120155334, + 0.023708872497081757, + 0.037896133959293365, + 0.020479658618569374, + -0.01741095632314682, + -0.05057836323976517, + -0.008709455840289593, + -0.013266217894852161, + 0.015497623942792416, + 0.01205222774296999, + -0.027619006112217903, + -0.10722710192203522, + -0.005731775891035795, + -0.07026918232440948, + 0.04038692265748978, + -0.05040004104375839, + 0.005217043682932854, + 0.03388015553355217, + 0.010141142643988132, + -0.07109049707651138, + 0.012917617335915565, + -0.022699229419231415, + -0.027181023731827736, + 0.01369607076048851, + 0.05940553918480873, + -0.0666455626487732, + 0.004041127394884825, + 0.05400310084223747, + -0.005153415258973837, + 0.021585319191217422, + -0.03905688598752022, + -0.0263311006128788, + 0.01328785065561533, + 0.00648899283260107, + -0.01907315105199814, + 0.019168950617313385, + 0.09318007528781891, + 0.05195644870400429, + 0.01619238220155239, + 0.04146619886159897, + 0.0936182364821434, + -0.022216904908418655, + 0.01723639667034149, + -0.029653336852788925, + 0.08174880594015121, + 0.01754760928452015, + -0.023688282817602158, + 0.02213711477816105, + 0.030211031436920166, + 0.05241619795560837, + -0.08753107488155365, + 0.012492592446506023, + -0.007995668798685074, + 0.04056641831994057, + -0.055074967443943024, + -0.030395345762372017, + 0.006586231756955385, + -0.0278791394084692, + -0.04829850792884827, + -0.016373807564377785, + -0.05235663801431656, + 0.015281729400157928, + -0.010130900889635086, + 0.06573318690061569, + 0.05475528538227081, + 0.05164555460214615, + 0.006315293721854687, + 0.019746508449316025, + 0.015135163441300392, + -0.031136205419898033, + 0.007764772046357393, + 0.0394541472196579, + -0.02604949288070202, + 0.013066614978015423, + 0.023597221821546555, + 0.006964786443859339, + -0.041713815182447433, + 0.009692543186247349, + -0.01894967071712017, + 0.02710210159420967, + 0.06985951215028763, + -0.11547199636697769, + 0.026909882202744484, + -0.05686219781637192, + 0.07252556830644608, + 0.02729402855038643, + 0.008035331964492798, + 0.024488260969519615, + 0.002778689842671156, + 0.011325910687446594, + -0.003569392953068018, + -0.0850648581981659, + 0.0012582677882164717, + -0.040039319545030594, + -0.03386136144399643, + 0.00014740921324118972, + -0.021747155115008354, + 0.010006153024733067, + -0.045477915555238724, + 0.03555833175778389, + 0.004154199734330177, + 0.027085134759545326, + -0.004759603179991245, + -0.06851977109909058, + -0.012431585229933262, + -0.04602782800793648, + 0.052711136639118195, + -0.08390163630247116, + 0.04075220972299576, + 0.030154278501868248, + -0.02538658119738102, + -0.06528583914041519, + 0.013150053098797798, + 0.04211396723985672, + -0.04961792379617691, + 0.010408340021967888, + -0.018248483538627625, + 0.01874774694442749, + -0.029981963336467743, + 0.03500630334019661, + 0.008376826532185078, + 0.005190200638025999, + 0.05301978066563606, + 0.005912765394896269, + 0.006089750677347183, + 0.004957679659128189, + 0.032940641045570374, + -0.06675024330615997, + -0.0792369619011879, + -0.03757181763648987, + 0.014747879467904568, + 0.021590597927570343, + -0.004220806993544102, + 0.0058377147652208805, + 0.025445397943258286, + 0.04735690727829933, + 0.025496503338217735, + -0.0009749596938490868, + -0.03635849058628082, + 0.05388989299535751, + 0.022338273003697395, + -0.014873513951897621, + -0.04598985239863396, + -0.01964334212243557, + -0.002764167496934533, + 0.06323042511940002, + -0.00022570922737941146, + 0.025602353736758232, + -0.08158840239048004, + 0.033581968396902084, + -0.010399993509054184, + 0.022933878004550934, + -0.04645109176635742, + 0.0237208753824234, + -0.003838959615677595, + 0.030994132161140442, + -0.0581846758723259, + 0.042504776269197464, + 0.009096813388168812, + -0.028246747329831123, + 0.057335738092660904, + -0.021672362461686134, + 0.016617396846413612, + 0.02242114767432213, + 0.04753721505403519, + -0.03262138366699219, + 0.0014356492320075631, + 0.052940450608730316, + -0.03302508965134621, + 0.03374477103352547, + -0.02883862517774105, + -0.03039146587252617, + 0.020496999844908714, + 0.06553924083709717, + 0.015572815202176571, + -0.008528808131814003, + -0.04120403155684471, + -0.042944908142089844, + 0.06550800800323486, + -0.00409502349793911, + -0.019560927525162697, + 0.0025754491798579693, + 0.049757979810237885, + 0.029423275962471962, + 0.011083927005529404, + 0.050855644047260284, + -0.04935210570693016, + 0.03638442978262901, + -0.052799563854932785, + 0.07322375476360321, + 0.051303647458553314, + -0.032500140368938446, + 0.019702143967151642, + 0.05000250041484833, + 0.005088430363684893, + 0.017879273742437363, + -0.019979150965809822, + 0.025685083121061325, + -0.028702659532427788, + 0.007295092102140188, + 0.04584210366010666, + -0.07941862940788269, + 0.02022925391793251, + 0.05224103108048439, + 0.015263560228049755, + 0.0002274672588100657, + 0.03551832213997841, + -0.029143039137125015, + -0.016852524131536484, + 0.033210039138793945, + 0.015557453967630863, + -0.0004994983319193125, + -0.019271982833743095, + -0.030236605554819107, + -0.05725082755088806, + -0.010165062732994556, + 0.019357409328222275, + 0.027202516794204712, + 0.01839538849890232, + -0.019694624468684196, + 0.02678288146853447, + -0.044801242649555206, + 0.015726404264569283, + 0.02533598057925701, + -0.027300124987959862, + 0.030965549871325493, + 0.01383476797491312, + 0.025441769510507584, + -0.042588166892528534, + -0.0026665041223168373, + -0.07306212931871414, + 0.04228546842932701, + 0.07770076394081116, + 0.02618120238184929, + 0.01646341010928154, + -0.04739517718553543, + -0.08538003265857697, + 0.040878623723983765, + 0.050099264830350876, + -0.012263220734894276, + -0.03874794393777847, + 0.02525949664413929, + -0.04332829639315605, + -0.00814410112798214, + 0.04538974538445473, + -0.04799098148941994, + 0.010895933955907822, + 0.023221634328365326, + 0.018915975466370583, + -0.08099763095378876, + -0.037038736045360565, + -0.038237277418375015, + -0.0205315463244915, + -0.034528762102127075, + -0.01136486791074276, + 0.01599818654358387, + -0.023073730990290642, + -0.011987966485321522, + -0.06666819006204605, + -0.04086934030056, + 0.002453841967508197, + 0.055486876517534256, + 0.013495233841240406, + 0.016096634790301323, + 0.01632685586810112, + 0.0046924808993935585, + -0.0013824260095134377, + -0.022637294605374336, + 0.053107455372810364, + 0.004315677098929882, + -0.02178102359175682, + 0.017118530347943306, + -0.03783629834651947, + -0.010677210986614227, + 0.016758006066083908, + -0.0037163058295845985, + -0.02830849587917328, + -0.028290648013353348, + 0.011388111859560013, + -0.06282684952020645, + 0.002664626343175769, + 0.001676127314567566, + 0.014519261196255684, + -0.021564491093158722, + -0.027759229764342308, + 0.022160736843943596, + -0.05039668455719948, + 0.019299058243632317, + -0.003910744562745094, + 0.023026909679174423, + -0.014358281157910824, + 0.040930043905973434, + -0.02965562790632248, + 0.0315079465508461, + 0.009793383069336414, + -0.011312074959278107, + 0.026616903021931648, + 0.08185574412345886, + 0.0022060233168303967, + 0.03195205330848694, + 0.017995746806263924, + 0.04716493934392929, + 0.043085839599370956, + -0.02036077342927456, + 0.03734202682971954, + 0.019890712574124336, + -0.05439202859997749, + -0.08098692446947098, + -0.03921710327267647, + -0.016108626499772072, + 0.028868334367871284, + -0.01334142405539751, + -0.04652391001582146, + -0.055355172604322433, + 0.00672161253169179, + -0.015201116912066936, + -0.027707237750291824, + 0.03995480760931969, + 0.028925718739628792, + -0.007712054066359997, + 0.0228127371519804, + -0.03518705815076828, + -0.00964331068098545, + -0.003235805546864867, + 0.07459256798028946, + -0.054491519927978516, + -0.010547894984483719, + -0.05564255267381668, + 0.017967883497476578, + -0.023240119218826294, + 0.0008635363774374127, + -0.026240376755595207, + -0.07990437746047974, + 0.025640642270445824, + -0.009579784236848354, + -0.0015484221512451768, + -0.05469866469502449, + -0.01371675543487072, + -0.053961653262376785, + 0.020929858088493347, + 0.011524469591677189, + -0.05228453502058983, + 0.038667384535074234, + -0.01055141631513834, + -0.041942697018384933, + -0.029565278440713882, + 0.06928738951683044, + 0.1073240265250206, + -0.026453327387571335, + -0.03961186856031418, + -0.03256576135754585, + -0.021278824657201767, + 0.018629232421517372, + 0.04511110484600067, + -0.012895888648927212, + -0.013234605081379414, + -0.06024298444390297, + -0.00470823934301734, + -0.023729337379336357, + -0.03319356217980385, + -0.033604737371206284, + -0.019526876509189606, + -0.03432648256421089, + 0.06283165514469147, + 0.0022637846413999796, + 0.07177982479333878, + 0.016291163861751556, + -0.03822924196720123, + -0.046136315912008286, + -0.010122058913111687, + -0.026826607063412666, + 0.030277421697974205, + 0.0012080231681466103, + 0.04549718648195267, + -0.028757764026522636, + 0.015382646583020687, + 0.04650441184639931, + 0.009145055897533894, + -0.03695225343108177, + 0.03564176335930824, + -0.054017260670661926, + 0.053990766406059265, + -0.03894525021314621, + 0.005253234412521124, + 0.05727563053369522, + 0.016917597502470016, + 0.0015583543572574854, + 0.035989921540021896, + -0.00906539149582386, + -0.014243338257074356, + -0.03303677216172218, + -0.01067740935832262, + -0.008197851479053497, + -0.05057824030518532, + 0.008696588687598705, + 0.04489292576909065, + -0.008058791980147362, + -0.023300092667341232, + 0.040308475494384766, + 0.0010585372801870108, + 0.01096348650753498, + -0.06801032274961472, + -0.00025523806107230484, + -0.011782528832554817, + 0.030658317729830742, + -0.02044806070625782, + -0.001490660011768341, + -0.019346261397004128, + 0.032130707055330276, + -0.034276120364665985, + -0.06216953322291374, + 0.0009086608188226819, + 0.001522677717730403, + -0.012745819054543972, + 0.06908354163169861, + -0.0035166162997484207, + -0.018514111638069153, + -0.039288099855184555, + -0.0444314107298851, + 0.03754890337586403, + 0.016863422468304634, + -0.03534917160868645, + -0.003182257292792201, + -0.04774448275566101, + -0.00003124616341665387, + -0.10518063604831696, + -0.004144659731537104, + 0.0723867118358612, + -0.0031207804568111897, + 0.013072462752461433, + -0.048622481524944305, + -0.033812060952186584, + 0.004568304400891066, + -0.0031037137378007174, + 0.021052075549960136, + 0.04156983643770218, + 0.03892498463392258, + -0.011406195349991322, + -0.031035030260682106, + 0.011150837875902653, + -0.011662237346172333, + 0.05525480583310127, + -0.03298760950565338, + -0.031118126586079597, + 0.08421474695205688, + -0.02164381556212902, + 0.023803481832146645, + -0.012235553935170174, + 0.01834927499294281, + 0.05319342762231827, + -0.0201859287917614, + -0.012603869661688805, + 0.023757725954055786, + 0.021485626697540283, + 0.012704837135970592, + 0.02149340882897377, + 0.02025606669485569, + 0.006970172747969627, + 0.050382718443870544, + -0.028631050139665604, + -0.003874230431392789, + -0.060862522572278976, + -0.0681929886341095, + 0.03328116983175278, + -0.0003696992062032223, + -0.04718751460313797, + -0.06874881684780121, + -0.024884505197405815, + 0.011152289807796478, + -0.02182626910507679, + 0.009133966639637947, + 0.016513297334313393, + 0.009121187962591648, + -0.03590567782521248, + 0.031725939363241196, + 0.003005493897944689, + 0.016160182654857635, + 0.02399451471865177, + -0.017237335443496704, + 0.01866758242249489, + -0.019074972718954086, + -0.0488954596221447, + -0.029238546267151833, + -0.021969163790345192, + 0.03000270202755928, + -0.012111065909266472, + -0.061692509800195694, + -0.027968399226665497, + 0.02743874490261078, + 0.007777548860758543, + 0.015492431819438934, + -0.005347430240362883, + -0.018200334161520004, + 0.0018336826469749212, + -0.031607042998075485, + -0.014118785038590431, + -0.024861887097358704, + -0.017011569812893867, + -0.03169884905219078, + -0.0850626602768898, + 0.029640695080161095, + -0.07870057225227356, + -0.011963256634771824, + 0.006744022481143475, + -0.04094612970948219, + -0.004237143788486719, + 0.01201427262276411, + -0.05417366325855255, + 0.04515045881271362, + -0.03326345607638359, + 0.012644425965845585, + -0.026208844035863876, + 0.04211442172527313, + -0.011998665519058704, + -0.006546241696923971, + 0.0011108559556305408, + 0.08723857253789902, + 0.05843894183635712, + 0.040107037872076035, + 0.028628408908843994, + 0.002568240510299802, + 0.01729537546634674, + 0.005429181270301342, + -0.02222994901239872, + -0.007678688503801823, + -0.026843709871172905, + -0.01726338267326355, + -0.0014378676423802972, + 0.03453582897782326, + 0.005992847960442305, + -0.012618916109204292, + 0.09048930555582047, + -0.004347036127001047, + -0.05761748179793358, + -0.024218415841460228, + -0.0617549791932106, + 0.023912576958537102, + 0.048499371856451035, + 0.002885707886889577, + -0.029988480731844902, + -0.01094681117683649, + -0.018467770889401436, + 0.003714068327099085, + -0.058079030364751816, + 0.017040394246578217, + -0.039894089102745056, + -0.030635381117463112, + 0.044455066323280334, + 0.04558039829134941, + 0.04559404402971268, + 0.020578967407345772, + -0.0054467832669615746, + -0.00569724990054965, + 0.009905865415930748, + -0.011255532503128052, + -0.0355696864426136, + -0.034046903252601624, + -0.002546388655900955, + 0.0034123039804399014, + -0.012156737968325615, + 0.0527615025639534, + -0.032744478434324265, + -0.046668991446495056, + -0.03462834656238556, + -0.06650368869304657, + -0.008561598137021065, + -0.0013868717942386866, + 0.06080976128578186, + 0.0077654700726270676, + 0.003287557978183031, + 0.05465131253004074, + 0.004132724367082119, + 0.024199068546295166, + 0.05864977836608887, + -0.06225745007395744, + 0.0057786013931035995, + -0.007371222134679556, + -0.005587731022387743, + -0.00950106605887413, + -0.061786990612745285, + 0.008341703563928604, + -0.04203549027442932, + -0.03493040055036545, + -0.02666497603058815, + 0.008635095320641994, + -0.0005944097065366805, + 0.034384552389383316, + 0.06463247537612915, + 0.030862923711538315, + 0.038226742297410965, + -0.00971216894686222, + -0.04456645995378494, + 0.021693125367164612, + -0.013434921391308308, + 0.04909699410200119, + -0.028396733105182648, + 0.01156967505812645, + -0.011123130097985268, + -0.0031161142978817225, + 0.016380202025175095, + 0.015161557123064995, + 0.02891436032950878, + -0.053235702216625214, + 0.02260618843138218, + -0.021886402741074562, + -0.05016421526670456, + -0.0190590750426054, + -0.005225166212767363, + 0.0320899523794651, + 0.019669990986585617, + -0.013529667630791664, + 0.016441943123936653, + 0.05976942181587219, + 0.023295745253562927, + 0.03328859061002731, + -0.044952381402254105, + -0.06350108981132507, + 0.0034671607427299023, + -0.01289446372538805, + -0.04644192010164261, + -0.014127887785434723, + 0.010741163976490498, + 0.030758243054151535, + 0.037426527589559555, + -0.019230302423238754, + 0.04674021899700165, + -0.10073813796043396, + -0.02519753761589527, + 0.0244305282831192, + 0.010897213593125343, + 0.017850957810878754, + 0.05721067264676094, + 0.0034028352238237858, + -0.05515863001346588, + -0.045212119817733765, + 0.005976893939077854, + 0.004804182797670364, + -0.03706206753849983, + -0.043186623603105545, + 0.03596680611371994, + -0.029261885210871696, + 0.029298892244696617, + 0.038443610072135925, + 0.04970880225300789, + -0.02848917432129383, + -0.008567516691982746, + 0.027900034561753273, + 0.03967684507369995, + -0.004614111967384815, + 0.011680900119245052, + 0.011586959473788738, + 0.013510816730558872, + -0.019214434549212456, + -0.007085992489010096, + -0.022214235737919807, + 0.0009897889103740454, + 0.05701182782649994, + -0.019148552790284157, + 0.0013918313197791576, + 0.0021684300154447556, + -0.044678740203380585, + 0.0040362002328038216, + 0.04208571836352348, + -0.004585193935781717, + -0.009162068367004395, + 0.0646393671631813, + 0.023202434182167053, + 0.031634483486413956, + -0.04858662188053131, + 0.03577597439289093, + -0.013750282116234303, + -0.016435980796813965, + -0.04169601947069168, + 0.026427248492836952, + 0.04319629445672035, + -0.007710671983659267, + -0.00981274526566267, + 0.006554502993822098 +]; diff --git a/packages/vertex_ai/test/matching_engine/mappers/index_endpoints_test.dart b/packages/vertex_ai/test/matching_engine/mappers/index_endpoints_test.dart new file mode 100644 index 00000000..29b9c296 --- /dev/null +++ b/packages/vertex_ai/test/matching_engine/mappers/index_endpoints_test.dart @@ -0,0 +1,273 @@ +// ignore_for_file: avoid_redundant_argument_values +import 'package:googleapis/aiplatform/v1.dart'; +import 'package:test/test.dart'; +import 'package:vertex_ai/src/matching_engine/mappers/index_endpoints.dart'; +import 'package:vertex_ai/vertex_ai.dart'; + +void main() { + group('VertexAIIndexEndpointsGoogleApisMapper tests', () { + test( + 'VertexAIIndexEndpointsGoogleApisMapper.mapPrivateServiceConnectConfig', + () { + const config = VertexAIPrivateServiceConnectConfig( + enablePrivateServiceConnect: true, + projectAllowlist: ['projectAllowlist'], + ); + final expected = GoogleCloudAiplatformV1PrivateServiceConnectConfig( + enablePrivateServiceConnect: true, + projectAllowlist: ['projectAllowlist'], + ); + + final res = + VertexAIIndexEndpointsGoogleApisMapper.mapPrivateServiceConnectConfig( + config, + ); + expect( + res.enablePrivateServiceConnect, + expected.enablePrivateServiceConnect, + ); + expect(res.projectAllowlist, expected.projectAllowlist); + }); + + test('VertexAIIndexEndpointsGoogleApisMapper.mapAutomaticResources', () { + const resources = VertexAIAutomaticResources( + minReplicaCount: 5, + maxReplicaCount: 5, + ); + final expected = GoogleCloudAiplatformV1AutomaticResources( + minReplicaCount: 5, + maxReplicaCount: 5, + ); + + final res = VertexAIIndexEndpointsGoogleApisMapper.mapAutomaticResources( + resources, + ); + expect(res.minReplicaCount, expected.minReplicaCount); + expect(res.maxReplicaCount, expected.maxReplicaCount); + }); + + test('VertexAIIndexEndpointsGoogleApisMapper.mapRequestQuery', () { + const query = VertexAIFindNeighborsRequestQuery( + datapoint: VertexAIIndexDatapoint( + datapointId: 'datapointId', + featureVector: [], + crowdingTag: null, + restricts: null, + ), + neighborCount: 1, + approximateNeighborCount: 1, + fractionLeafNodesToSearchOverride: 1.0, + perCrowdingAttributeNeighborCount: 1, + ); + final expected = GoogleCloudAiplatformV1FindNeighborsRequestQuery( + datapoint: GoogleCloudAiplatformV1IndexDatapoint( + datapointId: 'datapointId', + featureVector: [], + crowdingTag: null, + restricts: null, + ), + neighborCount: 1, + approximateNeighborCount: 1, + fractionLeafNodesToSearchOverride: 1.0, + perCrowdingAttributeNeighborCount: 1, + ); + + final res = VertexAIIndexEndpointsGoogleApisMapper.mapRequestQuery( + query, + ); + expect( + res.approximateNeighborCount, + expected.approximateNeighborCount, + ); + expect(res.datapoint?.datapointId, expected.datapoint?.datapointId); + expect(res.datapoint?.featureVector, expected.datapoint?.featureVector); + expect( + res.datapoint?.crowdingTag?.crowdingAttribute, + expected.datapoint?.crowdingTag?.crowdingAttribute, + ); + expect( + res.fractionLeafNodesToSearchOverride, + expected.fractionLeafNodesToSearchOverride, + ); + expect(res.neighborCount, expected.neighborCount); + expect( + res.perCrowdingAttributeNeighborCount, + expected.perCrowdingAttributeNeighborCount, + ); + }); + + test('VertexAIIndexEndpointsGoogleApisMapper.mapIndexEndpoint', () { + final indexEndpoint = GoogleCloudAiplatformV1IndexEndpoint( + name: 'projects/PROJECT_NUMBER/locations/LOCATION/indexes/INDEX_ID', + displayName: 'DISPLAY_NAME', + description: 'DESCRIPTION', + network: 'network', + privateServiceConnectConfig: + GoogleCloudAiplatformV1PrivateServiceConnectConfig( + enablePrivateServiceConnect: true, + projectAllowlist: ['projectAllowlist'], + ), + publicEndpointEnabled: true, + publicEndpointDomainName: 'publicEndpointDomainName', + deployedIndexes: [ + GoogleCloudAiplatformV1DeployedIndex( + id: 'id', + index: 'index', + displayName: 'displayName', + createTime: '2020-11-08T21:56:30.558449Z', + indexSyncTime: '2020-11-08T21:56:30.558449Z', + automaticResources: GoogleCloudAiplatformV1AutomaticResources( + minReplicaCount: 5, + maxReplicaCount: 5, + ), + dedicatedResources: GoogleCloudAiplatformV1DedicatedResources( + autoscalingMetricSpecs: [ + GoogleCloudAiplatformV1AutoscalingMetricSpec( + metricName: 'metricName', + target: 10, + ), + ], + machineSpec: GoogleCloudAiplatformV1MachineSpec( + machineType: 'machineType', + acceleratorType: 'acceleratorType', + acceleratorCount: 1, + ), + minReplicaCount: 1, + maxReplicaCount: 1, + ), + deployedIndexAuthConfig: + GoogleCloudAiplatformV1DeployedIndexAuthConfig( + authProvider: + GoogleCloudAiplatformV1DeployedIndexAuthConfigAuthProvider( + allowedIssuers: ['allowedIssuers'], + audiences: ['audiences'], + ), + ), + privateEndpoints: GoogleCloudAiplatformV1IndexPrivateEndpoints( + matchGrpcAddress: 'matchGrpcAddress', + serviceAttachment: 'serviceAttachment', + ), + reservedIpRanges: [ + 'reservedIpRanges', + ], + deploymentGroup: 'deploymentGroup', + enableAccessLogging: true, + ), + ], + labels: null, + etag: 'ETAG', + createTime: '2020-11-08T21:56:30.558449Z', + updateTime: '2020-11-08T22:39:25.048623Z', + ); + final expected = VertexAIIndexEndpoint( + name: 'projects/PROJECT_NUMBER/locations/LOCATION/indexes/INDEX_ID', + displayName: 'DISPLAY_NAME', + description: 'DESCRIPTION', + network: 'network', + privateServiceConnectConfig: const VertexAIPrivateServiceConnectConfig( + enablePrivateServiceConnect: true, + projectAllowlist: ['projectAllowlist'], + ), + publicEndpointEnabled: true, + publicEndpointDomainName: 'publicEndpointDomainName', + deployedIndexes: [ + VertexAIDeployedIndex( + id: 'id', + index: 'index', + displayName: 'displayName', + createTime: DateTime.parse('2020-11-08T21:56:30.558449Z'), + indexSyncTime: DateTime.parse('2020-11-08T21:56:30.558449Z'), + automaticResources: const VertexAIAutomaticResources( + minReplicaCount: 5, + maxReplicaCount: 5, + ), + dedicatedResources: const VertexAIDedicatedResources( + autoscalingMetricSpecs: [ + VertexAIAutoscalingMetricSpec( + metricName: 'metricName', + target: 10, + ), + ], + machineSpec: VertexAIMachineSpec( + machineType: 'machineType', + acceleratorType: 'acceleratorType', + acceleratorCount: 1, + ), + minReplicaCount: 1, + maxReplicaCount: 1, + ), + deployedIndexAuthConfig: const VertexAIDeployedIndexAuthConfig( + authProvider: VertexAIDeployedIndexAuthConfigAuthProvider( + allowedIssuers: ['allowedIssuers'], + audiences: ['audiences'], + ), + ), + privateEndpoints: const VertexAIIndexPrivateEndpoints( + matchGrpcAddress: 'matchGrpcAddress', + serviceAttachment: 'serviceAttachment', + ), + reservedIpRanges: const [ + 'reservedIpRanges', + ], + deploymentGroup: 'deploymentGroup', + enableAccessLogging: true, + ), + ], + labels: null, + etag: 'ETAG', + createTime: DateTime.parse('2020-11-08T21:56:30.558449Z'), + updateTime: DateTime.parse('2020-11-08T22:39:25.048623Z'), + ); + + final res = VertexAIIndexEndpointsGoogleApisMapper.mapIndexEndpoint( + indexEndpoint, + ); + expect(res, expected); + }); + + test('VertexAIIndexEndpointsGoogleApisMapper.mapFindNeighborsResponse', () { + final response = GoogleCloudAiplatformV1FindNeighborsResponse( + nearestNeighbors: [ + GoogleCloudAiplatformV1FindNeighborsResponseNearestNeighbors( + id: 'deployedIndexId', + neighbors: [ + GoogleCloudAiplatformV1FindNeighborsResponseNeighbor( + datapoint: GoogleCloudAiplatformV1IndexDatapoint( + datapointId: 'neighbor', + featureVector: [], + crowdingTag: null, + restricts: null, + ), + distance: 1.0, + ), + ], + ), + ], + ); + const expected = VertexAIFindNeighborsResponse( + nearestNeighbors: [ + VertexAIFindNeighborsResponseNearestNeighbors( + id: 'deployedIndexId', + neighbors: [ + VertexAIFindNeighborsResponseNeighbor( + datapoint: VertexAIIndexDatapoint( + datapointId: 'neighbor', + featureVector: [], + crowdingTag: null, + restricts: null, + ), + distance: 1.0, + ), + ], + ), + ], + ); + + final res = + VertexAIIndexEndpointsGoogleApisMapper.mapFindNeighborsResponse( + response, + ); + expect(res, expected); + }); + }); +} diff --git a/packages/vertex_ai/test/matching_engine/mappers/indexes_test.dart b/packages/vertex_ai/test/matching_engine/mappers/indexes_test.dart new file mode 100644 index 00000000..ac8a5182 --- /dev/null +++ b/packages/vertex_ai/test/matching_engine/mappers/indexes_test.dart @@ -0,0 +1,177 @@ +// ignore_for_file: avoid_redundant_argument_values +import 'package:googleapis/aiplatform/v1.dart'; +import 'package:test/test.dart'; +import 'package:vertex_ai/src/matching_engine/mappers/indexes.dart'; +import 'package:vertex_ai/vertex_ai.dart'; + +void main() { + group('VertexAIIndexGoogleApisMapper tests', () { + test('VertexAIIndexGoogleApisMapper.mapIndex', () { + final index = GoogleCloudAiplatformV1Index( + name: 'projects/PROJECT_NUMBER/locations/LOCATION/indexes/INDEX_ID', + displayName: 'DISPLAY_NAME', + description: 'DESCRIPTION', + metadataSchemaUri: 'METADATA_SCHEMA_URI', + metadata: const { + 'contentsDeltaUri': 'gs://BUCKET_NAME/PATH_TO_INDEX_DIR/', + 'isCompleteOverwrite': false, + 'config': { + 'dimensions': 100, + 'approximateNeighborsCount': 150, + 'distanceMeasureType': 'DOT_PRODUCT_DISTANCE', + 'featureNormType': 'NONE', + 'algorithmConfig': { + 'treeAhConfig': { + 'fractionLeafNodesToSearch': 0.1, + 'leafNodeEmbeddingCount': 900, + 'leafNodesToSearchPercent': 5, + } + }, + 'shardSize': 'SHARD_SIZE_MEDIUM', + } + }, + indexUpdateMethod: 'BATCH_UPDATE', + indexStats: GoogleCloudAiplatformV1IndexStats( + shardsCount: 1, + vectorsCount: '1000', + ), + etag: 'ETAG', + createTime: '2020-11-08T21:56:30.558449Z', + updateTime: '2020-11-08T22:39:25.048623Z', + ); + final expected = VertexAIIndex( + name: 'projects/PROJECT_NUMBER/locations/LOCATION/indexes/INDEX_ID', + displayName: 'DISPLAY_NAME', + description: 'DESCRIPTION', + metadataSchemaUri: 'METADATA_SCHEMA_URI', + metadata: const VertexAIIndexMetadata( + config: VertexAINearestNeighborSearchConfig( + dimensions: 100, + approximateNeighborsCount: 150, + distanceMeasureType: VertexAIDistanceMeasureType.dotProductDistance, + featureNormType: VertexAIFeatureNormType.none, + algorithmConfig: VertexAITreeAhAlgorithmConfig( + fractionLeafNodesToSearch: 0.1, + leafNodeEmbeddingCount: 900, + leafNodesToSearchPercent: 5, + ), + shardSize: VertexAIShardSize.medium, + ), + ), + indexUpdateMethod: VertexAIIndexUpdateMethod.batchUpdate, + indexStats: const VertexAIIndexStats( + shardsCount: 1, + vectorsCount: 1000, + ), + labels: null, + etag: 'ETAG', + createTime: DateTime.parse('2020-11-08T21:56:30.558449Z'), + updateTime: DateTime.parse('2020-11-08T22:39:25.048623Z'), + ); + + final res = VertexAIIndexGoogleApisMapper.mapIndex(index); + expect(res, expected); + }); + + test('VertexAIIndexGoogleApisMapper.mapStats', () { + final stats = GoogleCloudAiplatformV1IndexStats( + shardsCount: 2, + vectorsCount: '1000', + ); + const expected = VertexAIIndexStats( + shardsCount: 2, + vectorsCount: 1000, + ); + + final res = VertexAIIndexGoogleApisMapper.mapStats(stats); + expect(res, expected); + }); + + test('VertexAIIndexGoogleApisMapper.mapIndexDatapoint', () { + const datapoint = VertexAIIndexDatapoint( + datapointId: 'DATAPOINT_ID', + featureVector: [0.0], + crowdingTag: VertexAIIndexDatapointCrowdingTag( + crowdingAttribute: 'CROWDING_ATTRIBUTE', + ), + restricts: [ + VertexAIIndexDatapointRestriction( + namespace: 'NAMESPACE', + allowList: ['ALLOW_LIST'], + denyList: [], + ), + ], + ); + final expected = GoogleCloudAiplatformV1IndexDatapoint( + datapointId: 'DATAPOINT_ID', + featureVector: [0.0], + crowdingTag: GoogleCloudAiplatformV1IndexDatapointCrowdingTag( + crowdingAttribute: 'CROWDING_ATTRIBUTE', + ), + restricts: [ + GoogleCloudAiplatformV1IndexDatapointRestriction( + namespace: 'NAMESPACE', + allowList: ['ALLOW_LIST'], + denyList: [], + ), + ], + ); + + final res = VertexAIIndexGoogleApisMapper.mapIndexDatapoint(datapoint); + expect( + res.datapointId, + expected.datapointId, + ); + expect( + res.crowdingTag?.crowdingAttribute, + expected.crowdingTag?.crowdingAttribute, + ); + expect( + res.restricts?.first.namespace, + expected.restricts?.first.namespace, + ); + expect( + res.restricts?.first.allowList, + expected.restricts?.first.allowList, + ); + expect( + res.restricts?.first.denyList, + expected.restricts?.first.denyList, + ); + }); + + test('VertexAIIndexGoogleApisMapper.mapIndexDatapointDto', () { + final datapoint = GoogleCloudAiplatformV1IndexDatapoint( + datapointId: 'DATAPOINT_ID', + featureVector: [0.0], + crowdingTag: GoogleCloudAiplatformV1IndexDatapointCrowdingTag( + crowdingAttribute: 'CROWDING_ATTRIBUTE', + ), + restricts: [ + GoogleCloudAiplatformV1IndexDatapointRestriction( + namespace: 'NAMESPACE', + allowList: ['ALLOW_LIST'], + denyList: [], + ), + ], + ); + const expected = VertexAIIndexDatapoint( + datapointId: 'DATAPOINT_ID', + featureVector: [0.0], + crowdingTag: VertexAIIndexDatapointCrowdingTag( + crowdingAttribute: 'CROWDING_ATTRIBUTE', + ), + restricts: [ + VertexAIIndexDatapointRestriction( + namespace: 'NAMESPACE', + allowList: ['ALLOW_LIST'], + denyList: [], + ), + ], + ); + + final res = VertexAIIndexGoogleApisMapper.mapIndexDatapointDto(datapoint); + expect(res, expected); + }); + }); +} diff --git a/packages/vertex_ai/test/matching_engine/mappers/operation_test.dart b/packages/vertex_ai/test/matching_engine/mappers/operation_test.dart new file mode 100644 index 00000000..5d171777 --- /dev/null +++ b/packages/vertex_ai/test/matching_engine/mappers/operation_test.dart @@ -0,0 +1,37 @@ +// ignore_for_file: avoid_redundant_argument_values +import 'package:googleapis/aiplatform/v1.dart'; +import 'package:test/test.dart'; +import 'package:vertex_ai/src/matching_engine/mappers/mappers.dart'; +import 'package:vertex_ai/vertex_ai.dart'; + +void main() { + group('VertexAIOperationGoogleApisMapper tests', () { + test('VertexAIOperationGoogleApisMapper.mapOperation', () { + final operation = GoogleLongrunningOperation( + name: 'projects/PROJECT_NUMBER/locations/LOCATION/indexes/INDEX_ID', + done: true, + response: {}, + error: GoogleRpcStatus( + code: 0, + details: const [], + message: 'message', + ), + metadata: {}, + ); + const expected = VertexAIOperation( + name: 'projects/PROJECT_NUMBER/locations/LOCATION/indexes/INDEX_ID', + done: true, + response: {}, + error: VertexAIOperationError( + code: 0, + details: [], + message: 'message', + ), + metadata: {}, + ); + + final res = VertexAIOperationGoogleApisMapper.mapOperation(operation); + expect(res, expected); + }); + }); +}