Neo4j:探索非结构化文本的高效语义搜索方法

2023年08月29日 由 alex 发表 1389 0

自从ChatGPT出现以来,技术领域发生了一场变革性转变。ChatGPT在泛化能力上的卓越表现减少了需要专门的深度学习团队和大规模训练数据集来创建定制的自然语言处理(NLP)模型的要求。这使得许多NLP任务,如摘要和信息提取,比以往任何时候都更容易获得,实现了对NLP的民主化访问。然而,我们很快意识到类似ChatGPT的模型存在一些限制,例如知识时效性和无法访问私人信息。在我看来,随后发生的是生成式人工智能转型的第二波浪潮,涌现了基于检索增强生成(Retrieval Augmented Generation,RAG)的应用,其中在查询时向模型提供相关信息以构建更好、更准确的答案。


1


正如前面提到的,RAG应用需要一个智能搜索工具,能够基于用户输入检索相关信息,从而使LLM能够生成更准确、更实时的答案。起初,主要集中在使用语义搜索从非结构化文本中检索信息。然而,很快就明确了如果想要超越“与PDF聊天”应用,结构化和非结构化数据的结合才是RAG应用的最佳方法。


Neo4j一直是处理结构化信息的绝佳选择,但由于其蛮力搜索方法,对语义搜索一直感到吃力。然而,这种困难已经过去了,因为Neo4j在5.11版本中引入了一个新的向量索引,专门设计用于在非结构化文本或其他嵌入式数据模态上高效进行语义搜索。新增的向量索引使Neo4j成为大多数RAG应用的理想选择,因为它现在能够很好地处理结构化和非结构化数据。


在本文中,我将向你展示如何在Neo4j中设置一个向量索引,并将其整合到LangChain生态系统中。


Neo4j环境设置


你需要设置一个Neo4j 5.11或更高版本,最简单的方法是在Neo4j Aura上启动一个免费实例,该平台提供Neo4j数据库的云实例。另外,你还可以通过下载Neo4j Desktop应用程序并创建一个本地数据库实例来设置Neo4j数据库的本地实例。


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


from langchain.graphs import Neo4jGraph
NEO4J_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的向量索引实现旨在为节点标签的单个节点属性建立索引。例如,如果你想要在具有标签为Chunk的节点上对节点属性embedding建立索引,可以使用以下Cypher过程。


CALL db.index.vector.createNodeIndex(
  'wikipedia', // index name
  'Chunk',     // node label
  'embedding', // node property
   1536,       // vector size
   'cosine'    // similarity metric
)


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


填充向量索引


Neo4j以无模式(schema-less)的方式设计,这意味着它不对节点属性中的内容施加任何限制。例如,Chunk节点的嵌入属性可以存储整数、整数列表甚至字符串。让我们试一试。


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


这个查询在列表中的每个元素创建一个Chunk节点,并将元素作为嵌入属性的值。例如,第一个Chunk节点的嵌入属性值为1,第二个节点为[1,2,3],以此类推。Neo4j对于你可以存储在节点属性下的内容没有强制规定。然而,向量索引对应索引的值及其嵌入维度有明确的要求。


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


CALL db.index.vector.queryNodes(
  'wikipedia', // index name
   3, // topK neighbors to return
   [x in range(0,1535) | toFloat(x) / 2] // input vector
)
YIELD node, score
RETURN node.index AS index, score


如果你运行这个查询,你只会得到一个节点返回,即使你要求返回前3个邻居节点。为什么会这样呢?向量索引只对属性值进行索引,其中值是具有指定大小的浮点数列表。在这个例子中,只有一个嵌入属性值具有选择的长度1536的浮点数列表类型。


只有满足以下条件的节点才会被向量索引索引:


1. 节点包含配置的标签。


2. 节点包含配置的属性键。


3. 相应的属性值的类型为LIST<FLOAT>。


4. 相应值的size()与配置的维度相同。


5. 对于配置的相似度函数,该值是一个有效的向量。


将向量索引整合到LangChain生态系统中


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


1-2


该任务将包括以下步骤:


1. 检索维基百科文章


2. 对文本进行分块(Chunk)


3. 将文本及其向量表示与Neo4j一起存储


4. 实现自定义的LangChain类以支持RAG应用


在这个示例中,我们将只获取一个维基百科文章。我决定使用《巴尔德之门3》的页面。


import wikipedia
bg3 = wikipedia.page(pageid=60979422)



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


import os
from langchain.embeddings import OpenAIEmbeddings
os.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 row
CREATE (c:Chunk {text: row.text})
WITH c, row
CALL db.create.setVectorProperty(c, 'embedding', row.embedding)
YIELD node
RETURN distinct 'done'
""", {'data': chunks})


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


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


vector_search = """"""
WITH $embedding AS e
CALL db.index.vector.queryNodes('wikipedia',$k, e) yield node, score
RETURN node.text AS result
"""


正如你所看到的,我已经将索引名称硬编码了。如果你希望,可以通过添加适当的参数使其动态化。


定制的LangChain类实现非常简单。


class Neo4jVectorChain(Chain):
    """Chain for question-answering against a Neo4j vector index."""
    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, k=3) -> Dict[str, Any]:
        """Embed a question and do vector search."""
        question = inputs[self.input_key]
        
        # Embed the question
        embedding = self.embeddings.embed_query(question)
        
        # Retrieve relevant information from the vector index
        context = self.graph.query(
            vector_search, {'embedding': embedding, 'k': 3})
        context = [el['result'] for el in context]
        
        # Generate the answer
        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)True)
vector_qa.run("What is the gameplay of Baldur's Gate 3 like?")


响应


1-3


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


总结


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





文章来源:https://medium.com/towards-data-science/efficient-semantic-search-over-unstructured-text-in-neo4j-8179ad7ff451
欢迎关注ATYUN官方公众号
商务合作及内容投稿请联系邮箱:bd@atyun.com
评论 登录
热门职位
Maluuba
20000~40000/月
Cisco
25000~30000/月 深圳市
PilotAILabs
30000~60000/年 深圳市
写评论取消
回复取消