0
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?

【SaaS PoC Phase3】AWS SAM + pgvectorでSaaS PoC Phase 3を作る

0
Posted at

— multilingual-e5-smallでEmbedding・類似度検索・Browser表示まで

1. はじめに

前回のPhase 2では、AWS SAM Local上のLambda/PythonからSQLiteへデータを保存し、Browserから次の操作ができるところまで作成した。

Browser
  ↓
SAM Local API
  ↓
Lambda / Python
  ↓
SQLite

実装済みの主なAPIは次のとおり。

POST /items
GET  /items
GET  /items/{id}

POST /files
GET  /files
GET  /files/{id}

Phase 3では、この保存済みデータを使って意味検索を実装する。

目標は次の構成。

保存済みデータ
↓
Embedding
↓
Vector DB
↓
類似度検索
↓
Top 10
↓
Browser表示

今回は以下を採用した。

Embedding:
multilingual-e5-small

Vector DB:
PostgreSQL + pgvector

距離:
Cosine Similarity

Embedding次元:
384

2. Phase 3の進め方

いきなりBrowserまで作らず、3段階に分けた。

Phase 3-A
PostgreSQL + pgvector
↓
vector INSERT
↓
similarity query
↓
Top 10

Phase 3-B
既存Item
↓
Embedding
↓
VECTOR(384)
↓
PostgreSQL保存

Phase 3-C
Search API
↓
Browser UI

障害が起きたとき、

DBなのか
Embeddingなのか
APIなのか
Browserなのか

を切り分けやすくするためである。


3. PostgreSQL + pgvectorをDockerで起動する

まずVector DBだけを作る。

.env.phase3 を作成する。

cat > .env.phase3 <<'EOF'
POSTGRES_DB=saas_poc
POSTGRES_USER=saas_poc
POSTGRES_PASSWORD=<LOCAL_PASSWORD>
EOF

Gitには含めない。

echo '.env.phase3' >> .gitignore

compose.yaml を作成。

services:
  vector-db:
    image: pgvector/pgvector:0.8.6-pg18-bookworm
    container_name: saas-poc-vector-db

    environment:
      POSTGRES_DB: ${POSTGRES_DB}
      POSTGRES_USER: ${POSTGRES_USER}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}

    ports:
      - "127.0.0.1:5432:5432"

    volumes:
      - saas_poc_pgdata:/var/lib/postgresql

    healthcheck:
      test:
        [
          "CMD-SHELL",
          "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"
        ]
      interval: 5s
      timeout: 5s
      retries: 10

volumes:
  saas_poc_pgdata:

起動。

docker compose \
  --env-file .env.phase3 \
  up -d vector-db

確認。

docker inspect \
  --format='{{.State.Health.Status}}' \
  saas-poc-vector-db

結果。

healthy

これでPostgreSQLが利用可能になった。


4. pgvectorを有効化する

PostgreSQLへ入り、vector extensionを有効化する。

docker exec -i saas-poc-vector-db \
  psql -U saas_poc -d saas_poc <<'SQL'

CREATE EXTENSION IF NOT EXISTS vector;

SELECT
    extname,
    extversion
FROM pg_extension
WHERE extname = 'vector';

SQL

確認結果。

 extname | extversion
---------+------------
 vector  | 0.8.6

pgvectorが正常に読み込まれた。


5. まず3次元のダミーベクトルで試す

実際のEmbeddingをいきなり使う前に、Vector DB単体の動作を確認した。

CREATE TABLE vector_test (
    id BIGSERIAL PRIMARY KEY,
    label TEXT NOT NULL,
    embedding VECTOR(3) NOT NULL
);

ここでは意図的に、

第1軸 = 果物
第2軸 = 乗り物
第3軸 = 動物

のような人工的なベクトルを作る。

INSERT INTO vector_test (label, embedding)
VALUES
    ('apple',      '[1.00, 0.00, 0.00]'),
    ('orange',     '[0.95, 0.10, 0.00]'),
    ('banana',     '[0.90, 0.20, 0.00]'),
    ('grape',      '[0.80, 0.30, 0.10]'),

    ('car',        '[0.00, 1.00, 0.00]'),
    ('truck',      '[0.10, 0.95, 0.00]'),
    ('motorcycle', '[0.20, 0.90, 0.10]'),
    ('bicycle',    '[0.30, 0.80, 0.10]'),

    ('cat',        '[0.00, 0.00, 1.00]'),
    ('dog',        '[0.10, 0.00, 0.95]'),
    ('lion',       '[0.10, 0.10, 0.90]'),
    ('tiger',      '[0.20, 0.10, 0.85]');

