S

siwei.io

Siwei(思为) builds things and believes in Open Source.

Fusion GraphRAG Introduced

本文分享我们团队在 GraphRAG 领域的探索与实践历程,介绍一下 FusionGraphRAG。 本文将分享我们团队在 GraphRAG 领域的探索与实践历程。GraphRAG 是检索增强生成(RAG)技术的高级技术之一,旨在解决传统 RAG 的局限性。上周,我在 Twitter 上分享了关于 GraphRAG Indexing 模型蒸馏训练的工作,收获了诸多积极反馈。本文应朋友们建议整理而成,涵盖我们自 2023 年 6 月以来在 GraphRAG 基础研究上的进展,以及对高级 RAG 和 Fusion GraphRAG 的理解与实践。 0.1 RAG 与 GraphRAG 的缘起 0.1.1 RAG:检索增强生成 过去两年来,生成式 AI 和大语言模型的飞速发展堪称人类技术史上的重要里程碑。它们赋予计算机前所未有的上下文学习和推理能力,使得许多专属人类的复杂任务得以自动化甚至超越。然而,如何高效利用这些模型完成具体任务,成为技术落地的关键。 RAG(Retrieval Augmented Generation,检索增强生成)作为一种基础范式应运而生。其核心在于通过“检索”(Retrieval)增强“生成”(Generation): 生成:将任务描述和领域知识(即提示词)输入大语言模型,生成相应的回答。 检索增强:利用检索技术为模型提供高度相关的上下文知识,从而提升生成质量。 在 RAG 中,检索环节至关重要。根据应用场景的不同,RAG 可分为两类: 面向公共检索系统:如 Google 或学术搜索引擎,依赖大模型调用外部 API,适用于通用知识获取。市面上的代表性产品包括 Perplexity Search 和 DeepSeek。 面向私有领域知识:如企业内部的研发文档或研究报告,需结合智能文档处理(IDP)、知识索引(Index)和知识管理系统。此类场景对检索技术的精准性和深度提出了更高要求。 0.1.2 朴素的 RAG 对于非结构化或半结构化知识,朴素 RAG 的典型流程包括: 将文档分割成块(Chunk)。 通过向量化嵌入(Embedding)构建语义索引,或结合 BM25 全文索引实现检索。 在任务执行时,基于任务需求直接检索或通过检索重写(如 HyDE)优化结果。 然而,这种方法在知识组织上的局限性使其难以满足企业级需求,具体挑战将在下文详述。 0.1.3 RAG 的挑战 朴素 RAG 在实际应用中面临以下难题: 大海捞针:分块假设知识分布均匀,但某些关键信息可能被稀释在无关内容中,导致召回失败。 穿针引线:线性分割破坏了知识间的关联,难以还原全局上下文。 宏观问题:基于分片和关键词的索引无法有效应对全局性问题,如“哪些话题最具创新性”。 文档理解不足:缺乏智能文档处理时,公式、表格等复杂信息无法被正确解析和索引,影响回答质量。 0.1.4 高级 RAG 做好 RAG 并非一蹴而就,而是一个持续探索的过程。本质上,我们需要将对应用特点、RAG 技术特性以及知识本身的深刻理解,融入到 RAG 流水线的设计之中。 下图展示了一个近期发布的高级 RAG 总结性工作的简化图例:一个高级 RAG 可见,在现代高级的 RAG 方法中,数据持久化层包含 “Graph DB”(即图数据库,一种以图结构——“节点”和“边”为中心的数据库),而图数据库也正是本文主题 GraphRAG 的重要基础设施。 现代图数据库能够以高效、可扩展的方式驱动多租户、超大数据规模、高并发场景下复杂关联关系(图数据)的持久化、查询、访问和计算。它是高级 RAG 系统、智能体记忆系统(Agent Memory)的重要骨干基础设施,如下图所示。面向关联关系的图状结构,与我们梳理知识的脑图、甚至大脑中的神经元结构都非常相似。这种并非偶然的结构相似性,正是高级 RAG 知识索引解决朴素 RAG 方法挑战的关键。 常见的高级 RAG 方法,在特别的索引结构上,包括树状索引(Tree-RAG)、分层检索(Raptor)和图状索引(GraphRAG),其中 GraphRAG 是我们文章的重点。 0.1.5 GraphRAG GraphRAG 这个方法最初是我们在 2023 年 8、9 月份开始,在 LlamaIndex 社区上持续做的一些工作和两次公开的 Webinar 上提出,并给出了开源的实现和可复现的效果比较试验。 在最初的 GraphRAG 参考实践中,我们给出了索引期基于大模型的同构图谱抽取的实现和查询区基于实体、关系语义的捞取与可配置的子图查询的知识召回,并比较了基于 Text-to-Query 方法的一些效果比较与结合方案的实现。现在,为了区隔之后 Graph-based RAG 方法的演进,我们把最初的 GraphRAG 叫做 SubGraph RAG。 在当时的 workshop 上,我们分享过的两个观点是 在与知识打交道的过程中,只用分片处理的方法、绕过 Graph(Knowledge Graph)一定是不够的,这也是我们做 GraphRAG 探索的初衷 这个 GraphRAG 的方法仅仅是一个开始,我们在不断探索更多在 Graph 之上的探索、推理的策略 随后,我们和 Researcher: Diego 一起讨论,做了图索引之上 Chain of Exploration 的工作,并在 PyCon China 2023 上第一次给出了做了 Chain of Exploration 的分享。 之后,我们看到了非常多的 GraphRAG 相关工作被发表,比如 MIT 材料科学的两篇 利用我们的工作的 MechGPT、海马体 RAG 利用 pagerank 权重的工作 HippoRAG、LinkedIn 的利用 半结构化数据图建模的 RAG工作都非常有启发性。 当然,最让我们兴奋的工作是微软的 GraphRAG: From Local to Global 工作,他们充分探索了基于大模型抽取的知识图谱索引之上利用社区发现算法得到分级聚集结构下的全局性摘要,这些摘要是没有偏见的保有原始知识中天然聚集性质、重要性质的脉络知识,在回答带有宏观全局性的问题中效果非常好。 我们从这些后续的工作中受益匪浅,在与我们的客户 Graph based RAG 落地工作中,不断打磨、探索、累积了很多各个环节的优化、方法、策略。最终形成了我们称为 Fusion GraphRAG 的方案。 0.2 Fusion GraphRAG Fusion GraphRAG 是我们团队在 GraphRAG 基础上的创新实践。它融合了高级 RAG 技术,通过图状结构存储文档层级、章节关系及特殊元素(如公式、表格),实现高效、灵活的检索。 Fusion GraphRAG 的本质在于 Sota 的高级 RAG 方法融合、充分连接的元知识索引、充分打磨调优的 GraphRAG(可选索引)。 Fusion GraphRAG 通过在一个联通图谱内的“元知识”索引,清晰地揭示海量知识文档的内在关联,呈现从文件夹、文档、章节到段落、图表、公式的完整脉络,此为知识图谱的“元知识频谱”。 在此基础上,用户可选择不同粒度的知识抽取方法(非必须),构建图谱结构的图索引,形成“增强图频谱”。 进一步,用户可以对图索引和元知识层进行诸如图摘要、权重分配、时序/状语信息补充等增强操作,以提升知识检索和利用的效率。 附图 FusionGraphRAG 结构实例 graph TD %% ========= Style ========= classDef metaLayer fill:#f9f9f9,stroke:#b3b3b3,stroke-width:1px,font-size:13px; classDef graphLayer fill:#c6dcff,stroke:#3f6fd9,stroke-width:1.4px,font-size:13px; classDef graphSubLayer fill:#e8f1ff,stroke:#3f6fd9,stroke-width:1px,font-size:13px; classDef enrichLayer fill:#fff9e8,stroke:#cfa514,stroke-width:1px,stroke-dasharray:4 4,font-size:13px; classDef nodeText font-size:13px; classDef queryFlow stroke:#444,stroke-dasharray:5 5; %% ========= 元知识层 ========= subgraph MetaLayer["元知识层"] F1["📂 发动机控制项目"] F2["📂 高空失效案例库"] D1["📄 ECU 设计说明"] D2["📄 起动异常日志"] D3["📄 案例:高原起动失败"] F1 --> D1 F1 --> D2 F2 --> D3 end %% 文档结构 D1 --> S1A["系统简介"] S1A --> P1["1.1 接口说明"] --> L11["段落 1"] --> L12["段落 2"] --> L13["段落 3"] S1A --> P2["1.2 电压异常"] --> L21["段落 1"] --> L22["段落 2"] D1 --> S2A["硬件配置"] D2 --> S1B["故障描述"] --> B11["1.1 现象"] S1B --> B12["2.1 日志片段"] D2 --> S2B["操作记录"] D3 --> S1C["案例背景"] --> C11["1.1 4000 m 海拔"] S1C --> C12["2.1 逻辑异常"] D3 --> S2C["初步分析"] %% 多模态结构 CH1["图:电压‑转速曲线"] TB1["表:指令时序"] FM1["公式:Q = αP√(T/R)"] L12 --> CH1 L21 --> TB1 L22 --> FM1 %% ========= 图谱层 ========= subgraph GraphLayer["图谱层"] subgraph G1 ["电压故障社区"] Fault1["故障:ECU 电压低"] Sym1["症状:无法启动"] T1["时点:2023‑07‑22"] Env1["环境:4000 m"] end subgraph G2 ["操作流程社区"] Proc1["流程:更换模块"] Act1["措施:旁路供电"] Role1["角色:维修工程师"] end end class G1,G2 graphSubLayer CH1 --> Fault1 TB1 --> Proc1 FM1 --> Proc1 L13 --> Fault1 L22 --> Proc1 B11 --> Sym1 B12 --> Act1 C11 --> Env1 Fault1 -->|引发| Sym1 Sym1 -->|定位| Fault1 Proc1 -->|解决| Fault1 Fault1 -->|发生于| T1 Proc1 -->|执行| Role1 Sym1 -->|受环境| Env1 Act1 -->|关联| Proc1 %% ========= 知识增强层 ========= subgraph EnrichLayer["知识增强层"] Sum1['社区摘要:电压故障'] Sum2['社区摘要:操作流程'] ChapSum1['章节摘要:系统简介'] SeqEdge['时序增强'] Rank['重要度评分'] end G1 -->|摘要| Sum1 G2 -->|摘要| Sum2 SeqEdge --> GraphLayer S1A --> ChapSum1 %% Style apply class F1,F2,D1,D2,D3,S1A,S2A,S1B,S2B,S1C,S2C,P1,P2,L11,L12,L13,L21,L22,B11,B12,C11,C12 metaLayer class CH1,TB1,FM1 metaLayer class Fault1,Proc1,Sym1,T1,Role1,Env1,Act1 graphLayer class Sum1,Sum2,ChapSum1,Rank,SeqEdge enrichLayer class F1,F2,D1,D2,D3,S1A,S2A,S1B,S2B,S1C,S2C,P1,P2,L11,L12,L13,L21,L22,B11,B12,C11,C12,CH1,TB1,FM1,Fault1,Proc1,Sym1,T1,Role1,Env1,Act1,Sum1,Sum2,ChapSum1,Rank,SeqEdge nodeText 0.3 Fusion GraphRAG 实现私域 Deep Research Fusion GraphRAG 融合了先进的 IDP 技术与 RAG 索引方法,并将提取的多模态知识有机地关联为图状结构,从而精确映射真实世界知识的内在逻辑。依托高性能、支持图语义与向量检索的现代图数据库作为持久化底层,Fusion GraphRAG 能在这一统一图谱上实现面向元知识层次与图谱增强的高级检索。 例如: 多层级知识检索: 通过我们提出的 “Chain of Exploration” 检索器——一个能自主在图谱中探索并查找任务相关知识的智能 Agent,Fusion GraphRAG 可以在同时包含文件夹、文件层级以及章节(附摘要)结构信息的图谱上,既完成传统基于 Chunk 的检索任务,又能支持基于分层结构实现的脑图式 RAG 检索。(注:此方法不依赖昂贵的图谱抽取技术,后续将探讨如何进一步降低抽取成本。) 全局与局部深度分析: 通过引入图抽取与图社区摘要等增强手段,“Chain of Exploration” 能够在图谱上按需执行全局搜索以解决宏观问题,同时进行针对多个知识点的精准局部检索,甚至对涉及复杂关联的深度问题(如关键知识间路径查找)进行分析。 多 Agent 协同: 利用 Fusion GraphRAG 构建多 Agent 系统,可以将一整套算法与行为准则封装为一个专用的 Fusion GraphRAG Agent,此 Agent 可作为另一 RAG Agent 的召回计划生成器,在少而精的专业知识(know-how)构建的 Fusion Graph Index 指引下,自动规划出基于海量知识库的复杂查询方案。这为构建私有领域的自主学习 Deep Research/Action Agent 提供了有力支撑。 私有知识库内的深度研究: 在 Fusion GraphRAG 之上,我们可以轻松实现针对私有知识库的 Deep Researcher。依托天然的知识连接与索引,再结合针对 “Chain of Exploration” Agent 的优化提示工程,以及类似 DeepSeek R1 的推理模型,就能实现类似 Deep Researcher 的 RAG 召回效果,为深度研究提供强大动力。 0.4 回到 GraphRAG 时间线 在我们的视角下,GraphRAG 正在不断演进,下面简要梳理这一发展历程: GraphRAG - 2023-08 NebulaGraph 在 LlamaIndex 社区首次提出并实现了基本的 GraphRAG 模型。 GraphRAG with Community Summary - 2024-04 微软在论文中提出了基于社区发现算法实现全局摘要的 GraphRAG 方法,为图谱检索提供了新的思路。 Fusion GraphRAG 融合了文档层级、章节层级、以及文中各类特殊元素(如公式、表格、图表)的多频谱混合图状索引; 支持可选的知识抽取与知识增强(包括社区摘要、时序增强等),构建混合层级结构查询、全局图召回、知识频谱图谱召回以及(可选) Chain of Exploration 检索; 同时支持将公共知识图谱、现有知识图谱以及图状数据中的 Chain of Exploration 召回方法进行融合。 0.5 图抽取 需要指出的是,在大模型时代,对原始知识进行类似知识图谱的抽取并非 Fusion GraphRAG 的必选项,因为并非所有 RAG 任务都需要细粒度的“知识预习”。图抽取的代价较高,目前多数公开方案成本也不低。 注:LazyGraphRAG 的相关工作同样值得关注,它提供了另一种优化思路。 附:Fusion GraphRAG 的索引及增强流程示意图 0.6 让图抽取成为默认策略 高质量的 GraphRAG 索引虽然效果显著,但成本昂贵。我们的终极目标是让图抽取的成本降低到与 Embedding 类似的水平,实现大规模普及。 实际上,AI 技术社区(包括我们自身)已经在朝这一方向不断努力:利用 NER 模型、更小型(例如 Phi 系列)的 LLM 进行 SFT、小规模 LLM 的 few shot 方案、尽可能的可编程方法,甚至融合 NER 与 LLM 的多种方案,都是当前探索的方向。最近,我们也看到了一些令人振奋的曙光! 本质上,我们探索的基于 LLM 的抽取方法都采用了不同形式的 CoT(思维链)策略。近期,DeepSeek-MATH 论文中首次提出的 GRPO 方法在 DeepSeek R0 中用于冷启动获取 Think-aloud(思考过程)的深度 CoT 能力表现非常出色。同时,我们注意到基于 Qwen2.5(小尺寸 0.5b 模型)进行 GRPO 训练,在需要强推理能力的任务(例如数学问题)上,通过大约 200 步训练就能获得令人满意的效果。 结合这些观察,我们开展了如下探索: 数据收集与预处理: 利用 DeepSeek R1,在挑选的最新原始知识数据(晚于模型 cut-off date)的基础上进行抽取任务,并开启 <think> 模式,收集高质量的 CoT 与图抽取的 Ground Truth,筛选掉质量不合格或思考过程过长的样本,作为训练数据。 奖励机制设计: 基于开源社区中针对数学问题推理训练的 GRPO 奖励函数,我们设计了奖励高质量推理过程和高质量抽取结果的奖励函数。这两项奖励均依托高质量、低成本的指令大模型进行评估打分。 在一块 A100 显卡上经过约 200 步训练后,我们初步评估结果显示:基于 Qwen2.5-3b 的小尺寸抽取模型在引入 GRPO 训练后,其性能已经超越了 GPT-4o-mini。以下是各模型的对比结果: Criterion qwen2.5-3b qwen2.5-3b-GPRO gpt-4o-mini Accuracy ⭐⭐☆☆☆ ⭐⭐⭐☆☆ ⭐⭐☆☆☆ Completeness ⭐⭐☆☆☆ ⭐⭐⭐⭐☆ ⭐⭐⭐☆☆ Coherence ⭐⭐☆☆☆ ⭐⭐⭐☆☆ ⭐⭐☆☆☆ Relevance ⭐⭐⭐☆☆ ⭐⭐⭐⭐☆ ⭐⭐⭐☆☆ 正如当初推出首个 GraphRAG 开源实现时一样,这项工作仅仅是个起点。它证明了图抽取技术成本正在不断降低,未来我们有望大胆应用于更多场景,实现大规模的知识 GraphRAG。与此同时,这个方法也为特定领域范式数据抽取质量的提升提供了经济可行的方案。 训练数据、合成过程以及训练流程均已开源:https://github.com/wey-gu/grpo-graph-extraction 。 0.7 RAG 应用落地的挑战 众所周知,RAG 更多的是一种技术和功能,而非一个完整的产品。在企业级应用中,产品形态本身所面临的挑战,往往远超技术或原理上的难题。 举个例子,Excel 电子表格软件凭借其公式计算和数据可视化功能,为会计和企业数据处理领域带来了颠覆性的变革。理论上,只需学习 Excel 中的各类公式和图表功能,就能获得大量实战经验,成为专业的数据分析师。但实际上,我们观察到,大多数企业员工仅仅熟悉 Excel 的基本操作,而许多昂贵且高级的 SaaS 产品,其功能实际上也不过是 Excel 部分能力的延伸。 如果基于 RAG 能力构建的企业产品,只是一个具备强大应用编排和知识库索引能力的“无代码”平台,那么这样的平台很可能只能在企业内部的少部分用户或团队中落地。大部分宝贵的领域知识仍停留在个人或小圈子中,无法以智能高效的方式被全面激活和利用。 为了解决这一问题,我们基于 FusionGraphRAG 与 Agentic RAG 技术,打造了一个全新的高级知识库与低门槛应用平台 —— NebulaGraph AI 应用平台(内部命名为 “Catalyst”,即催化剂)。该平台的核心设计理念包括: 无需构建复杂 Workflow: 用户不需要自行搭建工作流。 无需编写繁琐 Prompt: 用户无需学习复杂的指令语法。 简化知识定义: 用户只需将对私有知识的理解转化为对不同“知识篮子”的定义。 智能对话交互: 用户通过与我们的“元智能体”交流,传达解决问题的方法。 这种设计大幅降低了使用门槛,使得企业内部的知识能够被更高效、更智能地激活与应用,真正实现“插上翅膀”的飞跃。 0.8 AI 应用平台 在这个平台中,企业用户可以将成百上千个文档轻松归入各自的知识“篮子”,并为每个篮子选择适合的“预学习”——即索引模式: 知识探索 用户可以随时在各个知识篮子中进行探索,快速定位所需信息,直观理解知识集合内的知识分布、深浅、特种。 智能应用生成 用户无需亲手编写复杂的 prompt,只需通过对话的方式与平台互动,即可生成各种类型的智能应用、迭代沉淀应用的 prompt。例如: “帮我构建一个投研报告生成助手” “我要做一个 xx 申报助手” “基于我所有的周报知识,构建一个 yy 领域回答助手,我想放到钉钉签名上,免得别人来烦我关于 yy 的问题” …… 平台内置了生成智能体应用的“元智能体”,让对话即成为开发工具。 问答搜索应用 平台支持构建问答型搜索应用,帮助用户快速获得精确答案。 知识溯源 同时,针对问答应用,基于连接的知识溯源,追踪答案的来源,确保信息透明可靠。 0.9 总结 GraphRAG 的发展代表了 RAG 技术从基础检索向高级知识管理的演进。通过解决传统 RAG 在知识稀释、关联丢失和宏观问题上的挑战,GraphRAG 为企业级应用提供了更高效、灵活的解决方案。我们团队开发的 Fusion GraphRAG 融合了智能文档处理(IDP)、多层次知识索引和图状结构,将知识的内在关联映射到检索过程中,显著提升了复杂任务的处理能力。 在实践中,我们通过优化图抽取成本(R1 知识蒸馏、 GRPO 方法、 Qwen2.5-3B 规模上的工作,并开源出来。),让 GraphRAG 技术更具经济可行性,使其能够广泛应用于企业知识管理和智能应用平台。 未来,我们相信 GraphRAG 将成为高级 RAG 和智能体记忆(Agent Memory)的核心基础设施,推动深层研究和智能决策的进一步发展。NebulaGraph AI 应用平台的推出,正是这一愿景的初步实现。 最后,对于 GraphRAG,这是 Sherman 和我们 GenAI team 每一位同学共同相信的观点: 狭义的 “GraphRAG”、甚至 “SubGraph RAG”,在特定场景仍然有优势,我们可以按需应用这个技术组合 Deep Research, Deep Search 的形态与 Graph-based RAG 方法,尤其是 Fusion GraphRAG 可以形成很好的互补 Graph Infra 是高级 RAG/Agent Memory 中不可绕过的结构与方法(Graph is inevitable in Advanced RAG),是以向量为基础的 RAG 的重要演进与互补

