PostgreSQL+pgvector で GraphRAG に挑戦 & pgvectorscale 0.7.x アップデート

540 Views

June 24, 25

スライド概要

第 53 回 PostgreSQL アンカンファレンス@オンライン 2025/6/24

profile-image

Qiita や Zenn でいろいろ書いてます。 https://qiita.com/hmatsu47 https://zenn.dev/hmatsu47 MySQL 8.0 の薄い本 : https://github.com/hmatsu47/mysql80_no_usui_hon Aurora MySQL v1 → v3 移行計画 : https://zenn.dev/hmatsu47/books/aurora-mysql3-plan-book https://speakerdeck.com/hmatsu47

シェア

またはPlayer版

埋め込む »CMSなどでJSが使えない場合

ダウンロード

関連スライド

各ページのテキスト
1.

PostgreSQL+pgvector で GraphRAG に挑戦 & pgvectorscale 0.7.x アップデート 第 53 回 PostgreSQL アンカンファレンス@オンライン 2025/6/24 まつひさ(hmatsu47)

2.

自己紹介 松久裕保(@hmatsu47) ● https://qiita.com/hmatsu47 ● 現在: ○ 名古屋で Web インフラのお守り係をしています ○ SRE チームに所属しつつ技術検証の支援をしています ○ 普段カンファレンス・勉強会では DB の話しかしていません (ほぼ) 2

3.

本日の内容/その 1 ● PostgreSQL+pgvector で GraphRAG に挑戦してみた ○ LlamaIndex にある TiDB 用グラフストア実装を AI コーディング エージェントの力を借りて PostgreSQL+pgvector に移植してみ た ○ サンプルデータを投入して、生成されたデータ構造を眺めてみた 3

4.

ちなみに/その 1 ● PostgreSQL のグラフ機能といえば ○ PostGIS 関連の extension に pgRouting がある ■ https://pgrouting.org/ ○ ただし最短経路・最短パス探索に特化しているので今回の用途に は使えない 4

5.

ちなみに/その 2 ● Oracle Database 23c にはグラフを扱う機能がある ○ SQL:2023 にはプロパティグラフ用の SQL 構文があり、Oracle Database 23c ではこれをサポート ■ ISO/IEC 9075-16 によって定義 ■ https://www.oracle.com/jp/a/ocom/docs/database/operational_property_graph_with_23cja.pdf 5

6.

復習/RAG ● RAG(Retrieval Augmented Generation:検索拡張生成) ○ 生成 AI が学習していない(弱い)知識について普通に質問する と、生成 AI は正しい答えを返せない ○ 関連知識を DB などから検索して持ってきてコンテキストとして 生成 AI に与えると、正しい答えを返せる ○ 関連知識を DB で検索するときの標準的な方法はベクトル検索 6

7.

復習/ベクトル検索 ● 意味が近い文章などを探すときに使う ○ 最近は生成 AI の埋め込みモデルを使って文章などをベクトル化 するのが主流 ○ 一般的な埋め込みモデルでは長い文章をそのままベクトル化する ことはできないので、文章を分割(チャンク化)してチャンクごと にベクトル化して DB に入れる ○ 質問文もベクトル化して、DB で「距離が近いベクトル」を探す ■ 生成 AI にコンテキストとして渡すのはベクトルではなくて元の文章など 7

8.

ベクトル検索型 RAG の弱点 ● チャンク化するときの分割方法が難しい ○ 細かく分割してしまうと必要な情報を生成 AI に渡せない ○ 大きすぎると埋め込みモデルで扱えない・検索精度が落ちる ● ソースとなる情報が分散していると取りこぼす ○ 脚注がある文章や図表・添付資料に補足があるケースなど ● 抽出したコンテキストがうまく LLM に伝わらない ○ 複雑・曖昧な文章をベクトル検索して LLM に渡すケースなど 8

9.

ベクトル検索型 RAG の弱点 主に下2つの対処のために ● チャンク化するときの分割方法が難しい ○ 細かく分割してしまうと必要な情報を生成 AI に渡せない GraphRAGを使うと良さそう? ○ 大きすぎると埋め込みモデルで扱えない・検索精度が落ちる ● ソースとなる情報が分散していると取りこぼす ○ 脚注がある文章や図表・添付資料に補足があるケースなど ● 抽出したコンテキストがうまく LLM に伝わらない ○ 複雑・曖昧な文章をベクトル検索して LLM に渡すケースなど 9