6. Cosine SimilarityでTop 10を取得する

果物方向 [1,0,0] を検索する。

SELECT
    id,
    label,
    ROUND(
        (1 - (embedding <=> '[1,0,0]'::vector))::numeric,
        4
    ) AS similarity
FROM vector_test
ORDER BY embedding <=> '[1,0,0]'::vector
LIMIT 10;

結果。

apple
orange
banana
grape
...

乗り物方向では、

car
truck
motorcycle
bicycle

動物方向では、

cat
dog
lion
tiger

が上位になった。

pgvectorによる

Vector
↓
Cosine Distance
↓
ORDER BY
↓
Top 10

が正常に動作した。


7. Python 3.11環境を作る

この環境ではOS標準Pythonが3.14だった。

Python 3.14.4

ただし既存のPhase 2 LambdaはPython 3.11で動作しているため、Phase 3も3.11へ揃えることにした。

Ubuntu標準リポジトリにはPython 3.11が存在しなかったため、uv を利用した。

curl -LsSf https://astral.sh/uv/install.sh | sh

Python 3.11をインストール。

~/.local/bin/uv python install 3.11

結果。

Python 3.11.16

仮想環境を作成。

~/.local/bin/uv venv \
  --python 3.11 \
  .venv-phase3

有効化。

source .venv-phase3/bin/activate

確認。

Python 3.11.16

8. PyTorchのCUDA版でディスク容量を使い切る

最初に普通に、

pip install sentence-transformers

を実行した。

ところがPyTorchがCUDA関連パッケージまで取得し始めた。

例:

torch
cuda-toolkit
nvidia-cudnn
nvidia-cublas
nvidia-cusparse
...

最終的に、

ERROR:
Disk quota exceeded

となった。

今回のPoCではGPUを使わない。

そこでCPU版PyTorchだけをインストールした。

~/.local/bin/uv pip install \
  --python .venv-phase3/bin/python \
  torch \
  --index-url https://download.pytorch.org/whl/cpu

確認。

import torch

print(torch.__version__)
print(torch.cuda.is_available())

結果。

2.14.0+cpu
False

これで巨大なCUDA依存を避けられた。


9. Sentence Transformersを導入する

続いて、

~/.local/bin/uv pip install \
  --python .venv-phase3/bin/python \
  sentence-transformers

今回使った主なバージョン。

Python                3.11.16
PyTorch               2.14.0+cpu
Sentence Transformers 6.0.1

Embeddingモデルは、

intfloat/multilingual-e5-small

を利用する。


10. multilingual-e5-smallを動かす

テストコード。

from sentence_transformers import SentenceTransformer

MODEL_NAME = "intfloat/multilingual-e5-small"

model = SentenceTransformer(
    MODEL_NAME,
    device="cpu"
)

texts = [
    "passage: AWS SAMでLambda APIを構築する",
    "passage: 猫はソファーで眠っている",
    "query: AWS Lambdaの開発方法",
]

embeddings = model.encode(
    texts,
    normalize_embeddings=True
)

print(embeddings.shape)

結果。

(3, 384)

つまり、

1文章
↓
384個の数値

へ変換される。

また、

AWS Lambdaの開発方法

というQueryは、

AWS SAMでLambda APIを構築する

とのSimilarityが猫の文章より高くなった。

意味的な類似度が取れていることを確認できた。


11. QEMU/KVMでIllegal instructionが発生する

次に12件の文章をまとめてEmbeddingしようとしたところ、

Illegal instruction (core dumped)

が発生した。

CPU情報を確認。

lscpu

結果。

Model name:
QEMU Virtual CPU version 2.5+

Hypervisor vendor:
KVM

CPU Flagsには、

sse
sse2
sse4_1
sse4_2

などは見えるが、

avx
avx2

は存在しなかった。

PyTorch単体の行列計算は動作した。

import torch

x = torch.randn(128, 128)
y = x @ x

そこでCPU Capabilityを明示的に制限した。

ATEN_CPU_CAPABILITY=default

実行。

ATEN_CPU_CAPABILITY=default python script.py

するとEmbeddingが正常に動作した。

今回の重要な実地知見の一つになった。

QEMU/KVM
↓
AVX / AVX2なし
↓
SentenceTransformer encode
↓
Illegal instruction
↓
ATEN_CPU_CAPABILITY=default
↓
正常動作

