RAGアプリケーションの精度をナレッジグラフで高める | by Tomaz Bratanic | Neo4j 開発社ブログ | Mar, 2024 | Medium
Neo4jとLangChainを使用したRAGアプリケーションでの知識グラフからの情報の構築と取得に関する実践的なガイド
グラフ検索強化生成(GraphRAG)は勢いを増し、従来のベクトル検索検索手法に強力な追加となっています。このアプローチは、グラフデータベースの構造化された性質を活用し、データをノードと関係性として整理することで、取得される情報の深さと文脈性を向上させています。
グラフは、さまざまなデータ型にわたる複雑な関係や属性を効果的に捉え、構造化された形で異種の相互接続情報を表現し、保存するのに優れています。一方、ベクトルデータベースは、高次元ベクトルを通じて非構造化データを処理することが得意であり、構造化された情報には苦労することがよくあります。RAGアプリケーションでは、構造化されたグラフデータと非構造化テキストを通じたベクトル検索を組み合わせることで、両者の利点を最大限に活用することができます。このブログ投稿では、それを実証していきます。
知識グラフをどのようにして作成するのか?
知識グラフを構築することは通常、最も難しい段階です。これにはデータの収集と構造化が含まれますが、これにはドメインとグラフモデリングの深い理解が必要です。
このプロセスを簡略化するために、私たちはLLMを使って実験してきました。LLMは言語と文脈の深い理解を持っており、知識グラフの作成プロセスの重要な部分を自動化することができます。これらのモデルはテキストデータを分析し、エンティティを特定し、その関係を理解し、グラフ構造で最も適切に表現する方法を提案することができます。
これらの実験の結果、私たちはLangChainに最初のグラフ構築モジュールのバージョンを追加しました。このブログ投稿でそのデモンストレーションを行います。
コードはGitHubで利用可能です。
Neo4j環境のセットアップ
Neo4jインスタンスをセットアップする必要があります。このブログ投稿の例に従ってください。最も簡単な方法は、Neo4j Auraで無料のインスタンスを開始することです。これは、Neo4jデータベースのクラウドインスタンスを提供しています。また、Neo4j Desktopアプリケーションをダウンロードしてローカルデータベースインスタンスを作成することもできます。
os.environ["OPENAI_API_KEY"] = "sk-"
os.environ["NEO4J_URI"] = "bolt://localhost:7687"
os.environ["NEO4J_USERNAME"] = "neo4j"
os.environ["NEO4J_PASSWORD"] = "password"
graph = Neo4jGraph()
また、このブログ投稿では、OpenAIのキーを提供する必要があります。
データ取り込み
このデモンストレーションでは、Elizabeth I’s のウィキペディアページを使用します。LangChain loadersを使用して、ウィキペディアからドキュメントをシームレスに取得して分割することができます。
# Read the wikipedia article
raw_documents = WikipediaLoader(query="Elizabeth I").load()
# Define chunking strategy
text_splitter = TokenTextSplitter(chunk_size=512, chunk_overlap=24)
documents = text_splitter.split_documents(raw_documents[:3])
提供された文書に基づいてグラフを構築する時が来ました。この目的のために、私たちは知識グラフをグラフデータベースに構築および保存することを大幅に簡略化する LLMGraphTransformer モジュールを実装しました。
llm=ChatOpenAI(temperature=0, model_name="gpt-4-0125-preview")
llm_transformer = LLMGraphTransformer(llm=llm)
# Extract graph data
graph_documents = llm_transformer.convert_to_graph_documents(documents)
# Store to neo4j
graph.add_graph_documents(
graph_documents,
baseEntityLabel=True,
include_source=True
)
知識グラフ生成チェーンが使用するLLMを定義できます。現在、OpenAIとMistralからの関数呼び出しモデルのみをサポートしています。ただし、将来的にはLLMの選択肢を拡大する予定です。この例では、最新のGPT-4を使用しています。生成されたグラフの品質は使用しているモデルに大きく依存することに注意してください。理論的には、常に最も能力のあるものを使用したいと思います。LLMグラフ変換機はグラフドキュメントを返し、これはadd\_graph\_documentsメソッドを使用してNeo4jにインポートできます。baseEntityLabelパラメータは、各ノードに追加の\_\_Entity\_\_ラベルを割り当て、インデックス作成とクエリのパフォーマンスを向上させます。include\_sourceパラメータは、ノードを元のドキュメントにリンクし、データの追跡とコンテキストの理解を容易にします。
Neo4jブラウザで生成されたグラフを確認できます。
この画像は生成されたグラフの一部を表していることに注意してください。
RAGのためのハイブリッド検索
グラフ生成後、RAGアプリケーションのためにベクトルとキーワードのインデックスを組み合わせたハイブリッド検索手法を使用します。
図は、ユーザーが質問を投げかけることから始まる検索プロセスを示しており、その質問はその後、RAGリトリーバーに送られます。このリトリーバーは、キーワードとベクトルの検索を使用して、非構造化テキストデータを検索し、知識グラフから収集した情報と組み合わせます。Neo4jにはキーワードとベクトルのインデックスが両方備わっているため、単一のデータベースシステムでこれらの3つの検索オプションを実装できます。これらのソースから収集されたデータはLLMに供給され、最終的な回答を生成して提供します。
Unstructured Data Retriever
Neo4jVector.from_existing_graph メソッドを使用して、キーワードとベクトルの検索をドキュメントに追加できます。このメソッドは、ハイブリッド検索アプローチのためのキーワードとベクトル検索インデックスを構成し、Document とラベルが付けられたノードを対象とします。さらに、テキスト埋め込みの値を計算します(もしそれらが欠けている場合)。
vector_index = Neo4jVector.from_existing_graph(
OpenAIEmbeddings(),
search_type="hybrid",
node_label="Document",
text_node_properties=["text"],
embedding_node_property="embedding"
)
その後、ベクトルインデックスはsimilarity\_searchメソッドで呼び出すことができます。
グラフリトリーバー
一方で、グラフの検索を設定することはより複雑ですが、より自由度が高いです。この例では、フルテキストインデックスを使用して関連するノードを特定し、それらの直接の隣接ノードを返します。
グラフリトリーバーは、入力内の関連するエンティティを特定することから始めます。簡単のため、LLMに人物、組織、場所を特定するよう指示します。これを実現するために、新たに追加されたwith\_structured\_outputメソッドを使用するために、LCELを使用します。
# Extract entities from text
class Entities(BaseModel):
"""Identifying information about entities."""
names: List[str] = Field(
...,
description="All the person, organization, or business entities that "
"appear in the text",
)
prompt = ChatPromptTemplate.from_messages(
[
(
"system",
"You are extracting organization and person entities from the text.",
),
(
"human",
"Use the given format to extract information from the following "
"input: {question}",
),
]
)
entity_chain = prompt | llm.with_structured_output(Entities)
テストしてみます。
entity_chain.invoke({"question": "Where was Amelia Earhart born?"}).names
# ['Amelia Earhart']
質問でエンティティを検出できるようになったので、それらをナレッジグラフにマッピングするためにフルテキストインデックスを使用しましょう。まず、フルテキストインデックスを定義し、少しのスペルミスを許容するフルテキストクエリを生成する関数が必要ですが、ここでは詳細には立ち入りません。
graph.query(
"CREATE FULLTEXT INDEX entity IF NOT EXISTS FOR (e:__Entity__) ON EACH [e.id]")
def generate_full_text_query(input: str) -> str:
"""
Generate a full-text search query for a given input string.
This function constructs a query string suitable for a full-text search.
It processes the input string by splitting it into words and appending a
similarity threshold (~2 changed characters) to each word, then combines
them using the AND operator. Useful for mapping entities from user questions
to database values, and allows for some misspelings.
"""
full_text_query = ""
words = [el for el in remove_lucene_chars(input).split() if el]
for word in words[:-1]:
full_text_query += f" {word}~2 AND"
full_text_query += f" {words[-1]}~2"
return full_text_query.strip()
すべてをまとめましょう。
# Fulltext index query
def structured_retriever(question: str) -> str:
"""
Collects the neighborhood of entities mentioned
in the question
"""
result = ""
entities = entity_chain.invoke({"question": question})
for entity in entities.names:
response = graph.query(
"""CALL db.index.fulltext.queryNodes('entity', $query, {limit:2})
YIELD node,score
CALL {
MATCH (node)-[r:!MENTIONS]->(neighbor)
RETURN node.id + ' - ' + type(r) + ' -> ' + neighbor.id AS output
UNION
MATCH (node)<-[r:!MENTIONS]-(neighbor)
RETURN neighbor.id + ' - ' + type(r) + ' -> ' + node.id AS output
}
RETURN output LIMIT 50
""",
{"query": generate_full_text_query(entity)},
)
result += "n".join([el['output'] for el in response])
return result
structured_retriever 関数は、ユーザーの質問に含まれるエンティティを検出することから始めます。次に、検出されたエンティティを反復処理し、Cypher テンプレートを使用して関連ノードの近隣を取得します。さあ、試してみましょう!
print(structured_retriever("Who is Elizabeth I?"))
# Elizabeth I - BORN_ON -> 7 September 1533
# Elizabeth I - DIED_ON -> 24 March 1603
# Elizabeth I - TITLE_HELD_FROM -> Queen Of England And Ireland
# Elizabeth I - TITLE_HELD_UNTIL -> 17 November 1558
# Elizabeth I - MEMBER_OF -> House Of Tudor
# Elizabeth I - CHILD_OF -> Henry Viii
# and more...
最終リトリーバー
最初に述べたように、非構造化データとグラフリトリーバーを組み合わせて、LLMに渡す最終的なコンテキストを作成します。
def retriever(question: str):
print(f"Search query: {question}")
structured_data = structured_retriever(question)
unstructured_data = [el.page_content for el in vector_index.similarity_search(question)]
final_data = f"""Structured data:
{structured_data}
Unstructured data:
{"#Document ". join(unstructured_data)}
"""
return final_data
Pythonを扱っているので、f文字列を使用して出力を簡単に連結することができます。
RAGチェーンの定義
RAGの検索コンポーネントを成功裏に実装しました。次に、統合されたハイブリッドリトリーバが提供するコンテキストを活用したプロンプトを導入し、RAGチェーンの実装を完了させることになります。
template = """Answer the question based only on the following context:
{context}
Question: {question}
"""
prompt = ChatPromptTemplate.from_template(template)
chain = (
RunnableParallel(
{
"context": _search_query | retriever,
"question": RunnablePassthrough(),
}
)
| prompt
| llm
| StrOutputParser()
)
最後に、ハイブリッドRAGの実装をテストできます。
chain.invoke({"question": "Which house did Elizabeth I belong to?"})
# Search query: Which house did Elizabeth I belong to?
# 'Elizabeth I belonged to the House of Tudor.'
私はクエリの書き換え機能も取り入れており、RAGチェーンが追加質問を可能にする会話設定に適応できるようにしています。ベクトルとキーワード検索方法を使用しているため、追加質問を書き換えて検索プロセスを最適化する必要があります。
chain.invoke(
{
"question": "When was she born?",
"chat_history": [("Which house did Elizabeth I belong to?", "House Of Tudor")],
}
)
# Search query: When was Elizabeth I born?
# 'Elizabeth I was born on 7 September 1533.'
When was she born? はまず When was Elizabeth I born? と書き換えられました。その書き換えられたクエリは、関連する文脈を取得して質問に答えるために使用されました。
RAGアプリケーションの強化を簡単にする
の導入により、ナレッジグラフの生成プロセスがスムーズでよりアクセスしやすくなり、ナレッジグラフが提供する深さとコンテキストでRAGアプリケーションを強化したいと考えている人々にとって、これがより簡単になります。これは始まりに過ぎず、今後も多くの改善が計画されています。
コードはGitHubで利用可能です。
この記事が気に入ったらサポートをしてみませんか?