2025/2/28
articleCard.readMore

Graph RAG: 知识图谱结合 LLM 的检索增强

本文为大家揭示我们优先提出的 Graph RAG 方法,这种结合知识图谱、图数据库作为大模型结合私有知识系统最新的技术栈,作为之前的图上下文学习、text2cypher 文章的第三篇文章。 本文为大家揭示我们优先提出的 Graph RAG 方法,这种结合知识图谱、图数据库作为大模型结合私有知识系统最新的技术栈,作为之前的图上下文学习、text2cypher 文章的第三篇文章。 1 Graph RAG 在第一篇关于上下文学习的博客中我们介绍过, RAG(Retrieval Argumented Generation)这种基于特定任务/问题的文档检索范式中,我们通常先收集必要的上下文,然后利用具有认知能力的机器学习模型进行上下文学习(in-context learning),来合成任务的答案。 借助 LLM 这个只需要“说话”就可以灵活处理复杂问题的感知层,只需要两步,就能搭建一个基于私有知识的智能应用: 利用各种搜索方式(比如 Embedding 与向量数据库)从给定的文档中检索相关知识。 利用 LLM 理解并智能地合成答案。 而这篇博客中,我们结合最新的探索进展和思考,尝试把 Graph RAG 和其他方法进行比较,说得更透一点。并且,我们决定开始用 Graph RAG 这个叫法来描述它。 实际上,Graph RAG,是最先又我和 Jerry Liu 的直播研讨会讨论和相关的讨论的 Twitter Thread中提到的,差不多的内容我在 NebulaGraph 社区直播 中也用中文介绍过。 2 在 RAG 中知识图谱的价值 这部分内容我们在第一篇文章中阐述过,比如一个查询:“告诉我所有关于苹果和乔布斯的事”,基于乔布斯自传这本书进行问答,而这个问题涉及到的上下文分布在自传这本书的 30 页(分块)的时候,传统的“分割数据,Embedding 再向量搜索”方法在多个文档块里用 top-k 去搜索的方法很难得到这种分散,细粒的完整信息。而且,这种方法还很容易遗漏互相关联的文档块,从而导致信息检索不完整。 除此之外,在之后一次技术会议中,我有幸和 leadscloud.com 的徐旭讨论之后(他们因为有知识图谱的技术背景,也做了和我们类似的探索和尝试!),让我意识到知识图谱可以减少基于嵌入的语义搜索所导致的不准确性。徐旭给出的一个有趣的例子是“保温大棚”与“保温杯”,尽管在语义上两者是存在相关性的,但在大多数场景下,这种通用语义(Embedding)下的相关性常常是我们不希望产生的,进而作为错误的上下文而引入“幻觉”。 这时候,保有领域知识的知识图谱则是非常直接可以缓解、消除这种幻觉的手段。 3 用 NebulaGraph 实现 Graph RAG 一个简单的 Graph RAG 可以如下去简单实现: 使用LLM(或其他)模型从问题中提取关键实体。 根据这些实体检索子图,深入到一定的深度(例如,2)。 利用获得的上下文利用LLM产生答案。 python # 伪代码 def _get_key_entities(query_str, llm=None ,with_llm=True): ... return _expand_synonyms(entities) def _retrieve_subgraph_context(entities, depth=2, limit=30): ... return nebulagraph_store.get_relations(entities, depth, limit) def _synthesize_answer(query_str, graph_rag_context, llm): return llm.predict(PROMPT_SYNTHESIZE_AND_REFINE, query_str, graph_rag_context) def simple_graph_rag(query_str, nebulagraph_store, llm): entities = _get_key_entities(query_str, llm) graph_rag_context = _retrieve_subgraph_context(entities) return _synthesize_answer( query_str, graph_rag_context, llm) 然而,有了像 Llama Index 这样方便的 LLM 编排工具,开发者可以专注于 LLM 的编排逻辑和 pipeline 设计,而不用亲自处理很多细节的抽象与实现。 所以,用 Llama Index,我们可以轻松搭建 Graph RAG,甚至整合更复杂的 RAG 逻辑,比如 Graph+Vector RAG。 在 Llama Index 中,我们有两种方法实现 Graph RAG: KnowledgeGraphIndex 用来从任何私有数据只是从零构建知识图谱(基于 LLM 或者其他语言模型),然后 4 行代码进行 Graph RAG。 text graph_store = NebulaGraphStore( space_name=space_name, edge_types=edge_types, rel_prop_names=rel_prop_names, tags=tags, ) storage_context = StorageContext.from_defaults(graph_store=graph_store) # Build KG kg_index = KnowledgeGraphIndex.from_documents( documents, storage_context=storage_context, max_triplets_per_chunk=10, space_name=space_name, edge_types=edge_types, rel_prop_names=rel_prop_names, tags=tags, ) kg_query_engine = kg_index.as_query_engine() KnowledgeGraphRAGQueryEngine 则可以在任何已经存在的知识图谱上进行 Graph RAG,不过我还没有完成这个 PR。 text graph_store = NebulaGraphStore( space_name=space_name, edge_types=edge_types, rel_prop_names=rel_prop_names, tags=tags, ) storage_context = StorageContext.from_defaults(graph_store=graph_store) graph_rag_query_engine = KnowledgeGraphRAGQueryEngine( storage_context=storage_context, ) 最后,我做了一个 streamlit 的 demo来比较 Graph RAG 与 Vector RAG,从中我们可以看到 Graph RAG 并没有取代 Embedding、向量搜索的方法,而是增强了/补充了它的不足。 4 text2cypher 基于图谱的 LLM 的另一种有趣方法是text2cypher。这种方法不依赖于实体的子图检索,而是将任务/问题翻译成一个面向答案的特定图查询,和我们常说的 text2sql 方法本质是一样的。 4.1 在 NebulaGraph 上进行 text2cypher 在之前的文章中我们已经介绍过,得益于 LLM,实现 text2cypher 比传统的 ML 方法更为简单和便宜。 比如,LangChain: NebulaGraphQAChain 和 Llama Index: KnowledgeGraphQueryEngine 让我们 3 行代码就能跑起来 text2cypher。 4.2 比较 text2cypher 和 (Sub)Graph RAG 这两种方法主要在其检索机制上有所不同。text2cypher 根据 KG 的 Schema 和给定的任务生成图形模式查询,而SubGraph RAG获取相关的子图以提供上下文。 两者都有其优点,为了大家更直观理解他们的特点,我做了这个 demo 视频: 我们可以看到两者的图查询模式在可视化下是有非常清晰的差异的。 4.3 结合text2cypher的Graph RAG 然而,两者并没有绝对的好与坏,不同场景下,它们各有优劣。 在现实世界中,我们可能并不总是知道哪种方法更有效(好帮助区分应该用哪一种),因此,我倾向于考虑同时利用两者,这样获取的两种检索结果作为上下文,一起来生成最终答案的效果可能是最好的。 具体的实现方法在这个 PR中已经可以做到了,只需要设置with_text2cypher=True,Graph RAG 就会包含text2cypher 上下文,敬请期待它的合并。 5 结论 通过将知识图谱、图存储集成到 LLM 技术栈中,Graph RAG 把 RAG 的上下文学习推向了一个新的高度。它能在 LLM 应用中,通过利用现有(或新建)的知识图谱,提取细粒度、精确调整、领域特定且互联的知识。 请继续关注图谱和LLM领域的更深入的探索和进一步的发展。 题图 prompt: A vast open book serves as the backdrop, with intricately interwoven nodes and lines forming a Graph on its pages. At the center of this graph, there’s a glowing brain symbolizing the Knowledge Graph. Rays of light emanate from the brain, reaching every corner of the graph, mirroring neural connections linking diverse information. On the right side of the illustration, a robotic arm with a pen is swiftly writing, representing the input and output of the AI large language model.

2023/8/15
articleCard.readMore

Text2Cypher:大语言模型驱动的图谱查询生成

从 GPT-3 开始展现出超出预期的”理解能力“开始,我们一直在做 Graph + LLM 技术组合、互补的研究、探索和阜分享,截止到现在 NebulaGraph 已经在 LlamaIndex 与 Langchain 项目做出了不少领先的贡献,从本文开始,我们就把其中一些阶段性的成功、方法单独分享给大家。 本文的主题是我们认为这个领域最低垂的果实,text2cypher:自然语言生成图查询。 1 Text2Cypher 顾名思义, Text2Cypher 做的就是把自然语言的文本转换成 Cypher 查询语句的这件事儿,和另一个大家可能已经比较熟悉的场景 Text2SQL:文本转换 SQL 在形式上没有什么区别。而本质上,大多数知识图谱、图数据库的应用都是在图上按照人类意愿进行查询,我们在图数据库上构造方便的可视化工具、封装方便的 API 的工作都是为这个目标服务的。 一直以来,阻碍图数据库、知识图谱被更广泛应用的主要因素可能就是查询图数据库的门槛了。那么,在没有大语言模型的时候,我们是怎么做的呢? 2 传统的 Text2Cypher 文本到查询的这个领域在大语言模型之前就一直存在这样的需求,一直是知识图谱最常见的应用之一,比如 KBQA(基于知识库的问答系统)的系统内部本质上就是 text2cypher。 这里以我之前写的项目 Siwi (发音:/ˈsɪwi/, 一个基于篮球运动员数据集的问答应用)为例,了解一下它的后端架构: asciiarmor ┌─────────────┬───────────────────────────────────┐ │ Speech │ Frontend │ │ ┌──────────▼──────────┐ Siwi, /ˈsɪwi/ │ │ │ Web_Speech_API │ A PoC of Dialog System │ │ │ Vue.JS │ With Graph Database │ │ │ │ Backed Knowledge Graph │ │ └──────────┬──────────┘ │ │ │ Sentence Backend │ │┌────────────┼────────────────────────────┐ │ ││ ┌──────────▼──────────┐ │ │ ││ │ Web API, Flask │ ./app/ │ │ ││ └──────────┬──────────┘ │ │ ││ │ Sentence ./bot/ │ │ ││ ┌──────────▼──────────┐ │ │ ││ │ Intent Matching, │ ./bot/classifier│ │ ││ │ Symentic Processing │ │ │ ││ └──────────┬──────────┘ │ │ ││ │ Intent, Enties │ │ ││ ┌──────────▼──────────┐ │ │ ││ │ Intent Actor │ ./bot/actions │ │ │└─┴──────────┬──────────┴─────────────────┘ │ │ │ Graph Query │ │ ┌──────────▼──────────┐ │ │ │ Graph Database │ NebulaGraph │ │ └─────────────────────┘ │ └─────────────────────────────────────────────────┘ 当一个问题语句发送过来之后,它首先要做意图识别(Intent)、实体识别(Entity),然后再利用 NLP 模型或者代码把相应的意图和实体构造成知识图谱的查询语句,最终查询图数据库,并根据返回结构构造答案。 可以想象,让程序能够: 从自然语言中理解意图:对应到哪一类支持回答的问题 找出实体:问题中涉及到的主要个体 从意图和实体构造查询语句 不可能是一个容易的开发工作,一个真正能够落地的实现要训练的模型或者实现的规则代码所考虑的边界条件可能非常多。 3 用语言模型做 Text2Cypher 而在”后大语言模型“时代,这种从前需要专门训练或者写规则的”智能“应用场景成了通用模型+提示工程(Prompt Engineering)就能完成的任务。 注:提示工程指通过自然语言描述,让生成模型、语言模型完成”智能“任务的方法。 事实上,在 GPT-3 刚发布之后,我就开始利用它帮助我写很多非常复杂的 Cypher 查询语句了,我发现它可以写很多非常复杂的模式匹配、多步条件那种之前我需要一点点调试半天才能写出来的语句,通常在它的答案之上,我只需要稍微修改就可以了,而且往往我还能从它的答案里知道我之前没了解到的 Cypher 语法盲区。 后来,在今年二月份的时候,我就试着实现了一个基于 GPT-3 (因为那时候还没有 GPT-3.5)的项目:ngql-GPT(代码仓库)。 它的工作原理非常简单,和 Text2SQL 没有区别,语言模型已经通过公共领域学习了 Cypher 的语法表达,我们在提出任务的时候,只需要让大模型知道我们要查询的图的 Schema 作为上下文就可以了。 所以,基本上 Prompt 就是: text 你是一位 NebulaGraph Cypher 专家,请根据给定的图 Schema 和问题,写出查询语句。 schema 如下: --- {schema} --- 问题如下: --- {question} --- 下面写出查询语句: 然而,真实世界的 prompt 往往还需要增加额外的要求: 只返回语句,不用给出解释,不用道歉 强调不要写超出 schema 之外的点、边类型 感兴趣的同学可以参考我在 LlamaIndex 的 KnowlegeGraph Query Engine 中的实现。 在真实场景中,我们想快速学习、构建大语言模型应用的时候,常常会用到 Langchain 或者 LlamaIndex 这样的编排(Orchestrator)工具,它们可以帮我们做很多合理的抽象,从而避免从头去实现很多通用的脚手架代码: 和不同语言模型交互 和不同向量数据库交互 数据分割 而且,这些编排工具还内置了很多工程方法的最佳实践,这样,我们常常调用一个方法就可以用到最新最好用的大语言模型研究论文的方法了,比如 FLARE、Guidence。 为此,我在 LlamaIndex 和 Langchain 中都贡献了可以方便进行 NebulaGraph 上 Text2Cypher 的工具,真正做到 3 行代码,Text2Cypher。 4 NebulaGraph 上的 Text2Cypher 在 LlamaIndex 的 KnowledgeQueryEngine 和 LangChain 的 NebulaGraphQAChain 中:NebulaGraph 图数据库的 Schema 获取、Cypher 语句生成的 Prompt、各种 LLM 的调用、结果的处理、衔接我们可以全都不用关心,开箱即用! 4.1 使用 LlamaIndex 用 LlamaIndex,我们只需要: 创建一个 NebulaGraphStore 实例 创建一个 KnowledgeQueryEngine 就可以直接进行问答了,是不是超级简单? 参考文档:https://gpt-index.readthedocs.io/en/latest/examples/query_engine/knowledge_graph_query_engine.html python from llama_index.query_engine import KnowledgeGraphQueryEngine from llama_index.storage.storage_context import StorageContext from llama_index.graph_stores import NebulaGraphStore graph_store = NebulaGraphStore( space_name=space_name, edge_types=edge_types, rel_prop_names=rel_prop_names, tags=tags) storage_context = StorageContext.from_defaults(graph_store=graph_store) nl2kg_query_engine = KnowledgeGraphQueryEngine( storage_context=storage_context, service_context=service_context, llm=llm, verbose=True, ) # 问答 response = nl2kg_query_engine.query( "Tell me about Peter Quill?", ) # 只生成语句 graph_query = nl2kg_query_engine.generate_query( "Tell me about Peter Quill?", ) 4.2 使用 Langchain 类似的,在 Langchain 里,我们只需要: 创建一个 NebulaGraph实例 创建一个 NebulaGraphQAChain 实例 就可以直接提问了。 参考文档:https://python.langchain.com/docs/modules/chains/additional/graph_nebula_qa python from langchain.chat_models import ChatOpenAI from langchain.chains import NebulaGraphQAChain from langchain.graphs import NebulaGraph graph = NebulaGraph( space=space_name, username="root", password="nebula", address="127.0.0.1", port=9669, session_pool_size=30, ) chain = NebulaGraphQAChain.from_llm( llm, graph=graph, verbose=True ) chain.run( "Tell me about Peter Quill?", ) 5 Demo demo 地址 这个 Demo 展示了如何利用 LLM 从不同类型的信息源(以维基百科为例)中抽取知识三元组,并存储到图数据库 NebulaGraph 中。 本 Demo 中,我们先抽取了维基百科中关于《银河护卫队3》的信息,然后利用 LLM 生成的知识三元组,构建了一个图谱。 然后利用 Cypher 查询图谱,最后利用 LlamaIndex 和 Langchain 中的 Text2Cypher,实现了自然语言查询图谱的功能。 您可以点击其他标签亲自试玩图谱的可视化、Cypher 查询、自然语言查询(Text2Cypher)等功能。 这里可以下载 完整的 Notebook。 6 结论 有了 LLM,知识图谱、NebulaGraph 图数据库中的的数据中进行 Text2Cypher 从来没有这么简单过。 一个具有更强人机、机器接入的知识图谱可以代表了全新的时代,我们可能不需要从前那样高额成本去实现图库之上的后端服务,也不再需要培训才能让领域专家从图中获取重要的洞察了。 利用 LlamaIndex 或者 Langchain 中的生态集成,我们可以几乎没有开发成本地几行代码把自己的应用、图数据智能化。 然而,Text2Cypher 只是一个开始,请大家关注我们后续的文章,展现更多知识图谱、图数据库为大语言模型生态带来的变革。 题图 prompt: In an artful fusion of language and AI, this minimalist oil painting captures the essence of technological advancement. Delicate brushstrokes depict a harmony of binary code and flowing words, converging into a central point. With a refined color palette and clean composition, the artwork represents the symbiotic relationship between language and artificial intelligence, inviting contemplation and appreciation.

2023/7/17
articleCard.readMore

NebulaGraph in Jupyter Notebook

