今更LangchainとNeo4jでグラフRAGを試してみる

# 背景
話には聞くだけでどういうのか実感がなかったものを、手のひらサイズで試してみる

## ちなみにグラフRAGって？
>ナレッジグラフを活用した生成AI（Generative AI）のアプローチです。通常のRAG（Retrieval-Augmented Generation）では、情報検索と生成を組み合わせて応答を生成しますが、グラフRAGではナレッジグラフをデータ構造として用い、検索プロセスを強化したり、生成内容の整合性を高めたりします。

## ナレッジグラフって？
>ナレッジグラフ（Knowledge Graph）は、情報やデータをエンティティ（物、人、場所、概念など）とその間の関係として表現したデータ構造の一種です。これにより、データが単なるテキストや数値の集まりではなく、意味を持ったつながりとして利用されます。

## ふつうのRAGと比べてどのような特徴があるの？
>通常のRAGでは非構造データを簡単に扱える分、以下のようなデメリットがあります。
・検索された文書間の関係性を考慮しない。
・情報が複数のドキュメントに分散している場合、その統合や推論が難しい。
・結果として、回答が部分的または曖昧になる可能性がある。

>比較してナレッジグラフRAGでは、以下のメリットがあります。
・文脈を考慮した検索が可能。たとえば、「この問題に関する製品の技術ドキュメントは？」という質問に対し、グラフ上のエンティティ間の関係をたどることで正確な回答を提供。
・情報が分散している場合でも、関連付けされたデータを統合して回答を生成。
・推論機能により、明示されていない情報も導き出せる。

なるほど、すごそう。おそらく実用するとなるとデータの関連付けの更新のために時間を費やすことになるのであろうが、RAGの課題としている部分に対応できている気がしておもしろそう。

# 実装

## Neo4Jを導入

dockerでneo4jを導入する
構築やハマった場所はこちらで

https://qiita.com/shin62833/items/0bc275e17095fe1f02b0



## Neo4Jにデータを登録する
```
// 映画と人物のノードを作成
CREATE (Inception:Movie {title:"Inception", released:2010, tagline:"Your mind is the scene of the crime"})
CREATE (Leonardo:Person {name:"Leonardo DiCaprio", born:1974})
CREATE (Joseph:Person {name:"Joseph Gordon-Levitt", born:1981})
CREATE (Ellen:Person {name:"Ellen Page", born:1987})
CREATE (Tom:Person {name:"Tom Hardy", born:1977})
CREATE (Ken:Person {name:"Ken Watanabe", born:1959})
CREATE (Dileep:Person {name:"Dileep Rao", born:1973})
CREATE (Cillian:Person {name:"Cillian Murphy", born:1976})
CREATE (Marion:Person {name:"Marion Cotillard", born:1975})
CREATE (Pete:Person {name:"Pete Postlethwaite", born:1946})
CREATE (Michael:Person {name:"Michael Caine", born:1933})
CREATE (ChristopherN:Person {name:"Christopher Nolan", born:1970})

// 映画「Inception」への出演関係を作成
CREATE (Leonardo)-[:ACTED_IN {roles:["Dom Cobb"]}]->(Inception)
CREATE (Joseph)-[:ACTED_IN {roles:["Arthur"]}]->(Inception)
CREATE (Ellen)-[:ACTED_IN {roles:["Ariadne"]}]->(Inception)
CREATE (Tom)-[:ACTED_IN {roles:["Eames"]}]->(Inception)
CREATE (Ken)-[:ACTED_IN {roles:["Saito"]}]->(Inception)
CREATE (Dileep)-[:ACTED_IN {roles:["Yusuf"]}]->(Inception)
CREATE (Cillian)-[:ACTED_IN {roles:["Robert Fischer"]}]->(Inception)
CREATE (Marion)-[:ACTED_IN {roles:["Mal"]}]->(Inception)
CREATE (Pete)-[:ACTED_IN {roles:["Maurice Fischer"]}]->(Inception)
CREATE (Michael)-[:ACTED_IN {roles:["Miles"]}]->(Inception)
CREATE (ChristopherN)-[:DIRECTED]->(Inception)

// 他の関係とノードを追加
CREATE (Interstellar:Movie {title:"Interstellar", released:2014, tagline:"Mankind was born on Earth. It was never meant to die here."})
CREATE (Anne:Person {name:"Anne Hathaway", born:1982})
CREATE (Jessica:Person {name:"Jessica Chastain", born:1977})
CREATE (Matt:Person {name:"Matt Damon", born:1970})

CREATE (Anne)-[:ACTED_IN {roles:["Amelia Brand"]}]->(Interstellar)
CREATE (Matthew)-[:ACTED_IN {roles:["Cooper"]}]->(Interstellar)
CREATE (Jessica)-[:ACTED_IN {roles:["Murph"]}]->(Interstellar)
CREATE (Matt)-[:ACTED_IN {roles:["Dr. Mann"]}]->(Interstellar)
CREATE (Michael)-[:ACTED_IN {roles:["Professor Brand"]}]->(Interstellar)
CREATE (ChristopherN)-[:DIRECTED]->(Interstellar)

// より多くの複雑な関係とノードを定義
CREATE (Dunkirk:Movie {title:"Dunkirk", released:2017, tagline:"Survival is Victory"})
CREATE (Harry:Person {name:"Harry Styles", born:1994})
CREATE (Fionn:Person {name:"Fionn Whitehead", born:1997})
CREATE (TomG:Person {name:"Tom Glynn-Carney", born:1995})

CREATE (Harry)-[:ACTED_IN {roles:["Alex"]}]->(Dunkirk)
CREATE (Fionn)-[:ACTED_IN {roles:["Tommy"]}]->(Dunkirk)
CREATE (TomG)-[:ACTED_IN {roles:["Peter"]}]->(Dunkirk)
CREATE (Kenneth)-[:ACTED_IN {roles:["Commander Bolton"]}]->(Dunkirk)
CREATE (Cillian)-[:ACTED_IN {roles:["Shivering Soldier"]}]->(Dunkirk)
CREATE (Tom)-[:ACTED_IN {roles:["Farrier"]}]->(Dunkirk)
CREATE (ChristopherN)-[:DIRECTED]->(Dunkirk)

// 関係とノードを拡張することで、よりリッチなデータセットを構築する

```

