在Neo4j中高效地对非结构化文本进行语义搜索

在Neo4j中高效语义搜索非结构化文本

将新添加的向量索引集成到LangChain中,增强您的RAG应用

自六个月前ChatGPT问世以来,技术领域发生了一场变革性的转变。ChatGPT在泛化能力方面表现出色,减少了创建定制的自然语言处理模型所需的专业深度学习团队和大量训练数据集的要求。这使得各种NLP任务,如摘要和信息提取,比以往更易于获得。然而,我们很快意识到ChatGPT等模型的局限性,例如知识日期截止和无法访问私人信息。在我看来,随之而来的是生成式人工智能变革的第二波浪潮,即检索增强生成(RAG)应用的兴起,您可以在查询时向模型提供相关信息,以构建更好、更准确的答案。

RAG application flow. Image by the author. Icons from https://www.flaticon.com/

如前所述,RAG应用需要一个智能搜索工具,能够根据用户输入检索附加信息,使LLM能够生成更准确、更实时的答案。起初,主要关注的是使用语义搜索从非结构化文本中检索信息。然而,很快就明显,结构化和非结构化数据的结合是RAG应用的最佳方法,如果您想跳出“与您的PDF聊天”的应用范畴。

Neo4j非常适合处理结构化信息,但由于其暴力搜索方法,在语义搜索方面遇到一些困难。然而,随着Neo4j在5.11版本中引入了一种新的向量索引,旨在高效地执行与非结构化文本或其他嵌入式数据模态的语义搜索,这一困境已经过去了。新增的向量索引使得Neo4j非常适合大多数RAG应用,因为它现在能够很好地处理结构化和非结构化数据。

在本博文中,我将向您展示如何在Neo4j中设置向量索引,并将其集成到LangChain生态系统中。代码可在GitHub上获得。

Neo4j环境设置

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

在实例化Neo4j数据库后,您可以使用LangChain库连接到它。

from langchain.graphs import Neo4jGraphNEO4J_URI="neo4j+s://1234.databases.neo4j.io"NEO4J_USERNAME="neo4j"NEO4J_PASSWORD="-"graph = Neo4jGraph(    url=NEO4J_URI,    username=NEO4J_USERNAME,    password=NEO4J_PASSWORD)

设置向量索引

Neo4j的向量索引由Lucene提供支持,其中Lucene实现了一种层次可导航的小世界(HNSW)图,用于在向量空间中执行近似最近邻(ANN)查询。

Neo4j的向量索引实现旨在为节点标签的单个节点属性建立索引。例如,如果您想要在其节点属性embedding上建立具有标签Chunk的节点索引,可以使用以下Cypher过程。

