コンテンツにスキップ

学習メモ — 知識グラフ RAG(LlamaIndex PropertyGraphIndex)

rag_graph_basics/ の写経で学んだことの整理。素手版(rag_basics/ex07)で「LLM トリプル抽出 → NetworkX → サブグラフ注入」を一度自分の手で書いた上で、代表ツール LlamaIndex の PropertyGraphIndex で同じ概念を組み直した記録。


1. このレクチャーで腹落ちさせたいこと

ベクトル RAG は「似ているか」で検索し、グラフ RAG は「繋がっているか」で検索する。

  • ベクトル RAG: 質問とチャンクの埋め込みが近いものを上位 k 件拾う。表面的な意味の近さ。
  • グラフ RAG: 文を (主語, 関係, 目的語) のトリプルに分解してグラフを作り、質問のエンティティ起点で関係をたどって関連サブグラフを集める。

単発の事実質問(「CoinPost とは?」)では両者に差は出ない。差が出るのは マルチホップ質問(複数の関係を連鎖させないと答えが出ない質問)。ex05 で「山本さんの所属先がある場所の名所は?」(山本さん→JAIST→石川県→金沢市→兼六園、3 ホップ)を投げると、ベクトル RAG は中間文を拾えず幻覚し、グラフ RAG は関係を辿って言い切る。


2. LlamaIndex の三層構造(LangChain と設計が違う)

LlamaIndex は Index → Retriever → QueryEngine の三層がはっきりしている。

Document(入力)
  │  from_documents(投入+抽出+格納を一括)
Index(PropertyGraphIndex / VectorStoreIndex …)  ← 索引そのもの
  │  as_retriever()
Retriever(検索だけ)                              ← 質問 → 関連ノード
  │  as_query_engine()(retriever + LLM)
QueryEngine(検索 + 生成)                          ← 質問 → 回答
  • Settings が DI コンテナの役割。Settings.llmSettings.embed_model にモデルを注入しておくと、from_documentsas_query_engine も引数で渡さなくてもそこから拾う(_corpus.pyconfigure_settings())。
  • LangChain の LCEL(| で繋ぐ宣言的パイプ)とは思想が違い、LlamaIndex は オブジェクトのメソッドチェーンindex.as_query_engine().query())で組む。

3. グラフ構築の中身(ex01)

index = PropertyGraphIndex.from_documents(
    documents,
    kg_extractors=[SimpleLLMPathExtractor(max_paths_per_chunk=8)],
)

from_documents の中で起きていること(素手版 rag_basics/ex07 との対応):

素手版(ex07) LlamaIndex
extract_triples()(OpenAI を直叩き、JSON でトリプル抽出) SimpleLLMPathExtractor(同じことを内部でやる)
nx.DiGraph()add_edge SimplePropertyGraphStore(in-memory グラフ)
自前の JSON パース extractor 内蔵の parse_fn

SimpleLLMPathExtractorスキーマを固定しない自由抽出。関係ラベル(「所属している」「位置する」)は LLM が文脈から命名する。エンティティの型や関係の種類を固定したいときは SchemaLLMPathExtractor を使う(→ 第 5 節「よくある誤解」)。


4. 検索の中身(ex03)

index.as_retriever() のデフォルトは 2 つの retriever の合成:

  • LLMSynonymRetriever: 質問から LLM に同義語・キーワードを出させ、その語に一致するエンティティを起点にサブグラフを辿る。表記揺れに強い。
  • VectorContextRetriever: 質問を埋め込み、エンティティの埋め込みと類似するノードを起点にする。ベクトルの良さも取り込む。

どちらも返すのは「チャンク」ではなく サブグラフを文字列化したものA -> 関係 -> B の連なり + include_text=True なら元テキスト)。retrieve 結果に -> を含む行が出ていれば「エンティティ起点でグラフを辿れている」証拠。


5. 用語集(必読)

混同しやすい語を階層・対比で並べた早見表。各語に「具体例(どの ex の何)」と「使う API」を併記。

5-1. 知識グラフの構成要素

用語 一言で 具体例(このレクチャー) 使う API / 判定
知識グラフ(Knowledge Graph) エンティティをノード、関係をエッジにした有向グラフ 山本さん→所属→JAIST、JAIST→位置する→石川県 … 全体 index.property_graph_store
トリプル(triplet) (主語, 関係, 目的語) の 3 つ組。グラフの 1 本の辺 (山本さん, 所属している, JAISTの修士課程) store.get_triplets(entity_names=...)
エンティティ(entity / EntityNode) グラフのノード。実体(人・組織・場所・概念) 山本さん / JAIST / 石川県 / 兼六園 get() の戻りで type(n).__name__ == "EntityNode"
関係(relation / Relation) エンティティ間の辺。ラベルを持つ 「所属している」「位置する」 トリプルの真ん中。r.label
チャンクノード(ChunkNode) 元テキストの断片。include_text 用に保持される 「JAIST は石川県に位置する…」の原文 get() の戻りで label == "text_chunk"

判定 1 文: 「文を読んで実体を表すならエンティティ、実体どうしを繋ぐ動詞・前置詞ならば関係」。

5-2. LlamaIndex の部品

