构建由 LLM 驱动的 Neo4j 应用程序——探索 Neo4j 的高级知识图谱功能

158 阅读13分钟

基于上一章介绍的基础搜索功能,本章将探索更复杂的知识探索、图推理和性能优化技术。在本章中,我们将利用 Neo4j 的高级功能,重点介绍如何将这些功能与 Haystack 集成,打造更智能的 AI 驱动搜索系统。

通过学习本章内容,你将能够深入挖掘知识图谱中的洞见,充分利用高级搜索功能,并确保你的 AI 搜索系统兼具高性能与可持续性。

本章主要涵盖以下内容:

  • 探索 Haystack 的高级知识探索功能
  • 使用 Haystack 进行图推理
  • 扩展 Haystack 与 Neo4j 的集成规模
  • AI 搜索系统的维护与监控最佳实践

技术要求

在开始本章学习之前,请确保你的开发环境已经配置好所需技术和工具。此外,Neo4j 实例应已加载第4章的数据和第5章生成的向量。以下是本章的技术要求:

  • Neo4j(v5.x及以上):需要在本地机器或服务器上安装并运行 Neo4j。可从 neo4j.com/download/ 下载。

  • Haystack(v1.x):我们将使用 Haystack 框架集成 AI 搜索功能。请按照 docs.haystack.deepset.ai/docs/instal… 指引安装。

  • Python(v3.8及以上):确保安装了 Python,下载地址:www.python.org/downloads/

  • OpenAI API 密钥:使用基于 GPT 的模型生成向量时需要有效的 OpenAI API 密钥:

    • 若无账号,请注册:platform.openai.com/signup
    • 注意:免费套餐的 API 密钥在本项目多数场景无法使用,你需要付费订阅以访问必要接口和使用额度。
    • 登录后,在仪表盘的 API 密钥页面(platform.openai.com/api-keys)生成新的密钥。

如果你已经完成了前几章的环境搭建,这些要求可视为已满足,无需重复安装。

本章所有代码均托管于 GitHub 仓库:github.com/PacktPublis…
该目录包含实现 Neo4j 与 Haystack 集成及高级知识图谱功能所需的所有脚本、文件和配置。请确保克隆或下载仓库,以便跟随本章的代码示例学习。

探索 Haystack 的高级知识探索功能

本节我们将深入探讨使用 Haystack 的更高级搜索能力。你已经在第5章将向量嵌入集成到 Neo4j 图中,现在是时候探索如何超越基础的相似度匹配,提升搜索能力。目标是从简单的基于检索的向量,转向对图谱中知识进行更细致、多层次的探索。

我们将探讨诸如基于上下文的推理,以及针对特定用例优化搜索功能的技术,以提供高度相关且智能的搜索结果。

首先谈谈基于上下文的推理。

上下文感知搜索

我们将在上一章“将 Haystack 连接到 Neo4j 实现高级向量搜索”中基于向量嵌入的方法基础上,结合 Haystack 的相似度搜索功能,实现跨越 Neo4j 图的多跳推理。这种方法允许搜索引擎沿着节点间的多重关系进行遍历,同时利用先进的 AI 检索手段。不再仅仅基于直接匹配检索节点或文档,而是利用 Haystack 探索相关节点间的路径,增加上下文层次,挖掘更深层次的洞见。图推理与相似度理解的结合,使搜索结果更智能、更相关。

以下代码片段展示了如何检索由同一导演执导的所有电影(以电影《盗梦空间》为例):

注意:电影标题作为变量 title 从主程序传入。

def fetch_multi_hop_related_movies(title):
    query = """
    MATCH (m:Movie {title: $title})<-[:DIRECTED]-(d:Director)-[:DIRECTED]->(related:Movie)
    RETURN related.title AS related_movie, related.overview AS overview
    """
    with driver.session() as session:
        result = session.run(query, title=title)
        documents = [
            {
                "content": record["overview"],
                "meta": {"title": record["related_movie"]}
            }
            for record in result
        ]
    return documents

接下来,使用 Haystack 对这些多跳相关电影进行基于相似度的排序:

