Neo4j x LangChain: Deep dive into the new Vector index implementation

Neo4j x LangChain:深入了解新的向量索引实现

6 分钟阅读

了解如何自定义 LangChain 的 Neo4j 向量索引封装器

编者按:这篇文章是与 Neo4j 团队合作撰写的。我们一直在与他们紧密合作开发他们的新向量索引,并且我们对其高效地对非结构化文本或其他嵌入式数据模式执行语义搜索的能力印象深刻,从而为 RAG 应用和更多自定义解锁了支持。

Neo4j 过去和现在都非常适合处理结构化信息,但由于其暴力方法,在语义搜索方面有点吃力。然而,这种吃力已成为过去,因为 Neo4j 在 5.11 版本中引入了新的向量索引,旨在高效地对非结构化文本或其他嵌入式数据模式执行语义搜索。新添加的向量索引使 Neo4j 非常适合大多数 RAG 应用,因为它现在可以很好地处理结构化和非结构化数据。

作者提供的图片。

这篇博文旨在引导您了解 LangChain 中 Neo4j 向量索引实现的所有自定义选项。

代码可在 GitHub 上找到。

Neo4j 环境设置

您需要设置 Neo4j 5.11 或更高版本才能按照这篇博文中的示例进行操作。最简单的方法是在 Neo4j Aura 上启动一个免费实例,它提供 Neo4j 数据库的云实例。或者,您也可以通过下载 Neo4j Desktop 应用程序并创建一个本地数据库实例来设置 Neo4j 数据库的本地实例。

示例数据集

为了这篇博文的目的,我们将使用 WikipediaLoader 从 Witcher 页面获取文本。

from langchain.document_loaders import WikipediaLoader
from langchain.text_splitter import CharacterTextSplitter

# Read the wikipedia article
raw_documents = WikipediaLoader(query="The Witcher").load()
# Define chunking strategy
text_splitter = CharacterTextSplitter.from_tiktoken_encoder(
    chunk_size=1000, chunk_overlap=20
)
# Chunk the document
documents = text_splitter.split_documents(raw_documents)
# Remove the summary
for d in documents:
    del d.metadata["summary"]

Neo4j 向量索引自定义

每个文本块在 Neo4j 中都存储为单个孤立节点。

导入文档的图模式。

默认情况下,LangChain 中的 Neo4j 向量索引实现使用 Chunk 节点标签表示文档,其中 text 属性存储文档的文本,embedding 属性保存文本的向量表示。该实现允许您自定义节点标签、文本和嵌入属性名称。

neo4j_db = Neo4jVector.from_documents(
    documents,
    OpenAIEmbeddings(),
    url=url,
    username=username,
    password=password,
    database="neo4j",  # neo4j by default
    index_name="wikipedia",  # vector by default
    node_label="WikipediaArticle",  # Chunk by default
    text_node_property="info",  # text by default
    embedding_node_property="vector",  # embedding by default
    create_id_index=True,  # True by default
)

在本示例中,我们指定要将文本块存储在 WikipediaArticle 节点标签下,其中 info 属性用于存储文本,vector 属性保存文本嵌入表示。如果您运行上面的示例,您应该在数据库中看到以下信息。

节点信息。

如前所述,我们将 info 属性定义为包含文本信息,而 vector 属性用于存储嵌入。任何其他属性(如 sourcetitle)都被视为文档元数据。

默认情况下,我们还在指定节点标签的 id 属性上创建一个 唯一节点属性约束,以加快导入速度。如果您不想创建唯一约束,可以将 create_id_index 设置为 false。您可以使用以下 Cypher 语句验证约束是否已创建

neo4j_db.query("SHOW CONSTRAINTS")
#[{'id': 4,
#  'name': 'constraint_e5da4d45',
#  'type': 'UNIQUENESS',
#  'entityType': 'NODE',
#  'labelsOrTypes': ['WikipediaArticle'],
#  'properties': ['id'],
#  'ownedIndex': 'constraint_e5da4d45',
#  'propertyType': None}]

正如您所预料的那样,我们还创建了一个向量索引,这将使我们能够执行快速的 ANN 搜索。

neo4j_db.query(
    """SHOW INDEXES
       YIELD name, type, labelsOrTypes, properties, options
       WHERE type = 'VECTOR'
    """
)
#[{'name': 'wikipedia',
#  'type': 'VECTOR',
#  'labelsOrTypes': ['WikipediaArticle'],
#  'properties': ['vector'],
#  'options': {'indexProvider': 'vector-1.0',
#   'indexConfig': {'vector.dimensions': 1536,
#    'vector.similarity_function': 'cosine'}}}]

