金曜日, 7月 18, 2025
金曜日, 7月 18, 2025
- Advertisment -
ホームニューステックニュースAmazon S3 Vectorsで激安RAGシステムを構築する

Amazon S3 Vectorsで激安RAGシステムを構築する


こんにちは👋

2025年7月15日、AWSからS3 Vectorsと呼ばれる機能がプレビュー公開されました。

Amazon S3 Vectors は、ベクトルの保存とクエリをネイティブにサポートする初のクラウドオブジェクトストアです。Amazon S3 に保存されたコンテンツの AI エージェント、AI 推論、セマンティック検索のために、コストに最適化された専用のベクトルストレージを提供します。

https://aws.amazon.com/jp/s3/features/vectors

元々AWSでRAGシステムを構築する場合は、KendraやOpenSearch Service、Aurora(pgvector)などの高コストなベクトルDBを用意する必要がありました。OpenSearch Serverlessなどはサーバレス…と謳っているものの、実態は起動時間によるOCU単位での従量課金であり、個人でRAGシステムを構築する際の大きな障壁となっていました。

今回発表されたS3 Vectorsは、低コストでベクトルインデックスの構築ができるサービスとなっており、保管されたベクトルのデータ量やAPIリクエストに対してのみ料金が発生します。これは従来の高コストなRAGシステムのイメージを打ち壊す、非常に素晴らしい機能です。

発表されてから割とすぐ書いたのにもかからわず、何故か先駆者の方々がたくさんいるので、ネタが被らないようKnowledge Basesを使わないタイプでS3 Vectorsを使ってみようと思います。

https://dev.classmethod.jp/articles/amazon-s3-vectors-preview-release

https://qiita.com/Syoitu/items/4ef4f2f0b1fe71ace919

今回の検証用に作成したコードをLambda + API Gateway + Amazon Bedrockで利用できる簡単なサーバレスRAGシステムを構築するサンプルリポジトリもついてるので、よかったらチェックしてみてください。

https://github.com/tosuri13/madeinabyss-s3vectors-search

今回作るRAGシステム

最近「メイドインアビス」という作品にハマっています(非常に度し難い作品なので、ぜひ読んでみてください)。Wikipediaに色々と情報が載っているので、こちらの内容をベクトル検索して回答できるRAGシステムを作成しようと思います。

https://ja.wikipedia.org/wiki/メイドインアビス

S3 Vectorsを用意する

まずは、AWSコンソールからポチポチとS3 Vectorsを作ってみます。

(本当はCloudFormationで作りたかったのですが、英語版の公式ドキュメント上にも記述がなかったので、今回は諦めてコンソール上の操作で作成します)

バージニア北部リージョンなどプレビュー公開の対象となっているリージョンでAmazon S3を開くと、サイドバーに「ベクトルバケット」が増えているので、こちらを選択します。

「ベクトルバケットを作成」を選択して、RAGの検索で使用するベクトルを格納するためのベクトルバケットを作成します。

適当に名前を付けて(通常のS3バケットと同様にグローバルで一意に定まる必要があります)、そのまま「ベクトルバケットを作成」を選択します。

ベクトルバケットが作成できたら、次にベクトルインデックスを作成します。

ベクトルインデックスは、OpenSearchのインデックスと同じようなもので、インデックス単位でベクトルやメタデータの管理などを行います。

「ベクトルインデックスを作成」を選択すると、インデックスの設定方法について色々と求められます。

名前は適当につけるとして、今回は埋め込みモデルにAmazon Titan Embed v2を使用したいので、デフォルトの次元数である1024を「ディメンション」に入力します。「距離メトリック」にはコサインを指定しましょう。

また、フィルタに使用しないメタデータの設定を追加で行うことができます。

デフォルトではベクトル追加時に付属したメタデータは全てフィルタ対象として扱われるようですが、処理の最適化なのか理由は分かりませんが、特定のメタデータをフィルタ対象から除くことができます(注意点としてインデックス作成時のみ設定可能です)。

埋め込み元のテキストを格納するtextメタデータは、フィルタしてもあんまり意味がないので、今回のフィルタ対象から除いておきましょう。

全ての設定項目が入力できたら「ベクトルインデックスを作成」を選択して、ベクトルインデックスを作成します。

現在はコンソールの画面からベクトルバケット・ベクトルインデックスの編集や削除はできず、AWS CLI経由でしか実行できないため注意しましょう。

  • ベクトルバケットの削除
aws s3vectors delete-vector-bucket --vector-bucket-name "your-s3-vector-bucket" --region us-east-1
  • ベクトルインデックスの削除
aws s3vectors delete-index --vector-bucket-name "your-s3-vector-bucket" --index-name "your-s3-vector-index" --region us-east-1

ベクトルを追加する

PythonのAWS SDKを使用して、先ほど作成したベクトルインデックスにベクトルを追加していきます。

用意したテキストデータを適度にチャンキングした後、Amazon BedrockのAmazon Titan Embed v2を使用して、各チャンクごとの埋め込みを作成します。

