14
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

GraphRAG を試してみたら意外と面倒だった話

Last updated at Posted at 2024-12-13

Introduction

RAG(Retrieval-Augmented Generation; 検索拡張生成)の派生形に GraphRAG という RAG があります。

その存在は以前から知っており、各所で「こういうのありますよ」と紹介をしていたのですが、仕組みはある程度理解しているので、特に実装は試さず放置していました。

Baseline RAG は(高い性能を求めないなら)動くものを実装するのはそこまで難しくないので、GraphRAG も比較的簡単だろうと思っていましたが、実際に取り組んでみると意外とめんどうくさかったのでこの場を借りて共有します。

想定読者

  • GraphRAG 興味あるけど、触ったことのない方
  • GraphRAG の雰囲気が知りたい方

まずは Neo4j の構築から始める

GraphRAG の最大の特徴は、ナレッジグラフをデータベースとして活用する点にあります。

RAG が主に単純な検索と生成を組み合わせたアプローチであるのに対し、GraphRAG はナレッジグラフによって構築された情報の関係性を取り込み、生成モデルに統合します。これにより、単なる情報の提示ではなく、情報間のつながりを反映した応答が可能となります。

項目 RAG GraphRAG
データベース 検索エンジン(ElasticSearch 等) ナレッジグラフ(Neo4j 等)
主な特徴 単純な検索と生成の組み合わせ グラフ構造による情報関係性の活用
メリット 実装が容易 文脈に基づいた精度の高い生成
デメリット データ間の関係性を扱えない 初期構築とクエリ設計に時間がかかる

GraphRAG を実装する上でナレッジグラフの作成は避けられません。

まずは、ナレッジグラフの基盤となる DBMS として、Neo4j を構築するところから始めます。Neo4j はグラフデータを扱うための強力なツールであり、ノードやリレーションシップを柔軟に定義できます。商用利用する場合はライセンス等色々と気になりポイントありますが、今回は検証なので問題なしです。

docker-compose.yml

Neo4j は公式で docker も公開しているので、docker-compose.yml を実行すれば簡単に構築できます。

docker-compose.yml
version: '3.8'

services:
  neo4j:
    image: neo4j:5.26.0
    container_name: neo4j
    ports:
      - "7474:7474" # HTTP ポート
      - "7687:7687" # Bolt プロトコル用ポート
    environment:
      - NEO4J_AUTH=neo4j/password # ユーザー名とパスワード
      - NEO4JLABS_PLUGINS=["apoc"] # apoc プラグイン用
      - NEO4J_dbms_security_procedures_unrestricted=apoc.*
    volumes:
      - ./volumes/data:/data
      - ./volumes/logs:/logs
      - ./volumes/import:/var/lib/neo4j/import
      - ./volumes/plugins:/plugins

ここでのポイントは NEO4JLABS_PLUGINS=["apoc"] です。
後で python から Neo4j を叩くときに、apoc プラグインを Neo4j にいれることを求められるので、環境変数で指定しておくと楽。
(環境変数で指定しておくことでインターネットから自動で取ってきてくれる。が、プロキシ環境下等、インターネットに制限がある環境だと自分で取ってくる必要がある可能性も。)

また、今回はやりませんが、Neo4j 上でベクトル計算を行う場合はベクトル計算用のプラグインも併せて導入する必要があります。

Neo4j を起動

docker-compose up で環境を起動し、http://localhost:7474/browser/ にアクセスする。

ログイン画面が表示されるので、ログインする。ユーザ名とパスワードは ↑ の docker-compose.yml で指定しているので、そのまま使っているなら以下の認証情報でログインできます。(パブリックなサーバに立てるときは必ず変更してください。)

  • Connect URL: neo4j://localhost:7687
  • Username: neo4j
  • Password: password

image.png

現時点では何のデータも入っていません。

データを入れるには、Neo4j の画面から Cypher クエリを叩いたり、python から入れたりなどいくつか方法があります。

テストデータをいれる

それでは構築した Neo4j にデータを入れていきます。

実際の業務でナレッジグラフを構築していく場合は、GraphRAG の性能を最大限に引き出すためも適切な設計が必要不可欠です。が、ここでは GraphRAG の検証なので、適当なデータを放り込んでいきます。

