Bilgi Grafikleri Kullanarak Nasıl RAG Yapılır?

Cahit Barkin Ozer
24 min readApr 16, 2024

Deeplearning.ai sitesindeki “Knowledge Graphs for RAG” kursunun Türkçe özetidir.

For English:

NEO4J

[https://neo4j.com/docs/getting-started/]

Neo4j, özünde yerel bir grafik veritabanıyla verileri doğal, bağlantılı bir durumda saklar ve yönetir. Grafik veritabanı, hem geçiş performansı hem de operasyon çalışma süresi açısından faydalı olan bir özellik grafiği yaklaşımını benimser. Neo4j bir grafik veritabanı olarak başladı ve çok sayıda araç, uygulama ve kütüphane içeren zengin bir ekosisteme dönüştü. Bu ekosistem, grafik teknolojilerini çalışma ortamınıza sorunsuz bir şekilde entegre etmenize olanak tanır.

Neo4j’yi yükleme ve dağıtma talimatları için Neo4j belgelerine bakın: [https://neo4j.com/docs/getting-started/get-started-with-neo4j/#neo4j-docs].

Bilgi grafikleri öncelikle “çok atlamalı sorular” için veya soruyu yanıtlamadan önce çözülmesi gereken sorguda bazı gizli bağlamlar olduğunda ve gizli bağlama ilişkin bilgiler bilgi grafikleri gibi yapılandırılmış verilerde bulunabildiğinde faydalıdır . Diyelim ki bir seyahat acentesi platformusunuz, seyahat destinasyonlarını içeren bir vektör veritabanınız ve platformun her kullanıcısının daha önce ziyaret ettiği yerlere ilişkin bilgilerin yer aldığı bir bilgi grafiğiniz var. Kullanıcı “Temmuz ayı için Asya’da ziyaret etmediğim plaj destinasyonlarını önerir misiniz?” daha sonra, kullanıcının önceden ziyaret ettiği hedefleri semantik arama sonuçlarından filtrelemek için bilgi grafiğini kullanabilirsiniz. Benzer şekilde, bilgi grafiğine destinasyonun coğrafyası, aylık iklimi vb. hakkında gerçek bilgiler ekleyebilirsiniz, böylece soruyu anlamsal arama yerleştirmelerine ve LLM çıkarımına daha az güvenerek daha güvenilir bir şekilde yanıtlayabilirsiniz.

RAG yaparken bilgi grafiklerini kullanmak yaşanılan zorlukları önemli ölçüde azaltabilir:

  • Bağlamsal Uygunluk: Grafik yapısı, alınan bilgilerin yalnızca alakalı değil aynı zamanda bağlamsal olmasını da sağlayarak yanıt oluşturma için daha zengin bir arka plan sağlar.
  • Karmaşık Sorgular: Bilgi grafiklerinin birbirine bağlı doğası, ilişkiler bağlamın derinlemesine anlaşılmasını sağladığından karmaşık sorguların verimli bir şekilde ele alınmasına olanak tanır.
  • Veri Entegrasyonu: Bilgi grafikleri, çeşitli veri türlerini entegre etme konusunda ustadır ve RAG yanıtlarını zenginleştiren birleşik bir görünüm sunar.

Bilgi Grafiklerinin Temelleri

Düğümler, Bilgi Grafiklerindeki veri kayıtlarıdır. Düğümler arasındaki ilişkiler de veri kayıtlarıdır.

Basit bir bilgi grafiği (PERSON)-[KNOWS]>(PERSON)

İlişkilerin her zaman bir türü ve yönü vardır.

Bilgi Grafiklerindeki İlişkiler

Düğümler, kendisinin özellikleri muhafaza ettiği bir ilişki içindedir. Grafik türleri birleştirilebilirlerdir.

(PERSON)-[TEACHES]>(COURSE)<[INTRODUCES]-(PERSON)

Resmi olarak düğümler ve ilişkiler, anahtar/değer özelliklerine sahip veri kayıtlarıdırlar.

Bilgi Grafiklerini Sorgulama

from dotenv import load_dotenv
import os

from langchain_community.graphs import Neo4jGraph

load_dotenv('.env', override=True)
NEO4J_URI = os.getenv('NEO4J_URI')
NEO4J_USERNAME = os.getenv('NEO4J_USERNAME')
NEO4J_PASSWORD = os.getenv('NEO4J_PASSWORD')
NEO4J_DATABASE = os.getenv('NEO4J_DATABASE')

LangChain’in Neo4j entegrasyonunu kullanarak bir bilgi grafiği örneğinin başlatılması:

kg = Neo4jGraph(
url=NEO4J_URI, username=NEO4J_USERNAME, password=NEO4J_PASSWORD, database=NEO4J_DATABASE
)

Movie bilgi grafiğini sorgulama

(PERSON)-[ACTED_IN]> (MOVIE)
Person ve Movie Düğümünün Özellikleri
Person ve Movie arasındaki tüm ilişkiler

Grafikteki tüm düğümleri eşleştirme:

cypher = """
MATCH (n)
RETURN count(n)
"""

result = kg.query(cypher)
print(result) # [{'count(n)':171}]


cypher = """
MATCH (n)
RETURN count(n) AS numberOfNodes
"""
result = kg.query(cypher)
print(result) # [{'numberOfNodes':171}]

print(f"There are {result[0]['numberOfNodes']} nodes in this graph.") # There are 171 nodes in this graph.

Düğüm etiketini belirterek yalnızca Movie düğümlerini eşleştirin:

cypher = """
MATCH (n:Movie)
RETURN count(n) AS numberOfMovies
"""
kg.query(cypher) # [{'numberOfMovies':38}]

Daha iyi okunabilirlik için düğüm kalıbı eşleşmesindeki değişken adını değiştirin:

cypher = """
MATCH (m:Movie)
RETURN count(m) AS numberOfMovies
"""
kg.query(cypher) # [{'numberOfMovies':38}]

Yalnıza Person düğümlerini eşleştirin:

cypher = """
MATCH (people:Person)
RETURN count(people) AS numberOfPeople
"""
kg.query(cypher) # [{'numberOfPeople':133}]

Person düğümündeki name özelliğinin değerini belirterek tek bir kişiyi eşleştirin:

cypher = """
MATCH (tom:Person {name:"Tom Hanks"})
RETURN tom
"""
kg.query(cypher)

Title özelliğinin değerini belirterek tek bir Movie eşleştirin:

cypher = """
MATCH (cloudAtlas:Movie {title:"Cloud Atlas"})
RETURN cloudAtlas
"""
kg.query(cypher)

Eşleşen Movie düğümünün yalnızca released özelliğini döndürür:

cypher = """
MATCH (cloudAtlas:Movie {title:"Cloud Atlas"})
RETURN cloudAtlas.released
"""
kg.query(cypher)

İki özellik döndürür:

cypher = """
MATCH (cloudAtlas:Movie {title:"Cloud Atlas"})
RETURN cloudAtlas.released, cloudAtlas.tagline
"""
kg.query(cypher)

Koşullu eşleştirme ile Cypher kalıpları:

cypher = """
MATCH (nineties:Movie)
WHERE nineties.released >= 1990
AND nineties.released < 2000
RETURN nineties.title
"""
kg.query(cypher)

Çoklu düğümlerle desen eşleştirme

cypher = """
MATCH (actor:Person)-[:ACTED_IN]->(movie:Movie)
RETURN actor.name, movie.title LIMIT 10
"""
kg.query(cypher)

cypher = """
MATCH (tom:Person {name: "Tom Hanks"})-[:ACTED_IN]->(tomHanksMovies:Movie)
RETURN tom.name,tomHanksMovies.title
"""
kg.query(cypher)

cypher = """
MATCH (tom:Person {name:"Tom Hanks"})-[:ACTED_IN]->(m)<-[:ACTED_IN]-(coActors)
RETURN coActors.name, m.title
"""
kg.query(cypher)

Grafikten Veri Silme

cypher = """
MATCH (emil:Person {name:"Emil Eifrem"})-[actedIn:ACTED_IN]->(movie:Movie)
RETURN emil.name, movie.title
"""
kg.query(cypher)

cypher = """
MATCH (emil:Person {name:"Emil Eifrem"})-[actedIn:ACTED_IN]->(movie:Movie)
DELETE actedIn
"""
kg.query(cypher)

Grafiğe Veri Ekleme

cypher = """
CREATE (andreas:Person {name:"Andreas"})
RETURN andreas
"""

kg.query(cypher)

cypher = """
MATCH (andreas:Person {name:"Andreas"}), (emil:Person {name:"Emil Eifrem"})
MERGE (andreas)-[hasRelationship:WORKS_WITH]->(emil)
RETURN andreas, hasRelationship, emil
"""
kg.query(cypher)

RAG İçin Metin Hazırlama

from dotenv import load_dotenv
import os

from langchain_community.graphs import Neo4jGraph

# Load from environment
load_dotenv('.env', override=True)
NEO4J_URI = os.getenv('NEO4J_URI')
NEO4J_USERNAME = os.getenv('NEO4J_USERNAME')
NEO4J_PASSWORD = os.getenv('NEO4J_PASSWORD')
NEO4J_DATABASE = os.getenv('NEO4J_DATABASE')
OPENAI_API_KEY = os.getenv('OPENAI_API_KEY')

# Note the code below is unique to this course environment, and not a
# standard part of Neo4j's integration with OpenAI. Remove if running
# in your own environment.
OPENAI_ENDPOINT = os.getenv('OPENAI_BASE_URL') + '/embeddings'

# Connect to the knowledge graph instance using LangChain
kg = Neo4jGraph(
url=NEO4J_URI, username=NEO4J_USERNAME, password=NEO4J_PASSWORD, database=NEO4J_DATABASE
)

Bir Vektör İndeksi Oluşturun:

kg.query("""
CREATE VECTOR INDEX movie_tagline_embeddings IF NOT EXISTS
FOR (m:Movie) ON (m.taglineEmbedding)
OPTIONS { indexConfig: {
`vector.dimensions`: 1536,
`vector.similarity_function`: 'cosine'
}}"""
)

kg.query("""
SHOW VECTOR INDEXES
"""
)

Vektör İndeksini Doldurun

OpenAI kullanarak her film sloganı için vektör temsilini hesaplayın. Movie düğümüne taglineEmbedding özelliği olarak vektör ekleyin

kg.query("""
MATCH (movie:Movie) WHERE movie.tagline IS NOT NULL
WITH movie, genai.vector.encode(
movie.tagline,
"OpenAI",
{
token: $openAiApiKey,
endpoint: $openAiEndpoint
}) AS vector
CALL db.create.setNodeVectorProperty(movie, "taglineEmbedding", vector)
""",
params={"openAiApiKey":OPENAI_API_KEY, "openAiEndpoint": OPENAI_ENDPOINT} )

result = kg.query("""
MATCH (m:Movie)
WHERE m.tagline IS NOT NULL
RETURN m.tagline, m.taglineEmbedding
LIMIT 1
"""
)

result[0]['m.tagline']
result[0]['m.taglineEmbedding'][:10]
len(result[0]['m.taglineEmbedding'])

Benzerlik Araması (Similarity Search)

Soru için yerleştirmeyi hesaplayın. Soru ve sloganEmbedding vektörlerinin benzerliğine göre eşleşen filmleri belirleyin.

question = "What movies are about love?"
kg.query("""
WITH genai.vector.encode(
$question,
"OpenAI",
{
token: $openAiApiKey,
endpoint: $openAiEndpoint
}) AS question_embedding
CALL db.index.vector.queryNodes(
'movie_tagline_embeddings',
$top_k,
question_embedding
) YIELD node AS movie, score
RETURN movie.title, movie.tagline, score
""",
params={"openAiApiKey":OPENAI_API_KEY,
"openAiEndpoint": OPENAI_ENDPOINT,
"question": question,
"top_k": 5
})

Sorunuzu sorun:

question = "What movies are about adventure?" # Change here with your question

kg.query("""
WITH genai.vector.encode(
$question,
"OpenAI",
{
token: $openAiApiKey,
endpoint: $openAiEndpoint
}) AS question_embedding
CALL db.index.vector.queryNodes(
'movie_tagline_embeddings',
$top_k,
question_embedding
) YIELD node AS movie, score
RETURN movie.title, movie.tagline, score
""",
params={"openAiApiKey":OPENAI_API_KEY,
"openAiEndpoint": OPENAI_ENDPOINT,
"question": question,
"top_k": 5
})

Metin Belgelerinden Belge Grafiği Oluşturma

from dotenv import load_dotenv
import os

# Common data processing
import json
import textwrap

# Langchain
from langchain_community.graphs import Neo4jGraph
from langchain_community.vectorstores import Neo4jVector
from langchain_openai import OpenAIEmbeddings
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain.chains import RetrievalQAWithSourcesChain
from langchain_openai import ChatOpenAI


# Load from environment
load_dotenv('.env', override=True)
NEO4J_URI = os.getenv('NEO4J_URI')
NEO4J_USERNAME = os.getenv('NEO4J_USERNAME')
NEO4J_PASSWORD = os.getenv('NEO4J_PASSWORD')
NEO4J_DATABASE = os.getenv('NEO4J_DATABASE') or 'neo4j'
OPENAI_API_KEY = os.getenv('OPENAI_API_KEY')
# Note the code below is unique to this course environment, and not a
# standard part of Neo4j's integration with OpenAI. Remove if running
# in your own environment.
OPENAI_ENDPOINT = os.getenv('OPENAI_BASE_URL') + '/embeddings'

# Global constants
VECTOR_INDEX_NAME = 'form_10k_chunks'
VECTOR_NODE_LABEL = 'Chunk'
VECTOR_SOURCE_PROPERTY = 'text'
VECTOR_EMBEDDING_PROPERTY = 'textEmbedding'

Form 10-K JSON dosyasına bir göz atın:

Halka açık şirketlerin her yıl Menkul Kıymetler ve Borsa Komisyonu (SEC) ile bir Form 10-K doldurmaları gerekmektedir. SEC’in EDGAR veritabanını kullanarak bu dosyalarda arama yapabilirsiniz [https://www.sec.gov/edgar/search/]. Önümüzdeki birkaç ders boyunca, NetApp adlı bir şirket için tek bir 10-K formu ile çalışacaksınız.

Veri Temizleme

Tamamlanmış 10-k formları XML bileşenleri içeren metin dosyaları olarak indirilebilir. Bunlarla çalışmak için aşağıdaki temizleme adımları uygulanmalıdır:

  • Regex ile dosyaları temizleme.
  • Beautiful Soup kullanarak XML’i Python veri yapılarına ayrıştırma.
  • SEC tarafından kullanılan bir şirket tanımlayıcısı olan CIK (Merkezi Dizin Anahtarı) kimliğini çıkarma.
  • Formun belirli bölümlerini çıkarma (Madde 1, 1a, 7 ve 7a).

Yaklaşım:

  • Langchain metin ayırıcı kullanarak form bölümlerini parçalara ayırın.
  • Her yığının bir düğüm olduğu bir grafik oluşturun ve yığın meta verilerini özellikler olarak ekleyin.
  • Bir vektör dizini oluşturun.
  • Her yığın için metin gömme vektörünü hesaplayın ve dizini doldurun.
  • İlgili parçaları bulmak için benzerlik aramasını kullanın.
first_file_name = "./data/form10k/0000950170-23-027948.json"
first_file_as_object = json.load(open(first_file_name))
for k,v in first_file_as_object.items():
print(k, type(v))
item1_text = first_file_as_object['item1']
item1_text[0:1500]

Form 10-K bölümlerini parçalara ayırın:
LangChain kullanarak metin ayırıcıyı ayarlayın. Şimdilik, yalnızca “madde 1” bölümündeki metni bölün.

text_splitter = RecursiveCharacterTextSplitter(
chunk_size = 2000,
chunk_overlap = 200,
length_function = len,
is_separator_regex = False,
)
item1_text_chunks = text_splitter.split_text(item1_text)
item1_text_chunks[0]

Form 10-K’nin tüm bölümlerini parçalara ayırmak için bir yardımcı işlev ayarlayın. İşleri hızlandırmak için her bölümdeki yığın sayısını 20 ile sınırlayacaksınız.

def split_form10k_data_from_file(file):
chunks_with_metadata = [] # use this to accumlate chunk records
file_as_object = json.load(open(file)) # open the json file
for item in ['item1','item1a','item7','item7a']: # pull these keys from the json
print(f'Processing {item} from {file}')
item_text = file_as_object[item] # grab the text of the item
item_text_chunks = text_splitter.split_text(item_text) # split the text into chunks
chunk_seq_id = 0
for chunk in item_text_chunks[:20]: # only take the first 20 chunks
form_id = file[file.rindex('/') + 1:file.rindex('.')] # extract form id from file name
# finally, construct a record with metadata and the chunk text
chunks_with_metadata.append({
'text': chunk,
# metadata from looping...
'f10kItem': item,
'chunkSeqId': chunk_seq_id,
# constructed metadata...
'formId': f'{form_id}', # pulled from the filename
'chunkId': f'{form_id}-{item}-chunk{chunk_seq_id:04d}',
# metadata from file...
'names': file_as_object['names'],
'cik': file_as_object['cik'],
'cusip6': file_as_object['cusip6'],
'source': file_as_object['source'],
})
chunk_seq_id += 1
print(f'\tSplit into {chunk_seq_id} chunks')
return chunks_with_metadata

first_file_chunks = split_form10k_data_from_file(first_file_name)
first_file_chunks[0]

Metin parçalarını kullanarak grafik düğümleri oluşturun:

merge_chunk_node_query = """
MERGE(mergedChunk:Chunk {chunkId: $chunkParam.chunkId})
ON CREATE SET
mergedChunk.names = $chunkParam.names,
mergedChunk.formId = $chunkParam.formId,
mergedChunk.cik = $chunkParam.cik,
mergedChunk.cusip6 = $chunkParam.cusip6,
mergedChunk.source = $chunkParam.source,
mergedChunk.f10kItem = $chunkParam.f10kItem,
mergedChunk.chunkSeqId = $chunkParam.chunkSeqId,
mergedChunk.text = $chunkParam.text
RETURN mergedChunk
"""

LangChain’i kullanarak grafik örneğine bağlantı kurun:

kg = Neo4jGraph(
url=NEO4J_URI, username=NEO4J_USERNAME, password=NEO4J_PASSWORD, database=NEO4J_DATABASE
)
# Create a single chunk node for now
kg.query(merge_chunk_node_query,
params={'chunkParam':first_file_chunks[0]})
# Create a uniqueness constraint to avoid duplicate chunks
kg.query("""
CREATE CONSTRAINT unique_chunk IF NOT EXISTS
FOR (c:Chunk) REQUIRE c.chunkId IS UNIQUE
""")
kg.query("SHOW INDEXES")

Tüm parçalar için döngü yapın ve düğümler oluşturun. Yukarıdaki metin bölme işlevinde 20 parça sınırı belirlediğiniz için 23 düğüm oluşturmalısınız.

node_count = 0
for chunk in first_file_chunks:
print(f"Creating `:Chunk` node for chunk ID {chunk['chunkId']}")
kg.query(merge_chunk_node_query,
params={
'chunkParam': chunk
})
node_count += 1

kg.query("""
MATCH (n)
RETURN count(n) as nodeCount
""")

Bir vektör dizini oluşturun:

kg.query("""
CREATE VECTOR INDEX `form_10k_chunks` IF NOT EXISTS
FOR (c:Chunk) ON (c.textEmbedding)
OPTIONS { indexConfig: {
`vector.dimensions`: 1536,
`vector.similarity_function`: 'cosine'
}}
""")
kg.query("SHOW INDEXES")

Parçalar için gömme vektörlerini hesaplayın ve dizini doldurun:
Bu sorgu, gömme vektörünü hesaplar ve bunu her parça düğümünde textEmbedding adı verilen bir özellik olarak saklar.

kg.query("""
MATCH (chunk:Chunk) WHERE chunk.textEmbedding IS NULL
WITH chunk, genai.vector.encode(
chunk.text,
"OpenAI",
{
token: $openAiApiKey,
endpoint: $openAiEndpoint
}) AS vector
CALL db.create.setNodeVectorProperty(chunk, "textEmbedding", vector)
""",
params={"openAiApiKey":OPENAI_API_KEY, "openAiEndpoint": OPENAI_ENDPOINT} )

kg.refresh_schema()
print(kg.schema)

Alakalı parçaları bulmak için benzerlik aramasını kullanın

Vektör indeksini kullanarak benzerlik araması gerçekleştirmek için bir yardım fonksiyonu ayarlayın.

def neo4j_vector_search(question):
"""Search for similar nodes using the Neo4j vector index"""
vector_search_query = """
WITH genai.vector.encode(
$question,
"OpenAI",
{
token: $openAiApiKey,
endpoint: $openAiEndpoint
}) AS question_embedding
CALL db.index.vector.queryNodes($index_name, $top_k, question_embedding) yield node, score
RETURN score, node.text AS text
"""
similar = kg.query(vector_search_query,
params={
'question': question,
'openAiApiKey':OPENAI_API_KEY,
'openAiEndpoint': OPENAI_ENDPOINT,
'index_name':VECTOR_INDEX_NAME,
'top_k': 10})
return similar

search_results = neo4j_vector_search(
'In a single sentence, tell me about Netapp.'
)

search_results[0]

Formla sohbet etmek için LangChain RAG iş akışını ayarlayın.

neo4j_vector_store = Neo4jVector.from_existing_graph(
embedding=OpenAIEmbeddings(),
url=NEO4J_URI,
username=NEO4J_USERNAME,
password=NEO4J_PASSWORD,
index_name=VECTOR_INDEX_NAME,
node_label=VECTOR_NODE_LABEL,
text_node_properties=[VECTOR_SOURCE_PROPERTY],
embedding_node_property=VECTOR_EMBEDDING_PROPERTY,
)
retriever = neo4j_vector_store.as_retriever()

Soru yanıtlamayı gerçekleştirmek için bir RetrievalQAWithSourcesChain kurun. Bu zincire ilişkin LangChain belgelerine buradan göz atabilirsiniz: [https://api.python.langchain.com/en/latest/chains/langchain.chains.qa_with_sources.retrieval.RetrievalQAWithSourcesChain.html]. SEC Bilgi Grafiğine İlişkiler Ekleme.

chain = RetrievalQAWithSourcesChain.from_chain_type(
ChatOpenAI(temperature=0),
chain_type="stuff",
retriever=retriever
)

def prettychain(question: str) -> str:
"""Pretty print the chain's response to a question"""
response = chain({"question": question},
return_only_outputs=True,)
print(textwrap.fill(response['answer'], 60))

Soru sormak:

question = "What is Netapp's primary business?"

prettychain(question)

prettychain("Where is Netapp headquartered?")

prettychain("""
Tell me about Netapp.
Limit your answer to a single sentence.
""")

prettychain("""
Tell me about Apple.
Limit your answer to a single sentence.
""")

prettychain("""
Tell me about Apple.
Limit your answer to a single sentence.
If you are unsure about the answer, say you don't know.
""")

prettychain("""
ADD YOUR OWN QUESTION HERE
""")

SEC Bilgi Grafiğine İlişkiler Ekleme

from dotenv import load_dotenv
import os

# Common data processing
import textwrap

# Langchain
from langchain_community.graphs import Neo4jGraph
from langchain_community.vectorstores import Neo4jVector
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain.chains import RetrievalQAWithSourcesChain
from langchain_openai import ChatOpenAI
from langchain_openai import OpenAIEmbeddings

# Load from environment
load_dotenv('.env', override=True)
NEO4J_URI = os.getenv('NEO4J_URI')
NEO4J_USERNAME = os.getenv('NEO4J_USERNAME')
NEO4J_PASSWORD = os.getenv('NEO4J_PASSWORD')
NEO4J_DATABASE = os.getenv('NEO4J_DATABASE') or 'neo4j'

# Global constants
VECTOR_INDEX_NAME = 'form_10k_chunks'
VECTOR_NODE_LABEL = 'Chunk'
VECTOR_SOURCE_PROPERTY = 'text'
VECTOR_EMBEDDING_PROPERTY = 'textEmbedding'

kg = Neo4jGraph(
url=NEO4J_URI, username=NEO4J_USERNAME, password=NEO4J_PASSWORD, database=NEO4J_DATABASE
)

Form 10-K düğümü oluşturma

Form 10-K’nin tamamını temsil edecek bir düğüm oluşturun. Formun tek bir bölümünden alınan meta verilerle doldurun.

cypher = """
MATCH (anyChunk:Chunk)
WITH anyChunk LIMIT 1
RETURN anyChunk { .names, .source, .formId, .cik, .cusip6 } as formInfo
"""
form_info_list = kg.query(cypher)

form_info = form_info_list[0]['formInfo']

cypher = """
MERGE (f:Form {formId: $formInfoParam.formId })
ON CREATE
SET f.names = $formInfoParam.names
SET f.source = $formInfoParam.source
SET f.cik = $formInfoParam.cik
SET f.cusip6 = $formInfoParam.cusip6
"""

kg.query(cypher, params={'formInfoParam': form_info})

kg.query("MATCH (f:Form) RETURN count(f) as formCount")

Her bölüm için linked list bir parça düğüm listesi oluşturun

Aynı bölümdeki parçaları tanımlayarak başlayın.

cypher = """
MATCH (from_same_form:Chunk)
WHERE from_same_form.formId = $formIdParam
RETURN from_same_form {.formId, .f10kItem, .chunkId, .chunkSeqId } as chunkInfo
LIMIT 10
"""

kg.query(cypher, params={'formIdParam': form_info['formId']})

Parçaları sıra ID’lerine göre sipariş edin.

cypher = """
MATCH (from_same_form:Chunk)
WHERE from_same_form.formId = $formIdParam
RETURN from_same_form {.formId, .f10kItem, .chunkId, .chunkSeqId } as chunkInfo
ORDER BY from_same_form.chunkSeqId ASC
LIMIT 10
"""

kg.query(cypher, params={'formIdParam': form_info['formId']})

Parçaları yalnızca “Item 1” bölümüyle sınırlayın, ardından bunları artan sırada düzenleyin.

cypher = """
MATCH (from_same_section:Chunk)
WHERE from_same_section.formId = $formIdParam
AND from_same_section.f10kItem = $f10kItemParam // NEW!!!
RETURN from_same_section { .formId, .f10kItem, .chunkId, .chunkSeqId }
ORDER BY from_same_section.chunkSeqId ASC
LIMIT 10
"""

kg.query(cypher, params={'formIdParam': form_info['formId'],
'f10kItemParam': 'item1'})

Sıralı parçaları bir liste halinde toplayın.

cypher = """
MATCH (from_same_section:Chunk)
WHERE from_same_section.formId = $formIdParam
AND from_same_section.f10kItem = $f10kItemParam
WITH from_same_section { .formId, .f10kItem, .chunkId, .chunkSeqId }
ORDER BY from_same_section.chunkSeqId ASC
LIMIT 10
RETURN collect(from_same_section) // NEW!!!
"""

kg.query(cypher, params={'formIdParam': form_info['formId'],
'f10kItemParam': 'item1'})

Sonraki parçalar arasına NEXT ilişkisi ekleme

Chunk düğümlerinin sıralı listesini bir NEXT ilişkisine bağlamak için Neo4j’deki apoc.nodes.link işlevini kullanın. Bunu yalnızca “Item 1” bölümünün başlaması için yapın.

cypher = """
MATCH (from_same_section:Chunk)
WHERE from_same_section.formId = $formIdParam
AND from_same_section.f10kItem = $f10kItemParam
WITH from_same_section
ORDER BY from_same_section.chunkSeqId ASC
WITH collect(from_same_section) as section_chunk_list
CALL apoc.nodes.link(
section_chunk_list,
"NEXT",
{avoidDuplicates: true}
) // NEW!!!
RETURN size(section_chunk_list)
"""

kg.query(cypher, params={'formIdParam': form_info['formId'],
'f10kItemParam': 'item1'})

kg.refresh_schema()

10-K formunun tüm bölümleri için döngü yapın ve ilişkiler oluşturun.

cypher = """
MATCH (from_same_section:Chunk)
WHERE from_same_section.formId = $formIdParam
AND from_same_section.f10kItem = $f10kItemParam
WITH from_same_section
ORDER BY from_same_section.chunkSeqId ASC
WITH collect(from_same_section) as section_chunk_list
CALL apoc.nodes.link(
section_chunk_list,
"NEXT",
{avoidDuplicates: true}
)
RETURN size(section_chunk_list)
"""
for form10kItemName in ['item1', 'item1a', 'item7', 'item7a']:
kg.query(cypher, params={'formIdParam':form_info['formId'],
'f10kItemParam': form10kItemName})

Parçaları PART_OF ilişkisiyle ana formlarına bağlayın:

cypher = """https://s172-31-2-251p19846.lab-aws-production.deeplearning.ai/notebooks/L5-add_relationships_to_kg.ipynb#Connect-chunks-to-their-parent-form-with-a-PART_OF-relationship
MATCH (c:Chunk), (f:Form)
WHERE c.formId = f.formId
MERGE (c)-[newRelationship:PART_OF]->(f)
RETURN count(newRelationship)
"""

kg.query(cypher)

Her bölümün ilk parçasında bir SECTION ilişkisi oluşturun:

cypher = """
MATCH (first:Chunk), (f:Form)
WHERE first.formId = f.formId
AND first.chunkSeqId = 0
WITH first, f
MERGE (f)-[r:SECTION {f10kItem: first.f10kItem}]->(first)
RETURN count(r)
"""

kg.query(cypher)

Örnek cypher sorguları

Item 1 bölümünün ilk parçasını döndürün.

cypher = """
MATCH (f:Form)-[r:SECTION]->(first:Chunk)
WHERE f.formId = $formIdParam
AND r.f10kItem = $f10kItemParam
RETURN first.chunkId as chunkId, first.text as text
"""

first_chunk_info = kg.query(cypher, params={
'formIdParam': form_info['formId'],
'f10kItemParam': 'item1'
})[0]

Madde 1 bölümünün ikinci parçasını alın.

cypher = """
MATCH (first:Chunk)-[:NEXT]->(nextChunk:Chunk)
WHERE first.chunkId = $chunkIdParam
RETURN nextChunk.chunkId as chunkId, nextChunk.text as text
"""

next_chunk_info = kg.query(cypher, params={
'chunkIdParam': first_chunk_info['chunkId']
})[0]

print(first_chunk_info['chunkId'], next_chunk_info['chunkId'])

Üç parçadan oluşan bir pencere döndürün:

cypher = """
MATCH (c1:Chunk)-[:NEXT]->(c2:Chunk)-[:NEXT]->(c3:Chunk)
WHERE c2.chunkId = $chunkIdParam
RETURN c1.chunkId, c2.chunkId, c3.chunkId
"""

kg.query(cypher,
params={'chunkIdParam': next_chunk_info['chunkId']})

Bilgiler bir grafik yapısında depolanır

Bir grafikteki eşleşen düğüm kalıpları ve ilişkilere yollar denir. Yolun uzunluğu yoldaki ilişkilerin sayısına eşittir. Yollar değişkenler olarak yakalanabilir ve sorguların başka yerlerinde kullanılabilir.

cypher = """
MATCH window = (c1:Chunk)-[:NEXT]->(c2:Chunk)-[:NEXT]->(c3:Chunk)
WHERE c1.chunkId = $chunkIdParam
RETURN length(window) as windowPathLength
"""

kg.query(cypher,
params={'chunkIdParam': next_chunk_info['chunkId']})

Değişken uzunluktaki pencereleri bulma

İlişki grafikte mevcut değilse kalıp eşleşmesi başarısız olur. Örneğin, bir bölümdeki ilk öbeğin kendisinden önce gelen bir öbeği yoktur, bu nedenle sonraki sorgu hiçbir şey döndürmez.

cypher = """
MATCH window=(c1:Chunk)-[:NEXT]->(c2:Chunk)-[:NEXT]->(c3:Chunk)
WHERE c2.chunkId = $chunkIdParam
RETURN nodes(window) as chunkList
"""
# pull the chunk ID from the first
kg.query(cypher,
params={'chunkIdParam': first_chunk_info['chunkId']})

NEXT ilişkisini değişken uzunluğa sahip olacak şekilde değiştirin.

cypher = """
MATCH window=
(:Chunk)-[:NEXT*0..1]->(c:Chunk)-[:NEXT*0..1]->(:Chunk)
WHERE c.chunkId = $chunkIdParam
RETURN length(window)
"""

kg.query(cypher,
params={'chunkIdParam': first_chunk_info['chunkId']})

Yalnızca en uzun yolu alın.

cypher = """
MATCH window=
(:Chunk)-[:NEXT*0..1]->(c:Chunk)-[:NEXT*0..1]->(:Chunk)
WHERE c.chunkId = $chunkIdParam
WITH window as longestChunkWindow
ORDER BY length(window) DESC LIMIT 1
RETURN length(longestChunkWindow)
"""

kg.query(cypher,
params={'chunkIdParam': first_chunk_info['chunkId']})

Cypher’ı kullanarak benzerlik aramasının sonuçlarını özelleştirin

Vektör deposu tanımını bir Cypher sorgusunu kabul edecek şekilde genişletin. Cypher sorgusu, vektör benzerlik aramasının sonuçlarını alır ve bunları bir şekilde değiştirir. Arama sonuçlarıyla birlikte yalnızca bazı ekstra metinleri döndüren basit bir sorguyla başlayın.

retrieval_query_extra_text = """
WITH node, score, "Andreas knows Cypher. " as extraText
RETURN extraText + "\n" + node.text as text,
score,
node {.source} AS metadata
"""

Sorguyu kullanmak için vektör deposunu kurun, ardından LangChain’de bir alıcı ve Soru-Cevap zinciri oluşturun.

vector_store_extra_text = Neo4jVector.from_existing_index(
embedding=OpenAIEmbeddings(),
url=NEO4J_URI,
username=NEO4J_USERNAME,
password=NEO4J_PASSWORD,
database="neo4j",
index_name=VECTOR_INDEX_NAME,
text_node_property=VECTOR_SOURCE_PROPERTY,
retrieval_query=retrieval_query_extra_text, # NEW !!!
)

# Create a retriever from the vector store
retriever_extra_text = vector_store_extra_text.as_retriever()

# Create a chatbot Question & Answer chain from the retriever
chain_extra_text = RetrievalQAWithSourcesChain.from_chain_type(
ChatOpenAI(temperature=0),
chain_type="stuff",
retriever=retriever_extra_text
)

# Ask a question
chain_extra_text(
{"question": "What topics does Andreas know about?"},
return_only_outputs=True)

LLM’in burada hem alınan metindeki bilgileri hem de ekstra metni kullanarak halüsinasyon gördüğünü unutmayın. Daha doğru bir yanıt almayı denemek için istemi değiştirin.

chain_extra_text(
{"question": "What single topic does Andreas know about?"},
return_only_outputs=True)

Aşağıdaki şablonla kendinizinkini deneyin:
Ek metninizi eklemek için aşağıdaki sorguyu değiştirin. Sonuçlarınızı hassaslaştırmak için istemi tasarlamayı deneyin. Cypher sorgusunu her değiştirdiğinizde vektör deposunu, alıcıyı ve zinciri sıfırlamanız gerekeceğini unutmayın.

# modify the retrieval extra text here then run the entire cell
retrieval_query_extra_text = """
WITH node, score, "Andreas knows Cypher. " as extraText
RETURN extraText + "\n" + node.text as text,
score,
node {.source} AS metadata
"""

vector_store_extra_text = Neo4jVector.from_existing_index(
embedding=OpenAIEmbeddings(),
url=NEO4J_URI,
username=NEO4J_USERNAME,
password=NEO4J_PASSWORD,
database="neo4j",
index_name=VECTOR_INDEX_NAME,
text_node_property=VECTOR_SOURCE_PROPERTY,
retrieval_query=retrieval_query_extra_text, # NEW !!!
)

# Create a retriever from the vector store
retriever_extra_text = vector_store_extra_text.as_retriever()

# Create a chatbot Question & Answer chain from the retriever
chain_extra_text = RetrievalQAWithSourcesChain.from_chain_type(
ChatOpenAI(temperature=0),
chain_type="stuff",
retriever=retriever_extra_text
)

Bir pencere kullanarak bir öbeğin etrafındaki bağlamı genişletin

Öncelikle tek bir düğümü alan normal bir vektör deposu oluşturun.

neo4j_vector_store = Neo4jVector.from_existing_graph(
embedding=OpenAIEmbeddings(),
url=NEO4J_URI,
username=NEO4J_USERNAME,
password=NEO4J_PASSWORD,
index_name=VECTOR_INDEX_NAME,
node_label=VECTOR_NODE_LABEL,
text_node_properties=[VECTOR_SOURCE_PROPERTY],
embedding_node_property=VECTOR_EMBEDDING_PROPERTY,
)
# Create a retriever from the vector store
windowless_retriever = neo4j_vector_store.as_retriever()

# Create a chatbot Question & Answer chain from the retriever
windowless_chain = RetrievalQAWithSourcesChain.from_chain_type(
ChatOpenAI(temperature=0),
chain_type="stuff",
retriever=windowless_retriever
)

Daha sonra ardışık parçaları almak için bir pencere alma sorgusu (window retrieval query) tanımlayın.

retrieval_query_window = """
MATCH window=
(:Chunk)-[:NEXT*0..1]->(node)-[:NEXT*0..1]->(:Chunk)
WITH node, score, window as longestWindow
ORDER BY length(window) DESC LIMIT 1
WITH nodes(longestWindow) as chunkList, node, score
UNWIND chunkList as chunkRows
WITH collect(chunkRows.text) as textList, node, score
RETURN apoc.text.join(textList, " \n ") as text,
score,
node {.source} AS metadata
"""

Pencere alma sorgusunu kullanacak bir QA zinciri ayarlayın.

vector_store_window = Neo4jVector.from_existing_index(
embedding=OpenAIEmbeddings(),
url=NEO4J_URI,
username=NEO4J_USERNAME,
password=NEO4J_PASSWORD,
database="neo4j",
index_name=VECTOR_INDEX_NAME,
text_node_property=VECTOR_SOURCE_PROPERTY,
retrieval_query=retrieval_query_window, # NEW!!!
)

# Create a retriever from the vector store
retriever_window = vector_store_window.as_retriever()

# Create a chatbot Question & Answer chain from the retriever
chain_window = RetrievalQAWithSourcesChain.from_chain_type(
ChatOpenAI(temperature=0),
chain_type="stuff",
retriever=retriever_window
)

İki zinciri karşılaştırma

question = "In a single sentence, tell me about Netapp's business."
answer = windowless_chain(
{"question": question},
return_only_outputs=True,
)
print(textwrap.fill(answer["answer"]))

answer = chain_window(
{"question": question},
return_only_outputs=True,
)
print(textwrap.fill(answer["answer"]))

SEC Bilgi Grafiğinin Genişletilmesi

Paketleri içe aktarın ve Neo4j’yi kurun:

from dotenv import load_dotenv
import os
import textwrap

# Langchain
from langchain_community.graphs import Neo4jGraph
from langchain_community.vectorstores import Neo4jVector
from langchain_openai import OpenAIEmbeddings
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain.chains import RetrievalQAWithSourcesChain
from langchain_openai import ChatOpenAI

# Load from environment
load_dotenv('.env', override=True)
NEO4J_URI = os.getenv('NEO4J_URI')
NEO4J_USERNAME = os.getenv('NEO4J_USERNAME')
NEO4J_PASSWORD = os.getenv('NEO4J_PASSWORD')
NEO4J_DATABASE = os.getenv('NEO4J_DATABASE') or 'neo4j'

# Global constants
VECTOR_INDEX_NAME = 'form_10k_chunks'
VECTOR_NODE_LABEL = 'Chunk'
VECTOR_SOURCE_PROPERTY = 'text'
VECTOR_EMBEDDING_PROPERTY = 'textEmbedding'

kg = Neo4jGraph(
url=NEO4J_URI,
username=NEO4J_USERNAME,
password=NEO4J_PASSWORD,
database=NEO4J_DATABASE
)

Form 13 koleksiyonunu okuyun

Yatırım yönetimi firmaları, şirketlere yaptıkları yatırımları Form 13 adı verilen bir belgeyi doldurarak SEC’ye bildirmelidir. NetApp’e yatırım yapan yöneticiler için Form 13'ün bir koleksiyonunu yükleyeceksiniz. Not defterinin üst kısmındaki Dosya menüsünü kullanarak veri dizinine giderek CSV dosyasını teslim alabilirsiniz: [https://learn.deeplearning.ai/courses/knowledge-graphs-rag/lesson/7/expanding-the-sec-knowledge-graph]

import csv

all_form13s = []

with open('./data/form13.csv', mode='r') as csv_file:
csv_reader = csv.DictReader(csv_file)
for row in csv_reader: # each row will be a dictionary
all_form13s.append(row)

İlk 5 Form 13'ün içeriğine bakın:

all_form13s[0:5]

Grafikte Company düğümleri oluşturun

Company düğümleri oluşturmak için Form 13'lerde tanımlanan şirketleri kullanın. Şimdilik tek bir Company var: NetApp.

# work with just the first form fow now
first_form13 = all_form13s[0]

cypher = """
MERGE (com:Company {cusip6: $cusip6})
ON CREATE
SET com.companyName = $companyName,
com.cusip = $cusip
"""

kg.query(cypher, params={
'cusip6':first_form13['cusip6'],
'companyName':first_form13['companyName'],
'cusip':first_form13['cusip']
})

cypher = """
MATCH (com:Company)
RETURN com LIMIT 1
"""

kg.query(cypher)

Şirket adını Form 10-K ile eşleşecek şekilde güncelleyin:

cypher = """
MATCH (com:Company), (form:Form)
WHERE com.cusip6 = form.cusip6
RETURN com.companyName, form.names
"""

kg.query(cypher)

cypher = """
MATCH (com:Company), (form:Form)
WHERE com.cusip6 = form.cusip6
SET com.names = form.names
"""

kg.query(cypher)

Şirket ile Form-10K düğümü arasında bir FILED ilişkisi oluşturun:

kg.query("""
MATCH (com:Company), (form:Form)
WHERE com.cusip6 = form.cusip6
MERGE (com)-[:FILED]->(form)
""")

Yönetici düğümleri oluşturun

NetApp’a yatırımlarını bildirmek üzere Form 13'ü dolduran şirketler için bir manager düğümü oluşturun. Listedeki ilk Form 13'ü dolduran tek yönetici ile başlayın.

cypher = """
MERGE (mgr:Manager {managerCik: $managerParam.managerCik})
ON CREATE
SET mgr.managerName = $managerParam.managerName,
mgr.managerAddress = $managerParam.managerAddress
"""

kg.query(cypher, params={'managerParam': first_form13})

kg.query("""
MATCH (mgr:Manager)
RETURN mgr LIMIT 1
""")

Birden çok yönetici oluşmasını önlemek için bir benzersizlik kısıtı oluşturun.

kg.query("""
CREATE CONSTRAINT unique_manager
IF NOT EXISTS
FOR (n:Manager)
REQUIRE n.managerCik IS UNIQUE
""")

Metin aramasını etkinleştirmek için yönetici adlarının tam metin dizinini oluşturun.

kg.query("""
CREATE FULLTEXT INDEX fullTextManagerNames
IF NOT EXISTS
FOR (mgr:Manager)
ON EACH [mgr.managerName]
""")

kg.query("""
CALL db.index.fulltext.queryNodes("fullTextManagerNames",
"royal bank") YIELD node, score
RETURN node.managerName, score
""")

Form 13 dosyalayan tüm şirketler için düğümler oluşturun:

cypher = """
MERGE (mgr:Manager {managerCik: $managerParam.managerCik})
ON CREATE
SET mgr.managerName = $managerParam.managerName,
mgr.managerAddress = $managerParam.managerAddress
"""
# loop through all Form 13s
for form13 in all_form13s:
kg.query(cypher, params={'managerParam': form13 })

kg.query("""
MATCH (mgr:Manager)
RETURN count(mgr)
""")

Yöneticiler ve şirketler arasında ilişkiler kurun

Form 13'teki verilere dayanarak şirketleri yöneticilerle eşleştirin. Yönetici ve şirket arasında bir OWNS_STOCK_IN ilişkisi oluşturun. Listedeki ilk Form 13'ü dolduran tek yönetici ile başlayın.

cypher = """
MATCH (mgr:Manager {managerCik: $investmentParam.managerCik}),
(com:Company {cusip6: $investmentParam.cusip6})
RETURN mgr.managerName, com.companyName, $investmentParam as investment
"""

kg.query(cypher, params={
'investmentParam': first_form13
})

cypher = """
MATCH (mgr:Manager {managerCik: $ownsParam.managerCik}),
(com:Company {cusip6: $ownsParam.cusip6})
MERGE (mgr)-[owns:OWNS_STOCK_IN {
reportCalendarOrQuarter: $ownsParam.reportCalendarOrQuarter
}]->(com)
ON CREATE
SET owns.value = toFloat($ownsParam.value),
owns.shares = toInteger($ownsParam.shares)
RETURN mgr.managerName, owns.reportCalendarOrQuarter, com.companyName
"""

kg.query(cypher, params={ 'ownsParam': first_form13 })

kg.query("""
MATCH (mgr:Manager {managerCik: $ownsParam.managerCik})
-[owns:OWNS_STOCK_IN]->
(com:Company {cusip6: $ownsParam.cusip6})
RETURN owns { .shares, .value }
""", params={ 'ownsParam': first_form13 })

Form 13'leri dolduran tüm yöneticiler ile şirket arasında ilişkiler kurun:

cypher = """
MATCH (mgr:Manager {managerCik: $ownsParam.managerCik}),
(com:Company {cusip6: $ownsParam.cusip6})
MERGE (mgr)-[owns:OWNS_STOCK_IN {
reportCalendarOrQuarter: $ownsParam.reportCalendarOrQuarter
}]->(com)
ON CREATE
SET owns.value = toFloat($ownsParam.value),
owns.shares = toInteger($ownsParam.shares)
"""

#loop through all Form 13s
for form13 in all_form13s:
kg.query(cypher, params={'ownsParam': form13 })

cypher = """
MATCH (:Manager)-[owns:OWNS_STOCK_IN]->(:Company)
RETURN count(owns) as investments
"""

kg.query(cypher)

kg.refresh_schema()
print(textwrap.fill(kg.schema, 60))

Yatırımcı sayısını belirleyin

Bir form 10-K yığınını bularak başlayın ve sonraki sorgularda kullanmak üzere kaydedin.

cypher = """
MATCH (chunk:Chunk)
RETURN chunk.chunkId as chunkId LIMIT 1
"""

chunk_rows = kg.query(cypher)
print(chunk_rows)

chunk_first_row = chunk_rows[0]
print(chunk_first_row)

ref_chunk_id = chunk_first_row['chunkId']
ref_chunk_id

Form 10-K yığınından şirketlere ve yöneticilere giden bir yol oluşturun.

cypher = """
MATCH (:Chunk {chunkId: $chunkIdParam})-[:PART_OF]->(f:Form)
RETURN f.source
"""

kg.query(cypher, params={'chunkIdParam': ref_chunk_id})

cypher = """
MATCH (:Chunk {chunkId: $chunkIdParam})-[:PART_OF]->(f:Form),
(com:Company)-[:FILED]->(f)
RETURN com.companyName as name
"""

kg.query(cypher, params={'chunkIdParam': ref_chunk_id})

cypher = """
MATCH (:Chunk {chunkId: $chunkIdParam})-[:PART_OF]->(f:Form),
(com:Company)-[:FILED]->(f),
(mgr:Manager)-[:OWNS_STOCK_IN]->(com)
RETURN com.companyName,
count(mgr.managerName) as numberOfinvestors
LIMIT 1
"""

kg.query(cypher, params={
'chunkIdParam': ref_chunk_id
})

LLM için ek bağlam oluşturmak üzere sorguları kullanın

Bir yöneticinin bir şirkete ne kadar hisse senedi yatırdığını belirten cümleler oluşturun.

cypher = """
MATCH (:Chunk {chunkId: $chunkIdParam})-[:PART_OF]->(f:Form),
(com:Company)-[:FILED]->(f),
(mgr:Manager)-[owns:OWNS_STOCK_IN]->(com)
RETURN mgr.managerName + " owns " + owns.shares +
" shares of " + com.companyName +
" at a value of $" +
apoc.number.format(toInteger(owns.value)) AS text
LIMIT 10
"""
kg.query(cypher, params={
'chunkIdParam': ref_chunk_id
})

results = kg.query(cypher, params={
'chunkIdParam': ref_chunk_id
})
print(textwrap.fill(results[0]['text'], 60))

Düz bir Soru-cevap zinciri oluşturun. Yalnızca benzerlik araması yapılmaktadır, Cypher Query ile artırma yoktur.

vector_store = Neo4jVector.from_existing_graph(
embedding=OpenAIEmbeddings(),
url=NEO4J_URI,
username=NEO4J_USERNAME,
password=NEO4J_PASSWORD,
index_name=VECTOR_INDEX_NAME,
node_label=VECTOR_NODE_LABEL,
text_node_properties=[VECTOR_SOURCE_PROPERTY],
embedding_node_property=VECTOR_EMBEDDING_PROPERTY,
)
# Create a retriever from the vector store
retriever = vector_store.as_retriever()

# Create a chatbot Question & Answer chain from the retriever
plain_chain = RetrievalQAWithSourcesChain.from_chain_type(
ChatOpenAI(temperature=0),
chain_type="stuff",
retriever=retriever
)

İkinci bir QA zinciri oluşturun. Yukarıdaki yatırım sorgusu tarafından bulunan cümleleri kullanarak benzerlik aramasını artırın.

investment_retrieval_query = """
MATCH (node)-[:PART_OF]->(f:Form),
(f)<-[:FILED]-(com:Company),
(com)<-[owns:OWNS_STOCK_IN]-(mgr:Manager)
WITH node, score, mgr, owns, com
ORDER BY owns.shares DESC LIMIT 10
WITH collect (
mgr.managerName +
" owns " + owns.shares +
" shares in " + com.companyName +
" at a value of $" +
apoc.number.format(toInteger(owns.value)) + "."
) AS investment_statements, node, score
RETURN apoc.text.join(investment_statements, "\n") +
"\n" + node.text AS text,
score,
{
source: node.source
} as metadata
"""

vector_store_with_investment = Neo4jVector.from_existing_index(
OpenAIEmbeddings(),
url=NEO4J_URI,
username=NEO4J_USERNAME,
password=NEO4J_PASSWORD,
database="neo4j",
index_name=VECTOR_INDEX_NAME,
text_node_property=VECTOR_SOURCE_PROPERTY,
retrieval_query=investment_retrieval_query,
)

# Create a retriever from the vector store
retriever_with_investments = vector_store_with_investment.as_retriever()

# Create a chatbot Question & Answer chain from the retriever
investment_chain = RetrievalQAWithSourcesChain.from_chain_type(
ChatOpenAI(temperature=0),
chain_type="stuff",
retriever=retriever_with_investments
)

Çıktıları karşılaştırın!

question = "In a single sentence, tell me about Netapp."

plain_chain(
{"question": question},
return_only_outputs=True,
)

investment_chain(
{"question": question},
return_only_outputs=True,
)

Soru yatırımcılar hakkında soru sormadığı için LLM yatırımcı bilgilerini kullanmamıştır. Soruyu değiştirin ve tekrar sorun.

question = "In a single sentence, tell me about Netapp investors."

plain_chain(
{"question": question},
return_only_outputs=True,
)

investment_chain(
{"question": question},
return_only_outputs=True,
)

Başka bilgiler almak için yukarıdaki sorguyu değiştirmeyi deneyin. Farklı sorular sormayı deneyin. Cypher sorgusunu değiştirirseniz, retriever ve QA zincirini sıfırlamanız gerekeceğini unutmayın.

SEC Bilgi Grafiği ile Sohbet Etme

Bir bilgi grafiği nasıl oluşturulur?

Minimum uygulanabilir grafik (MVG) ile başlayın, ardından grafiği büyütmek için ayıklayın, geliştirin, genişletin ve tekrarlayın. Çıkarma, ilginç bilgileri tanımlamaktır. Geliştirme, verileri güçlendirmektir. Genişletme, bağlamı genişletmek için bilgileri birbirine bağlamaktır.

Metinden kaynağa MVG
From text to source MVG
SEC formları ile ayıklayın, geliştirin ve genişletin

Birbirinden bahseden şirketleri çapraz bağlayarak; metinden çıkarılan kişileri, yerleri ve konuları ekleyerek, daha fazla form verisi veya ilgili veri kaynağı ekleyerek ve alaka düzeyini iyileştirmek ve geri bildirim sağlamak için kullanıcılar ekleyerek bilgi grafiğini büyütmeye devam edebilirsiniz.

Extract (:Adresses) Düğümler:

  1. Tam adres dizelerini ayıklayın.
  2. Şehir, eyalet ve ülkenin yanı sıra yükseklik ve boylamı elde etmek için coğrafi kodlama yapın.

Jeo-uzamsal indeks ile geliştirin

  1. Mesafe sorgularını etkinleştirin.

Genişlet -[:LOCATED_AT]->ilişkiler

  1. (:Yönetici), (:Şirket) düğümlerini (:Adres)’e bağlayın.
Konum soruları sorun

Paketleri içe aktarın ve Neo4j’yi kurun

from dotenv import load_dotenv
import os

import textwrap

# Langchain
from langchain_community.graphs import Neo4jGraph
from langchain_community.vectorstores import Neo4jVector
from langchain_openai import OpenAIEmbeddings
from langchain.chains import RetrievalQAWithSourcesChain
from langchain.prompts.prompt import PromptTemplate
from langchain.chains import GraphCypherQAChain
from langchain_openai import ChatOpenAI

# Load from environment
load_dotenv('.env', override=True)
NEO4J_URI = os.getenv('NEO4J_URI')
NEO4J_USERNAME = os.getenv('NEO4J_USERNAME')
NEO4J_PASSWORD = os.getenv('NEO4J_PASSWORD')
NEO4J_DATABASE = os.getenv('NEO4J_DATABASE') or 'neo4j'
OPENAI_API_KEY = os.getenv('OPENAI_API_KEY')

# Note the code below is unique to this course environment, and not a
# standard part of Neo4j's integration with OpenAI. Remove if running
# in your own environment.
OPENAI_ENDPOINT = os.getenv('OPENAI_BASE_URL') + '/embeddings'

# Global constants
VECTOR_INDEX_NAME = 'form_10k_chunks'
VECTOR_NODE_LABEL = 'Chunk'
VECTOR_SOURCE_PROPERTY = 'text'
VECTOR_EMBEDDING_PROPERTY = 'textEmbedding'

kg = Neo4jGraph(
url=NEO4J_URI, username=NEO4J_USERNAME, password=NEO4J_PASSWORD, database=NEO4J_DATABASE
)

Güncellenmiş SEC belgeleri grafiğini keşfedin

Videoda tartışılan adres bilgilerini de içeren güncellenmiş bir grafikle çalışalım.

kg.refresh_schema()
print(textwrap.fill(kg.schema, 60))

Rastgele bir Manager’ın adresini kontrol edin. Aşağıdaki sorgu tarafından döndürülen şirket videodakinden farklı olabilir.

kg.query("""
MATCH (mgr:Manager)-[:LOCATED_AT]->(addr:Address)
RETURN mgr, addr
LIMIT 1
""")

Royal Bank adlı bir manager için tam metin araması.

kg.query("""
CALL db.index.fulltext.queryNodes(
"fullTextManagerNames",
"royal bank") YIELD node, score
RETURN node.managerName, score LIMIT 1
""")

Royal Bank’ın adresini bulun.

kg.query("""
CALL db.index.fulltext.queryNodes(
"fullTextManagerNames",
"royal bank"
) YIELD node, score
WITH node as mgr LIMIT 1
MATCH (mgr:Manager)-[:LOCATED_AT]->(addr:Address)
RETURN mgr.managerName, addr
""")

Hangi eyalette en çok yatırım şirketi olduğunu belirleyin.

kg.query("""
MATCH p=(:Manager)-[:LOCATED_AT]->(address:Address)
RETURN address.state as state, count(address.state) as numManagers
ORDER BY numManagers DESC
LIMIT 10
""")

Hangi eyalette en çok yatırım şirketi olduğunu belirleyin.

kg.query("""
MATCH p=(:Company)-[:LOCATED_AT]->(address:Address)
RETURN address.state as state, count(address.state) as numCompanies
ORDER BY numCompanies DESC
""")

Kaliforniya’da en çok yatırım firmasının bulunduğu şehirler hangileridir?

kg.query("""
MATCH p=(:Manager)-[:LOCATED_AT]->(address:Address)
WHERE address.state = 'California'
RETURN address.city as city, count(address.city) as numManagers
ORDER BY numManagers DESC
LIMIT 10
""")

Kaliforniya’da en çok şirketin listelendiği şehir hangisidir?

kg.query("""
MATCH p=(:Company)-[:LOCATED_AT]->(address:Address)
WHERE address.state = 'California'
RETURN address.city as city, count(address.city) as numCompanies
ORDER BY numCompanies DESC
""")

San Francisco’daki en iyi yatırım firmaları hangileridir?

kg.query("""
MATCH p=(mgr:Manager)-[:LOCATED_AT]->(address:Address),
(mgr)-[owns:OWNS_STOCK_IN]->(:Company)
WHERE address.city = "San Francisco"
RETURN mgr.managerName, sum(owns.value) as totalInvestmentValue
ORDER BY totalInvestmentValue DESC
LIMIT 10
""")

Santa Clara’da hangi şirketler var?

kg.query("""
MATCH (com:Company)-[:LOCATED_AT]->(address:Address)
WHERE address.city = "Santa Clara"
RETURN com.companyName
""")

Santa Clara yakınlarında hangi şirketler var?

kg.query("""
MATCH (sc:Address)
WHERE sc.city = "Santa Clara"
MATCH (com:Company)-[:LOCATED_AT]->(comAddr:Address)
WHERE point.distance(sc.location, comAddr.location) < 10000
RETURN com.companyName, com.companyAddress
""")

Santa Clara yakınlarında hangi yatırım şirketleri var?

Arama yarıçapını genişletmek için sorgudaki mesafeyi güncellemeyi deneyelim.

kg.query("""
MATCH (address:Address)
WHERE address.city = "Santa Clara"
MATCH (mgr:Manager)-[:LOCATED_AT]->(managerAddress:Address)
WHERE point.distance(address.location,
managerAddress.location) < 10000
RETURN mgr.managerName, mgr.managerAddress
""")

Palo Alto Networks yakınlarında hangi yatırım şirketleri var?

Tam metin aramanın yazım hatalarını işleyebileceğini unutmayın!

# Which investment firms are near Palo Aalto Networks?
kg.query("""
CALL db.index.fulltext.queryNodes(
"fullTextCompanyNames",
"Palo Aalto Networks"
) YIELD node, score
WITH node as com
MATCH (com)-[:LOCATED_AT]->(comAddress:Address),
(mgr:Manager)-[:LOCATED_AT]->(mgrAddress:Address)
WHERE point.distance(comAddress.location,
mgrAddress.location) < 10000
RETURN mgr,
toInteger(point.distance(comAddress.location,
mgrAddress.location) / 1000) as distanceKm
ORDER BY distanceKm ASC
LIMIT 10
""")

Cypher hakkında daha fazla bilgiyi neo4j web sitesinden edinebilirsiniz: [https://neo4j.com/product/cypher-graph-query-language/]

LLM ile Cypher Yazmak

Bir LLM’e Cypher yazmayı öğretmek için birkaç atışlık öğrenmeyi kullanalım. OpenAI’nin GPT 3.5 modelini kullanacağız. Ayrıca LangChain içinde GraphCypherQAChain adlı yeni bir Neo4j entegrasyonu kullanacağız.

CYPHER_GENERATION_TEMPLATE = """Task:Generate Cypher statement to 
query a graph database.
Instructions:
Use only the provided relationship types and properties in the
schema. Do not use any other relationship types or properties that
are not provided.
Schema:
{schema}
Note: Do not include any explanations or apologies in your responses.
Do not respond to any questions that might ask anything else than
for you to construct a Cypher statement.
Do not include any text except the generated Cypher statement.
Examples: Here are a few examples of generated Cypher
statements for particular questions:

# What investment firms are in San Francisco?
MATCH (mgr:Manager)-[:LOCATED_AT]->(mgrAddress:Address)
WHERE mgrAddress.city = 'San Francisco'
RETURN mgr.managerName
The question is:
{question}"""

CYPHER_GENERATION_PROMPT = PromptTemplate(
input_variables=["schema", "question"],
template=CYPHER_GENERATION_TEMPLATE
)

cypherChain = GraphCypherQAChain.from_llm(
ChatOpenAI(temperature=0),
graph=kg,
verbose=True,
cypher_prompt=CYPHER_GENERATION_PROMPT,
)

def prettyCypherChain(question: str) -> str:
response = cypherChain.run(question)
print(textwrap.fill(response, 60))

prettyCypherChain("What investment firms are in San Francisco?")

prettyCypherChain("What investment firms are in Menlo Park?")

prettyCypherChain("What companies are in Santa Clara?")

prettyCypherChain("What investment firms are near Santa Clara?")

LLM’ye yeni Cypher kalıplarını öğretmek için istemi genişletin

CYPHER_GENERATION_TEMPLATE = """Task:Generate Cypher statement to query a graph database.
Instructions:
Use only the provided relationship types and properties in the schema.
Do not use any other relationship types or properties that are not provided.
Schema:
{schema}
Note: Do not include any explanations or apologies in your responses.
Do not respond to any questions that might ask anything else than for you to construct a Cypher statement.
Do not include any text except the generated Cypher statement.
Examples: Here are a few examples of generated Cypher statements for particular questions:

# What investment firms are in San Francisco?
MATCH (mgr:Manager)-[:LOCATED_AT]->(mgrAddress:Address)
WHERE mgrAddress.city = 'San Francisco'
RETURN mgr.managerName

# What investment firms are near Santa Clara?
MATCH (address:Address)
WHERE address.city = "Santa Clara"
MATCH (mgr:Manager)-[:LOCATED_AT]->(managerAddress:Address)
WHERE point.distance(address.location,
managerAddress.location) < 10000
RETURN mgr.managerName, mgr.managerAddress

The question is:
{question}"""

Cypher oluşturma istemini yeni bir şablonla güncelleyin ve Cypher zincirini yeni istemi kullanacak şekilde yeniden başlatın. Cypher oluşturma şablonunda bir değişiklik yaptığınızda bu kodu yeniden çalıştırın!

CYPHER_GENERATION_PROMPT = PromptTemplate(
input_variables=["schema", "question"],
template=CYPHER_GENERATION_TEMPLATE
)

cypherChain = GraphCypherQAChain.from_llm(
ChatOpenAI(temperature=0),
graph=kg,
verbose=True,
cypher_prompt=CYPHER_GENERATION_PROMPT,
)

prettyCypherChain("What investment firms are near Santa Clara?")

Form 10K yığınlarından bilgi almak için sorguyu genişletin

CYPHER_GENERATION_TEMPLATE = """Task:Generate Cypher statement to query a graph database.
Instructions:
Use only the provided relationship types and properties in the schema.
Do not use any other relationship types or properties that are not provided.
Schema:
{schema}
Note: Do not include any explanations or apologies in your responses.
Do not respond to any questions that might ask anything else than for you to construct a Cypher statement.
Do not include any text except the generated Cypher statement.
Examples: Here are a few examples of generated Cypher statements for particular questions:

# What investment firms are in San Francisco?
MATCH (mgr:Manager)-[:LOCATED_AT]->(mgrAddress:Address)
WHERE mgrAddress.city = 'San Francisco'
RETURN mgr.managerName

# What investment firms are near Santa Clara?
MATCH (address:Address)
WHERE address.city = "Santa Clara"
MATCH (mgr:Manager)-[:LOCATED_AT]->(managerAddress:Address)
WHERE point.distance(address.location,
managerAddress.location) < 10000
RETURN mgr.managerName, mgr.managerAddress

# What does Palo Alto Networks do?
CALL db.index.fulltext.queryNodes(
"fullTextCompanyNames",
"Palo Alto Networks"
) YIELD node, score
WITH node as com
MATCH (com)-[:FILED]->(f:Form),
(f)-[s:SECTION]->(c:Chunk)
WHERE s.f10kItem = "item1"
RETURN c.text

The question is:
{question}"""

CYPHER_GENERATION_PROMPT = PromptTemplate(
input_variables=["schema", "question"],
template=CYPHER_GENERATION_TEMPLATE
)

cypherChain = GraphCypherQAChain.from_llm(
ChatOpenAI(temperature=0),
graph=kg,
verbose=True,
cypher_prompt=CYPHER_GENERATION_PROMPT,
)

prettyCypherChain("What does Palo Alto Networks do?")

Grafik hakkında farklı sorular sormak için aşağıdaki Cypher oluşturma istemini güncelleyebilirsiniz. Grafik yapısını hatırlatmak için “check schema” kısmını çalıştırabilirsiniz.

# Check the graph schema
kg.refresh_schema()
print(textwrap.fill(kg.schema, 60))

CYPHER_GENERATION_TEMPLATE = """Task:Generate Cypher statement to query a graph database.
Instructions:
Use only the provided relationship types and properties in the schema.
Do not use any other relationship types or properties that are not provided.
Schema:
{schema}
Note: Do not include any explanations or apologies in your responses.
Do not respond to any questions that might ask anything else than for you to construct a Cypher statement.
Do not include any text except the generated Cypher statement.
Examples: Here are a few examples of generated Cypher statements for particular questions:

# What investment firms are in San Francisco?
MATCH (mgr:Manager)-[:LOCATED_AT]->(mgrAddress:Address)
WHERE mgrAddress.city = 'San Francisco'
RETURN mgr.managerName

# What investment firms are near Santa Clara?
MATCH (address:Address)
WHERE address.city = "Santa Clara"
MATCH (mgr:Manager)-[:LOCATED_AT]->(managerAddress:Address)
WHERE point.distance(address.location,
managerAddress.location) < 10000
RETURN mgr.managerName, mgr.managerAddress

# What does Palo Alto Networks do?
CALL db.index.fulltext.queryNodes(
"fullTextCompanyNames",
"Palo Alto Networks"
) YIELD node, score
WITH node as com
MATCH (com)-[:FILED]->(f:Form),
(f)-[s:SECTION]->(c:Chunk)
WHERE s.f10kItem = "item1"
RETURN c.text

The question is:
{question}"""

# Update the prompt and reset the QA chain
CYPHER_GENERATION_PROMPT = PromptTemplate(
input_variables=["schema", "question"],
template=CYPHER_GENERATION_TEMPLATE
)

cypherChain = GraphCypherQAChain.from_llm(
ChatOpenAI(temperature=0),
graph=kg,
verbose=True,
cypher_prompt=CYPHER_GENERATION_PROMPT,
)

prettyCypherChain("<<REPLACE WITH YOUR QUESTION>>")

Kaynaklar

[1] Deeplearningai, (April 2024), Knowledge Graphs for RAG:

[https://learn.deeplearning.ai/courses/knowledge-graphs-rag]

--

--

Cahit Barkin Ozer

Daha fazla şey öğrenmek ve daha iyi olmak isteyen bir yazılım mühendisi.