Skip to main content
Neo4j 是一个开源图数据库,集成了对向量相似性搜索的支持。
它支持:
  • 近似最近邻搜索
  • 欧几里得相似性和余弦相似性
  • 结合向量和关键词搜索的混合搜索
本笔记本展示了如何使用 Neo4j 向量索引 (Neo4jVector)。 请参阅安装说明
# Pip 安装必要的包
pip install -qU  neo4j
pip install -qU  langchain-openai langchain-neo4j
pip install -qU  tiktoken
我们希望使用 OpenAIEmbeddings,因此需要获取 OpenAI API 密钥。
import getpass
import os

if "OPENAI_API_KEY" not in os.environ:
    os.environ["OPENAI_API_KEY"] = getpass.getpass("OpenAI API Key:")
OpenAI API Key: ········
from langchain_community.document_loaders import TextLoader
from langchain_core.documents import Document
from langchain_neo4j import Neo4jVector
from langchain_openai import OpenAIEmbeddings
from langchain_text_splitters import CharacterTextSplitter
loader = TextLoader("../../how_to/state_of_the_union.txt")

documents = loader.load()
text_splitter = CharacterTextSplitter(chunk_size=1000, chunk_overlap=0)
docs = text_splitter.split_documents(documents)

embeddings = OpenAIEmbeddings()
# Neo4jVector 需要 Neo4j 数据库凭据

url = "bolt://localhost:7687"
username = "neo4j"
password = "password"

# 你也可以使用环境变量而不是直接传递命名参数
# os.environ["NEO4J_URI"] = "bolt://localhost:7687"
# os.environ["NEO4J_USERNAME"] = "neo4j"
# os.environ["NEO4J_PASSWORD"] = "pleaseletmein"

使用余弦距离的相似性搜索(默认)

# Neo4jVector 模块将连接到 Neo4j,并在需要时创建向量索引。

db = Neo4jVector.from_documents(
    docs, OpenAIEmbeddings(), url=url, username=username, password=password
)
query = "总统对凯坦吉·布朗·杰克逊说了什么"
docs_with_score = db.similarity_search_with_score(query, k=2)
for doc, score in docs_with_score:
    print("-" * 80)
    print("Score: ", score)
    print(doc.page_content)
    print("-" * 80)
--------------------------------------------------------------------------------
Score:  0.9076391458511353
今晚。我呼吁参议院:通过《自由投票法案》。通过《约翰·刘易斯投票权法案》。并且,趁此机会,通过《披露法案》,以便美国人能够知道谁在资助我们的选举。

今晚,我想向一位毕生致力于服务这个国家的人致敬:斯蒂芬·布雷耶大法官——一位陆军退伍军人、宪法学者,以及即将退休的美国最高法院大法官。布雷耶大法官,感谢您的服务。

总统最严肃的宪法职责之一是提名某人在美国最高法院任职。

而我四天前就做了这件事,我提名了巡回上诉法院法官凯坦吉·布朗·杰克逊。她是我国顶尖的法律头脑之一,将继续布雷耶大法官卓越的遗产。
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
Score:  0.8912242650985718
一位前私人执业顶级诉讼律师。一位前联邦公设辩护人。并且来自一个公立学校教育工作者和警察的家庭。一位共识建立者。自从她被提名以来,她获得了广泛的支持——从警察兄弟会到民主党和共和党任命的前法官。

如果我们想要推进自由和正义,我们需要确保边境安全并修复移民系统。

我们可以两者兼顾。在我们的边境,我们安装了新技术,如尖端扫描仪,以更好地检测毒品走私。

我们与墨西哥和危地马拉建立了联合巡逻队,以抓捕更多的人口贩子。

我们正在设立专门的移民法官,以便逃离迫害和暴力的家庭能够更快地审理他们的案件。

我们正在确保承诺并支持南美洲和中美洲的合作伙伴,以收容更多难民并确保他们自己的边境安全。
--------------------------------------------------------------------------------

使用向量存储

上面,我们从头开始创建了一个向量存储。然而,很多时候我们希望使用现有的向量存储。 为此,我们可以直接初始化它。
index_name = "vector"  # 默认索引名称

