LlamaIndexでGraphRAGを作って、Bedrockのナレッジベースと精度比較！

2024年12月4日、Amazon Bedrock Knowledge basesがGraphRAGをサポートするようになったというアップデートがありました！

https://aws.amazon.com/jp/about-aws/whats-new/2024/12/amazon-bedrock-knowledge-bases-graphrag-preview/

ただ、以前の検証で「このGraphRAGあんまり精度高くなくね…？」となりました。
まだプレビュー版ということもあると思いますが、現状だとBaseline RAGとそれほど変わらない精度でした。
参考として以下の記事に結果をまとめてあります。

https://qiita.com/har1101/items/f64535c4459820d47524

ただ、これとは別の方法でもGraphRAGをAWS上で構築できるようです。
今回はそれを試してみて、ナレッジベース上から作成するGraphRAGと精度を比較することが目的です。

# 実際に構築してみる
GraphRAGを構築するにあたって、今回参考にさせていただいた記事は以下です。

https://qiita.com/minorun365/items/a62dec701bef17cf7562

https://community.aws/content/2fbDaroTl5DlP2KYECrsdmvwAG3/build-a-graphrag-proof-of-concept

今回はナレッジベースの機能としてではなく、LlamaIndexとAmazon Neptuneを用いてGraphRAGを構築します。

Amazon Neptuneとは、フルマネージド型のグラフデータベースです。
ここでは、GraphRAGにおけるナレッジグラフを作成するために用います。

https://docs.aws.amazon.com/ja_jp/neptune/latest/userguide/intro.html

LlamaIndexとは、生成AIアプリ開発で利用できるフレームワークの1つです。
とりわけRAG関連の機能が充実しており、公式としてAWS上でGraphRAGを構築する方法が記載されているので、こちらを利用します。

https://aws.amazon.com/jp/about-aws/whats-new/2024/05/llamaIndex-amazon-neptune-graphrag-applications/

## 使用するAWSリソース
- Amazon Bedrock
    - Claude 3.5 Sonnet(回答生成用)
    - Amazon Titan Embeddings G1 - Text(埋め込み用)
- Amazon Neptune
    - データベース(プロビジョンド)
    - Jupyter Notebook(実態はSageMaker AI)

### Amazon Bedrockのモデルアクセス有効化
Bedrockコンソールを開き、以下2つのモデルアクセスを有効化します。
・Amazon Titan Embeddings G1 - Text
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3616210/cb84137c-fd54-ca77-61ad-7426d7e951ac.png)

・Claude 3.5 Sonnet
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3616210/67c4ca7c-a250-5bd7-ad98-9b23da840b41.png)

### Amazon Neptuneの構築
Neptuneコンソールにおいて、メニュー一覧から「データベース」配下にある「クラスター」を選択し、「データベースを作成」をクリックします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3616210/f5ae1afb-e6fd-ea95-4f7c-c8af328d08bd.png)

作成画面では、以下のパラメータを選択します。記載していないものはデフォルトでOKです。
- インスタンスタイプ：`Provisioned`
- テンプレート：`開発とテスト`
- クラスターストレージ設定：`Neptune Standard`
- インスタンスクラス：`バースト可能クラス` -> `db.t4g.medium`
- VPCセキュリティグループ：`新規作成` (後で少し書き加えます)
    - セキュリティグループ名：`NeptuneSecurityGroup`
- ノートブックを作成：有効化
    - インターネットアクセス：`Amazon SageMakerを介した直接アクセス`

Neptuneにはサーバーレス版もありますが、サーバーレスとは名ばかりの従量課金制です。最低利用料金がかかってくるスタイルですね。

それよりも、プロビジョンド版で無料利用枠を使ったほうが安く済むそうなのでこちらを使用します。
***
また、ここでノートブックというものも作成します。具体的にはJupyter Notebookですね。

Jupyter Notebookとは、ライブコードとナレーションテキストを含む、完全マネージド型のインタラクティブな開発環境です。
主に機械学習・データ解析などのデータサイエンスに利用されます。

Neptune DBクラスターと同じVPC内にノートブックが作成されるので、ノートブック経由で簡単にNeptuneを操作することができます。

ちなみに、Jupyter Notebookの裏側はSageMaker AIのノートブックインスタンスです。
SageMaker AIのコンソール上でもノートブックが確認できます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3616210/18c2a332-ecfc-081e-13b3-e191a10c9ac2.png)
***
また、1点ネットワーク周りで追加設定が必要です。
DB作成時、一緒に作成されたセキュリティグループを以下のように設定してください。
- インバウンドルール：
    - タイプ：カスタムTCP
    - プロトコル：TCP
    - ポート範囲：8182
    - ソース：NeptuneSecurityGroup
