pgvector/embeddingによるセマンティック検索基盤の構築
Atlas Community 版は PostgreSQL 拡張を管理できない — CREATE EXTENSION の運用パターン
4か月前
Atlas(Community 版)で宣言的スキーマから migration を差分生成する構成では、desired schema に CREATE EXTENSION を含めると「extensions are available to logged-in users only」エラーになる(拡張管理は Pro 機能)。pgvector など拡張前提の型を使う場合は次のパターンで回避する。
運用パターン:
- 拡張の作成は自動生成の対象から外し、手書き migration(
CREATE EXTENSION IF NOT EXISTS ...)として migration ディレクトリに追加する。タイムスタンプは後続の自動生成 migration より古くする。 - desired schema(schema.sql 等)には拡張を含めない。Community 版は拡張を inspect しないため、DB 側にだけ拡張が存在しても drift とは判定されず、以後の差分生成と共存できる。
- golang-migrate 形式を使っている場合、手書きファイル追加後に
atlas migrate hashで atlas.sum を更新しないと整合性エラーになる。
dev database への配慮: Atlas は差分計算用の dev database に migration と desired schema を replay するため、拡張型のカラム(例: vector(768))を解決するには dev DB 側でも拡張が使える必要がある。手書き migration が replay されるので通常は揃うが、新規環境向けには DB コンテナの initdb スクリプト(/docker-entrypoint-initdb.d/)でも拡張を作成しておくと確実。既存ボリュームには initdb が走らない点に注意し、一度だけ手動で CREATE EXTENSION を実行する。
検証方法: 手書き migration 追加 → 差分生成 → 適用 → もう一度差分生成して「no changes to be made」になれば、自動生成パイプラインと衝突していない。
Go の Bun ORM + pgvector をスキーマ自動生成と共存させる方法
4か月前
Bun ORM のモデル定義から DDL を自動生成する構成に pgvector のベクトルカラムを組み込む場合、pgvector-go パッケージの pgvector.Vector 型をフィールドに使い、bun:"type:vector(768)" のように型タグで次元を明示する。これで CreateTable の DDL 生成、INSERT 時のベクトルリテラル化([0.1,0.2,...] 形式)、SELECT の scan がすべて素通りする。[]float32 を直接フィールドにすると Postgres 配列形式({...})で書き込まれて vector 型と互換がないため通らない。
落とし穴:
- belongs-to リレーションを定義しても CreateTable の FK 自動生成が効かないケースがある。FK が必要なら明示的に DDL を出力する(深追いするより明示定義が確実)。
- カラムの次元数と embedding API の出力次元は必ず一致させる。API 側のレスポンス次元をサービス層で検証してから保存すると、モデル変更時の混入を防げる。
- 類似検索はコサイン距離演算子
<=>を使えばベクトルの正規化は不要(距離計算自体が正規化を含む)。1 - 距離を類似度スコアとして返すと扱いやすい。
インデックスの判断基準: 数千件規模までは索引なしの全件スキャンで十分速い(1 ベクトル 768 次元 ≒ 3KB)。件数が増えて遅くなってから HNSW インデックスを後付けすればよく、最初から張る必要はない。pgvector のインデックスは 2000 次元が上限のため、高次元モデルを使う場合は出力次元の縮小(例: 768)を先に決めておく。
追加インフラなしでセマンティック検索を導入する構成(既存 PostgreSQL + 埋め込み API)
4か月前
個人〜小規模サービスにセマンティック検索を入れる場合、専用ベクトル DB(Pinecone、Qdrant 等)を増やすより、既存の PostgreSQL に pgvector 拡張を入れ、埋め込み生成は外部 API に任せる構成が費用・運用の両面で有利。managed PostgreSQL(Render 等)は pgvector を標準サポートしており CREATE EXTENSION だけで使える。埋め込み API(Gemini の embedding モデル等)は個人メモ規模なら月数円以下で済む。
設計の要点:
- embedding は本体テーブルに混ぜず、1:1 の別テーブル(
<entity>_embeddings)に分離する。モデル変更による再生成・次元変更時に本体スキーマへ影響せず、未生成レコードの検出(LEFT JOIN で NULL)も簡単になる。 - embedding 生成は本体の保存トランザクションの成否に影響させない。保存成功後に同期生成し、失敗はログのみで縮退(そのレコードが検索に出ないだけ)。これで外部 API 障害がコア機能を壊さない。
- 既存データには backfill 用のバッチ処理を用意する。1 回あたりの件数上限と「残りがあるか」フラグを返すページング設計にすると、API レートやタイムアウトに収まる。新規・更新分は保存フックで自動生成されるので backfill は導入時の一回もの。
- 埋め込み API のタスクタイプは保存側を文書用(RETRIEVAL_DOCUMENT)、検索側をクエリ用(RETRIEVAL_QUERY)に分けると検索精度が上がる。
併用の判断: セマンティック検索は言い換えに強い一方、ID・コマンド名・固有名詞の完全一致に弱い。既存のキーワード検索(ILIKE / 全文検索)は廃止せず用途別に併存させ、必要になったらスコア統合(ハイブリッド検索)へ進化させる余地を残す。
Embedding(埋め込み)の基礎と実務上の性質
4か月前
Embedding(埋め込み)は、テキストを固定長の数値ベクトル(数百〜数千個の浮動小数点数)に変換したもの。「意味が近いテキストほどベクトル空間上で近い位置になる」よう設計され、テキストに「意味の座標」を与える操作。
作られ方
embedding モデル(Gemini gemini-embedding-001、OpenAI text-embedding-3 等)は Transformer ベース。対照学習で「意味的に関連するペアは近く、無関係なペアは遠く」なるよう訓練される(LLM は次単語予測、embedding は意味の距離が正しくなるよう訓練、という違い)。各次元に人間可読な意味はなく、意味は全次元の位置関係として分散的に表現される。
近さ=コサイン類似度
ベクトルの近さは一般にコサイン類似度(なす角度)で測る。同方向=1、直交=0。長さでなく向きを見るのは、長さはテキスト長に影響され意味は向きに現れるため。pgvector のコサイン距離演算子は距離(=1−類似度)を返すので「1−距離」をスコアにし、距離が小さい順に上位 N 件を取れば最近傍検索。目安は 0.85 以上でほぼ同内容、0.7 前後で関連あり、0.5 以下でほぼ無関係(モデル依存なので絶対視しない)。
キーワード検索との違い
キーワード検索(ILIKE/全文検索)は文字列の表層一致で、識別子の完全一致に強いが言い換えに弱い。セマンティック検索は意味の座標で、言い換え(「DBの移行」↔「マイグレーション」)に強いが識別子に弱い。上位互換でなく相互補完なので両方を併存させるのが定石。
実務上の性質
- タスクタイプ: 検索は非対称。保存は RETRIEVAL_DOCUMENT、検索クエリは RETRIEVAL_QUERY を指定すると対応ペアが近づく。
- 次元削減: gemini-embedding-001 は素で 3072 次元だが outputDimensionality で縮小可(重要情報を前方次元に詰める「マトリョーシカ表現学習」のため)。768 次元で精度の大部分を保ち、ストレージ 1/4、pgvector のインデックス上限 2000 次元内に収まる。
- モデルロックイン: 別モデル/別次元設定のベクトルは座標系が別物で比較不能。モデル変更時は全データ再生成が必須なので、embedding は本体と別テーブルに分離しておくと安全。
- 入力長: モデルにトークン上限あり(gemini-embedding-001 は 2048)。超過は切り捨てなので長文はチャンク分割。メモ程度なら 1テキスト=1ベクトルで十分。
検索以外の用途
同じ「意味の座標」で、RAG(質問に近い文書を LLM に渡す)/重複検知/クラスタリング/レコメンドが同じ仕組みで実現できる。embedding 生成パイプラインを一度作れば土台を流用できる。