12. Phase 2のItemをテストデータとして登録する

Phase 2のSQLiteはLambdaコンテナの /tmp を使っているため、コンテナ再作成後はItemが空になっていた。

[]

そこで意味検索用のItemを12件登録した。

分類は、

AWS系
動物系
乗り物系

の3グループ。

例:

{
  "title": "AWS SAM",
  "text": "AWS SAMを使ってLambda APIをローカル開発する"
}
{
  "title": "トラ",
  "text": "トラは縞模様を持つ大型のネコ科動物である"
}
{
  "title": "トラック",
  "text": "トラックは荷物を道路で輸送するための車両である"
}

13. title + textをEmbeddingする

今回の検索対象は、

items.title + items.text

とした。

E5では文書側に、

passage:

を付ける。

texts = [
    f"passage: {item['title']} {item['text']}"
    for item in items
]

Embedding。

embeddings = model.encode(
    texts,
    normalize_embeddings=True
)

結果。

items: 12
embedding shape: (12, 384)

すべて、

dimension = 384
norm = 1.0

になった。


14. PostgreSQLへ384次元Vectorを保存する

実データ用テーブルを作る。

CREATE TABLE item_vectors (
    item_id TEXT PRIMARY KEY,
    title TEXT NOT NULL,
    text TEXT NOT NULL,
    source_created_at TIMESTAMPTZ NOT NULL,
    embedding_model TEXT NOT NULL,
    embedding VECTOR(384) NOT NULL,
    embedded_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

PythonからPostgreSQLへ接続するため、

uv pip install \
  "psycopg[binary]" \
  pgvector

を追加した。

保存処理では、

from pgvector.psycopg import register_vector

register_vector(conn)

してからINSERTする。

12件すべて保存後、

SELECT COUNT(*)
FROM item_vectors;

結果。

12

さらに、

SELECT
    title,
    vector_dims(embedding)
FROM item_vectors;

を確認すると、全件、

384

だった。


15. Search APIをFastAPIで作る

EmbeddingモデルをSAM/Lambdaへ直接入れると構成が重くなるため、Phase 3では検索APIを分離した。

構成。

Browser
↓
Search API :3001
↓
Embedding
↓
PostgreSQL + pgvector

FastAPIを導入。

uv pip install fastapi uvicorn

検索処理の中心部分。

query_vector = model.encode(
    f"query: {query_text}",
    normalize_embeddings=True,
)

SQL。

SELECT
    item_id,
    title,
    text,
    embedding_model,
    1 - (embedding <=> %s) AS similarity
FROM item_vectors
ORDER BY similarity DESC
LIMIT %s

APIは、

GET /health
GET /search?q=...

とした。


16. Search APIを起動する

QEMU/KVM対策を含めて起動する。

ATEN_CPU_CAPABILITY=default \
python -m uvicorn \
  src.search_api.app:app \
  --host 0.0.0.0 \
  --port 3001

Health Check。

curl http://127.0.0.1:3001/health

結果。

{
  "status": "ok",
  "model": "intfloat/multilingual-e5-small",
  "dimension": 384
}

17. 日本語の意味検索を試す

例えば、

Lambdaをローカルで開発したい

で検索。

上位。

AWS SAM
AWS Lambda
Amazon DynamoDB
Amazon S3

次に、

大型のネコ科動物

結果。

1. トラ
2. ライオン
3. 犬
4. 猫

Similarityは、

トラ      0.8928
ライオン  0.8892
犬        0.8168
猫        0.8131

だった。

さらに、

荷物を運ぶ道路車両

では、

トラック
自動車
自転車
オートバイ

が上位になった。

単純な文字列一致ではなく、意味的な近さで順位付けできている。


18. Browser UIへ意味検索を追加する

Phase 2のBrowser UIに、

8. 意味検索

を追加した。

検索APIは、

const SEARCH_API_BASE =
    `http://${location.hostname}:3001`;

として呼び出す。

検索。

const response = await fetch(
    `${SEARCH_API_BASE}/search?` +
    new URLSearchParams({
        q: query,
        limit: "10"
    })
);

結果には、

順位
タイトル
Similarity
本文
item_id

を表示した。

Browserから、

大型のネコ科動物

を検索すると、

1. トラ
2. ライオン
3. 犬
4. 猫
...

とTop 10が表示された。


19. CORSを設定する

Browserは:8000、Search APIは:3001なので別Originになる。

FastAPI側でCORSを許可した。

app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],
    allow_credentials=False,
    allow_methods=["GET"],
    allow_headers=["*"],
)