现在,我们可以在 Jupyter Notebook 中更方便地玩 NebulaGraph 图数据库了,只需要%ngql MATCH p=(n:player)->() RETURN p 就可以直接查询 ,%ng_draw 就可以画出返回结果。 English version 最近,我把两年前一直没完成的 NebulaGraph 的 Jupyter Notebook 扩展: ipython-ngql 重构,正式发布了,现在它除了完全适配 NebulaGrpah 3.x 所有查询之外,还支持了 Notebook 内的返回结果可视化,本文给大家介绍一下如何使用 ipython-ngql ! 1 安装 安装非常简单,只需要在 Jupyter Notebook 里边执行 %pip install ipython-ngql 然后再加载它就好: python %pip install ipython-ngql %load_ext ngql 然后,我们就可以用 %ngql 这个 Jupyter Magic word 连接 NebulaGraph 了: python %ngql --address 127.0.0.1 --port 9669 --user root --password nebula 当连接成功之后,SHOW SPACES 的结果会返回在这个 notebook cell 下。 💡 注:你可以从 Docker 桌面版的扩展市场里搜索 NebulaGraph 一键安装本地开发环境,进入 NebulaGraph Docker 扩展内部,点击 NebulaGraph AI ,点击 Install NX Mode 安装本地的 NebulaGraph + Jupyter Notebook 开发环境。 2 查询 现在支持两种语法 %ngql 接单行查询和 %%ngql 接多行查询。 2.1 单行查询 例如: text %ngql USE basketballplayer; %ngql MATCH (v:player{name:"Tim Duncan"})-->(v2:player) RETURN v2.player.name AS Name; 2.2 多行查询 例如: python %%ngql ADD HOSTS "storaged3":9779,"storaged4":9779; SHOW HOSTS; 3 渲染结果 而在任意一个查询之后,紧跟着一个 %ng_draw 就可以把结果可视化渲染出来: python # one query %ngql GET SUBGRAPH 2 STEPS FROM "player101" YIELD VERTICES AS nodes, EDGES AS relationships; %ng_draw # another query %ngql match p=(:player)-[]->() return p LIMIT 5 %ng_draw 效果: 并且,渲染的结果还会被保存为单文件 html ,这样我们可以内嵌到任意网页中,像是: 4 高级应用 下面还有一些方便的高级应用。比如 %ngql help 可以获得更多帮助信息。 4.1 操作查询结果为 pandas df 每一次 query 之后,返回的结果会被存到 _ 变量中,我们可以对它进行读取: 4.2 返回原始 ResultSet 默认,返回的结果的格式是 pandas df,而如果我们想在 Jupyter Notebook 中交互调试 Python 的 NebulaGraph 应用代码的时候,我们也可以将返回结果设置为原始的 ResultSet 格式,方便直观进行 Query 与结果解析,例如: python In [1] : %config IPythonNGQL.ngql_result_style="raw" In [2] : %%ngql USE pokemon_club; ...: GO FROM "Tom" OVER owns_pokemon YIELD owns_pokemon._dst as pokemon_id ...: | GO FROM $-.pokemon_id OVER owns_pokemon REVERSELY YIELD owns_pokemon._dst AS Trainer_Name; ...: ...: Out[3]: ResultSet(ExecutionResponse( error_code=0, latency_in_us=3270, data=DataSet( column_names=[b'Trainer_Name'], rows=[Row( values=[Value( sVal=b'Tom')]), ... Row( values=[Value( sVal=b'Wey')])]), space_name=b'pokemon_club')) In [4]: r = _ In [5]: r.column_values(key='Trainer_Name')[0].cast() Out[5]: 'Tom' 4.3 查询模板 此外,我还给大家支持了模板功能,语法沿用了 Jinja2 的 {{ variable }} ,详见这个例子: 5 未来 之后,我打算增强可视化的自定义选项,也欢迎社区里的大伙来贡献新的 feature、idea。 项目的 repo 在 👉🏻 https://github.com/wey-gu/ipython-ngql

2023/6/6
articleCard.readMore

图谱驱动的大语言模型 Llama Index

如何利用图谱构建更好的 In-context Learning 大语言模型应用。 English version 注:本文是我最初以英文撰写的,然后麻烦 ChatGPT 帮我翻译成了英文,翻译的 prompt 是: text In this thread, you are a Chinese Tech blogger to help translate my blog in markdown from English into Chinese, the blog style is clear, fun yet professional. I will paste chapters in markdown to you and you will send back the translated and polished version. 1 LLM 应用的范式 作为认知智能的一大突破,LLM 已经改变了许多行业,以一种我们没有预料到的方式进行自动化、加速和启用。每天都会看到新的 LLN 应用被创建出来,我们仍然在探索如何利用这种魔力的新方法和用例。 将 LLM 引入流程的最典型模式之一是要求 LLM 根据专有的/特定领域的知识理解事物。目前,我们可以向 LLM 添加两种范式以获取这些知识:微调——fine-tune和上下文学习—— in-context learning。 微调是指对 LLM 模型进行附加训练,以增加额外的知识;而上下文学习是在查询提示中添加一些额外的知识。我们目前观察到,由于其简单性,上下文学习比微调更受欢迎。 在本博客中,我将分享我们在上下文学习方法方面所做的工作。 2 Llama Index:数据与 LLM 之间的接口 2.1 上下文学习 上下文学习的基本思想是使用现有的 LLM(未更新)来处理特定知识数据集的特殊任务。 例如,要构建一个可以回答关于某个人的任何问题,甚至扮演一个人的数字化化身的应用程序,我们可以将上下文学习应用于一本自传书籍和 LLM。在实践中,应用程序将使用用户的问题和从书中"搜索"到的一些信息构建提示,然后查询 LLM 来获取答案。 asciiarmor ┌───────┐ ┌─────────────────┐ ┌─────────┐ │ │ │ Docs/Knowledge │ │ │ │ │ └─────────────────┘ │ │ │ User │─────────────────────────────────────▶ LLM │ │ │ │ │ │ │ │ │ └───────┘ └─────────┘ 在这种搜索方法中,实现从文档/知识(上述示例中的那本书)中获取与特定任务相关信息的最有效方式之一是利用嵌入(Embedding)。 2.2 嵌入(Embedding) 嵌入通常指的是将现实世界的事物映射到多维空间中的向量的方法。例如,我们可以将图像映射到一个(64 x 64)维度的空间中,如果映射足够好,两个图像之间的距离可以反映它们的相似性。 嵌入的另一个例子是 word2vec 算法,它将每个单词都映射到一个向量中。例如,如果嵌入足够好,我们可以对它们进行加法和减法操作,可能会得到以下结果: vec(apple) + vec(pie) =~ vec("apple apie") 或者向量测量值 vec(apple) + vec(pie) - vec("apple apie") 趋近于0: |vec(apple) + vec(pie) - vec("apple apie")| =~ 0 类似地,“pear” 应该比 “dinosaur” 更接近 “apple”: |vec(apple) - vec(pear)| < |vec(apple) - vec(dinosaur)| 有了这个基础,理论上我们可以搜索与给定问题更相关的书籍片段。基本过程如下: 将书籍分割为小片段,为每个片段创建嵌入并存储它们 当有一个问题时,计算问题的嵌入 通过计算距离找到与书籍片段最相似的前 K 个嵌入 使用问题和书籍片段构建提示 使用提示查询 LLM asciiarmor ┌────┬────┬────┬────┐ │ 1 │ 2 │ 3 │ 4 │ ├────┴────┴────┴────┤ │ Docs/Knowledge │ ┌───────┐ │ ... │ ┌─────────┐ │ │ ├────┬────┬────┬────┤ │ │ │ │ │ 95 │ 96 │ │ │ │ │ │ │ └────┴────┴────┴────┘ │ │ │ User │─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─▶ LLM │ │ │ │ │ │ │ │ │ └───────┘ ┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐ └─────────┘ │ ┌──────────────────────────┐ ▲ └────────┼▶│ Tell me ....., please │├───────┘ └──────────────────────────┘ │ ┌────┐ ┌────┐ │ │ 3 │ │ 96 │ │ └────┘ └────┘ │ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ 2.3 Llama Index Llama Index 是一个开源工具包,它能帮助我们以最佳实践去做 in-context learning: 它提供了各种数据加载器,以统一格式序列化文档/知识,例如 PDF、维基百科页面、Notion、Twitter 等等,这样我们可以无需自行处理预处理、将数据分割为片段等操作。 它还可以帮助我们创建嵌入(以及其他形式的索引),并以一行代码的方式存储嵌入(在内存中或向量数据库中)。 它内置了提示和其他工程实现,因此我们无需从头开始创建和研究,例如,用4行代码在现有数据上创建一个聊天机器人。 3 文档分割和嵌入的问题 嵌入和向量搜索在许多情况下效果良好,但在某些情况下仍存在挑战,其中之一是可能丢失全局上下文/跨节点上下文。 想象一下,当查询"请告诉我关于作者和 foo 的事情",在这本书中,假设编号为 1、3、6、19~25、30~44 和 96~99 的分段都涉及到 foo 这个主题。则在这种情况下,简单地搜索与书籍片段相关的前 k 个嵌入可能效果不尽人意,因为这时候只考虑与之最相关的几个片段(比如 k = 3),从而丢失了许多上下文信息。 asciiarmor ┌────┬────┬────┬────┐ │ 1 │ 2 │ 3 │ 4 │ ├────┴────┴────┴────┤ │ Docs/Knowledge │ │ ... │ ├────┬────┬────┬────┤ │ 95 │ 96 │ │ │ └────┴────┴────┴────┘ 而解决、缓解这个问题的方法,在 Llama Index 工具的语境下,就是创建组合索引和综合索引。 其中,向量存储(VectorStore)只是其中的一部分。除此之外,我们可以定义一个摘要索引和/或树形索引等,以将不同类型的问题路由到不同的索引,从而避免在需要全局上下文时丧失它。 然而,借助知识图谱,我们可以采取更有意思的方法: 4 知识图谱 知识图谱这个术语最初由谷歌在2012年5月提出,作为其增强搜索结果和向用户提供更多上下文信息的努力的一部分。知识图谱旨在理解实体之间的关系,并直接提供查询的答案,而不仅仅返回相关网页的列表。 知识图谱是一种以图形格式组织和连接信息的方式,其中节点表示实体,边表示实体之间的关系。图形结构允许高效地存储、检索和分析数据。 它的结构如下图所示: 那么知识图谱到底能怎么帮到我们呢? 5 嵌入和知识图谱的结合 这里的基本思想是,作为信息的精炼格式,知识图谱可以以比我们对原始数据/文档进行的分割更小的粒度进行查询/搜索。因此,通过不替换大块的数据,而是将两者结合起来,我们可以更好地搜索需要全局/跨节点上下文的查询。 请看下面的图示,假设问题是关于 x 的,所有数据片段中有20个与它高度相关。现在,除了获取主要上下文的前3个文档片段(比如编号为 1、2 和 96 的文档片段),我们还从知识图谱中对 x 进行两次跳转查询,那么完整的上下文将包括: 问题:“Tell me things about the author and x” 来自文档片段编号 1、2 和 96 的原始文档,在 Llama Index 中,它们被称为节点 1、节点 2 和节点 96。 包含 “x” 的知识图谱中的 10 个三元组,通过对 x 进行两层深度的图遍历得到: x -> y(来自节点 1) x -> a(来自节点 2) x -> m(来自节点 4) x <- b-> c(来自节点 95) x -> d(来自节点 96) n -> x(来自节点 98) x <- z <- i(来自节点 1 和节点 3) x <- z <- b(来自节点 1 和节点 95) asciiarmor ┌──────────────────┬──────────────────┬──────────────────┬──────────────────┐ │ .─. .─. │ .─. .─. │ .─. │ .─. .─. │ │( x )─────▶ y ) │ ( x )─────▶ a ) │ ( j ) │ ( m )◀────( x ) │ │ `▲' `─' │ `─' `─' │ `─' │ `─' `─' │ │ │ 1 │ 2 │ 3 │ │ 4 │ │ .─. │ │ .▼. │ │ │( z )─────────────┼──────────────────┼──────────▶( i )─┐│ │ │ `◀────┐ │ │ `─' ││ │ ├───────┼──────────┴──────────────────┴─────────────────┼┴──────────────────┤ │ │ Docs/Knowledge │ │ │ │ ... │ │ │ │ │ │ ├───────┼──────────┬──────────────────┬─────────────────┼┬──────────────────┤ │ .─. └──────. │ .─. │ ││ .─. │ │ ( x ◀─────( b ) │ ( x ) │ └┼▶( n ) │ │ `─' `─' │ `─' │ │ `─' │ │ 95 │ │ │ 96 │ │ │ 98 │ │ .▼. │ .▼. │ │ ▼ │ │ ( c ) │ ( d ) │ │ .─. │ │ `─' │ `─' │ │ ( x ) │ └──────────────────┴──────────────────┴──────────────────┴──`─'─────────────┘ 显然,那些(可能很宝贵的)涉及到主题 x 的精炼信息来自于其他节点以及跨节点的信息,都因为我们引入知识图谱的步骤,而能够被包含在 prompt 中,用于进行上下文学习,从而克服了前边提到的问题。 6 Llama Index 中的知识图谱进展 最初,William F.H.将知识图谱的抽象概念引入了 Llama Index,其中知识图谱中的三元组与关键词相关联,并存储在内存中的文档中,随后Logan Markewich还增加了每个三元组的嵌入。 最近的几周中,我一直在与社区合作,致力于将 “GraphStore” 存储上下文引入 Llama Index,从而引入了知识图谱的外部存储。首个实现是使用我自从 2021 年以来一直在开发的开源分布式图数据库 NebulaGraph。 在实现过程中,还引入了遍历图的多个跳数选项以及在前 k 个节点中收集更多关键实体的选项(用于在知识图谱中搜索以获得更多全局上下文),我们仍在对这些变更进行完善。 引入 GraphStore 后,还可以从现有的知识图谱中进行上下文学习,并与其他索引结合使用,这也非常有前景,因为知识图谱被认为具有比其他结构化数据更高的信息密度。 在接下来的几周里,我将在本博客中更新有关 Llama Index 中的知识图谱相关工作的内容,然后在 PR 合并后,分享端到端的演示项目和教程。请继续关注!

2023/6/1
articleCard.readMore

Nebulagraph Artificial Intelligence Suite

