LangChainを使ってグラフデータベースにRAGする

# はじめに

RAGの情報源として、グラフデータベースを使用する例を紹介します。

### 環境
```bash
openai==1.6.1
langchain==0.0.354
neo4j==5.16.0
```
## グラフデータ
### Twitchネットワークグラフの概要
 今回はTwitchネットワークグラフを使用します。以下の図のように、配信者(Stream)を中心に、どのゲーム(Game)をプレイしたか、どの言語(Language)を使用したか、といった情報が格納されています。データは2021/05/7から2021/05/10の間に収集されています。[^b]

[^b]:https://towardsdatascience.com/twitchverse-a-network-analysis-of-twitch-universe-using-neo4j-graph-data-science-d7218b4453ff


![スクリーンショット 2024-01-08 1.03.48.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/75296/008e09ed-45b3-8070-3dd7-8141aa5075d2.png)

### グラフデータの構築

　まずneo4jのsandboxページにアクセスし、Launch the Free Sandboxをクリックします。アカウントの登録、規約同意、どこで働いてるかなど聞かれますので回答してください。
![スクリーンショット 2024-01-08 1.00.50.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/75296/7b59f84a-7812-911c-7ea4-476266bd7a21.png)

　次に、データセットを選択します。今回はTwitchデータを選択して、Createをクリックしてください。
![スクリーンショット 2024-01-08 1.14.05.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/75296/b3e7d36a-8f7a-c9cc-1628-1e1a987b4d4b.png)

　データセットが作成されます。(10数秒程度で完了しました。)
右端の▼→Connection detailsの順でクリックし、データセットのPasswordとBolt URLを記録しておきます。

![スクリーンショット 2024-01-08 1.29.48.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/75296/8118b271-10f1-7b4e-8bd0-06847ac06aad.png)

　データベースの構築は以上になります。Openをクリックした先に、Twitchデータを使用した、初心者ガイドがあるためグラフデータベースに馴染みがない場合は一通り使ってみることをオススメします。

## RAGする

### コード
　LangChainを使えば、10行程度で実装できます。GraphCypherQAChainでは、LLMを２回使用します。[^a]

[^a]:https://github.com/langchain-ai/langchainjs/blob/main/langchain/src/chains/graph_qa/prompts.ts

1. 質問文とスキーマをプロンプトに入れて、Cypherクエリを生成する。
1. 質問文とクエリ結果をプロンプトに入れて、最終回答を生成する。

```python3
from langchain_openai import ChatOpenAI
from langchain.chains import GraphCypherQAChain
from langchain_community.graphs import Neo4jGraph

chatmodel = ChatOpenAI(temperature=0, model_name='gpt-3.5-turbo')

graph = Neo4jGraph(
    url="bolt://xxxxxxxxxx:xxxxx",
    username="neo4j",
    password="xxxx-xxxx-xxxxx"
)
chain = GraphCypherQAChain.from_llm(
    chatmodel, graph=graph, verbose=True
)
```
### 質問してみる

#### ケース１:配信者の人数を聞く
 まず、配信者の人数を聞いてみます。
途中出力の`MATCH (s:Stream) RETURN COUNT(s) AS streamCount`が、質問文から生成されたCypherクエリで、`[{'streamCount': 4540}]`がクエリ結果です。
質問の意図通りの適切なクエリを作成し、回答も適切にまとめています。
```python3 
# gpt-3.5-turboを使用
chain.run('配信者の人数を教えてください。')

# > Entering new GraphCypherQAChain chain...
# Generated Cypher:
# MATCH (s:Stream)
# RETURN COUNT(s) AS streamCount
# Full Context:
# [{'streamCount': 4540}]
#
# > Finished chain.
# '配信者の人数は4540人です。'
```

#### ケース２:人気のゲームタイトルを聞く
 次に、人気のゲームタイトルを聞いてみます。

 まず配信者とゲームのペアをすべて検索しています。次に、その中の先頭10件[^c]のゲームタイトルをプロンプトに入れ最終回答を作成しています。今回はたまたま先頭10件がRust(というゲーム)だったため、最終回答はRustが人気と答えています。実際に今回のデータでは、それほど人気ではないため、回答は失敗です

 個人的には、一番多くのの配信者にプレイされているゲームを教えてほしいところでしたが、聞き方が悪かったのかもしれません。

