pgliteとpgvectorでDocker不要のローカル類似度検索を実装する

文章の「意味の近さ」で検索したいとき、多くの構成はベクトルDBやDockerで立てたPostgreSQLを前提にする。だが小規模な検証やテスト、ブラウザ内アプリなら、WASM版PostgreSQLのpgliteとベクトル拡張pgvectorを組み合わせるだけで、サーバもDockerも立てずに類似度検索が動く。この記事は、環境構築からEmbedding、距離演算子の使い分け、検索SQL、インデックスまでを実装目線でまとめ、旧来解説に多い距離演算子の取り違えも訂正する。

まとめ

  • pgliteはWebAssembly版のPostgreSQLで、Node.js・ブラウザ・Bun・Denoにnpmで組み込める。ローカル開発・テスト・ブラウザ内アプリでDockerのPostgreSQLコンテナが要らないのが最大の利点。
  • pgvector拡張はpgliteに別パッケージ(@electric-sql/pglite-pgvector)として同梱でき、Dockerや外部サーバなしでベクトル型と距離検索が使える。
  • 文章はEmbeddingモデルで数値ベクトルに変換して保存する。2026年時点の標準はOpenAIのtext-embedding-3-small(1536次元)/text-embedding-3-large(3072次元)で、完全ローカルならTransformers.jsも選べる。
  • 距離演算子は<=>がコサイン距離、<->がユークリッド距離、<#>が負の内積、<+>がL1距離。文章検索はコサイン距離(<=>)が標準
  • pgliteは単一接続の組み込みDBであり、多数同時接続や大規模データを捌く本番サーバの代替ではない。用途を見極めて使う。

以下、pgliteの立ち位置から順に、実装で迷いやすい点を具体的に見ていく。

pgliteの立ち位置——Docker不要でPostgreSQLを動かす仕組み

pgliteは、PostgreSQL本体をWebAssembly(WASM)へビルドし、TypeScriptクライアントから呼び出せるようにした組み込みデータベースだ。ElectricSQLが開発し、gzip約3MBと軽量で、Node.js・ブラウザ・Bun・Denoで動く。別プロセスもネイティブバイナリも不要で、2026年時点で週次1,000万ダウンロードに達している。データはメモリ、ブラウザではIndexedDB、Node.jsではファイルシステムに永続化できる。

DockerのPostgreSQLとの違いと使い分け

ローカル開発でPostgreSQLを使うとき、多くの人はdocker run postgresでコンテナを立てる。pgliteはこのコンテナを不要にする。アプリと同じプロセス内でSQLを実行するため、Dockerデーモンもポート管理も接続待ちもいらず、CIやテストでは起動が速く後片付けも要らない。ブラウザ内で完結するデモやオフラインアプリも、pgliteなら外部通信ゼロで組める。

一方でpgliteは単一接続・単一プロセスの組み込みDBで、ネットワーク越しの同時接続やレプリケーションは想定していない。複数のアプリサーバから共有する本番データベースや、数百万行を高スループットで捌く用途では、Dockerや管理サービスで動く通常のPostgreSQLを使う。pgliteが向くのは「開発・テスト・デモ・ブラウザ内」、Dockerや本番PostgreSQLが向くのは「共有・大規模・高並行」と切り分けると判断を誤らない。

SQLiteとの違い

同じ組み込みDBのSQLiteと比べると、pgliteの強みはPostgreSQL互換であることだ。vector型やJSONB、全文検索、ウィンドウ関数など、本番のPostgreSQLで使う機能をそのままローカルで再現できる。SQLite用のベクトル拡張(sqlite-vec等)もあるが、本番がPostgreSQLならpgliteでスキーマやSQLを共有でき、移行時の差異が小さい。ブラウザで完結させたい、かつPostgreSQLと同じSQLを使いたい、という条件で選ばれる。

pgliteとpgvectorの環境構築(Node.js/Python)