CALL db.index.vector.createNodeIndex(  'wikipedia', // 索引名称  'Chunk',     // 节点标签  'embedding', // 节点属性   1536,       // 向量大小   'cosine'    // 相似度度量)

除了索引名称、节点标签和属性外,还必须指定向量大小(嵌入维度)和相似度度量。我们将使用OpenAI的text-embedding-ada-002嵌入模型,该模型使用向量大小1536来表示嵌入空间中的文本。目前,仅支持余弦欧氏相似度度量。OpenAI建议在使用其嵌入模型时使用余弦相似度度量。

填充向量索引

Neo4j是一个没有架构限制的数据库,这意味着它不会对节点属性中的内容进行任何限制。例如,Chunk节点的embedding属性可以存储整数、整数列表甚至字符串。让我们来试一试。

WITH [1, [1,2,3], ["2","5"], [x in range(0, 1535) | toFloat(x)]] AS exampleValuesUNWIND range(0, size(exampleValues) - 1) as indexCREATE (:Chunk {embedding: exampleValues[index], index: index})

这个查询会为列表中的每个元素创建一个Chunk节点,并将该元素作为embedding属性的值。例如,第一个Chunk节点的embedding属性值为1,第二个节点的值为[1,2,3],以此类推。Neo4j不会对节点属性中的内容进行任何限制。然而,向量索引对存储的值以及嵌入维度有明确的要求。

我们可以通过执行向量索引搜索来测试哪些值已被索引。

CALL db.index.vector.queryNodes(  'wikipedia', // 索引名称   3, // 返回的最佳邻居数   [x in range(0,1535) | toFloat(x) / 2] // 输入向量)YIELD node, scoreRETURN node.index AS index, score

如果运行此查询,将只返回一个节点,即使您请求返回最佳的3个邻居。为什么会这样呢?向量索引只对属性值进行索引,其中值是具有指定长度的浮点数列表。在这个例子中,只有一个embedding属性值具有浮点数列表类型,长度为1536。

如果满足以下条件,节点将被向量索引所索引:

  • 节点包含配置的标签。
  • 节点包含配置的属性键。
  • 相应属性值的类型为LIST<FLOAT>
  • 相应值的size()与配置的维度相同。
  • 该值是配置的相似性函数的有效向量。

将向量索引集成到LangChain生态系统中

现在,我们将实现一个简单的自定义LangChain类,它将使用Neo4j向量索引来检索相关信息,以生成准确和最新的答案。但首先,我们需要填充向量索引。

<img alt="在RAG应用中使用Neo4j向量索引的数据流。由作者创建的图像。图标来自flaticons。

该任务包括以下步骤:

  • 检索维基百科文章
  • 对文本进行分块
  • 将文本及其向量表示存储在Neo4j中
  • 实现自定义的LangChain类以支持RAG应用

在这个例子中,我们只会获取一篇维基百科文章。我决定使用《Baldur’s Gate 3》页面。

import wikipediabg3 = wikipedia.page(pageid=60979422)

接下来,我们需要对文本进行分块和嵌入。我们将使用双换行符作为分隔符,将文本分割为各个部分,然后使用OpenAI的嵌入模型为每个部分生成适当的向量表示。

import osfrom langchain.embeddings import OpenAIEmbeddingsos.environ["OPENAI_API_KEY"] = "API_KEY"embeddings = OpenAIEmbeddings()chunks = [{'text':el, 'embedding': embeddings.embed_query(el)} for                  el in bg3.content.split("\n\n") if len(el) > 50]

在继续使用LangChain类之前,我们需要将文本块导入到Neo4j中。

graph.query("""UNWIND $data AS rowCREATE (c:Chunk {text: row.text})WITH c, rowCALL db.create.setVectorProperty(c, 'embedding', row.embedding)YIELD nodeRETURN distinct 'done'""", {'data': chunks})

你可以注意到,我使用了db.create.setVectorProperty过程将向量存储到Neo4j中。该过程用于验证属性值确实是一个浮点数列表。此外,它还可以减少向量属性的存储空间约50%。因此,建议始终使用该过程将向量存储到Neo4j中。

现在,我们可以实现用于从Neo4j向量索引中检索信息并用于生成答案的自定义LangChain类。首先,我们将定义用于检索信息的Cypher语句。

vector_search = """WITH $embedding AS eCALL db.index.vector.queryNodes('wikipedia',3, e) yield node, scoreRETURN node.text AS resultORDER BY score DESCLIMIT 3"""

正如你所见,我已经硬编码了索引名称和要检索的相邻节点个数k。如果你愿意,可以通过添加适当的参数来使其动态化。

自定义的LangChain类的实现非常简单明了。

class Neo4jVectorChain(Chain):    """用于针对Neo4j向量索引进行问答的链条。"""    graph: Neo4jGraph = Field(exclude=True)    input_key: str = "query"  #: :meta private:    output_key: str = "result"  #: :meta private:    embeddings: OpenAIEmbeddings = OpenAIEmbeddings()    qa_chain: LLMChain = LLMChain(llm=ChatOpenAI(temperature=0), prompt=CHAT_PROMPT)    def _call(self, inputs: Dict[str, str], run_manager) -> Dict[str, Any]:        """嵌入问题并进行向量搜索。"""        question = inputs[self.input_key]                # 嵌入问题        embedding = self.embeddings.embed_query(question)                # 从向量索引中检索相关信息        context = self.graph.query(            vector_search, {'embedding': embedding})        context = [el['result'] for el in context]                # 生成答案        result = self.qa_chain(            {"question": question, "context": context},        )        final_result = result[self.qa_chain.output_key]        return {self.output_key: final_result}

为了提高可读性,我省略了一些样板代码。当你调用Neo4jVectorChain时,将执行以下步骤:

  1. 使用相关的嵌入模型嵌入问题
  2. 使用文本嵌入值从向量索引中检索最相似的内容
  3. 使用来自相似内容的提供的上下文生成答案

现在我们可以测试我们的实现。

vector_qa = Neo4jVectorChain(graph=graph, embeddings=embeddings, verbose=True)vector_qa.run("巴尔德之门3的玩法是什么样的?")

响应

Generated response. Image by the author.

通过使用verbose选项,您还可以评估从用于生成答案的向量索引中检索到的上下文。

总结

利用Neo4j的新向量索引功能,您可以创建一个统一的数据源,有效地支持检索增强生成应用程序。这不仅使您能够实现“与您的PDF或文档聊天”的解决方案,还可以进行实时分析,所有这些都来自一个单一、强大的数据源。这种多用途的实用工具可以简化您的操作,增强数据协同,使Neo4j成为管理结构化和非结构化数据的绝佳解决方案。

代码可在GitHub上找到。