1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Bedrock Knowledge Bases】メタデータのちょっと意外?な使い方で、あなたのRAGをもうワンランクアップ!

Last updated at Posted at 2025-09-17

はじめに

Amazon Bedrock Knowledge Bases(以下、ナレッジベース)は利用していますか?
私たちのチームでは、システムの仕様書を取り込み、GenUを介して利用しています。

それでは、ナレッジベースの『メタデータ』は使っていますか?
メタデータは本来、RAGにおいてドキュメントをフィルタリングするための機能です。が、私たちのチームでは、そのメタデータがAPIのレスポンスに含まれることを利用し、以下のように、参照したファイルのパス(仕様書のあるファイルサーバのパス)を表示するようにしています。

スクリーンショット 2025-09-12 13.00.30.png

動けばよしの精神でやっています。用語など不正確な部分があるかと思いますがご容赦ください。致命的であればコメントやDMでご連絡いただければ幸いです。
最後に実装例を示しています。稚拙ですが、せっかくなのでご参照ください。

背景

私のチームでは、担当システムの仕様書をナレッジベース(S3 Vectors)に取り込み、GenUを介して利用しています。(1500点以上のExcelファイルです、トホホ...)

ただ、RAGも100%正確ではありません。原因の1つとして、図が含まれていたり、セルが結合されていたり(あるあるですよね?)など、 そもそも仕様書がLLMにとって読みやすい形になっていないというのがあると思います。
かといって、それをすべて直すのは現実的ではありませんから、RAGはあくまでも検索の補助
そしてもちろん、生成AIの出力ですから、ファクトチェックは人間の責任ですよね?

GenUとは:『生成AIを安全に業務活用するための、ビジネスユースケース集を備えたアプリケーション実装』です。AWS公式に公開されているプロジェクトです。
https://aws-samples.github.io/generative-ai-use-cases/ja/index.html

課題

人間が確認を行うために、チャットボットは参照したファイルパスを提示するようにしたいと考えました。[注1]
しかも、そのファイルパスは、単なるS3のファイルパスではなく、普段担当者が参照しているファイルサーバのパスにしたほうが効果的です。
そうすれば、コピペでファイルをすぐに参照できます。

しかし、
S3 Vectorsが日本語ファイル名をサポートしていない[注2]
ため、S3に仕様書をフォルダごとアップロードしても、日本語パスだと取り込みでエラーになります。

スクリーンショット 2025-09-12 19.23.47.png

[注1] GenUでは、参照したファイルのS3パスを示し、DLできる機能があります。利用者本人の確認だけならそれで問題ありません。が、社内メンバーにソースを共有する場合は、普段使いのファイルサーバのパスを共有するのが便利です。

[注2] S3 Vectorsではなく、OpenSearchServerlessやNeptuneなど他のベクトルDBサービスなら可能かもしれません、が試せていません。

解決案

概要

以下の方法で解決しました。(※2,3の処理は、StepFunctionsで実装しています。『実装詳細』をご参照ください)

  1. まず、取り込みたいドキュメントを、仕様書のフォルダ階層そのままでS3にアップロード。OriginalDocumentsに上げたものとします
  2. 日本語だとダメなので、ハッシュ値など英数字ファイル名に変換して別フォルダにコピー。ImportedDocumentsに階層を保持せずにコピーするものとします
  3. ImportedDocuments内の仕様書1つずつに対して、metadata.jsonを作成。その中に、S3のパスを記載
  4. チャットボットで表示する際に、metadataの情報を表示する。その際、OriginalDocumentsの部分を、利用しているファイルサーバに文字列置換

フォルダ構成的には次のイメージです。