def perform_similarity_search_with_multi_hop(query, movie_title):
    # 从 Neo4j 获取多跳相关电影
    multi_hop_docs = fetch_multi_hop_related_movies(movie_title)
    if not multi_hop_docs:
        print(f"未找到与《{movie_title}》相关的电影")
        return
    # 写入文档存储
    document_store.write_documents(multi_hop_docs)
    # 为搜索查询(如“时间旅行”)生成向量
    query_embedding = text_embedder.run(query).get("embedding")
    if query_embedding is None:
        print("查询向量生成失败。")
        return
    # 仅在多跳相关电影中执行向量搜索
    similar_docs = document_store.query_by_embedding(
        query_embedding, top_k=3
    )
    if not similar_docs:
        print("未找到相似文档。")
        return
    for doc in similar_docs:
        title = doc.meta.get("title", "N/A")
        overview = doc.meta.get("overview", "N/A")
        score = doc.score
        print(
            f"标题: {title}\n剧情摘要: {overview}\n"
            f"得分: {score:.2f}\n{'-'*40}"
        )
    print("\n\n")

需要注意的是,由于我们只导入了原始数据集的一部分,数据中并不包含一对多的关系(即导演执导多部电影),因此搜索结果可能会出现“未找到与《盗梦空间》相关的电影”之类的情况。

你可以尝试更新脚本,导入完整数据集(例如从 AuraDB 免费版升级到 AuraDB 专业版或商务关键版,或使用 Neo4j Desktop 版本),体验多跳推理的完整功能。

动态搜索查询与灵活的搜索过滤器

知识图谱的一个优势是能够在搜索查询中动态应用过滤条件。

下面的代码片段演示了如何在 Haystack 查询中加入过滤器和约束,使用户可以基于特定参数(例如时间范围、分类或实体间关系)来细化搜索结果。这种灵活性对构建更具交互性和上下文丰富的搜索系统至关重要:

def perform_filtered_search(query):
    pipeline = Pipeline()
    pipeline.add_component("query_embedder", text_embedder)
    # pipeline.add_component("retriever", retriever)
    pipeline.add_component(
        "retriever", 
        Neo4jEmbeddingRetriever(document_store=document_store)
    )
    pipeline.connect(
        "query_embedder.embedding", "retriever.query_embedding"
    )
    result = pipeline.run(
        data={
            "query_embedder": {"text": query},
            "retriever": {
                "top_k": 5,
                "filters": {
                    "field": "release_date", "operator": ">=", 
                    "value": "1995-11-17"
                },
            },
        }
    )
    # 从检索结果中提取文档
    documents = result["retriever"]["documents"]
    for doc in documents:
        # 从文档元数据中提取标题和剧情摘要
        title = doc.meta.get("title", "N/A")
        overview = doc.meta.get("overview", "N/A")
        # 从文档中提取得分(非元数据)
        score = getattr(doc, "score", None)
        # 格式化得分,如无则显示"N/A"
        score_display = f"{score:.2f}" if score is not None else "N/A"
        # 打印标题、剧情摘要和得分(无得分则显示"N/A")
        print(
            f"Title: {title}\nOverview: {overview}\n"
            f"Score: {score_display}\n{'-'*40}\n"
        )

该代码示例展示了如何使用动态过滤器(例如 release_date)来细化搜索结果。通过引入这些过滤器,可以对特定字段加以约束,比如仅显示某日期之后的文档,或按类别、评分等属性进行过滤。这一功能使得用户能够将结果范围缩小到最相关的内容,有效提升搜索体验。

采用这种方式,你可以轻松扩展或调整过滤条件,以适应不同需求,为知识图谱中的数据交互提供灵活且强大的手段。

搜索优化:针对特定用例定制搜索

并非所有搜索系统都是相同构建的。无论你是在构建推荐引擎还是特定领域的搜索工具,都需要不同的优化策略。本节将探讨如何为你的独特用例定制 Haystack 的搜索配置,确保针对特定数据实现最佳性能和相关性。同时,我们也会介绍在大规模环境下调优模型和索引的重要性。

请看以下代码示例:

def perform_optimized_search(query, top_k):
    optimized_results = document_store.query_by_embedding(
        query_embedding=text_embedder.run(query).get("embedding"), 
        top_k=top_k
    )
    for doc in optimized_results:
        title = doc.meta["title"]
        overview = doc.meta.get("overview", "N/A")
        print(f"Title: {title}\nOverview: {overview}\n{'-'*40}")

该代码展示了如何调整参数(如 top_k),以微调搜索查询返回的结果数量——而非模型本身。top_k 参数决定了基于向量相似度检索时返回的最高结果数量。

注意
以上代码仅为片段,完整版本请见 GitHub 仓库:github.com/PacktPublis…

利用 Haystack 的相似度检索能力(如上下文感知搜索和动态过滤),你可以创建更精准的 AI 驱动搜索系统,实现更优的搜索优化。但搜索仅仅是开始。

下一节,我们将超越搜索,利用 Haystack 的推理能力以及 Neo4j 知识图谱中的关系,实现基于图的推理。