確認。

curl -i \
  -H 'Origin: http://127.0.0.1:8000' \
  --get \
  --data-urlencode 'q=大型のネコ科動物' \
  --data-urlencode 'limit=3' \
  http://127.0.0.1:3001/search

レスポンス。

HTTP/1.1 200 OK
access-control-allow-origin: *

BrowserからSearch APIを呼べることを確認した。


20. 最終構成

Phase 3完了時点の構成は次のようになった。

Browser :8000
│
├─ Phase 2
│   ↓
│  SAM API :3000
│   ↓
│  Lambda / Python 3.11
│   ↓
│  SQLite /tmp
│
└─ Phase 3
    ↓
   Search API :3001
    ↓
   multilingual-e5-small
    ↓
   384-dimensional Embedding
    ↓
   PostgreSQL 18
    ↓
   pgvector 0.8.6
    ↓
   Cosine Similarity
    ↓
   Top 10

21. 今回ハマったポイント

今回特に重要だったのは次の3点だった。

21.1 Pythonのバージョン

OS標準はPython 3.14だったが、既存Lambdaに合わせてPython 3.11へ統一した。

uv を使うことでOSのPythonを変更せずに済んだ。

21.2 CUDA版PyTorch

普通にPyTorchを入れるとCUDA関連パッケージまで取得し、ディスク容量を大量に消費した。

今回はGPU不要なので、

CPU-only PyTorch

を明示した。

21.3 QEMU/KVMとIllegal instruction

Embedding実行時に、

Illegal instruction

が発生した。

QEMU Virtual CPUでAVX/AVX2が公開されていない環境だった。

最終的に、

ATEN_CPU_CAPABILITY=default

を付けることで動作した。


22. Phase 2とPhase 3で保存先の性質が違う

Phase 2は、

/tmp/saas-poc/metadata.db

を利用している。

Lambda/SAMコンテナが再生成されると消える可能性がある。

一方Phase 3のPostgreSQLは、

Docker named volume

を利用している。

saas-poc_saas_poc_pgdata

そのため、

元Itemは消えた
Vectorは残っている

という状態も起こり得る。

PoCとしては許容しているが、本番設計では元データとVectorデータのライフサイクルを揃える必要がある。


23. Gitへ保存する

最終コミット。

git commit \
  -m "feat: add Phase 3 semantic vector search"

結果。

9d969d2 feat: add Phase 3 semantic vector search

最終状態。

branch: main
working tree: clean

Phase 2最終コミット、

06d13b6

からPhase 3の、

9d969d2

まで接続できた。


24. Phase 3でできるようになったこと

Phase 2までは、

WHERE id = ?

というキー検索だった。

Phase 3では、

「大型のネコ科動物」

のような自然文を入力すると、

トラ
ライオン
犬
猫
...

という意味的に近いデータを取得できるようになった。

つまり、

IDを知っているデータを探す

から、

意味的に近いデータを探す

へ検索方式を拡張できた。


25. 今後

今回のPhase 3はVector SearchのPoCであり、まだ本番向けではない。

今後の候補としては、

・SQLiteから本番永続DBへの移行
・ファイル本文抽出
・Chunk分割
・ファイル本文のEmbedding
・HNSW / IVFFlat Index
・Vector更新処理
・削除同期
・認証 / 認可
・本番デプロイ
・RAG

などがある。

特にPhase 2で実装したファイルアップロードとPhase 3のVector Searchを接続すれば、

ファイルアップロード
↓
本文抽出
↓
Chunk
↓
Embedding
↓
Vector DB
↓
意味検索

まで発展させられる。


26. まとめ

今回のPhase 3では、

PostgreSQL
+
pgvector
+
multilingual-e5-small
+
FastAPI
+
Browser

を組み合わせ、意味検索の一連の流れを構築した。

最終的には、

Browser
↓
自然文Query
↓
Embedding
↓
pgvector
↓
Cosine Similarity
↓
Top 10
↓
Browser表示

まで動作した。

特に今回、

DB
Embedding
API
Browser

を一段ずつ確認していったことで、Pythonバージョン、CUDA依存、QEMU/KVMのCPU命令問題などが発生しても、原因を切り分けながら進められた。

Phase 3としては、これでひとまず完成とする。

0
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
0
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?