<S3 Bucket>
  ├-- OriginalDocuments
  |    ├-- 基本設計書
  |    |     ├-- 設計書1.xlsx
  |    |     ├-- 設計書2.xlsx
  |    |     └-- ....
  |    └-- 詳細設計書
  |          ├-- 詳細設計書1.xlsx
  |          ├-- 詳細設計書2.xlsx
  |          └-- ....
  └-- ImportedDocuments
       ├-- hash-hogehoge.xlsx
       ├-- hash-hogehoge.xlsx.metadata.json
       ├-- hash-fugafuga.xlsx
       ├-- hash-fugafuga.xlsx.metadata.json
       └-- ....

補足

概要3) metadata.jsonについて

取り込み対象ファイルに対して、[同じファイル名].metadata.jsonが必要です。
hash-hogehoge.xlsxの場合、hash-hogehoge.xlsx.metadata.jsonです。

参考記事:Knowledge bases for Amazon Bedrock でメタデータフィルタリングを試してみる

さて、この.metadata.json内に、次のように、S3のパスを記述します。

hash-hogehoge.xlsx.metadata.json
{
  "metadataAttributes":
  {
    "originalPath": "OriginalDocuments/基本設計書/テーブル定義.xlsx"
  }
}

概要4) APIのレスポンスにおけるmetadataについて

次に、レスポンスを確認しておきましょう。metadata.jsonを作っておくと、こんなふうに返ります。metadataがたしかに返るので、これをGenUの表示に利用できることがわかるかと思います。

retrieve-and-generateしたときのレスポンス
{
    "citations": [
        {
            "generatedResponsePart": {
                "textResponsePart": {
                    "span": {"end": 160,"start": 0},
                    "text": "共通ユーザマスタテーブルには、..."
                }
            },
            "retrievedReferences": [
                {
                    "content": {
                        "text": "データ項目名n  テーブルID テーブル名...",
                        "type": "TEXT"
                    },
                    "location": {
                        "s3Location": {
                            "uri": "s3://<bucket>/ImportedDocuments/f0f882ad9d7511a9aaaa.xlsx"
                            //☝️これはS3のキー
                        },
                        "type": "S3"
                    },
                    "metadata": {
                        "x-amz-bedrock-kb-source-uri": "s3://<bucket>/ImportedDocuments/f0f882ad9d7511a9aaaa.xlsx",
                        "x-amz-bedrock-kb-data-source-id": "xxxxxx",
                        "originalPath": "OriginalDocuments/基本設計書/テーブル定義.xlsx",
                        //☝️この通り、返ってくることが分かる
                        "x-amz-bedrock-kb-chunk-id": "xxxx-xxx-xxx..."
                    }
                },
                {...}//他のテキスト
            ]
        }
    ],
    "output": {
        "text": "共通ユーザマスタテーブルには、..."
    },
    "sessionId": "xxxx..."
}

GenUを使うならば、この辺りを書き換えると良いでしょう。

originalPath = ref.metadata?.['originalPath'];

こんな感じで取り出せるので、referenceTextに追加されるように変更しましょう。

実装詳細

稚拙ですが、Stepfunctionsを貼っておきます。

StepFunctionsの実装について
  • DynamoDBで変換前ファイルパス、変換後ファイル名、ファイル更新日(S3アップロード日時)を管理する機能を追加
    • 仕様書の更新がある場合のみStateを回したいので、DynamoDB上のファイル更新日を取得→処理済みファイルの作成日時と同じならスキップする、としています
    • テーブルの構成は次のとおり
      • NewPath(パーティションキー)
      • LastModified(ソートキー)
      • OriginalPath
  • Stateを二つに分割
    • UpdateKnowledge_main.jsonで全ファイルを走査
      →mapで処理して、UpdateKnowledge_subを呼び出し
  • 登場してくる各Lambdaの役割
    • GenerateNewFileName: ハッシュ値ファイル名に変換
    • CopyFile: ファイル名をハッシュ値に変更しながらコピー先フォルダにコピー
      (StateMachine内のCopyObjectだと、日本語ファイルがうまく処理できなかったため)
    • CreateMetadataJson(Lambda): 作成するmetadata.jsonの情報を作成
      (originalPathの他に、category, subsystemを付与しています)