10.

GraphRAG(グラフインデックスを使う RAG) ● 関連知識の格納と検索にグラフ構造を利用 ○ たまたま(?)昨日(6/23)開催された JAWS-UG AI/ML 支部 #27 の発表資料の解説がわかりやすい ■ https://speakerdeck.com/tosuri13/graphragnoshi-zu-mimaruwakari 10

11.

LlamaIndex ● 主に RAG 向けの検索インデックス用フレームワーク ○ LangChain と比較されることがよくあるが、LangChain よりも インデックスに特化 ■ インデックスの構築と検索(retrieve)・データの投入など 11

12.

Property Graph Index ● プロパティグラフで構成されるインデックス ○ ノードとエッジ(リレーション)で構成 ■ エッジは方向性をもった矢印で表現(有向グラフ) ■ ノードとエッジはラベル(カテゴリ・タイプ)とプロパティ(メタデータ) を持つことが可能 ○ 様々な情報を格納できるが、デフォルト(SimpleLLMPathExtractor & ImplicitPathExtractor)ではトリプレット(主語・述語・目的語)と、 文章チャンクの接続関係がインデックスに展開される 12

13.

たとえばこんな感じ(トリプレットのグラフの例) ● この図については後ほど 説明 13

14.

ただし PostgreSQL+pgvector は非対応なので ● Amazon Q Developer GitHub 統合で TiDB 用を移植 ○ AI コーディングエージェント(プレビュー提供中) 14

15.

移植は思ったより難航 ● トークン数の限界、過去作業に関するコンテキスト引き 継ぎなどでそこそこ苦労 ○ ORM として SQLAlchemy を使っているが、PostgreSQL 用の Dialect(方言)と TiDB 用の(外部)Dialect ではベクトルの記述・ 比較演算子に加えて JSON や配列(リスト)値の埋め込み方が違う などのハマりポイントがいくつかあった ■ JSON は JSONB に、配列を扱う IN 句は ANY に書き換えるなどして対処 ○ コンテキスト引き継ぎは諦めて都度 Sub-Issue で細かく指示 15

16.

完成後、サンプルデータを投入して試してみた ● LlamaIndex のサンプル文書の日本語訳 ○ ポール・グレアムのエッセイ ■ https://github.com/hmatsu47/llama_index_property_graph_test/blob/main/dat a/example_ja.txt 16

17.

試しやすいように ● Streamlit で単答チャットアプリ化 17

18.

インデックス生成/文書のチャンク化→グラフ化 ● 1,000 文字前後(デフォルト)の文章に分割して保存 ○ 1 文書あたり 1 つの親(node)ノードを生成 ○ チャンク化した文章を text_chunk ノードとして保存 ● チャンクの接続関係(前後・親)をグラフ化 ○ text_chunk ノードから親ノードを指す SOURCE エッジを生成 ○ text_chunk ノードに保存された文章の前後関係を表す PREVIOUS / NEXT エッジを生成 18

19.

インデックス生成/トリプレットの抽出 ● チャンク化した文章から「主語+述語+目的語」の組み 合わせをいくつか抽出 ○ 主語と目的語を entity ノードとして個別に保存 ○ 主語・述語・目的語の関係性をエッジとして保存 ○ 抽出元の文章チャンクを示す ID(識別子)をノード・エッジそれぞ れのプロパティに記録 私 食べる パン 19

20.

インデックス生成/各ノードにベクトルを保存 ● ベクトル検索用の埋め込みベクトルを保存 ○ text_chunk ノードには文章チャンクの埋め込みベクトル ○ entity ノードにはキーワード(主語・目的語)の埋め込みベクトル ○ node ノードには保存せず(null) 20

21.

実際のテーブル構成 postgres=# \x auto Expanded display is used automatically. postgres=# \d List of relations Schema | Name | Type | Owner --------+---------------------+----------+---------public | pg_nodes | table | postgres public | pg_relations | table | postgres public | pg_relations_id_seq | sequence | postgres (3 rows) 21

22.

