ククログ

株式会社クリアコード > ククログ > PostgreSQL Conference Japan 2025:PostgreSQLでのセマンティックサーチへの挑戦 #pgcon25j

PostgreSQL Conference Japan 2025:PostgreSQLでのセマンティックサーチへの挑戦 #pgcon25j

PostgreSQL Conference Japan 2025に参加して、「PostgreSQLでのセマンティックサーチへの挑戦」というタイトルで発表をした堀本と阿部です。

スライドを公開しましたが、補足・解説テキストを追加した記事も残します。

発表の内容

主に次の4つについて発表しました。

  1. キーワード検索とセマンティックサーチの比較
  2. セマンティックサーチを実現するために
  3. PostgreSQLでセマンティクサーチ
  4. 性能とユースケース

この記事では特に補足説明が必要なところだけ抜粋して説明していきます。

1. キーワード検索とセマンティックサーチの比較

「ほぼ同じ意味だけど表現が少し違うデータ」を例にキーワード検索とセマンティックサーチの違いを説明しました。 スライドの説明で内容はおわかりいただけると思いますので、本記事ではセマンティックサーチの課題についてだけ補足します。

スライドを見ると、セマンティックサーチはあらゆる検索課題を解決してくれる万能な技術のように感じられるかもしれません。 しかし、意味の関連性に基づいて情報を探し出す仕組みである一方で、必ずしもユーザーの意図と完全に一致した結果が得られるとは限りません。

まずキーワード検索で必要な条件に合う範囲に絞り込み、その結果を意味的な類似度の高い順に並べ替える(リランク)、という組み合わせ方をすると、より目的に合った情報にたどり着きやすくなることもあります。 これは「キーワード検索 + リランク」の例ですが、キーワード検索とセマンティックサーチのそれぞれで対象ドキュメント絞り込み、それをリランクするというアプローチもあります。

重要なのは検索の要件や目的に応じて、キーワード検索とセマンティックサーチを適切に使い分ける、あるいは組み合わせて使うことです。

2. セマンティックサーチを実現するために

「IVF(Inverted File Index)」について説明していますが、スライド中に「IVF」という単語が登場しないので、その点だけ補足します。

スライドでいうとこれこれで説明しているのがIVFです。

IVFは検索対象のベクトルデータをあらかじめ複数のクラスタに分類し、それぞれのクラスタを代表するベクトルをインデックスとして使う方式です。

検索時にはクエリに最も近い代表ベクトルを持つクラスタを特定し、そのクラスタ内のベクトルのみを探索するため効率的に検索を行うことができます。

このように部分的なクラスタ内探索によって、全データを逐一検索するよりも高速に似たデータを見つけられるのが特徴です。

3. PostgreSQLでセマンティクサーチ

pgvectorとPGroongaでセマンティックサーチをする例を紹介しました。 説明したいことはスライドを読んでいただければわかると思います。 ただ、例示しているSQLが少し複雑なのでもう少しシンプルなSQLを例に違いを説明します。

pgvectorはユーザーがテキストデータをベクトル化し、そのベクトルをPostgreSQLにINSERTする必要があります。 一方、PGroongaは内部で自動的にテキストをベクトル化して管理するため、ユーザーはベクトルデータを意識せずにセマンティックサーチを利用できます。

pgvector

データ登録の例:

INSERT INTO contents_for_pgvector (content_embedding)
VALUES ('[0.1, 0.2, 0.3, ...]');

別途、テキストデータをベクトル化してデータを挿入する必要があります。

類似度でソートの例:

SELECT * FROM contents_for_pgvector
ORDER BY content_embedding <-> '[0.3, 0.1, 0.2, ...]';

検索のときも検索クエリをベクトル化したベクトルを使ってソートする必要があります。

参考: https://github.com/pgvector/pgvector

PGroonga

データ登録の例:

INSERT INTO contents_for_pgroonga (text)
VALUES ('I am a boy');

見慣れたINSERT文ではないでしょうか。PGroongaの場合は内部で自動でベクトル化するため、テキストデータをそのままINSERTするだけで良いです。

