diff --git a/apidocs/namespaces/opensearchserverless/README.md b/apidocs/namespaces/opensearchserverless/README.md index efe6bbab..ab4d4ff1 100644 --- a/apidocs/namespaces/opensearchserverless/README.md +++ b/apidocs/namespaces/opensearchserverless/README.md @@ -21,4 +21,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 14f9d504..20efc285 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 @@ -34,47 +32,75 @@ 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` *** ### collectionId -> **collectionId**: `string` +> `readonly` **collectionId**: `string` -The ID of the collection. +#### Overrides + +`VectorCollectionBase.collectionId` *** ### collectionName -> **collectionName**: `string` +> `readonly` **collectionName**: `string` + +#### Overrides -The name of the collection. +`VectorCollectionBase.collectionName` *** ### dataAccessPolicy -> **dataAccessPolicy**: `CfnAccessPolicy` +> `readonly` **dataAccessPolicy**: `CfnAccessPolicy` + +#### Overrides + +`VectorCollectionBase.dataAccessPolicy` + +*** + +### env + +> `readonly` **env**: `ResourceEnvironment` -An OpenSearch Access Policy that allows access to the index. +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` *** @@ -86,18 +112,236 @@ 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) -Indicates whether to use standby replicas for the collection. +#### Overrides + +`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` + +*** + +### allowAccessFrom() + +> **allowAccessFrom**(...`ipRanges`): `void` + +Updates the network policy to allow access from specific IP ranges. + +#### Parameters + +• ...**ipRanges**: `string`[] + +The IP ranges to allow access from + +#### Returns + +`void` + +*** + +### 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` + +*** + +### createErrorRateAlarm() + +> **createErrorRateAlarm**(`props`?): `Alarm` + +Creates an alarm for high error rates. + +#### Parameters + +• **props?**: `CreateAlarmOptions` + +Properties for the alarm + +#### Returns + +`Alarm` + +The created alarm + +*** + +### createHighLatencyAlarm() + +> **createHighLatencyAlarm**(`props`?): `Alarm` + +Creates an alarm for high search latency. + +#### Parameters + +• **props?**: `CreateAlarmOptions` + +Properties for the alarm + +#### Returns + +`Alarm` + +The created alarm + +*** + +### 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` @@ -116,6 +360,120 @@ 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` @@ -128,7 +486,77 @@ Returns a string representation of this construct. #### Inherited from -`Construct.toString` +`VectorCollectionBase.toString` + +*** + +### useCustomerManagedKey() + +> **useCustomerManagedKey**(`kmsKeyArn`): `void` + +Updates the encryption policy to use a customer managed KMS key. + +#### Parameters + +• **kmsKeyArn**: `string` + +The ARN of the KMS key to use + +#### Returns + +`void` + +*** + +### 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) + +*** + +### fromCollectionName() + +> `static` **fromCollectionName**(`constructScope`, `constructId`, `collectionName`): [`IVectorCollection`](../interfaces/IVectorCollection.md) + +Import an existing collection using its name. + +#### Parameters + +• **constructScope**: `Construct` + +The parent creating construct (usually `this`). + +• **constructId**: `string` + +The construct's name. + +• **collectionName**: `string` + +The name of the collection to import. + +#### Returns + +[`IVectorCollection`](../interfaces/IVectorCollection.md) *** @@ -166,4 +594,110 @@ 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 999bdb91..fd190638 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/interfaces/IVectorCollection.md b/apidocs/namespaces/opensearchserverless/interfaces/IVectorCollection.md new file mode 100644 index 00000000..71f114df --- /dev/null +++ b/apidocs/namespaces/opensearchserverless/interfaces/IVectorCollection.md @@ -0,0 +1,225 @@ +[**@cdklabs/generative-ai-cdk-constructs**](../../../README.md) • **Docs** + +*** + +[@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. + +*** + +### 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..7ceb9498 --- /dev/null +++ b/apidocs/namespaces/opensearchserverless/interfaces/VectorCollectionAttributes.md @@ -0,0 +1,41 @@ +[**@cdklabs/generative-ai-cdk-constructs**](../../../README.md) • **Docs** + +*** + +[@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 + +*** + +### 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 9fbde384..58bf8d2e 100644 --- a/apidocs/namespaces/opensearchserverless/interfaces/VectorCollectionProps.md +++ b/apidocs/namespaces/opensearchserverless/interfaces/VectorCollectionProps.md @@ -6,13 +6,22 @@ # 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. -> `readonly` **collectionName**: `string` +#### Default -The name of the collection. +```ts +- A CDK generated name will be used +``` *** @@ -25,5 +34,5 @@ Indicates whether to use standby replicas for the collection. #### Default ```ts -ENABLED +VectorCollectionStandbyReplicas.ENABLED ``` diff --git a/src/cdk-lib/opensearchserverless/vector-collection.ts b/src/cdk-lib/opensearchserverless/vector-collection.ts index cc74c332..9bac0998 100644 --- a/src/cdk-lib/opensearchserverless/vector-collection.ts +++ b/src/cdk-lib/opensearchserverless/vector-collection.ts @@ -11,64 +11,317 @@ * 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 { +/** + * Attributes for importing an existing vector collection. + */ +export interface VectorCollectionAttributes { /** - * The name of the collection. + * 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; +} + +/** + * 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; + /** * Indicates whether to use standby replicas for the collection. * - * @default ENABLED + * @default VectorCollectionStandbyReplicas.ENABLED */ readonly standbyReplicas?: VectorCollectionStandbyReplicas; } /** - * 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; + + /** + * 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 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 name. + * @param constructScope The parent creating construct (usually `this`). + * @param constructId The construct's name. + * @param collectionName The name of the collection to import. + */ + public static fromCollectionName(constructScope: Construct, constructId: string, collectionName: string): IVectorCollection { + class Import extends VectorCollectionBase { + public readonly collectionArn: string; + public readonly collectionId: string; + public readonly collectionName: string; + public readonly standbyReplicas: VectorCollectionStandbyReplicas; + public readonly aossPolicy: iam.ManagedPolicy; + public readonly dataAccessPolicy: oss.CfnAccessPolicy; + + constructor(scope: Construct, id: string) { + super(scope, id); + + this.collectionId = collectionName; + this.collectionName = collectionName; + this.collectionArn = cdk.Stack.of(scope).formatArn({ + service: 'aoss', + resource: 'collection', + resourceName: collectionName, + }); + this.standbyReplicas = VectorCollectionStandbyReplicas.ENABLED; + + 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); + } + + /** + * 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 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; /** * An OpenSearch Access Policy document that will become `dataAccessPolicy`. @@ -82,7 +335,8 @@ 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; @@ -150,9 +404,8 @@ export class VectorCollection extends Construct { }, ); - collection.addDependency(encryptionPolicy); - collection.addDependency(networkPolicy); - + collection.node.addDependency(encryptionPolicy); + collection.node.addDependency(networkPolicy); const isDataAccessPolicyNotEmpty = new cdk.CfnCondition(this, 'IsDataAccessPolicyNotEmpty', { expression: cdk.Fn.conditionNot(cdk.Fn.conditionEquals(0, cdk.Lazy.number({ @@ -163,6 +416,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', @@ -170,13 +424,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: [ @@ -209,4 +483,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..6fc335bc 100644 --- a/test/cdk-lib/opensearchserverless/vector-collection.test.ts +++ b/test/cdk-lib/opensearchserverless/vector-collection.test.ts @@ -180,4 +180,222 @@ 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.fromCollectionName(stack, 'ImportedCollection', 'test-collection'); + 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, + }); + + app.synth(); + template = Template.fromStack(stack); + }); + + test('Should have the correct resources for imported collections', () => { + template.resourceCountIs('AWS::IAM::ManagedPolicy', 2); + template.resourceCountIs('AWS::OpenSearchServerless::AccessPolicy', 2); + }); + + 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); + }); + }); });