今日は前回のAmazon S3 Vectors (2) : Python スクリプトからベクトルデータの操作に引き続きAmazon S3 Vectors を触っていきます。S3 Vectors は現在プレビュー中でありまだまだRAGを実装するための必要な機能がそろっていません。
例えばBedrock ナレッジからデータをベクトル化して投入する場合、textにのみ対応しているようです。この記事ではPDFを外部ライブラリを用いてチャンクという適切な文字列サイズに分割しベクトル化を行うサンプルをご紹介します。
また同じくS3 Vectorsには現在インデックスの中身を削除する機能がAPI 経由でのみ提供されており、コンソールからの削除はできません。かつ削除APIはアイテムを1個づつ削除するオペレーションのみをサポートしているため、バッチアイテム削除コマンドがないようです。それを実現させるサンプルも併せてご紹介します。
さっそくやってみる
インデックスに格納されているアイテムの一括削除
まずは以下の内容でdelete.py
を作成します。
import boto3
s3vectors = boto3.client("s3vectors", region_name="us-east-1")
bucket_name = "vectorbucket"
index_name = "vectorindex"
# 1. ベクトルキーの一覧を取得
paginator = s3vectors.get_paginator("list_vectors")
pages = paginator.paginate(vectorBucketName=bucket_name, indexName=index_name)
all_keys = []
for page in pages:
for vector in page.get("vectors", []):
all_keys.append(vector["key"])
# 2. 各キーを1件ずつ削除(keys=[key] の形式)
for key in all_keys:
print(f"Deleting vector: {key}")
s3vectors.delete_vectors(
vectorBucketName=bucket_name,
indexName=index_name,
keys=[key]
)
s3vectors.get_paginator("list_vectors")
pages = paginator.paginate(vectorBucketName=bucket_name, indexName=index_name)
の部分でアイテムの一覧を取得します。
次に以下の部分で一覧のkeyを取得します。
all_keys = []
for page in pages:
for vector in page.get("vectors", []):
all_keys.append(vector["key"])
その後keyを指定してアイテムを削除しています。
s3vectors.delete_vectors(
vectorBucketName=bucket_name,
indexName=index_name,
keys=[key]
)
python delete.py
を実行すれば削除完了です。
PDFの取り込み
pip install PyPDF2 pycryptodome
を実行してPDFの取り込みに必要な2つのライブラリをインストールします。
その後put.pyを以下に書き換えます。
import boto3
import json
from PyPDF2 import PdfReader
# ===== 設定 =====
region = "us-east-1"
pdf_path = "example.pdf"
vector_bucket_name = "vectorbucket"
vector_index_name = "vectorindex"
chunk_size = 500
max_metadata_bytes = 1800 # UTF-8バイトでの制限(安全マージン付き)
def split_text_by_length(text, max_chars=2000):
return [text[i:i+max_chars] for i in range(0, len(text), max_chars)]
def trim_to_max_bytes(s, max_bytes):
"""指定されたUTF-8バイト数以内に文字列を収める"""
encoded = s.encode("utf-8")
if len(encoded) <= max_bytes:
return s
# 切り詰め
trimmed = encoded[:max_bytes]
# バイト列が途中のマルチバイト文字で終わっていないか確認し、復元
while True:
try:
return trimmed.decode("utf-8")
except UnicodeDecodeError:
trimmed = trimmed[:-1]
def extract_chunks_from_pdf(pdf_path, chunk_size=2000):
reader = PdfReader(pdf_path)
chunks = []
for page_number, page in enumerate(reader.pages, start=1):
text = page.extract_text()
if text:
split_chunks = split_text_by_length(text.strip(), chunk_size)
for chunk_index, chunk in enumerate(split_chunks):
chunks.append({
"text": chunk,
"page": page_number,
"chunk": chunk_index + 1
})
return chunks
# ===== クライアント初期化 =====
bedrock = boto3.client("bedrock-runtime", region_name=region)
s3vectors = boto3.client("s3vectors", region_name=region)
# ===== チャンク抽出 =====
chunks = extract_chunks_from_pdf(pdf_path, chunk_size)
# ===== ベクトル化 & 登録 =====
vectors = []
for i, chunk in enumerate(chunks):
response = bedrock.invoke_model(
modelId="amazon.titan-embed-text-v2:0",
body=json.dumps({"inputText": chunk["text"]})
)
response_body = json.loads(response["body"].read())
embedding = response_body["embedding"]
source_text_trimmed = trim_to_max_bytes(chunk["text"], max_metadata_bytes)
vector = {
"key": f"page-{chunk['page']}-chunk-{chunk['chunk']}",
"data": {"float32": embedding},
"metadata": {
"source_text": source_text_trimmed,
"page_number": chunk["page"],
"chunk_number": chunk["chunk"]
}
}
vectors.append(vector)
# ===== S3 Vectors に登録 =====
s3vectors.put_vectors(
vectorBucketName=vector_bucket_name,
indexName=vector_index_name,
vectors=vectors
)
print(f"{len(vectors)} 個のベクトルを S3 Vectors に登録しました。")
本来RAG様に外部ナレッジを作成する場合ある程度適度な長さに文章を分割してからベクトル化を行います。
過去試したBedrockからのtextデータ登録では自動で行われていましたが、s3vectorsのコマンドにはその機能がないため、事前にpythonスクリプトでPDFの内容をtextに変換した後分割しています。
https://serverless.co.jp/blog/g2oa60nfvp/
その際以下で分割する長さを指定しています。
chunk_size = 500
max_metadata_bytes = 1800 # UTF-8バイトでの制限(安全マージン付き)
S3Vectorsは現時点では2048バイトまでのメタデータに対応しておりこれは増やすことができません。そのため分割する文字列を500文字、メタデータ(分割された文字列がそのままS3Vectorsに格納されます)を600文字(UTF-8で最大1800バイト)としています。
以下のコマンドでテスト用PDFをダウンロードしてリネームしておきます。
wget https://www.ppc.go.jp/files/pdf/20220401_personal_basicpolicy.pdf
mv 20220401_personal_basicpolicy.pdf example.pdf
python put.py
を実行します。
python put.py
49 個のベクトルを S3 Vectors に登録しました。
PDFの中身が500文字単位で分割されベクトル化が行われた結果49個のアイテムが生成されています。それらのアイテムをベクトル化前の元文章をメタデータにしてセットでインデックスに保存しています。
データの検索
では次にデータの検索を行います。get.py
の中身をいかに置き換えて実行します。
import boto3
import json
# ===== 設定 =====
region = "us-east-1"
vector_bucket_name = "vectorbucket"
vector_index_name = "vectorindex"
input_text = "個人情報とは?"
# ===== クライアント初期化 =====
bedrock = boto3.client("bedrock-runtime", region_name=region)
s3vectors = boto3.client("s3vectors", region_name=region)
# ===== 入力テキストをベクトルに変換 =====
response = bedrock.invoke_model(
modelId="amazon.titan-embed-text-v2:0",
body=json.dumps({"inputText": input_text})
)
embedding = json.loads(response["body"].read())["embedding"]
# ===== ベクトル検索実行 =====
response = s3vectors.query_vectors(
vectorBucketName=vector_bucket_name,
indexName=vector_index_name,
queryVector={"float32": embedding},
topK=1,
returnDistance=True,
returnMetadata=True
)
# ===== 結果を日本語で正しく表示 =====
print(json.dumps(response["vectors"], indent=2, ensure_ascii=False))
python get.py
を実行すれば結果が出力されます。
python get.py
[
{
"key": "page-2-chunk-1",
"metadata": {
"chunk_number": 1,
"source_text": "り、多種多様かつ膨大なデータの収集・分析等が容易かつ高度化している。このような\nデータや技術が官民や地域の枠又は国境を越えて利活用されることにより、官民双方のサービスの向上や、地域の活性化、新産業・新サービスの創出、国際競争力の強化や我が国発のイノベーション創出が図られることが一層期待されている。 \nまた、新型コロナウイルス感染症対応に伴う新しい生活様式の進展と相まって、 地域、\n国境や老若男女問わず、様々な個人や業種・業態の事業者等がデジタル社会に参画し、生命、身体、財産といった、人や組織の具体的な権利利益に直接関わるデータが、量的にも質的にも、これまで以上に生成・流通・蓄積・共有等されている。 \n特に、 個人に関する情報 (個人情報、 仮名加工情報、 匿名加工情報及び個人関連情報。\n以下「個人情報等」という。 )については、高度なデジタル技術を用いた方法により、個\n人の利益のみならず公益のために活用することが可能となってきており、その利用価値は高いとされ、 従前にもまして、 幅広く取り扱われるようになってきている。 その中で、\n個人情報及びプライバシーという概念が世の中に広く認識さ",
"page_number": 2
},
"distance": 0.5845088362693787
}
]
これは input_text = "個人情報とは?"
で指定した文字列をベクトル化して、一番類似しているベクトルを出力した結果です。