使用 Haystack 进行图推理

本节将探讨如何将 Haystack 的功能扩展到基础搜索之外,结合 Neo4j 强大的图推理特性。传统搜索方法基于文本相似度检索结果,而图推理则通过利用知识图谱中实体之间丰富的关系,帮助你发现更深层的洞见。通过将 Haystack 的相似度理解能力与 Neo4j 中的结构化数据结合,你可以执行更复杂的查询,跨越多重连接,揭示隐藏模式,挖掘具有上下文丰富度的见解。

本节将指导你构建这些高级推理能力,将搜索系统转变为智能的知识驱动工具。

跨越多重关系揭示隐藏洞见

图遍历有助于发现实体间的联系,而跨越多种关系类型的多跳遍历则能揭示知识图谱中的隐藏模式。无论是在电影、演员、导演还是类型之间,沿着不同路径遍历 Neo4j,都能生成超越直接关系的更深层洞见。这种多步遍历使你能够以基础搜索无法企及的方式探索数据,发现可能被忽略的连接。

接下来,我们将学习如何使用多种关系类型和多跳查询获取更复杂的结果,然后结合 Haystack 的相似度搜索能力进行精炼和排序。

以下示例演示如何查找与电影《侏罗纪公园》拥有相同演员和导演的电影,不仅揭示直接合作,也发现间接关联:

def fetch_multi_hop_related_movies(title):
    query = """
    MATCH (m:Movie {title: $title})<-[:ACTED_IN|DIRECTED]-(p)-
        [:ACTED_IN|DIRECTED]->(related:Movie)
    WITH related.title AS related_movie, p.name AS person,
         CASE
           WHEN (p)-[:ACTED_IN]->(m) AND (p)-[:ACTED_IN]->(related) THEN 'Actor'
           WHEN (p)-[:DIRECTED]->(m) AND (p)-[:DIRECTED]->(related) THEN 'Director'
           ELSE 'Unknown Role'
         END AS role,
         related.overview AS overview, related.embedding AS embedding
     RETURN related_movie, person, role, overview, embedding
    """
    with driver.session() as session:
        result = session.run(query, title=title)
        documents = []
        for record in result:
            documents.append(
                Document(
                    content=record.get("overview", "No overview available"),  # 将剧情摘要存为内容
                    meta={
                        "title": record.get("related_movie", "Unknown Movie"),   # 电影标题
                        "person": record.get("person", "Unknown Person"),        # 演员/导演名字
                        "role": record.get("role", "Unknown Role"),               # 角色类型(演员或导演)
                        "embedding": record.get("embedding", "No embedding available")  # 预先计算好的向量
                    },
                )
            )
    return documents

通过路径查询解锁洞见

图推理的另一个强大功能是能够查询节点间的特定路径。例如,查找两部电影通过一系列合作关系是如何连接的,可以揭示意想不到的见解。

请看以下查询:

MATCH path = (m1:Movie {title: "Inception"})-[:ACTED_IN*3]-(m2:Movie)
RETURN m1.title, m2.title, path

该查询查找电影《盗梦空间》(Inception)与另一部电影之间通过三层演员关系的连接路径。

image.png

图中所示的示意图展示了电影图中的三跳路径遍历,起点为电影《盗梦空间》(Inception),通过一系列演员合作链条最终到达电影 C。该路径是通过 Cypher 查询实现的,查询使用了三次重复的 ACTED_IN 关系来探索电影间的连接。在示例中,《盗梦空间》通过演员 A 与电影 B 相连,电影 B 又通过演员 B 连接到电影 C。每一次跳转代表从电影到演员,或从演员到电影的转换,形成一个三跳的无向遍历。此可视化突出展示了 Neo4j 中多跳推理如何揭示更深层次的间接关系,这对于内容发现、推荐系统和合作网络分析等应用非常有价值。

注意
以上仅为代码片段,完整版本请见 GitHub 仓库:github.com/PacktPublis…

通过结合 Neo4j 的图推理与 Haystack 的相似度理解,我们能够捕捉数据中有意义的关联,如电影与演员间的关系、导演的多跳合作,以及实体间复杂路径的挖掘。

接下来,我们将探讨如何优化这些流程,确保随着图的复杂度和规模增长,系统依然保持高性能。

扩展 Haystack 与 Neo4j 的集成

随着系统规模的扩大,对 Haystack 和 Neo4j 的需求也随之增加。尤其是在处理更大规模的数据集、更复杂的图结构和更高级的搜索功能时,性能优化变得至关重要。

