From 915556261c845b3a7bde987d752322129a9806a3 Mon Sep 17 00:00:00 2001 From: Erdem Ayyildiz Date: Wed, 4 Dec 2024 18:19:17 +0100 Subject: [PATCH] chore(opensearchserverless): refactor cdk construct (#797) * chore(opensearchserverless): refactor oss collection --------- Signed-off-by: Alain Krok Co-authored-by: Erdem Ayyildiz Co-authored-by: Alain Krok --- .../namespaces/opensearchserverless/README.md | 3 + .../classes/VectorCollection.md | 534 +++++++++++++++++- .../VectorCollectionStandbyReplicas.md | 6 + .../enumerations/VectorCollectionType.md | 36 ++ .../interfaces/IVectorCollection.md | 247 ++++++++ .../interfaces/VectorCollectionAttributes.md | 49 ++ .../interfaces/VectorCollectionProps.md | 47 +- .../opensearchserverless/vector-collection.ts | 351 +++++++++++- .../vector-collection.test.ts | 299 +++++++++- 9 files changed, 1517 insertions(+), 55 deletions(-) create mode 100644 apidocs/namespaces/opensearchserverless/enumerations/VectorCollectionType.md create mode 100644 apidocs/namespaces/opensearchserverless/interfaces/IVectorCollection.md create mode 100644 apidocs/namespaces/opensearchserverless/interfaces/VectorCollectionAttributes.md diff --git a/apidocs/namespaces/opensearchserverless/README.md b/apidocs/namespaces/opensearchserverless/README.md index 7612fcbf..001a4bc1 100644 --- a/apidocs/namespaces/opensearchserverless/README.md +++ b/apidocs/namespaces/opensearchserverless/README.md @@ -14,6 +14,7 @@ - [TokenFilterType](enumerations/TokenFilterType.md) - [TokenizerType](enumerations/TokenizerType.md) - [VectorCollectionStandbyReplicas](enumerations/VectorCollectionStandbyReplicas.md) +- [VectorCollectionType](enumerations/VectorCollectionType.md) ### Classes @@ -21,4 +22,6 @@ ### Interfaces +- [IVectorCollection](interfaces/IVectorCollection.md) +- [VectorCollectionAttributes](interfaces/VectorCollectionAttributes.md) - [VectorCollectionProps](interfaces/VectorCollectionProps.md) diff --git a/apidocs/namespaces/opensearchserverless/classes/VectorCollection.md b/apidocs/namespaces/opensearchserverless/classes/VectorCollection.md index 6f6d41d1..9d28c40e 100644 --- a/apidocs/namespaces/opensearchserverless/classes/VectorCollection.md +++ b/apidocs/namespaces/opensearchserverless/classes/VectorCollection.md @@ -6,13 +6,11 @@ # Class: VectorCollection -Deploys an OpenSearch Serverless Collection to be used as a vector store. - -It includes all policies. +Provides a vector search collection in Amazon OpenSearch Serverless. ## Extends -- `Construct` +- `VectorCollectionBase` ## Constructors @@ -40,47 +38,97 @@ It includes all policies. #### Overrides -`Construct.constructor` +`VectorCollectionBase.constructor` ## Properties ### aossPolicy -> **aossPolicy**: `ManagedPolicy` +> `readonly` **aossPolicy**: `ManagedPolicy` + +#### Overrides -An IAM policy that allows API access to the collection. +`VectorCollectionBase.aossPolicy` *** ### collectionArn -> **collectionArn**: `string` +> `readonly` **collectionArn**: `string` -The ARN of the collection. +#### Overrides + +`VectorCollectionBase.collectionArn` + +*** + +### collectionEndpoint + +> `readonly` **collectionEndpoint**: `string` *** ### collectionId -> **collectionId**: `string` +> `readonly` **collectionId**: `string` + +#### Overrides -The ID of the collection. +`VectorCollectionBase.collectionId` *** ### collectionName -> **collectionName**: `string` +> `readonly` **collectionName**: `string` + +#### Overrides + +`VectorCollectionBase.collectionName` + +*** + +### collectionType + +> `readonly` **collectionType**: [`VectorCollectionType`](../enumerations/VectorCollectionType.md) + +#### Overrides + +`VectorCollectionBase.collectionType` -The name of the collection. +*** + +### dashboardEndpoint + +> `readonly` **dashboardEndpoint**: `string` *** ### dataAccessPolicy -> **dataAccessPolicy**: `CfnAccessPolicy` +> `readonly` **dataAccessPolicy**: `CfnAccessPolicy` -An OpenSearch Access Policy that allows access to the index. +#### Overrides + +`VectorCollectionBase.dataAccessPolicy` + +*** + +### env + +> `readonly` **env**: `ResourceEnvironment` + +The environment this resource belongs to. +For resources that are created and managed by the CDK +(generally, those created by creating new class instances like Role, Bucket, etc.), +this is always the same as the environment of the stack they belong to; +however, for imported resources +(those obtained from static methods like fromRoleArn, fromBucketName, etc.), +that might be different than the stack they were imported into. + +#### Inherited from + +`VectorCollectionBase.env` *** @@ -92,18 +140,186 @@ The tree node. #### Inherited from -`Construct.node` +`VectorCollectionBase.node` + +*** + +### physicalName + +> `protected` `readonly` **physicalName**: `string` + +Returns a string-encoded token that resolves to the physical name that +should be passed to the CloudFormation resource. + +This value will resolve to one of the following: +- a concrete value (e.g. `"my-awesome-bucket"`) +- `undefined`, when a name should be generated by CloudFormation +- a concrete name generated automatically during synthesis, in + cross-environment scenarios. + +#### Inherited from + +`VectorCollectionBase.physicalName` + +*** + +### stack + +> `readonly` **stack**: `Stack` + +The stack in which this resource is defined. + +#### Inherited from + +`VectorCollectionBase.stack` *** ### standbyReplicas -> **standbyReplicas**: [`VectorCollectionStandbyReplicas`](../enumerations/VectorCollectionStandbyReplicas.md) +> `readonly` **standbyReplicas**: [`VectorCollectionStandbyReplicas`](../enumerations/VectorCollectionStandbyReplicas.md) + +#### Overrides -Indicates whether to use standby replicas for the collection. +`VectorCollectionBase.standbyReplicas` ## Methods +### \_enableCrossEnvironment() + +> **\_enableCrossEnvironment**(): `void` + +**`Internal`** + +Called when this resource is referenced across environments +(account/region) to order to request that a physical name will be generated +for this resource during synthesis, so the resource can be referenced +through its absolute name/arn. + +#### Returns + +`void` + +#### Inherited from + +`VectorCollectionBase._enableCrossEnvironment` + +*** + +### applyRemovalPolicy() + +> **applyRemovalPolicy**(`policy`): `void` + +Apply the given removal policy to this resource + +The Removal Policy controls what happens to this resource when it stops +being managed by CloudFormation, either because you've removed it from the +CDK application or because you've made a change that requires the resource +to be replaced. + +The resource can be deleted (`RemovalPolicy.DESTROY`), or left in your AWS +account for data recovery and cleanup later (`RemovalPolicy.RETAIN`). + +#### Parameters + +##### policy + +`RemovalPolicy` + +#### Returns + +`void` + +#### Inherited from + +`VectorCollectionBase.applyRemovalPolicy` + +*** + +### generatePhysicalName() + +> `protected` **generatePhysicalName**(): `string` + +#### Returns + +`string` + +#### Inherited from + +`VectorCollectionBase.generatePhysicalName` + +*** + +### getResourceArnAttribute() + +> `protected` **getResourceArnAttribute**(`arnAttr`, `arnComponents`): `string` + +Returns an environment-sensitive token that should be used for the +resource's "ARN" attribute (e.g. `bucket.bucketArn`). + +Normally, this token will resolve to `arnAttr`, but if the resource is +referenced across environments, `arnComponents` will be used to synthesize +a concrete ARN with the resource's physical name. Make sure to reference +`this.physicalName` in `arnComponents`. + +#### Parameters + +##### arnAttr + +`string` + +The CFN attribute which resolves to the ARN of the resource. +Commonly it will be called "Arn" (e.g. `resource.attrArn`), but sometimes +it's the CFN resource's `ref`. + +##### arnComponents + +`ArnComponents` + +The format of the ARN of this resource. You must +reference `this.physicalName` somewhere within the ARN in order for +cross-environment references to work. + +#### Returns + +`string` + +#### Inherited from + +`VectorCollectionBase.getResourceArnAttribute` + +*** + +### getResourceNameAttribute() + +> `protected` **getResourceNameAttribute**(`nameAttr`): `string` + +Returns an environment-sensitive token that should be used for the +resource's "name" attribute (e.g. `bucket.bucketName`). + +Normally, this token will resolve to `nameAttr`, but if the resource is +referenced across environments, it will be resolved to `this.physicalName`, +which will be a concrete name. + +#### Parameters + +##### nameAttr + +`string` + +The CFN attribute which resolves to the resource's name. +Commonly this is the resource's `ref`. + +#### Returns + +`string` + +#### Inherited from + +`VectorCollectionBase.getResourceNameAttribute` + +*** + ### grantDataAccess() > **grantDataAccess**(`grantee`): `void` @@ -124,6 +340,132 @@ The role to grant access to. *** +### metric() + +> **metric**(`metricName`, `props`?): `Metric` + +Return the given named metric for this VectorCollection. + +#### Parameters + +##### metricName + +`string` + +The name of the metric + +##### props? + +`MetricOptions` + +Properties for the metric + +#### Returns + +`Metric` + +#### Inherited from + +`VectorCollectionBase.metric` + +*** + +### metricIndexRequestCount() + +> **metricIndexRequestCount**(`props`?): `Metric` + +Metric for the number of index requests. + +#### Parameters + +##### props? + +`MetricOptions` + +Properties for the metric + +#### Returns + +`Metric` + +#### Inherited from + +`VectorCollectionBase.metricIndexRequestCount` + +*** + +### metricSearchLatency() + +> **metricSearchLatency**(`props`?): `Metric` + +Metric for the search latency. + +#### Parameters + +##### props? + +`MetricOptions` + +Properties for the metric + +#### Returns + +`Metric` + +#### Inherited from + +`VectorCollectionBase.metricSearchLatency` + +*** + +### metricSearchLatencyP90() + +> **metricSearchLatencyP90**(`props`?): `Metric` + +Metric for the 90th percentile search latency. + +#### Parameters + +##### props? + +`MetricOptions` + +Properties for the metric + +#### Returns + +`Metric` + +#### Inherited from + +`VectorCollectionBase.metricSearchLatencyP90` + +*** + +### metricSearchRequestCount() + +> **metricSearchRequestCount**(`props`?): `Metric` + +Metric for the number of search requests. + +#### Parameters + +##### props? + +`MetricOptions` + +Properties for the metric + +#### Returns + +`Metric` + +#### Inherited from + +`VectorCollectionBase.metricSearchRequestCount` + +*** + ### toString() > **toString**(): `string` @@ -136,7 +478,39 @@ Returns a string representation of this construct. #### Inherited from -`Construct.toString` +`VectorCollectionBase.toString` + +*** + +### fromCollectionAttributes() + +> `static` **fromCollectionAttributes**(`constructScope`, `constructId`, `attrs`): [`IVectorCollection`](../interfaces/IVectorCollection.md) + +Import an existing collection using its attributes. + +#### Parameters + +##### constructScope + +`Construct` + +The parent creating construct. + +##### constructId + +`string` + +The construct's name. + +##### attrs + +[`VectorCollectionAttributes`](../interfaces/VectorCollectionAttributes.md) + +The collection attributes to use. + +#### Returns + +[`IVectorCollection`](../interfaces/IVectorCollection.md) *** @@ -176,4 +550,124 @@ true if `x` is an object created from a class which extends `Construct`. #### Inherited from -`Construct.isConstruct` +`VectorCollectionBase.isConstruct` + +*** + +### isOwnedResource() + +> `static` **isOwnedResource**(`construct`): `boolean` + +Returns true if the construct was created by CDK, and false otherwise + +#### Parameters + +##### construct + +`IConstruct` + +#### Returns + +`boolean` + +#### Inherited from + +`VectorCollectionBase.isOwnedResource` + +*** + +### isResource() + +> `static` **isResource**(`construct`): `construct is Resource` + +Check whether the given construct is a Resource + +#### Parameters + +##### construct + +`IConstruct` + +#### Returns + +`construct is Resource` + +#### Inherited from + +`VectorCollectionBase.isResource` + +*** + +### metricAll() + +> `static` **metricAll**(`metricName`, `props`?): `Metric` + +Return metrics for all vector collections. + +#### Parameters + +##### metricName + +`string` + +##### props? + +`MetricOptions` + +#### Returns + +`Metric` + +*** + +### metricAllIndexRequestCount() + +> `static` **metricAllIndexRequestCount**(`props`?): `Metric` + +Metric for the total number of index requests across all collections. + +#### Parameters + +##### props? + +`MetricOptions` + +#### Returns + +`Metric` + +*** + +### metricAllSearchLatency() + +> `static` **metricAllSearchLatency**(`props`?): `Metric` + +Metric for average search latency across all collections. + +#### Parameters + +##### props? + +`MetricOptions` + +#### Returns + +`Metric` + +*** + +### metricAllSearchRequestCount() + +> `static` **metricAllSearchRequestCount**(`props`?): `Metric` + +Metric for the total number of search requests across all collections. + +#### Parameters + +##### props? + +`MetricOptions` + +#### Returns + +`Metric` diff --git a/apidocs/namespaces/opensearchserverless/enumerations/VectorCollectionStandbyReplicas.md b/apidocs/namespaces/opensearchserverless/enumerations/VectorCollectionStandbyReplicas.md index d3a6d60c..e942e182 100644 --- a/apidocs/namespaces/opensearchserverless/enumerations/VectorCollectionStandbyReplicas.md +++ b/apidocs/namespaces/opensearchserverless/enumerations/VectorCollectionStandbyReplicas.md @@ -6,14 +6,20 @@ # Enumeration: VectorCollectionStandbyReplicas +Configuration for standby replicas in a vector collection. + ## Enumeration Members ### DISABLED > **DISABLED**: `"DISABLED"` +Disable standby replicas to reduce costs + *** ### ENABLED > **ENABLED**: `"ENABLED"` + +Enable standby replicas for high availability diff --git a/apidocs/namespaces/opensearchserverless/enumerations/VectorCollectionType.md b/apidocs/namespaces/opensearchserverless/enumerations/VectorCollectionType.md new file mode 100644 index 00000000..22b3f0b6 --- /dev/null +++ b/apidocs/namespaces/opensearchserverless/enumerations/VectorCollectionType.md @@ -0,0 +1,36 @@ +[**@cdklabs/generative-ai-cdk-constructs**](../../../README.md) + +*** + +[@cdklabs/generative-ai-cdk-constructs](../../../README.md) / [opensearchserverless](../README.md) / VectorCollectionType + +# Enumeration: VectorCollectionType + +The type of collection. + +## Enumeration Members + +### SEARCH + +> **SEARCH**: `"SEARCH"` + +Search – Full-text search that powers applications in your internal networks (content management systems, legal documents) and internet-facing applications, +such as ecommerce website search and content search. + +*** + +### TIMESERIES + +> **TIMESERIES**: `"TIMESERIES"` + +Time series – The log analytics segment that focuses on analyzing large volumes of semi-structured, +machine-generated data in real-time for operational, security, user behavior, and business insights. + +*** + +### VECTORSEARCH + +> **VECTORSEARCH**: `"VECTORSEARCH"` + +Vector search – Semantic search on vector embeddings that simplifies vector data management and powers machine learning (ML) augmented search experiences and generative AI applications, +such as chatbots, personal assistants, and fraud detection. diff --git a/apidocs/namespaces/opensearchserverless/interfaces/IVectorCollection.md b/apidocs/namespaces/opensearchserverless/interfaces/IVectorCollection.md new file mode 100644 index 00000000..98805f6e --- /dev/null +++ b/apidocs/namespaces/opensearchserverless/interfaces/IVectorCollection.md @@ -0,0 +1,247 @@ +[**@cdklabs/generative-ai-cdk-constructs**](../../../README.md) + +*** + +[@cdklabs/generative-ai-cdk-constructs](../../../README.md) / [opensearchserverless](../README.md) / IVectorCollection + +# Interface: IVectorCollection + +Interface representing a vector collection + +## Extends + +- `IResource` + +## Properties + +### aossPolicy + +> `readonly` **aossPolicy**: `ManagedPolicy` + +An IAM policy that allows API access to the collection. + +*** + +### collectionArn + +> `readonly` **collectionArn**: `string` + +The ARN of the collection. + +*** + +### collectionId + +> `readonly` **collectionId**: `string` + +The ID of the collection. + +*** + +### collectionName + +> `readonly` **collectionName**: `string` + +The name of the collection. + +*** + +### collectionType + +> `readonly` **collectionType**: [`VectorCollectionType`](../enumerations/VectorCollectionType.md) + +Type of collection + +*** + +### dataAccessPolicy + +> `readonly` **dataAccessPolicy**: `CfnAccessPolicy` + +An OpenSearch Access Policy that allows access to the index. + +*** + +### env + +> `readonly` **env**: `ResourceEnvironment` + +The environment this resource belongs to. +For resources that are created and managed by the CDK +(generally, those created by creating new class instances like Role, Bucket, etc.), +this is always the same as the environment of the stack they belong to; +however, for imported resources +(those obtained from static methods like fromRoleArn, fromBucketName, etc.), +that might be different than the stack they were imported into. + +#### Inherited from + +`cdk.IResource.env` + +*** + +### node + +> `readonly` **node**: `Node` + +The tree node. + +#### Inherited from + +`cdk.IResource.node` + +*** + +### stack + +> `readonly` **stack**: `Stack` + +The stack in which this resource is defined. + +#### Inherited from + +`cdk.IResource.stack` + +*** + +### standbyReplicas + +> `readonly` **standbyReplicas**: [`VectorCollectionStandbyReplicas`](../enumerations/VectorCollectionStandbyReplicas.md) + +Indicates whether standby replicas are enabled. + +## Methods + +### applyRemovalPolicy() + +> **applyRemovalPolicy**(`policy`): `void` + +Apply the given removal policy to this resource + +The Removal Policy controls what happens to this resource when it stops +being managed by CloudFormation, either because you've removed it from the +CDK application or because you've made a change that requires the resource +to be replaced. + +The resource can be deleted (`RemovalPolicy.DESTROY`), or left in your AWS +account for data recovery and cleanup later (`RemovalPolicy.RETAIN`). + +#### Parameters + +##### policy + +`RemovalPolicy` + +#### Returns + +`void` + +#### Inherited from + +`cdk.IResource.applyRemovalPolicy` + +*** + +### metric() + +> **metric**(`metricName`, `props`?): `Metric` + +Return the given named metric for this VectorCollection. + +#### Parameters + +##### metricName + +`string` + +The name of the metric + +##### props? + +`MetricOptions` + +Properties for the metric + +#### Returns + +`Metric` + +*** + +### metricIndexRequestCount() + +> **metricIndexRequestCount**(`props`?): `Metric` + +Metric for the number of index requests. + +#### Parameters + +##### props? + +`MetricOptions` + +Properties for the metric + +#### Returns + +`Metric` + +*** + +### metricSearchLatency() + +> **metricSearchLatency**(`props`?): `Metric` + +Metric for the search latency. + +#### Parameters + +##### props? + +`MetricOptions` + +Properties for the metric + +#### Returns + +`Metric` + +*** + +### metricSearchLatencyP90() + +> **metricSearchLatencyP90**(`props`?): `Metric` + +Metric for the 90th percentile search latency. + +#### Parameters + +##### props? + +`MetricOptions` + +Properties for the metric + +#### Returns + +`Metric` + +*** + +### metricSearchRequestCount() + +> **metricSearchRequestCount**(`props`?): `Metric` + +Metric for the number of search requests. + +#### Parameters + +##### props? + +`MetricOptions` + +Properties for the metric + +#### Returns + +`Metric` diff --git a/apidocs/namespaces/opensearchserverless/interfaces/VectorCollectionAttributes.md b/apidocs/namespaces/opensearchserverless/interfaces/VectorCollectionAttributes.md new file mode 100644 index 00000000..275e075b --- /dev/null +++ b/apidocs/namespaces/opensearchserverless/interfaces/VectorCollectionAttributes.md @@ -0,0 +1,49 @@ +[**@cdklabs/generative-ai-cdk-constructs**](../../../README.md) + +*** + +[@cdklabs/generative-ai-cdk-constructs](../../../README.md) / [opensearchserverless](../README.md) / VectorCollectionAttributes + +# Interface: VectorCollectionAttributes + +Attributes for importing an existing vector collection. + +## Properties + +### collectionArn + +> `readonly` **collectionArn**: `string` + +The ARN of the collection + +*** + +### collectionId + +> `readonly` **collectionId**: `string` + +The ID of the collection + +*** + +### collectionName + +> `readonly` **collectionName**: `string` + +The name of the collection + +*** + +### collectionType + +> `readonly` **collectionType**: [`VectorCollectionType`](../enumerations/VectorCollectionType.md) + +The type of collection + +*** + +### standbyReplicas + +> `readonly` **standbyReplicas**: [`VectorCollectionStandbyReplicas`](../enumerations/VectorCollectionStandbyReplicas.md) + +The standby replicas configuration diff --git a/apidocs/namespaces/opensearchserverless/interfaces/VectorCollectionProps.md b/apidocs/namespaces/opensearchserverless/interfaces/VectorCollectionProps.md index aa0476a7..1e72dcff 100644 --- a/apidocs/namespaces/opensearchserverless/interfaces/VectorCollectionProps.md +++ b/apidocs/namespaces/opensearchserverless/interfaces/VectorCollectionProps.md @@ -6,13 +6,36 @@ # Interface: VectorCollectionProps +Properties for configuring the vector collection. + ## Properties -### collectionName +### collectionName? + +> `readonly` `optional` **collectionName**: `string` + +The name of the collection. Must be between 3-32 characters long and contain only +lowercase letters, numbers, and hyphens. + +#### Default + +```ts +- A CDK generated name will be used +``` + +*** + +### collectionType? + +> `readonly` `optional` **collectionType**: [`VectorCollectionType`](../enumerations/VectorCollectionType.md) + +Type of vector collection -> `readonly` **collectionName**: `string` +#### Default -The name of the collection. +```ts +- VECTORSEARCH +``` *** @@ -24,6 +47,14 @@ A user defined IAM policy that allows API access to the collection. *** +### description? + +> `readonly` `optional` **description**: `string` + +Description for the collection + +*** + ### standbyReplicas? > `readonly` `optional` **standbyReplicas**: [`VectorCollectionStandbyReplicas`](../enumerations/VectorCollectionStandbyReplicas.md) @@ -33,5 +64,13 @@ Indicates whether to use standby replicas for the collection. #### Default ```ts -ENABLED +VectorCollectionStandbyReplicas.ENABLED ``` + +*** + +### tags? + +> `readonly` `optional` **tags**: `CfnTag`[] + +A list of tags associated with the inference profile. diff --git a/src/cdk-lib/opensearchserverless/vector-collection.ts b/src/cdk-lib/opensearchserverless/vector-collection.ts index 1b2a3e22..92f8021d 100644 --- a/src/cdk-lib/opensearchserverless/vector-collection.ts +++ b/src/cdk-lib/opensearchserverless/vector-collection.ts @@ -11,26 +11,101 @@ * and limitations under the License. */ import * as cdk from 'aws-cdk-lib'; +import * as cloudwatch from 'aws-cdk-lib/aws-cloudwatch'; import * as iam from 'aws-cdk-lib/aws-iam'; import * as oss from 'aws-cdk-lib/aws-opensearchserverless'; import { Construct } from 'constructs'; import { generatePhysicalNameV2 } from '../../common/helpers/utils'; +/** + * Configuration for standby replicas in a vector collection. + */ export enum VectorCollectionStandbyReplicas { + /** + * Enable standby replicas for high availability + */ ENABLED = 'ENABLED', + + /** + * Disable standby replicas to reduce costs + */ DISABLED = 'DISABLED', } -export interface VectorCollectionProps { +/** + * The type of collection. + */ +export enum VectorCollectionType { /** - * The name of the collection. + * Search – Full-text search that powers applications in your internal networks (content management systems, legal documents) and internet-facing applications, + * such as ecommerce website search and content search. + */ + SEARCH = 'SEARCH', + + /** + * Time series – The log analytics segment that focuses on analyzing large volumes of semi-structured, + * machine-generated data in real-time for operational, security, user behavior, and business insights. + */ + TIMESERIES = 'TIMESERIES', + + /** + * Vector search – Semantic search on vector embeddings that simplifies vector data management and powers machine learning (ML) augmented search experiences and generative AI applications, + * such as chatbots, personal assistants, and fraud detection. + */ + VECTORSEARCH = 'VECTORSEARCH' +} + +/** + * Attributes for importing an existing vector collection. + */ +export interface VectorCollectionAttributes { + /** + * The name of the collection */ readonly collectionName: string; + /** + * The ID of the collection + */ + readonly collectionId: string; + + /** + * The ARN of the collection + */ + readonly collectionArn: string; + + /** + * The standby replicas configuration + */ + readonly standbyReplicas: VectorCollectionStandbyReplicas; + + /** + * The type of collection + */ + readonly collectionType: VectorCollectionType; +} + +/** + * Properties for configuring the vector collection. + */ +export interface VectorCollectionProps { + /** + * The name of the collection. Must be between 3-32 characters long and contain only + * lowercase letters, numbers, and hyphens. + * + * @default - A CDK generated name will be used + */ + readonly collectionName?: string; + + /** + * Description for the collection + */ + readonly description?: string; + /** * Indicates whether to use standby replicas for the collection. * - * @default ENABLED + * @default VectorCollectionStandbyReplicas.ENABLED */ readonly standbyReplicas?: VectorCollectionStandbyReplicas; @@ -38,42 +113,233 @@ export interface VectorCollectionProps { * A user defined IAM policy that allows API access to the collection. */ readonly customAossPolicy?: iam.ManagedPolicy; + + /** + * Type of vector collection + * + * @default - VECTORSEARCH + */ + readonly collectionType?: VectorCollectionType; + + /** + * A list of tags associated with the inference profile. + * */ + readonly tags?: Array; } /** - * Deploys an OpenSearch Serverless Collection to be used as a vector store. - * - * It includes all policies. + * Interface representing a vector collection */ -export class VectorCollection extends Construct { +export interface IVectorCollection extends cdk.IResource { /** * The name of the collection. */ - public collectionName: string; + readonly collectionName: string; /** - * Indicates whether to use standby replicas for the collection. + * The ID of the collection. */ - public standbyReplicas: VectorCollectionStandbyReplicas; + readonly collectionId: string; /** - * The ID of the collection. + * The ARN of the collection. */ - public collectionId: string; + readonly collectionArn: string; + /** - * The ARN of the collection. + * Indicates whether standby replicas are enabled. */ - public collectionArn: string; + readonly standbyReplicas: VectorCollectionStandbyReplicas; /** * An IAM policy that allows API access to the collection. */ - public aossPolicy: iam.ManagedPolicy; + readonly aossPolicy: iam.ManagedPolicy; /** * An OpenSearch Access Policy that allows access to the index. */ - public dataAccessPolicy: oss.CfnAccessPolicy; + readonly dataAccessPolicy: oss.CfnAccessPolicy; + + /** + * Type of collection + */ + readonly collectionType: VectorCollectionType; + + /** + * Return the given named metric for this VectorCollection. + * + * @param metricName The name of the metric + * @param props Properties for the metric + */ + metric(metricName: string, props?: cloudwatch.MetricOptions): cloudwatch.Metric; + + /** + * Metric for the number of search requests. + * + * @param props Properties for the metric + */ + metricSearchRequestCount(props?: cloudwatch.MetricOptions): cloudwatch.Metric; + + /** + * Metric for the number of index requests. + * + * @param props Properties for the metric + */ + metricIndexRequestCount(props?: cloudwatch.MetricOptions): cloudwatch.Metric; + + /** + * Metric for the search latency. + * + * @param props Properties for the metric + */ + metricSearchLatency(props?: cloudwatch.MetricOptions): cloudwatch.Metric; + + /** + * Metric for the 90th percentile search latency. + * + * @param props Properties for the metric + */ + metricSearchLatencyP90(props?: cloudwatch.MetricOptions): cloudwatch.Metric; +} + +/** + * A new or imported vector collection. + */ +abstract class VectorCollectionBase extends cdk.Resource implements IVectorCollection { + public abstract readonly collectionName: string; + public abstract readonly collectionId: string; + public abstract readonly collectionArn: string; + public abstract readonly standbyReplicas: VectorCollectionStandbyReplicas; + public abstract readonly aossPolicy: iam.ManagedPolicy; + public abstract readonly dataAccessPolicy: oss.CfnAccessPolicy; + public abstract readonly collectionType: VectorCollectionType; + + public metric(metricName: string, props?: cloudwatch.MetricOptions): cloudwatch.Metric { + return new cloudwatch.Metric({ + namespace: 'AWS/AOSS', + metricName, + dimensionsMap: { + CollectionId: this.collectionId, + }, + ...props, + }); + } + + public metricSearchRequestCount(props?: cloudwatch.MetricOptions): cloudwatch.Metric { + return this.metric('SearchRequestCount', props); + } + + public metricIndexRequestCount(props?: cloudwatch.MetricOptions): cloudwatch.Metric { + return this.metric('IndexRequestCount', props); + } + + public metricSearchLatency(props?: cloudwatch.MetricOptions): cloudwatch.Metric { + return this.metric('SearchLatency', { statistic: 'Average', ...props }); + } + + public metricSearchLatencyP90(props?: cloudwatch.MetricOptions): cloudwatch.Metric { + return this.metric('SearchLatency', { statistic: 'p90', ...props }); + } +} + +/** + * Provides a vector search collection in Amazon OpenSearch Serverless. + */ +export class VectorCollection extends VectorCollectionBase { + /** + * Return metrics for all vector collections. + */ + public static metricAll(metricName: string, props?: cloudwatch.MetricOptions): cloudwatch.Metric { + return new cloudwatch.Metric({ + namespace: 'AWS/AOSS', + metricName, + statistic: 'Sum', + ...props, + }); + } + + /** + * Metric for the total number of search requests across all collections. + */ + public static metricAllSearchRequestCount(props?: cloudwatch.MetricOptions): cloudwatch.Metric { + return this.metricAll('SearchRequestCount', props); + } + + /** + * Metric for the total number of index requests across all collections. + */ + public static metricAllIndexRequestCount(props?: cloudwatch.MetricOptions): cloudwatch.Metric { + return this.metricAll('IndexRequestCount', props); + } + + /** + * Metric for average search latency across all collections. + */ + public static metricAllSearchLatency(props?: cloudwatch.MetricOptions): cloudwatch.Metric { + return this.metricAll('SearchLatency', { + statistic: 'Average', + ...props, + }); + } + + /** + * Import an existing collection using its attributes. + * @param constructScope The parent creating construct. + * @param constructId The construct's name. + * @param attrs The collection attributes to use. + */ + public static fromCollectionAttributes( + constructScope: Construct, + constructId: string, + attrs: VectorCollectionAttributes, + ): IVectorCollection { + class Import extends VectorCollectionBase { + public readonly collectionArn = attrs.collectionArn; + public readonly collectionId = attrs.collectionId; + public readonly collectionName = attrs.collectionName; + public readonly standbyReplicas = attrs.standbyReplicas; + public readonly collectionType = attrs.collectionType; + public readonly aossPolicy: iam.ManagedPolicy; + public readonly dataAccessPolicy: oss.CfnAccessPolicy; + + constructor(scope: Construct, id: string) { + super(scope, id); + + this.aossPolicy = new iam.ManagedPolicy(this, 'ImportedAOSSPolicy', { + statements: [ + new iam.PolicyStatement({ + effect: iam.Effect.ALLOW, + actions: ['aoss:APIAccessAll'], + resources: [this.collectionArn], + }), + ], + }); + + this.dataAccessPolicy = new oss.CfnAccessPolicy(this, 'ImportedDataAccessPolicy', { + name: generatePhysicalNameV2(this, 'DataAccessPolicy', { maxLength: 32, lower: true }), + type: 'data', + policy: '[]', + }); + } + } + return new Import(constructScope, constructId); + } + + public readonly collectionName: string; + public readonly standbyReplicas: VectorCollectionStandbyReplicas; + public readonly collectionId: string; + public readonly collectionArn: string; + public readonly aossPolicy: iam.ManagedPolicy; + public readonly dataAccessPolicy: oss.CfnAccessPolicy; + public readonly collectionType: VectorCollectionType; + public readonly collectionEndpoint: string; + public readonly dashboardEndpoint: string; + + /** + * Instance of CfnCollection. + */ + private readonly _resource: oss.CfnCollection; /** * An OpenSearch Access Policy document that will become `dataAccessPolicy`. @@ -87,9 +353,11 @@ export class VectorCollection extends Construct { this.collectionName = props?.collectionName ?? generatePhysicalNameV2( this, 'VectorStore', - { maxLength: 32, lower: true }); + { maxLength: 32, lower: true }, + ); this.standbyReplicas = props?.standbyReplicas ?? VectorCollectionStandbyReplicas.ENABLED; + this.collectionType = props?.collectionType ?? VectorCollectionType.VECTORSEARCH; const encryptionPolicyName = generatePhysicalNameV2(this, 'EncryptionPolicy', @@ -131,14 +399,18 @@ export class VectorCollection extends Construct { ]), }); - const collection = new oss.CfnCollection(this, 'VectorCollection', { + this._resource = new oss.CfnCollection(this, 'VectorCollection', { name: this.collectionName, - type: 'VECTORSEARCH', + type: this.collectionType, standbyReplicas: this.standbyReplicas, + description: props?.description, + tags: props?.tags, }); - this.collectionArn = collection.attrArn; - this.collectionId = collection.attrId; + this.collectionArn = this._resource.attrArn; + this.collectionId = this._resource.attrId; + this.collectionEndpoint = this._resource.attrCollectionEndpoint; + this.dashboardEndpoint = this._resource.attrDashboardEndpoint; if (props?.customAossPolicy) { this.aossPolicy = props.customAossPolicy; @@ -152,16 +424,15 @@ export class VectorCollection extends Construct { actions: [ 'aoss:APIAccessAll', ], - resources: [collection.attrArn], + resources: [this._resource.attrArn], }), ], }, ); } - collection.addDependency(encryptionPolicy); - collection.addDependency(networkPolicy); - + this._resource.node.addDependency(encryptionPolicy); + this._resource.node.addDependency(networkPolicy); const isDataAccessPolicyNotEmpty = new cdk.CfnCondition(this, 'IsDataAccessPolicyNotEmpty', { expression: cdk.Fn.conditionNot(cdk.Fn.conditionEquals(0, cdk.Lazy.number({ @@ -172,6 +443,7 @@ export class VectorCollection extends Construct { const dataAccessPolicyName = generatePhysicalNameV2(this, 'DataAccessPolicy', { maxLength: 32, lower: true }); + this.dataAccessPolicy = new oss.CfnAccessPolicy(this, 'DataAccessPolicy', { name: dataAccessPolicyName, type: 'data', @@ -179,13 +451,33 @@ export class VectorCollection extends Construct { produce: () => JSON.stringify(this.dataAccessPolicyDocument), }), }); + this.dataAccessPolicy.cfnOptions.condition = isDataAccessPolicyNotEmpty; - } + this.node.addValidation({ + validate: () => { + const errors: string[] = []; + + if (this.collectionName) { + if (!/^[a-z0-9-]+$/.test(this.collectionName)) { + errors.push('Collection name must contain only lowercase letters, numbers, and hyphens'); + } + if (this.collectionName.length < 3 || this.collectionName.length > 32) { + errors.push('Collection name must be between 3 and 32 characters'); + } + } + + return errors; + }, + }); + + cdk.Tags.of(this).add('Name', this.collectionName); + cdk.Tags.of(this).add('Type', 'VectorCollection'); + } /** - * Grants the specified role access to data in the collection. - * @param grantee The role to grant access to. - */ + * Grants the specified role access to data in the collection. + * @param grantee The role to grant access to. + */ grantDataAccess(grantee: iam.IRole) { this.dataAccessPolicyDocument.push({ Rules: [ @@ -218,4 +510,3 @@ export class VectorCollection extends Construct { grantee.addManagedPolicy(this.aossPolicy); } } - diff --git a/test/cdk-lib/opensearchserverless/vector-collection.test.ts b/test/cdk-lib/opensearchserverless/vector-collection.test.ts index db856bbd..e42020d1 100644 --- a/test/cdk-lib/opensearchserverless/vector-collection.test.ts +++ b/test/cdk-lib/opensearchserverless/vector-collection.test.ts @@ -15,7 +15,7 @@ import * as cdk from 'aws-cdk-lib'; import { Annotations, Match, Template } from 'aws-cdk-lib/assertions'; import * as iam from 'aws-cdk-lib/aws-iam'; import { AwsSolutionsChecks } from 'cdk-nag'; -import { VectorCollection, VectorCollectionStandbyReplicas } from '../../../src/cdk-lib/opensearchserverless'; +import { VectorCollection, VectorCollectionStandbyReplicas, VectorCollectionType } from '../../../src/cdk-lib/opensearchserverless'; function setupStack() { @@ -180,4 +180,301 @@ describe('OpenSearch Serverless Vector Store', () => { expect(errors).toHaveLength(0); }); }); + + describe('Static Methods', () => { + let app: cdk.App; + let stack: cdk.Stack; + let template: Template; + + beforeEach(() => { + app = new cdk.App(); + cdk.Aspects.of(app).add(new AwsSolutionsChecks()); + stack = new cdk.Stack(app, 'test-stack', { + env: { + account: '123456789012', + region: 'us-east-1', + }, + }); + + VectorCollection.fromCollectionAttributes(stack, 'ImportedAttributes', { + collectionName: 'test-collection-2', + collectionId: 'test-id', + collectionArn: 'arn:aws:aoss:us-east-1:123456789012:collection/test-collection-2', + standbyReplicas: VectorCollectionStandbyReplicas.DISABLED, + collectionType: VectorCollectionType.VECTORSEARCH, + }); + + app.synth(); + template = Template.fromStack(stack); + }); + + test('Should have the correct resources for imported collections', () => { + template.resourceCountIs('AWS::IAM::ManagedPolicy', 1); + template.resourceCountIs('AWS::OpenSearchServerless::AccessPolicy', 1); + }); + + test('No unsuppressed Errors', () => { + const errors = Annotations.fromStack(stack).findError( + '*', + Match.stringLikeRegexp('AwsSolutions-.*'), + ); + expect(errors).toHaveLength(0); + }); + }); + + describe('Network and Encryption Policies', () => { + let app: cdk.App; + let stack: cdk.Stack; + + beforeEach(() => { + app = new cdk.App(); + cdk.Aspects.of(app).add(new AwsSolutionsChecks()); + stack = new cdk.Stack(app, 'test-stack', { + env: { + account: '123456789012', + region: 'us-east-1', + }, + }); + + app.synth(); + }); + + test('No unsuppressed Errors', () => { + const errors = Annotations.fromStack(stack).findError( + '*', + Match.stringLikeRegexp('AwsSolutions-.*'), + ); + expect(errors).toHaveLength(0); + }); + }); + + describe('Validation and Defaults', () => { + let stack: cdk.Stack; + + beforeEach(() => { + const app = new cdk.App(); + cdk.Aspects.of(app).add(new AwsSolutionsChecks()); + stack = new cdk.Stack(app, 'test-stack', { + env: { + account: '123456789012', + region: 'us-east-1', + }, + }); + }); + + test('Should validate collection name length', () => { + // Testing short name + const shortNameCollection = new VectorCollection(stack, 'TestShortName', { + collectionName: 'ab', + }); + + expect(() => { + const errors = shortNameCollection.node.validate(); + if (errors.length > 0) { + throw new Error(errors[0]); + } + }).toThrow(/Collection name must be between 3 and 32 characters/); + + // Testing long name + const longNameCollection = new VectorCollection(stack, 'TestLongName', { + collectionName: 'a'.repeat(33), + }); + + expect(() => { + const errors = longNameCollection.node.validate(); + if (errors.length > 0) { + throw new Error(errors[0]); + } + }).toThrow(/Collection name must be between 3 and 32 characters/); + }); + + test('Should validate collection name characters', () => { + const invalidCharsCollection = new VectorCollection(stack, 'TestInvalidChars', { + collectionName: 'Invalid_Name', + }); + + expect(() => { + const errors = invalidCharsCollection.node.validate(); + if (errors.length > 0) { + throw new Error(errors[0]); + } + }).toThrow(/Collection name must contain only lowercase letters, numbers, and hyphens/); + }); + + test('Should use default values when props are not provided', () => { + // Create a fresh app and stack for this test to avoid validation errors from other tests + const app = new cdk.App(); + const testStack = new cdk.Stack(app, 'test-defaults-stack', { + env: { + account: '123456789012', + region: 'us-east-1', + }, + }); + + const defaultVector = new VectorCollection(testStack, 'TestDefaultsVector'); + + expect(defaultVector.standbyReplicas).toBe(VectorCollectionStandbyReplicas.ENABLED); + expect(defaultVector.collectionName).toMatch(/^vectorstore[a-z0-9]+$/); + + const template = Template.fromStack(testStack); + + template.hasResourceProperties('AWS::OpenSearchServerless::Collection', { + Type: 'VECTORSEARCH', + StandbyReplicas: 'ENABLED', + }); + + // Verify security policies are created + template.resourceCountIs('AWS::OpenSearchServerless::SecurityPolicy', 2); + template.hasResourceProperties('AWS::OpenSearchServerless::SecurityPolicy', { + Type: 'network', + }); + template.hasResourceProperties('AWS::OpenSearchServerless::SecurityPolicy', { + Type: 'encryption', + }); + }); + }); + + describe('Alarms and Metrics', () => { + let app: cdk.App; + let stack: cdk.Stack; + + beforeEach(() => { + app = new cdk.App(); + cdk.Aspects.of(app).add(new AwsSolutionsChecks()); + stack = new cdk.Stack(app, 'test-stack', { + env: { + account: '123456789012', + region: 'us-east-1', + }, + }); + + app.synth(); + }); + + test('Should create static metrics with correct properties', () => { + const customMetric = VectorCollection.metricAll('CustomMetric', { + statistic: 'Maximum', + period: cdk.Duration.minutes(5), + }); + + expect(customMetric.namespace).toBe('AWS/AOSS'); + expect(customMetric.metricName).toBe('CustomMetric'); + expect(customMetric.statistic).toBe('Maximum'); + expect(customMetric.period!.toMinutes()).toBe(5); + + const searchRequestMetric = VectorCollection.metricAllSearchRequestCount({ + period: cdk.Duration.minutes(1), + }); + + expect(searchRequestMetric.namespace).toBe('AWS/AOSS'); + expect(searchRequestMetric.metricName).toBe('SearchRequestCount'); + expect(searchRequestMetric.statistic).toBe('Sum'); + expect(searchRequestMetric.period!.toMinutes()).toBe(1); + + const indexRequestMetric = VectorCollection.metricAllIndexRequestCount({ + period: cdk.Duration.minutes(1), + }); + + expect(indexRequestMetric.namespace).toBe('AWS/AOSS'); + expect(indexRequestMetric.metricName).toBe('IndexRequestCount'); + expect(indexRequestMetric.statistic).toBe('Sum'); + expect(indexRequestMetric.period!.toMinutes()).toBe(1); + + const searchLatencyMetric = VectorCollection.metricAllSearchLatency({ + period: cdk.Duration.minutes(1), + }); + + expect(searchLatencyMetric.namespace).toBe('AWS/AOSS'); + expect(searchLatencyMetric.metricName).toBe('SearchLatency'); + expect(searchLatencyMetric.statistic).toBe('Average'); + expect(searchLatencyMetric.period!.toMinutes()).toBe(1); + }); + + test('No unsuppressed Errors', () => { + const errors = Annotations.fromStack(stack).findError( + '*', + Match.stringLikeRegexp('AwsSolutions-.*'), + ); + expect(errors).toHaveLength(0); + }); + }); }); + +describe('OpenSearch Serverless optional props', () => { + let stack: cdk.Stack; + + beforeEach(() => { + const app = new cdk.App(); + cdk.Aspects.of(app).add(new AwsSolutionsChecks()); + stack = new cdk.Stack(app, 'TestStack'); + }); + + test('Basic Creation with type TIMESERIES', () => { + + const collectionName = 'test-aoss-collection'; + const standbyReplicas = VectorCollectionStandbyReplicas.DISABLED; + const collectionType = VectorCollectionType.TIMESERIES; + + new VectorCollection(stack, 'test-aoss-vector', { + collectionName: collectionName, + standbyReplicas: standbyReplicas, + description: 'Test description', + collectionType: collectionType, + tags: [{ key: 'test-key', value: 'test-value' }], + }); + + Template.fromStack(stack).hasResourceProperties('AWS::OpenSearchServerless::Collection', { + StandbyReplicas: 'DISABLED', + Description: 'Test description', + Tags: [ + { + Key: 'Name', // added by the construct + Value: collectionName, + }, + { + Key: 'test-key', // custom added + Value: 'test-value', + }, + { + Key: 'Type', // added by the construct + Value: 'VectorCollection', + }, + ], + Type: 'TIMESERIES', + }); + }); + + test('Basic Creation with type SEARCH', () => { + + const collectionName = 'test-aoss-collection'; + const standbyReplicas = VectorCollectionStandbyReplicas.ENABLED; + const collectionType = VectorCollectionType.SEARCH; + + new VectorCollection(stack, 'test-aoss-vector', { + collectionName: collectionName, + standbyReplicas: standbyReplicas, + collectionType: collectionType, + tags: [{ key: 'test-key', value: 'test-value' }], + }); + + Template.fromStack(stack).hasResourceProperties('AWS::OpenSearchServerless::Collection', { + StandbyReplicas: 'ENABLED', + Description: Match.absent(), + Tags: [ + { + Key: 'Name', // added by the construct + Value: collectionName, + }, + { + Key: 'test-key', // custom added + Value: 'test-value', + }, + { + Key: 'Type', // added by the construct + Value: 'VectorCollection', + }, + ], + Type: 'SEARCH', + }); + }); +}); \ No newline at end of file