ノード用テーブル(pg_nodes)の定義 postgres=# \d pg_nodes Table "public.pg_nodes" Column | Type | Collation | Nullable | Default ------------+-----------------------------+-----------+----------+--------id | character varying(512) | | not null | text | text | | | name | character varying(512) | | | label | character varying(512) | | not null | properties | jsonb | | | embedding | vector(1024) | | | created_at | timestamp without time zone | | not null | now() updated_at | timestamp without time zone | | not null | now() Indexes: "pg_nodes_pkey" PRIMARY KEY, btree (id) Referenced by: TABLE "pg_relations" CONSTRAINT "pg_relations_source_id_fkey" FOREIGN KEY (source_id) REFERENCES pg_nodes(id) TABLE "pg_relations" CONSTRAINT "pg_relations_target_id_fkey" FOREIGN KEY (target_id) REFERENCES pg_nodes(id) ノードは埋め込みベクトル を持てる 22

23.

エッジ用テーブル(pg_relations)の定義 postgres=# \d pg_relations Table "public.pg_relations" Column | Type | Collation | Nullable | Default ------------+-----------------------------+-----------+----------+----------------------------------------id | integer | | not null | nextval('pg_relations_id_seq'::regclass) label | character varying(512) | | not null | source_id | character varying(512) | | | target_id | character varying(512) | | | properties | jsonb | | | created_at | timestamp without time zone | | not null | now() updated_at | timestamp without time zone | | not null | now() Indexes: "pg_relations_pkey" PRIMARY KEY, btree (id) Foreign-key constraints: "pg_relations_source_id_fkey" FOREIGN KEY (source_id) REFERENCES pg_nodes(id) "pg_relations_target_id_fkey" FOREIGN KEY (target_id) REFERENCES pg_nodes(id) 23

24.

ノード用テーブルに含まれる label(タイプ)の内訳 postgres=# SELECT label, COUNT(*) AS label_count FROM pg_nodes GROUP BY label ORDER BY label; label | label_count ------------+------------entity | 242 node | 1 text_chunk | 20 (3 rows) node は 1 文書あたり 1 行(レコード) text_chunk は文章をチャンク化(分割)したもの (親は node になる) 24

25.
[beta]
node 行(レコード)の例
postgres=# SELECT id, length(text) AS text_length, name, label, properties, (embedding IS NOT NULL) AS
embedding_exists, created_at, updated_at FROM pg_nodes WHERE label = 'node';
-[ RECORD 1 ]----+------------------------------------id
| c29a6201-5921-4a01-bf6c-5cbf13f246dd
text_length
|
name
|
label
| node
properties
| {}
embedding_exists | f
created_at
| 2025-06-21 13:47:11.327101
updated_at
| 2025-06-21 13:47:11.327101

文章チャンクとnameは
埋め込みベクトルを
持たない
埋め込みベクトルも
持たない

25

26.
[beta]
text_chunk 行(レコード)の例
postgres=# SELECT id, length(text) AS text_length, name, label, properties, (embedding IS NOT NULL) AS
embedding_exists, created_at, updated_at FROM pg_nodes WHERE label = 'text_chunk' ORDER BY created_at LIMIT
1;
-[ RECORD 1 ]----+------------------------------------------------------------------------------------(略)
id
| 74b585c0-6889-46eb-9c3c-75d4e68dae78
text_length
| 975
name
|
label
| text_chunk
properties
| {"doc_id": "c29a6201-5921-4a01-bf6c-5cbf13f246dd", (略)}
embedding_exists | t
created_at
| 2025-06-21 13:47:09.82389
updated_at
| 2025-06-21 13:47:09.835153

文章チャンクを持つ
埋め込みベクトルを
nameは持たない
持たない

文章チャンクの
埋め込みベクトルを持つ

26

27.

文章チャンク関連のエッジ行の内訳 postgres=# SELECT COUNT(*) FROM pg_relations; count ------253 (1 row) postgres=# SELECT label, COUNT(label) FROM pg_relations WHERE label IN('SOURCE', 'PREVIOUS', 'NEXT') GROUP BY label ORDER BY label; label | count ----------+------NEXT | 19 PREVIOUS | 19 SOURCE | 20 (3 rows) 文章チャンク関連の エッジの数 27