介绍新项目! ng_ai:NebulaGraph 的图算法套件,好用的 NebulaGraph 的 high-level Python Algorithm API,它的目标是让 NebulaGraph 的数据科学家用户能够用很少的代码量执行图上的算法相关的任务。 1 Nebulagraph AI 套件 这周,NebulaGraph 3.5.0 发布啦,@whitewum 吴老师建议我们把而之前一段时间 NebulaGraph 社区里开启的新项目 ng_ai 公开给大家,本文就是第一篇介绍 ng_ai 的文章! 1.1 ng_ai 是什么 ng_ai 的全名是:Nebulagraph AI Suite,顾名思义,它是在 NebulaGraph 之上跑算法的 Python 套件,希望能给 NebulaGraph 的数据科学家用户一个自然、简洁的高级 API,用很少的代码量执行图上的算法相关的任务。 在 ng_ai 这个开源项目里,我们希望快速迭代、公开讨论、演进它,而这背后的目标是: Simplifying things in surprising ways. 1.2 ng_ai 的特点 为了让 NebulaGraph 社区的同学拥有顺滑的算法体验,ng_ai 有以下特点: 与 NebulaGraph 紧密结合,方便从其中读、写图数据 支持多引擎、后端,目前支持 Spark(NebulaGraph Algorithm)、NetworkX,之后会支持 DGL、PyG 友好、符合直觉的 API 设计 与 NebulaGraph 的 UDF 无缝结合,支持从 Query 中调用 ng_ai 任务 友好的自定义算法接口,方便用户自己实现算法(尚未完成) 一键试玩环境(基于 Docker Extention) 2 我可以用 ng_ai 干什么 2.1 跑分布式 pagerank 算法 如果在一个大图上,基于 Nebula-Algorithms 分布式地跑 pagerank 算法,我们可以这么做: python from ng_ai import NebulaReader # read data with spark engine, scan mode reader = NebulaReader(engine="spark") reader.scan(edge="follow", props="degree") df = reader.read() # run pagerank algorithm pr_result = df.algo.pagerank(reset_prob=0.15, max_iter=10) 2.2 写回算法结果到 NebulaGraph 假设我们要跑一个 label propagation 算法,然后把结果写回 NebulaGraph,我们可以这么做: 先确保要写回 TAG 的 schema 已经创建好了,写到 label_propagation.cluster_id 字段里: sql CREATE TAG IF NOT EXISTS label_propagation ( cluster_id string NOT NULL ); 我们先执行算法: python df_result = df.algo.label_propagation() 再看一下结果的 schema: python df_result.printSchema() root |-- _id: string (nullable = false) |-- lpa: string (nullable = false) 然后,代码里这么写,我们把 lpa 的结果写回 NebulaGraph 中的 cluster_id 字段里({"lpa": "cluster_id"}): python from ng_ai import NebulaWriter from ng_ai.config import NebulaGraphConfig config = NebulaGraphConfig() writer = NebulaWriter( data=df_result, sink="nebulagraph_vertex", config=config, engine="spark" ) # map column louvain into property cluster_id properties = {"lpa": "cluster_id"} writer.set_options( tag="label_propagation", vid_field="_id", properties=properties, batch_size=256, write_mode="insert", ) # write back to NebulaGraph writer.write() 最后,我们可以验证一下结果啦: cypher USE basketballplayer; MATCH (v:label_propagation) RETURN id(v), v.label_propagation.cluster_id LIMIT 3; 结果: SQL +-------------+--------------------------------+ | id(v) | v.label_propagation.cluster_id | +-------------+--------------------------------+ | "player103" | "player101" | | "player113" | "player129" | | "player121" | "player129" | +-------------+--------------------------------+ 更详细的例子参考:ng_ai/examples 2.2.1 通过 nGQL 调用算法 从 NebulaGraph 3.5.0 之后,我们可以写自己的 UDF 来从 nGQL 里调用自己实现的函数,ng_ai 也用这个能力来实现了一个 ng_ai 函数,它可以从 nGQL 里调用 ng_ai 的算法,例如: sql -- Prepare the write schema USE basketballplayer; CREATE TAG IF NOT EXISTS pagerank(pagerank string); :sleep 20; -- Call with ng_ai() RETURN ng_ai("pagerank", ["follow"], ["degree"], "spark", {space: "basketballplayer", max_iter: 10}, {write_mode: "insert"}) 更详细的例子参考:ng_ai/examples 2.2.2 单机运行算法 在单机、本地的环境里,ng_ai 支持基于 NetworkX 运行算法,例如: 读取图为 ng_ai graph 对象: python from ng_ai import NebulaReader from ng_ai.config import NebulaGraphConfig # read data with nebula/networkx engine, query mode config_dict = { "graphd_hosts": "graphd:9669", "user": "root", "password": "nebula", "space": "basketballplayer", } config = NebulaGraphConfig(**config_dict) reader = NebulaReader(engine="nebula", config=config) reader.query(edges=["follow", "serve"], props=[["degree"], []]) g = reader.read() 查看、画图: python g.show(10) g.draw() 运行算法: python pr_result = g.algo.pagerank(reset_prob=0.15, max_iter=10) 写回 NebulaGraph: python from ng_ai import NebulaWriter writer = NebulaWriter( data=pr_result, sink="nebulagraph_vertex", config=config, engine="nebula", ) # properties to write properties = ["pagerank"] writer.set_options( tag="pagerank", properties=properties, batch_size=256, write_mode="insert", ) # write back to NebulaGraph writer.write() 其他算法: python # get all algorithms g.algo.get_all_algo() # get help of each algo help(g.algo.node2vec) # call the algo g.algo.node2vec() 更详细的例子参考:ng_ai/examples 2.2.3 可视化图算法结果 再演示一个 NetworkX 引擎情况下,计算 Louvain、PageRank 并可视化的例子: 先执行两个算法: python pr_result = g.algo.pagerank(reset_prob=0.15, max_iter=10) louvain_result = g.algo.louvain() 这次我们手写一个好看一点的画图函数: python from matplotlib.colors import ListedColormap def draw_graph_louvain_pr(G, pr_result, louvain_result, colors=["#1984c5", "#22a7f0", "#63bff0", "#a7d5ed", "#e2e2e2", "#e1a692", "#de6e56", "#e14b31", "#c23728"]): # Define positions for the nodes pos = nx.spring_layout(G) # Create a figure and set the axis limits fig, ax = plt.subplots(figsize=(35, 15)) ax.set_xlim(-1, 1) ax.set_ylim(-1, 1) # Create a colormap from the colors list cmap = ListedColormap(colors) # Draw the nodes and edges of the graph node_colors = [louvain_result[node] for node in G.nodes()] node_sizes = [70000 * pr_result[node] for node in G.nodes()] nx.draw_networkx_nodes(G, pos=pos, ax=ax, node_color=node_colors, node_size=node_sizes, cmap=cmap, vmin=0, vmax=max(louvain_result.values())) nx.draw_networkx_edges(G, pos=pos, ax=ax, edge_color='gray', width=1, connectionstyle='arc3, rad=0.2', arrowstyle='-|>', arrows=True) # Extract edge labels as a dictionary edge_labels = nx.get_edge_attributes(G, 'label') # Add edge labels to the graph for edge, label in edge_labels.items(): ax.text((pos[edge[0]][0] + pos[edge[1]][0])/2, (pos[edge[0]][1] + pos[edge[1]][1])/2, label, fontsize=12, color='black', ha='center', va='center') # Add node labels to the graph node_labels = {n: G.nodes[n]['label'] if 'label' in G.nodes[n] else n for n in G.nodes()} nx.draw_networkx_labels(G, pos=pos, ax=ax, labels=node_labels, font_size=12, font_color='black') # Add colorbar for community colors sm = plt.cm.ScalarMappable(cmap=cmap, norm=plt.Normalize(vmin=0, vmax=max(louvain_result.values()))) sm.set_array([]) cbar = plt.colorbar(sm, ax=ax, ticks=range(max(louvain_result.values()) + 1), shrink=0.5) cbar.ax.set_yticklabels([f'Community {i}' for i in range(max(louvain_result.values()) + 1)]) # Show the figure plt.show() draw_graph_louvain_pr(G, pr_result=pr_result, louvain_result=louvain_result) 效果如图: 更详细的例子参考:ng_ai/examples 2.2.4 更方便的 Notebook 操作 NebulaGraph 结合 NebulaGraph 的 Jupyter Notebook 插件: https://github.com/wey-gu/ipython-ngql ,我们还可以更方便的操作 NebulaGraph: 在 Jupyter Notbook 里安装这个插件可以通过 ng_ai 的 extras 安装: python %pip install ng_ai[jupyter] %load_ext ngql 也可以单独安装 python %pip install ipython-ngql %load_ext ngql 之后,我们就可以在 Notebook 里直接使用 %ngql 命令来执行 NGQL 语句了: python %ngql --address 127.0.0.1 --port 9669 --user root --password nebula %ngql USE basketballplayer; %ngql MATCH (v:player{name:"Tim Duncan"})-->(v2:player) RETURN v2.player.name AS Name; 注,多行的 Query 用两个百分号就好了 %%ngql 最后,我们还能在 Jupyter Notebook 里直接可视化渲染结果!只需要 %ng_draw 就可以啦! python %ngql match p=(:player)-[]->() return p LIMIT 5 %ng_draw 效果如下: 3 未来工作 现在 ng_ai 还在开发中,我们还有很多工作要做: 完善 reader 模式,现在 NebulaGraph/NetworkX 的读取数据只支持 Query-Mode,还需要支持 Scan-Mode 实现基于 dgl(GNN) 的链路预测、节点分类等算法,例如: python model = g.algo.gnn_link_prediction() result = model.train() # query src, dst to be predicted model.predict(src_vertex, dst_vertices) UDA,自定义算法 快速部署工具 ng_ai 是完全 build in public 的,欢迎社区的大家们来参与,一起来完善 ng_ai,让 NebulaGraph 上的 AI 算法更加简单易用! 4 试玩 ng_ai 我们已经准备好了一键部署的 NebulaGraph + Studio + ng_ai in Jupyter 的环境,只需要大家从 Docker Desktop 的 Extension(扩展)中搜索 NebulaGraph,就可以试完了。 安装 NebulaGraph Docker 插件 在 Docker Desktop 的插件市场搜索 NebulaGraph,点击安装 安装 ng_ai playground 进入 NebulaGraph 插件,点击Install NX Mode,安装 ng_ai 的 NetworkX playground,通常要等几分钟等待安装完成。 进入 NetworkX playground 点击Jupyter NB NetworkX,进入 NetworkX playground。 5 ng_ai 的架构 ng_ai 的架构如下,它的核心模块有: Reader:负责从 NebulaGraph 读取数据 Writer:负责将数据写入 NebulaGraph *Engine:负责适配不同运行时,例如 Spark、DGL、NetowrkX 等 Algo:算法模块,例如 PageRank、Louvain、GNN_Link_Predict 等 此外,为了支持 nGQL 中的调用,还有两个模块: ng_ai-udf:负责将 UDF 注册到 NebulaGraph,接受 ng_ai 的 query 调用,访问 ng_ai API ng_ai-api:ng_ai 的 API 服务,接受 UDF 的调用,访问 ng_ai 核心模块 asciiarmor ┌───────────────────────────────────────────────────┐ │ Spark Cluster │ │ .─────. .─────. .─────. .─────. │ │ ; : ; : ; : ; : │ ┌─▶│ : ; : ; : ; : ; │ │ │ ╲ ╱ ╲ ╱ ╲ ╱ ╲ ╱ │ │ │ `───' `───' `───' `───' │ Algo Spark │ Engine└───────────────────────────────────────────────────┘ │ ┌────────────────────────────────────────────────────┬──────────┐ └──┤ │ │ │ NebulaGraph AI Suite(ngai) │ ngai-api │◀─┐ │ │ │ │ │ └──────────┤ │ │ ┌────────┐ ┌──────┐ ┌────────┐ ┌─────┐ │ │ │ │ Reader │ │ Algo │ │ Writer │ │ GNN │ │ │ ┌───────▶│ └────────┘ └──────┘ └────────┘ └─────┘ │ │ │ │ │ │ │ │ │ │ │ │ ├────────────┴───┬────────┴─────┐ └──────┐ │ │ │ │ ▼ ▼ ▼ ▼ │ │ │ │ ┌─────────────┐ ┌──────────────┐ ┌──────────┐ ┌──────────┐ │ │ │ ┌──┤ │ SparkEngine │ │ NebulaEngine │ │ NetworkX │ │ DGLEngine│ │ │ │ │ │ └─────────────┘ └──────────────┘ └──────────┘ └──────────┘ │ │ │ │ └──────────┬────────────────────────────────────────────────────┘ │ │ │ │ Spark │ │ │ └────────Reader ────────────┐ │ │ Spark Query Mode │ │ │ Reader │ │ │Scan Mode ▼ ┌─────────┐ │ │ ┌───────────────────────────────────────────────────┬─────────┤ ngai-udf│◀─────────────┐ │ │ │ │ └─────────┤ │ │ │ │ NebulaGraph Graph Engine Nebula-GraphD │ ngai-GraphD │ │ │ │ ├──────────────────────────────┬────────────────────┼───────────────────┘ │ │ │ │ │ │ │ │ │ │ NebulaGraph Storage Engine │ │ │ │ │ │ │ │ │ │ └─▶│ Nebula-StorageD │ Nebula-Metad │ │ │ │ │ │ │ │ └──────────────────────────────┴────────────────────┘ │ │ │ │ ┌───────────────────────────────────────────────────────────────────────────────────────┐ │ │ │ RETURN ng_ai("pagerank", ["follow"], ["degree"], "spark", {space:"basketballplayer"}) │──┘ │ └───────────────────────────────────────────────────────────────────────────────────────┘ │ ┌─────────────────────────────────────────────────────────────┐ │ │ from ng_ai import NebulaReader │ │ │ │ │ │ # read data with spark engine, scan mode │ │ │ reader = NebulaReader(engine="spark") │ │ │ reader.scan(edge="follow", props="degree") │ └──│ df = reader.read() │ │ │ │ # run pagerank algorithm │ │ pr_result = df.algo.pagerank(reset_prob=0.15, max_iter=10) │ │ │ └─────────────────────────────────────────────────────────────┘

2023/5/2
articleCard.readMore

图数据库驱动的基础设施运维示例