[^c]:GraphCypherQAChainには参照するデータの数を決めるパラメータ(top_k)を持っており、デフォルトではtop_k=10です。質問文(Cypherクエリを生成するときの要件)には10件までしか参照できないことを伝えていなかったため、とりあえずすべてのゲームタイトルを答えたのかもしれません。https://api.python.langchain.com/en/latest/chains/langchain.chains.graph_qa.cypher.GraphCypherQAChain.html#langchain.chains.graph_qa.cypher.GraphCypherQAChain.top_k
``` python3
# gpt-3.5-turboを使用
chain.run('人気のゲームのタイトルを教えてください。')

# > Entering new GraphCypherQAChain chain...
# Generated Cypher:
# MATCH (s:Stream)-[:PLAYS]->(g:Game)
# RETURN g.name
# Full Context:
# [{'g.name': 'Rust'}, {'g.name': 'Rust'}, {'g.name': 'Rust'}, {'g.name': 'Rust'}, {'g.name': 'Rust'}, {'g.name': 'Rust'}, {'g.name': 'Rust'}, {'g.name': 'Rust'}, {'g.name': 'Rust'}, {'g.name': 'Rust'}]

# > Finished chain.
# '人気のゲームのタイトルは「Rust」です。'
```

 ちなみに、モデルをgpt4にすると。。。
おぉ!質問文に明記されていない意図を汲み取って回答してくれています。[^d]

[^d]:langhchainのテンプレートを見ると、クエリ作成部分はgpt-4を使用し、最終回答を作成する部分はgpt-3.5-turboを使うといった、使い分けしている例を見ます。クエリ作成部分の方が難易度が高いということでしょか?https://github.com/langchain-ai/langchain/tree/master/templates/neo4j-cypher


``` python3
# gpt-4を使用
chain.run('人気のゲームのタイトルを教えてください。')

# > Entering new GraphCypherQAChain chain...
# Generated Cypher:
# MATCH (s:Stream)-[:PLAYS]->(g:Game)
# RETURN g.name AS Game_Title, COUNT(*) AS Popularity
# ORDER BY Popularity DESC
# LIMIT 10
# Full Context:
# [{'Game_Title': 'Just Chatting', 'Popularity': 868}, {'Game_Title': 'Resident Evil Village', 'Popularity': 442}, {'Game_Title': 'Grand Theft Auto V', 'Popularity': 380}, {'Game_Title': 'League of Legends', 'Popularity': 279}, {'Game_Title': 'Fortnite', 'Popularity': 217}, {'Game_Title': 'VALORANT', 'Popularity': 184}, {'Game_Title': 'Call of Duty: Warzone', 'Popularity': 173}, {'Game_Title': 'Apex Legends', 'Popularity': 172}, {'Game_Title': 'Counter-Strike: Global Offensive', 'Popularity': 147}, {'Game_Title': 'Minecraft', 'Popularity': 142}]
#
# > Finished chain.
# "人気のゲームのタイトルは以下の通りです：'Just Chatting'、'Resident Evil Village'、'Grand Theft Auto V'、'League of Legends'、'Fortnite'、'VALORANT'、'Call of Duty: Warzone'、'Apex Legends'、'Counter-Strike: Global Offensive'、そして 'Minecraft'です。"
```

#### ケース3:マインクラフトをプレイした人が、好きなゲームを聞く

 もう少しグラフデータベースならではの複雑な質問として「Minecraftをプレイしている配信者に人気の他のゲームはなんですか？」と聞いてみます。
先程と同様に該当データ((マインクラフト)<-(配信者)->(他のゲーム)の組み合わせ)をすべて列挙して、回答してもらおうとしています。参照の上限は10件なのでうまく回答できていません。

```python3
# gpt-3.5-turboを使用
chain.run('Minecraftをプレイしている配信者に人気の他のゲームはなんですか？')

# > Entering new GraphCypherQAChain chain...
# Generated Cypher:
# MATCH (s:Stream)-[:PLAYS]->(g:Game {name: 'Minecraft'})
# MATCH (s)-[:PLAYS]->(g2:Game)
# WHERE g2 <> g
# RETURN DISTINCT g2.name
# Full Context:
# [{'g2.name': 'Just Chatting'}, {'g2.name': 'Apex Legends'}, {'g2.name': 'Middle-earth: Shadow of War'}, {'g2.name': 'Subnautica'}, {'g2.name': 'First Class Trouble'}, {'g2.name': 'Worms W.M.D'}, {'g2.name': 'Grand Theft Auto V'}, {'g2.name': 'RimWorld'}, {'g2.name': 'Resident Evil Village'}, {'g2.name': 'VALORANT'}]
# 
# > Finished chain.
# 'Minecraftをプレイしている配信者に人気の他のゲームは、Just Chatting、Apex Legends、Middle-earth: Shadow of War、Subnautica、First Class Trouble、Worms W.M.D、Grand Theft Auto V、RimWorld、Resident Evil Village、VALORANTなどがあります。'
```

 先程と同じくGPT-4でも質問してみます。
