Knowledge Bases for Amazon Bedrock (with OpenSearch Serverless)をSAMで実装してみた
こんにちは、つくぼし(tsukuboshi0755)です!
Knowledge Bases for Amazon Bedrockが東京リージョンにやってきた事で、RAG構成の候補としてKnowledge Basesをより使いやすくなったかと思います。
以前Knowledge Basesを用いたRAG構成として、ベクターストアにAuroraを利用したパターンをCloudFormationで作成しました。
ただ多くの方々は、Knowledge Basesコンソールのクイック作成でも用いられる、ベクターストアにOpenSearch Serverlessを利用したパターンで作成する事が多いのではないでしょうか。
そこで今回は、ベクターストアにOpenSearch Serverlessを利用したパターンをSAMで実装してみたので、紹介したいと思います!
なおインデックスのセットアップの実施で一部カスタムリソースを含む形になるため、その点にご留意頂けるとありがたいです。
構成
今回SAMで作成する構成図は以下のとおりです。
Knowledge Bases for Amazon Bedrockに必要なリソースとして、以下のリソースも合わせて作成します。
- ベクターストア:OpenSearch Serverless
- データソース:S3
またLambda-Backed Custom Resourceを利用して、OpenSearch Serverlessに対するインデックスのセットアップを自動化します。
テンプレート
全体のコードは、以下のリポジトリに格納しています。
以下では、テンプレートの主要な部分を抜粋して説明します。
OpenSearch Serverless
OSSCollection: Type: 'AWS::OpenSearchServerless::Collection' Properties: Name: !Sub '${AWS::StackName}-col' Type: VECTORSEARCH StandbyReplicas: !Ref OSSCollectionStandbyReplicas DependsOn: OSSEncryptionPolicy OSSEncryptionPolicy: Type: 'AWS::OpenSearchServerless::SecurityPolicy' Properties: Name: !Sub '${AWS::StackName}-ep' Type: encryption Policy: !Sub >- { "Rules": [ { "Resource": [ "collection/${AWS::StackName}-col" ], "ResourceType": "collection" } ], "AWSOwnedKey": true } OSSNetworkPolicy: Type: 'AWS::OpenSearchServerless::SecurityPolicy' Properties: Name: !Sub '${AWS::StackName}-np' Type: network Policy: !Sub >- [ { "Rules": [ { "Resource": [ "collection/${AWS::StackName}-col" ], "ResourceType": "dashboard" }, { "Resource": [ "collection/${AWS::StackName}-col" ], "ResourceType": "collection" } ], "AllowFromPublic": true } ] OSSDataAccessPolicy: Type: 'AWS::OpenSearchServerless::AccessPolicy' Properties: Name: !Sub '${AWS::StackName}-dp' Type: data Policy: !Sub - >- [ { "Rules": [ { "Resource": [ "collection/${AWS::StackName}-col" ], "Permission": [ "aoss:CreateCollectionItems", "aoss:UpdateCollectionItems", "aoss:DescribeCollectionItems" ], "ResourceType": "collection" }, { "Resource": [ "index/${AWS::StackName}-col/*" ], "Permission": [ "aoss:CreateIndex", "aoss:UpdateIndex", "aoss:DescribeIndex", "aoss:ReadDocument", "aoss:WriteDocument" ], "ResourceType": "index" } ], "Principal": [ "${BedrockKnowledgeBaseRoleArn}", "${CreateIndexFunctionRoleArn}", "${OSSDataAccessPrincipalArn}" ], "Description": "" } ] - BedrockKnowledgeBaseRoleArn: !GetAtt BedrockKnowledgeBaseRole.Arn CreateIndexFunctionRoleArn: !GetAtt CreateIndexFunctionRole.Arn
Knowledge Base for Amazon Bedrockのベクターストアとして必要なOpenSearch Serverlessを作成します。
今回のOpenSearch Serverlessは基本的に以下を参照して作成しているため、詳細についてはこちらのの記事をご参照ください。
なお上記のブログからの変更点として、データアクセスポリシーに対して後述のKnowledge Bases及びインデックスセットアップ関数に対するロールにも、アクセス権限を付与するようにしています。
S3 Bucket
DataSourceBucket: Type: AWS::S3::Bucket Properties: BucketName: !Sub '${AWS::StackName}-ds-bucket-${AWS::AccountId}' PublicAccessBlockConfiguration: BlockPublicAcls: true BlockPublicPolicy: true IgnorePublicAcls: true RestrictPublicBuckets: true BucketEncryption: ServerSideEncryptionConfiguration: - ServerSideEncryptionByDefault: SSEAlgorithm: AES256 BucketKeyEnabled: true OwnershipControls: Rules: - ObjectOwnership: BucketOwnerEnforced
Knowledge Base for Amazon Bedrockのデータソースとして必要なS3バケットを作成します。
このバケットに対してユーザーがドキュメントをアップロードする事で、Knowledge Base for Amazon Bedrockがデータソースとして利用できるようになります。
IAM Policy
BedrockFMPolicyForKB: Type: AWS::IAM::ManagedPolicy Properties: ManagedPolicyName: !Sub ${AWS::StackName}-bedrock-fm-policy PolicyDocument: Version: '2012-10-17' Statement: - Sid: BedrockInvokeModelStatement Effect: Allow Action: - 'bedrock:InvokeModel' Resource: !Sub arn:aws:bedrock:${AWS::Region}::foundation-model/${EmbeddingModelId} Roles: - !Ref BedrockKnowledgeBaseRole BedrockOSSPolicyForKB: Type: AWS::IAM::ManagedPolicy Properties: ManagedPolicyName: !Sub ${AWS::StackName}-bedrock-oss-policy PolicyDocument: Version: '2012-10-17' Statement: - Sid: OpenSearchServerlessAPIAccessAllStatement Effect: Allow Action: - 'aoss:APIAccessAll' Resource: !GetAtt OSSCollection.Arn Roles: - !Ref BedrockKnowledgeBaseRole - !Ref CreateIndexFunctionRole BedrockS3PolicyForKB: Type: AWS::IAM::ManagedPolicy Properties: ManagedPolicyName: !Sub ${AWS::StackName}-bedrock-s3-policy PolicyDocument: Version: '2012-10-17' Statement: - Sid: S3ListBucketStatement Effect: Allow Action: - 's3:ListBucket' Resource: !GetAtt DataSourceBucket.Arn Condition: StringEquals: aws:ResourceAccount: !Ref 'AWS::AccountId' - Sid: S3GetObjectStatement Effect: Allow Action: - 's3:GetObject' Resource: - !Join - '' - - 'arn:aws:s3:::' - !Ref DataSourceBucket - '/*' Condition: StringEquals: aws:ResourceAccount: !Ref 'AWS::AccountId' Roles: - !Ref BedrockKnowledgeBaseRole
Lambda-Backed Custom Resource及びKnowledge Base for Amazon Bedrockに必要なIAMポリシーを作成します。
Bedrock、Secrets Manager、OpenSearch Serverless、S3に対するアクセス権限を設定し、IAMロールにアタッチします。
Lambda-Backed Custom Resource
- SAMテンプレート
SetupOSSIndex: Type: 'Custom::OpenSearchServerlessIndex' Properties: ServiceToken: !GetAtt CreateIndexFunction.Arn Region: !Ref 'AWS::Region' Dimension: !If [IsTitanEmbedTextV1, 1536, 1024] EmbeddingModelId: !Ref EmbeddingModelId CollectionId: !GetAtt OSSCollection.Id IndexName: !FindInMap [IndexMap, OSSIndexName, Name] VectorField: !FindInMap [IndexMap, OSSVectorField, Name] MappingText: !FindInMap [IndexMap, OSSMappingText, Name] MappingMetadata: !FindInMap [IndexMap, OSSMappingMetadata, Name] DependsOn: - BedrockOSSPolicyForKB CreateIndexFunctionRole: Type: 'AWS::IAM::Role' Properties: RoleName: !Sub ${AWS::StackName}-create-index-function-role AssumeRolePolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Principal: Service: lambda.amazonaws.com Action: 'sts:AssumeRole' ManagedPolicyArns: - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole CreateIndexFunction: Type: AWS::Serverless::Function Properties: FunctionName: !Sub '${AWS::StackName}-create-index-function' Handler: index.lambda_handler Role: !GetAtt CreateIndexFunctionRole.Arn Runtime: python3.12 Timeout: 600 LoggingConfig: LogFormat: JSON ApplicationLogLevel: INFO SystemLogLevel: INFO Layers: - !Ref OSSLayer CodeUri: function/ OSSLayer: Type: AWS::Serverless::LayerVersion Properties: LayerName: !Sub '${AWS::StackName}-create-index-function-layer' ContentUri: layer/ CompatibleRuntimes: - python3.12 Metadata: BuildMethod: python3.12
- Lambdaコード
import logging import time from typing import Any, Dict import boto3 import cfnresponse from opensearchpy import AWSV4SignerAuth, OpenSearch, RequestsHttpConnection logger = logging.getLogger() def lambda_handler(event: Dict[str, Any], context: Any) -> None: request_type = event["RequestType"] logger.info(f"RequestType: {request_type}") try: if request_type == "Create": # Get the parameters from the event region = event["ResourceProperties"]["Region"] dimension = event["ResourceProperties"]["Dimension"] colection_id = event["ResourceProperties"]["CollectionId"] index_name = event["ResourceProperties"]["IndexName"] vector_field = event["ResourceProperties"]["VectorField"] mapping_text = event["ResourceProperties"]["MappingText"] mapping_metadata = event["ResourceProperties"]["MappingMetadata"] # Create OpenSearch client service = "aoss" host = colection_id + "." + region + "." + service + "." + "amazonaws.com" credentials = boto3.Session().get_credentials() auth = AWSV4SignerAuth(credentials, region, service) client = OpenSearch( hosts=[{"host": host, "port": 443}], http_auth=auth, use_ssl=True, verify_certs=True, connection_class=RequestsHttpConnection, pool_maxsize=20, ) # Create OpenSearch Index (Wait for 30 seconds for index creation) index_body = { "settings": { "index": { "number_of_shards": "2", "number_of_replicas": "0", "knn": "true", } }, "mappings": { "properties": { vector_field: { "type": "knn_vector", "dimension": dimension, "method": { "name": "hnsw", "space_type": "l2", "engine": "faiss", "parameters": {}, }, }, mapping_text: { "type": "text", }, mapping_metadata: {"type": "text", "index": "false"}, } }, } response = client.indices.create( index_name, body=index_body, ) logger.info(f"Index creating: {response}") time.sleep(30) cfnresponse.send(event, context, cfnresponse.SUCCESS, {}) if request_type == "Update": cfnresponse.send(event, context, cfnresponse.SUCCESS, {}) if request_type == "Delete": cfnresponse.send(event, context, cfnresponse.SUCCESS, {}) except Exception as e: cfnresponse.send(event, context, cfnresponse.FAILED, {"Message": str(e)})
今回のテンプレートの肝となる箇所です。
Knowledge Base for Amazon BedrockのベクターストアとしてOpenSearch Serverlessを利用する際、別途インデックスのセットアップが必要となります。
こちらはCloudFormationでは対応していないため、カスタムリソースを使用してリクエストタイプがCreateとなる時のみ、OpenSearch Serverlessに対してOpenSearch APIをLambda関数で実行する事で、インデックスのセットアップを自動化します。
カスタムリソース及びリクエストタイプについては、以下の記事をご参照ください。
また今回はOpenSearch APIとしてopensearch-py
モジュールを使用しています。
そのためrequirements.txt
にopensearch-py
を記載し、Lambda関数のデプロイ時にレイヤーとして追加しています。
opensearch-py
については、以下の公式ドキュメントをご参照ください。
カスタムリソースとして定義したLambda関数では、以下のセットアップ処理についてOpenSearch APIを実行します。
- コレクションへの接続
- ベクターストア用のインデックス作成
カスタムリソースによりインデックスが正常に作成されているかどうかを確認するには、OpenSearchコンソールを使用します。
作成したコレクションに対して、以下のようなインデックスが確認できればOKです。
またコンソールだけでなく、awscurl
またはcurl
コマンド等を用いる事で、インデックスの詳細設定を確認できます。
CloudShell等の環境で簡単に確認できるため、必要に応じてご参照ください。
# エンドポイントの指定 AOSS_ENDPOINT=<OpenSearch Serverlessのエンドポイント> INDEX_NAME=bedrock-knowledge-base-default-index # awscurlのインストール pip install awscurl # インデックスの確認 awscurl --service aoss --region ${AWS_REGION} \ -X GET \ ${AOSS_ENDPOINT}/${INDEX_NAME} | jq -r .
上記を実施した際に、以下のようなインデックスの詳細設定が確認できればOKです。
{ "bedrock-knowledge-base-default-index": { "aliases": {}, "mappings": { "properties": { "AMAZON_BEDROCK_METADATA": { "type": "text", "index": false }, "AMAZON_BEDROCK_TEXT_CHUNK": { "type": "text" }, "bedrock-knowledge-base-default-vector": { "type": "knn_vector", "dimension": 1024, // ベクトルの次元数 1536 or 1024 "method": { "engine": "faiss", "space_type": "l2", "name": "hnsw", "parameters": {} } } } }, "settings": { "index": { "number_of_shards": "2", "provided_name": "bedrock-knowledge-base-default-index", "knn": "true", "creation_date": "XXXXXXXXXXXXX", "number_of_replicas": "0", "uuid": "xxxxxxxxxxxxxxxxxxxx", "version": { "created": "XXXXXXXXX" } } } } }
Knowledge Base for Amazon Bedrock
BedrockKnowledgeBaseRole: Type: AWS::IAM::Role Properties: RoleName: !Sub ${AWS::StackName}-bedrock-kb-role AssumeRolePolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Principal: Service: [bedrock.amazonaws.com] Action: ['sts:AssumeRole'] BedrockKnowledgeBase: Type: AWS::Bedrock::KnowledgeBase Properties: Name: !Sub ${AWS::StackName}-knowledge-base KnowledgeBaseConfiguration: Type: VECTOR VectorKnowledgeBaseConfiguration: EmbeddingModelArn: !Sub arn:aws:bedrock:${AWS::Region}::foundation-model/${EmbeddingModelId} RoleArn: !GetAtt BedrockKnowledgeBaseRole.Arn StorageConfiguration: Type: OPENSEARCH_SERVERLESS OpensearchServerlessConfiguration: CollectionArn: !GetAtt OSSCollection.Arn FieldMapping: MetadataField: !FindInMap [IndexMap, OSSMappingMetadata, Name] TextField: !FindInMap [IndexMap, OSSMappingText, Name] VectorField: !FindInMap [IndexMap, OSSVectorField, Name] VectorIndexName: !FindInMap [IndexMap, OSSIndexName, Name] DependsOn: SetupOSSIndex BedrockKnowledgeBaseDS: Type: AWS::Bedrock::DataSource Properties: DataDeletionPolicy: RETAIN KnowledgeBaseId: !Ref BedrockKnowledgeBase Name: !Sub ${AWS::StackName}-data-source DataSourceConfiguration: Type: S3 S3Configuration: BucketArn: !GetAtt DataSourceBucket.Arn
最後にKnowledge Base for Amazon Bedrockを作成し、事前に作成したOpenSearch Serverless、Secrets Manager、S3を紐づけます。
EmbeddingModeArn
では、Knowledge Base for Amazon Bedrockに対して利用する埋め込みモデルのARNを指定します。
またOpensearchServerlessConfiguration
では、OpenSearch Serverlessに対するコレクション及びインデックス情報を指定します。
動作確認
上記のテンプレートをデプロイする事で、Knowledge Base for Amazon Bedrockを利用して正常に文書データを検索できるか確認します。
なお今回は、最近Knowledge Base for Amazon Bedrockが利用可能になった東京リージョンap-northeast-1
でデプロイを行います。
SAM テンプレートのデプロイ
以下のコマンドを実行し、SAMテンプレートをデプロイします。
# プリンシパルARNを指定 IAM_PRINCIPAL_ARN=<IAMプリンシパルARN> # SAM テンプレートのデプロイ sam build sam deploy --parameter-overrides OSSDataAccessPrincipalArn=${IAM_PRINCIPAL_ARN}
EmbeddingModelId
では、Knowledge Base for Amazon Bedrockに対して利用する埋め込みモデルのIDを指定します。
今回はデフォルトのcohere.embed-multilingual-v3
を指定します。
OSSCollectionStandbyReplicas
は、OpenSearch Serverlessのスタンバイレプリカの有効/無効を指定します。
こちらも今回はデフォルトのDISABLED
を指定します。
OSSDataAccessPrincipalArn
では、OpenSearch Serverlessに対してアクセス権限を持つIAMプリンシパル(ユーザー/ロール)のARNを指定します。
こちらは指定必須であるため、OpenSearch Serverlessにアクセスが必要なIAMプリンシパルのARNを指定してください。
なおOpenSearch Serverlessのデプロイ時間が長く、およそ20分程度かかります。
S3バケットへのデータ投入
次にCloudShell上で以下のコマンドを実施し、PDFファイルを作成したS3バケットにアップロードします。
# S3 バケット名を設定 BUCKET_NAME=<バケット名> # AWS の公式ドキュメントの PDF ファイルをダウンロード mkdir -p pdf cd pdf wget https://docs.aws.amazon.com/ja_jp/amazondynamodb/latest/developerguide/dynamodb-dg.pdf -O DynamoDB.pdf wget https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/lambda-dg.pdf -O Lambda.pdf wget https://docs.aws.amazon.com/ja_jp/vpc/latest/userguide/vpc-ug.pdf -O VPC.pdf wget https://docs.aws.amazon.com/ja_jp/bedrock/latest/userguide/bedrock-ug.pdf -O Bedrock.pdf cd .. # S3 バケットに PDF ファイルをアップロード aws s3 cp pdf s3://${BUCKET_NAME} --recursive
ファイルのアップロードが成功すると、以下の通り作成したS3バケットに4つのPDFファイルが作成されます。
Knowledge Baseデータソースの同期
続いてBedrockのコンソールに移動し、左ペインの"ナレッジベース"に切り替え、作成したナレッジベースをクリックします。
ナレッジベース画面中部あたりのデータソースに移動し、同期をクリックします。
同期中は、ステータスがSyncingになります。
データソースの同期にも、およそ20分程度かかります。
同期が完了すると、ステータスがReadyに戻り、最終同期時刻が表示されます。
Knowledge Baseでの検索実行
最後に検索が可能か確認するために、ナレッジベース画面のテストをクリックし、ナレッジベーステストを実施します。
例えば"回答を生成"をOFFにした状態で、「IPアドレス」と入力し送信すると、該当の用語に類似するデータソースのチャンクが表示されます。
また"回答を生成"をONにした状態で、「Lambdaに割り当てられる最大メモリを教えてください」と入力し送信すると、データソースのチャンクを元に正しく「10,240MB」という回答を返します。
これでナレッジベースが正しく動作している事を確認できました!
最後に
今回はベクターストアにOpenSearch Serverlessを利用したKnowledge Bases for Amazon BedrockをSAMで実装してみました。
ベクターストアのセットアップまで含めるとなかなかIaC化が難しいKnowledge Basesですが、コード管理する事でより検証がしやすくなるかと思います。
ぜひこちらのテンプレートを参考に、RAGの検証を試してみてください!
以上、つくぼし(tsukuboshi0755)でした!