類似度でソートの例:

SELECT * FROM contents_for_pgroonga
ORDER BY text <&@*> pgroonga_condition('boy');

類似度でソートするときも検索クエリのテキストをそのまま使うことができます。 (pgroonga_condition()という関数を実行していますが、これによってベクトル化をしているわけではありません。検索するときにいろいろオプションを指定するための関数です。この例ではないものとして見てください。)

4. 性能とユースケース

性能検証

発表時には検証していなかった内容も含めて再検証したので、本記事ではそれを紹介します。

測定環境
  • PostgreSQL 18
  • PGroonga mainブランチ
  • pgvector v0.8.1
    • IVFFlatインデックス
  • Wikipediaのタイトル
    • 1,477,194 件
検索速度

5回実行した中央値です。

検証内容 時間
PGroonga ベクトル化含む(n_probes = 10) 94.517 ms
pgvector ベクトル化含む(ivfflat.probes = 1) 72.378 ms
pgvector ベクトル化含む(ivfflat.probes = 5) 87.189 ms
pgvector ベクトル化含む(ivfflat.probes = 10) 98.991 ms
pgvector ベクトル化含まず 35.699 ms

検証に使ったクエリは次のとおりです。

pgvectorのivfflat.probesは検索に含めるクラスタの数(IVFの説明の代表ベクトルの数)です。 PGroongaは10で固定ですが、pgvectorはオプションで指定できます。 多くするほど再現率は向上しますが、処理対象のベクトルが増えるので遅くなります。

PGroonga ベクトル化含む:

SET enable_seqscan = off;

SELECT title FROM wikipedia
ORDER BY title <&@*> pgroonga_condition('異世界転生して無双したよ')
LIMIT 5;

現在、PGroongaにてpgvectorのivfflat.probesに相当する値(n_probes)は10で固定です。

pgvector ベクトル化含む(PGroongaのpgroonga_language_model_vectorize()関数でベクトル化しています。):

SET enable_seqscan = off;
SET ivfflat.probes = 1;

SELECT title FROM wikipedia
ORDER BY embedding <-> pgroonga_language_model_vectorize(
  'hf:///groonga/all-MiniLM-L6-v2-Q4_K_M-GGUF',
  '異世界転生して無双したよ')::vector
LIMIT 5;

ivfflat.probesについて: https://github.com/pgvector/pgvector?tab=readme-ov-file#query-options-1

参考: SELECT pgroonga_language_model_vectorize(...); の単品実行は50 ms程度。

pgvector ベクトル化含まず:

SET enable_seqscan = off;

SELECT title FROM pgvector_wikipedia
ORDER BY embedding <-> '[0.008015699, ...("異世界転生して無双したよ"のベクトル)]'
LIMIT 5;
参考: インデックスのサイズ

PGroongaはベクトルを自動で作成し内部的に保持するので、インデックスにベクトルのサイズも含まれています。 仕組みが違うためpgvectorとPGroongaのインデックスサイズを比較してもあまり意味はありませんが、参考まで記載します。

インデックス サイズ
pgvector IVFFlat 796 MB
PGroonga 約 348 MB

確認方法

  • pgvector
    • pg_size_pretty(pg_relation_size('インデックス名'))でインデックスサイズを確認しました。
  • PGroonga
    • PGroongaのインデックスはpg_relation_size()で確認できません。
    • データディレクトリにpgrnで始まるファイルがあるのでそれのサイズを合計します。
      • pgrn.lmで始まるファイルは言語モデルなのでそれは除きます。
補足: 「PGroongaはベクトルを自動で作成し内部的に保持する」について

スライドでも紹介していますがPGroongaではRaBitQで量子化した値を保持しています。 検索対象のドキュメントをベクトル化した元のベクトルは保持しておらず、RaBitQで量子化した値のみを保持しています。 これによりデータ量を削減しています。

加えて効率的な検索を行うためのIVFで使う代表点のベクトルも保持しています。

ユースケース

資料と発表当日にはPostgreSQLの日本語ドキュメントをセマンティックサーチした例を紹介しました。 DROP TABLE ...を忘れてしまったので、テーブルを削除するときのSQLを調べたいという設定での例です。