28.
[beta]
チャンクの前後関係を示すエッジ行(レコード)の例
postgres=# SELECT id, label, source_id, target_id, properties, created_at, updated_at FROM pg_relations
WHERE label = 'PREVIOUS' ORDER BY created_at LIMIT 2;
-[ RECORD 1 ]-----------------------------------------------------------------------------------------(略)
id
| 23
label
| PREVIOUS
source_id | 927e5ae7-a57b-4681-8737-86fc99fa2cb8
target_id | 74b585c0-6889-46eb-9c3c-75d4e68dae78
properties | {(略), "triplet_source_id": "927e5ae7-a57b-4681-8737-86fc99fa2cb8", (略)}
created_at | 2025-06-21 13:47:11.409412
updated_at | 2025-06-21 13:47:11.413127
-[ RECORD 2 ]-----------------------------------------------------------------------------------------(略)
id
| 36
label
| PREVIOUS
source_id | d5580129-a61c-41db-8003-25187e473c0b
target_id | 927e5ae7-a57b-4681-8737-86fc99fa2cb8
properties | {(略), "triplet_source_id": "d5580129-a61c-41db-8003-25187e473c0b", (略)}
created_at | 2025-06-21 13:47:11.488719
updated_at | 2025-06-21 13:47:11.493809

1つ前のチャンクのID

28

29.

文章チャンクのグラフ構造 ● node(黄)ノードが 中心にある ○ 全ての text_chunk(青) ノードから SOURCE エッジで接続 29

30.
[beta]
ノードに含まれる entity 行(レコード)の例
postgres=# SELECT id, length(text) AS text_length, name, label, properties, (embedding IS NOT NULL) AS
embedding_exists, created_at, updated_at FROM pg_nodes WHERE label = 'entity' ORDER BY created_at LIMIT 2;
-[ RECORD 1 ]----+------------------------------------------------------------------------------------(略)
id
| 私
text_length
|
name
| 私
label
| entity
properties
| {(略), "triplet_source_id": "64ce47cd-969f-4bdc-9eda-ee18e7caf20c", (略)}
embedding_exists | t
created_at
| 2025-06-21 13:47:09.913373
updated_at
| 2025-06-21 13:47:10.518213
-[ RECORD 2 ]----+------------------------------------------------------------------------------------(略)
id
| 文章を書くこと
text_length
|
name
| 文章を書くこと
label
| entity
properties
| {(略), "triplet_source_id": "1775422f-573d-4ade-8fce-50a4fcf1a463", (略)}
embedding_exists | t
created_at
| 2025-06-21 13:47:09.916022
updated_at
| 2025-06-21 13:47:10.570029

単語(主語・目的語)を主キー(id)に
→同じ単語が複数登録されることはない

30

31.
[beta]
ノードに含まれる entity 行(レコード)の例
postgres=# SELECT id, length(text) AS text_length, name, label, properties, (embedding IS NOT NULL) AS
embedding_exists, created_at, updated_at FROM pg_nodes WHERE label = 'entity' ORDER BY created_at LIMIT 2;
-[ RECORD 1 ]----+------------------------------------------------------------------------------------(略)
id
| 私
text_length
|
name
| 私
label
| entity
properties
| {(略), "triplet_source_id": "64ce47cd-969f-4bdc-9eda-ee18e7caf20c", (略)}
embedding_exists | t
created_at
| 2025-06-21 13:47:09.913373
updated_at
| 2025-06-21 13:47:10.518213
-[ RECORD 2 ]----+------------------------------------------------------------------------------------(略)
id
| 文章を書くこと
text_length
|
name
| 文章を書くこと
label
| entity
properties
| {(略), "triplet_source_id": "1775422f-573d-4ade-8fce-50a4fcf1a463", (略)}
embedding_exists | t
created_at
| 2025-06-21 13:47:09.916022
updated_at
| 2025-06-21 13:47:10.570029

nameを持つ(idと同じ)

31