うまく回答できていますね。
```python3
# gpt-4を使用
chain.run('Minecraftをプレイしている配信者に、人気の他のゲームはなんですか？')

# > Entering new GraphCypherQAChain chain...
# Generated Cypher:
# MATCH (s:Stream)-[:PLAYS]->(g:Game {name: 'Minecraft'}), (s)-[:PLAYS]->(otherGame:Game)
# RETURN otherGame.name AS Other_Game, COUNT(*) AS Popularity
# ORDER BY Popularity DESC
# Full Context:
# [{'Other_Game': 'Just Chatting', 'Popularity': 28}, {'Other_Game': 'First Class Trouble', 'Popularity': 5}, {'Other_Game': 'Resident Evil Village', 'Popularity': 5}, {'Other_Game': 'VALORANT', 'Popularity': 5}, {'Other_Game': 'Grand Theft Auto V', 'Popularity': 4}, {'Other_Game': 'LOST ARK', 'Popularity': 3}, {'Other_Game': 'Apex Legends', 'Popularity': 2}, {'Other_Game': 'Subnautica', 'Popularity': 2}, {'Other_Game': 'Worms W.M.D', 'Popularity': 2}, {'Other_Game': 'Music', 'Popularity': 2}]
# 
# > Finished chain.
# "配信者がMinecraft以外でよくプレイしているゲームは、'Just Chatting'が最も人気で、その人気度は28です。次に人気なのは'First Class Trouble'、'Resident Evil Village'、'VALORANT'で、これらのゲームの人気度は5です。'Grand Theft Auto V'の人気度は4、'LOST ARK'の人気度は3です。また、'Apex Legends'、'Subnautica'、'Worms W.M.D'、そして'Music'も人気があり、それぞれの人気度は2です。"
```

 ちなみに下の図が「(マインクラフト)<-(配信者)->(他のゲーム)」を描画したグラフになります。水色ノードがゲーム、橙色ノードが配信者です。この図を見るとJust Chatting(雑談),VALORANTが中心にあり矢印も多いため、人気のようです。

![スクリーンショット 2024-01-08 3.25.31.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/75296/848c2756-c626-c1d8-6626-6c1863172448.png)

#### ケース４:簡単な質問だが回答できない例
 最後に簡単な質問ですがGPT-4でも回答できない質問例として「日本語を使用している配信者を10名教えてください。」を紹介します。

 以下の例では日本語を使用する配信者は０人となっています。
理由は、今回`{name: '日本語'}`の言語ノードを使用した配信者を検索していますが、このデータセットでの言語ノードの名称は英字２文字(en,ko..)なので、`{name: 'ja'}`で検索する必要があります。この情報はグラフのスキーマには乗っていないため、失敗します。
```python3
# 失敗する例, gpt-4を使用
chain.run('日本語を使用している配信者を10名教えてください。')

# > Entering new GraphCypherQAChain chain...
# Generated Cypher:
# MATCH (s:Stream)-[:HAS_LANGUAGE]->(l:Language {name: '日本語'}) RETURN s.name LIMIT 10
# Full Context:
# []
# 
# > Finished chain.
# '申し訳ありませんが、その質問に対する答えはわかりません。'
```

 以下のように、明記するとうまく動きます。
```python3
# 成功する例, gpt-4を使用
chain.run('日本語(ノード名はja)を使用している配信者を10名教えてください。')
```

 このようにスキーマを見ただけではクエリを作れないケースの対応方法としては以下の様な物があると思います。

* プロンプトに書いておく
  * 日本語はja, 英語はenなどの情報をあらかじめプロンプトに記載しておく。
  * 言語など少数の場合は有効かもしれませんが、ゲーム名のように数が多く更新も多い場合には向いていません。
* 類似するノードを検索する。
  * 「日本語」と入力された時、最初に「ja, en, ko, ...」の中からもっとも近い値を選択する機能を挟む。
  * ノードの名前や説明をベクトル化する機能は用意されているため、日本語からjaを選び出すことを試してみましたが、わざわざ検索機能を挟むのはオーバースペックな気がします。
  * ゲーム名のように数が多い場合は、ベクトル検索に限らず何かしら検索機能を挟むのは有効だと思います。。(例えば、マインクラフト,mincraft, マイクラフトといったワードからMinecraftを選びたい時)

 LangChainに事前に用意されているxxxxChainがお手軽に使える事や、LCELの書き方がよくわかんない事などから、カスタマイズをめんどくさがってしまいましたが、安定して動かしたいならカスタマイズは必須ですね。。。

 以上、RAGの情報源として、グラフデータベースを使用する例の紹介でした。そのうち最後に紹介した「類似するノードを検索する。」についてもまとめようと思います。質問コメントあればお気軽に<(_ _)>