pgvectorはpgliteに標準同梱ではなく、拡張パッケージとして追加する。ここを取り違えてnpm install pgliteとすると動かない。正しいパッケージ名で導入する。

Node.jsでの導入とpgvector拡張の有効化

本体と拡張の2つをインストールする。

npm install @electric-sql/pglite @electric-sql/pglite-pgvector

PGliteインスタンス生成時にvector拡張を渡し、SQLで拡張を有効化する。

import { PGlite } from '@electric-sql/pglite'
import { vector } from '@electric-sql/pglite-pgvector'

const db = new PGlite({ extensions: { vector } })
await db.exec('CREATE EXTENSION IF NOT EXISTS vector;')

これでDockerも外部PostgreSQLもなく、ブラウザやNode.jsのプロセス内でベクトル検索の下地が整う。ブラウザで永続化したい場合はnew PGlite('idb://mydb', { extensions: { vector } })のようにIndexedDBのデータディレクトリを指定する。

Pythonから使う場合(py-pglite)

PythonでpgliteをアプリのランタイムDBとして常用する用途は想定されていない。Python側の主用途はテストで、py-pgliteがPGliteを起動して一時的なPostgreSQLをpytestに提供し、PostgreSQLをインストールせずに実DBでテストできる。pgvectorを使うテストは拡張付きで入れる。

pip install py-pglite[extensions]

py-pgliteはpglite_sessionpglite_engineといったpytestフィクスチャを提供し、テストごとにDBを生成・破棄して分離する。SQLAlchemyやSQLModelと組み合わせ、psycopgにvector型を登録すれば、埋め込み列を持つテーブルの作成・INSERT・類似度検索までテストで検証できる。本番のPython実装は通常のPostgreSQL+pgvectorに接続し、pgliteはそのテスト代役、と役割を分けるのが実態に合う。

vector型テーブルの作成とデータ登録

拡張を有効化したら、埋め込みを格納するvector列を持つテーブルを定義する。次元数はEmbeddingモデルの出力に合わせて固定する。

CREATE TABLE faqs (
  id serial PRIMARY KEY,
  question text,
  embedding vector(1536)
);

登録は通常のINSERTで、ベクトルを'[0.12, -0.34, ...]'の文字列として渡す。vector(1536)に対して次元がずれた配列を渡すとエラーで拒否されるため、Embedding生成側で次元を固定し、まとめて入れる際はバッチINSERTにするとよい。

文章のベクトル化(Embedding)とモデル選択

類似度検索の精度は、文章をどのモデルでベクトル化するかで大きく変わる。用途に応じてクラウドAPIかローカルモデルかを選ぶ。

Embeddingモデルの選び方(2026年時点)

OpenAIを使う場合、標準はtext-embedding-3-small(1536次元)とtext-embedding-3-large(3072次元)だ。精度重視なら3-large、コストとスループット重視なら3-smallで、いずれもdimensionsパラメータで次元を縮約できる(Matryoshka表現学習により品質劣化を抑えて短くできる)。旧来解説にあるtext-embedding-ada-002はレガシー扱いで、新規実装では3系を使う。

外部にテキストを送れない、あるいはブラウザ内で完結させたい場合は、Transformers.jsのSentence Transformers(例:all-MiniLM-L6-v2、384次元)をローカル推論で使える。pgliteと組み合わせれば、Embedding生成もベクトル保存も検索もすべてブラウザ内で動く完全ローカル構成になる。どのモデルを選んでも、テーブルのvector(N)の次元をモデル出力に一致させる点は共通だ。

距離演算子とコサイン類似度の使い分け

pgvectorは複数の距離演算子を持ち、これを取り違えると意図した類似度で並ばない。旧来の日本語解説には「<#>がコサイン、<=>がマンハッタン」とする誤りが散見されるが、pgvector公式の定義は次のとおりだ。