图技术在大型、复杂基础设施之中 SRE/DevOps 的实践参考,本文以 OpenStack 系统之上的图数据库增强的运维案例为例,揭示图数据库、图算法的智能运维方法,全流程示例代码开源。 因为有一些还未采用图技术的 DevOps/Infra 领域同学在 NebulaGraph 社区询问参考的案例,我最近试着实践了一下如何利用图的能力与优势去帮助在复杂基础设施上构建辅助运维系统,希望能帮助到感兴趣 Infra Ops 领域、同时感兴趣图数据库、图算法的大家,全过程都是可以复现、并且开源的。 复杂的基础设施运维环境通常包含非常多、不同层面的资源(manifest),为了能够尽量还原真实世界的复杂环境、又保持这个实例项目的复杂度不会失控,我选择了用一个基础设施平台:OpenStack 作为例子。 本文实现了 OpenStack 系统上分别利用 Push 和 Pull 两种模式将资源之中被图谱建模的图中点、边信息加载到 NebulaGraph 里的 Graph ETL 管道的路径。 在图谱之上,本文探索如下用例: 告警、状态的推理与传导; 网络直连与互联关系; 镜像、云盘、快照血缘管理; 高相关性虚机预警; 秘钥泄漏的图上风控分析; 镜像、云盘漏洞范围分析; 宿主机逃离影响范围分析; 脆弱依赖资源检测; 1 试验环境搭建 1.1 背景知识 OpenStack 是一个开源的云计算平台,提供了类似于 AWS 的云服务。它提供了一组可插拔的模块,包括了计算,存储和网络等功能,可以帮助用户构建和管理云环境。OpenStack采用分布式架构,支持多种操作系统和硬件平台,可以在企业级和服务提供商级环境中使用。 最初由 NASA 和 Rackspace Inc. 发起的 nova (虚拟化计算项目)和 swift (兼容 S3 的对象存储)项目组成,OpenStack 现在由非常多不同的子项目组成: 本实验中,我们设计的 OpenStack 主要项目有: Nova 是 OpenStack 的计算服务,用于管理虚拟机。 Cinder 是 OpenStack 的块存储服务,用于管理云存储。 Neutron 是 OpenStack 的网络服务,用于管理云网络。 Glance 是 OpenStack 的镜像服务,用于管理云镜像。 Horizon 是 OpenStack 的可视化控制台服务。 除此之外,我还引入了 Vitrage 项目辅助我们收集部分资源数据: Vitrage 是 OpenStack 中的一个高级分析和可视化工具,用于分析和可视化 OpenStack 环境中的资源和事件。它可以汇集来自 OpenStack 各个服务的数据,并使用图形化方式展示出来。Vitrage 发现和诊断问题,提高 OpenStack 环境的可用性和可维护性。 得益于 OpenStack Decouple 的设计理念,Vitrage 可以很容易、无侵入式(只需要修改需要收集的服务两行配置)就可以在 OpenStack 的消息队列中订阅资源信息的 push 消息。 不过比较遗憾的是 Vitrage 这个项目已经有好多个 release cycle 没有什么大的更新了,应该是比较不活跃的状态了,比如在 zed 里,它的 Vitrage Dashboard 作为 Horizon 插件已经无法正常工作了,本实验只利用它的资源收集能力。 1.2 环境准备搭建 1.2.1 NebulaGraph 集群 首次快速试玩安装 NebulaGraph 的话,有这么几个选项: 阿里云上的 NebulaGraph 企业版(带有企业版独有的满血版可视化工具:Explorer,可以图探索、画布内跑图算法),可以获得一个月所有资源免费使用资格: 访问 https://www.siwei.io/try-aliyun 获得 Nebula-Up 一键安装 NebulaGraph 开源社区版本,需要一个带有 Docker、Docker Desktop 环境的机器 访问 https://github.com/wey-gu/nebula-up 有经验的同学可以参考文档进行部署: 访问 https://docs.nebula-graph.com.cn/ 1.2.2 OpenStack 集群 注意:如果大家已经有现成的 OpenStack 集群,这一步可以忽略,您只需要再去安装 OpenStack Vitrage 就好了。 本文需要的 OpenStack 集群是一个多机的环境,为此,我准备了在 Linux Server 上利用 Libvirt 和 Linux Bridge 搭建多个虚拟机用来模拟 OpenStack 的物理机,得益于 CPU 的嵌套虚拟化和 qemu,后边我们完全可以在虚拟机搭建的实验环境中模拟可正常工作的 OpenStack nova instance 虚机。 整个流程我都放在 https://github.com/wey-gu/openstack-graph/#environment-setup 这里了,感兴趣的同学可以访问、获取。 虚拟机搭建之后,我们还需要模拟真实的 Infra 环境,创建很多资源:整个过程也在 https://github.com/wey-gu/openstack-graph/#create-resources-on-openstack 有详细列出,想要动手操作的同学可以参考来亲自上手一下。 参考如上步骤操作之后,我们应该可以通过 Horizon Dashboard 查看集群和资源: 我们创建了几个虚拟机: 几个网盘,其中四个挂载在不同的虚拟机上 集群租户的网络拓扑: 我们还能通过 OpenStack Vitrage 的 API/CLI 获得部分主要资源的拓扑: bash source openrc admin admin vitrage topology show --all-tenants 它的结果是一个 JSON,里边已经按照边(links)和点(nodes)序列化图结构的数据了。 json { "directed": true, "graph": {}, "links": [ { "vitrage_is_deleted": false, "relationship_type": "contains", "source": 0, "target": 11, "key": "contains" }, { "vitrage_is_deleted": false, "relationship_type": "contains", "source": 0, "target": 13, "key": "contains" }, ... { "vitrage_is_deleted": false, "relationship_type": "attached", "source": 27, "target": 28, "key": "attached" } ], "multigraph": true, "nodes": [ { "id": "node0", "vitrage_type": "nova.host", "vitrage_category": "RESOURCE", "vitrage_is_deleted": false, "update_timestamp": "2023-01-13T08:06:48Z", "vitrage_sample_timestamp": "2023-01-13T08:06:49Z", "vitrage_is_placeholder": false, "vitrage_id": "630b4c2c-5347-4073-91a3-255ec18dadfc", "name": "node0", "vitrage_cached_id": "d043d278a6a712909e30e50ca8ec2364", "is_real_vitrage_id": true, "vitrage_aggregated_state": "AVAILABLE", "vitrage_operational_state": "OK", "vitrage_datasource_name": "nova.host", "state": "available", "graph_index": 0 }, { "id": "nova", "vitrage_type": "nova.zone", "vitrage_category": "RESOURCE", "vitrage_is_deleted": false, "vitrage_sample_timestamp": "2023-01-12T03:06:48Z", "vitrage_is_placeholder": false, "vitrage_id": "a1e9c808-dac8-4b59-8f80-f21a90e9869d", "vitrage_cached_id": "125f1d8c4451a6385cc2cfa2b0ba45be", "is_real_vitrage_id": true, "vitrage_aggregated_state": "AVAILABLE", "vitrage_operational_state": "OK", "state": "available", "update_timestamp": "2023-01-12T03:06:48Z", "name": "nova", "vitrage_datasource_name": "nova.zone", "graph_index": 1 }, ... "raw": true } 2 图谱建模 本实验环境中,我们考虑纳入如下资源进入图谱: nova instance: 是Nova服务中的虚拟机实例,每个nova instance都有自己的配置信息(如CPU、内存、磁盘等),有时候我们就叫它 server 或者 VM、虚机。 nova host是Nova服务中的物理主机,是nova instance运行的物理环境。nova host上面会运行nova-compute服务,这个服务负责管理和调度nova instance。nova host上面还可能运行其他服务,如网络服务等。 nova keypair: 是Nova服务中的密钥对,用于访问nova instance cinder volume: 是Cinder服务中的云存储卷,可以 attach 到nova instance上做为硬盘 cinder snapshot: 是Cinder服务中的云存储快照,可以在cinder volume上做快照 glance image: 是Glance服务中的镜像,可以作为创建nova instance时候的启动硬盘 neutron network: 是Neutron服务中的网络,可以用于配置nova instance的网络连接 neutron port: 是Neutron服务中的端口,用来连接nova instance和neutron network之间,在 nova instance 虚拟机上,一个 port 常常对应一个网卡(如果不是 trunk port 的话)。 他们之间的关系如下: 3 基础设施图 ETL 接下来我们解决从基础设施中抽取资源元数据的问题, 3.1 push 模式 这里的 push 指的是基础设施为主语,从资源方向我们的图谱系统主动、事件驱动地发出资源变动的信息。它的好处是资源中的实时性好,但是坏处是依赖基础设施自身,很多非常瘦的、软件定义/可编程程度不高的组件、比如某些硬件设备没有 push 机制,或者像是古老的软件系统不一定能存在 push 的接口,改造起来有侵入性。 前边提及过,OpenStack 自身是存在 Push hook 的机制的,它的子项目 vitrage 就利用这个机制很优雅地收集系统资源、告警等信息进入图中,类似的机制在其他平台中也是可以实现的。 本实验中我们就利用 vitrage 的机制去收集一部分图谱中的资源信息,如下图,可以看到 vitrage 会在 OpenStack message bus 中订阅 nova/cinder/neutron 等服务中的资源时间,把事件传入 Entity Queue,经过处理,存储到 Entity Graph 中。 在此之上,我们可以通过 vitrage API 获取图谱的拓扑,来消费它。 注意:实际上 Vitrage 服务还提供了推理告警、推理状态、定义决策事件的能力,这里我们并没有采用,后边我们在图上做的一些事情甚至还和它的能力有一些重叠。 这里我只是用它来展示 push 模式的工作机制,如果没有 Virtrage 这个项目存在,我们也可以比较容易通过 OpenStack 的 oslo.messaging 这个库很容易写出在 Message Bus(可能是 Kafka, RabbitMQ 等不同底层实现)上订阅资源时间的应用,然后把事件通过 Flink/ Kafka/ Pulsar 等方式接驳 NebulaGraph。 因为 Vitrage 的存在,我就偷懒不用去实现这部分逻辑,只消写一小部分代码调用 Vitrage API 取这个数据就可以了,讽刺的是,从这个角度来看,这其实是一种 pull 的模式了,不用拘泥它本质上算是哪一种方式,至少在资源发起测,我们把它当做 push 模式的例子看待吧。 这部分从 Vitrage 抓取的代码我放在 https://github.com/wey-gu/openstack-graph/blob/main/utils/vitrage_to_graph.py 了,调用方式很简单,在有 OpenStack 客户端的环境中,执行它就可以了,比如: bash # 连到 node0 上 ssh stack@node0_ip # 进入 devstack 目录 cd devstack # 下载 vitrage 中图数据,解析为 NeublaGraph DML/DQL 的工具 wget https://raw.githubusercontent.com/wey-gu/openstack-graph/main/utils/vitrage_to_graph.py # 执行它 python3 vitrage_to_graph.py 执行之后,会生成如下文件: schema.ngql 图数据的 Schema 定义 vertices/ 点数据的文件夹 edges/ 边数据的文件夹 3.2 pull 模式 反过来,pull 模式是从资源外部定期或者事件驱动地拉取资源,存入图谱的方式。刚好本实验中 vitrage 抓取的资源是有限的,有一些额外的资源我单独写了 python 的代码来主动全量抓取,pull 模式的好处是对资源方没有任何侵入性,只需要调用它的接口获取信息就可以了,坏处则是有的系统不太容易获得增量变化,可能只能全量去取。 这部分我抓取的关系如下: glance_used_by: image -[:used_by]-> instance (get from instance) glance_created_from: image -[:created_from]-> volume (get from image) nova_keypair_used_by: keypair -[:used_by]-> instance (get from instance) cinder_snapshot_created_from: volume snapshot -[:created_from]-> volume (get from snapshot) cinder_volume_created_from: volume -[:created_from]-> volume snapshot (get from volume) cinder_volume_created_from: volume -[:created_from]-> image (get from volume) 类似的,它的代码放在 https://github.com/wey-gu/openstack-graph/blob/main/utils/pull_resources_to_graph.py 之中,在真实场景下,我们可能会用 Apache Airflow、dagster 甚至是 cron job 等方式定期执行它。 我们手动执行的方式也很简单: bash # 连到 node0 上 ssh stack@node0_ip # 进入 devstack 目录 cd devstack # 下载抓取 OpenStack 资源,生成 NeublaGraph DML/DQL 的工具 wget https://raw.githubusercontent.com/wey-gu/openstack-graph/main/utils/pull_resources_to_graph.py.py # 执行它 python3 pull_resources_to_graph.py 执行之后,会生成点、边的 ngql 语句在两个文件夹下: vertices/ 点数据的文件夹 edges/ 边数据的文件夹 3.3 加载数据到 NebulaGraph 我们只需要在 NebulaGraph Studio Console, Explorer Console 或者 NebulaGraph 命令行 Console 中执行上边生成的 .ngql 文件就好了: bash # DDL from vitrage cat schema.ngql # DDL and DML for both push and pull mode data cat edges/*.ngql cat vertices/*.ngql 之后,在 NebulaGraph 中我们会有一个叫做 openstack 的图空间,用这个查询可以查到所有数据: cypher MATCH (n) WITH n LIMIT 1000 OPTIONAL MATCH p=(n)--() RETURN p, n 然后渲染在 explorer 中,手动设置一下数据的图标,就可以看到我们 OpenStack 集群里的所有租户的资源图了: 接下来我们终于可以在图上看看有意思的洞察了。 4 基于图谱的基础设施运维示例 作为非 SRE、DevOps 人员,我尝试藉由自己在 OpenStack 和图技术的理解想象出下边的一些实例,希望能帮助到需要的读者们。 4.1 告警、状态的推理与传导 这部分我收到了 vitrage 项目的启发,参考它们给出的实例文档:这里。 借助资源图谱实时图查询、图计算甚至图可视化能力,我们可以在图上推理、传导一些信息,把重要的时间藉由图上组织好的知识分发到需要收到通知的人、组织、系统。 一个简单的例子是,比如我们在 nova host(虚拟机的宿主机、hypervisor 机器,以下简称宿主机),中获得了一个告警、事件的时候,可能是网卡失败、物理硬盘预警、CPU占用过高之类的告警。我们可以借助图谱查询获得所有相关联的虚机,然后把(WARN)级别的告警发出去或者设置它们为(亚健康)的状态。 这样,获得通知的对象,往往是一些用户的系统,就可以根据他们预先定义好的策略做一些自动化运维,或者通知的 hook: 收到“宿主机 CPU 过高”的告警的情形下,可以根据用户自己设定的不同策略把虚机迁移走,或者更高级复杂的撤离方式(开始不接受新的 traffic,创建新的替代 workload,然后 gracefully 关闭这个 workload) “控制面网络故障”告警情况下,这时候往往无法成功进行主机的车里、迁移,故可以考虑触发备份主机、启动新 workload、关机 其他“(亚健康)状态”,可以作为负载层面出问题的根因分析(RCA)依据 下边,我们给出一个在图谱上进行告警、状态传导的查询例子,我们假设 vid 为 node0 的宿主机出现了高 CPU 的告警,则这个查询可以得到所有其上的虚机,获得时间、告警通知列表: cypher MATCH (vm:nova_instance)<-[:`contains`]-(host_CPU_high:nova_host) WHERE id(host_CPU_high) == "node0" RETURN vm.nova_instance.name AS VM_to_raise_CPU_alarms 这其中查询的图模式是从 host_CPU_high 这个 nova_host 向外经由 contains 这个关系指向 vm 这个 nova_instance 的: cypher (vm:nova_instance)<-[:`contains`]-(host_CPU_high:nova_host) 它的结果是: VM_to_raise_CPU_alarms server-4 server-3 server-1 server-0 如果我们把查询改动一下,选择输出全路径,则可以看到这个信息传导的方向: cypher MATCH p=(vm:nova_instance)<-[:`contains`]-(host_CPU_high:nova_host) WHERE id(host_CPU_high) == "node0" RETURN p 在 Explorer 中渲染,点击 N 跳检测: 第一个例子比较简单,甚至不是很有必要用图的能力(这种因为一跳查询表结构中也是很轻松地,我们用一两个 nova API call 就可以搞定等价的信息获取了),这里只是一个例子,实际上我们在图上可以做很多更 Graphy(具有图属性的)、复杂、独特的工作,我们慢慢来看。 4.2 网络可达检测 我们来考虑这样的场景,在 OpenStack 中,不同的主机可以连接到相同的子网(VPC),主机也可以连接到多个子网之中,这样,主机之间的网络连通性信息、与网络联通相关的推理、传导都可以在图上进行。 注:在真实世界中,这里可能还要考虑 Security Group、Router、Switch 等因素,本利中我们用到的 OpenStack 是 L2 only 的 Setup,比较简化。 获得与虚机 server_a 同一 VPC 的所有其他虚机看起来很容易表达了: cypher MATCH (server_a)--(:neutron_port)--(:neutron_network)--(:neutron_port)--(server_b:`nova_instance`) WHERE id(server_a) == "server-0" RETURN server_b.nova_instance.name AS L2_connected_server 结果如下: L2_connected_server server-1 看起来很初级呀,接下来我们再查询与虚机 server_a 同一 VPC、或者有可能通过跨网络虚机而互联的主机的所有其他虚机,这时候,我们除了共享 neutron network(VPC) 的情况,还要查询所有二层直连的虚机可能通过其他 VPC 连出去的的虚机,这里,我们用到了 OPTIONAL MATCH 的表达,表示可能匹配到的模式: cypher MATCH (server_a)--(:neutron_port)--(net:neutron_network)--(:neutron_port)--(server_b:`nova_instance`) WHERE id(server_a) == "server-0" OPTIONAL MATCH (server_b)--()--(other_net:neutron_network)--(:neutron_port)--(server_c:`nova_instance`) WITH server_a, server_b AS same_subnet_machines, server_c AS routeable_machines WHERE routeable_machines != server_a RETURN same_subnet_machines.nova_instance.name AS L2_connected_server, routeable_machines.nova_instance.name AS cross_vpc_server 可以看到结果里,跨网络潜在的相连主机还有 server-3: L2_connected_server cross_vpc_server server-1 server-3 我们将其可视化,同样,修改输出为路径 p 和 p1。 cypher MATCH p=(server_a)--(:neutron_port)--(net:neutron_network)--(:neutron_port)--(server_b:`nova_instance`) WHERE id(server_a) == "server-0" OPTIONAL MATCH p1=(server_b)--()--(other_net:neutron_network)--(:neutron_port)--(server_c:`nova_instance`) RETURN p, p1 它可能的连接路径一目了然 有了获得这些信息的能力,我们可以可编程地连接告警、状态、安全风控、网络等方方面面系统了,因为这不是本文的重点,这里就不加以赘述了,欢迎大家来 NebulaGraph 社区分享你们的图洞察使用方式。 接下来我们来看看存储相关的例子。 4.3 镜像、云盘、快照的血缘 在基础设施中,云盘(iSCSI、Ceph、NFS)、镜像、快照之间有多重复杂的关系,比如: 一个系统镜像可能从某一个虚拟机挂载的云盘或者一个快照创建 一个云盘可能是从一个系统镜像、一个快照或者另一个云盘创建 一个快照是从一个云盘创建的 这种血缘信息的识别和管理是很有必要的。下边的查询可以获得给定的虚机 server-0 的所有存储血缘: cypher MATCH p=(server_a)-[:`attached`|created_from|used_by]-(step1) WHERE id(server_a) == "server-0" OPTIONAL MATCH p1=(step1)-[:created_from*1..5]-(step2) RETURN p, p1 我们可以看到结果中: server-0 的启动镜像(这里它是从本地盘启动的,没有挂载云盘)是从 volume-1 创建的 volume-1 是从 cirros-0.5.2-x86_64-disk 这个镜像创建的 此外,还有其他有分叉关系的存储资源和他们也息息相关 接下来,我们不只考虑存储资源,再看看涉及云盘(cinder_volume)挂载(attached)这层关系下的血缘关系: cypher MATCH p=(server_a)-[:`attached`|created_from|used_by]-(step1) WHERE id(server_a) == "server-4" OPTIONAL MATCH p1=(step1)-[:created_from|attached*1..5]-(step2) RETURN p, p1 这次,我们可以从渲染图中读出这样的洞察: server-4 的启动镜像(这里它是从本地盘启动的)是从 volume-1 创建的 而 volume-1 现在挂载在 server-6 上 volume-1 是从 cirros-0.5.2-x86_64-disk 这个镜像创建的 同样 cirros-0.5.2-x86_64-disk 镜像被很多其他虚机在采用 server-4 同时挂载了数据盘 volume-2 而 volume-2 是一个多挂载的盘,它同时挂载在 server-3 之上 server-3 的系统启动盘是从快照 snapshot-202301111800-volume-1 克隆创建的 快照 snapshot-202301111800-volume-1 是曾经从 volume-1 创建的 volume-1 现在挂载在 server-6 上 快照不一定是从 server-6 而来,因为镜像可能被重新挂载过 而这些血缘信息可以被用在资源生命周期管理、根因分析、安全告警、状态传导上,这里不加以赘述。 4.4 高相关性虚机预警 下面再给一个节点相似度的应用,我们可以在全图或者子图上,利用图算法找到与一个虚机在图上关系的维度上最相似的其他虚机,基于在这种相关性增加新的关系,并在关系上做风险事件预警。 这次的图算法应用中,我们按照一个典型的从[快速子图验证]到[全图生产应用的]工作流。 4.4.1 在子图上快速验证:浏览器内算法 首先,我们试着从 server-0 的三度子图上做算法的验证。 cypher GET SUBGRAPH 3 STEPS FROM "server-0" YIELD VERTICES AS nodes, EDGES AS relationships; 将结果渲染在画布上,我们可以看到子图中包含了其他几个虚机: 然后,我们利用 explorer 中的浏览器内图算法,可以非常方便地验证我们的想法,这里,我们使用 Jaccard SImilarity 相似性算法,进行 server-0 与 server-1,server-3,server-4,server-6 迭代分别得到相似性: 可以看出,在 3 步子图内,和 server-0 最近接的虚机是 server-4。进一步我们可以简单在子图上看看两者之间的路径作为相似性的解释: 在这个可解释结果中,我们知道 server-0 与 server-4 相似的原因可能是: 坐落在同一个宿主机:node-0 使用同一个镜像:cirros_mod_from_volume-1 如此,我们最终落地的预警机制可能是,当 server-0 出现某一问题、告警时候,给相似的 server-4 也设定预警,预警理由就是它们在同样主机、同样镜像。 4.4.2 落地算法为应用:Workflow+Analytics 有了前边的快速实验,我们可以借助 workflow + NebulaGraph Analytics 把它落地为全图上的算法,利用 Analytics 分布式能力去执行。 在生产上,我们利用 Workflow 的 DAG 编排能力创建两个前后相连的任务: 取临近虚机 全图算相似度 第一个任务如下,它实时从给定的虚机(这里写死了 server-0,但是 workflow 可以把这里作为参数化,并封装任务为可以被 API 触发的异步服务): cypher MATCH (n)-[*1..5]-(m:`nova_instance`) WHERE id(n) == "server-0" AND n != m RETURN distinct id(m) 这里 Query job 我们输出待比较的其他虚机的 vid。 接着,JaccardSImilarity job 中,我们选择 ids1 为 server-0(这里如上,上线时是参数化的),ids2 从上游取(前边的 Query job),选择在 openstack 全图扫描所有类型的边。 保存、运行,我们可以看到,结果如下,区别是这次它运算了更多的目标虚机,并且迭代作用范围是全图而非一个子图,可以看到结果是一致的,这是因为子图上关联度大的点和相近的边在 Jaccard 算法里起到了更主要的作用。 4.5 安全相关场景 基础设施资源中的关联关系和金融、内容系统、电商领域的风控场景有相似的地方,很多场景本质上利用到了图谱关系中的知识,在图库上实时获取这些复杂多跳天然带有可解释性的安全洞察非常适合。 4.5.1 秘钥泄漏风控分析 先看一个秘钥泄漏的场景:假设 key-0 被安全部门确定被泄漏了,我们可以在毫秒时间内获得如下查询: 直接采用了密钥的虚机 与采用秘钥的虚机网络直连的机器 与采用秘钥的虚机跨网络相连的机器 cypher MATCH (key_leaked)-[:`used_by`]->(involved_server:nova_instance)--(:neutron_port)--(net:neutron_network)--(:neutron_port)--(server_b:nova_instance) WHERE id(key_leaked) == "key-0" OPTIONAL MATCH (server_b)--()--(other_net:neutron_network)--(:neutron_port)--(server_c:nova_instance) WITH involved_server, server_b AS same_subnet_machines, server_c AS cross_net_machines WHERE cross_net_machines != involved_server RETURN involved_server.nova_instance.name AS with_key, same_subnet_machines.nova_instance.name AS l2_vms, cross_net_machines.nova_instance.name AS cross_vpc_vms 贴一下部分结果,我们知道 server-4 采用了这个 keypair,并且 server-6 和它在同一个网络,同时,有一定可能,通过 server-6,server-1,2,0,5 也受到了威胁、影响,相关的机器可以被触发不同级别的告警来降低安全事故的影响。 with_key l2_vms cross_vpc_vms server-4 server-6 server-1 server-4 server-6 server-2 server-4 server-6 server-0 server-4 server-6 server-5 这个查询改造为可视化结果: cypher MATCH p=(key_leaked)-[:`used_by`]->(involved_server:nova_instance)--(:neutron_port)--(net:neutron_network)--(:neutron_port)--(server_b:nova_instance) WHERE id(key_leaked) == "key-0" OPTIONAL MATCH p1=(server_b)--()--(other_net:neutron_network)--(:neutron_port)--(server_c:nova_instance) RETURN p,p1 在 explorer 里 应用 Dagre-LR 的布局,一关联关系很清晰的被展示出来,也许可以考虑把它引用在安全事故的报告分发给虚机租户。 4.5.2 镜像、云盘漏洞范围分析 类似的,一个镜像被扫出漏洞,我们可以瞬间查到涉及到的资源,并做出相应 镜像文件有漏洞 cypher MATCH p=(image_risky)-[:`created_from`]-(step1) WHERE id(image_risky) == "cirros-0.5.2-x86_64-disk" OPTIONAL MATCH p1=(step1)-[:created_from*1..5]-(step2) RETURN p, p1 一个云盘有漏洞 cypher MATCH p=(volume_risky)-[:`created_from`]-(step1) WHERE id(volume_risky) == "volume-1" OPTIONAL MATCH p1=(step1)-[:created_from*1..5]-(step2) RETURN p, p1 4.5.3 潜在宿主机逃离影响范围分析 最后,我们讨论一个比较严重的安全问题:宿主机逃离。 在极端的情况下如果在我们得到消息,server-0 发生了有可能影响宿主机的安全时间的时候,仅仅关闭这个宿主机是不够的,受影响的范围可能已经扩大了,然而,我们不可能因为这样关闭整个机房,所以,利用图谱辅助找出受影响范围会有一些帮助。 下面的查询模式是: 找出可能被影响的子网(VPC),标记最高级别风险子网为后续定位做准备 找到可能被控制了的宿主机 从宿主机触发,找出同主机的其他虚机 从其他虚机触发,找到它们的子网(VPC) 从其他虚机触发,找到可能已经被影响的网盘(防止被挂载到其他机器,扩大影响) cypher MATCH (server_escaping_hypervisor)<-[:`contains`]-(hypervisor_compromised:nova_host) WHERE id(server_escaping_hypervisor) == "server-0" OPTIONAL MATCH (server_escaping_hypervisor)<-[:attached]-(:neutron_port)<-[:contains]-(impacted_subnet_high:neutron_network) OPTIONAL MATCH (hypervisor_compromised)-[:`contains`]->(server_same_host:nova_instance) OPTIONAL MATCH (server_same_host)<-[:attached]-(:neutron_port)<-[:contains]-(impacted_subnet:neutron_network) OPTIONAL MATCH (server_same_host)<-[:attached]-(impacted_volume:cinder_volume) RETURN impacted_subnet_high.neutron_network.name AS impacted_subnet_high, hypervisor_compromised.nova_host.name AS hypervisor_compromised, impacted_subnet.neutron_network.name AS impacted_subnet, [server_same_host.nova_instance.name, server_same_host.nova_instance.instance_name] AS server_same_host, impacted_volume.cinder_volume.name AS impacted_volume 结果中列出了 server-0 被控制之后,考虑宿主机逃离的情况下可能受影响的扩散范围。 impacted_subnet_high hypervisor_compromised impacted_subnet server_same_host impacted_volume shared node0 shared [“server-0”, “instance-00000001”] Empty shared node0 shared [“server-1”, “instance-00000002”] ffaeb199-47f4-4d95-89b2-97fba3c1bcfe shared node0 private [“server-1”, “instance-00000002”] ffaeb199-47f4-4d95-89b2-97fba3c1bcfe shared node0 private [“server-3”, “instance-00000005”] c9db7c2e-c712-49d6-8019-14b82de8542d shared node0 private [“server-3”, “instance-00000005”] volume-2 shared node0 public [“server-4”, “instance-00000006”] volume-2 咱们再看看它的可视化结果。 cypher MATCH p=(server_escaping_hypervisor)<-[:`contains`]-(hypervisor_compromised:nova_host) WHERE id(server_escaping_hypervisor) == "server-0" OPTIONAL MATCH p0=(server_escaping_hypervisor)<-[:attached]-(:neutron_port)<-[:contains]-(impacted_subnet_high:neutron_network) OPTIONAL MATCH p1=(hypervisor_compromised)-[:`contains`]->(server_same_host:nova_instance) OPTIONAL MATCH p2=(server_same_host)<-[:attached]-(:neutron_port)<-[:contains]-(impacted_subnet:neutron_network) OPTIONAL MATCH p3=(server_same_host)<-[:attached]-(impacted_volume:cinder_volume) RETURN p,p0,p1,p2,p3 选择 Dagre 布局之后,可以比较清晰看出影响资源的范围,从这些可能受影响的虚机、网络、网盘出发,可以进一步采取需要的措施了。 4.6 重点关注资源检测 最后,利用 Betweenness Centrality 算法,我们可以得出基础设施中影响面大的那些,”脆弱环节“,这些资源不一定真的处在危险的状态,只是说,它们处在了比较重要的资源之间的交汇处,一旦它们出问题,出问题的代价可能会非常大。 识别出这样的资源之后我们可以考虑: 有针对性采用更激进、昂贵的健康检查策略; 设定更高的支持、关切级别; 主动迁移相关联的资源以降低”脆弱环节“对整体基础设施可用性的影响范围; 这次,我们就只在浏览器内部的子图上做算法流程验证,读者朋友们可以自己试着利用开源的 NebulaGraph Algorithm 或者付费的 NebulaGraph Workflow+Analytics 做全图上的等价操作。 首先,我们在前边用过的方式去扫描图上 1000 个点,并且从其出发,跳一跳,获得一个比较随机的子图,在我们当前的数据集下,这实际上捞取了全图的数据: cypher MATCH (n) WITH n LIMIT 1000 OPTIONAL MATCH p=(n)--() RETURN p, n 在其之上,我们运行 Betweenness Centrality 之后,得到 node0 是分值最大的”脆弱一环“,的确,它是我们当前实验中负载最大的宿主机,可以想象它确实是故障之后全局影响最大的一个资源。 5 总结 在海量数据、企业云、混合云的复杂基础设施运维场景下,利用图数据库图算法的能力做高效的辅助运维工作是一个十分值得的尝试与技术投资。 NebulaGraph 作为高性能、开源、分布式的新一代云原生图数据库,是一个很值得考虑的图基础设施选型目标。 欢迎大家在文末留言讨论,本文的可复现环境和示例的 ETL 管道的代码、示例数据全都在 https://github.com/wey-gu/openstack-graph/ 开源,欢迎大家来一起完善。 题图版权:Ivan

