Upgrade to Pro — share decks privately, control downloads, hide ads and more …

LlamaIndex の Property Graph Index を PostgreSQL ...

LlamaIndex の Property Graph Index を PostgreSQL 上に構築してデータ構造を見てみる

JAWS-UG AI/ML #27 2025/6/23 LT

Avatar for hmatsu47

hmatsu47

June 23, 2025
Tweet

More Decks by hmatsu47

Other Decks in Technology

Transcript

  1. 自己紹介 松久裕保(@hmatsu47) • https://qiita.com/hmatsu47 • 現在: ◦ 名古屋で Web インフラのお守り係をしています

    ◦ SRE チームに所属しつつ技術検証の支援をしています ◦ 普段カンファレンス・勉強会では DB の話しかしていません (ほぼ) 2
  2. 本日の内容 • LlamaIndex の Property Graph Index ◦ Bedrock ナレッジベースの

    GraphRAG とよく比較される • PostgreSQL 上に構築 ◦ TiDB 用の実装を Amazon Q Developer の力を借りて移植 • サンプル文書のインデックスを作成し生成されたデータ の内容を確認 • 検索時にデータがどのように使われるかを確認 3
  3. なぜこの話を? • 多くの人にとってグラフデータベースは馴染みがない ◦ Neo4j や Neptune などを使っている人はそんなに多くないはず • RDBMS

    なら多くの人が使っている ◦ 少しはとっつきやすい? ◦ RDBMS のテーブル上にグラフ構造を展開したほうがイメージが つきやすいかも? 4
  4. Property Graph Index • プロパティグラフで構成されるインデックス ◦ ノードとエッジ(リレーション)で構成 ▪ エッジは方向性をもった矢印で表現(有向グラフ) ▪

    ノードとエッジはラベル(カテゴリ・タイプ)とプロパティ(メタデータ) を持つことが可能 ◦ 様々な情報を格納できるが、デフォルト(SimpleLLMPathExtractor & ImplicitPathExtractor)ではトリプレット(主語・述語・目的語)と、 文章チャンクの接続関係がインデックスに展開される 7
  5. ただし PostgreSQL + pgvector は非対応なので • Amazon Q Developer GitHub

    統合で TiDB 用を移植 ◦ トークン数の限界、過去作業に関するコンテキスト引き継ぎなど でそこそこ苦労 ▪ 詳細は省略 8
  6. 文書のチャンク化→グラフ化 • 1,000 文字前後(デフォルト)の文章に分割して保存 ◦ 1 文書あたり 1 つの親(node)ノードを生成 ◦

    チャンク化した文章を text_chunk ノードとして保存 • チャンクの接続関係(前後・親)をグラフ化 ◦ text_chunk ノードから親ノードを指す SOURCE エッジを生成 ◦ text_chunk ノードに保存された文章の前後関係を表す PREVIOUS / NEXT エッジを生成 9
  7. 実際のテーブル構成 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) 14
  8. ノード用テーブル(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) 15 ノードは埋め込みベクトル を持てる
  9. エッジ用テーブル(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) 16
  10. ノード用テーブルに含まれる 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) 17 node は 1 文書あたり 1 行(レコード) text_chunk は文章をチャンク化(分割)したもの (親は node になる)
  11. 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 18 埋め込みベクトルも 持たない 埋め込みベクトルを 持たない 文章チャンクは持たない
  12. 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 19 文章チャンクの 埋め込みベクトルを持つ 埋め込みベクトルを 持たない 文章チャンクを持つ nameは持たない
  13. 文章チャンク関連のエッジ行の内訳 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) 20 文章チャンク関連の エッジの数
  14. 子(チャンク)→親を示すエッジ行(レコード)の例 postgres=# SELECT id, label, source_id, target_id, properties, created_at, updated_at

    FROM pg_relations WHERE label = 'SOURCE' ORDER BY created_at LIMIT 2; -[ RECORD 1 ]-----------------------------------------------------------------------------------------(略) id | 11 label | SOURCE source_id | 74b585c0-6889-46eb-9c3c-75d4e68dae78 target_id | c29a6201-5921-4a01-bf6c-5cbf13f246dd properties | {(略), "triplet_source_id": "74b585c0-6889-46eb-9c3c-75d4e68dae78", (略)} created_at | 2025-06-21 13:47:11.329644 updated_at | 2025-06-21 13:47:11.331238 -[ RECORD 2 ]-----------------------------------------------------------------------------------------(略) id | 22 label | SOURCE source_id | 927e5ae7-a57b-4681-8737-86fc99fa2cb8 target_id | c29a6201-5921-4a01-bf6c-5cbf13f246dd properties | {(略), "triplet_source_id": "927e5ae7-a57b-4681-8737-86fc99fa2cb8", (略)} created_at | 2025-06-21 13:47:11.403122 updated_at | 2025-06-21 13:47:11.407789 21 親(node)のIDは同じ
  15. チャンクの前後関係を示すエッジ行(レコード)の例 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 22 1つ前のチャンクのID
  16. ノードに含まれる 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 24 単語(主語・目的語)を主キー(id)に →同じ単語が複数登録されることはない
  17. ノードに含まれる 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 25 同じ単語が別の文章チャンクに出てきたら どんどん上書き(UPSERT)される
  18. ノードに含まれる 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 26 nameを持つ(idと同じ)
  19. ノードに含まれる 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 27 id:1「私」と id:2「文章を書くこと」が 埋め込みベクトル化されている
  20. トリプレットを示すエッジ行(レコード)の例 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 28 idはシーケンス値 →同じ組み合わせのトリプレットが複数存在し  うる(別の文章チャンクから抽出した場合)
  21. トリプレットを示すエッジ行(レコード)の例 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 29 同じ組み合わせが別の文章チャンクに現れ ても上書き(UPSERT)されない
  22. 検索時(デフォルトの Retriever 構成) • LLM に渡すコンテキストをグラフストアで検索・取得 ◦ VectorContextRetriever で entity

    ノードをベクトル検索 ▪ ベクトル類似度の高い entity ノードの単語を含むトリプレットを取得 ▪ あわせてトリプレット抽出元の text_chunk ノードを取得 ◦ LLMSynonymRetriever で類義語を複数(デフォルト 10 個)生成 し、それらを使って entity ノードを主キー検索 ▪ 同じ主キー値を持つ entity ノードの単語を含むトリプレットを取得 ▪ あわせてトリプレット抽出元の text_chunk ノードを取得 31
  23. 検索時(デフォルトの Retriever 構成) • 取得したトリプレットと文章チャンクをコンテキストと して付加して質問文を LLM に送信 ◦ ここから先は通常の

    RAG と同じ • 文章チャンクのグラフ構造は使用していない(おそらく) ◦ トリプレットのエッジに保存された ID を使って text_chunk ノードを取得してコンテキストとして使っているのみ 32
  24. 実際の送信プロンプト例 • 質問文「学生時代にしたことは?」 33 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: 検索・取得したトリプレット 検索・取得した文章チャンク 質問文
  25. 試してみた感想 • 応答が少し遅い ◦ LLMSynonymRetriever で類義語抽出を LLM にさせている部分 の待ち時間が余分にかかっている ▪

    今回のケースではあまり有効に機能していない様子だったので LLMSynonymRetriever を外しても良かったかも? 35