はじめに
皆さん、こんにちは。
私は業務でデータ利活用基盤を取り扱っているため、dbtやIceberg、そしてAWS GenUに取り組む必要があると考えています。特に AWS Japan Top Engineer として、GenUを扱い、その活用を広めることが責務だと感じています。
しかし、私はこれまで CloudFormation を好んで使っており、(逆張り思考も重なって)Cfn テンプレートをシンプルかつ汎用性・拡張性の高い形で作ることに注力してきました。そのため、改めてGenU の CDK コードを読もうとしても、なかなか理解が進みませんでした。
そこで、CDK を学びながら、その過程を記事としてまとめることにしました。
前回までのおさらい
前回までで、以下が完了しました。
GenU の CDK は最大で以下の 6 つの子スタックを作成します。
CloudFrontWafStack
RagKnowledgeBaseStack
AgentStack
GuardrailStack
-
GenerativeAiUseCasesStack
※メインスタック DashboardStack
今回は GenU 内の RagKnowledgeBaseStack
スタックを解説していきたいと思います。
RagKnowledgeBaseStack スタック
RagKnowledgeBaseStack は Bedrock ナレッジベースおよび OpenSearch Serverless
ベクトルストア、データソースである S3 のスタックです。
アーキテクチャ図でいうと、以下の赤枠の部分にあたります。
RagKnowledgeBaseStack の実体は packages/cdk/lib/rag-knowledge-base-stack.ts
にあります。結構ボリュームが多いですが読んでいきましょう。
import { Stack, StackProps, RemovalPolicy } from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as cdk from 'aws-cdk-lib';
import * as lambda from 'aws-cdk-lib/aws-lambda';
import * as bedrock from 'aws-cdk-lib/aws-bedrock';
import * as oss from 'aws-cdk-lib/aws-opensearchserverless';
import * as s3 from 'aws-cdk-lib/aws-s3';
import * as s3Deploy from 'aws-cdk-lib/aws-s3-deployment';
import * as iam from 'aws-cdk-lib/aws-iam';
import { ProcessedStackInput } from './stack-input';
const UUID = '339C5FED-A1B5-43B6-B40A-5E8E59E5734D';
// 以下が現状 Embedding model としてサポートされているモデル ID
// Dimension は最終的に Custom resource の props として渡すが
// 勝手に型が変換されてしまう Issue があるため、number ではなく string にしておく
// https://github.com/aws-cloudformation/cloudformation-coverage-roadmap/issues/1037
const MODEL_VECTOR_MAPPING: { [key: string]: string } = {
'amazon.titan-embed-text-v1': '1536',
'amazon.titan-embed-text-v2:0': '1024',
'cohere.embed-multilingual-v3': '1024',
'cohere.embed-english-v3': '1024',
};
// parsingConfiguration で PDF ファイルの中に埋め込まれている画像やグラフや表を読み取る機能がある。
// 読み取る際のプロンプトは任意のものが定義できる。以下に const として定義する。利用環境によってプロンプトを変更することで、より高い精度を期待できる。
// https://docs.aws.amazon.com/bedrock/latest/userguide/kb-chunking-parsing.html#kb-advanced-parsing
const PARSING_PROMPT = `ドキュメントに含まれる画像やグラフや表などの Image コンテンツからテキストを書き写して、コードブロックではないMarkdown構文で出力してください。以下の手順に従ってください:
1. 提供されたページを注意深く調べてください。
2. ページに存在するすべての要素を特定してください。これには見出し、本文、脚注、表、視覚化、キャプション、ページ番号などが含まれます。
3. Markdown構文のフォーマットを使用して出力してください :
- 見出し:主見出しには#、セクションには##、サブセクションには###など
- リスト:箇条書きには* または -、番号付きリストには1. 2. 3.
- 繰り返しは避けてください
4. 要素が Visualization の場合:
- 自然言語で詳細な説明を提供してください
- 説明を提供した後、Visualization 内のテキストは転写しないでください
5. 要素が表の場合:
- Markdownの表を作成し、すべての行が同じ列数を持つようにしてください
- セルの配置をできるだけ忠実に維持してください
- 表を複数の表に分割しないでください
- 結合されたセルが複数の行や列にまたがる場合、テキストを左上のセルに配置し、他のセルには ' ' を出力してください
- 列の区切りには | を使用し、ヘッダー行の区切りには |-|-| を使用してください
- セルに複数の項目がある場合、別々の行にリストしてください
- 表にサブヘッダーがある場合、サブヘッダーをヘッダーから別の行で分離してください
6. 要素が段落の場合:
- 各テキスト要素を表示されているとおりに正確に転写してください
7. 要素がヘッダー、フッター、脚注、ページ番号の場合:
- 各テキスト要素を表示されているとおりに正確に転写してください
出力例:
Y軸に「売上高($百万)」、X軸に「年」とラベル付けされた年間売上高を示す棒グラフ。グラフには2018年($12M)、2019年($18M)、2020年($8M)、2021年($22M)の棒がある。
図3:このグラフは年間売上高を百万ドル単位で示しています。2020年はCOVID-19パンデミックの影響で大幅に減少しました。
年次報告書
財務ハイライト
収益:$40M
利益:$12M
EPS:$1.25
| | 12月31日終了年度 | |
2021 2022
キャッシュフロー:
営業活動 $ 46,327 $ 46,752
投資活動 (58,154) (37,601)
財務活動 6,291 9,718`;
const EMBEDDING_MODELS = Object.keys(MODEL_VECTOR_MAPPING);
interface OpenSearchServerlessIndexProps {
collectionId: string;
vectorIndexName: string;
vectorField: string;
metadataField: string;
textField: string;
vectorDimension: string;
}
class OpenSearchServerlessIndex extends Construct {
public readonly customResourceHandler: lambda.IFunction;
public readonly customResource: cdk.CustomResource;
constructor(
scope: Construct,
id: string,
props: OpenSearchServerlessIndexProps
) {
super(scope, id);
const customResourceHandler = new lambda.SingletonFunction(
this,
'OpenSearchServerlessIndex',
{
runtime: lambda.Runtime.NODEJS_LATEST,
code: lambda.Code.fromAsset('custom-resources'),
handler: 'oss-index.handler',
uuid: UUID,
lambdaPurpose: 'OpenSearchServerlessIndex',
timeout: cdk.Duration.minutes(15),
}
);
const customResource = new cdk.CustomResource(this, 'CustomResource', {
serviceToken: customResourceHandler.functionArn,
resourceType: 'Custom::OssIndex',
properties: props,
});
this.customResourceHandler = customResourceHandler;
this.customResource = customResource;
}
}
export interface RagKnowledgeBaseStackProps extends StackProps {
params: ProcessedStackInput;
collectionName?: string;
vectorIndexName?: string;
vectorField?: string;
metadataField?: string;
textField?: string;
}
export class RagKnowledgeBaseStack extends Stack {
public readonly knowledgeBaseId: string;
public readonly dataSourceBucketName: string;
constructor(scope: Construct, id: string, props: RagKnowledgeBaseStackProps) {
super(scope, id, props);
const {
env,
embeddingModelId,
ragKnowledgeBaseStandbyReplicas,
ragKnowledgeBaseAdvancedParsing,
ragKnowledgeBaseAdvancedParsingModelId,
} = props.params;
if (typeof embeddingModelId !== 'string') {
throw new Error(
'Knowledge Base RAG が有効になっていますが、embeddingModelId が指定されていません'
);
}
if (!EMBEDDING_MODELS.includes(embeddingModelId)) {
throw new Error(
`embeddingModelId が無効な値です (有効な embeddingModelId ${EMBEDDING_MODELS})`
);
}
const collectionName =
props.collectionName ?? `generative-ai-use-cases-jp${env.toLowerCase()}`;
const vectorIndexName =
props.vectorIndexName ?? 'bedrock-knowledge-base-default';
const vectorField =
props.vectorField ?? 'bedrock-knowledge-base-default-vector';
const textField = props.textField ?? 'AMAZON_BEDROCK_TEXT_CHUNK';
const metadataField = props.metadataField ?? 'AMAZON_BEDROCK_METADATA';
const knowledgeBaseRole = new iam.Role(this, 'KnowledgeBaseRole', {
assumedBy: new iam.ServicePrincipal('bedrock.amazonaws.com'),
});
if (
ragKnowledgeBaseAdvancedParsing &&
typeof ragKnowledgeBaseAdvancedParsingModelId !== 'string'
) {
throw new Error(
'Knowledge Base RAG の Advanced Parsing が有効ですが、ragKnowledgeBaseAdvancedParsingModelId が指定されていないか、文字列ではありません'
);
}
const collection = new oss.CfnCollection(this, 'Collection', {
name: collectionName,
description: 'GenU Collection',
type: 'VECTORSEARCH',
standbyReplicas: ragKnowledgeBaseStandbyReplicas ? 'ENABLED' : 'DISABLED',
});
const ossIndex = new OpenSearchServerlessIndex(this, 'OssIndex', {
collectionId: collection.ref,
vectorIndexName,
vectorField,
textField,
metadataField,
vectorDimension: MODEL_VECTOR_MAPPING[embeddingModelId],
});
ossIndex.customResourceHandler.addToRolePolicy(
new iam.PolicyStatement({
effect: iam.Effect.ALLOW,
resources: [cdk.Token.asString(collection.getAtt('Arn'))],
actions: ['aoss:APIAccessAll'],
})
);
const accessPolicy = new oss.CfnAccessPolicy(this, 'AccessPolicy', {
name: collectionName,
policy: JSON.stringify([
{
Rules: [
{
Resource: [`collection/${collectionName}`],
Permission: [
'aoss:DescribeCollectionItems',
'aoss:CreateCollectionItems',
'aoss:UpdateCollectionItems',
],
ResourceType: 'collection',
},
{
Resource: [`index/${collectionName}/*`],
Permission: [
'aoss:UpdateIndex',
'aoss:DescribeIndex',
'aoss:ReadDocument',
'aoss:WriteDocument',
'aoss:CreateIndex',
'aoss:DeleteIndex',
],
ResourceType: 'index',
},
],
Principal: [
knowledgeBaseRole.roleArn,
ossIndex.customResourceHandler.role?.roleArn,
],
Description: '',
},
]),
type: 'data',
});
const networkPolicy = new oss.CfnSecurityPolicy(this, 'NetworkPolicy', {
name: collectionName,
policy: JSON.stringify([
{
Rules: [
{
Resource: [`collection/${collectionName}`],
ResourceType: 'collection',
},
{
Resource: [`collection/${collectionName}`],
ResourceType: 'dashboard',
},
],
AllowFromPublic: true,
},
]),
type: 'network',
});
const encryptionPolicy = new oss.CfnSecurityPolicy(
this,
'EncryptionPolicy',
{
name: collectionName,
policy: JSON.stringify({
Rules: [
{
Resource: [`collection/${collectionName}`],
ResourceType: 'collection',
},
],
AWSOwnedKey: true,
}),
type: 'encryption',
}
);
collection.node.addDependency(accessPolicy);
collection.node.addDependency(networkPolicy);
collection.node.addDependency(encryptionPolicy);
const accessLogsBucket = new s3.Bucket(this, 'DataSourceAccessLogsBucket', {
blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,
encryption: s3.BucketEncryption.S3_MANAGED,
autoDeleteObjects: true,
removalPolicy: RemovalPolicy.DESTROY,
objectOwnership: s3.ObjectOwnership.OBJECT_WRITER,
enforceSSL: true,
});
const dataSourceBucket = new s3.Bucket(this, 'DataSourceBucket', {
blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,
encryption: s3.BucketEncryption.S3_MANAGED,
autoDeleteObjects: true,
removalPolicy: cdk.RemovalPolicy.DESTROY,
objectOwnership: s3.ObjectOwnership.OBJECT_WRITER,
serverAccessLogsBucket: accessLogsBucket,
serverAccessLogsPrefix: 'AccessLogs/',
enforceSSL: true,
});
knowledgeBaseRole.addToPolicy(
new iam.PolicyStatement({
effect: iam.Effect.ALLOW,
resources: ['*'],
actions: ['bedrock:InvokeModel'],
})
);
knowledgeBaseRole.addToPolicy(
new iam.PolicyStatement({
effect: iam.Effect.ALLOW,
resources: [cdk.Token.asString(collection.getAtt('Arn'))],
actions: ['aoss:APIAccessAll'],
})
);
knowledgeBaseRole.addToPolicy(
new iam.PolicyStatement({
effect: iam.Effect.ALLOW,
resources: [`arn:aws:s3:::${dataSourceBucket.bucketName}`],
actions: ['s3:ListBucket'],
})
);
knowledgeBaseRole.addToPolicy(
new iam.PolicyStatement({
effect: iam.Effect.ALLOW,
resources: [`arn:aws:s3:::${dataSourceBucket.bucketName}/*`],
actions: ['s3:GetObject'],
})
);
const knowledgeBase = new bedrock.CfnKnowledgeBase(this, 'KnowledgeBase', {
name: collectionName,
roleArn: knowledgeBaseRole.roleArn,
knowledgeBaseConfiguration: {
type: 'VECTOR',
vectorKnowledgeBaseConfiguration: {
embeddingModelArn: `arn:aws:bedrock:${this.region}::foundation-model/${embeddingModelId}`,
},
},
storageConfiguration: {
type: 'OPENSEARCH_SERVERLESS',
opensearchServerlessConfiguration: {
collectionArn: cdk.Token.asString(collection.getAtt('Arn')),
fieldMapping: {
metadataField,
textField,
vectorField,
},
vectorIndexName,
},
},
});
new bedrock.CfnDataSource(this, 'DataSource', {
dataSourceConfiguration: {
s3Configuration: {
bucketArn: `arn:aws:s3:::${dataSourceBucket.bucketName}`,
inclusionPrefixes: ['docs/'],
},
type: 'S3',
},
vectorIngestionConfiguration: {
...(ragKnowledgeBaseAdvancedParsing
? {
// Advanced Parsing を有効化する場合のみ、parsingConfiguration を構成する
parsingConfiguration: {
parsingStrategy: 'BEDROCK_FOUNDATION_MODEL',
bedrockFoundationModelConfiguration: {
modelArn: `arn:aws:bedrock:${this.region}::foundation-model/${ragKnowledgeBaseAdvancedParsingModelId}`,
parsingPrompt: {
parsingPromptText: PARSING_PROMPT,
},
},
},
}
: {}),
// チャンク戦略を変更したい場合は、以下のコメントアウトを外して、各種パラメータを調整することで、環境に合わせた環境構築が可能です。
// 以下の 4 種類のチャンク戦略が選択可能です。
// - デフォルト (何も指定しない)
// - セマンティックチャンク
// - 階層チャンク
// - 標準チャンク
// 詳細は以下の Document を参照ください。
// https://docs.aws.amazon.com/bedrock/latest/userguide/kb-chunking-parsing.html
// https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_bedrock.CfnDataSource.ChunkingConfigurationProperty.html
//
// セマンティックチャンク
// chunkingConfiguration: {
// chunkingStrategy: 'SEMANTIC',
// semanticChunkingConfiguration: {
// maxTokens: 300,
// bufferSize: 0,
// breakpointPercentileThreshold: 95,
// },
// },
//
// 階層チャンク
// chunkingConfiguration: {
// chunkingStrategy: 'HIERARCHICAL',
// hierarchicalChunkingConfiguration: {
// levelConfigurations: [
// {
// maxTokens: 1500, // 親チャンクの Max Token サイズ
// },
// {
// maxTokens: 300, // 子チャンクの Max Token サイズ
// },
// ],
// overlapTokens: 60,
// },
// },
//
// 標準チャンク
// chunkingConfiguration: {
// chunkingStrategy: 'FIXED_SIZE',
// fixedSizeChunkingConfiguration: {
// maxTokens: 300,
// overlapPercentage: 10,
// },
// },
},
knowledgeBaseId: knowledgeBase.ref,
name: 's3-data-source',
});
knowledgeBase.addDependency(collection);
knowledgeBase.node.addDependency(ossIndex.customResource);
new s3Deploy.BucketDeployment(this, 'DeployDocs', {
sources: [s3Deploy.Source.asset('./rag-docs')],
destinationBucket: dataSourceBucket,
// 以前の設定で同 Bucket にアクセスログが残っている可能性があるため、この設定は残す
exclude: ['AccessLogs/*', 'logs*'],
});
this.knowledgeBaseId = knowledgeBase.ref;
this.dataSourceBucketName = dataSourceBucket.bucketName;
}
}
このスタックでは、以下のリソース群を作成しています。
Role
CfnCollection
OpenSearchServerlessIndex
CfnAccessPolicy
-
CfnSecurityPolicy
2 つ -
Bucket
2 つ CfnKnowledgeBase
CfnDataSource
BucketDeployment
上から 1 つずつ見ていきます。
RagKnowledgeBaseStack > Role リソース
Role は Bedrock ナレッジベースにアタッチするロールです。
以下のソースコードが Role の定義です。
const knowledgeBaseRole = new iam.Role(this, 'KnowledgeBaseRole', {
assumedBy: new iam.ServicePrincipal('bedrock.amazonaws.com'),
});
/* 中略 */
knowledgeBaseRole.addToPolicy(
new iam.PolicyStatement({
effect: iam.Effect.ALLOW,
resources: ['*'],
actions: ['bedrock:InvokeModel'],
})
);
knowledgeBaseRole.addToPolicy(
new iam.PolicyStatement({
effect: iam.Effect.ALLOW,
resources: [cdk.Token.asString(collection.getAtt('Arn'))],
actions: ['aoss:APIAccessAll'],
})
);
knowledgeBaseRole.addToPolicy(
new iam.PolicyStatement({
effect: iam.Effect.ALLOW,
resources: [`arn:aws:s3:::${dataSourceBucket.bucketName}`],
actions: ['s3:ListBucket'],
})
);
knowledgeBaseRole.addToPolicy(
new iam.PolicyStatement({
effect: iam.Effect.ALLOW,
resources: [`arn:aws:s3:::${dataSourceBucket.bucketName}/*`],
actions: ['s3:GetObject'],
})
);
この中では、以下の AWS リソースを生成しています。
-
KnowledgeBaseRole
ロール-
bedrock:InvokeModel
の許可権限- 対象リソースは全リソース
-
aoss:APIAccessAll
の許可権限- 対象リソースは OpenSearchServerless コレクション(後述)
-
s3:ListBucket
の許可権限- 対象リソースは S3 の dataSourceBucket バケット(後述)
-
s3:GetObject
の許可権限- 対象リソースは S3 の dataSourceBucket バケット(後述)
-
このロールを後述する CfnKnowledgeBase
にアタッチすることで、ナレッジベースから Bedrock のモデル呼び出し (おそらく後述のプロンプトによるパース用)、OpenSearchServerless の API 呼び出し、S3 からのファイル取得が行えるようになります。
RagKnowledgeBaseStack > CfnCollection リソース
CfnCollection は OpenSearchServerless コレクションリソースです。
以下のソースコードが CfnCollection の定義です。
const {
env,
embeddingModelId,
ragKnowledgeBaseStandbyReplicas,
ragKnowledgeBaseAdvancedParsing,
ragKnowledgeBaseAdvancedParsingModelId,
} = props.params;
/* 中略 */
const collectionName =
props.collectionName ?? `generative-ai-use-cases-jp${env.toLowerCase()}`;
/* 中略 */
const collection = new oss.CfnCollection(this, 'Collection', {
name: collectionName,
description: 'GenU Collection',
type: 'VECTORSEARCH',
standbyReplicas: ragKnowledgeBaseStandbyReplicas ? 'ENABLED' : 'DISABLED',
});
この中では、以下の AWS リソースを生成しています。
-
generative-ai-use-cases-jp
OpenSearchServerless コレクション- コレクションタイプは「ベクトル検索 (VECTORSEARCH) 」
- レプリカはパラメータ
ragKnowledgeBaseStandbyReplicas
で有効/無効を設定- 初期値は false
packages/cdk/lib/stack-input.ts
で設定 - 有効化すると可用性が上がるが、コストも上がる
- 初期値は false
この OpenSearchServerless コレクションに対し、後述する分析用のインデックスを追加したり、アクセスポリシーやネットワークポリシーを追加していきます。
RagKnowledgeBaseStack > OpenSearchServerlessIndex リソース
OpenSearchServerlessIndex は OpenSearch Serverless のインデックスリソース (を作成するカスタムリソース) です。
以下のソースコードが OpenSearchServerlessIndex の定義です。
// 以下が現状 Embedding model としてサポートされているモデル ID
// Dimension は最終的に Custom resource の props として渡すが
// 勝手に型が変換されてしまう Issue があるため、number ではなく string にしておく
// https://github.com/aws-cloudformation/cloudformation-coverage-roadmap/issues/1037
const MODEL_VECTOR_MAPPING: { [key: string]: string } = {
'amazon.titan-embed-text-v1': '1536',
'amazon.titan-embed-text-v2:0': '1024',
'cohere.embed-multilingual-v3': '1024',
'cohere.embed-english-v3': '1024',
};
/* 中略 */
class OpenSearchServerlessIndex extends Construct {
public readonly customResourceHandler: lambda.IFunction;
public readonly customResource: cdk.CustomResource;
constructor(
scope: Construct,
id: string,
props: OpenSearchServerlessIndexProps
) {
super(scope, id);
const customResourceHandler = new lambda.SingletonFunction(
this,
'OpenSearchServerlessIndex',
{
runtime: lambda.Runtime.NODEJS_LATEST,
code: lambda.Code.fromAsset('custom-resources'),
handler: 'oss-index.handler',
uuid: UUID,
lambdaPurpose: 'OpenSearchServerlessIndex',
timeout: cdk.Duration.minutes(15),
}
);
const customResource = new cdk.CustomResource(this, 'CustomResource', {
serviceToken: customResourceHandler.functionArn,
resourceType: 'Custom::OssIndex',
properties: props,
});
this.customResourceHandler = customResourceHandler;
this.customResource = customResource;
}
}
/* 中略 */
const {
env,
embeddingModelId,
ragKnowledgeBaseStandbyReplicas,
ragKnowledgeBaseAdvancedParsing,
ragKnowledgeBaseAdvancedParsingModelId,
} = props.params;
if (typeof embeddingModelId !== 'string') {
throw new Error(
'Knowledge Base RAG が有効になっていますが、embeddingModelId が指定されていません'
);
}
if (!EMBEDDING_MODELS.includes(embeddingModelId)) {
throw new Error(
`embeddingModelId が無効な値です (有効な embeddingModelId ${EMBEDDING_MODELS})`
);
}
/* 中略 */
const vectorIndexName =
props.vectorIndexName ?? 'bedrock-knowledge-base-default';
const vectorField =
props.vectorField ?? 'bedrock-knowledge-base-default-vector';
const textField = props.textField ?? 'AMAZON_BEDROCK_TEXT_CHUNK';
const metadataField = props.metadataField ?? 'AMAZON_BEDROCK_METADATA';
/* 中略 */
const ossIndex = new OpenSearchServerlessIndex(this, 'OssIndex', {
collectionId: collection.ref,
vectorIndexName,
vectorField,
textField,
metadataField,
vecotrDimension: MODEL_VECTOR_MAPPING[embeddingModelId],
});
ossIndex.customResourceHandler.addToRolePolicy(
new iam.PolicyStatement({
effect: iam.Effect.ALLOW,
resources: [cdk.Token.asString(collection.getAtt('Arn'))],
actions: ['aoss:APIAccessAll'],
})
);
この中では、以下の AWS リソースを生成しています。
-
OpenSearchServerlessIndex
コンストラクト-
SingletonFunction
lambda 関数- コンストラクトとして部品化しているため、CDK 内で 1 つの Lambda 関数となるよう
SingletonFunction
で定義されている - ソースコードは後述
-
addToRolePolicy
にて OpenSearchServerless コレクションの API 呼び出しを許可
- コンストラクトとして部品化しているため、CDK 内で 1 つの Lambda 関数となるよう
-
CDKのカスタムリソース
- 上記の Lambda 関数を呼び出す
- 呼び出し時のパラメータは以下の通り
- collectionId: 前述の OpenSearchServerless コレクションのコレクション ID
- vectorIndexName: パラメータ値。初期値は
bedrock-knowledge-base-default
- vectorField: パラメータ値。初期値は
bedrock-knowledge-base-default-vector
- textField: パラメータ値。初期値は
AMAZON_BEDROCK_TEXT_CHUNK
- metadataField: パラメータ値。初期値は
AMAZON_BEDROCK_METADATA
- vecotrDimension:
MODEL_VECTOR_MAPPING[embeddingModelId]
にてベクトル次元数を取得-
embeddingModelId
は必須かつ以下のいずれか-
amazon.titan-embed-text-v1
=> 1536 次元が設定される, -
amazon.titan-embed-text-v2:0
=> 1024 次元が設定される, -
cohere.embed-multilingual-v3
=> 1024 次元が設定される, -
cohere.embed-english-v3
=> 1024 次元が設定される,
-
-
-
OpenSearchServerlessIndex 関数の処理
この OpenSearchServerlessIndex
関数の実体は [packages/cdk/custom-resources/oss-index.js
] にあります。
const { defaultProvider } = require('@aws-sdk/credential-provider-node');
const { Client } = require('@opensearch-project/opensearch');
const { AwsSigv4Signer } = require('@opensearch-project/opensearch/aws');
const sleep = (msec) => new Promise((resolve) => setTimeout(resolve, msec));
const updateStatus = async (event, status, reason, physicalResourceId) => {
const body = JSON.stringify({
Status: status,
Reason: reason,
PhysicalResourceId: physicalResourceId,
StackId: event.StackId,
RequestId: event.RequestId,
LogicalResourceId: event.LogicalResourceId,
NoEcho: false,
Data: {},
});
const res = await fetch(event.ResponseURL, {
method: 'PUT',
body,
headers: {
'Content-Type': '',
'Content-Length': body.length.toString(),
},
});
// 失敗時の記録のために残す
console.log(res);
console.log(await res.text());
};
exports.handler = async (event, context) => {
// 失敗時の記録のために残す
console.log(event);
const props = event.ResourceProperties;
const collectionId = props.collectionId;
const region = process.env.AWS_DEFAULT_REGION;
const client = new Client({
...AwsSigv4Signer({
region,
service: 'aoss',
getCredentials: () => {
const credentialsProvider = defaultProvider();
return credentialsProvider();
},
}),
node: `https://${collectionId}.${region}.aoss.amazonaws.com`,
});
try {
switch (event.RequestType) {
case 'Create':
await client.indices.create({
index: props.vectorIndexName,
body: {
mappings: {
properties: {
[props.metadataField]: {
type: 'text',
index: false,
},
[props.textField]: {
type: 'text',
analyzer: 'custom_kuromoji_analyzer',
},
[props.vectorField]: {
type: 'knn_vector',
dimension: Number(props.vectorDimension),
method: {
engine: 'faiss',
space_type: 'l2',
name: 'hnsw',
parameters: {},
},
},
},
},
settings: {
index: {
knn: true,
analysis: {
analyzer: {
custom_kuromoji_analyzer: {
type: 'custom',
tokenizer: 'kuromoji_tokenizer',
filter: [
'kuromoji_baseform',
'kuromoji_part_of_speech',
'kuromoji_stemmer',
'lowercase',
'ja_stop',
],
char_filter: [
'kuromoji_iteration_mark',
'icu_normalizer',
'html_strip',
],
},
},
},
},
},
},
});
await sleep(60 * 1000); // sleep 60s to confirm the creation
await updateStatus(
event,
'SUCCESS',
'Successfully created',
props.vectorIndexName
);
break;
case 'Update':
await updateStatus(
event,
'SUCCESS',
'Update operation is not supported',
props.vectorIndexName
);
break;
case 'Delete':
const index = event.PhysicalResourceId;
await client.indices.delete({
index,
});
await updateStatus(event, 'SUCCESS', 'Successfully deleted', index);
break;
}
} catch (e) {
console.log('---- Error');
console.log(e);
const physicalResourceId =
props.vectorIndexName || event.PhysicalResourceId;
await updateStatus(event, 'FAILED', e.message, physicalResourceId);
}
};
この Lambda 関数では AWS SDK を用いて OpenSearch Serverless 用のインデックスを作成しています。
- Amazon OpenSearch Serverless クライアントの作成
- パラメータの collectionId を元に Amazon OpenSearch Serverless クライアントを作成
- スタック操作
event.RequestType
によって以下の処理を実行- リソース作成:
Create
-
opensearch-js を利用してインデックスを作成
- パラメータの
vectorIndexName
,metadataField
,textField
,vectorField
,vecotrDimension
を元に mappings を設定 - settings で日本語のトークナイザーとしてよく使われる
kuromoji_tokenizer
をカスタマイズ指定- filter:
kuromoji_baseform
,kuromoji_part_of_speech
,kuromoji_stemmer
,lowercase
,ja_stop
- char_filter:
kuromoji_iteration_mark
,icu_normalizer
,html_strip
- filter:
- パラメータの
- 60 秒待つ
- ステータスを
SUCCESS
, メッセージをSuccessfully created
に更新
-
opensearch-js を利用してインデックスを作成
- リソース更新:
Update
- 何もしない
- ステータスを
SUCCESS
, メッセージをUpdate operation is not supported
に更新
- リソース削除:
Delete
- リソースを削除
- ステータスを
SUCCESS
, メッセージをSuccessfully deleted
に更新
- リソース作成:
これを見ると、Create
の場合、作成が終わっていても終わっていなくても 60 秒後にSUCCESS
を返すようです。
RagKnowledgeBaseStack > CfnAccessPolicy リソース
CfnAccessPolicy は、OpenSearch Serverless のデータアクセスポリシーです。
以下のソースコードが CfnAccessPolicy の定義です。
const accessPolicy = new oss.CfnAccessPolicy(this, 'AccessPolicy', {
name: collectionName,
policy: JSON.stringify([
{
Rules: [
{
Resource: [`collection/${collectionName}`],
Permission: [
'aoss:DescribeCollectionItems',
'aoss:CreateCollectionItems',
'aoss:UpdateCollectionItems',
],
ResourceType: 'collection',
},
{
Resource: [`index/${collectionName}/*`],
Permission: [
'aoss:UpdateIndex',
'aoss:DescribeIndex',
'aoss:ReadDocument',
'aoss:WriteDocument',
'aoss:CreateIndex',
'aoss:DeleteIndex',
],
ResourceType: 'index',
},
],
Principal: [
knowledgeBaseRole.roleArn,
ossIndex.customResourceHandler.role?.roleArn,
],
Description: '',
},
]),
type: 'data',
});
/* 中略 */
collection.node.addDependency(accessPolicy);
この中では、サポートされているポリシーのアクセス許可のうち、DeleteCollectionItems
を除く以下のポリシーが許可されています。
- OpenSearch Serverless コレクションへのアイテム登録/更新/削除
- OpenSearch Serverless インデックスの登録/更新/削除
また、OpenSearch Serverless コレクションはこのアクセスポリシーに依存すること (アクセスポリシー作成後にコレクションを作成すること) が明示されています。
RagKnowledgeBaseStack > CfnSecurityPolicy リソース
CfnSecurityPolicy は、OpenSearch Serverless のネットワークポリシー、暗号化ポリシーを設定しています。
以下のソースコードが CfnSecurityPolicy の定義です。
const networkPolicy = new oss.CfnSecurityPolicy(this, 'NetworkPolicy', {
name: collectionName,
policy: JSON.stringify([
{
Rules: [
{
Resource: [`collection/${collectionName}`],
ResourceType: 'collection',
},
{
Resource: [`collection/${collectionName}`],
ResourceType: 'dashboard',
},
],
AllowFromPublic: true,
},
]),
type: 'network',
});
const encryptionPolicy = new oss.CfnSecurityPolicy(
this,
'EncryptionPolicy',
{
name: collectionName,
policy: JSON.stringify({
Rules: [
{
Resource: [`collection/${collectionName}`],
ResourceType: 'collection',
},
],
AWSOwnedKey: true,
}),
type: 'encryption',
}
);
/* 中略 */
collection.node.addDependency(networkPolicy);
collection.node.addDependency(encryptionPolicy);
この中では、OpenSearch Serverless のネットワークポリシーと暗号化ポリシーを生成しています。
- ネットワークポリシー
-
dashboard
,collection
共にパブリックアクセスを許可
-
- 暗号化ポリシー
- AWS マネージドキーを使用して
collection
を暗号化
- AWS マネージドキーを使用して
また、アクセスポリシー同様、OpenSearch Serverless コレクションはこれらのポリシーに依存することが明示されています。
RagKnowledgeBaseStack > Bucket リソース
Bucket はデータソースおよびアクセスログ保管用の S3 バケットリソースです。
以下のソースコードが Bucket の定義です。
const accessLogsBucket = new s3.Bucket(this, 'DataSourceAccessLogsBucket', {
blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,
encryption: s3.BucketEncryption.S3_MANAGED,
autoDeleteObjects: true,
removalPolicy: RemovalPolicy.DESTROY,
objectOwnership: s3.ObjectOwnership.OBJECT_WRITER,
enforceSSL: true,
});
const dataSourceBucket = new s3.Bucket(this, 'DataSourceBucket', {
blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,
encryption: s3.BucketEncryption.S3_MANAGED,
autoDeleteObjects: true,
removalPolicy: cdk.RemovalPolicy.DESTROY,
objectOwnership: s3.ObjectOwnership.OBJECT_WRITER,
serverAccessLogsBucket: accessLogsBucket,
serverAccessLogsPrefix: 'AccessLogs/',
enforceSSL: true,
});
この中では、以下の S3 バケットを生成しています。
- アクセスログ保管用バケット
- パブリックアクセス: ブロック
- 暗号化: AWS マネージドキーでの暗号化
- 削除ポリシー: バケットを削除 (バケットに含まれるオブジェクトも削除)
- オブジェクトの所有者: オブジェクトをアップロードしたアカウント
- SSL アクセス: 強制
- データソースバケット
- パブリックアクセス: ブロック
- 暗号化: AWS マネージドキーでの暗号化
- 削除ポリシー: バケットを削除 (バケットに含まれるオブジェクトも削除)
- オブジェクトの所有者: オブジェクトをアップロードしたアカウント
- アクセスログ: アクセスログ保管用バケットの
AccessLogs/
配下を指定 - SSL アクセス: 強制
また、前述のロールの指定により、 Bedrock ナレッジベースはデータソース S3 バケットに対し、オブジェクトの取得を許可しています。
RagKnowledgeBaseStack > CfnKnowledgeBase リソース
CfnKnowledgeBase は Bedrock ナレッジベースのリソースです。
以下のソースコードが CfnKnowledgeBase の定義です。
const knowledgeBase = new bedrock.CfnKnowledgeBase(this, 'KnowledgeBase', {
name: collectionName,
roleArn: knowledgeBaseRole.roleArn,
knowledgeBaseConfiguration: {
type: 'VECTOR',
vectorKnowledgeBaseConfiguration: {
embeddingModelArn: `arn:aws:bedrock:${this.region}::foundation-model/${embeddingModelId}`,
},
},
storageConfiguration: {
type: 'OPENSEARCH_SERVERLESS',
opensearchServerlessConfiguration: {
collectionArn: cdk.Token.asString(collection.getAtt('Arn')),
fieldMapping: {
metadataField,
textField,
vectorField,
},
vectorIndexName,
},
},
});
/* 中略 */
knowledgeBase.addDependency(collection);
knowledgeBase.node.addDependency(ossIndex.customResource);
この中では、以下の Bedrock ナレッジベースを生成しています。
- 名前は OpenSearch Serverless のコレクションと同名
- 前述の Bedrock ナレッジベース用ロールを指定
- 変換するデータのタイプは ベクトル を指定
- 生成 AI のモデルは前述の通りパラメータの
embeddingModelId
で指定する (必須かつ以下のいずれか)amazon.titan-embed-text-v1
amazon.titan-embed-text-v2:0
cohere.embed-multilingual-v3
cohere.embed-english-v3
- ストレージに前述の OpenSearch Serverless コレクションを指定
また、Bedrock ナレッジベースは、OpenSearch Serverless コレクション、および、インデックス (を作成する Lambda 関数カスタムリソース) に依存させることが明示されています。
RagKnowledgeBaseStack > CfnDataSource リソース
CfnDataSource は Bedrock データソースのリソースです。
Bedrock ナレッジベースがベクトルストアのリソースであることに対し、Bedrock データソースは外部データをナレッジベースに取り込む際の設定を定義します。
以下のソースコードが CfnDataSource の定義です。
new bedrock.CfnDataSource(this, 'DataSource', {
dataSourceConfiguration: {
s3Configuration: {
bucketArn: `arn:aws:s3:::${dataSourceBucket.bucketName}`,
inclusionPrefixes: ['docs/'],
},
type: 'S3',
},
vectorIngestionConfiguration: {
...(ragKnowledgeBaseAdvancedParsing
? {
// Advanced Parsing を有効化する場合のみ、parsingConfiguration を構成する
parsingConfiguration: {
parsingStrategy: 'BEDROCK_FOUNDATION_MODEL',
bedrockFoundationModelConfiguration: {
modelArn: `arn:aws:bedrock:${this.region}::foundation-model/${ragKnowledgeBaseAdvancedParsingModelId}`,
parsingPrompt: {
parsingPromptText: PARSING_PROMPT,
},
},
},
}
: {}),
// チャンク戦略を変更したい場合は、以下のコメントアウトを外して、各種パラメータを調整することで、環境に合わせた環境構築が可能です。
// 以下の 4 種類のチャンク戦略が選択可能です。
// - デフォルト (何も指定しない)
// - セマンティックチャンク
// - 階層チャンク
// - 標準チャンク
// 詳細は以下の Document を参照ください。
// https://docs.aws.amazon.com/bedrock/latest/userguide/kb-chunking-parsing.html
// https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_bedrock.CfnDataSource.ChunkingConfigurationProperty.html
//
// セマンティックチャンク
// chunkingConfiguration: {
// chunkingStrategy: 'SEMANTIC',
// semanticChunkingConfiguration: {
// maxTokens: 300,
// bufferSize: 0,
// breakpointPercentileThreshold: 95,
// },
// },
//
// 階層チャンク
// chunkingConfiguration: {
// chunkingStrategy: 'HIERARCHICAL',
// hierarchicalChunkingConfiguration: {
// levelConfigurations: [
// {
// maxTokens: 1500, // 親チャンクの Max Token サイズ
// },
// {
// maxTokens: 300, // 子チャンクの Max Token サイズ
// },
// ],
// overlapTokens: 60,
// },
// },
//
// 標準チャンク
// chunkingConfiguration: {
// chunkingStrategy: 'FIXED_SIZE',
// fixedSizeChunkingConfiguration: {
// maxTokens: 300,
// overlapPercentage: 10,
// },
// },
},
knowledgeBaseId: knowledgeBase.ref,
name: 's3-data-source',
});
この中では、以下の Bedrock データソースを生成しています。
- 名前は固定値
s3-data-source
- データソースとして前述のデータソース S3 バケットを指定 (パスプレフィックスは
docs/
) - データソースを取り込む際のチャンク戦略を指定
- パラメータで
ragKnowledgeBaseAdvancedParsing
をtrue
に指定すると、anthropic.claude-3-sonnet-20240229-v1:0
モデルを使用したプロンプトでパース指示が可能 (packages/cdk/lib/stack-input.ts
) -
ragKnowledgeBaseAdvancedParsing
のデフォルトは false (packages/cdk/lib/stack-input.ts
) - その他、チャンク戦略を変更する場合のサンプルコードがコメント内に記載されている
- セマンティックチャンク
- 階層チャンク
- 標準チャンク
- パラメータで
- ナレッジベース: 前述の Bedrock ナレッジベースの ナレッジベース ID を指定
データソースのパース指示用プロンプト
ragKnowledgeBaseAdvancedParsing
を true
に指定した場合、デフォルトでは以下のパース指示プロンプトが設定されます。コメントにも記載のある通り、利用環境によってプロンプトを変えることで精度向上が期待できます。
// parsingConfiguration で PDF ファイルの中に埋め込まれている画像やグラフや表を読み取る機能がある。
// 読み取る際のプロンプトは任意のものが定義できる。以下に const として定義する。利用環境によってプロンプトを変更することで、より高い精度を期待できる。
// https://docs.aws.amazon.com/bedrock/latest/userguide/kb-chunking-parsing.html#kb-advanced-parsing
const PARSING_PROMPT = `ドキュメントに含まれる画像やグラフや表などの Image コンテンツからテキストを書き写して、コードブロックではないMarkdown構文で出力してください。以下の手順に従ってください:
1. 提供されたページを注意深く調べてください。
2. ページに存在するすべての要素を特定してください。これには見出し、本文、脚注、表、視覚化、キャプション、ページ番号などが含まれます。
3. Markdown構文のフォーマットを使用して出力してください :
- 見出し:主見出しには#、セクションには##、サブセクションには###など
- リスト:箇条書きには* または -、番号付きリストには1. 2. 3.
- 繰り返しは避けてください
4. 要素が Visualization の場合:
- 自然言語で詳細な説明を提供してください
- 説明を提供した後、Visualization 内のテキストは転写しないでください
5. 要素が表の場合:
- Markdownの表を作成し、すべての行が同じ列数を持つようにしてください
- セルの配置をできるだけ忠実に維持してください
- 表を複数の表に分割しないでください
- 結合されたセルが複数の行や列にまたがる場合、テキストを左上のセルに配置し、他のセルには ' ' を出力してください
- 列の区切りには | を使用し、ヘッダー行の区切りには |-|-| を使用してください
- セルに複数の項目がある場合、別々の行にリストしてください
- 表にサブヘッダーがある場合、サブヘッダーをヘッダーから別の行で分離してください
6. 要素が段落の場合:
- 各テキスト要素を表示されているとおりに正確に転写してください
7. 要素がヘッダー、フッター、脚注、ページ番号の場合:
- 各テキスト要素を表示されているとおりに正確に転写してください
出力例:
Y軸に「売上高($百万)」、X軸に「年」とラベル付けされた年間売上高を示す棒グラフ。グラフには2018年($12M)、2019年($18M)、2020年($8M)、2021年($22M)の棒がある。
図3:このグラフは年間売上高を百万ドル単位で示しています。2020年はCOVID-19パンデミックの影響で大幅に減少しました。
年次報告書
財務ハイライト
収益:$40M
利益:$12M
EPS:$1.25
| | 12月31日終了年度 | |
2021 2022
キャッシュフロー:
営業活動 $ 46,327 $ 46,752
投資活動 (58,154) (37,601)
財務活動 6,291 9,718`;
RagKnowledgeBaseStack > BucketDeployment コンストラクト
BucketDeployment は S3 バケットへのデプロイ (ファイルアップロード) を指定します。
以下のソースコードが BucketDeployment の定義です。
new s3Deploy.BucketDeployment(this, 'DeployDocs', {
sources: [s3Deploy.Source.asset('./rag-docs')],
destinationBucket: dataSourceBucket,
// 以前の設定で同 Bucket にアクセスログが残っている可能性があるため、この設定は残す
exclude: ['AccessLogs/*', 'logs*'],
});
この中では、データソース S3 バケットに、./rag-docs
配下のローカルファイルをアップロードすることを指定しています。誤ってログ用のフォルダにファイルを送信しないような設定も入っています。
以上です。
今回は RAG の設定ということもあり、相当長くなりました。
次回は GenU 内の WebSearchAgentStack
スタックを解説していきたいと思います。