2023/1/13
articleCard.readMore

连接微信群、Slack 和 GitHub:社区开放沟通的基础设施搭建

NebulaGraph 社区如何构建工具让 Slack、WeChat 中宝贵的群聊讨论同步到公共领域 1 要开放,不要封闭 在开源社区中,开放的一个重要意义是社区内的沟通、讨论应该是透明、包容并且方便所有成员访问的。这意味着社区中的任何人都应该能够参与讨论和决策过程,并且所有相关信息应该公开和自由地共享。 在公共场合进行沟通在开源理念中是重要的,正式这种方式使得社区的成员可以进行有效地共同工作,分享想法和反馈,为项目或社区做出贡献。 为了更清楚表达,我举几个反面的例子: 要求贡献者使用对他们来说难以访问或难以使用的工具可能会妨碍开源社区中的开放沟通。 这可能是由于多种原因,例如: 工具可能昂贵或需要许可证,而并非所有贡献者都能负担得起。 工具可能难以使用或需要很高的技术经验积累,而并非所有贡献者都具备。 工具可能在某些操作系统或设备上不兼容,这可能使一些贡献者难以访问它们。 在不与社区其他成员分享上下文、过程或结果的情况下,只在线下(例如通过当面沟通、IM 或电话会议)进行决策可能会使很重要的知识只被少数贡献者掌握。 这可能会阻止其他人在这些知识之上做贡献或从中学习,阻碍了开源社区所必需的开放沟通和协作。 没有把系统、功能设计和提案信息以公开方式文档化、归档下来,例如只提供某一个公司内网的链接,从而可能伤害开源社区的透明度和包容性。 因为这样的结果是社区的其他成员很难保持对社区进展的正常了解、就更不用说参与进来做贡献了。为了促进透明度和包容性,开源社区应尽量确保所有重要的信息公开和自由地共享、尽可能保有细节地被公开归档。 2 挑战 为了使社区(或工作环境)的沟通保持透明、高效和健康,其实已经存在一些共识,和通用的做法: 异步优于同步,在分布式和全球协作的情况下,同步通信在大多数情况下成本高且效率低。 因此,推荐使用 GitHub Discussion 和 StackOverflow 进行提问式的沟通。 专题(Thread)讨论优于广播(Fan out),注意力是宝贵的,向所有人群发最终常常导致重要信息没有人真的读。 因此,我们在 GitHub Discussion 和 Slack 中设有分类、频道。建立 SIG 来讨论一些有趣的主题(并归档沟通的结果),而不是将所有事情带到社区会议广泛讨论。 优先选择可搜索/文本、版本控制、协作的方式与工具,并在可能的情况下鼓励成员们给其他人反馈;在基础设施上跟踪文档、设计流程,并且提供评论、review 的能力。 为此,我们用 etherpad.opendev.org 来记录社区会议文档。 但是,就像我们还是需要同步的沟通、有 IM 和会议的需求一样,还是存在一些特例的情况,我们不能盲目追求异步、绝对的开放的,正如前边提到的,能让更多参与者公平、方便与社区连结本身也是开放的一部分,尽管使用的基础设施是可能是封闭的。事实上,几乎所有的开源社区都在用类似的方式建立他们的开源社区沟通平台: Slack 在 IM 消息中中支持丰富的格式化(支持 markdown!)和 Thread 系统,其现代化设计和开放/软定义接口使我们的工作流程可能非常优美流畅。 与 Slack 相比,微信在技术社区中在许多方面都很不理想(只是因为它不是专为这样场景而设计的!),但在国内,它是社区中所有人都可以访问的唯一平台。每个人都有一个微信账号,而只有很少一部分人会每天查邮件。 于是,我们面临的问题是,在 NebulaGraph 社区中有两个平台承担了沟通的重要部分,但这些信息在几个月后就会消失,它们在短时间内只能被割裂的一部分贡献者看到,而未来没有人或其他平台可以读到、搜到和参考、引用这些有价值的讨论。 3 我们摸索的方案 曾经有一段时间,我们会自己手动收集 Slack、微信群里的讨论摘要,定期分享、归档在公共领域,这个方法也确实带来了一些价值,然而,我们最后都没坚持下去,原因很简单:1. 这太费事儿了,完全不 scale;2. 这种摘要其实不好平衡能被归档信息的裁剪程度,有时候细节非常重要却不容易被摘要保留。 3.1 搞定 Slack 的信息孤岛 2022 年 10 月,我注意到了 linen.dev 这个开源项目同时也是一个 SaaS 服务,有了它,我们可以把 Discord 和 Slack 中的每个 thread 保留,它整站看起来和 Discord/ Slack 机会样,但是,它完全是可以匿名被访问、引用和被搜索引擎搜索的。 经过几个月的评估,我们最终决定了订阅 linen.dev 服务。为此,我们可以获得: 不用去碰现有的 Slack,所有 Slack 的好处都能得以保留 有了这样一个社区的站点 https://community-chat.nebula-graph.io/,其中,Slack 中的每个公共频道内容都能被匿名访问、能被搜索引擎收录,而访客还可以很容易知道怎么加入我们的 Slack,如图右上角: 这个站会实时同步 Slack 里的消息,重要的是,它是面向搜索引擎优化的,你可以搜搜 Kotlin 社区通过 linen 被收录的网页有多少,搜这个:“site: slack-chats.kotlinlang.org”。 每一个 thread 都有一个无需登录的只读 URL,我们可以方便去分享、引用它,虽然这件事儿本身就是超链接、URL的作用,但是在现在已经变得非常不容易了,比如这个新闻里提到现在新一代的人们更倾向于在抖音里搜索而不是在公共领域里。 有了它,我们可以非常开心地在 GitHub 里引用任意一个 Slack 讨论话题: 解决了 Slack 的问题之后,唯一剩下的痛点就是微信群了,每周都有挺多非常宝贵的讨论在社区群中进行却不能被保留下来,真实太令人心疼了,终于有一天,我们决定把这个问题解决。 3.2 解决微信群的信息公开化 首先,能不能直接用 Linen 一把梭,同步群消息呢?我确实在 Linen 社区和他们的 Kam 讨论直接解决 IM 同步的可能,不过到现在,他们都没有优先考虑😭。 然后,我就在想如果直接把微信同步到 Slack,Linen 不就能把微信的信息也收录了吗? 在 Twitter 上 求助黑客/开源社区 + 一番调研确定了没有这样的东西存在之后,我决定搞一个,做成开源项目,我花了一点时间实现了最初的版本。 石头汤来了 👉🏻:https://t.co/Fdhm9MkoBb#NebulaGraph 社区微信群现在已经会被同步到 slack 了。 — Wey Gu 古思为 (@wey_gu) December 15, 2022 万万没想到,当我做到把消息从微信同步到 Slack 之后,随之而来的问题是,通过 Slack API 发出的消息 Linen 并不会收录。 为此,我放弃了 linen 一把梭的美好愿望,转而考虑把消息同步到其他公共领域,而我第一个想到的就是 GitHub Discussions 之中,又花了周末的下午加晚上,把它做出来了: 现在,这个机器人程序会把配置好的微信群消息同时同步到 Slack 频道和 GitHub Discussion 中给定的标签下的主题中,每一个群一个礼拜是一个主题,所有的消息都是主题下的评论。 3.3 小结 现在,我们保留了所有 Slack/微信的美好的一面的同时,把它们中的讨论消息历史全都归档、索引并公开到这两个域之下了,是不是很酷呢? https://community-chat.nebula-graph.io/ https://github.com/vesoft-inc/nebula-community/discussions/categories/wechat-chat-history 3.4 后续工作 这个同步微信的项目是 Apache 2.0 协议开源的,并且现在由我和Frost Ming在维护,这里还有很多待增强、实现的新功能、新任务,欢迎大家来试玩、贡献。让我们一起把开源社区的沟通做的多一点开放、少一点封闭吧~ 项目地址 👉🏻 https://github.com/wey-gu/chatroom-syncer. 4 结论 有效的沟通是成功的开源社区的基石,因为它让协作、分享思想与知识、以及所有成员的参与成为可能。为了确保沟通透明、包容和有效,对于开源社区来说,让所有成员有机会参与讨论和决策以及公开自由地分享相关信息是非常重要的。 我们 NebulaGraph 社区的建设者/贡献者将继续寻找和黑客方法,以开放和良好的方式使人们连接在一起,和大家共建更好的开源、技术社区。 题图版权:Artem Beliaikin

2022/12/19
articleCard.readMore

图数据库的社交网络应用