今回は Langchain が WikipediaLoader というおもしろい機能を提供しているので、それを使ってデータを入れていきます。

検証用データの作成

wikipedia-loader.py
from langchain_community.document_loaders import WikipediaLoader

def recurrent_wikiloader(word, recurrent):
docs = WikipediaLoader(
    query="Nomura Research Institute",
    lang="en",
    load_max_docs=4,
    load_all_available_meta=False,
).load()

WikipediaLoader は指定したクエリで Wikipedia の検索をしてページを取ってきてくれる機能。

今回は弊社 Nomura Research Instituteen の Wikipedia で検索し、出てきた関連ワードで検索、またさらに関連ワードで検索……と再帰的に繰り返し、ナレッジグラフを構築します。
なお、en の理由は後で利用する ML モデルに英語ベースのものがあるからです。OpenAI API 等を利用する場合は日本語で大丈夫です。

再帰的な検索を関数にしたらこんな感じ。

from langchain_community.document_loaders import WikipediaLoader

def recursive_wikipedia_search(query, load_max_docs=4, lang="en", depth=3, visited=None):
    """
    再帰的にWikipediaを検索する関数。

    Args:
        query (str): 検索する単語。
        load_max_docs (int): 取得する関連ワード数。
        lang (str): 言語コード (デフォルトは "en")。
        depth (int): 再帰の深さ。
        visited (set): すでに訪れた単語のセット。

    Returns:
        dict: 記事とリンクされた単語の構造体。
    """
    if visited is None:
        visited = set()

    if depth == 0 or query in visited:
        return {}

    print(f"Searching for: {query} (Depth: {depth})")
    visited.add(query)

    loader = WikipediaLoader(
        query=query,
        lang=lang,
        load_max_docs=load_max_docs,
        load_all_available_meta=False,
    )
    try:
        documents = loader.load()
    except Exception as e:
        print(f"Failed to load article for {query}: {e}")
        return {}

    next_queries = [document.metadata["title"] for document in documents if document.metadata["title"] not in visited]

    # 再帰的に検索
    result = {query: {"summary": documents[0].metadata["summary"], "linked": next_queries}}
    for next_query in next_queries:
        nested_results = recursive_wikipedia_search(next_query, load_max_docs=load_max_docs, lang=lang, depth=depth-1, visited=visited)
        if nested_results:
            result.update(nested_results)

    return result

GraphRAG の Retriever

データの作成が終わり、データを Neo4j に入れていく前に GraphRAG の Retriever について説明しておきます。

GraphRAG では Retriever はナレッジグラフに対してクエリを投げて検索を行いますが、Neo4j を使う場合いくつかの戦略があります。

  • ノード・リレーション検索
  • ベクトル検索
  • ワード検索

いずれも Cypher クエリによる検索になりますが、これらを使って上手く実装すれば Baseline RAG も包含した形で GraphRAG を実装できるかもしれません。

データ入力

それでは、docs からロードしたデータを Neo4j に放り込んでいきます。
今回は Wikipedia のページの Title と Summary および、Summary の Embedding Vector を入れていきます。

ちなみに、個人的感想として、Neo4j を使う場合はこの Embedding Vector の格納が曲者です。
Python で浮動小数点を扱う場合、6.76569268e-02 みたいな表記がされますが、Neo4j は e-01 という表現を受け付けないので一度文字列に変換して入力してやる必要があります。
これがめちゃくちゃ面倒。

from neo4j import GraphDatabase
from sentence_transformers import SentenceTransformer
import numpy as np

# 接続情報定義
uri = "bolt://localhost:7687"
driver = GraphDatabase.driver(uri, auth=("neo4j", "password"))

# embedding model
model = SentenceTransformer('sentence-transformers/all-MiniLM-L6-v2')


def add_page(name, summary, vector):
    with driver.session() as session:
        session.run(f"""CREATE (p:Page {{name: "{name}", summary: "{summary}", vector: [{",".join([f"{float(e):10f}"for e in vector]).replace(" ", "")}]}})""")