32.
[beta]
ノードに含まれる entity 行(レコード)の例
postgres=# SELECT id, length(text) AS text_length, name, label, properties, (embedding IS NOT NULL) AS
embedding_exists, created_at, updated_at FROM pg_nodes WHERE label = 'entity' ORDER BY created_at LIMIT 2;
-[ RECORD 1 ]----+------------------------------------------------------------------------------------(略)
id
| 私
text_length
|
name
| 私
label
| entity
properties
| {(略), "triplet_source_id": "64ce47cd-969f-4bdc-9eda-ee18e7caf20c", (略)}
embedding_exists | t
created_at
| 2025-06-21 13:47:09.913373
updated_at
| 2025-06-21 13:47:10.518213
-[ RECORD 2 ]----+------------------------------------------------------------------------------------(略)
id
| 文章を書くこと
text_length
|
name
| 文章を書くこと
label
| entity
properties
| {(略), "triplet_source_id": "1775422f-573d-4ade-8fce-50a4fcf1a463", (略)}
embedding_exists | t
created_at
| 2025-06-21 13:47:09.916022
updated_at
| 2025-06-21 13:47:10.570029

id:1「私」と id:2「文章を書くこと」が
埋め込みベクトル化されている

32

33.
[beta]
トリプレットを示すエッジ行(レコード)の例
postgres=# SELECT id, label, source_id, target_id, properties, created_at, updated_at FROM pg_relations
ORDER BY created_at LIMIT 2;
-[ RECORD 1 ]-----------------------------------------------------------------------------------------(略)
id
| 1
label
| 取り組んできた
source_id | 私
target_id | 文章を書くこと
properties | {(略), "triplet_source_id": "74b585c0-6889-46eb-9c3c-75d4e68dae78", (略)}
created_at | 2025-06-21 13:47:11.275447
updated_at | 2025-06-21 13:47:11.282648
-[ RECORD 2 ]-----------------------------------------------------------------------------------------(略)
id
| 2
label
| 取り組んできた
source_id | 私
target_id | プログラミング
properties | {(略), "triplet_source_id": "74b585c0-6889-46eb-9c3c-75d4e68dae78", (略)}
created_at | 2025-06-21 13:47:11.284701
updated_at | 2025-06-21 13:47:11.287974

idはシーケンス値
→同じ組み合わせのトリプレットが複数存在し
うる(別の文章チャンクから抽出した場合)

33

34.

トリプレットのグラフ構造(一部) ● entity ノード「私」を 中心に見てみる ○ 一部、複数階層の接続が 抽出されている (私→初めて書いた→プログラム →終了しない→ことがある/ →入力された→パンチカード) 34

35.

検索時(デフォルトの Retriever 構成) ● LLM に渡すコンテキストをグラフストアで検索・取得 ○ VectorContextRetriever で entity ノードをベクトル検索 ■ ベクトル類似度の高い entity ノードの単語を含むトリプレットを取得 ■ あわせてトリプレット抽出元の text_chunk ノードを取得 ○ LLMSynonymRetriever で類義語を複数(デフォルト 10 個)生成 し、それらを使って entity ノードを主キー検索 ■ 同じ主キー値を持つ entity ノードの単語を含むトリプレットを取得 ■ あわせてトリプレット抽出元の text_chunk ノードを取得 35

36.

entity ノードをベクトル検索(コード関連部分) with Session(self._engine) as session: result = ( session.query( self._node_model, self._node_model.embedding.cosine_distance( query.query_embedding ).label("embedding_distance"), ) .filter(self._node_model.name.is_not(None)) .order_by(sql.asc("embedding_distance")) .limit(query.similarity_top_k) .all() ) nameがNone(null)ではないノード →entityノードに限定してベクトル検索 36

37.
[beta]
グラフ構造を辿る SQL 文のテンプレート
WITH RECURSIVE PATH AS
(SELECT 1 AS depth,
r.source_id,
r.target_id,
r.label,
r.properties
FROM {relation_table} r
WHERE r.source_id = ANY(:ids)
UNION ALL SELECT p.depth + 1,
r.source_id,
r.target_id,
r.label,
r.properties
FROM PATH p
JOIN {relation_table} r ON p.target_id = r.source_id
WHERE p.depth < :depth )

再帰CTE
(共通テーブル式)