图数据库的社交网络应用 本文是一个基于 NebulaGraph 上解决社交网络问题的常规方法综述。其中介绍的方法提供都了 Playground 供大家学习、玩耍。 社交网络大家都不陌生,无论是微信、微博、B 站还是大众点评、知乎、陌陌等服务,其本质上的用户都形成了社交网络。 在一个社交网络系统中,我们可以用图数据库来表示用户和他们的连接关系。图数据库能允许对用户之间的关系进行有效的查询,使得各种基于连接查找、统计、分析的社交网络上的业务实现变得可行、高效。 例如,图形数据库可以用来识别网络中的“有影响力用户”,或者根据用户之间的共同点对新的连接(好友关系、关心的内容)进行推荐,再或者寻找社群中相聚集的不同人群、社区,进行用户画像。图形数据库因为在能支撑复杂多跳查询的同时也能支持实时写入、更新,使其非常适合应用在用户关系不断变化的社交网络系统之上。 1 图建模 为了给出一些常见社交场景的应用示例,我会把大多数例子建立在一个典型的小型社交网络上,社交网络天然就是一张网络、图的形态。 为此,我在 NebulaGraph 官方示例数据集:篮球运动员之上,增加了三种点: 地址 地点 文章 五种边: 发文 评论 住在 属于(地点) 它的建模非常自然: 2 数据导入 2.1 加载默认数据集 首先,我们加载默认的 basketballplayer 数据集。 在命令行 console 之中,我们只需要执行 :play basketballplayer 就可以。 而在 NebulaGraph Studio/Explorer 之中,我们可以在欢迎页点击下载就部署这份基础数据集。 2.2 加载社交网络 schema 其次我们执行下边的语句,首先是 Schema 定义的语句: sql CREATE TAG IF NOT EXISTS post(title string NOT NULL); CREATE EDGE created_post(post_time timestamp); CREATE EDGE commented_at(post_time timestamp); CREATE TAG address(address string NOT NULL, `geo_point` geography(point)); CREATE TAG place(name string NOT NULL, `geo_point` geography(point)); CREATE EDGE belong_to(); CREATE EDGE lived_in(); 2.3 加载数据 然后,在等两个心跳时间以上之后(20秒),我们可以执行数据插入: sql INSERT VERTEX post(title) values \ "post1":("a beautify flower"), "post2":("my first bike"), "post3":("I can swim"), \ "post4":("I love you, Dad"), "post5":("I hate coriander"), "post6":("my best friend, tom"), \ "post7":("my best friend, jerry"), "post8":("Frank, the cat"), "post9":("sushi rocks"), \ "post10":("I love you, Mom"), "post11":("Let's have a party!"); INSERT EDGE created_post(post_time) values \ "player100"->"post1":(timestamp("2019-01-01 00:30:06")), \ "player111"->"post2":(timestamp("2016-11-23 10:04:50")), \ "player101"->"post3":(timestamp("2019-11-11 10:44:06")), \ "player103"->"post4":(timestamp("2014-12-01 20:45:11")), \ "player102"->"post5":(timestamp("2015-03-01 00:30:06")), \ "player104"->"post6":(timestamp("2017-09-21 23:30:06")), \ "player125"->"post7":(timestamp("2018-01-01 00:44:23")), \ "player106"->"post8":(timestamp("2019-01-01 00:30:06")), \ "player117"->"post9":(timestamp("2022-01-01 22:23:30")), \ "player108"->"post10":(timestamp("2011-01-01 10:00:30")), \ "player100"->"post11":(timestamp("2021-11-01 11:10:30")); INSERT EDGE commented_at(post_time) values \ "player105"->"post1":(timestamp("2019-01-02 00:30:06")), \ "player109"->"post1":(timestamp("2016-11-24 10:04:50")), \ "player113"->"post3":(timestamp("2019-11-13 10:44:06")), \ "player101"->"post4":(timestamp("2014-12-04 20:45:11")), \ "player102"->"post1":(timestamp("2015-03-03 00:30:06")), \ "player103"->"post1":(timestamp("2017-09-23 23:30:06")), \ "player102"->"post7":(timestamp("2018-01-04 00:44:23")), \ "player101"->"post8":(timestamp("2019-01-04 00:30:06")), \ "player106"->"post9":(timestamp("2022-01-02 22:23:30")), \ "player105"->"post10":(timestamp("2011-01-11 10:00:30")), \ "player130"->"post1":(timestamp("2019-01-02 00:30:06")), \ "player131"->"post2":(timestamp("2016-11-24 10:04:50")), \ "player131"->"post3":(timestamp("2019-11-13 10:44:06")), \ "player133"->"post4":(timestamp("2014-12-04 20:45:11")), \ "player132"->"post5":(timestamp("2015-03-03 00:30:06")), \ "player134"->"post6":(timestamp("2017-09-23 23:30:06")), \ "player135"->"post7":(timestamp("2018-01-04 00:44:23")), \ "player136"->"post8":(timestamp("2019-01-04 00:30:06")), \ "player137"->"post9":(timestamp("2022-01-02 22:23:30")), \ "player138"->"post10":(timestamp("2011-01-11 10:00:30")), \ "player141"->"post1":(timestamp("2019-01-03 00:30:06")), \ "player142"->"post2":(timestamp("2016-11-25 10:04:50")), \ "player143"->"post3":(timestamp("2019-11-14 10:44:06")), \ "player144"->"post4":(timestamp("2014-12-05 20:45:11")), \ "player145"->"post5":(timestamp("2015-03-04 00:30:06")), \ "player146"->"post6":(timestamp("2017-09-24 23:30:06")), \ "player147"->"post7":(timestamp("2018-01-05 00:44:23")), \ "player148"->"post8":(timestamp("2019-01-05 00:30:06")), \ "player139"->"post9":(timestamp("2022-01-03 22:23:30")), \ "player140"->"post10":(timestamp("2011-01-12 10:01:30")), \ "player141"->"post1":(timestamp("2019-01-04 00:34:06")), \ "player102"->"post2":(timestamp("2016-11-26 10:06:50")), \ "player103"->"post3":(timestamp("2019-11-15 10:45:06")), \ "player104"->"post4":(timestamp("2014-12-06 20:47:11")), \ "player105"->"post5":(timestamp("2015-03-05 00:32:06")), \ "player106"->"post6":(timestamp("2017-09-25 23:31:06")), \ "player107"->"post7":(timestamp("2018-01-06 00:46:23")), \ "player118"->"post8":(timestamp("2019-01-06 00:35:06")), \ "player119"->"post9":(timestamp("2022-01-04 22:26:30")), \ "player110"->"post10":(timestamp("2011-01-15 10:00:30")), \ "player111"->"post1":(timestamp("2019-01-06 00:30:06")), \ "player104"->"post11":(timestamp("2022-01-15 10:00:30")), \ "player125"->"post11":(timestamp("2022-02-15 10:00:30")), \ "player113"->"post11":(timestamp("2022-03-15 10:00:30")), \ "player102"->"post11":(timestamp("2022-04-15 10:00:30")), \ "player108"->"post11":(timestamp("2022-05-15 10:00:30")); INSERT VERTEX `address` (`address`, `geo_point`) VALUES \ "addr_0":("Brittany Forge Apt. 718 East Eric WV 97881", ST_Point(1,2)),\ "addr_1":("Richard Curve Kingstad AZ 05660", ST_Point(3,4)),\ "addr_2":("Schmidt Key Lake Charles AL 36174", ST_Point(13.13,-87.65)),\ "addr_3":("5 Joanna Key Suite 704 Frankshire OK 03035", ST_Point(5,6)),\ "addr_4":("1 Payne Circle Mitchellfort LA 73053", ST_Point(7,8)),\ "addr_5":("2 Klein Mission New Annetteton HI 05775", ST_Point(9,10)),\ "addr_6":("1 Vanessa Stravenue Suite 184 Baileyville NY 46381", ST_Point(11,12)),\ "addr_7":("John Garden Port John LA 54602", ST_Point(13,14)),\ "addr_8":("11 Webb Groves Tiffanyside MN 14566", ST_Point(15,16)),\ "addr_9":("70 Robinson Locks Suite 113 East Veronica ND 87845", ST_Point(17,18)),\ "addr_10":("24 Mcknight Port Apt. 028 Sarahborough MD 38195", ST_Point(19,20)),\ "addr_11":("0337 Mason Corner Apt. 900 Toddmouth FL 61464", ST_Point(21,22)),\ "addr_12":("7 Davis Station Apt. 691 Pittmanfort HI 29746", ST_Point(23,24)),\ "addr_13":("1 Southport Street Apt. 098 Westport KY 85907", ST_Point(120.12,30.16)),\ "addr_14":("Weber Unions Eddieland MT 64619", ST_Point(25,26)),\ "addr_15":("1 Amanda Freeway Lisaland NJ 94933", ST_Point(27,28)),\ "addr_16":("2 Klein HI 05775", ST_Point(9,10)),\ "addr_17":("Schmidt Key Lake Charles AL 13617", ST_Point(13.12, -87.60)),\ "addr_18":("Rodriguez Track East Connorfort NC 63144", ST_Point(29,30)); INSERT VERTEX `place` (`name`, `geo_point`) VALUES \ "WV":("West Virginia", ST_Point(1,2.5)),\ "AZ":("Arizona", ST_Point(3,4.5)),\ "AL":("Alabama", ST_Point(13.13,-87)),\ "OK":("Oklahoma", ST_Point(5,6.1)),\ "LA":("Louisiana", ST_Point(7,8.1)),\ "HI":("Hawaii", ST_Point(9,10.1)),\ "NY":("New York", ST_Point(11,12.1)),\ "MN":("Minnesota", ST_Point(15,16.1)),\ "ND":("North Dakota", ST_Point(17,18.1)),\ "FL":("Florida", ST_Point(21,22.1)),\ "KY":("Kentucky", ST_Point(120.12,30)),\ "MT":("Montana", ST_Point(25,26.1)),\ "NJ":("New Jersey", ST_Point(27,28.1)),\ "NC":("North Carolina", ST_Point(29,30.1)); INSERT EDGE `belong_to`() VALUES \ "addr_0"->"WV":(),\ "addr_1"->"AZ":(),\ "addr_2"->"AL":(),\ "addr_3"->"OK":(),\ "addr_4"->"LA":(),\ "addr_5"->"HI":(),\ "addr_6"->"NY":(),\ "addr_7"->"LA":(),\ "addr_8"->"MN":(),\ "addr_9"->"ND":(),\ "addr_10"->"MD":(),\ "addr_11"->"FL":(),\ "addr_12"->"HI":(),\ "addr_13"->"KY":(),\ "addr_14"->"MT":(),\ "addr_15"->"NJ":(),\ "addr_16"->"HI":(),\ "addr_17"->"AL":(),\ "addr_18"->"NC":(); INSERT EDGE `lived_in`() VALUES \ "player100"->"addr_4":(),\ "player101"->"addr_7":(),\ "player102"->"addr_2":(),\ "player103"->"addr_3":(),\ "player104"->"addr_0":(),\ "player105"->"addr_5":(),\ "player106"->"addr_6":(),\ "player107"->"addr_1":(),\ "player108"->"addr_8":(),\ "player109"->"addr_9":(),\ "player110"->"addr_10":(),\ "player111"->"addr_11":(),\ "player112"->"addr_12":(),\ "player113"->"addr_13":(),\ "player114"->"addr_14":(),\ "player115"->"addr_15":(),\ "player116"->"addr_16":(),\ "player117"->"addr_17":(),\ "player118"->"addr_18":(); 2.4 数据初探 首先,我们看看数据统计 sql [basketballplayer]> SUBMIT JOB STATS; +------------+ | New Job Id | +------------+ | 10 | +------------+ [basketballplayer]> SHOW STATS; +---------+----------------+-------+ | Type | Name | Count | +---------+----------------+-------+ | "Tag" | "address" | 19 | | "Tag" | "place" | 14 | | "Tag" | "player" | 51 | | "Tag" | "post" | 10 | | "Tag" | "team" | 30 | | "Edge" | "belong_to" | 19 | | "Edge" | "commented_at" | 40 | | "Edge" | "created_post" | 10 | | "Edge" | "follow" | 81 | | "Edge" | "lived_in" | 19 | | "Edge" | "serve" | 152 | | "Space" | "vertices" | 124 | | "Space" | "edges" | 321 | +---------+----------------+-------+ Got 13 rows (time spent 1038/51372 us) 查一下所有的数据 sql MATCH ()-[e]->() RETURN e LIMIT 10000 因为数据量太小了,所以可以把所有数据在 NebulaGraph Explorer 中渲染出来: 3 找出网络中的关键人物 识别社交网络中的有影响的关键人物们(influencers)涉及使用各种指标和方法来识别在特定网络中拥有大量影响力的个人。这对很多业务场景都有帮助都很有用,比如用于营销或研究网络中的信息传播。 识别他们的方法有很多,具体的方法和考量的信息、关系、角度也取决于这些关键人物的类型、和获取他们的目的。 一些常见的方法包括看一个人拥有的粉丝或内容被消费的数量,他们在其帖子、视频上读者的参与度,以及他们的内容的影响力(转发、引用)。这些方法在图上也是可以做的,但是比较平凡,我就不举例了,在这里,我们可以试着用评估、计算节点重要性的图算法,在图上得出这些关键人物。 3.1 PageRank PageRank 是一个非常“古老的”图算法,它通过考虑图上点之间的关系数量去迭代,得到每一个点的得分(Rank),最初由 Google 的创始人 Larry Page 和 Sergey Brin 提出并应用在早期的 Google 搜索引擎中,用来排序搜索结果,这里的 Page 可以是 Larry Page 的姓和 Web Page 的双关了。 在现代、复杂的搜索引擎中,PageRank 早就因为其过于简单而被弃用,但是在其他图结构网络场景中,PageRank 仍然在发光发热,社交网络中我们可以粗略地认为所有链接的重要程度类似,去运行这个算法找出那些关键的用户。 在 NebulaGraph 中,我们可以利用 NebulaGraph Algorithm、NebulaGraph Analytics 去在大的全图上运行 PageRank,而在日常的分析、验证、设计阶段,我们不需要在全量数据上跑结果,而在很小的子图上(最多上万),我们可以轻松地在浏览器里边运行各种图算法去得出线上业务可以用的方法。 今天,我们就用 NebulaGraph Explorer 内置的浏览器内图算法功能执行一下 PageRank 看看(具体方法这里略去,可以参考文档,不过其实就是点一下鼠标的事儿): 我们可以从上边看到,PageRank 计算之后所有绿色的 player(人)中,“player.name: Tim Duncan” 是最大的一个点,与之相关联的关系看起来的确不少,我们在图上选择他,再右键反选,选择除了 Tim Duncan 之外的所有点,用退格键删除所有其他的点,然后在他作为起点双向探索出1到5步,可以得到 Tim Duncan 的子图: 从子图中可以看到 Tim Duncan 和非常多其他球员有关注的关系的同时,一些其他很受欢迎的队员和他一起一样服役过非常热门的热刺(Spurs)队,这些都印证了 PageRank 的评估方式。 现在我们再看看其他判定维度下的算法会不会得出一样的结论呢? 3.2 Betweenness Centrality 作为另一个流行的节点重要性算法,通过计算一个节点对于图中的中介、桥梁作用来衡量节点的重要性,这里的桥梁作用是有数学定义的量化算法,这里就不展开说了,不过从感官上可以看出它是另一个角度很符合直觉地去评估重要性的方法。 我们重新在画布上查询所有的点边之后,在浏览器里运行 Betweenness Centrality 算法,这次的结果是: 从它的五跳内子图可以看出,与之前 PageRank 所得的关键人物 Tim Duncan 呈现的星星状态不同,Dejounte Murray 的子图呈现簇状,在感官、直觉上可以想象 Dejounte Murray 真的在很多节点之间的最小路径的必经之路上,而 Tim Duncan 则似乎和更多的重要连接者产生了关联。 在实际的应用场景中,我们通常要通过不同方式的定义的理解、不同执行结果的试验、分析去找到我们关注的关键人物产生影响的结构特征,用来针对不同需求选择不同的算法。 4 找出社区、聚集群体 社交网络中的社区检测是一种通过分析社交关系来发现社区结构的技术。社区结构是指在社交网络、图谱中相互联系密切的一组节点,这些节点通常具有相似的特征或兴趣。例如,社区结构可能表现为用户根据共同的话题或兴趣聚集在一起的一组用户。 社区检测的目的是通过对社交网络进行分析,找出不同社区的边界,并确定每个社区中的节点。这一过程可以通过使用各种算法来完成,例如标签传播算法、弱联通分量算法和 Louvain 算法等。通过发现社区结构,可以更好地了解社交网络的结构和特征,并有助于社交网络服务提供方更好地推断和预测社交网络中的行为,帮助做好社交网络的治理、广告投放、市场营销等。 由于我们的数据集是非真实的,我在不同的算法之下得出的结果并不能展现出真实的意涵,所以本章只是展示一下利用几个图算法进行社区识别之后的结果,在真实世界的案例中,我们还应该在此基础之上利用领域知识或者其他技术手段协同给出不同群体、社区的画像、标签。 标签传播算法效果: Louvain 算法效果: 弱联通分量算法效果: 在后边的章节,我们有机会可以在更小、更简单的子图上再次验证这几个算法,结果会更有可解释性一些。 5 好友亲密度 通过社区识别算法,其实是能够在一定程度上,在全局计算获得兴趣相近、关联紧密的好友的。那么如何获得一个给定用户的其他亲密好友呢?我们可以通过计算这个用户的好友中,和他共同好友的个数来排序获得这一信息! 我们拿 “Tim Duncan” 举例,我们知道,他的两度好友(好友的好友:(:player{name: "Tim Duncan"})-[:follow]-(f:player)-[:follow]-(fof:player))如果同时也是他的好友的话,那么他们这个中间的好友就是他和这个朋友的共同好友(Mutual Friend),那么有理由相信那些和 Tim Duncan 有更多共同好友的人可能跟他有更高亲密度: cypher MATCH (start:`player`{name: "Tim Duncan"})-[:`follow`]-(f:`player`)-[:`follow`]-(fof:`player`), (start:`player`)-[:`follow`]-(fof:`player`) RETURN fof.`player`.name, count(DISTINCT f) AS NrOfMutualF ORDER BY NrOfMutualF DESC; 这个计算结果是,“Tony Parker” 和 Tim 有 5 个共同好友,最为亲密。 fof.player.name NrOfMutualF Tony Parker 5 Dejounte Murray 4 Manu Ginobili 3 Marco Belinelli 3 Danny Green 2 Boris Diaw 1 LaMarcus Aldridge 1 Tiago Splitter 1 下面,咱们通过可视化来验证一下这个结果吧! 先看看每一个好友的共同好友(f:)都是谁? cypher MATCH (start:player{name: "Tim Duncan"})-[:`follow`]-(f:player)-[:`follow`]-(fof:player), (start:player)-[:`follow`]-(fof:player) RETURN fof.player.name, collect(DISTINCT f.player.name); 结果如下: fof.player.name collect(distinct f.player.name) Boris Diaw [“Tony Parker”] Manu Ginobili [“Dejounte Murray”, “Tiago Splitter”, “Tony Parker”] LaMarcus Aldridge [“Tony Parker”] Tiago Splitter [“Manu Ginobili”] Tony Parker [“Dejounte Murray”, “Boris Diaw”, “Manu Ginobili”, “Marco Belinelli”, “LaMarcus Aldridge”] Dejounte Murray [“Danny Green”, “Tony Parker”, “Manu Ginobili”, “Marco Belinelli”] Danny Green [“Dejounte Murray”, “Marco Belinelli”] Marco Belinelli [“Dejounte Murray”, “Danny Green”, “Tony Parker”] 然后我们在 Explorer 上可视化一下这个结果: 首先,我们把 Tim 的量度好友路径全查出来 cypher MATCH p=(start:player{name: "Tim Duncan"})-[:`follow`]-(f:player)-[:follow]-(fof:player) RETURN p 然后我们在其中按照度去渲染节点大小,并选中 Tim 和 Tony,并在两者之间查询 follow 类型边、双向、最多 2 跳的全部路径: 可以看出他们之间是最亲密的朋友没跑了,而且他们的共同好友也在路径之中: ["Dejounte Murray", "Boris Diaw", "Manu Ginobili", "Marco Belinelli", "LaMarcus Aldridge"] 5.1 朋友圈子里的小群体 这时候,如前边提到,这份数据集本身的非真实性,使得社区发现算法的结果不能得到其中洞察的内涵,现在我们可以接着这个小的子图,看看 Tim 的好友中可以如何区分群组、社区呢,咱们跑一个 Louvain 、弱联通分量、标签传播看看: 弱联通分量,可以把 Tim 等朋友们大体分割出两三个相互不连通的部分,非常符合连通分量的直观理解和定义。 标签传播,我们可以通过控制迭代次数按需去通过随机的传播划定出不同的划分度,结果可以有一定的区分度: 20 次迭代 1000 次迭代 Louvain,是一个比较高效、稳定的算法,基本上在这个子图下我们可以在很小的迭代次数下得到很符合直觉的划分: 6 新朋友推荐 接着前边二度朋友(朋友的朋友)的思路,我们可以很容易把那些还不是朋友的二度朋友作为推荐添加的好友,而排序规则则是他们之间的共同好友数量: cypher MATCH (start:player{name: "Tim Duncan"})-[:`follow`]-(f:player)-[:`follow`]-(fof:player) WHERE NOT (start:player)-[:`follow`]-(fof:player) AND fof != start RETURN fof.player.name, count(DISTINCT f) AS NrOfMutualF ORDER BY NrOfMutualF DESC; fof.player.name NrOfMutualF LeBron James 2 James Harden 1 Chris Paul 1 Yao Ming 1 Damian Lillard 1 JaVale McGee 1 Kevin Durant 1 Kyle Anderson 1 Rudy Gay 1 Russell Westbrook 1 显然,LeBron 最值得推荐!再看看这些共同好友都是谁? fof.player.name collect(distinct f.player.name) James Harden [“Dejounte Murray”] LeBron James [“Danny Green”, “Dejounte Murray”] Chris Paul [“Dejounte Murray”] Yao Ming [“Shaquille O’Neal”] Damian Lillard [“LaMarcus Aldridge”] JaVale McGee [“Shaquille O’Neal”] Kevin Durant [“Dejounte Murray”] Kyle Anderson [“Dejounte Murray”] Rudy Gay [“LaMarcus Aldridge”] Russell Westbrook [“Dejounte Murray”] 同样,我们在刚才的子图里找找 LeBron James 吧!我们把它俩之间的两步、双向路径找出来,果然只会经过 ["Danny Green", "Dejounte Murray"] 并且,没有直接的连接: 现在,系统会给两边发提醒:“hey,也许你们应该交个朋友!” 7 共同邻居 查找共同邻居是一个很常见的图库查询,它的场景可能根据不同的邻居关系,节点类型,同构、异构,带来不同的场景,前边两个场景下的共同好友本质上是两点之间的共同邻居,直接查询这样的关系用 OpenCypher 的表达非常简单。 7.1 两点之间的共同邻居 比如这个表达可以查询两个用户之间的共性、交集,结果可能是共同团队、去过的地方、兴趣爱好、共同参与的帖子回复等等: cypher MATCH p = (`v0`)--()--(`v1`) WHERE id(`v0`) == "player100" AND id(`v1`) == "player104" RETURN p 而限定了边的类型之后,这个查询就限定在共同好友的查询了。 cypher MATCH p = (v0)--(:`follow`)--(v1) WHERE id(v0) == "player100" AND id(v1) == "player104" RETURN p 7.2 多点之间的共同邻居:内容推送 下面,我们给出一个多点共同邻居的场景,我们从一个文章触发,查出所有在这个文章上有互动的用户,找到这一群体中的共同邻居。 这个共同邻居有什么用处呢?很自然,如果这个共同邻居还没有和这个文章有任何交互,我们可以把这个文章推荐给他。 这个查询的实现很有意思: 第一个 MATCH 是查到所有 post11 文章下留言和作者这些人的总人数 在第二个 MATCH 之后,我们查到所有这群人的一度好友路径中,这些文章过的交互用户的一度好友的参与过文章的朋友数量刚好等于这个参与文章的用户的数量的这些人,他们其实就是这些所有参与用户的共同好友。 cypher MATCH (blog:post)<-[e]-(:player) WHERE id(blog) == "post11" WITH blog, count(e) AS invoved_user_count MATCH (blog:post)<-[]-(users:player)-[:`follow`]-(common_neighbor:player) WITH toSet(collect(users)) AS users, common_neighbor, invoved_user_count WHERE size(users) == invoved_user_count RETURN common_neighbor 而这个人就是…Tony! cypher +-----------------------------------------------------+ | common_neighbor | +-----------------------------------------------------+ | ("player101" :player{age: 36, name: "Tony Parker"}) | +-----------------------------------------------------+ 而我们可以很容易在可视化中国验证它: cypher MATCH p=(blog:post)<-[]-(users:player)-[:`follow`]-(common_neighbor:player) WHERE id(blog) == "post11" RETURN p 渲染这个查询结果,然后再这篇叫做 “Let’s have a party!” 的文章与 Tony 之间查找评论、po文、关注三类边的双向、两跳查询,就可以看到这些参与文章的人们无一例外,都是 Tony 的好友,而只有 Tony 自己还没去文章里留言! 而 Party 怎么可以少了 Tony 呢?难道是他的惊喜生日 Party,Opps,我们是不是不应该告诉他? 8 信息流 我在之前写过基于图技术的推荐系统实现方法,其中描述了现代推荐系统中内容过滤、排序方法可以在图谱上进行,社交网络中有一点相似但又不同的场景是信息流(Feed),它的产生类似于推荐系统中的个性化,同时有具有很高的时效性,借助于包含了内容行为知识的社交图谱可以很直观、高效去实现个性化的信息流生成。 8.1 好友参与的内容 最简单、直接的信息流定义可能就是在朋友圈、微博 feed 上刷一下关注的人创建、参与的内容列表了,先不考虑排序的问题,这些内容一定是: 一定时间段内好友创建的内容 一定时间端内好友评论的内容 我们可以用 cypher 表达这个查询用户 id 为 player100 的信息流: cypher MATCH (feed_owner:player)-[:`follow`]-(friend:player) WHERE id(feed_owner) == "player100" OPTIONAL MATCH (friend:player)-[newly_commented:commented_at]->(:post)<-[:created_post]-(feed_owner:player) WHERE newly_commented.post_time > timestamp("2010-01-01 00:00:00") OPTIONAL MATCH (friend:player)-[newly_created:created_post]->(po:post) WHERE newly_created.post_time > timestamp("2010-01-01 00:00:00") WITH DISTINCT friend, collect(DISTINCT po.post.title) + collect("comment of " + dst(newly_commented)) AS feeds WHERE size(feeds) > 0 RETURN friend.player.name, feeds friend.player.name feeds Boris Diaw [“I love you, Mom”, “comment of post11”] Marco Belinelli [“my best friend, tom”, “comment of post11”] Danny Green [“comment of post1”] Tiago Splitter [“comment of post1”] Dejounte Murray [“comment of post11”] Tony Parker [“I can swim”] LaMarcus Aldridge [“I hate coriander”, “comment of post11”, “comment of post1”] Manu Ginobili [“my best friend, jerry”, “comment of post11”, “comment of post11”] 于是,我们可以把这些评论、文章发送到用户的 feed 之上了。 我们也来看看他们在图上的样子吧,我们输出所有查到的路径: cypher MATCH p=(feed_owner:player)-[:`follow`]-(friend:player) WHERE id(feed_owner) == "player100" OPTIONAL MATCH p_comment=(friend:player)-[newly_commented:commented_at]->(:post)<-[:created_post]-(feed_owner:player) WHERE newly_commented.post_time > timestamp("2010-01-01 00:00:00") OPTIONAL MATCH p_post=(friend:player)-[newly_created:created_post]->(po:post) WHERE newly_created.post_time > timestamp("2010-01-01 00:00:00") RETURN p, p_comment, p_post 渲染在 Explorer 上,选择“神经网络”这个布局,可以很清晰看出这些粉色的文章节点,还有代表评论的边。 8.2 附近好友的内容 我们再进一步,把地理信息考虑进来,获取那些住址的经纬度小于一定距离朋友相关的内容。 这里,我们用到了 NebulaGraph 的 GeoSpatial 地理功能,ST_Distance(home.address.geo_point, friend_addr.address.geo_point) AS distance WHERE distance < 1000000 的约束条件帮我们表达了距离的限制。 cypher MATCH (home:address)-[:lived_in]-(feed_owner:player)-[:`follow`]-(friend:player)-[:lived_in]-(friend_addr:address) WHERE id(feed_owner) == "player100" WITH feed_owner, friend, ST_Distance(home.address.geo_point, friend_addr.address.geo_point) AS distance WHERE distance < 1000000 OPTIONAL MATCH (friend:player)-[newly_commented:commented_at]->(:post)<-[:created_post]-(feed_owner:player) WHERE newly_commented.post_time > timestamp("2010-01-01 00:00:00") OPTIONAL MATCH (friend:player)-[newly_created:created_post]->(po:post) WHERE newly_created.post_time > timestamp("2010-01-01 00:00:00") WITH DISTINCT friend, collect(DISTINCT po.post.title) + collect("comment of " + dst(newly_commented)) AS feeds WHERE size(feeds) > 0 RETURN friend.player.name, feeds friend.player.name feeds Marco Belinelli [“my best friend, tom”, “comment of post11”] Tony Parker [“I can swim”] Danny Green [“comment of post1”] 这时候,从可视化这个结果也可以看到住址这一关系,以及它们的经纬度信息,我手动根据它们的经纬度,把地址的节点在图上排布了一下可以看到这个 feed 的主人 Tim(player100) 的住址(7,8)刚好在其他好友住址的中间位置,这些临近好友的相关的文章和参与评论的内容将被作为信息流推送给 Tim: 9 时空关系追踪 时空关系追踪这个图谱应用是在公共安全、物流、疫情防控等场景下,利用图遍历将繁杂、凌乱的信息充分利用起来的典型应用。当我们建立起这样的图谱之后往往只需要简单的图查询就可以获得非常有用的洞察。本章节我给大家距离一下这个应用场景。 9.1 数据集 为此,我创建了一个虚拟的数据集由来构建一个时空关系图谱。数据集的生成程序和一份可以直接用的文件都放在了 GitHub 上,仓库地址是: https://github.com/wey-gu/covid-track-graph-datagen 。 它的数据建模如下: 在一个全新的环境里,我们可以用下边的 3 行命令就准备好这个图谱: bash # 安装 NebulaGraph + NebulaGraph Studio curl -fsSL nebula-up.siwei.io/install.sh | bash -s -- v3 # 下载数据集 git clone https://github.com/wey-gu/covid-track-graph-datagen && cd covid-track-graph-datagen # 导入数据集 docker run --rm -ti \ --network=nebula-net \ -v ${PWD}/:/root \ vesoft/nebula-importer:v3.2.0 \ --config /root/nebula-importer-config.yaml 然后我们在 console 里查看一下数据 bash ~/.nebula-up/console.sh # 进入 console 了,进到 covid_trace 图空间(刚才创建的) USE covid_trace; # 执行数据统计的任务 SHOW JOB STATS 结果: bash (root@nebula) [covid_trace]> SHOW STATS +---------+------------+--------+ | Type | Name | Count | +---------+------------+--------+ | "Tag" | "人" | 10000 | | "Tag" | "地址" | 1000 | | "Tag" | "城市" | 341 | | "Tag" | "村镇" | 42950 | | "Tag" | "省份" | 32 | | "Tag" | "联系方式" | 0 | | "Tag" | "行政区" | 3134 | | "Tag" | "街道" | 667911 | | "Edge" | "住址" | 0 | | "Edge" | "到访" | 19986 | | "Edge" | "同住" | 19998 | | "Edge" | "属于" | 715336 | | "Space" | "vertices" | 725368 | | "Space" | "edges" | 755320 | +---------+------------+--------+ Got 14 rows (time spent 1087/46271 us) 9.2 两人之间的关联 很自然,利用路径查询就可以了: cypher # 最短 FIND SHORTEST PATH FROM "p_100" TO "p_101" OVER * BIDIRECT YIELD PATH AS paths # 所有路径 FIND ALL PATH FROM "p_100" TO "p_101" OVER * BIDIRECT YIELD PATH AS paths | LIMIT 10 最短路径结果: paths <(“p_100”)<-[:同住@0 {}]-(“p_2136”)<-[:同住@0 {}]-(“p_3708”)-[:到访@0 {}]->(“a_125”)<-[:到访@0 {}]-(“p_101”)> 所有路径结果: paths <(“p_100”)<-[:同住@0 {}]-(“p_2136”)<-[:同住@0 {}]-(“p_3708”)-[:到访@0 {}]->(“a_125”)<-[:到访@0 {}]-(“p_101”)> <(“p_100”)-[:到访@0 {}]->(“a_328”)<-[:到访@0 {}]-(“p_6976”)<-[:同住@0 {}]-(“p_261”)-[:到访@0 {}]->(“a_352”)<-[:到访@0 {}]-(“p_101”)> <(“p_100”)-[:同住@0 {}]->(“p_8709”)-[:同住@0 {}]->(“p_9315”)-[:同住@0 {}]->(“p_261”)-[:到访@0 {}]->(“a_352”)<-[:到访@0 {}]-(“p_101”)> <(“p_100”)-[:到访@0 {}]->(“a_328”)<-[:到访@0 {}]-(“p_6311”)-[:同住@0 {}]->(“p_3941”)-[:到访@0 {}]->(“a_345”)<-[:到访@0 {}]-(“p_101”)> <(“p_100”)-[:到访@0 {}]->(“a_328”)<-[:到访@0 {}]-(“p_5046”)-[:同住@0 {}]->(“p_3993”)-[:到访@0 {}]->(“a_144”)<-[:到访@0 {}]-(“p_101”)> <(“p_100”)-[:同住@0 {}]->(“p_3457”)-[:到访@0 {}]->(“a_199”)<-[:到访@0 {}]-(“p_6771”)-[:到访@0 {}]->(“a_458”)<-[:到访@0 {}]-(“p_101”)> <(“p_100”)<-[:同住@0 {}]-(“p_1462”)-[:到访@0 {}]->(“a_922”)<-[:到访@0 {}]-(“p_5869”)-[:到访@0 {}]->(“a_345”)<-[:到访@0 {}]-(“p_101”)> <(“p_100”)<-[:同住@0 {}]-(“p_9489”)-[:到访@0 {}]->(“a_985”)<-[:到访@0 {}]-(“p_2733”)-[:到访@0 {}]->(“a_458”)<-[:到访@0 {}]-(“p_101”)> <(“p_100”)<-[:同住@0 {}]-(“p_9489”)-[:到访@0 {}]->(“a_905”)<-[:到访@0 {}]-(“p_2733”)-[:到访@0 {}]->(“a_458”)<-[:到访@0 {}]-(“p_101”)> <(“p_100”)-[:到访@0 {}]->(“a_89”)<-[:到访@0 {}]-(“p_1333”)<-[:同住@0 {}]-(“p_1683”)-[:到访@0 {}]->(“a_345”)<-[:到访@0 {}]-(“p_101”)> 我们把所有路径进行可视化渲染,标记出起点终点的两人,并在其中查到他们的最短路径,他们之间的千丝万缕关系就一目了然了,无论是商业洞察、公共安全还是疫情防控的目的,有了这个信息,相应的工作都可以如虎添翼地向下进展。 当然,在真实的系统上,可能我们只需要关心两个用户之间的关联远近,得出量化的评估: cypher FIND SHORTEST PATH FROM "p_100" TO "p_101" OVER * BIDIRECT YIELD PATH AS paths | YIELD collect(length($-.paths)) AS len | YIELD coalesce($-.len[0], -1) AS len 结果中我们只关心他们之间最短路径的长度为:4。 len 4 9.3 时空相交的人 进一步我们可以用图语义勾勒出我们想确定的任何带有时间与空间信息的模式,在图谱中实时查询出来,比如对给定的人,他的 id 是 p_101,我们相差在特定时间里所有和他有时空相交的人,这意味着那些人在 p_101 访问某一地方的时间段之内也逗留、访问了这些地方: cypher MATCH (p:人)-[`visit0`:到访]->(`addr`:地址)<-[`visit1`:到访]-(p1:人) WHERE id(p) == "p_101" AND `visit0`.`start_time` < `visit1`.`end_time` AND `visit0`.`end_time` > `visit1`.`start_time` RETURN `addr`.地址.`name`, collect(p1.人.`name`) 我们得到了再每一个到访地点的时空相交人列表如下: addr.地址.name collect(p1.人.name) 闵行仇路q座 255960 [“徐畅”, “王佳”, “曾亮”, “姜桂香”, “邵秀英”, “韦婷婷”, “陶玉”, “马坤”, “黄想”, “张秀芳”, “颜桂芳”, “张洋”] 丰都北京路J座 725701 [“陈春梅”, “施婷婷”, “井成”, “范文”, “王楠”, “尚明”, “薛秀珍”, “宋金凤”, “杨雪”, “邓丽华”, “李杨”, “温佳”, “叶玉”, “周明”, “王桂珍”, “段玉华”, “金成”, “黄鑫”, “邬兵”, “魏柳”, “王兰英”, “杨柳”] 普陀潜江路P座 210730 [“储平”, “洪红霞”, “沈玉英”, “王洁”, “董玉英”, “邓凤英”, “谢海燕”, “梁雷”, “张畅”, “任玉兰”, “贾宇”, “汪成”, “孙琴”, “纪红梅”, “王欣”, “陈兵”, “张成”, “王东”, “谷霞”, “林成”] 普陀武街f座 706352 [“邢成”, “张建军”, “张鑫”, “戴涛”, “蔡洋”, “汪燕”, “尹亮”, “何利”, “何玉”, “周波”, “金秀珍”, “杨波”, “张帅”, “周柳”, “马云”, “张建华”, “王丽丽”, “陈丽”, “万萍”] 城东贵阳街O座 110567 [“李洁”, “陈静”, “王建国”, “方淑华”, “古想”, “漆萍”, “詹桂花”, “王成”, “李慧”, “孙娜”, “马伟”, “谢杰”, “王鹏”, “鞠桂英”, “莫桂英”, “汪雷”, “黄彬”, “李玉梅”, “祝红梅”] 现在,我们在图上可视化这个结果看看: cypher MATCH (p:人)-[`visit0`:到访]->(`addr`:地址)<-[`visit1`:到访]-(p1:人) WHERE id(p) == "p_101" AND `visit0`.`start_time` < `visit1`.`end_time` AND `visit0`.`end_time` > `visit1`.`start_time` RETURN paths; 结果中我们标记了 p_101 为不同的图标,在用标签传播算法识别一下聚集社区,是不是一图胜千言呢? 9.4 最近去过的省份 最后,我们再用简单的查询模式表达出一个人在给定时间内,比如从一个时间点开始,到访过的所有省份 cypher MATCH (p:人)-[visit:到访]->(`addr`:地址)-[:属于*5]-(province:省份) WHERE id(p) == "p_101" AND visit.start_time > 1625469000 RETURN province.省份.name, collect(addr.地址.name); 看起来他/她去过不少地方呢: province.省份.name collect(addr.地址.name) 四川省 [“闵行仇路q座 255960”] 山东省 [“城东贵阳街O座 110567”] 云南省 [“丰都北京路J座 725701”] 福建省 [“普陀潜江路P座 210730”] 内蒙古自治区 [“普陀武街f座 706352”] 老轨迹,我们在图上看看这个结果吧,这次,我们选择 Dagre-LR 这个布局渲染,结果是不是非常清晰呢? 10 总结 我们给出了不少社交网络里的应用案例,包括: 查找关键的人 识别聚集的人群、社群 判定两个用户之间的亲密度 推荐新朋友 利用共同邻居精准推送重要内容 根据好友关系、地理位置推送信息流 利用时空关系图谱查询人与人之间关系、获取时空相交的人、访问过的省份 社交网络作为天然的图结构,非常适合用图的技术来存储、查询、计算、分析与可视化去解决其上的各式各样的问题,NebulaGraph 的强大处理能力和可视化能力使得我们已知很多公司在使用它作为社交领域的图存储、计算层,这其中包括:网易游戏、微信、Line、Soul、快手和知乎等等很多行业领先的团队,希望大家通过本章能对社交领域的图技术应有有一个初步的认识。 题图版权:by Ryoji