演算子 距離・指標 主な用途
<=> コサイン距離 文章・意味検索の標準
<-> ユークリッド距離(L2) 画像・座標など大きさが意味を持つ場合
<#> 負の内積 正規化済みベクトルの高速比較
<+> マンハッタン距離(L1) 外れ値の影響を抑えたい場合/pgvector 0.7で追加

バイナリベクトル向けにはハミング距離<~>、ジャッカード距離<%>もある。<#>がPostgreSQLの昇順インデックス制約に合わせて「負の」内積を返す点は運用上の注意点で、値の符号を意識しておく。

文章検索でコサイン類似度が標準とされる理由

コサイン距離(<=>)はベクトルの「向き」で近さを測り、長さ(大きさ)の違いを無視する。文章のEmbeddingは長い文ほどノルムが大きくなりがちだが、意味の近さは向きに現れるため、長短にかかわらず安定して類似判定できる。FAQ検索や文書検索、レコメンドといった自然言語のユースケースでコサイン類似度が第一候補になるのはこのためだ。類似度スコアとして扱うときは、コサイン距離を1 - (embedding <=> query)で「1に近いほど類似」に変換すると直感的になる。コサイン類似度の計算式や仕組みはコサイン類似度とは?計算式・仕組みと類似検索での実装で詳しく確認できる。画像特徴量やセンサー値など大きさ自体が意味を持つデータでは、ユークリッド距離(<->)やL1距離(<+>)が適する場面もあり、実データでベンチマークして選ぶのが確実だ。

類似度検索SQLの書き方

検索は、クエリ文をEmbeddingしたベクトルを距離演算子で比較し、近い順に並べるだけで書ける。外部検索エンジンを別に用意する必要はない。

基本の近傍検索とスコア表示

クエリベクトルに近いFAQを上位5件取り出すSQLは次のとおり。距離をそのままSELECTすればスコアも返せる。

SELECT id, question,
       embedding <=> '[0.12, -0.34, ...]' AS distance
FROM faqs
ORDER BY embedding <=> '[0.12, -0.34, ...]'
LIMIT 5;

アプリ側では、ユーザー入力をEmbeddingしてベクトル文字列を作り、この'[...]'に差し込む。距離が小さいほど類似しているので、昇順(ORDER BY)で近い順に並ぶ。

閾値やカテゴリで絞り込む

意味的に離れた結果を落とすには距離の閾値を、対象範囲を限定するにはWHERE句を組み合わせる。pgvectorの距離は通常のSQL条件と一緒に書ける。

SELECT question
FROM faqs
WHERE category = '返品'
  AND embedding <=> '[...]' < 0.35
ORDER BY embedding <=> '[...]'
LIMIT 3;

閾値(例では0.35)はデータとモデルで最適値が変わるため、実際の問い合わせ例で調整する。「一定以上似た候補が無ければ結果なし」といった制御も、この閾値で実装できる。

インデックスによる高速化(HNSW/IVFFlat)

行数が増えると、全件と距離を計算する全走査では遅くなる。pgvectorは近似最近傍探索(ANN)のインデックスとしてHNSWIVFFlatを持つ。インデックスの演算子クラスは、検索で使う距離に合わせる必要がある(コサインならvector_cosine_ops、L2ならvector_l2_ops、内積ならvector_ip_ops)。

-- コサイン距離用のHNSWインデックス
CREATE INDEX ON faqs USING hnsw (embedding vector_cosine_ops);

-- IVFFlat(事前にlists数を決め、ANALYZE後に使う)
CREATE INDEX ON faqs USING ivfflat (embedding vector_cosine_ops) WITH (lists = 100);

HNSWは事前学習が不要で挿入・削除に強く、更新の多いデータやリアルタイム検索に向く。IVFFlatはベクトル空間をクラスタに分けて一部だけ探索する方式で、静的な大量データを省メモリで扱える一方、投入後のANALYZElists調整が要る。更新頻度が高いならHNSW、データが固定で件数が多いならIVFFlat、と選ぶのが基本だ。

