どうも、ネクストスケープでFoundation CoE部の部長やっている太田です!!
はじめに
去年の8月までChatworkを使っており、社内標準であるTeamsへの移行を行いました。
でもChatworkにナレッジ的に会話が残っている、AWS使って何か出来ないかなってのが今回のブログの内容の起点となる。
やろうとしたこと
社内のChatwork過去ログをRAG化して、Teams Botから自然言語で検索できるようにしたかった。「あの件どうなってたっけ」「去年の〇〇プロジェクトで何か問題なかった?」みたいな質問に答えさせたい。
データはS3に溜まっていた。CSVが1,342ファイル、フィルタ後のメッセージ数は約883,413件。Embeddingには多言語対応のCohere Embed Multilingual v3(1,024次元)を使い、ベクトルDBはとりあえずChromaDBで動かそうと思っていた。
VPSのスペックは控えめだった。Lightsailのt3.medium、RAM 3.7GB。まあ行けるだろうと思っていた。全然行けなかった。
何度やっても75%で落ちた
処理はファイル単位のストリーミング、チェックポイントによる再開、署名切れ対策のBedrockクライアント定期リフレッシュ、重複ID排除と、思いつく対策は全部入れた。それでも75〜80%のところで毎回プロセスが落ちた。
ログにはこう出ていた。
chromadb.errors.InvalidDimensionException: Error loading hnsw index
クラッシュのたびにHNSWのインデックスファイルが中途半端な状態で残り、次回起動時に読み込めずに死ぬ。起動時に破損を検出してディレクトリごと削除する処理も入れたが、そうすると進捗がリセットされる。コードを直すたびにまた75%あたりで落ちる、というループが続いた。
「ChromaDBの使い方が悪いのか、何か設定が足りないのか」と思いながらしばらく試行錯誤していたが、ある時点でようやく計算した。
883,413件 × 1,024次元 × 4バイト(float32)= 約3.6GB
ChromaDBのHNSWインデックスはベクトルをメモリ上に保持する。VPSのRAMは3.7GB。OSとPythonのオーバーヘッドを乗せれば余裕でオーバーする。コードの問題ではなく、4GBしかないVPSでこのデータ量を動かそうとしていたこと自体が無理だった。75〜80%で落ちていたのは、その時点でメモリが物理的に限界に達していたからだ。
インスタンスタイプを一時的にアップグレードしてメモリを増やすか、OpenSearch Serverlessに移行するかを考えていたとき、去年 re:Invent に参加した時に発表された、S3 Vectorsを思い出した。
S3 Vectorsとは
AWSが2025年に発表した、S3の新しいバケット種別だ。ベクトルの保存と近似最近傍検索をフルマネージドで提供する。
ChromaDBとの最大の違いはメモリの話ではなく、インデックスがローカルに存在しないという点にある。データはS3に置かれ、QueryVectorsのAPIを叩けば検索が返ってくる。VPSのRAMは関係しない。
コストも魅力的だった。同じデータをOpenSearch Serverlessで動かすと月$86前後かかるが、S3 Vectorsなら月数円の見込みだ。
2026年3月時点ではus-east-1のみ対応で東京リージョンはまだ来ていない。Bedrock(Cohere Embed)は東京リージョンで動かしているので、クロスリージョン構成になる。
実装
バケットとインデックスの作成
import boto3
client = boto3.client("s3vectors", region_name="us-east-1")
client.create_vector_bucket(vectorBucketName="chatwork-vectors")
client.create_index(
vectorBucketName="chatwork-vectors",
indexName="chatwork-messages",
dataType="float32",
dimension=1024,
distanceMetric="cosine",
)
ベクトルの保存
vectors = [
{
"key": vid,
"data": {"float32": embedding},
"metadata": {
"text": text[:500],
"日時": str(row.get("日時", "")),
"ルーム名": str(row.get("ルーム名", "")),
"アカウント名": str(row.get("アカウント名", "")),
},
}
for vid, embedding, text, row in zip(ids, embeddings, texts, rows)
]
s3v.put_vectors(
vectorBucketName="chatwork-vectors",
indexName="chatwork-messages",
vectors=vectors,
)
検索
resp = s3v.query_vectors(
vectorBucketName="chatwork-vectors",
indexName="chatwork-messages",
queryVector={"float32": query_vector},
topK=5,
returnMetadata=True,
)
ルーム名でフィルタをかける場合はこう書く。
resp = s3v.query_vectors(
...
filter={"ルーム名": {"eq": "開発チーム"}},
)
ここでもハマった
AmazonS3FullAccessでは動かない
IAMのポリシーにAmazonS3FullAccessを付けていたが、S3 Vectorsは別サービス扱いでこのポリシーに含まれていない。AccessDeniedExceptionが出るまで気づかなかった。以下のインラインポリシーを追加する必要がある。
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": "s3vectors:*",
"Resource": "*"
}
]
}
メタデータの型指定はDynamoDB形式ではない
ドキュメントをちゃんと読んでいなくてDynamoDB風の型指定を使ってしまった。
# これは動かない
"metadata": {"ルーム名": {"S": "開発チーム"}}
# 正しい
"metadata": {"ルーム名": "開発チーム"}
ValidationException: Metadata values must be strings, numbers, booleans, or arraysというエラーが出る。文字列・数値・真偽値・配列をそのまま渡す。
ChromaDB構成との比較
| ChromaDB(旧) | S3 Vectors(新) | |
|---|---|---|
| 取り込み時のメモリ | ~3.6GB(上限超え) | ほぼゼロ |
| 検索時のメモリ | ~3.6GB(常駐) | ほぼゼロ |
| インデックス破損 | 頻発 | なし |
| クラッシュ後の復旧 | インデックス消去が必要 | データはS3に残る |
| 月額コスト(883k件) | VPS代のみ(動作不可) | 数円 |
| インフラ管理 | 必要 | 不要 |
現在の状態
この記事を書いている時点でも、バックグラウンドでingestが動いている。チェックポイントを入れているので、止まれば次回実行で続きから再開する。ChromaDBのときと違い、75%で落ちる恐怖がない。
完走したら、検索精度の評価とブログの続きを書く予定だ。
Teams Botとして動かす
RAGが動くことを確認してから、TeamsのチャンネルにLambdaを繋いだ。
構成はシンプルだ。Power AutomateでTeamsのメッセージ投稿をトリガーにして、API Gateway経由でLambdaを呼び、返ってきた回答をそのままチャンネルに投稿する。
[Teams: Chatwork Library チャンネル]
→ Power Automate
→ API Gateway
→ Lambda(Embed → S3 Vectors検索 → Claude回答生成)
→ Teamsに返信投稿
Lambda自体はFunction URLで外部公開しようとしたが、アカウントレベルのパブリックアクセスブロックに阻まれて403が返り続けた。HTTP APIのAPI Gatewayを前に置いて回避した。
HTTP APIはAPIキー機能を持っていないため、Lambda側でカスタムヘッダー(x-api-key)の検証を実装した。
API_KEY = "..."
def lambda_handler(event, context):
headers = event.get("headers", {})
provided_key = headers.get("x-api-key") or headers.get("X-Api-Key")
if provided_key != API_KEY:
return {"statusCode": 401, "body": json.dumps({"error": "Unauthorized"})}
...
Power Automateでもいくつか詰まった。
Teamsのメッセージ本文はHTML形式で来るため、<p>テスト</p>のようにタグが混入する。replace()で除去する「作成」ステップをHTTPアクションの前に挟んだ。
replace(replace(replace(triggerBody()?['body']?['content'], '<p>', ''), '</p>', ''), '<br>', ' ')
「チャネル内のメッセージで応答します」アクションはBot Frameworkを経由するせいか404エラーが続いたため、「チャネルにメッセージを投稿する」に切り替えた。こちらは安定して動いた。
実際に動かしてみて
「XXXXX案件の残課題ある?」と投げると、こんな回答が返ってきた。
リリース後の動作確認で内部課題が複数出ていることが言及されています。「残件」については、XXXXX案件の状況を見ながら対応する予定となっています。XXXXXとして「XXXXXの残タスク」があり、Redmineのチケット(#9999999999)として管理されています。
※機密情報はマスクした。
実際のチャットワークのログから具体的な情報が引けている。距離スコアは0.28〜0.35あたりで、意味的に近いメッセージをちゃんと拾えている。
月額コスト
100クエリ/日(3,000クエリ/月)で試算するとこのくらいになる。
| サービス | 月額目安 |
|---|---|
| S3 Vectors(431,718件保存 + クエリ) | ~$1〜2 |
| Bedrock Cohere Embed(クエリ時) | ~$0.3 |
| Bedrock Claude Sonnet(回答生成) | ~$15〜20 |
| Lambda + API Gateway | ~$0.01 |
| 合計 | ~$17〜23 |
コストの大半はClaudeの回答生成だ。クエリ数が少なければ月$2〜3で収まる。Claude Haikuに変えれば約1/20になる。
ChromaDBが動かなかったVPS代を払い続けていたことを考えると、S3 Vectorsへの移行は正解だった。
さいごに
ほぼ趣味のような感じでやってみたが楽しかった。
S3 Vectorsは発表時から触ってみたかったサービスだったから試せてよかった。
参考