def add_relationship(page1, page2):
    with driver.session() as session:
        session.run(f"""MATCH (a:Page {{name: "{page1}"}}), (b:Page {{name: "{page2}"}})
    CREATE (a)-[:LINKs]->(b)""")


for k, v in pages_dict.items():
    add_page(k, v["summary"].replace('"', '\''), model.encode(v["summary"]))

for k, v in pages_dict.items():
    for linking_page in v["linked"]:
        if linking_page not in pages_dict.keys():
            add_page(linking_page, "", "")
        add_relationship(linking_page, k)

データ入れるとこんな感じになります。
ここまでのデータは今回の検証ではいらないのですが、データをいっぱい入れるとナレッジグラフ感があっていいですね。

graph.png

長くなりましたが、これで準備は完了です。

GraphRAG を試す

GraphRAG の流れを改めて説明すると GraphRAG の流れは以下のようになります。

  1. GraphRAG のシステムがユーザからクエリを受け取る
  2. GraphRAG からの情報取得のために、LLM がクエリを参考に Cypher クエリを生成する
  3. (Cypher クエリが正しいかどうかを検証したい場合検証用のチェーンで検証する)※ 今回はしない
  4. Cypher クエリを使って Neo4j に問い合わせる
  5. 取得した Context を踏まえてユーザからのクエリの推論

流れとしては単純なのですが、GraphRAG の最も重要な部分であり、かつ、今回の検証で最も鬼門だったのが "Cypher クエリの生成" です。今回は弱いモデルでも試したのですが、全く正しいクエリを生成してくれませんでした。
(私の周りでは GraphRAG を使っているという話をあまり聞かないのですが、このあたりの不安定さがいまいち流行らない原因の一つなのかなと感じました。)

Please provide three Wikipedia pages that link to Nomura Research Institute. (野村総合研究所をリンクしている Wikipedia のページを 3 つ教えてください。)

何はともあれ、構築したナレッジグラフで GraphRAG を試してみます。

Context なし(GraphRAG からの情報がない場合)

まずは比較対象として、ナレッジグラフがないと問題が解けないことを確認しておきましょう。
単純にモデルに質問を行うチェーンを構築します。

llm は各自お好きなモデルを用意して下さい。
本記事では GPT-4o-miniLlama3.2-3B で試しました。

from langchain.chains import LLMChain, TransformChain
from langchain.prompts import PromptTemplate
# 質問応答チェーン用のプロンプト
qa_prompt = PromptTemplate(
    template="Answer the following question based on the provided context: {context}\n\nQuestion: {question}\n\nAnswer:",
    input_variables=["context", "question"]
)

# 質問応答チェーン
qa_chain = LLMChain(
    llm=llm,
    prompt=qa_prompt
)

qa_chain.run(context="", question="Please provide three Wikipedia pages that link to Nomura Research Institute. ")

想定通り正しく回答できていないことがわかります。

GPT-4o-mini Llama-3.2-3B
I cannot browse the web or access real-time Wikipedia data. However, you can search for pages that link to "Nomura Research Institute" by visiting Wikipedia and using the "What links here" feature. To do this: 1. Go to the Wikipedia page for "Nomura Research Institute." 2. On the left-hand side, under the "Tools" section, click on "What links here." 3. This will show you a list of Wikipedia pages that link to the Nomura Research Institute page. This method will give you the most accurate and up-to-date results. The following three Wikipedia pages link to Nomura Research Institute:\n\n1. Nomura Research Institute: This page provides an overview of Nomura Research Institute, a Japanese multinational information technology company. It includes information about the company's history, services, and locations.\n2. Nomura Research Institute (company): This page provides more detailed information about Nomura Research Institute, including its corporate structure, financial information, and notable projects.\n3. Nomura Research Institute (disambiguation): This page provides a list of all pages on Wikipedia that link to Nomura Research Institute. It includes links to pages related to the company, such as its subsidiaries and affiliated companies.

Context あり(GraphRAG からの情報がある場合)

それでは、本命の GraphRAG を使った推論に移ります。
ユーザクエリから Cypher クエリを生成するために以下のようなチェーンを構築します。

from langchain_neo4j import Neo4jGraph, GraphCypherQAChain