ただしpglite(WASM)はブラウザのメモリやスレッドに制約があり、インデックス構築は負荷が大きい。数千件程度までなら全走査でも数十ミリ秒で返ることが多く、無理にインデックスを張らない判断も有効だ。数十万件超を高速に捌く必要が出た時点で、Dockerや管理サービスの通常PostgreSQLへ移すのが現実的な線引きになる。

ハイブリッド検索・実用例とpgliteの限界

ベクトル検索は意味の近さに強い反面、固有名詞や型番の完全一致には弱い。実務では他の検索と組み合わせて弱点を補う。

全文検索との組み合わせ

PostgreSQLの全文検索(tsvector)でキーワードを含む行に絞り込み、その中をベクトル距離で並べ替えると、精度と速度のバランスが取れる。pgliteでも同じSQLがそのまま動く。BM25などのキーワード検索とベクトル検索をRRFで統合する設計はRAGでBM25とベクトル検索を統合するハイブリッド検索の仕組みで詳しく扱っている。ベクトル検索と意味検索(セマンティック検索)の位置づけの違いはベクトル検索とセマンティック検索の違いを参照すると整理しやすい。

FAQ・レコメンド・RAGへの発展

同じ仕組みで、FAQの類似質問検索(言い回しが違っても意味で一致)、商品説明文を使ったレコメンド、社内文書の意味検索が実装できる。生成AIのRAG(検索拡張生成)でも、pgvectorに埋め込みを溜めて関連文書を引くバックエンドとして使える。小規模な検証やデモをpgliteで素早く立ち上げ、要件が固まったら本番PostgreSQLへ同じスキーマで移す、という進め方が現実的だ。

pgliteを使うべきでない場面

pgliteは万能ではない。複数サーバから共有する本番DB、常時多数の同時接続、数百万〜数千万行の高スループット検索には向かない。単一接続・単一プロセス・WASMの性能上限という制約があるためだ。これらが要件なら、最初からDockerや管理サービスの通常PostgreSQL+pgvectorで組むべきで、pgliteは開発・テスト・ブラウザ内といった「軽く速く回す」領域に絞って使うと本領を発揮する。

よくある質問(FAQ)

pgliteはDockerのPostgreSQLの代わりになりますか?

ローカル開発・テスト・ブラウザ内アプリでは代わりになる。Dockerコンテナを立てずに同一プロセスでPostgreSQLとpgvectorが動くため、起動が速く後片付けも不要だ。ただし複数サーバからの共有や大規模・高並行の本番用途は対象外で、その場合は通常のPostgreSQLを使う。

Pythonでpgliteは使えますか?

主にテスト用途で使える。py-pgliteがPGliteを起動して一時的なPostgreSQLをpytestに提供し、PostgreSQLをインストールせず実DBでテストできる。pgvectorを使うテストはpip install py-pglite[extensions]で拡張付きにする。本番のPython実装は通常のPostgreSQLに接続する構成が基本だ。

類似度検索ではコサイン類似度とユークリッド距離のどちらを使うべきですか?

文章の意味検索ならコサイン距離(<=>)が標準だ。長さの違いを無視して向きで測るため、文の長短に左右されにくい。画像特徴量や座標など大きさが意味を持つデータでは、ユークリッド距離(<->)が適する場合がある。

OpenAIのどのEmbeddingモデルを使えばよいですか?

2026年時点ではtext-embedding-3-small(1536次元)かtext-embedding-3-large(3072次元)を使う。コスト重視なら3-small、精度重視なら3-largeで、dimensionsで次元を縮約できる。text-embedding-ada-002はレガシーのため新規では避ける。

pgliteとSQLiteはどう違いますか?

pgliteはPostgreSQL互換で、vector型・JSONB・全文検索など本番PostgreSQLの機能をローカルで再現できる。本番がPostgreSQLならスキーマやSQLを共有でき移行差異が小さい。SQLiteとの互換性が要る場合はSQLiteを選ぶ。

関連記事

資料請求

RELATED POSTS 関連記事