- アウトバウンドルール
→デフォルトで設定されている0.0.0.0/0宛のルールはそのままでOKです
    - タイプ：カスタムTCP
    - プロトコル：TCP
    - ポート範囲：8182
    - 送信先：NeptuneSecurityGroup

このセキュリティグループはNeptuneインスタンスだけでなく、Jupyter Notebookにもアタッチされるので、このような設定にします。

(ネットワーク周りでトラブりまくった結果この設定に行き着きましたが、他にも良い設定があるかもしれません。)

### ナレッジグラフを作成する
ここからはノートブックからNeptune内に接続し、ナレッジグラフを作成していきます。

作成したノートブックを選択して、「JupyterLabを開く」を選択します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3616210/469169f3-a28c-804d-45c3-ee6229052337.png)

その後、「Launcher」画面で「Python 3」を選択します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3616210/f1dd4002-2442-2734-8787-3dfdd991650e.png)

その後、以下コマンドをコードブロックに入力し、実行していきます。
実行するには、画面上部の「▶」を押すか、Shift+Enterを押します。

#### 1.必要なパッケージをインストールする
ターミナル上でpip installするのと同じイメージです。
```python
%pip install boto3
%pip install llama-index-llms-bedrock
%pip install llama-index-embeddings-bedrock
%pip install llama-index-graph-stores-neptune
%pip install llama-index
```

実行後は、画面下部に注目してください。`Idle`になったら次へ進むようにしましょう。
- `Python 3 | Busy` -> 処理実行中
- `Python 3 | Idle` -> 処理完了済み
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3616210/86ffe76b-7014-b4b9-3ff5-5ddbe8569d2a.png)

:::note
依存関係のエラーが発生することもありますが、続行で問題ありません。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3616210/8f96210d-af5d-c9f0-2c3c-964e2c6a6151.png)
:::

***

#### 2.インストールしたライブラリをPythonコード上で使用できるようにインポートする
コードでimport文を書くのと同じイメージです。
```python
from llama_index.llms.bedrock import Bedrock
from llama_index.embeddings.bedrock import BedrockEmbedding
from llama_index.core import (
StorageContext,
SimpleDirectoryReader,
KnowledgeGraphIndex,
Settings
)
from llama_index.core.query_engine import KnowledgeGraphQueryEngine
from llama_index.graph_stores.neptune import NeptuneDatabaseGraphStore
from IPython.display import Markdown, display
```

***

#### 3.変数を登録する
デプロイ時に使用する変数を登録します。
```python
region_name = "<NotebookやNeptuneを作成したリージョン>"
llmodel = "anthropic.claude-3-5-sonnet-20240620-v1:0"
embed_model = "amazon.titan-embed-text-v1"
neptune_endpoint = "<Neptune DBクラスターの書き込みエンドポイント>"
```

***

#### 4.指定したAmazon Bedrockを使用するようにLLMを設定する
上記の3.で指定したBedrockの基盤モデルを使用するように設定します。
```python
llm = Bedrock(
  model=llmodel,
  region_name=region_name,
)
embed_model = BedrockEmbedding(model=embed_model)
Settings.llm = llm
Settings.chunk_size = 512
Settings.embed_model = embed_model
```
`Settings.chunk_size`は、テキストデータを処理する際の分割サイズを指定するパラメータです。
後ほどまた登場します。

***

#### 5.検索対象ドキュメントをNeptuneに登録
まずは検索対象ドキュメントをNotebook内に格納します。
ノートブック上で上部のフォルダ作成ボタンを押し、ルート直下に`Data`フォルダを作成します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3616210/b84a0b50-8e20-029b-afec-98cab657b96c.png)

作成したDataフォルダ内に、検索対象ドキュメントを格納します。
ここでは競走馬であるドウデュースのドキュメントを使用します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3616210/12774eb7-48f5-ab0c-d61c-3646c2ad437a.png)

そして以下のコマンドを実行し、ドキュメントを読み込んで処理できるようにします。
```python
documents = SimpleDirectoryReader("./Data").load_data()
print(documents)
```
printコマンドで、どのように読み込まれているかが確認できます。
特に日本語PDFの場合は別の方法で読み込まないと文字化けすることもあるらしい？ので一度確認することをおすすめします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3616210/ba32324f-3747-8493-2fea-52d853e389b3.png)

確認できたら、そのドキュメントを元にナレッジグラフを作成していきます。
```python
graph_store = NeptuneDatabaseGraphStore(host=neptune_endpoint, port=8182)
storage_context = StorageContext.from_defaults(graph_store=graph_store)
index = KnowledgeGraphIndex.from_documents(
    documents,
    storage_context=storage_context,
    max_triplets_per_chunk=2,
)
```

1行目でNeptuneデータベースへの接続設定を行っています。