# Neo4j のセットアップ
neo4j_graph = Neo4jGraph(
    url="bolt://localhost:7687",
    username="neo4j",
    password="password"
)

# Cypher クエリ生成用のプロンプト
cypher_prompt = PromptTemplate(
    template="Create a Cypher query to answer the following question: {question}\nAnswer:",
    input_variables=["question"]
)

# Cypher クエリ生成チェーン
cypher_generation_chain = LLMChain(
    llm=llm,
    prompt=cypher_prompt
)

# LangChain QA チェーンのセットアップ
gc_qa_chain = GraphCypherQAChain(
    graph=neo4j_graph,
    cypher_generation_chain=cypher_generation_chain,
    qa_chain=qa_chain,
    output_parser=output_parser,
    graph_schema=neo4j_graph.get_schema,
    allow_dangerous_requests=True,
)

Langchain を用いて一気に回答を生成する場合は ↑ のようなチェーンを組めばよさそうです。
が、実際のところ gc_qa_chain はほとんど正しく動かず。
というのも、Cypher クエリの出力が安定せず、チェーンの途中でエラーが出て止まってしまうためです。

今回は検証の為、output_parser を色々と試行錯誤して以下のような結果を出すまで頑張りましたが、実際のところはこのあたりの実装がかなり難しくなるかと思いました。正直 Langchain でがちがちにチェーン組むよりも、自前である程度書く方が楽かもしれないです。Graph RAG を扱えるライブラリは Langchain 以外にも存在しているので、比較してみるのもよいかもしれません。
(だれかうまいやり方を知っていたら教えてください。)

いずれにせよ、試行錯誤で以下の結果が得られました。

GPT-4o-mini Llama-3.2-3B
Cypher クエリ MATCH (source:Page)-[:LINKs]->(target:Page {name: "Nomura Research Institute"}) RETURN source.name AS WikipediaPage LIMIT 3; SELECT DISTINCT p1.name AS "Source", p2.name AS "Target" FROM Page p1 JOIN Page p2 ON p1.name = p2.name WHERE p1.name!= p2.name AND p1.name IN (:Page) AND p2.name IN (:Page) ORDER BY p1.name ASC, p2.name ASC;Note: The above query will give you three pages that link to Nomura Research Institute.
最終アウトプット Here are three Wikipedia pages that link to Nomura Research Institute:\n\n1. List of computer security companies \n2. Nomura \n3. Richard Koo エラー (Cypher クエリが生成できていない。)

GPT-4o-mini の回答では以下の 3 つということで正しく回答が出来ています。

  • List of computer security companies
  • Nomura
  • Richard Koo

この問題は簡単なものなので、それほど感動する結果ではないのですが、GraphRAG ありなし比較としては十分かと思います。

なお正しい Cypher クエリが出力できた時点で勝ち確な問題ではあるのですが、そもそも Llama3.2-3B では Cypher クエリが正しく出力できず。GraphRAG を組む前に使いたいモデルで Cypher クエリの出力ができるかどうかは試しておく必要がありそうです。


お粗末な実装ですが、これで GraphRAGの大体の実装は完了です。

なお、今回は試しませんでしたが、もし Neo4j でベクトル検索がしたくなった場合は、gds プラグインを Neo4j に入れ、↓の感じのクエリを LLM に生成してもらうとよさそうです。
(単純な cypher クエリでもモデルによっては難しかったので、こんなの絶対生成してくれなそうですが...。)

MATCH (n:Page), (m:Page)
WHERE id(n) = 1
RETURN m, 
       gds.alpha.similarity.cosine(n.vector, m.vector) AS similarity
ORDER BY similarity DESC
LIMIT 10

まとめ

ということで、簡単ですが、GraphRAG を大まかな流れに沿って作成しました。

自分で実装してみると、GraphRAG はかなり調整が必要そうということがわかりました。今回は試したのは検証レベルのお粗末な実装なので、実際に使う場合はチェーンを組みなおすなりして、実用に足るものに変更する必要がありそうです。

もっと上手い実装をご存じの方いらっしゃったら、コメントで教えてください。

それでは、ここまで読んでくださった方ありがとうございました。

14
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
14
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?