本节将聚焦于确保 Haystack 与 Neo4j 集成能够高效应对增长负载的最佳实践和技术。我们将探讨查询优化、缓存策略、索引改进,以及如何扩展基础设施以满足性能需求,同时不牺牲速度和准确性。

优化大规模图的 Neo4j 查询

随着 Neo4j 图规模和复杂度的增加,查询性能可能下降,尤其是涉及多重关系遍历或处理大数据集时。以下是提升 Neo4j 查询性能的一些技巧:

  • 使用索引和约束:确保对常用查询属性(如 titlename)建立索引。索引可以加快节点查找,提高遍历效率:
CREATE INDEX FOR (m:Movie) ON (m.title);
CREATE INDEX FOR (p:Person) ON (p.name);
  • 分析并优化查询:使用 Neo4j 的 PROFILEEXPLAIN 关键词分析查询性能,了解哪些部分影响速度,找出优化点:
PROFILE MATCH (m:Movie {title: "Inception"}) RETURN m;
  • 提前限制结果数量:处理大结果集时,尽早限制返回的节点数量,避免过度抓取数据:
MATCH (m:Movie)-[:ACTED_IN]->(a:Actor) RETURN m.title LIMIT 10;

缓存向量和查询结果

在扩展 Haystack 和 Neo4j 时,缓存有助于减少重复计算和网络请求,显著提升性能。通过缓存向量和查询结果,可以提高搜索系统的效率,尤其是在处理高并发查询时。以下是缓存策略的示例及其优势:

  • 缓存向量:将 Haystack 生成的向量缓存到 Neo4j 或独立缓存层(如 Redis),避免对频繁查询重复计算向量。
# 缓存向量示例
embedding_cache = {}  # 简单的内存缓存,规模大时建议使用 Redis

def get_cached_embedding(query):
    if query in embedding_cache:
        return embedding_cache[query]
    else:
        embedding = text_embedder.run(query).get("embedding")
        embedding_cache[query] = embedding
        return embedding
  • 缓存查询结果:对高频执行的 Neo4j 查询,可将结果缓存到内存或 Redis、Memcached 等缓存系统,减轻 Neo4j 负载,快速返回热门查询结果。
# 使用 Redis 缓存 Neo4j 查询结果示例
import redis
cache = redis.Redis()

def get_cached_query_result(query):
    cached_result = cache.get(query)
    if cached_result:
        return cached_result
    else:
        result = run_neo4j_query(query)
        cache.set(query, result)
        return result

高效使用向量索引

随着向量搜索功能的扩展,优化 Neo4j 中的向量索引对保持性能至关重要。你可以这样操作:

  • 配置高性能向量索引:根据你的向量维度和搜索需求,确保 Neo4j 中的向量索引配置合理:
CREATE VECTOR INDEX overview_embeddings IF NOT EXISTS
FOR (m:Movie) ON (m.embedding)
OPTIONS {
    indexConfig: {
        `vector.dimensions`: 1536, 
        `vector.similarity_function`: 'cosine'
    }
}
  • 批量写入操作:在向 Neo4j 写入大量向量时,使用批量操作减少单次写入的开销:
document_store.write_documents(embeddings_list, batch_size=100)  # 优化性能的批量大小

负载均衡与横向扩展

为了应对 Haystack 和 Neo4j 日益增长的访问量和负载,实施横向扩展和负载均衡至关重要。通过负载均衡和横向扩展,可以确保系统在高流量下依然保持响应迅速且具备弹性。以下是两种方案如何助力系统扩展性的说明:

  • 扩展 Neo4j:利用 Neo4j AuraDB 或 Neo4j 集群,可以将数据库负载分散到多个实例上,提升读写能力。这对于需要大规模快速数据检索和处理的应用尤其有利。
  • 对 Haystack 进行负载均衡:通过负载均衡器将进入的搜索请求分配给多个 Haystack 实例,避免单点实例过载。这种方式能维持性能稳定,确保高可用性,支持需求增长。
  • 使用 Kubernetes:在 Kubernetes 上部署容器化的 Haystack 实例,根据流量动态调整副本数量,实现灵活扩展。Kubernetes 动态编排这些副本,确保资源与需求匹配,高效应对使用高峰。下面是一个 Kubernetes 部署配置示例,通过创建多个副本高效处理增加的流量:
apiVersion: apps/v1
kind: Deployment
metadata:
  name: haystack-deployment
spec:
  replicas: 3  # 根据流量调整副本数量
  selector:
    matchLabels:
      app: haystack
  template:
    metadata:
      labels:
        app: haystack
    spec:
      containers:
      - name: haystack
        image: haystack:latest