2行目でグラフデータを格納するためのストレージコンテキストを初期化し、作成したNeptune接続設定を用いるように指定しています。

3行目以降で、ナレッジグラフを作成していきます。具体的な設定は以下の通りです。
・`documents`：入力となるドキュメント(先ほどDataフォルダ配下に格納したもの)
・`storage_context`：作成したストレージコンテキスト
・`max_triplets_per_chunk=2`：各テキストチャンクから抽出する関係(トリプレット)の最大数を2とする

:::note
トリプレットとは、「主語‐述語‐目的語」のことです。

グラフデータベースでは、「ノード」と「エッジ」という2つの要素で以下のようにデータが埋め込まれます。
これを置き換えると、「主語‐述語‐目的語」のように表現することができます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3616210/ce0c3439-c33d-70fc-d9ef-d1e0d6561952.png)

トリプレットを抽出することで、グラフが作成できるようになる、というわけですね。
トリプレットも後ほど再び出てくるので、頭の片隅に置いていてください。
:::

実行すると、恐らく数分待つと思います。
ステータスがIdleになれば、ナレッジグラフ作成完了です！

## グラフを可視化してみる
Neptuneコンソールのノートブックから「Graph Explorer」を開きます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3616210/aeed7b91-f6df-bade-d4ef-aa8df9ef00ec.png)

Entityをクリックします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3616210/ad682f9e-afd9-823d-fbdf-302072562a5a.png)

すると、ドキュメントから抽出されたノードが表示されます。画面右側にある「Send to Explorer」を片っ端からクリックしましょう。
これによって、グラフの中で確認できるようになります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3616210/e7d5d888-7dc1-c63b-cdec-54503fc3e44d.png)

登録できたら、右上の「Open Graph Explorer」をクリック。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3616210/7c0ed4b5-ee51-5ed6-6de2-5395d080fe5a.png)

すると、こんな風に大量のノードが出てきます。ただ、これだと何がどうなってるのか全くわかりませんね。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3616210/f970250b-ea94-8afa-599b-99e88c0f7cc1.png)

そこで、このノードをダブルクリックしましょう。そのノードに紐づくエッジが出現します。
各ノードの上に書いてある各数字が、そのノードの持つエッジ数ですね。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3616210/478e14e3-dba9-cd4a-063e-fee0068efba1.png)

すべてのノードをダブルクリックしたら、画面上部の更新マークを押します。
すると、ノードとエッジの位置が整理整頓されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3616210/e7ab9707-49f5-725e-2030-7c11d66e14bf.png)

拡大してみると、エッジがどういった関係性を持っているかが表示されます。
数が多いとごちゃっとして見にくいですが…
なにはともあれ、これでナレッジグラフが作成できました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3616210/4a3829e6-4092-e8d5-ba5d-19fe6053b2bd.png)

# クエリを投げてみる
ノートブックに戻って、以下コマンドからクエリを投げてみましょう。
```python
response = index.as_query_engine().query(
"<好きな質問を入力>",
)
display(Markdown(f"<b>{response}</b>"))
```
↓
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3616210/b6444e9c-89a1-71fd-5454-dc662e81a5d3.png)

ふむふむ。まぁ正解でしょうかね。ただ、本当は合計3回戦ったので、全部のレース戦績を出してほしいところです。
ということでもう1つ質問。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3616210/f251e339-8f31-fa5c-88a4-7901cd24e447.png)

あれ…上手く出力されませんでした。

追加でもう1つ質問。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3616210/6b8877ef-9b4e-4a95-f0ef-e3ac2c853e22.png)

こちらも概ね正しいのですが、すべてではないですね。全部で5レース走っているので、3/5だけきちんと出力されています。

上記3つの質問からわかったことは、2022年で時空が止まっており、**2023年以降のデータが入っていない**こと。

ということで、もしかしたら**ドキュメントの埋め込みが一部で止まってしまっているのかも**しれません。
その原因になりうるのは2箇所。

1. `Settings.chunk_size`の値が小さい
1. `max_triplets_per_chunk`の値が小さい

`Settings.chunk_size`は各チャンクのサイズなのであまり関係ないだろうと思いつつも、もう少し大きくても良いと判断して、Embeddingで使用したTitanの上限である1536まで増やし、コマンドを実行します。
```python
# Settings.chunk_sizeを変更する
llm = Bedrock(
model=llmodel,
region_name=region_name,
)
embed_model = BedrockEmbedding(model=embed_model)
Settings.llm = llm
Settings.chunk_size = 1536 #512から1536へ変更
Settings.embed_model = embed_model
```

