673 Views
September 11, 25
スライド概要
第 35 回 中国地方 DB 勉強会 in 岡山 2025/9/13
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
PostgreSQL でもできる!GraphRAG 第 35 回 中国地方 DB 勉強会 in 岡山 2025/9/13 まつひさ(hmatsu47)
自己紹介 松久裕保(@hmatsu47) ● https://qiita.com/hmatsu47 ● 現在: ○ 名古屋で Web インフラのお守り係をしています ○ SRE チームに所属しつつ AI 導入の後方支援などをしています ○ 来月(10/11)開催の JAWS FESTA 2025 in 金沢で Aurora DSQL のトランザクションとの上手な付き合い方に関する話をします 2
本日の内容 ● RAG おさらい ● GraphRAG とは? ● PostgreSQL+pgvector で GraphRAG に挑戦 ○ 環境構築 ○ 試用 3
RAG おさらい 4
RAG(Retrieval Augmented Generation:検索拡張生成) ● LLM が学習していない(弱い)知識を補う ○ そのような知識について単純に質問すると、LLM は正しい答えを 返せない ○ 質問に関連する知識を DB などから検索・取得し、コンテキスト として LLM に与えると、正しい答えを返せる 5
RAG の種類 [1] Naive RAG ● 初期に登場したシンプルな構成の RAG ○ あらかじめ関連知識として与える文章を分割(チャンク化)→ベク トル化してベクトルストアに保存しておく ○ 問い合わせ時には質問文に類似するチャンクをベクトルストアか らベクトル検索によって取得し、コンテキストとして質問文とと もに LLM に渡す 6
【参考】ベクトル検索 ● 意味が近い文章などを探すときに使う ○ 最近は埋め込みモデルを使って文章などをベクトル化するのが主 流 ○ 一般的な埋め込みモデルでは長い文章をそのままベクトル化する ことはできないので、文章を分割(チャンク化)してチャンクごと にベクトル化してベクトルストアに入れる ○ 質問文もベクトル化して「距離が近いベクトル」を探す ■ LLM にコンテキストとして渡すのはベクトルではなくて元の文章など 7
【参考】ベクトル検索 ● 詳しくはこちらを参照 ○ ベクトルストア入門(BuriKaigi2025 の発表資料) https://www.docswell.com/s/hmatsu47/ZP2LY6-2025-01-19-235645 8
Naive RAG の弱点 ● チャンク化するときの分割方法が難しい ○ 細かく分割してしまうと必要な情報を LLM に渡せない ○ 大きすぎると埋め込みモデルで扱えない・検索精度が落ちる ● ソースとなる情報が分散していると取りこぼす ○ 脚注がある文章や図表・添付資料に補足があるケースなど ● 抽出したコンテキストがうまく LLM に伝わらない ○ 複雑・曖昧な文章をベクトル検索して LLM に渡すケースなど 9
RAG の種類 [2] Advanced RAG ● 関連知識の検索(Retrieval)に前処理と後処理を加え精度 を高める ○ ハイブリッド検索、リランキングなど ○ ほかにもいろいろな方法がある ● GraphRAG も Advanced RAG の一種 ○ グラフインデックスを使う 10
GraphRAG とは? 11
GraphRAG(グラフインデックスを使う RAG) ● 関連知識の格納と検索にグラフ構造を利用 ○ よく使われる(言及される)グラフ構造には以下の 2 種類がある ■ RDF グラフ ■ プロパティグラフ ○ よく対比されるが対立する概念ではない(と個人的に思っている) ■ RDF グラフをプロパティグラフに変換(プロパティグラフ向けの仕組みの上 で RDF グラフに相当するものを表現)することもできる 12
[1] RDF グラフ ● RDF(Resource Description Framework) ○ (元々は)ウェブ上のデータを一貫性のある方法で記述するための フレームワーク ○ 主語・述語・目的語で構成されるトリプレット(トリプル)と呼ば れる構造を使ってデータの関連性を表す 私 食べる パン ● RDF をグラフ化して表すもの→ RDF グラフ 13
[2] プロパティグラフ ● ノード・エッジ(リレーションシップ)・プロパティを使用 して情報を整理・表現するもの ○ ノードにはラベルとプロパティを付与可能 ○ エッジには方向性がありプロパティを付与可能 ■ ラベルが付与可能なものも ● RDF グラフよりも柔軟にグラフ構造を表現可能 ○ 実際の例は後述 14
【再掲】Naive RAG の弱点 ● チャンク化するときの分割方法が難しい ○ 細かく分割してしまうと必要な情報を LLM に渡せない ○ 大きすぎると埋め込みモデルで扱えない・検索精度が落ちる ● ソースとなる情報が分散していると取りこぼす ○ 脚注がある文章や図表・添付資料に補足があるケースなど ● 抽出したコンテキストがうまく LLM に伝わらない ○ 複雑・曖昧な文章をベクトル検索して LLM に渡すケースなど 15
【再掲】Naive RAG の弱点 主に下2つの対処のために ● チャンク化するときの分割方法が難しい ○ 細かく分割してしまうと必要な情報を LLM に渡せない GraphRAGを使うと良さそう? ○ 大きすぎると埋め込みモデルで扱えない・検索精度が落ちる ● ソースとなる情報が分散していると取りこぼす ○ 脚注がある文章や図表・添付資料に補足があるケースなど ● 抽出したコンテキストがうまく LLM に伝わらない ○ 複雑・曖昧な文章をベクトル検索して LLM に渡すケースなど 16
ちなみに [1] ● PostgreSQL のグラフ機能といえば ○ PostGIS 関連の extension に pgRouting がある https://pgrouting.org/ ○ ただし最短経路・最短パス探索に特化しているので GraphRAG 向けには使いづらい 17
ちなみに [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 18
PostgreSQL+pgvector で GraphRAG に挑戦 19
[1] 環境構築 20
使ったフレームワークなど ● LlamaIndex ○ 本体(Core) ○ Property Graph Index ○ PostgreSQL+pgvector 向けグラフストア実装 ■ TiDB 用の実装を移植 ● Streamlit ○ チャット用の UI として 21
LlamaIndex ● 主に RAG 向けの検索インデックス用フレームワーク ○ LangChain と比較されることがよくあるが、LangChain よりも インデックスに特化 ■ インデックスの構築と検索(retrieve)・データの投入など 22
Property Graph Index ● プロパティグラフで構成されるインデックス ○ ノードとエッジ(リレーション)で構成 ■ エッジは方向性をもった矢印で表現(有向グラフ) ■ ノードとエッジはラベル(カテゴリ・タイプ)とプロパティ(メタデータ) を持つことが可能 ○ 様々な情報を格納できるが、デフォルト(SimpleLLMPathExtractor & ImplicitPathExtractor)ではトリプレット(主語・述語・目的語)と、 文章チャンクの接続関係がインデックスに展開される 23
たとえばこんな感じ(トリプレットのグラフの例) ● この図については後ほど 説明 24
PostgreSQL+pgvector 向けグラフストア実装 ● Amazon Q Developer GitHub 統合で TiDB 用を移植 ○ AI コーディングエージェント(プレビュー提供中) 25
移植は思ったより難航 ● トークン数の限界、過去作業に関するコンテキスト引き 継ぎなどでそこそこ苦労 ○ ORM として SQLAlchemy を使っているが、PostgreSQL 用の Dialect(方言)と TiDB 用の(外部)Dialect ではベクトルの記述・ 比較演算子に加えて JSON や配列(リスト)値の埋め込み方が違う などのハマりポイントがいくつかあった ■ JSON は JSONB に、配列を扱う IN 句は ANY に書き換えるなどして対処 ○ コンテキスト引き継ぎは諦めて都度 Sub-Issue で細かく指示 26
Streamlit ● データ可視化・分析系 Web アプリ開発用フレームワーク ○ 言語は Python ○ 一般的なデータ可視化・分析系 UI 以外にチャットボット向け UI も含む ■ 今回はこれを使う 27
[2] 試用 28
サンプルデータを投入 ● LlamaIndex のサンプル文書の日本語訳 ○ ポール・グレアムのエッセイ ■ https://github.com/hmatsu47/llama_index_property_graph_test/blob/main/dat a/example_ja.txt 29
Streamlit で単答チャットアプリ化 30
インデックス生成 [1] 文書のチャンク化→グラフ化 ● 1,000 文字前後(デフォルト)の文章に分割して保存 ○ 1 文書あたり 1 つの親(node)ノードを生成 ○ チャンク化した文章を text_chunk ノードとして保存 ● チャンクの接続関係(前後・親)をグラフ化 ○ text_chunk ノードから親ノードを指す SOURCE エッジを生成 ○ text_chunk ノードに保存された文章の前後関係を表す PREVIOUS / NEXT エッジを生成 31
インデックス生成 [2] トリプレットの抽出 ● チャンク化した文章から「主語+述語+目的語」の組み 合わせをいくつか抽出 ○ 主語と目的語を entity ノードとして個別に保存 ○ 主語・述語・目的語の関係性をエッジとして保存 ○ 抽出元の文章チャンクを示す ID(識別子)をノード・エッジそれぞ れのプロパティに記録 私 食べる パン 32
インデックス生成 [3] 各ノードにベクトルを保存 ● ベクトル検索用の埋め込みベクトルを保存 ○ text_chunk ノードには文章チャンクの埋め込みベクトル ○ entity ノードにはキーワード(主語・目的語)の埋め込みベクトル ○ node ノードには保存せず(null) 33
実際のテーブル構成 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) 34
ノード用テーブル(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) ノードは埋め込みベクトル を持てる 35
エッジ用テーブル(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) 36
ノード用テーブルに含まれる 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 になる) 37
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は
埋め込みベクトルを
持たない
埋め込みベクトルも
持たない
38
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は持たない
持たない
文章チャンクの
埋め込みベクトルを持つ
39
文章チャンク関連のエッジ行の内訳 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) 文章チャンク関連の エッジの数 40
チャンクの前後関係を示すエッジ行(レコード)の例
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
41
文章チャンクのグラフ構造 ● node(黄)ノードが 中心にある ○ 全ての text_chunk(青) ノードから SOURCE エッジで接続 42
ノードに含まれる 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)に
→同じ単語が複数登録されることはない
43
ノードに含まれる 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と同じ)
44
ノードに含まれる 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「文章を書くこと」が
埋め込みベクトル化されている
45
トリプレットを示すエッジ行(レコード)の例
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はシーケンス値
→同じ組み合わせのトリプレットが複数存在し
うる(別の文章チャンクから抽出した場合)
46
トリプレットのグラフ構造(一部) ● entity ノード「私」を 中心に見てみる ○ 一部、複数階層の接続が 抽出されている (私→初めて書いた→プログラム →終了しない→ことがある/ →入力された→パンチカード) 47
検索時(デフォルトの Retriever 構成) ● LLM に渡すコンテキストをグラフストアで検索・取得 ○ VectorContextRetriever で entity ノードをベクトル検索 ■ ベクトル類似度の高い entity ノードの単語を含むトリプレットを取得 ■ あわせてトリプレット抽出元の text_chunk ノードを取得 ○ LLMSynonymRetriever で類義語を複数(デフォルト 10 個)生成 し、それらを使って entity ノードを主キー検索 ■ 同じ主キー値を持つ entity ノードの単語を含むトリプレットを取得 ■ あわせてトリプレット抽出元の text_chunk ノードを取得 48
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ノードに限定してベクトル検索 49
グラフ構造を辿る 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;
(右上に続く)
50
LLM に送信 ● 取得したトリプレットと文章チャンクをコンテキストと して付加して質問文を LLM に送信 ○ ここから先は通常の RAG と同じ ● 文章チャンクのグラフ構造は使用していない(おそらく) ○ トリプレットのエッジに保存された ID を使って text_chunk ノードを取得してコンテキストとして使っているのみ 51
実際の送信プロンプト例(コンテキストと質問文) ● 質問文「学生時代にしたことは?」 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: 質問文 52
試してみた感想 ● 応答内容が絞り込まれている印象 ○ ハルシネーションが軽減される代わりに少しそっけない? ■ プロンプトとパラメータのチューニング次第? →取得トリプレット数や辿るグラフ階層の数、取得チャンク数など ○ 別々の場所にある文章チャンクを関連づけて抽出している模様 ■ 文章チャンクから抽出したトリプレットのグラフ構造を介して 53
試してみた感想 ● 応答が少し遅い ○ LLMSynonymRetriever で類義語抽出を LLM にさせている部分 の待ち時間が余分にかかっている ■ 今回のケースではあまり有効に機能していない様子だったので LLMSynonymRetriever を外しても良かったかも? 54
試してみた感想 ● 条件次第で RDBMS もグラフストアとして使用可能? ○ 辿るグラフ階層数が 1(デフォルト)であれば通常の JOIN で十分 ■ 2 〜 3 階層になってくるとエッジの数が格段に増えそうなので厳しい? 55
参考(今回使ったコードなど) ● 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 56