(左下から続く)
SELECT e1.id AS e1_id,
e1.name AS e1_name,
e1.label AS e1_label,
e1.properties AS e1_properties,
p.label AS rel_label,
p.properties AS rel_properties,
e2.id AS e2_id,
e2.name AS e2_name,
e2.label AS e2_label,
e2.properties AS e2_properties
FROM PATH p
JOIN {node_table} e1 ON p.source_id = e1.id
JOIN {node_table} e2 ON p.target_id = e2.id
ORDER BY p.depth
LIMIT :limit;

(右上に続く)

37

38.

LLM に送信 ● 取得したトリプレットと文章チャンクをコンテキストと して付加して質問文を LLM に送信 ○ ここから先は通常の RAG と同じ ● 文章チャンクのグラフ構造は使用していない(おそらく) ○ トリプレットのエッジに保存された ID を使って text_chunk ノードを取得してコンテキストとして使っているのみ 38

39.

実際の送信プロンプト例(コンテキストと質問文) ● 質問文「学生時代にしたことは?」 Context information is below. --------------------file_path: (略) 検索・取得したトリプレット Here are some facts extracted from the provided text: 卒業証書 -> 記載 -> Artificial intelligence 学生 -> 独学 -> 問題なかった 学生 -> 意識 -> 進むべき道 (略) 授業の中でではなく、独学という形ではあったが、それでも問題なかった。この数年間、私は自分が進むべき道をはっきりと意識していた。 検索・取得した文章チャンク 学部の卒業論文では、SHRDLUをリバースエンジニアリングした。私はこのプログラムを作ることが本当に好きだった。 (略) --------------------Given the context information and not prior knowledge, answer the query. Query: 学生時代にしたことは? Answer: 質問文 39

40.

試してみた感想 ● 応答内容が絞り込まれている印象 ○ ハルシネーションが軽減される代わりに少しそっけない? ■ プロンプトとパラメータのチューニング次第? →取得トリプレット数や辿るグラフ階層の数、取得チャンク数など 40

41.

試してみた感想 ● 応答が少し遅い ○ LLMSynonymRetriever で類義語抽出を LLM にさせている部分 の待ち時間が余分にかかっている ■ 今回のケースではあまり有効に機能していない様子だったので LLMSynonymRetriever を外しても良かったかも? 41

42.

試してみた感想 ● 条件次第で RDBMS もグラフストアとして使用可能? ○ 辿るグラフ階層数が 1(デフォルト)であれば通常の JOIN で十分 ■ 2 〜 3 階層になってくるとエッジの数が格段に増えそうなので厳しい? 42

43.

参考(今回使ったコードなど) ● GitHub リポジトリ ○ https://github.com/hmatsu47/llama-index-graph-stores-postgres ○ https://github.com/hmatsu47/llama_index_property_graph_test ○ https://github.com/hmatsu47/llama_index/issues?q=is%3Aissue%20state %3Aclosed 43

44.

本日の内容/その 2 ● pgvectorscale が Filtered-DiskANN に対応 ○ 0.7.0 から(2025/6/24 現在の最新は 0.7.1) 44

45.

pgvectorscale(0.6.x まで) ● PostgreSQL+pgvector のベクトル検索機能を強化する ための extension ○ pgvector と組み合わせて使う ● StreamingDiskANN インデックスを実装 ○ Microsoft が開発した DiskANN をベースにしたベクトル検索用 インデックス ○ https://www.docswell.com/s/hmatsu47/5J4LWM-2024-08-20-013209 45

46.

pgvectorscale 0.7.0 ● Filtered DiskANN インデックスを実装 ○ フィルタ条件のあるベクトル検索の性能を改善 ○ https://harsha-simhadri.org/pubs/Filtered-DiskANN23.pdf 46

47.

一般的なベクトル検索用インデックスの弱点 ● ベクトル以外の条件でフィルタする場合、まずベクトル 検索用インデックスで近い順に探索してから、フィルタ 条件で絞り込む必要がある ○ フィルタ条件で先に対象を絞り込むことができないため、処理が 非効率になることも 47

48.

たとえば ● 「グローバルなソフトウェアエンジニア DB(仮) 」 ○ 約 5,000 万レコード存在 ○ 紹介文をベクトル検索したい ○ 居住国・読み書き可能な言語(例:英語)・コーディングが可能な プログラミング言語(例:Java)などの分類で絞り込みたい ● このケースでは ○ 紹介文をベクトル検索してから、分類などで絞り込む流れになる ■ 分類を先に絞り込んだほうが効率的だったとしても構造上無理 48