そして、`max_triplets_per_chunk`も変更します。ここでは2から10に増やします。
以下のコマンドを実行。
```python
# 同じドキュメントと同じストレージコンテキストを使用して、新しいパラメータでインデックスを再作成
storage_context = StorageContext.from_defaults(graph_store=graph_store)
index = KnowledgeGraphIndex.from_documents(
    documents,
    storage_context=storage_context,
    max_triplets_per_chunk=10, #2から10に変更
)
```

:::note
`max_triplets_per_chunk`で指定する数について補足です。
具体例を示すと、`max_triplets_per_chunk`が2の時は以下のような挙動になります。
```python
# 以下のテキストが1チャンクとする:
text = """
AWSはクラウドサービスを提供しています。
EC2はAWSのコンピューティングサービスです。
S3はAWSのストレージサービスです。
Lambda はサーバーレスサービスです。
"""

# max_triplets_per_chunk=2 の場合、抽出されるトリプレットは:
# 1: (AWS, 提供する, クラウドサービス)
# 2: (EC2, 部分である, AWSサービス)
# それ以下のトリプレットは抽出されない
```

上記を踏まえると、各チャンクにおいて限られたトリプレットしか取得できていなかったのだと想像ができました。

その数字を増やしてあげれば、各チャンク内で取得できるトリプレットの数が増えます。
するとナレッジグラフ内のノードやエッジの数も増えます。
結果的によりきちんとドキュメントをナレッジグラフに反映することに繋がり、出力精度を向上させることができます。

(と、ここまで書いていて思ったのですが、`Settings.chunk_size`の数字も増やしたのはミスだったかもしれませんね…各チャンク内で取得するトリプレットの数を増やしても、各チャンクの最大値を増やしたらいたちごっこになるので…)
:::

そして再度同じ質問をすると…？
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3616210/0893d9ff-4af7-5512-1705-0b16136ec841.png)

**ｷﾀ━━━━(ﾟ∀ﾟ)━━━━!!**
**これこれ！！これですよこれ！！僕が求めていたものは！！！**

もう1つの方はニエル賞が消えましたが、4/5レース分の結果が出力されているので、さっきのよりは全然良いです！
(多分`Settings.chunk_size`も増やしたためだと思いますが)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3616210/8e1feaf2-ff8b-b496-c884-ad7c5b066585.png)

ということで、**チャンク数やトリプレットはチューニングするのが重要そう**です！

:::note warn
下記のように接続エラーが起こることもあります。
その時は再度実行すれば上手くいくことがあります。原因不明です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3616210/88e72704-bec8-0ba3-578a-8b611975681d.png)
:::

# ナレッジベースから作れるGraphRAGと比較する
以前作成したナレッジベースのGraphRAGでは、上記の質問に対してまともに答えられませんでした。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3616210/0ec1214e-86ba-60f1-7a76-0d00a16fc526.png)

単純に**私の検証における精度のみを比較すると、LlamaIndexを用いてGraphRAGを作成した方が良さそう**です。

ただ、**ナレッジベースの方が圧倒的に構築は楽ですし、手間もかかりません**。
また、出力のソースがどこだったのかわかるので、かなりデバッグやチューニングがしやすそうです。

何よりre:Inventでアップデートが大量に来ていることからも、今後もより一層機能が充実していきそうです。

ということでまとめるとこんな感じになりそうです(ふくち調べ)。
| 評価項目 | ナレッジベースのGraphRAG | LlamaIndexのGraphRAG |
|:-|:-|:-|
| 出力精度 | - (今後に期待) | ◎ (チューニング必要) |
| 構築の手軽さ | ○ (直感的) | △ (慣れるまでが大変) |
| アプリとの連携 | ◎ <br>(APIコール、AWSサービスと連携) | ○ <br>(できる、今度試す) |
| LLMOps | ○<br>(トレース・チャンク確認可能、<br>LLM-as-a-Judgeも可能(プレビュー)) | △ <br>(トレース・チャンクは<br>確認不可能、Ragasで<br>出力評価は可能そう) |

※間違っている可能性大いにあります！
※「そうなんだな〜自分も試してみるか〜」くらいな感じでお願いしますmm

# まとめ
**ナレッジベース上で作れるGraphRAGの精度が、LlamaIndexで作成するGraphRAGくらい良いものになりますように！！**

今後の伸びしろは抜群なので、GAに期待！

# おまけ
最後に、出走が叶わなくなってしまった2024年有馬記念、ドウデュースの引退レース結果を妄想してもらいました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3616210/15ddd1c4-92ec-fc5c-ecc2-830fedc80ab6.png)

**「最終コーナーを回ったところで武豊騎手が外に持ち出し、一気に加速」**
この光景が目に浮かぶようです。

最後にはBedrockも言葉を紡げなくなるほど感動して泣いてしまいましたね()。

たくさんの夢をありがとう、ドウデュース。