2022/12/8
articleCard.readMore

chatGPT 加 NebulaGraph 预测 2022 世界杯冠军球队

一次利用 chatGPT 给出数据抓取代码,借助 NebulaGraph 图数据库与图算法预测体育赛事的尝试。 chatGPT and Nebulagraph Predict Fifa World Cup [TOC] --> 1 蹭热度 最近因为世界杯进行时,被这篇 Cambridge Intelligence 的文章启发(仅仅利用有限的信息量和条件,借助图算法的方法做合理的冠军预测),讨论到也可以试着用 NebulaGraph 玩玩冠军预测,还能顺道科普一波图库技术和图算法。 本来想着几个小时撸出来一个方案,被数据集的收集劝退了,我是是在懒得去 wikepedia 里抓取出来需要的数据,索性就按下放了几天。 同时,另一个热潮是最近 OpenAI 发布了 chatGPT 服务,它可以实现各种语言可是实现的复杂任务设计包括: 随时帮你实现一段什么样的代码 模拟成任意一个 prompt 界面:shell、python、virtual machine、甚至你创造的语言 假设、带入你给定的人设,和你聊天 写诗歌、rap、散文 找出一段代码的 bug 解释一段复杂的正则表达式的含义 chatGPT 的上下文能力、理解力到的前所未有的程度以至于所有人都在讨论新的工作方式:如何掌握让机器帮助我们完成任务。 所以,当我试过让 chatGPT 帮我写复杂的图数据库查询语句、解释复杂图查询语句的含义、解释一大段 Bison 代码含义之后我才意识到:为什么不让 chatGPT 帮我写好抓取数据的代码呢? 2 抓取数据 我真试了一下,结果是:完全可以,而且似乎真的很容易。 整个过程基本上我像是一个代码考试的面试官、或者是一个产品经理,提出我的需求,chatGPT 给出代码的实现。我们再试着跑起来代码,找到代码中不合理的地方,指出来、给出建议,chatGPT 就真的能理解我指出的点,并给出相应的修正,像是: 这一全过程我就不在这里列出来了,不过我把生成的代码和整个讨论的过程都分享在这里,感兴趣的同学可以去看看。 最终生成的数据是一个 CSV 文件: 代码生成的文件 world_cup_squads.csv 手动修改、分开了生日和年龄的列 world_cup_squads_v0.csv 它像这样,包含的信息有:球队、小组、编号、位置、球员名字、生日、年龄、参加国际比赛场次、进球数、服役俱乐部。 CSV Team,Group,No.,Pos.,Player,DOB,Age,Caps,Goals,Club Ecuador,A,1,1GK,Hernán Galíndez,(1987-03-30)30 March 1987,35,12,0,Aucas Ecuador,A,2,2DF,Félix Torres,(1997-01-11)11 January 1997,25,17,2,Santos Laguna Ecuador,A,3,2DF,Piero Hincapié,(2002-01-09)9 January 2002,20,21,1,Bayer Leverkusen Ecuador,A,4,2DF,Robert Arboleda,(1991-10-22)22 October 1991,31,33,2,São Paulo Ecuador,A,5,3MF,José Cifuentes,(1999-03-12)12 March 1999,23,11,0,Los Angeles FC 手动删除了 CSV 表头 world_cup_squads_no_headers.csv 3 图方法预测2022世界杯 3.1 图建模 前提,本文使用 NebulaGraph 和 NebulaGraph Explorer,你可以在阿里云上免费申请半个月的试用,入口链接是👉🏻 http://c.nxw.so/d52oz ,不要被这个陌生的短链接域名吓到,它只是帮助我们统计有多少同学通过这里访问到 https://market.aliyun.com 而已。 图建模(Graph Modeling)是把真实世界信息以”点–>边“的图形式去抽象与表示。 这里,我们根据公共领域能够容易获得信息、把它映射成如下的点与边: 点: player(球员) team(球队) group(小组) club(俱乐部) 边: groupedin(球队属于哪一小组) belongto(队员属于国家队) serve(队员在俱乐部服役) 而队员的年龄、参加国际场次(caps)、进球数(goals)则很自然作为 player 这一类点的属性。 下图是这个 schema 在 NebulaGraph Studio/Explorer(后边称 Studio/Explorer) 中的截图: 然后,我们可以在右上角把它保存,创建一个新的图空间,应用这个图建模到图空间里。 注:参考文档 https://docs.nebula-graph.com.cn/3.3.0/nebula-explorer/db-management/draft/ 3.2 导入数据进 NebulaGraph 有了图建模,我们可以把之前的 CSV 文件(无表头版本)上传到 Studio 或者 Explorer 里,通过点、选关联不同的列到点边中的 vid 和属性: 之后点击导入,就把整个图导入到 NebulaGraph 了,成功之后,我们还得到了整个csv –> Nebula Importer 的关联配置文件:nebula_importer_config_fifa.yml,你可以直接拖拽整个配置,不用自己去配置它了。 注:参考文档 https://docs.nebula-graph.com.cn/3.3.0/nebula-explorer/db-management/11.import-data/ 导入之后,我们可以在 schema 界面里查看数据统计,我们可以看到有 831 名球员参加了 2022 卡塔尔世界杯,他们服役在 295 个不同的俱乐部: 注:参考文档 https://docs.nebula-graph.com.cn/3.3.0/nebula-explorer/db-management/10.create-schema/#_6 3.3 探索数据 3.3.1 查询数据 下面,我们试着把所有的数据展示出来看看吧,首先,借助 NebulaGraph Explorer,我用拖拽的方式画出来任意类型的点(TAG)和任意类型点(TAG)之间的边,这里我们知道所有的点都是至少包含在一个边里的,所以不会漏掉任何孤立的点。 让它帮我生成查询的语句,这里,它默认 LIMIT 100 了,我们手动改大一些( LIMIT 10000),让它在 Console 里执行。 3.3.2 初步观察数据 结果渲染出来是这样子,可以看到它天然形成了一簇一簇的模式。 这些外围、形成的簇多是一些由非传统意义上的有全球影响力的俱乐部,和非传统的足球厉害的国家队的球员组成,因为通常这些俱乐部只有一两个球员,而且他们还集中在一个国家队、地区,所以没有和很多其他球员、国家队产生连接。 3.3.3 图算法辅助分析 在我点击了 Explorer 中的两个按钮之后(详细参考后边的文档链接),在浏览器里,我们可以看到整个图已经变成: 注:这部分功能可以参考文档 https://docs.nebula-graph.com.cn/3.3.0/nebula-explorer/graph-explorer/graph-algorithm/ 这里,利用到了两个图算法来分析这里的洞察: 利用点的出入度,改变他们的显示大小突出重要程度 利用 Louvain 算法区分点的社区分割 可以看到红色的大点是鼎鼎大名的巴塞罗那,而它的球员们也被红色标记了。 3.4 预测冠军算法 为了能充分利用图的魔法(与图上的隐含条件、信息),我的思路是选择一种利用链接进行节点重要程度分析的图算法,找出拥有更高重要性的点,对他们全局迭代、排序,从而获得前几名的国家队排名。 这些方法其实就体现在厉害的球员同时拥有更大的社区、连接度,同时,为了增加传统强队之间的区分度,我准备把出场率、进球数的信息也考虑进来。 最终,我的算法是: 取出所有的 (球员)-服役->(俱乐部) 的关系,过滤其中进球数过少、单场进球过少的球员(以平衡一些弱队的老球员带来的过大影响) 从所有过滤之后的球员中向外探索,获得国家队 在以上的子图上运行 Betweenness Centrality 算法,计算节点重要度评分 3.4.1 算法过程 首先,我们取出所有进球数超过 10,场均进球超过 0.2 的 (球员)-服役->(俱乐部) 的子图: cypher MATCH ()-[e]->() WITH e LIMIT 10000 WITH e AS e WHERE e.goals > 10 AND toFloat(e.goals)/e.caps > 0.2 RETURN e 注:为了方便,我把进球数和出场数也作为了 serve 边上的属性了。 然后,我们全选图上的所有点,点击左边的工具栏,选择出方向的 belongto 边,向外进行图拓展(遍历),同时选择将拓展得到的新点标记为旗帜的 icon: 现在,我们获得了最终的子图,我们利用工具栏里的浏览器内的图算法功能,执行 BNC(Betweenness Centrality) 然后,这个子图变成了这样子: 4 预测结果 最终,我们根据 Betweenness Centrality 的值,排序,可以得到最终的获胜球队应该是:巴西 🇧🇷! 其次是比利时、德国、英格兰、法国、阿根廷,让我们等两个礼拜回来看看预测结果是否准确吧 :D。 注:排序数据(其中还有非参赛球队的点) Vertex Betweenness Centrality Brazil🇧🇷 3499 Paris Saint-Germain 3073.3333333333300 Neymar 3000 Tottenham Hotspur 2740 Belgium🇧🇪 2587.833333333330 Richarlison 2541 Kevin De Bruyne 2184 Manchester City 2125 İlkay Gündoğan 2064 Germany🇩🇪 2046 Harry Kane (captain 1869 England🏴󠁧󠁢󠁥󠁮󠁧󠁿 1864 France🇫🇷 1858.6666666666700 Argentina🇦🇷 1834.6666666666700 Bayern Munich 1567 Kylian Mbappé 1535.3333333333300 Lionel Messi (captain 1535.3333333333300 Gabriel Jesus 1344 题图:这个文章的图也是用 OpenAI DALL-E 2 生成,并用 DALL-E 2 Outpainting 扩充的,原图。

2022/12/6
articleCard.readMore