LangChain 实现创建了一个名为 wikipedia 的向量索引,该索引对 WikipediaArticle 节点的 vector 属性进行索引。此外,提供的配置告知我们,向量嵌入维度为 1536,并使用 cosine 相似度函数。

加载更多文档

您可以使用 add_documents 方法将更多文档加载到实例化的向量索引中。

neo4j_db.add_documents(
    [
        Document(
            page_content="LangChain is the coolest library since the Library of Alexandria",
            metadata={"author": "Tomaz", "confidence": 1.0}
        )
    ],
    ids=["langchain"],
)

LangChain 允许您为 add_document 方法提供文档 ID,这可以用于跨不同系统同步信息,并使更新或删除相关文本块变得更容易。

加载现有索引

如果您在 Neo4j 中有一个包含数据的现有向量索引,则可以使用 from_existing_method 连接到它。

existing_index = Neo4jVector.from_existing_index(
    OpenAIEmbeddings(),
    url=url,
    username=username,
    password=password,
    index_name="wikipedia",
    text_node_property="info",  # Need to define if it is not default
)

首先,from_existing_method 检查数据库中是否实际存在具有提供名称的索引。如果存在,它可以从索引配置映射中检索节点标签和嵌入节点属性,这意味着您不必手动设置这些。

print(existing_index.node_label) # WikipediaArticle
print(existing_index.embedding_node_property) # vector

但是,索引信息不包含文本节点属性信息。因此,如果您使用默认属性 (text) 以外的任何属性,请使用 text_node_property 参数指定它。

自定义检索查询

由于 Neo4j 是原生图形数据库,LangChain 中的向量索引实现允许自定义和丰富返回的信息。但是,此功能适用于更高级的用户,因为您负责自定义数据加载以及检索。

retrieval_query 参数允许您收集、转换或计算要从相似性搜索返回的任何其他图形信息。为了更好地理解它,我们可以查看代码中的实际实现。

read_query = (
    "CALL db.index.vector.queryNodes($index, $k, $embedding) "
    "YIELD node, score "
) + retrieval_query

从代码中,我们可以看到向量相似性搜索是硬编码的。但是,我们可以选择添加任何中间步骤并返回其他信息。检索查询必须返回以下三列

  • text (字符串):这通常是与已检索节点关联的文本数据。这可以是节点的主要内容、名称、描述或任何其他基于文本的信息。
  • score (浮点数):这表示查询向量与返回节点关联的向量之间的相似度得分。该分数量化查询与返回节点的相似程度,通常在 0 到 1 的范围内
  • metadata (字典):这是一个更灵活的列,可以包含有关节点或搜索的其他信息。它可以是一个字典(或映射),其中包含各种属性或属性,这些属性或属性为返回的节点提供更多上下文。

我们将向 WikipediaArticle 节点添加关系以演示此功能。

existing_index.query(
    """MATCH (w:WikipediaArticle {id:'langchain'})
       MERGE (w)<-[:EDITED_BY]-(:Person {name:"Galileo"})
    """
)

我们已向具有给定 ID 的 WikipediaArticle 节点添加了 EDITED_BY 关系。现在让我们测试一下自定义检索选项。

retrieval_query = """
OPTIONAL MATCH (node)<-[:EDITED_BY]-(p)
WITH node, score, collect(p) AS editors
RETURN node.info AS text,
       score, 
       node {.*, vector: Null, info: Null, editors: editors} AS metadata
"""

existing_index_return = Neo4jVector.from_existing_index(
    OpenAIEmbeddings(),
    url=url,
    username=username,
    password=password,
    database="neo4j",
    index_name="wikipedia",
    text_node_property="info",
    retrieval_query=retrieval_query,
)

我不会过多介绍 Cypher 的细节。您可以使用许多资源来学习基本语法以及更多内容,例如 Neo4j Graph Academy。要构建有效的检索查询,您必须知道来自向量相似性搜索的相关节点在 node 引用变量下可用,而相似性度量值在 score 引用下可用。

让我们尝试一下。

existing_index_return.similarity_search(
    "What do you know about LangChain?", k=1)

#[
#   Document("page_content=""LangChain is the coolest library since the Library of Alexandria",
#   "metadata="{
#      "author":"Tomaz",
#      "confidence":1.0,
#      "id":"langchain",
#      "editors":[
#         {
#            "name":"Galileo"
#         }
#      ]
#   }")"
#]

您可以观察到元数据信息包含 editor 属性,该属性是从图形信息计算得出的。

总结

Neo4j 中新添加的向量索引实现使其能够支持依赖于结构化和非结构化数据的 RAG 应用,使其非常适合高度复杂和关联的数据集。

代码可在 GitHub 上找到。