— 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としては、これでひとまず完成とする。