素朴にLIKE%テーブル%消す%などで検索すると次のような結果が得られます。

SELECT title, SUBSTRING(content FROM 1 FOR 15)
FROM jpug_doc_contents
WHERE content LIKE '%テーブル%消す%'
limit 12;
         title          |          substring
------------------------+------------------------------
 contrib                | 付録F 追加で提供されるモジュ
 ddl-priv               | 5.8. 権限                   +
                        |                             +
                        | オブジェク
 ddl-schemas            | 5.10. スキーマ              +
                        |                             +
                        | Po
 explicit-locking       | 13.3. 明示的ロック          +
                        |                             +
                        |
 extend-extensions      | 36.17. 関連するオブジェ
 glossary               | 付録M 用語集                +
                        |                             +
                        | これはPos
 hot-standby            | 26.4. ホットスタンバイ
 jit-reason             | 30.1. JITコンパイルと
 lo                     | F.21. lo — ラージオ
 plpgsql-implementation | 41.11. PL/pgSQL
 protocol-flow          | 53.2. メッセージの流れ
 reference              | パート VI. リファレンス     +
                        |
(12 rows)

各ページの先頭のみを表示していますが、欲しい情報が書いてあるページは得られてなさそうです。

これを次のようなクエリで意味の近いもの順で並び替えると、少しいい感じの結果が得られます。

SELECT title, SUBSTRING(content FROM 1 FOR 15)
FROM jpug_doc_contents
ORDER BY content <&@*> pgroonga_condition('テーブルを消す')
LIMIT 12;
            title            |         substring
-----------------------------+---------------------------
 tutorial-table              | 2.3. 新しいテーブルの作成
 ddl-basics                  | 5.1. テーブルの基本      +
                             |                          +
                             |
 ddl-depend                  | 5.15. 依存関係の追跡     +
                             |
 ddl-alter                   | 5.7. テーブルの変更      +
                             |                          +
                             |
 datatype-boolean            | 8.6. 論理値データ型      +
                             |                          +
                             |
 ddl-inherit                 | 5.11. 継承               +
                             |                          +
                             | Post
 logicaldecoding-synchronous | 47.8. ロジカルデコーディ
 tutorial-delete             | 2.9. 削除                +
                             |                          +
                             | DELET
 sql-delete                  | DELETEDELETE —
 sql-droptablespace          | DROP TABLESPACE
 sql-droptable               | DROP TABLEDROP
 indexes-intro               | 11.1. はじめに           +
                             |                          +
                             | 次の
(12 rows)

一見、欲しい情報が書いてあるページでなさそうですが、一番上のページはこちらで、ドキュメントの最後に DROP TABLE tablename; があります!

ただし、目的のDROP TABLEのページが一番上に出ていないので、そこは課題として改善していきたいです。(上述の例では11番目にDROP TABLEのページがあります。)

補足

PostgreSQLの日本語ドキュメントをセマンティックサーチする例では言語モデルにhf:///groonga/multilingual-e5-base-Q4_K_M-GGUFを使いました。 このモデルを使うためにはこの修正も必要なので、マージされるまでもう少々お待ちください。

また、今回はDROP TABLEが一番上に出ませんでしたが、言語モデルを変えると改善する可能性があります。

まとめ

「PostgreSQLでのセマンティックサーチへの挑戦」の発表スライドを補足する記事でした。 当日は他の発表を聞いたりして大変勉強になりました。スタッフのみなさまやいろいろお話させていただいた方もありがとうございました!

PGroongaでセマンティックサーチがしたい、PGroongaでより良い検索をしたい、などPGroongaやPostgreSQLで期待通りの検索ができずお困りの場合はお問い合わせよりご連絡ください。

関連記事

OSSデータベース取り取り時報で当日の発表について紹介されました。こちらもぜひご覧ください!

PGroongaでのセマンティックサーチはGroongaのセマンティックサーチの機能を利用しています。 ククログのGroongaでのセマンティックサーチの実装もご覧ください。