store = Neo4jVector.from_existing_index(
    OpenAIEmbeddings(),
    url=url,
    username=username,
    password=password,
    index_name=index_name,
)
我们也可以使用 from_existing_graph 方法从现有图初始化向量存储。此方法从数据库中提取相关的文本信息,计算并将文本嵌入存储回数据库。
# 首先,我们在图中创建示例数据
store.query(
    "CREATE (p:Person {name: 'Tomaz', location:'Slovenia', hobby:'Bicycle', age: 33})"
)
[]
# 现在我们从现有图初始化
existing_graph = Neo4jVector.from_existing_graph(
    embedding=OpenAIEmbeddings(),
    url=url,
    username=username,
    password=password,
    index_name="person_index",
    node_label="Person",
    text_node_properties=["name", "location"],
    embedding_node_property="embedding",
)
result = existing_graph.similarity_search("Slovenia", k=1)
result[0]
Document(page_content='\nname: Tomaz\nlocation: Slovenia', metadata={'age': 33, 'hobby': 'Bicycle'})
Neo4j 还支持关系向量索引,其中嵌入作为关系属性存储并被索引。关系向量索引无法通过 LangChain 填充,但你可以将其连接到现有的关系向量索引。
# 首先,我们在图中创建示例数据和索引
store.query(
    "MERGE (p:Person {name: 'Tomaz'}) "
    "MERGE (p1:Person {name:'Leann'}) "
    "MERGE (p1)-[:FRIEND {text:'example text', embedding:$embedding}]->(p2)",
    params={"embedding": OpenAIEmbeddings().embed_query("example text")},
)
# 创建一个向量索引
relationship_index = "relationship_vector"
store.query(
    """
CREATE VECTOR INDEX $relationship_index
IF NOT EXISTS
FOR ()-[r:FRIEND]-() ON (r.embedding)
OPTIONS {indexConfig: {
 `vector.dimensions`: 1536,
 `vector.similarity_function`: 'cosine'
}}
""",
    params={"relationship_index": relationship_index},
)
[]
relationship_vector = Neo4jVector.from_existing_relationship_index(
    OpenAIEmbeddings(),
    url=url,
    username=username,
    password=password,
    index_name=relationship_index,
    text_node_property="text",
)
relationship_vector.similarity_search("Example")
[Document(page_content='example text')]

元数据过滤

Neo4j 向量存储还支持通过结合并行运行时和精确最近邻搜索进行元数据过滤。 需要 Neo4j 5.18 或更高版本。 相等过滤具有以下语法。
existing_graph.similarity_search(
    "Slovenia",
    filter={"hobby": "Bicycle", "name": "Tomaz"},
)
[Document(page_content='\nname: Tomaz\nlocation: Slovenia', metadata={'age': 33, 'hobby': 'Bicycle'})]
元数据过滤还支持以下运算符:
  • $eq: 等于
  • $ne: 不等于
  • $lt: 小于
  • $lte: 小于或等于
  • $gt: 大于
  • $gte: 大于或等于
  • $in: 在值列表中
  • $nin: 不在值列表中
  • $between: 在两个值之间
  • $like: 文本包含值
  • $ilike: 小写文本包含值
existing_graph.similarity_search(
    "Slovenia",
    filter={"hobby": {"$eq": "Bicycle"}, "age": {"$gt": 15}},
)
[Document(page_content='\nname: Tomaz\nlocation: Slovenia', metadata={'age': 33, 'hobby': 'Bicycle'})]
你也可以在过滤器之间使用 OR 运算符
existing_graph.similarity_search(
    "Slovenia",
    filter={"$or": [{"hobby": {"$eq": "Bicycle"}}, {"age": {"$gt": 15}}]},
)
[Document(page_content='\nname: Tomaz\nlocation: Slovenia', metadata={'age': 33, 'hobby': 'Bicycle'})]

添加文档

我们可以向现有的向量存储添加文档。
store.add_documents([Document(page_content="foo")])
['acbd18db4cc2f85cedef654fccc4a4d8']
docs_with_score = store.similarity_search_with_score("foo")
docs_with_score[0]
(Document(page_content='foo'), 0.9999997615814209)

使用检索查询自定义响应

你也可以通过使用自定义 Cypher 代码片段来从图中获取其他信息,从而自定义响应。 在底层,最终的 Cypher 语句是这样构建的:
read_query = (
  "CALL db.index.vector.queryNodes($index, $k, $embedding) "
  "YIELD node, score "
) + retrieval_query
检索查询必须返回以下三列:
  • text: Union[str, Dict] = 用于填充文档 page_content 的值
  • score: Float = 相似性分数
  • metadata: Dict = 文档的附加元数据