用語 一言で 役割 使う API
PropertyGraphIndex グラフ RAG 用の Index 投入→抽出→格納→検索→生成の中心 PropertyGraphIndex.from_documents(...)
PathExtractor 文 → トリプルの抽出器 LLM にトリプルを抜かせる SimpleLLMPathExtractor(自由)/ SchemaLLMPathExtractor(スキーマ固定)
SimplePropertyGraphStore in-memory グラフストア サーバ不要でグラフを保持(Neo4j の代わり) from_documents のデフォルト
PGRetriever グラフ検索器 質問 → 関連サブグラフ index.as_retriever()(内部は LLMSynonym + VectorContext)
QueryEngine 検索+生成 質問 → 回答 index.as_query_engine().query(q)

判定 1 文: 「それは抽出(文→グラフ)か、検索(質問→サブグラフ)か。前者は PathExtractor、後者は PGRetriever。」

5-3. グラフ RAG vs ベクトル RAG

ベクトル RAG グラフ RAG
索引の単位 チャンク(埋め込みベクトル) トリプル(エンティティ+関係)
検索の判定 類似度(似ているか) 到達可能性(繋がっているか)
マルチホップ 苦手(連鎖が切れる) 得意
構築コスト 埋め込みのみ(安い) LLM 抽出が要る(やや高い)
説明可能性 低い 高い(辿った関係を示せる)
代表 Index VectorStoreIndex PropertyGraphIndex

マルチホップ質問 = 複数の関係を連鎖させないと答えが出ない質問。例:「A の所属先がある場所の名所は?」は所属→場所→名所の 3 ホップ。これがグラフ RAG が勝つ典型。


6. よくある誤解の訂正

  • 「グラフ RAG はベクトル RAG の上位互換」は誤り。 単発の事実検索しかしないなら、抽出に LLM を使わない分ベクトル RAG の方が安く速い。マルチホップ・関係経路が必要なときだけグラフ RAG を選ぶ。
  • get_triplets() を引数なしで呼べば全トリプルが返る」は誤り。 SimplePropertyGraphStore.get_triplets()entity_names 等のフィルタを渡さないと空リストを返す。全件見たいときは先に get() で EntityNode を集め、その名前を entity_names= に渡す(ex01/ex02 の実装)。
  • 「抽出すれば同じ実体は 1 ノードに集約される」は誤り。 自由抽出(SimpleLLMPathExtractor)では「Jaist」と「Jaist の修士課程」のようにエンティティが分裂しうる。分裂するとマルチホップが切れる。対策は ① スキーマ固定の SchemaLLMPathExtractor、② エンティティ正規化、③ コーパスを 1 文 1 関係で書く。なお as_query_engine 経由なら LLMSynonymRetriever が同義語で吸収するので、分裂があってもマルチホップは成立しやすい(ex05 で確認)。
  • 「ベクトル RAG が答えられなかった = 黙る」は誤り。 ベクトル RAG は中間文を拾えないと、LLM が文脈の穴を一般知識で埋めて幻覚する(ex05 で「石川県の名所には兼六園や金沢城、21世紀美術館…」とコーパス外の情報が混入した)。「答えない」のではなく「それっぽく間違える」のが怖いところ。
  • 「LlamaIndex には langfuse 連携が無いから計装できない」は誤り。 LangChain のような CallbackHandler 連携が無くても、自前の query 関数を @observe で包めば 1 トレースとして記録できる(_trace.pyobserve_if_enabled)。

7. 困りごと → 見る/使うもの(クイック早見表)

困りごと 見る/使うもの
どんなトリプルが抽出された? ex01。store.get_triplets(entity_names=...) で列挙
グラフの形(ハブ・エッジ)を見たい ex02。NetworkX に載せ替えて degree() / edges(data=True)
検索だけ単体で確かめたい ex03。index.as_retriever().retrieve(q)
回答まで作りたい ex04。index.as_query_engine().query(q)
ベクトル RAG と差が出るか見たい ex05。マルチホップ質問で VectorStoreIndex と並べる
トレースを残したい / グラフを図にしたい ex06。@observe + NetworkX → Mermaid
エンティティが分裂して困る SchemaLLMPathExtractor でスキーマ固定、またはコーパスを 1 文 1 関係に
マルチホップが切れる as_query_engine(LLMSynonym 経由)を使う / グラフの形を ex02 で確認

8. 連載・素手版との対応

  • 素手版グラフ RAG: rag_basics/ex07_graph_rag.py(OpenAI 直叩き + NetworkX)。まずこれを読んで「トリプル抽出 → グラフ → サブグラフ注入」の最小実装を理解してから本フォルダに進むと、from_documents が何を畳んでいるか分かる。
  • ロードマップ: ../../rag/STUDY_NOTES.md STEP 6(GraphRAG)。
  • 別フレームワーク比較: ベクトル RAG 側は rag_basics(素手)/ rag_vector_basics(LangChain + Chroma)。同じ RAG を「素手 → LangChain → LlamaIndex」と 3 系統で書くと、各フレームワークの設計思想の違いが立体的に見える。

作成: 2026-06-12 / 最終更新: 2026-06-12