49.

Filtered DiskANN インデックス ● ラベルを内包した ANN(近似最近傍探索)インデックス ○ インデックス構造にフィルタ用のラベルを追加 ○ ChatGPT(o3)先生曰く(論文の内容から) DiskANN(グラフ型 ANN インデックス+SSD ストレージ)の派生で、「ラベル(属性)で 絞り込む近似近傍検索」をインデックス構築段階から支援します。具体的には FilteredVamana(逐次構築) StitchedVamana(バッチ構築/サブグラフ合成) の 2 方式を実装し、いずれも Vamana グラフに「ベクトル距離だけでなくラベル共有も考慮 した辺」を張ります。 49

50.

Vamana? ● DiskANN のコアデータ構造(らしい) ○ https://zilliz.com/jp/learn/DiskANN-and-the-Vamana-Algorithm 50

51.

使い方/テーブル・インデックス作成 ● SMALLINT 値の配列でラベルを指定 ○ 配列構造だが「ラベル」なので「複数列」を示すものではない ■ 複合インデックスとは違う CREATE TABLE documents ( id SERIAL PRIMARY KEY, embedding VECTOR(1536), labels SMALLINT[], -- Array of category labels status TEXT, created_at TIMESTAMPTZ ); CREATE INDEX ON documents USING diskann (embedding vector_cosine_ops, labels); 51

52.
[beta]
使い方/検索
● ベクトル検索とラベルでのフィルタリングの併用例
○ ラベルは OR 検索
○ 意味づけを行うマスタテーブルのようなものを別に作ると良い
■

詳細は pgvectorscale の GitHub リポジトリの説明を参照
●

https://github.com/timescale/pgvectorscale?tab=readme-ov-file#giving-semantic-m
eaning-to-labels

SELECT * FROM documents
WHERE labels && ARRAY[1, 3] -- Documents with label 1 OR 3
ORDER BY embedding <=> '[...]'
LIMIT 10;

52

53.

実際に試…せなかった ● 性能比較するにはかなりのデータ量が必要 ○ メモリを超える量をテストデータとして用意するのはしんどい ■ Hugging Face で公開されている大容量データを加工するのは大変 ■ 埋め込みベクトル化など LLM の API 使用料金も地味に嵩む ● 代わりに ChatGPT(o3)先生に聞いてみた フィルタで絞り込まれた後のレコード数が数百~数千程度におさまると仮定します。そのケース で、ベクトルインデックスを作らずに事前にほかの属性値で絞り込んで数百~数千程度の少ないレ コード数に対してベクトル距離計算する場合と比較したメリットを教えてください。 53

54.

ChatGPT(o3)先生からの回答(まとめ部分) まとめ — 「いま速い」だけでは足りない場面が多い 1. 高同時接続・多テナントになると 300–2 000 本 × QPS が CPU とメモリ帯域の支配項になる。 Filtered-DiskANN は候補数を 1 桁に抑え、10× 以上の QPS を同じコア数で処理。 2. SSD や分散ストレージでもレイテンシが安定し、キャッシュヒットを前提としない設計が可能。 3. フィルタ選択性が揺らいでも tail-latency を守れる(特異ラベル・複合フィルタ・データ増大)。 4. インデックス構築コストは “StitchedVamana でも 1.5–2×” 程度で、一度組めば ROI が大きい。 結論 「常に 1 000 本以下」という理想状態と単発 APIなら線形スキャンで十分。 しかし スループット・コスト・最悪時レイテンシ・将来拡張まで含めた 運用要件 がある場合、 Filtered-DiskANN は “グラフにラベルを書き込む” だけで そのリスクとコストを丸ごと吸収できます。 54

55.

というわけで(データ容量がメモリ容量を超える前提で) ● 分類で絞り込んだ結果が 1,000 を超えないケース ○ ANN インデックスを作らず分類だけ通常の(B 木)インデックス を使って絞り込んでも良さそう ● 分類で絞り込んだ結果が 1,000 を超えうるケース ○ Filtered DiskANN インデックスが特におすすめ 55