50_UpdateKnowledge_main.json
// 1. コピー元フォルダのファイルを一覧化
// 2. 拡張子が.xls* だったら、mapでサブマシンを呼び出し
// 3. 最後に、ナレッジベースの同期を実行
{
  "Comment": "State machine to move files in S3 and update DynamoDB",
  "StartAt": "ConfigureParameters",
  "States": {
    "ConfigureParameters": {
      "Type": "Pass",
      "Result": {
        "source_bucket": "<コピー元S3バケット名>",
        "destination_bucket": "<コピー先S3バケット名>",
        "table_name": "<DynamoDBテーブル名>",
        "source_folder": "OriginalDocuments/", // コピー元フォルダ
        "destination_folder": "ImportedDocuments" // コピー先フォルダ
      },
      "Next": "ProcessFiles"
    },
    "ProcessFiles": {
      "Type": "Map",
      "Parameters": {
        "source_bucket.$": "$.source_bucket",
        "destination_bucket.$": "$.destination_bucket",
        "table_name.$": "$.table_name",
        "source_folder.$": "$.source_folder",
        "destination_folder.$": "$.destination_folder",
        "file_key.$": "$$.Map.Item.Value.Key",
        "last_modified.$": "$$.Map.Item.Value.LastModified"
      },
      "Iterator": {
        "StartAt": "IsFile",
        "States": {
          "IsFile": {
            "Type": "Choice",
            "Choices": [
              {
                "Not": {
                  "Variable": "$.file_key",
                  "StringMatches": "*.xls*"
                },
                "Next": "Continue"
              }
            ],
            "Default": "Step Functions StartExecution"
          },
          "Step Functions StartExecution": {
            "Type": "Task",
            "Resource": "arn:aws:states:::states:startExecution.sync:2",
            "Parameters": {
              "StateMachineArn": "arn:aws:states:<リージョン>:<アカウントID>:stateMachine:UpdateKnowledge_sub",
              "Input.$": "$"
            },
            "Next": "Continue"
          },
          "Continue": {
            "Type": "Pass",
            "End": true
          }
        },
        "ProcessorConfig": {
          "Mode": "DISTRIBUTED",
          "ExecutionType": "STANDARD"
        }
      },
      "MaxConcurrency": 50,
      "Label": "ProcessFiles",
      "ItemReader": {
        "Resource": "arn:aws:states:::s3:listObjectsV2",
        "Parameters": {
          "Bucket.$": "$.source_bucket",
          "Prefix.$": "$.source_folder"
        }
      },
      "ResultPath": null,
      "Next": "StartIngestionJob"
    },
    "StartIngestionJob": {
      "Type": "Task",
      "Parameters": {
        "DataSourceId": "<ナレッジベースのデータソースID>",
        "KnowledgeBaseId": "<ナレッジベースID>"
      },
      "Resource": "arn:aws:states:::aws-sdk:bedrockagent:startIngestionJob",
      "End": true
    }
  }
}
UpdateKnowledge_sub.json
// 1. GenerateNewFileName(Lambda)でハッシュ値ファイル名に変換
// 2. DynamoDBをチェック。ハッシュ値ファイル名と更新日が同じ件数を取得
// 3. 2.の値が1以上なら処理を終了。0なら処理を継続
// 4. CopyFile(Lammbda)でファイル名をハッシュ値に変更しながらコピー先フォルダにコピー
//    ※StateMachine内のCopyObjectだと、日本語ファイルがうまく処理できず、Lambdaを用意しました
// 5. CreateMetadataJson(Lambda)で作成するmetadata.jsonの情報を作成。
//    ※originalPathの他に、category, subsystemを付与しています
// 6. metadata.jsonを生成して、S3にアップロード
// 7. 処理済み情報をDynamoDBに保存
{
  "Comment": "State machine to move files in S3 and update DynamoDB",
  "StartAt": "GenerateNewFileName",
  "States": {
    "GenerateNewFileName": {
      "Type": "Task",
      "Resource": "arn:aws:lambda:<リージョン>:<アカウントID>:function:GenerateNewFileName",
      "ResultPath": "$.GenerateNewFileName",
      "Next": "SetParameter"
    },
    "SetParameter": {
      "Type": "Pass",
      "Next": "CheckDynamoDB",
      "Parameters": {
        "source_bucket.$": "$.source_bucket",
        "destination_bucket.$": "$.destination_bucket",
        "table_name.$": "$.table_name",
        "source_folder.$": "$.source_folder",
        "destination_folder.$": "$.destination_folder",
        "file_key.$": "$.file_key",
        "last_modified.$": "$.GenerateNewFileName.last_modified",
        "GenerateNewFileName.$": "$.GenerateNewFileName"
      }
    },
    "Continue": {
      "Type": "Pass",
      "End": true
    },
    "CheckDynamoDB": {
      "Type": "Task",
      "Resource": "arn:aws:states:::aws-sdk:dynamodb:query",
      "Parameters": {
        "TableName.$": "$.table_name",
        "KeyConditionExpression": "NewPath = :new_path AND LastModified = :last_modified",
        "ExpressionAttributeValues": {
          ":new_path": {
            "S.$": "$.GenerateNewFileName.new_file_key"
          },
          ":last_modified": {
            "S.$": "$.last_modified"
          }
        }
      },
      "Next": "CheckDBCount",
      "ResultPath": "$.CheckDynamoDB"
    },
    "CheckDBCount": {
      "Type": "Choice",
      "Choices": [
        {
          "Variable": "$.CheckDynamoDB.Count",
          "NumericGreaterThan": 0,
          "Next": "GetExistingFileName"
        }
      ],
      "Default": "CopyFile"
    },
    "GetExistingFileName": {
      "Type": "Pass",
      "ResultPath": "$.new_file_key",
      "Next": "Continue"
    },
    "CopyFile": {
      "Type": "Task",
      "Resource": "arn:aws:states:::lambda:invoke",
      "Parameters": {
        "Payload.$": "$",
        "FunctionName": "arn:aws:lambda:<リージョン>:<アカウントID>:function:CopyFile:$LATEST"
      },
      "Retry": [
        {
          "ErrorEquals": [
            "Lambda.ServiceException",
            "Lambda.AWSLambdaException",
            "Lambda.SdkClientException",
            "Lambda.TooManyRequestsException"
          ],
          "IntervalSeconds": 1,
          "MaxAttempts": 3,
          "BackoffRate": 2
        }
      ],
      "Next": "CreateMetadataJson",
      "ResultPath": "$.CopyFile",
      "ResultSelector": {
        "statusCode.$": "$.Payload.statusCode",
        "file_key.$": "$.Payload.file_key",
        "dest_key.$": "$.Payload.dest_key"
      }
    },
    "CreateMetadataJson": {
      "Type": "Task",
      "Resource": "arn:aws:states:::lambda:invoke",
      "Parameters": {
        "Payload.$": "$",
        "FunctionName": "arn:aws:lambda:<リージョン>:<アカウントID>:function:CreateMetadatajson:$LATEST"
      },
      "Retry": [
        {
          "ErrorEquals": [
            "Lambda.ServiceException",
            "Lambda.AWSLambdaException",
            "Lambda.SdkClientException",
            "Lambda.TooManyRequestsException"
          ],
          "IntervalSeconds": 1,
          "MaxAttempts": 3,
          "BackoffRate": 2
        }
      ],
      "Next": "SetMetadataJson",
      "ResultPath": "$.GenerateSummary",
      "ResultSelector": {
        "statusCode.$": "$.Payload.statusCode",
        "metadataAttributes.$": "$.Payload.metadataAttributes"
      },
      "Catch": [
        {
          "ErrorEquals": [
            "States.ALL"
          ],
          "Next": "SaveMappingForError",
          "ResultPath": "$.GenerateSummary"
        }
      ]
    },
    "SetMetadataJson": {
      "Type": "Pass",
      "Next": "PutObject",
      "ResultPath": "$.MakeMetadataJson",
      "Parameters": {
        "original_path.$": "$.file_key",
        "metadata_json_filename.$": "States.Format('{}/{}.metadata.json', $.destination_folder, $.GenerateNewFileName.new_file_key)"
      }
    },
    "PutObject": {
      "Type": "Task",
      "Parameters": {
        "Body": {
          "metadataAttributes.$": "$.GenerateSummary.metadataAttributes"
        },
        "Bucket.$": "$.destination_bucket",
        "Key.$": "$.MakeMetadataJson.metadata_json_filename"
      },
      "Resource": "arn:aws:states:::aws-sdk:s3:putObject",
      "Next": "SaveMapping",
      "ResultPath": "$.PutObject"
    },
    "SaveMapping": {
      "Type": "Task",
      "Resource": "arn:aws:states:::aws-sdk:dynamodb:putItem",
      "Parameters": {
        "TableName.$": "$.table_name",
        "Item": {
          "NewPath": {
            "S.$": "$.GenerateNewFileName.new_file_key"
          },
          "OriginalPath": {
            "S.$": "$.file_key"
          },
          "LastModified": {
            "S.$": "$.last_modified"
          }
        }
      },
      "End": true
    },
    "SaveMappingForError": {
      "Type": "Task",
      "Resource": "arn:aws:states:::aws-sdk:dynamodb:putItem",
      "Parameters": {
        "TableName.$": "$.table_name",
        "Item": {
          "NewPath": {
            "S.$": "$.GenerateNewFileName.new_file_key"
          },
          "OriginalPath": {
            "S.$": "$.file_key"
          },
          "LastModified": {
            "S.$": "$.last_modified"
          },
          "Skipped": {
            "N": "1"
          }
        }
      },
      "Catch": [
        {
          "ErrorEquals": [
            "States.ALL"
          ],
          "Next": "Continue"
        }
      ],
      "End": true
    }
  }
}

