Neo4j LLM Knowledge Graph Builderを試す
ここで知った
ざっと要約
Neo4j LLM ナレッジグラフビルダーは、非構造化テキストをナレッジグラフに変換するオンラインアプリケーションです。OpenAI, Gemini, Llama3, Diffbot, Claude, QwenなどのLLMモデルを用いて、PDF、ドキュメント、画像、ウェブページ、YouTubeビデオのトランスクリプトを処理し、レキシカルグラフとエンティティグラフをNeo4jデータベースに保存します。
ユーザーは抽出スキーマを設定し、クリーンアップ処理を適用できます。GraphRAG、Vector、Text2CypherなどのRAGアプローチを用いてデータに対して質問し、回答生成過程を可視化できます。
アプリケーションはReactで構築されたフロントエンドと、Python FastAPIベースのGoogle Cloud Run上で動作するバックエンドを持ち、Docker Composeによるローカルデプロイも可能です。Neo4jが貢献したllm-graph-transformerモジュールをLangChainや他のラングチェーン統合に用います。
主な特徴は以下の通りです。
- サポートされるファイル形式: PDF、ドキュメント、URL、S3/GCSバケット内のデータ
- 利用可能なLLMモデル: OpenAI, Gemini, Llama3, Diffbot, Claude, Qwen
- データ格納: Neo4jデータベース
- デプロイ方法: オンライン、ローカル (Docker Compose)
詳細な機能、使用方法、チュートリアルは公式ドキュメントとリポジトリで確認できます。
オンラインで試せる手順は上記ドキュメントにある。また、LLM Knowledge Graph BuilderはDockerでローカルでも動かせるみたい。
手順に従って、オンラインでやってみる。
まず、Neo4j AuraDBでインスタンス作成。無料で1インスタンス作成できる。
Freeを選択
インスタンスへの接続情報をファイルでダウンロード
インスタンスが作成される。そこそこ時間がかかるので待つ。
以下のように表示されればOK。
LLM-Knowledge Graph Builderを開く。
インスタンスへの接続を入力する画面が表示される。先ほどダウンロードした接続情報ファイルをドラッグ&ドロップすると自動で入力されるので、"Connect"をクリック。
接続できたら、左のメニューからコンテンツデータを読み込ませる。
データの読み込み方法は4通りで、上から順に以下となっている。
- ファイルアップロード
- Google Cloud Storage
- Amazon S3
- URLリンク
今回は手っ取り早くURL指定で行う。以下のWikipediaのページを使う。
Wikipediaはあらかじめ想定されているみたいなので、URLを入力して"Submit"する。なお、Wikipedia以外のURLは右のアイコンをクリックしてURLを入力すれば良い。
読み込みが始まり・・・
以下のようになれば完了、閉じて元の画面に戻る。
ファイルが読み込まれた。ただし、まだこの状態ではグラフは生成されていない。
細かいところは一旦置いておいてグラフを生成してみようと思うが、LLM Graph BuilderはLangChainで書かれているらしく、読み込んだファイルがどのように処理されるかは"How It Works"に書かれている。
- アップロードされたソースは、グラフ内のドキュメントノードとして保存される
- 各ドキュメント(タイプ)は、LangChainローダーで読み込まれる。
- コンテンツはチャンクに分割される
- チャンクはグラフに保存され、ドキュメントと相互に接続され、高度なRAGパターンが作成される。
- 類似度の高いチャンクは、SIMILAR関係で接続され、kNNグラフを形成する
- 埋め込みが計算され、チャンクおよびベクトルインデックスに格納される
- llm-graph-transformerまたはdiffbot-graph-transformerを使用して、テキストからエンティティとリレーションシップが抽出される
- エンティティとリレーションシップはグラフに格納され、元のチャンクに接続される
エンティティとリレーションシップの抽出はLLMを使って行われることがわかる。このLLMのモデルは左下で選択できる。オンラインではOpenAI gpt-4o/Gemini 1.0 Pro/Diffbotの3種類の様子。モデルのAPIキーは自分で設定する場所がないので、Neo4j側が用意したものを使っていることになっていると思われる。
今回は、OpenAI gpt-4oを使って、グラフを作成してみる。"Generate Graph"をクリック。
ステータスがProcessingに変わるので、完了するまで待つ。
完了した。
早速チャットしてみよう。右の吹き出しアイコンをクリック。
チャット画面が表示されるので適当にチャットしてみるとこんな感じ。
んー、かなりあっさりしているな。。。。
生成された回答の根拠となったコンテキストは"Details"から確認できる様子。
3つのタブがあるが、Sources Usedは読み込んだドキュメントの情報が表示されるだけ。
Top Entities userは、グラフ検索で上位となったエンティティがリストアップされている。下のボタンをクリックしてみる。
回答生成に使用されたエンティティやリレーションシップが表示される様子。
Chunksタブは、おそらくベクトル検索ないし全文検索で取得したチャンクが表示されていると思われる。
なお、右上でチャット時のモードを切り替えれる。デフォルトでは、グラフ検索+ベクトル検索+全文検索?のハイブリッドになっていて、ベクトル検索だけにすることもできるみたい。
元の画面に戻る。上でDetailsからグラフを表示することができたけども、より詳細にグラフを操作する場合は、"Explorer Graph with Bloom"をクリック。
Neo4jのグラフ操作を行うインタフェースが表示される(Bloomというのね、知らなかった)。チャットのDetailsだと操作が限られているが、こちらだとノードやリレーションシップを詳細に確認したり、Cypherクエリを実行したりできる。
とりあえずここまででまず感じたこと、あくまでも個人の意見。
- レスポンスが遅い。回答に10秒ぐらいかかっているような印象がある。
- 正直回答があっさりし過ぎているように思えるし、検索されたチャンクも本来取得したいものではなさそう
- WikipediaからのURLでデータ読み込みできるのは良いのだけども、どのようなテキスト抽出が行われているか?どのようにチャンク分割されているか?がわからないので、精度にどう影響しているかがわからない。
- チャット画面からのグラフ操作でできることは限られていて、回答の根拠の確認として参考にするのが難しい。
- あと、チャット画面からいちいち切り替えるのが面倒(切り替えるたびにレンダリングに時間がかかるし)。同時に表示しておいてほしい。
遅いということについては、オンラインだとリソース限られているだろうし、ローカルで動かせば変わるのかもしれない。
検索精度ってところについては期待したほどではないってのが正直な印象だけども、英語が一番精度が高いというふうに書いてあるし、今回の例では固有名詞も多く出てくるので、LLMがいかに文章構造をうまくグラフ化できるか?というところに難しい面もあるとは思っている。また、本来であればスキーマを自分で設定したほうが良いはずで、このあたりも何もせずにやっているので、まあ致し方ないかなというところ。
少し作成されたグラフを見てみる。
まずドキュメントには以下とあった。
抽出は、ドキュメントとチャンク(エンベッディング付き)のレキシカルグラフと、ノードとその関係を持つエンティティグラフに変換し、これらは両方ともNeo4jデータベースに保存されます。 抽出スキーマを設定し、抽出後にクリーンアップ処理を適用することができます。
つまり、グラフは大きく分けて2つのグラフが混在していることになる。
- ドキュメントとチャンクの関係性を表したグラフ
- 元のドキュメント(
Document
)とそのチャンク(Chunk
)をそれぞれノードとする - それらを紐付けをリレーションシップ(
PART_OF
とかFIRST_CHUNK
とかNEXT_CHUNK
とか)とする - 各チャンクのテキストはEmbedding化されプロパティとして付与される。
- ドキュメントの構造を表すようなグラフになる
- 元のドキュメント(
- ドキュメントをLLMに解析させて、ドキュメント内のエンティティ間の関係性を表したグラフ
- ドキュメント内のエンティティ(例「オグリキャップ」「武豊」「JRA]等)からノードを作成
- ドキュメント内のエンティティ間のリレーションシップを作成(例「オグリキャップ」→[ジョッキー]→「武豊」等)
- 各エンティティのテキストもEmbedding化されプロパティとして付与される。
- ドキュメントの内容を表すようなグラフになる
- さらにこの2つのグラフが関連しあって1つの大きなグラフとなる
- 例えば、前者のチャンクノードが、後者のエンティティノードを「含む」リレーションシップ(
HAS_ENTITY
)を持つ、等
- 例えば、前者のチャンクノードが、後者のエンティティノードを「含む」リレーションシップ(
ただし、これはあくまでも今回の例であって、実際には作成するたびにグラフは変わるのではないかと思う。前者はチャンク分割のロジックが変わらなければそれほど構造に変化がないと思うが、後者についてはLLMがどういったエンティティやリレーションを抽出するか?によって変わってくるのではないかと思うので。
前者をCypherで抽出してみる。
MATCH (n:Document {fileName:"オグリキャップ"})-[r]-(m)
RETURN n,r,m
中心のノードがドキュメントそのもので周辺のノードは全部そのチャンクになっている
後者も同様に。
MATCH (n:Person {id:"Yutaka Take"})-[r]-(m)
RETURN n,r,m
こちらは上の例で書いたようなエンティティ間のリレーションシップだけでなく、チャンクとのリレーションシップも見える。
改めて全体像も。
MATCH (n)-[r]-()
RETURN n,r
ドキュメントの構造と内容が関連し合って大きなグラフとなっているのがわかる。
グラフを見ているといくつか気づくことがある(雑なCypher・・・)
MATCH (n1:Animal {id:"オグリキャップ"})-[r1]-(m1)
MATCH (n2:Horse {id:"Oguri Cap"})-[r2]-(m2)
RETURN n1,n2,r1,r2,m1,m2
- エンティティが、異なる表記(日本語と英語)、または別のノードラベルで重複している
- 例:
Animal
ノードの「オグリキャップ」とHorse
ノードの「オグリキャップ」 - 例:
Person
ノードで、「英語・姓だけ」「日本語・姓だけ」「「日本語・姓名」
- 例:
LLMが解析した結果から必ずしも期待した通りのノードやリレーションシップが生成されるわけではないため、これを修正することができるらしい。
"Graph Enhancement"をクリック。
Graph Enhancementには4つのタブがある。
- Entity Extraction Settings
- Disconnected Nodes
- De-Duplication Of Nodes
- Post Processing Jobs
ちょっと順番は変わるけども、Diconnected Nodesから見てみる。
Diconnected Nodesでは、他のエンティティノードとリレーションが張られていないエンティティノードを削除することができる。ただ上記の例だと実際にはチャンクノードとはリレーションがあるように思える。検索的にはエンティティから検索すれば辿れるのではないか?という気もするのだが・・・
実際のグラフはこうなっている。
MATCH (n:Animal {id:"トウショウボーイ"})-[r]-(m)
RETURN n,r,m
Diconnected Nodesの出力にあったように、4つのチャンクから「トウショウボーイ」エンティティに対してリレーションが張られているように見える。
実際に検索してみる
検索に引っかからない。Detailsを見ても何も情報がない。
エンティティを使った検索のロジックがどうなっているか?はわかっていないが、少なくとも今回の例ではこのような「宙ぶらりん」のエンティティは存在しても意味がなさそうだということはわかったので、削除しても問題はなさそう。
次に、De-Duplication Of Nodes。
これは冒頭にも記載したような「表記揺れ」みたいなものを抽出して、1つにマージするということになる。実際のグラフはこうなっている。
MATCH (n:Animal)-[r]->(m)
WHERE n.id = "Oguri Cap" OR n.id = "Oguri_Cap"
RETURN n, r, m
別のエンティティノードになっていて、それぞれのリレーションの範囲は分かれているのがわかる。当然検索時には辿れない可能性が高いと思われる。
ではこれをマージする。
再度グラフを見てみると、1つのエンティティノードに集約されたのがわかる。
ただし、さすがに日本語と英語の違いまでは検出してくれない様なので、ここは手動でCypherクエリを書いてマージする必要がありそう。
MATCH (n:Animal)-[r]->(m)
WHERE n.id = "Oguri Cap" OR n.id = "オグリキャップ"
RETURN n, r, m
次に、Post Processing Jobs。
ここは検索精度向上に関する機能の有効・無効を設定できる。基本的には全部有効で良さそうに思える。
- Materialize Text Chunk Similarities
- グラフ内のチャンク間の関連性を強化する
- KNN)アルゴリズムを用いて、類似度スコアが0.8以上であるチャンクを自動的で結びつける
- Enable Hybrid Search And Fulltext Search In Bloom
- グラフ内の検索能力を最適化する
- データベースラベルに全文インデックスを再構築し、キーワード検索の速度と効率を大幅に向上させる
- Materialize Entity Similarities
- エンティティ(人、場所、概念など)同士の類似性を数値化した表現(エンティティベクトル)を生成する
- 類似エンティティのクラスタリング、重複検出、類似性に基づく検索など、より高度なエンティティ分析が可能になる
最後に、Entity Extraction Settings。
ここでは、テキストの内容から、どうやってエンティティを抽出するか?のスキーマを設定することができる。ノードラベルやリレーションシップタイプをスキーマとして定義することで、LLMがエンティティを抽出する際にこれに従って抽出をおこなうことになる。
スキーマの指定方法はいろいろあるみたい
- 事前に定義されたスキーマを使う
- 自分でカスタムにスキーマを定義する
- すでに作成されたグラフのスキーマを使う
- テキストからスキーマを作成させる
事前定義されたスキーマはこういうのもが用意されている。
で「反映」みたいなボタンがない。。。
色々調べてみたけども、どうやらスキーマはグラフ作成前に指定しておく必要がある様子。まあ当然といえば当然だと思うのだが、"Graph Enhancements"の設定は、グラフ作成前に設定しておくべきものと、グラフ作成後に設定できるもの、が1つのメニューの中に混在しているので、ちょっとここはわかりにくいなと感じた。
で、他の方法だけども、こんな感じでちまちま入力したり
すでに作成されたグラフのスキーマを呼び出してカスタマイズしたり(ただしこの場合、すでに読み取り済みのファイルを削除してからグラフを作成し直す必要があると思う)
テキストを貼り付けて事前にスキーマを作成しておいたり(自分が試したときは、ドキュメント・チャンクのノードが定義されなかったけど、どうなるんだろう?あと自分で定義したスキーマを貼り付けることもできるみたい)
というような形で、スキーマを定義することができる様子。
精度が高いグラフを作成するには、読み込ませるデータに合わせたスキーマを事前に定義しておいたほうがいいだろうし、作成されたグラフの調整も必要にはなると思うが、ここがなかなかしんどいところ。。。
ただGUIでポチポチできるので、このあたりの試行錯誤を繰り返すのが、楽になるかもしれない。
まとめ
ちらほら使いにくいところはあるものの、シンプルにナレッジグラフを使ったRAGチャットを試せるのは良いし、オンラインのリソースだけ・かつ無料で、っていうのも、敷居が低くて良いと思う。ローカルで自分で立てるオプションがあるのもの良い。
ただ、自分もナレッジグラフを使ったRAGには前から興味があって、
- LlamaIndexのいろいろなナレッジグラフインデックスモジュールを使ったRAGを試してみたり
- GraphRAG試してみたり
- 最近、Neo4jの無料オンラインコースをいくつか受講してみた
してみたり、といろいろ試してきた経験を踏まえると(ぜんぜん少ないとは思うが)、精度高いグラフを構築するってのはなかなかハードルが高いなぁと感じている。
- いかに精度高いグラフが作成できるか?が重要
- 立ちはだかる日本語の壁
- LLMがどれだけ精度高くエンティティを抽出してグラフを作ってくれるかに依存
- できたものが自分のユースケースに沿っているか、そうでなければ再トライ
- いっそ真面目にスキーマ設計するほうが速いのでは?と思うが、それはそれでかなり大変そう
- 仮に実運用乗せたあとの運用
- プロパティグラフの理解や、Cypherの習熟も、ある程度は必要
- メンテやチューニングも大変なのでは・・・
特に業務でやる場合だと、求められる精度とかけれる稼働のバランスは悩ましい問題になると思うので、それに見合ったユースケースじゃないとナレッジグラフはしんどいかなーというのが個人的に感じているところ。少なくとも「なんとなく適当でもいい感じに・よしなに」やってくれるようなものではないかなぁ(それはどれも同じかもしれないけど)。
とはいえ、上にも書いた通り、シンプルにGUIでポチポチしながら色々試してみたり、ってのがやりやすいので、色々触ってみればいいと思う。
あと、無料オンラインコースやりながら思ったのだけど、Neo4jはドキュメントも豊富だし、ほんとにいろいろなリソースを用意してくれている。そういうものを積極的に活用すれば理解を深めていけると思う。
ナレッジグラフ、楽しい。