チャンクした埋め込みはdataプロパティに追加します。float32というプロパティがあるので、Int8 VectorやBinary Vectorにも対応している、あるいはする予定があるんだと思います。

メタデータは元のテキストを格納するtextとWikiのタイトルを格納するtitleを追加します。textはフィルタ対象から除外しましたが、titleはフィルタ対象として使えるので、特定のWikipediaのページのみをRAGの検索対象として扱うみたいなこともできますね。

最後にs3vectorsのクライアントからput_vectorsを呼び出して、先ほど作成したベクトルインデックスにベクトルを追加します。

import os
import uuid
from pathlib import Path

import boto3
from langchain_aws.embeddings import BedrockEmbeddings
from langchain_text_splitters import MarkdownTextSplitter
from tqdm import tqdm


def create_index() -> None:

    (省略)

    bedrock_client = boto3.client("bedrock-runtime", "us-east-1")
    embedding_model = BedrockEmbeddings(
        client=bedrock_client,
        model_id="amazon.titan-embed-text-v2:0",
    )

    vectors = []
    print("● Create vectors from source text...")

    for chunk in tqdm(chunks):
        vectors.append(
            {
                "key": str(uuid.uuid4()),
                "data": {
                    "float32": embedding_model.embed_query(chunk),
                },
                "metadata": {
                    "text": chunk,
                    "title": title,
                },
            }
        )

    s3vectors_client = boto3.client("s3vectors", "us-east-1")
    s3vectors_client.put_vectors(
        vectorBucketName=os.environ["VECTOR_BUCKET_NAME"],
        indexName=os.environ["VECTOR_INDEX_NAME"],
        vectors=vectors,
    )

    print(f"● Successfully stored {len(vectors)} vectors")

    return None

ベクトルを検索する

まずは、ローカルでS3 Vectorsに対してクエリしてみます。

同じくAmazon Titan Embed v2で質問を埋め込んだ後、類似するベクトル上位3件を取得してみようと思います。

returnMetadatareturnDistanceなどのフィールドを有効化しておくことで、ベクトルに紐付くメタデータを取得したり、距離メトリックの値を取得することができます。

import os

import boto3
from langchain_aws import BedrockEmbeddings


def query(question: str) -> None:
    bedrock_client = boto3.client("bedrock-runtime", "us-east-1")
    embedding_model = BedrockEmbeddings(
        client=bedrock_client,
        model_id="amazon.titan-embed-text-v2:0",
    )
    embedding = embedding_model.embed_query(question)

    s3vectors_client = boto3.client("s3vectors", "us-east-1")
    response = s3vectors_client.query_vectors(
        vectorBucketName=os.environ["VECTOR_BUCKET_NAME"],
        indexName=os.environ["VECTOR_INDEX_NAME"],
        queryVector={
            "float32": embedding,
        },
        topK=3,
        returnMetadata=True,
        returnDistance=True,
    )

    for vector in response["vectors"]:
        print(vector)

Claude Sonnet 4などの強い基盤モデルが詳しく知らなさそうな、メイドインアビスにおける白笛の探窟家について聞いてみましょう。

❯ uv run tools/query.py -q "白笛の探窟家について教えて!"
{'key': '5fd10254-0545-4b50-9ef1-4d17ae55a9a8', 'metadata': {'text': '(省略) 白笛\n\n探窟家における最高位。限界深度は無制限。この位に到達した探窟家は数えるほどしかおらず、白笛の探窟家はみな“伝説的英雄”と称される。\n\n国外の者も、手続きを踏んで鈴付きから始めれば笛とライセンスを取得できる。(省略)', 'title': 'メイドインアビス'}, 'distance': 0.556890606880188}
{'key': '6a263fb0-020f-4ccb-8861-637c818150c8', 'metadata': {'text': '(省略) 最高位の探窟家である白笛は「**奈落の星**(ネザースター)」とも呼ばれ、また各人ごとに「○○卿」という異名が付く(「○○卿」は公的に探窟家組合の名簿に掲載される称号、「動かざるオーゼン」などの二つ名は新聞や世間が呼ぶ通称から定着したもの[19])。物語開始時点では、「不動卿」動かざるオーゼン\n、「黎明卿」新しきボンドルド 、「神秘卿」神秘のスラージョ\n、「先導卿」選ばれしワクナ及びラストダイブ中(5話以降は笛がアビスから帰ったため公的には死亡扱い)の「殲滅卿」殲滅のライザの五人が現職の白笛として判明している。(省略)', 'title': 'メイドインアビス'}, 'distance': 0.6135679483413696}
{'key': '2a45b5ef-4633-49c1-bd75-62b7cece14df', 'metadata': {'text': '(省略) 母と同じ白笛を目指しており、アビスの底に思いを馳せている。10年ぶりに発見された母の笛と伝言をきっかけに、アビスの底を目指すことを決意する[23]。(省略)', 'title': 'メイドインアビス'}, 'distance': 0.6537579298019409}

内容はともかく、質問に似たテキストが取得できていることが分かるかと思います。
体感ですが検索にかかるレイテンシもかなり速く、通常的な利用範囲であれば全く問題ないと思います。

Lambdaに載せてデプロイする