最後に、日本語ファイル名のファイルをコピーするLambdaを貼っておきます

CopyFile.py(Lambda)
import boto3
from unicodedata import normalize

def lambda_handler(event, context):   
    s3 = boto3.client('s3')
    
    source_bucket = event['source_bucket']
    source_key = event['file_key']
    dest_bucket = event['destination_bucket']
    dest_key = event['GenerateNewFileName']['new_file_key']
    destination_folder = event['destination_folder']
    
    dest_key = f'{destination_folder}/{dest_key}'
    
    #print(source_key)
    u_source_key = normalize('NFC', source_key)
    #print(u_source_key)
    
    
    s3.copy_object(
        CopySource={'Bucket': source_bucket, 'Key': source_key},
        Bucket=dest_bucket,
        Key=dest_key
    )
    
    return {
        'statusCode': 200,
        'file_key': source_key,
        'dest_key': dest_key
    }

次のステップ

  • GenUにStorage Browser for Amazon S3を組み込み、GenU上からファイルのアップロードを可能にする
  • 今回紹介したStepFunctionsを実行するボタン、Lambdaを用意して、同期を簡単にする

スクリーンショット 2025-09-17 10.45.52.png

Storage Browser for Amazon S3 とは: Storage Browser for Amazon S3 を使用して、アプリケーションを通じてユーザーをデータに接続

  • OriginalDocumentsフォルダからファイルを削除したらImportedDocumentsにも反映する
    追加には対処できるのですが、削除ができません。S3のトリガーからLambda経由でできそうですが、こちらは未実装です。

おわりに

生成AI、RAGが普及して、業務は大幅に効率しました。が、LLMも完璧ではありません。それでも、RAGがあるのとないので、本当に天と地の差です。最大限活用するための工夫の1つを紹介しました。

metadataの意外な使い方を示せたのではないでしょうか。
同じような課題に直面した方の参考になれば幸いです。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?