インサートしたデータを確認
```
MATCH (p:Person)-[r:ACTED_IN]->(m:Movie {title: "Inception"})
RETURN p.name, r.roles, m.title, m.released
```
結果
```
[
  {
    "p.name": "Michael Caine",
    "r.roles": [
      "Miles"
    ],
    "m.title": "Inception",
    "m.released": 2010
  },
  {
    "p.name": "Pete Postlethwaite",
    "r.roles": [
      "Maurice Fischer"
    ],
    "m.title": "Inception",
    "m.released": 2010
  },
  {
    "p.name": "Marion Cotillard",
    "r.roles": [
      "Mal"
    ],
    "m.title": "Inception",
    "m.released": 2010
  },
  {
    "p.name": "Cillian Murphy",
    "r.roles": [
      "Robert Fischer"
    ],
    "m.title": "Inception",
    "m.released": 2010
  },
  {
    "p.name": "Dileep Rao",
    "r.roles": [
      "Yusuf"
    ],
    "m.title": "Inception",
    "m.released": 2010
  },
  {
    "p.name": "Ken Watanabe",
    "r.roles": [
      "Saito"
    ],
    "m.title": "Inception",
    "m.released": 2010
  },
  {
    "p.name": "Tom Hardy",
    "r.roles": [
      "Eames"
    ],
    "m.title": "Inception",
    "m.released": 2010
  },
  {
    "p.name": "Ellen Page",
    "r.roles": [
      "Ariadne"
    ],
    "m.title": "Inception",
    "m.released": 2010
  },
  {
    "p.name": "Joseph Gordon-Levitt",
    "r.roles": [
      "Arthur"
    ],
    "m.title": "Inception",
    "m.released": 2010
  },
  {
    "p.name": "Leonardo DiCaprio",
    "r.roles": [
      "Dom Cobb"
    ],
    "m.title": "Inception",
    "m.released": 2010
  }
]
```

## LangchainとNeo4JでグラフRAGしてみる
以下の記事に記載のコードを参考にする
もうデータは挿入してしまったのでその部分は実装しない

https://blog.langchain.dev/constructing-knowledge-graphs-from-text-using-openai-functions/

Langchainや入出力GUIとしてのStreamlitはこちらで作成したものを流用する

https://qiita.com/shin62833/items/3fc5be20f3045a979fae

Streamlitの画面と一緒に作る
```
import streamlit as st
from langchain_openai import ChatOpenAI
from langchain.graphs import Neo4jGraph
from langchain.chains import GraphCypherQAChain


# Set API keys from session state
openai_api_key = st.session_state.openai_api_key
serper_api_key = st.session_state.serper_api_key

# Streamlit app
st.subheader('Graph RAG')
search_query = st.text_input("Enter Search Query")

url = "bolt://172.18.0.1:7687"
username ="hoge"
password = "fuga"
graph = Neo4jGraph(
    url=url,
    username=username,
    password=password
)

# If the 'Search' button is clicked
if st.button("Search"):
    # Validate inputs
    if not openai_api_key or not serper_api_key:
        st.error("Please provide the missing API keys in Settings.")
    elif not search_query.strip():
        st.error("Please provide the search query.")
    else:
        try:
            with st.spinner('Please wait...'):
                # Initialize the OpenAI module, load the Google Serper API tool, and run the search query using an agent
                llm = ChatOpenAI(temperature=0, openai_api_key=openai_api_key, verbose=True)

                graph.refresh_schema()

                cypher_chain = GraphCypherQAChain.from_llm(
                    graph=graph,
                    cypher_llm=llm,
                    qa_llm=llm,
                    validate_cypher=True, # Validate relationship directions
                    verbose=True
                )

                result = cypher_chain.invoke({"query": search_query})

                st.success(result)
        except Exception as e:
            st.exception(f"An error occurred: {e}")
```

動かしてみる
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1911348/c5c75e90-955e-0b77-0356-cbe8d4ae8cfc.png)

ログ
```
> Entering new GraphCypherQAChain chain...
Generated Cypher:
MATCH (p:Person)-[:ACTED_IN]->(m:Movie)
WHERE m.title = 'Inception'
RETURN p.name
Full Context:
[{'p.name': 'Michael Caine'}, {'p.name': 'Pete Postlethwaite'}, {'p.name': 'Marion Cotillard'}, {'p.name': 'Cillian Murphy'}, {'p.name': 'Dileep Rao'}, {'p.name': 'Ken Watanabe'}, {'p.name': 'Tom Hardy'}, {'p.name': 'Ellen Page'}, {'p.name': 'Joseph Gordon-Levitt'}, {'p.name': 'Leonardo DiCaprio'}]

> Finished chain.
```

ちなみに
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1911348/14256ea0-8e33-a791-dcaa-fb0faee08006.png)


# 今後何したい？

- 通常のRAGとの性能差がわかるような実験をしたい
- 巷では通常RAGとグラフRAGとの組み合わせによって精度向上を期待している様子なのでそれもためしてみたい