Lambda側には、RAGとしてそのまま回答を生成させる機能まで付けたいと思います。

取得したドキュメントはXML形式に変換して、ユーザプロンプトに加えるシンプルな形にします。LangChainだとサクッと書けるので便利ですね。

import json
import os

import boto3
import xmltodict
from langchain_aws.chat_models import ChatBedrockConverse
from langchain_aws.embeddings import BedrockEmbeddings
from langchain_core.messages import HumanMessage, SystemMessage

VECTOR_BUCKET_NAME = os.environ["VECTOR_BUCKET_NAME"]
VECTOR_INDEX_NAME = os.environ["VECTOR_INDEX_NAME"]


def handler(event, context):
    request = json.loads(event["body"])
    question = request["question"]

    bedrock_client = boto3.client("bedrock-runtime", "us-east-1")
    embedding_model = BedrockEmbeddings(
        client=bedrock_client,
        model_id="amazon.titan-embed-text-v2:0",
    )
    embedding = embedding_model.embed_query(question)

    s3vectors_client = boto3.client("s3vectors", "us-east-1")
    response = s3vectors_client.query_vectors(
        vectorBucketName=os.environ["VECTOR_BUCKET_NAME"],
        indexName=os.environ["VECTOR_INDEX_NAME"],
        queryVector={
            "float32": embedding,
        },
        topK=3,
        returnMetadata=True,
        returnDistance=True,
    )

    xml_docs = xmltodict.unparse(
        {
            "documents": {
                "document": [
                    {
                        "text": vector["metadata"]["text"],
                    }
                    for vector in response["vectors"]
                ]
            }
        },
        full_document=False,
        pretty=True,
    )

    messages = [
        SystemMessage(
            (
                "あなたはメイドインアビスと呼ばれる作品に関する質問に回答するチャットボットです。\n"
                "参考となるドキュメントに記載されている内容に基づいて回答を生成してください"
            )
        ),
        HumanMessage((f"# 参考ドキュメント\n{xml_docs}\n# 質問\n{question}")),
    ]

    model = ChatBedrockConverse(
        client=bedrock_client,
        model="us.anthropic.claude-sonnet-4-20250514-v1:0",
    )
    response = model.invoke(messages)
    answer = response.content

    return {
        "statusCode": 200,
        "body": json.dumps({"answer": answer}, ensure_ascii=False),
    }

Lambdaに引っ付けるIAMの権限には、s3vectors:GetVectorss3vectors:QueryVectorsなどが必要です。s3:ではなくs3vectors:である点に注意です。

  PolicyDocument:
    Version: "2012-10-17"
    Statement:
      - Effect: "Allow"
        Action: 
          - "bedrock:InvokeModel"
          - "s3vectors:GetVectors"
          - "s3vectors:QueryVectors"

あとは、AWS SAMでビルド&デプロイします。
この辺り記載すると冗長になるので、詳しく知りたい方はリポジトリを見てください。

API経由で質問する

デプロイしたAPI Gatewayのエンドポイントに対して、curlコマンドを打ってみます。

curl -X POST "your-endpoint-domain" -d '{"question": "白笛の探窟家について教えて!"}'

answerだけ抜粋

メイドインアビスの白笛について詳しく説明しますね!
白笛とは
白笛は探窟家における最高位のランクで、限界深度は無制限です。この位に到達した探窟家は数えるほどしかおらず、みな「伝説英雄」と称されています。
白笛の特別な呼び方

「奈落の星(ネザースター)」とも呼ばれます
各人ごとに「○○卿」という公式の異名が付きます
さらに「動かざるオーゼン」 などの二つ名も持っています

現職の白笛たち(物語開始時点)

「不動卿」動かざるオーゼン
「黎明卿」新しきボンドルド
「神秘卿」神秘のスラージョ
「先導卿」選ばれしワク ナ
「殲滅卿」殲滅のライザ(ラストダイブ中で公的には死亡扱い)

白笛の特殊な笛
白笛が持つ笛は他のランクと大きく異なります:

二級遺物「ユアワース」(命を響く石)を加工して作られている
深界六層降への立ち入りや特定の遺物を起動するキーの役割
所有者個人に合わせて特殊な原料・製法で生成
所有者以外では機能しない仕組み

主人公リコの母ライザも白笛の一人で、リコは母と同じ白笛を目してアビスの底を目指しています!

ちゃんと取得したドキュメントを使用して回答できていますね。

あとは、これをDiscordなどのコミュニケーションツールのBotに繋げたり、Webアプリのチャットボットとして使用したり、好きなところでサーバレスなRAGシステムを組み込むことができます。

まとめ

S3 Vectorsを使用することで、簡単かつ安価なサーバレスRAGシステムを構築することができました!

まだプレビュー段階なので仕様は変更される可能性はありますが、今後のAWSにおけるRAGシステム構築を一気に変える非常に素晴らしい機能だと思いました。気になる方はぜひ触ってみてください。



Source link

Views: 0

RELATED ARTICLES

返事を書く

あなたのコメントを入力してください。
ここにあなたの名前を入力してください

- Advertisment -