通过实施这些优化策略,可以确保 Haystack 与 Neo4j 的集成在数据和查询复杂度增长时依然保持高性能和良好扩展性。无论是通过缓存、高效索引,还是横向扩展基础设施,这些技术都帮助你在不断增长的负载下保持速度和准确性。系统规模扩大时,性能优化固然重要,但维护和监控 AI 搜索系统的健康同样关键。

注意 想了解 Neo4j 如何实现行业领先的速度和扩展性,尤其是在数据和查询复杂度增长时?请阅读这篇博客:《Achieve Unrivaled Speed and Scalability with Neo4j》(neo4j.com/blog/machin…)。

下一节,我们将探讨保持系统长期稳定运行的最佳实践,重点介绍性能监控、告警设置以及确保系统稳定可靠的策略,超越代码层面。

维护与监控 AI 驱动搜索系统的最佳实践

构建强大的 AI 驱动搜索系统仅仅是开始。为了确保其长期成功,你需要超越初始搭建,专注于系统的持续维护与监控。定期性能检查、主动监控和完善的日志策略对于识别瓶颈、预防系统故障和优化资源使用至关重要。

本节将介绍保持 Haystack 与 Neo4j 集成稳定运行的最佳实践,包括监控关键性能指标、设置关键问题告警,以及实施可持续维护流程,确保搜索系统在规模扩大时依然可靠高效。

性能优化不是一次性的工作。我们需要持续监控和收集指标,识别瓶颈和改进空间。下面介绍如何实现这些目标。

监控 Neo4j 和 Haystack 性能

定期跟踪查询响应时间、数据库性能和整体系统健康状况,是维护 AI 驱动搜索系统的基础。应为 Neo4j 和 Haystack 设置监控,追踪关键指标,发现瓶颈,保证系统平稳运行:

  • Neo4j 监控:利用 Neo4j 自带的指标功能及与 Prometheus、Grafana 等工具的集成,实时可视化查询性能和系统负载。
  • Haystack 监控:使用 Grafana 和 Prometheus 监控 Haystack 的查询吞吐量、延迟和响应时间。

示例:监控 Haystack 查询响应时间

import time

start_time = time.time()
result = retriever.retrieve(query)
end_time = time.time()
response_time = end_time - start_time
print(f"查询响应时间: {response_time} 秒")

设置关键问题告警

配置自动告警,确保在性能异常或系统故障时及时收到通知。通过 Prometheus 配合 Alertmanager 或 Grafana,你可以为慢查询、搜索失败或负载升高设置阈值告警。

例如,可以创建告警,当 Neo4j 查询响应时间超过设定阈值,或 Haystack 搜索延迟超出可接受范围时触发。

更多 Neo4j 监控与告警内容,请参考:neo4j.com/docs/operat…

实施日志策略

详尽的日志有助于排查问题,理解故障或性能下降的根因。应在 Haystack 和 Neo4j 中均实现日志记录,包含查询执行时间、失败情况及系统资源使用等。

Neo4j 日志相关内容:neo4j.com/docs/operat…
Haystack 日志与调试说明:docs.haystack.deepset.ai/docs/debug

建立定期维护机制

定期维护能确保 AI 搜索系统长时间保持最佳性能,具体包括:

通过执行上述最佳实践,你可以确保 AI 搜索系统稳健、可靠,并具备应对变化需求的适应性。主动监控、有效日志和定期维护,帮助你在问题影响性能前发现隐患,保证系统在数据和查询量增长时平稳运行。这些策略不仅预防停机和低效,还支持系统平滑演进与扩展。持续关注监控与维护,是保持 AI 驱动搜索系统长期成功的关键。

总结

本章中,我们探讨了如何优化 Haystack 与 Neo4j 集成的性能,并建立了维护和监控 AI 驱动搜索系统的最佳实践。你学习了缓存策略、高效索引、查询优化以及扩展基础设施以应对不断增长的数据和查询负载的关键方法。我们还强调了监控系统性能、设置告警和实施完善日志策略的重要性,以确保系统长时间平稳运行。这些知识是构建随着数据和复杂度增加而保持快速、可靠且可扩展的搜索系统的关键第一步。

随着我们告一段落 Haystack 部分的内容,本书接下来的章节将重点转向 Spring AI 框架和 LangChain4j 与 Neo4j 的集成。你将在后续章节中探索如何将这些技术结合起来,构建复杂的推荐系统,进一步增强 AI 驱动应用的能力。