RDS (PostgreSQL) + Amazon Bedrockで始めるベクトル検索入門
はじめに
こんにちは。エンジニアの瀬沼です。
ECサイトなどで商品検索をするとき、ユーザーが知っている言葉と実際の商品名が一致しないことはよくあります。たとえば「ランニングシューズ」で検索したいのに商品名が「マラソン用スニーカー」だったり、「工場で引く白い線」と検索したいのに実際の商品名は「ラインテープ」だったり。このような意味検索を実現できるのが、ベクトル検索です。
一般的なデータベース検索ではLIKE '%keyword%'や全文検索インデックスを使ったキーワードマッチングが主流ですが、ベクトル検索では文章の「意味」を数値ベクトルとして表現し、類似度で検索結果をソートします。
本記事では、PostgreSQLのpgvector拡張機能とAmazon Bedrock(Titanモデル)を使ったベクトル検索の基本を、実際のSQLクエリを交えて解説します。
ベクトル検索の位置づけ
一般的なECサイトなどでは、ベクトル検索はキーワード検索を置き換えるものではなく、補完するものとして考えるのが実践的ではないかと考えています。
ベクトル検索は純粋に「意味的な類似度」で結果を返すため、ビジネス要件(特定メーカー商品の優先表示、在庫状況、利益率、季節商品のプロモーションなど)を直接反映することが難しい側面があります。そのため、検索結果をある程度コントロールできるキーワード検索と組み合わせることで、ユーザー体験とビジネス目標の両立が図れると考えています。
検索フローの例
文字列マッチングとの違い
文字列マッチング(LIKE検索) は、文字列が一致する商品のみを検索します。
SELECT * FROM products
WHERE name LIKE '%ドライバー%'
OR description LIKE '%ドライバー%';
この方法では「プラスドライバー」「マイナスドライバー」は検索できても、「ねじ回し」「精密工具」といった類似商品は見逃してしまいます。
ベクトル検索は、テキストを多次元ベクトル(例: 1024次元)に変換し、意味の近さで並べ替えます。文字が一致しなくても「意味が近い」ものが上位に来ます。
具体例:「工場で引く白い線」で検索した場合
| 検索方式 | 検索結果 |
|---|---|
文字列マッチングLIKE '%工場で引く白い線%'
|
0件ヒット (商品名に「工場で引く白い線」という文字列がない) |
| ベクトル検索 意味的な類似度で検索 |
✅ ラインテープ 白 50mm(距離: 0.12) ✅ フロアマーキングテープ(距離: 0.18) ✅ 区画線引きテープ(距離: 0.22) ※距離が小さいほど意味が近い |
ベクトル検索では、「工場で引く白い線」という意味を理解して、文字列が一致しなくても適切な商品を提案できます。
補足: 「工場で引く白い線」「熱いものを持つときの手袋」といった自然言語での検索は、検索ログを見ると意外と多く見られます。ユーザーは商品名を知らなくても、用途やシーンから検索したいというニーズを持っています。
このように、文字列マッチングとベクトル検索を組み合わせることで、検索の取りこぼしを防ぎつつ、ビジネス目標とユーザー体験の両立が図れると考えています。
技術構成
今回は、PostgreSQL(RDS)からAmazon Bedrockを直接呼び出す構成です。
PostgreSQL内からaws_ml拡張機能を使ってBedrockのAPIを呼び出すことで、SQL文の中でテキストをベクトルに変換したり、検索結果をリランキングできます。
技術スタック
-
Amazon Aurora PostgreSQL 15: ベクトルデータの保存と検索(
aws_ml拡張はAurora PostgreSQL専用) - pgvector拡張: ベクトル型とベクトル演算子を利用
- aws_ml拡張: PostgreSQL内から直接Bedrockを呼び出す(IAMロール/ポリシーの設定が必要。公式ドキュメントを参照ください。)
- Amazon Bedrock Titan: 汎用的な埋め込みモデル
PostgreSQLのセットアップ
1. 拡張機能のインストール
まず、PostgreSQLに必要な拡張機能をインストールします。
-- ベクトル検索機能を有効化
CREATE EXTENSION IF NOT EXISTS vector;
-- aws_ml経由でBedrockを呼び出すために有効化
CREATE EXTENSION aws_ml CASCADE;
2. テーブルの設計
ベクトル検索用のテーブルを作成します。
-- 商品テーブル(ベクトルも含む)
CREATE TABLE products (
id INTEGER PRIMARY KEY,
product_name TEXT,
embedding VECTOR(1024) -- ベクトル埋め込み(1024次元)
);
3. PostgreSQL関数でBedrockを呼び出す
PostgreSQL内から直接Bedrock APIを呼び出せる関数を定義します。入力したテキストをPostgreSQLのベクトル型に変換する関数です。aws_bedrock.invoke_model_get_embeddings()でBedrockを呼び出しています。
CREATE FUNCTION titan(text)
RETURNS vector(1024)
LANGUAGE sql
AS $$
SELECT aws_bedrock.invoke_model_get_embeddings(
'amazon.titan-embed-text-v2:0',
'application/json',
'embedding',
json_build_object('inputText', $1)::text
)::vector(1024);
$$;
-- titan()を経由してembedding化が可能になります
SELECT titan('ラインテープ');
-- 結果例: [-0.0234375, 0.015625, -0.01171875, ... ] (1024次元)
4. サンプルデータの投入とインデックス作成
-- サンプルデータの投入
INSERT INTO products (id, product_name, embedding) VALUES
(1, 'ラインテープ 白', titan('ラインテープ 白')),
(2, '区画線引きテープ 黄', titan('区画線引きテープ 黄')),
(3, '作業用ヘルメット 白 通気孔付き', titan('作業用ヘルメット 白 通気孔付き'));
-- インデックスの作成(HNSWを使用)
CREATE INDEX ON products
USING hnsw (embedding vector_cosine_ops);
インデックスについて
HNSWは高速なベクトル検索用のインデックスです。細かいパラメータ調整もできますが、今回はデフォルト設定で進めます。
ベクトル化の精度向上
今回は商品名のみをベクトル化していますが、実際の運用では商品の説明文、カテゴリ、規格、材質などを組み合わせてベクトル化することで、より精度の高い検索が期待できます。
実際のベクトル検索クエリ
基本的なベクトル検索
検索キーワードをベクトル化し、保存されたベクトルとの距離を計算して類似商品を検索します。
SELECT
product_name,
(1 - (embedding <=> titan('工場で引く白い線'))) * 100 AS similarity -- <=> はコサイン距離演算子
FROM
products
ORDER BY similarity DESC
LIMIT 3;
-- 結果
-[ RECORD 1 ]+-------------------------------
product_name | ラインテープ 白
similarity | 30.48747272752178
-[ RECORD 2 ]+-------------------------------
product_name | 区画線引きテープ 黄
similarity | 26.444516546207165
-[ RECORD 3 ]+-------------------------------
product_name | 作業用ヘルメット 白 通気孔付き
similarity | 25.750187128804626
簡単なテストデータを用意して実行した例です。similarityスコアを見ると、「工場で引く白い線」という検索クエリに対して、「ラインテープ 白」が意味的に最も近いため上位に来ていることが分かります。
さらなる精度向上:リランキング
ベクトル検索は「意味が近い候補」を集めることが得意ですが、上位にはノイズも混ざります。そこで、ベクトル検索で絞った候補をリランキングモデル(例:Cohere Rerank)で再評価し、最終順位を入れ替えることもできます。
今回の構成では、PostgreSQLで上位100件を取得したあと、その候補テキストをまとめてBedrock経由でリランキングモデルに渡すだけで実装できます。検索基盤を追加せずに「上位の当たり率」を上げられるので、精度改善の選択肢としてクエリの一例を紹介します。
WITH
-- ベクトル検索で候補100件を取得
candidates AS (
SELECT
id,
product_name,
row_number() OVER (ORDER BY embedding <=> titan('工場に引く白い線')) - 1 AS idx
FROM products
ORDER BY embedding <=> titan('工場に引く白い線')
LIMIT 100
),
-- 候補をJSON配列に変換
items_as_json AS (
SELECT json_agg(product_name ORDER BY idx) AS documents_json
FROM candidates
),
-- Cohereでリランキング
reranked AS (
SELECT aws_bedrock.invoke_model(
model_id := 'cohere.rerank-v3-5:0'::text,
content_type := 'application/json'::text,
accept_type := '*/*'::text,
model_input := json_build_object(
'query', '工場に引く白い線',
'documents', (SELECT documents_json FROM items_as_json),
'top_n', 100,
'api_version', 2
)::text
) AS response
)
-- リランキング結果を取得(JSONパース処理は長いため省略)
SELECT product_name, rerank_score
FROM candidates
JOIN json_to_recordset(...)
ORDER BY rerank_score DESC
LIMIT 10;
まとめ
本記事では、PostgreSQLのpgvectorとAmazon Bedrockを使ったベクトル検索の基本を解説しました。
ベクトル検索は、「工場で引く白い線」のような曖昧な表現に対して強力です。一方で、すべての検索をベクトル検索に置き換えてしまうと、ユーザーが明確な商品名で検索する場合にかえってUXが低下したり、ビジネス上の優先順位(在庫状況、利益率、プロモーション)を反映できず売上に悪影響を及ぼす可能性もあります。
そのため、従来の文字列マッチングで十分な結果が得られる場合はそちらを優先し、0件ヒットや少数ヒットの場合にベクトル検索を補完的に活用する、などといった使い分けが現実的です。最終的には、ユーザー体験とビジネス目標の両立を意識して設計するのが重要だと考えています。
この記事を書いた人
瀬沼 慎太郎
2023年中途入社
Discussion