GraphRAGで自家製Notion Q&A作って遊んでみた

# 概要
今回は最近話題のGraphRAGを使って、自家製NotionQ&Aを遊んでみた、という記事です

Microsoftが発表した、GraphRAGというグラフを用いたRAGをMicrosoftのGitHubを参考にして使って、LlamaIndexのNotionPageReaderも使って、Notion AIの主要機能の一つであるNotion Q&Aを実装しました。

https://www.microsoft.com/en-us/research/blog/graphrag-unlocking-llm-discovery-on-narrative-private-data/

（LlamaIndexでGraphRAG使ってやりたかったのですが、ちょっと複雑そうでやめちゃいました。やり方を記事にしてくださる方のご登場をお待ちしてます🥺）

(あくまで話題の手法で遊んでみた、記事なので詳しい方がもっと発展的な記事をあげてくださるのを待っています🥺）

:::note alert
OpenAIのAPIを使います。情報が外部に流出するリスクを気にする方は、重要な内容を含めないようにしてください。
:::


## 背景
僕はNotionをこよなく愛す大学院生です。Notionキャンパスリーダーにも選んでいただきました。
もはやNotionは第二の脳で、何から何までNotionに情報を蓄積しています。

もちろんNotionAIも活用していて、中でもNotionQ&Aは自分のNotionの内容を検索できるという神機能なので活用していました。

https://www.notion.so/ja-jp/product/ai

ただ、少し前のNotionAIの強化アップデートを境に、僕だけなのかわかりませんが、以前は答えられていた質問に急に答えてくれなくなり、ほぼ使い物にならなくなってしまいました。。。（NotionQ&Aの中身のアーキテクチャーが変更されて不具合発生中？）
→あくまで僕の話ですし、他の方でうまくいっていないという話は聞かないので本当に僕だけかも？？？

そこでウズウズしていたので、自分でNotionQ&A作っちゃえばいいじゃん！！！ということで、最近話題のGraphRAGの実装してみたいという思いも相待って、GraphRAGで自家製NotionQ&Aを作ってみるということになりました。

https://x.com/A7_data/status/1819068312854360283

（実装後の写真。前までは答えてくれていたのに...😢）
前までできていたので、技術的に不可能ということではないということはわかっているので、復旧を待つのみです。

# 自己紹介
普段データサイエンティストを目指して勉強している、大学院の1年生です。
GNN×インフラを中心に普段は研究しています。生成AI（特にLLM）に興味を持ち、日々勉強中です。

これまでのデータサイエンスの勉強記録や優良教材・記事の共有、シェアを日々Twitter（X）で行っています。フォロワーはありがたいことに8400人を超えました。

共に成長していきたい！という方はぜひ覗いてみてくださいね

https://x.com/A7_data

僕についてもっと知りたい方はこちら

https://mohki7.notion.site/Mototsugu-Oki-c478f842c4de48f394e2e24b58e206bb

# GraphRAGについて
## 通常のRAG
通常のRAGでは
1. 文章をベクトル化
1. ベクトルデータベースに保存
1. ユーザーからの質問（クエリ）と類似度が高い文章をそこから持ってくる

この3ステップに分けられる。

## GraphRAG
GraphRAGでは、文章をベクトル化してベクトルデータベースに保存する代わりに、文章からグラフを作成してグラフデータベースに保存する。

これによって、文章内に出てくる単語同士の関係性などを保持して保存ができるため、単なるベクトル化ではできないことができる。

実際、これによってユーザーからの質問が抽象的でもとのドキュメントには直接的には言及がないような内容であっても、RAGの検索としてヒットさせ、出力することが可能になる。

よくわからなかった方はこのYouTube動画を見ればわかると思う

https://youtu.be/PqAgkfg0MA0?si=eaAnjRVEhcvyxAz1

# Notionインテグレーション、データベースIDの取得
今回は自分のNotion内の特定のデータベースに格納されている内容について問い合わせるので、必要なものを準備します。

必要なものは「**Notion インテグレーションキー**」と「**データベースID**」です。

もう他の記事でたくさん解説されていると思うので、ここでは解説しません。この記事とか参考になります。

:::note warn
この記事内で、データベースIDが「https://www.notion.so/<データベースID>?v=<ビューID>」のように説明されていますが、正しくは、notion.so/の後にユーザーネームが入るのでそのユーザーネームの直後から?vの直前までになります。
:::


https://zenn.dev/kou_pg_0131/articles/notion-api-usage


# GraphRAGの実装
さて、実装していきましょう！！！

OpenAIのAPIが必要です。（Geminiなら無料で使えたのでやりたかったのですが、GraphRAGに応用する方法がわからなかったのでどなたか書いてくださると嬉しいです🥺）

今回はGPT-4o-miniを使いました。理由は安いから、それだけです。GraphRAGは元の文章（検索対象）からグラフを作成するので、APIコストが多少多くかかるというデメリットがあります。高額なモデルを選択するのはやめておきましょう。
（デフォルトでは確かGPT-4-turboになっています。高いです。忘れずに変更しましょう）

https://platform.openai.com/docs/overview

OpenAIのAPIを持ってない方は取得しましょう

さて、書いていきます

```
!pip install graphrag
```
まずはGraphRAGを使えるようにインストール。

```
!python -m graphrag.index --init --root ./ragtarget
```
GraphRAGを構築するフォルダで、このコマンドを実行します。
すると、GraphRAGのために必要なフォルダが作られます。おそらくここでエラーが出ますが安心してください。あとで治りますよ。

ちなみになんでエラーになるかというと、inputフォルダの中身がない（もしくはinputフォルダ自体がない）からです。GraphRAGライブラリでは、このinputフォルダの中にあるフォルダの内容を元にグラフを作っていきますが、まだデータを入れていないのでエラーになります。

以下では、そのinputフォルダにNotionのデーベースの内容を入れるコードを実装します。

:::note warn
ここで生成されるsettings.yamlの中のmodel指定部分をGPT-4o-miniなど安いモデルに変更しましょう。
.env内のAPI_KEYを自分のOpenAI APIキーに変更することも忘れずに。
:::

```
import os
os.environ['OPENAI_API_KEY'] = os.environ.get('OPENAI_API_KEY')
os.environ['NOTION_INTEGRATION_TOKEN'] = os.getenv('NOTION_INTEGRATION_TOKEN')
os.environ['DATABASE_ID'] = os.environ.get('DATABASE_ID')
database_id = os.environ.get("DATABASE_ID")
integration_token = os.environ.get("NOTION_INTEGRATION_TOKEN")
```

各APIキー、データベースIDを読み込めるようにします。
素人コードですいません。なんか読み込みがうまくいかなくて悪戦苦闘しているうちにこんなコードになりました...🥺

```
import logging
import sys
from llama_index.core import SummaryIndex
from IPython.display import Markdown, display
import os
from tqdm import tqdm
import requests
from llama_index.readers.notion import NotionPageReader
import urllib3
# ログレベルの設定
# logging.basicConfig(stream=sys.stdout, level=logging.DEBUG, force=True)
urllib3.disable_warnings()
logging.getLogger("urllib3").setLevel(logging.WARNING)
from llama_index.core import Settings, VectorStoreIndex
from llama_index.llms.openai import OpenAI
```
必要なライブラリをインポートします

```
def get_database_pages(databese_id):
    url = f"https://api.notion.com/v1/databases/{databese_id}/query"
    headers = {
        "Notion-Version": "2022-06-28",
        'Authorization': 'Bearer ' + integration_token,
        'Content-Type': 'application/json',
    }
    response = requests.post(url, headers=headers)
    return response.json()

def extract_page_titles(database_data):
    page_titles = []
    for page in database_data.get('results', []):
        properties = page.get('properties', {})
        for prop in properties.values():
            if prop['type'] == 'title':
                title = prop['title'][0]['plain_text'] if prop['title'] else ''
                page_titles.append(title)
                break
    return page_titles


# # 使用例
# database_data = get_database_pages(DATABASE_ID)
# page_titles = extract_page_titles(database_data)
# print(page_titles)

def get_page_title(page_id):
    url = f"https://api.notion.com/v1/pages/{page_id}"
    headers = {
        "Notion-Version": "2022-06-28",
        'Authorization': 'Bearer ' + integration_token,
        'Content-Type': 'application/json',
    }
    response = requests.get(url, headers=headers)
    page_data = response.json()
    
    # ページのプロパティからタイトルを抽出
    properties = page_data.get('properties', {})
    for prop in properties.values():
        if prop['type'] == 'title':
            return prop['title'][0]['plain_text'] if prop['title'] else ''
    
    # タイトルが見つからない場合は空文字列を返す
    return ''

# 使用例
# page_id = "page_id here"
# title = get_page_title(page_id)
# print(f"Page Title: {title}")
```

Notion APIキーを使って、データベースの各ページの内容を取得する機能を実装します。
（GraphRAGのライブラリのinputフォルダの中身を用意します。）
（ページごとに内容が異なるようなデータベースで僕は作ったので、ページごとにページの名前を取得し、そのページの名前をフォルダ名、中身の.txtファイルにそのページの中身、となるように実装しています。RAG的にもそっちの方がわかりやすいのかな？と勝手に思ってそうしました。）

```
# Initialize NotionPageReader
reader = NotionPageReader(integration_token=integration_token)

# Load data from Notion
# 指定したデータベースが持つページのデータを読み込む
documents = reader.load_data(page_ids=reader.query_database(DATABASE_ID))  # List of page IDs to load

# 辞書型に整形
# ページタイトルをキー、ページ内容をバリューとする辞書を作成
docs = {}
for doc in tqdm(documents):
    page_title, page_contents = get_page_title(doc.id_), doc.text
    docs[page_title] = page_contents

# 辞書をtxtに書き出す。キーをファイル名、値をコンテンツとして書き出す
for key, value in tqdm(docs.items()):
    file_name = key.replace('.', '_').replace('/', '-')
    with open(f"ragtarget/input/{file_name}.txt", "w") as f:
        f.write(value)
```
これによって、目的だったGraphRAGの「input」フォルダの中に自分のNotionデータベースの内容がどんどんテキストファイルで格納されていくはずです。

```
!python -m graphrag.index --root ./ragtarget
```
あとはこの1行を実行すれば、inputフォルダの中身に従ってグラフが構築されていきます。
このグラフ生成にOpenAIのモデルを使っています。プロンプトを見たい方はpromptsフォルダの中に入ってるので見てみると勉強になると思います。

inputフォルダの中身が多いとそれなりに時間とお金がかかります。待ちましょう。
まあでも僕もそれなりの量を使いましたが、色々試行錯誤含めて0.2ドルほどで済んでるのでまあ良いでしょう。30円くらい？

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3179677/b5be61b8-5330-f4ea-63ed-323f11ed7b73.png)


# 完成！
グラフ作成が無事終われば、完成です！！！
（もしエラーが出た方がいらっしゃったら教えてください🥺記事をアップデートします）

早速質問してみましょう。
以下のコードでできます。

```
!python -m graphrag.query \
--root ./ragtarget \
--method global \
"質問文"
```
--method globalの部分を、--method localとすればローカルサーチも可能です。

無事、回答は得られましたか？
僕もいくつか試しましたが、質問を抽象的にしてもしっかり答えられていた印象です。


# 終わりに
いかがでしたか？
今回は遊んでみた、という記事なのでだいぶ粗かったと思いますが、今後はLlamaIndexで実装してちゃんとプロダクトに組み込めるようにしていきたいですね

頑張ります。ぜひアドバイスなどしていただける方は、僕のXのDMや質問箱に送ってくださるとめちゃくちゃ喜びます🥺

僕のXでは日々、データサイエンスやAI、GNNやNotion、Arcなどの便利ツールなどについて、有益情報、優良教材、記事のシェア、勉強記録の発信などをしています。

フォロワーはありがたいことに8400人を超えました。これからも日々発信していくので、共に成長していきたい！という方はぜひ、覗いてみてください！！！

https://x.com/A7_data

ではまた！！！！