在此博客文章中了解更多。
retrieval_query = """
RETURN "Name:" + node.name AS text, score, {foo:"bar"} AS metadata
"""
retrieval_example = Neo4jVector.from_existing_index(
    OpenAIEmbeddings(),
    url=url,
    username=username,
    password=password,
    index_name="person_index",
    retrieval_query=retrieval_query,
)
retrieval_example.similarity_search("Foo", k=1)
[Document(page_content='Name:Tomaz', metadata={'foo': 'bar'})]
这是一个将除 embedding 之外的所有节点属性作为字典传递给 text 列的示例,
retrieval_query = """
RETURN node {.name, .age, .hobby} AS text, score, {foo:"bar"} AS metadata
"""
retrieval_example = Neo4jVector.from_existing_index(
    OpenAIEmbeddings(),
    url=url,
    username=username,
    password=password,
    index_name="person_index",
    retrieval_query=retrieval_query,
)
retrieval_example.similarity_search("Foo", k=1)
[Document(page_content='name: Tomaz\nage: 33\nhobby: Bicycle\n', metadata={'foo': 'bar'})]
你也可以将 Cypher 参数传递给检索查询。 参数可用于额外的过滤、遍历等…
retrieval_query = """
RETURN node {.*, embedding:Null, extra: $extra} AS text, score, {foo:"bar"} AS metadata
"""
retrieval_example = Neo4jVector.from_existing_index(
    OpenAIEmbeddings(),
    url=url,
    username=username,
    password=password,
    index_name="person_index",
    retrieval_query=retrieval_query,
)
retrieval_example.similarity_search("Foo", k=1, params={"extra": "ParamInfo"})
[Document(page_content='location: Slovenia\nextra: ParamInfo\nname: Tomaz\nage: 33\nhobby: Bicycle\nembedding: None\n', metadata={'foo': 'bar'})]

混合搜索(向量 + 关键词)

Neo4j 集成了向量和关键词索引,这允许你使用混合搜索方法
# Neo4jVector 模块将连接到 Neo4j,并在需要时创建向量和关键词索引。
hybrid_db = Neo4jVector.from_documents(
    docs,
    OpenAIEmbeddings(),
    url=url,
    username=username,
    password=password,
    search_type="hybrid",
)
要从现有索引加载混合搜索,你必须同时提供向量和关键词索引
index_name = "vector"  # 默认索引名称
keyword_index_name = "keyword"  # 默认关键词索引名称

store = Neo4jVector.from_existing_index(
    OpenAIEmbeddings(),
    url=url,
    username=username,
    password=password,
    index_name=index_name,
    keyword_index_name=keyword_index_name,
    search_type="hybrid",
)

检索器选项

本节展示了如何将 Neo4jVector 用作检索器。
retriever = store.as_retriever()
retriever.invoke(query)[0]
Document(page_content='今晚。我呼吁参议院:通过《自由投票法案》。通过《约翰·刘易斯投票权法案》。并且,趁此机会,通过《披露法案》,以便美国人能够知道谁在资助我们的选举。 \n\n今晚,我想向一位毕生致力于服务这个国家的人致敬:斯蒂芬·布雷耶大法官——一位陆军退伍军人、宪法学者,以及即将退休的美国最高法院大法官。布雷耶大法官,感谢您的服务。 \n\n总统最严肃的宪法职责之一是提名某人在美国最高法院任职。 \n\n而我四天前就做了这件事,我提名了巡回上诉法院法官凯坦吉·布朗·杰克逊。她是我国顶尖的法律头脑之一,将继续布雷耶大法官卓越的遗产。', metadata={'source': '../../how_to/state_of_the_union.txt'})

带来源的问答

本节介绍了如何使用索引进行带来源的问答。它通过使用 RetrievalQAWithSourcesChain 来实现,该链从索引中查找文档。
from langchain_classic.chains import RetrievalQAWithSourcesChain
from langchain_openai import ChatOpenAI
chain = RetrievalQAWithSourcesChain.from_chain_type(
    ChatOpenAI(temperature=0), chain_type="stuff", retriever=retriever
)
chain.invoke(
    {"question": "总统对布雷耶大法官说了什么"},
    return_only_outputs=True,
)
{'answer': '总统表彰了斯蒂芬·布雷耶大法官对国家的服务,并提到了他从美国最高法院退休。\n',
 'sources': '../../how_to/state_of_the_union.txt'}