Neo4j【从无到有从有到无】【N3】使用图进行数据建模

目录

1.模式与目标(Models and Goals)

2.标记的属性图模型

3.查询图:Cypher简介

3.1.Cypher Philosophy

3.2.MATCH

3.3.RETURN

3.4.其他Cypher语法

4.关系和图建模的比较

4.1.系统管理领域中的关系建模

4.2.系统管理域中的图形建模

4.3.测试模型

5.跨域模型

5.1.创建莎士比亚图

5.2.开始查询

5.3.声明要查找的信息模式

5.4.约束匹配

5.5.处理结果

5.6.查询链接

5.7.常见的建模陷阱

5.7.1.电子邮件出处问题域

5.7.2.明智的第一次迭代?

5.7.3.第二次的魅力

5.7.4.不断发展的领域

6.识别节点和关系

7.避免反模式

8.摘要


在前面的章节中,我们描述了图数据库与其他NOSQL存储和传统关系数据库相比的巨大优势。 但是选择采用图形数据库后,出现了一个问题:如何在图形中建模?

本章重点介绍图建模。 首先从标签化的属性图模型(最广泛使用的图数据模型)的概述开始,然后概述本书中大多数代码示例所使用的图查询语言:Cypher。 尽管存在几种图形查询语言,但是Cypher部署最广泛,使其成为事实上的标准。 它也很容易学习和理解,特别是对于那些来自SQL背景的人。 有了这些基础知识之后,我们将直接研究一些图形建模的示例。 在基于系统管理域的第一个示例中,我们比较了关系和图形建模技术。 在第二个例子(莎士比亚文学的生产和消费)中,我们使用图来连接和查询几个不同的领域。 在本章的结尾,我们介绍了使用图形建模时的一些常见陷阱,并重点介绍了一些好的做法。

1.模式与目标(Models and Goals)

在深入研究图形建模之前,请先对模型进行概括。 建模是受特定需求或目标推动的抽象活动。 我们进行建模是为了将不守法域的特定方面带入可以对其进行结构化和处理的空间。 世界没有“really is,”的自然表现,只有许多有目的的选择,抽象和简化,其中一些在满足特定目标方面比其他更为有用。

在这方面,图形表示形式没有什么不同。 但是,它们与许多其他数据建模技术的不同之处可能在于逻辑模型与物理模型之间的紧密联系。 关系数据管理技术要求我们偏离域的自然语言表示形式:首先通过将表示形式组合成逻辑模型,然后将其强制为物理模型。 这些转换在我们对世界的概念化与该模型的数据库实例化之间引入了语义上的矛盾。 使用图形数据库,这个差距大大缩小了。

我们已经在图形中进行交流

图形建模自然符合我们倾向于使用圆和框从域中提取细节,然后通过用箭头和线将它们连接起来来描述这些事物之间的联系的方式。 当今的图形数据库比其他任何数据库技术都更“whiteboard friendly”。问题的典型白板视图是图形。 我们在创意和分析模式下绘制的内容与我们在数据库内部实现的数据模型紧密对应。

在表达性方面,图形数据库减少了困扰关系数据库实现多年的分析和实现之间的阻抗失配。 这种图模型特别有趣的是,它们不仅传达了我们认为事物之间的关系,而且还清楚地传达了我们想问的领域内的各种问题。

我们将在本章中看到,图模型和图查询实际上只是同一枚硬币的两个方面。

2.标记的属性图模型

我们在第1章中介绍了标记的属性图模型。总而言之,这些是其显着的特征:

  • 带标签的属性图由节点,关系,属性和标签组成。
  • 节点包含属性。 将节点视为以任意键值对形式存储属性的文档。 在Neo4j中,键是字符串,值是Java字符串和原始数据类型,以及这些类型的数组。
  • 节点可以用一个或多个标签标记。 标签将节点分组在一起,并指示它们在数据集中扮演的角色。
  • 关系连接节点并构造图。 关系始终具有一个方向,一个名称以及一个开始节点和一个结束节点,没有悬空的关系。 关系的方向和名称共同为节点的结构增加了语义的清晰度。
  • 像节点一样,关系也可以具有属性。 向关系添加属性的功能对于为图形算法提供其他元数据,为关系添加其他语义(包括质量和权重)以及在运行时约束查询特别有用。

这些简单的原语是我们创建复杂且语义丰富的模型所需要的。 到目前为止,我们所有的模型都是以图表的形式。 图表非常适合在任何技术环境之外描述图表,但是在使用数据库时,我们需要其他一些机制来创建,处理和查询数据。 我们需要一种查询语言。

3.查询图:Cypher简介

Cypher是一种表现力强(但很紧凑)的图形数据库查询语言。 尽管当前特定于Neo4j,但它与我们将图形表示为图形的习惯紧密相关,这使其非常适合以编程方式描述图形。 因此,在本书的其余部分中,我们将使用Cypher来说明图形查询和图形构造。 Cypher可以说是最容易学习的图形查询语言,并且是学习图形的重要基础。 一旦了解了Cypher,就可以很容易地进行分支和学习其他图形查询语言。

在以下各节中,我们将简要介绍Cypher。 但是,这不是Cypher的参考文档,它只是一个友好的介绍,因此以后我们可以探索更有趣的图形查询方案。

其他查询语言

其他图形数据库具有其他查询数据的方式。许多人,包括的Neo4j,支持RDF查询语言SPARQL和势在必行,基于路径的查询语言 Gremlin。 但是,我们的兴趣在于将属性图的表达能力与声明性查询语言结合使用,因此在本书中,我们几乎只关注Cypher。

3.1.Cypher Philosophy

Cypher旨在使开发人员,数据库专业人员和业务利益相关者易于阅读和理解。它更容易从一个事实,即它是在与雅阁的方式导出使用我们直观地描述使用图图表。

Cypher使用户(或代表用户运行的应用程序)能够要求数据库查找与特定模式匹配的数据。 通俗地说,我们要求数据库“查找类似的东西”。我们描述“类似的东西”外观的方式是使用ASCII艺术画它们。 图3-1显示了一个简单模式的示例。

此模式描述了三个共同的朋友。 这是Cypher中等效的ASCII艺术作品表示形式:

(emil)<-[:KNOWS]-(jim)-[:KNOWS]->(ian)-[:KNOWS]->(emil)

此模式描述了一条路径,该路径将一个称为jim的节点连接到两个称为ian和emil的节点,还将ian节点连接到emil节点。 ian,jim和emil是标识符。 标识符使我们在描述模式时可以多次引用同一个节点—一种技巧,可以帮助我们绕过以下事实:查询语言只有一个维度(文本从左到右),而可以放置图形图 二维化。 尽管偶尔需要以这种方式重复标识符,但意图仍然很明确。 Cypher 模式非常自然地遵循我们在白板上绘制图形的方式。

先前的Cypher模式描述了一种简单的图形结构,但尚未引用数据库中的任何特定数据。 要将模式绑定到现有数据集中的特定节点和关系,我们必须指定一些属性值和节点标签,以帮助在数据集中定位相关元素。 例如:

(emil:Person {name:'Emil'}) <-[:KNOWS]-(jim:Person {name:'Jim'})-[:KNOWS]->(ian:Person {name:'Ian'})-[:KNOWS]->(emil)

在这里,我们使用其name属性和Person标签将每个节点绑定到其标识符。 例如,emil identifer绑定到数据集中的一个节点,该节点的标签为Person,名称属性为Emil。 以这种方式将模式的某些部分固定到实际数据是Cypher的常规做法,我们将在以下各节中看到。

举例说明

关于图的有趣之处在于,它们倾向于包含节点和关系的特定实例,而不是类或原型。 通常甚至会使用由实节点和关系构成的较小子图来说明甚至非常大的图。 换句话说,我们倾向于通过示例来描述图形。

ASCII艺术图形模式是Cypher的基础。 Cypher查询使用谓词将模式的一个或多个部分锚定到图形中的特定位置,然后弯曲未锚定的部分以查找局部匹配项。

Cypher根据查询中的标签和属性谓词确定实际图形中与模式的某些部分绑定到的锚点。 在大多数情况下,Cypher使用有关现有索引,约束和谓词的元信息来自动找出问题。 但是,有时它有助于指定一些其他提示。

像大多数查询语言一样,Cypher由子句组成。 最简单的查询由一个MATCH子句和一个RETURN子句组成(我们将在本章稍后介绍在Cypher查询中可以使用的其他子句)。 这是一个使用以下三个子句来查找名为Jim的用户的共同朋友的Cypher查询示例:

MATCH (a:Person {name:'Jim'})-[:KNOWS]->(b)-[:KNOWS]->(c),
(a)-[:KNOWS]->(c)
RETURN b, c

让我们更详细地查看每个子句。

3.2.MATCH

MATCH子句是大多数Cypher查询的核心。 这是示例说明部分。 使用ASCII字符表示节点和关系,我们绘制感兴趣的数据。我们用括号绘制节点,并使用带有大于或小于号(->和<-)的破折号对绘制关系。 <和>符号指示关系方向。 在短划线之间,用方括号括起来,并用冒号作为前缀,我们放置了关系名称。 节点标签类似地以冒号作为前缀。 然后,在花括号(非常像Javascript对象)中指定节点(和关系)属性键值对。

在示例查询中,我们正在寻找一个名为Person的节点,其名称属性为Jim。 该查询的返回值绑定到标识符a。该标识符使我们可以在整个查询的其余部分中引用表示Jim的节点。

此起始节点是简单模式 (a)-[:KNOWS]->(b)-[:KNOWS]->(c), (a)-[:KNOWS]->(c) 描述了一个包含三个节点的路径,其中一个是
绑定到标识符a,其他绑定到b和c。 这些节点通过以下方式连接多个KNOWS关系,如图3-1所示。

从理论上讲,这种模式可能会在整个图形数据中多次出现。 对于较大的用户集,可能存在许多与此模式相对应的相互关系。要本地化查询,我们需要它的某些部分锚定到图中的一个或多个地点。 在指定我们要寻找一个标记为Person且名称属性值为Jim的节点时,我们已将模式绑定到图中的特定节点,即代表Jim的节点。 然后,Cypher将模式的其余部分与紧靠该锚点的图形进行匹配。 这样,它将发现要绑定到其他标识符的节点。 尽管a将始终固定在Jim上,但是b和c将在查询执行时绑定到一系列节点上。

另外,我们可以在WHERE子句中将锚表示为谓词。

MATCH (a:Person)-[:KNOWS]->(b)-[:KNOWS]->(c), (a)-[:KNOWS]->(c)
WHERE a.name = 'Jim'
RETURN b, c

在这里,我们已将属性查找从MATCH子句移到WHERE子句。结果与之前的查询相同。

3.3.RETURN

此子句指定应将匹配数据中的哪些节点,关系和属性返回给客户端。 在示例查询中,我们有兴趣返回绑定到b和c标识符的节点。 当客户端迭代结果时,每个匹配节点都被延迟绑定到其标识符。

3.4.其他Cypher语法

我们可以在Cypher查询中使用的其他子句包括:

  • WHERE 
    • 提供用于过滤模式匹配结果的条件。
  • CREATE and  CREATE UNIQUE
    • 创建节点和关系。
  • MERGE
    • 通过重用与提供的谓词匹配的现有节点和关系,或通过创建新的节点和关系,确保图中存在提供的模式。
  • DELETE
    • 删除节点,关系和属性。
  • SET
    • 设置属性值。
  • FOREACH
    • 对列表中的每个元素执行更新操作。
  • UNION
    • 合并两个或多个查询的结果。
  • WITH
    • 链接后续查询部分并将结果从一个查询转发到下一个查询。 类似于Unix中的管道命令。
  • START
    • 在图中指定一个或多个明确的起点-节点或关系。 (不建议使用START,而应在MATCH子句中指定锚点。)

如果这些条款看起来很熟悉(尤其是您是SQL开发人员),那就太好了! Cypher旨在使您足够熟悉,以帮助您沿着学习曲线快速移动。 同时,它的区别足以强调我们是在处理图形,而不是关系集。

我们将在本章后面看到这些条款的一些示例。 在它们发生的地方,我们将更详细地描述它们如何工作。

现在,我们已经了解了如何使用Cypher来描述和查询图形,下面我们来看一些图形建模的示例。

4.关系和图建模的比较

为了介绍图形建模,我们将研究如何使用基于关系和基于图形的技术对域进行建模。 大多数开发人员和数据专业人员都熟悉RDBMS(关系数据库管理系统)和相关的数据建模技术。 结果,比较将突出一些相似之处和许多差异。 特别是,我们将看到从概念图模型转换为物理图模型有多么容易,并且相对于关系模型,图模型扭曲了我们要表示的内容。

为了便于进行比较,我们将研究一个简单的数据中心管理域。 在此领域中,数个数据中心代表使用不同基础架构(从虚拟机到物理负载平衡器)的许多客户,支持许多应用程序。 此域的示例如图3-2所示。

在图3-2中,我们看到了一些应用程序以及支持它们的必要数据中心基础结构的简化视图。 由节点App 1,App 2和App 3表示的应用程序依赖于标记为Database Server 1、2、3的数据库集群。 尽管用户在逻辑上取决于应用程序及其数据的可用性,但用户与应用程序之间存在其他物理基础架构; 此基础结构包括虚拟客户端(VM10、11、20、30、31),真实服务器(Service1、2、3),服务器机架(Rack1、2)和负载均衡器(Balance 1、2) ),位于应用程序的前面。 当然,在每个组件之间都有许多网络元素:电缆,交换机,配线架,NIC(网络接口控制器),电源,空调等,所有这些元素在不方便的时候都会发生故障。 为了完成图片,我们有一个应用程序3的单一用户,由用户3表示。

作为此类系统的运营商,我们有两个主要关注点:

  • 持续提供满足(或超过)服务水平协议的功能,包括执行前瞻性分析以确定单点故障的能力以及追溯分析以快速确定引起客户对服务可用性投诉的原因的能力。
  • 为消耗的资源开账单,包括硬件,虚拟化,网络配置的成本,甚至软件开发和运营的成本(因为这些只是我们在此处看到的系统的逻辑扩展)。

如果我们要构建数据中心管理解决方案,则需要确保基础数据模型允许我们以有效解决这些主要问题的方式来存储和查询数据。 我们还将希望能够随着应用程序产品组合的变化,数据中心的物理布局的发展以及虚拟机实例的迁移而更新基础模型。 考虑到这些需求和约束,让我们看看关系模型和图形模型的比较。

4.1.系统管理领域中的关系建模

关系世界中建模的初始阶段与许多其他数据建模技术的初始阶段相似:也就是说,我们试图理解并同意域中的实体,它们之间的相互关系以及控制其状态转换的规则 。 其中大多数倾向于非正式地完成,通常是通过白板草图以及主题专家,系统与数据架构师之间的讨论来完成。 为了表达我们的共同理解和共识,我们通常创建一个如图3-2所示的图表。

下一阶段以更严格的形式捕获此协议,例如实体关系(E-R)图-另一个图。 使用更严格的表示法将概念模型转换为逻辑模型,为我们提供了第二次机会来完善我们的领域词汇,以便可以与关系数据库专家共享。 (这种方法并非总是必要的:熟练的关系用户经常直接进行表设计和规范化操作,而无需先描述中间的E-R图。)在我们的示例中,我们已在图3-3所示的E-R图中捕获了域。

尽管是图,但ER图立即证明了关系模型捕获富域的缺点。尽管它们允许命名关系(图数据库完全包含了某种东西,但关系存储却没有),但ER图仅允许单个,无向 ,实体之间的命名关系。 在这方面,关系模型不适用于实体域之间的关系既丰富又语义丰富多样的现实世界域。

找到合适的逻辑模型后,我们将其映射到表和关系中,对它们进行规范化以消除数据冗余。在许多情况下,此步骤很简单,例如将E-R图转录为表格形式,然后通过SQL命令将这些表加载到数据库中。但是,即使是最简单的情况也可以用来突出关系模型的特质。例如,在图3-4中,我们看到大量意外复杂性以外键约束(所有带注释[FK])的形式进入了模型,该约束支持一对多关系以及联接表(例如AppDatabase),它支持多对多关系-在添加一行实际用户数据之前,所有这些都需要支持。这些约束是简单存在的模型级元数据,因此我们可以在查询时具体化表之间的关系。但是,人们还是敏锐地感觉到这种结构性数据的存在,因为它会使服务于数据库而不是用户的数据变得混乱和模糊。

现在,我们有了一个相对于该领域相对忠诚的规范化模型。 尽管该模型以外键和联接表的形式带来了相当大的意外复杂性,但其中没有重复的数据。 但是我们的设计工作尚未完成。 关系范式的挑战之一是规范化模型通常不够快,无法满足实际需求。 对于许多生产系统,规范化的模式在理论上适合于回答我们可能希望对域提出的任何特殊问题,在实践中必须对其进行进一步调整并使其专门用于特定的访问模式。 换句话说,为了使关系存储能够很好地满足常规应用程序的需求,我们必须放弃任何具有真实域亲和力的痕迹,并接受必须更改用户的数据模型以适合数据库引擎(而不是用户)的观点。 这种技术称为非规范化

为了获得查询性能,非规范化涉及复制数据(在某些情况下,实质上是重复的)。 以用户及其联系方式为例。 典型的用户通常有几个电子邮件地址,在完全规范化的模型中,我们会将其存储在单独的EMAIL表中。 但是,为了减少联接和两个表之间的联接带来的性能损失,在USER表中内联此数据,添加一个或多个列来存储用户最重要的电子邮件地址是很常见的。

尽管进行非规范化可能是安全的事情(假设开发人员了解非规范化模型及其如何映射到以域为中心的代码,并从数据库获得强大的事务支持),但这通常不是一项琐碎的任务。为了获得最佳结果, 我们通常会请一位真正的RDBMS专家来将我们的规范化模型改成与基础RDBMS和物理存储层的特征一致的非规范化模型。 为此,我们接受可能存在大量的数据冗余。

我们可能会认为所有这些设计规范化非规范化工作都是可以接受的,因为这是一项一次性的任务。 这种学派认为,工作成本在系统的整个生命周期(包括开发和生产)中摊销,因此与项目的总成本相比,生成高效的关系模型的工作量相对较小。 这是一个吸引人的概念,但是在许多情况下,它与现实不符,因为系统不仅在开发过程中会发生变化,而且在产品生命周期中也会发生变化。

我们可能会认为所有这些设计规范化非规范化工作都是可以接受的,因为这是一项一次性的任务。 这种学派认为,工作成本在系统的整个生命周期(包括开发和生产)中摊销,因此与项目的总成本相比,生成高效的关系模型的工作量相对较小。 这是一个吸引人的概念,但是在许多情况下,它与现实不符,因为系统不仅在开发过程中会发生变化,而且在产品生命周期中也会发生变化。

数据模型更改的摊销视图(在模型中,开发过程中代价高昂的更改被生产中稳定模型的长期利益所掩盖)假定系统在生产环境中花费了大部分时间,并且这些生产环境是稳定的。 尽管大多数系统可能会在生产环境中花费大部分时间,但这些环境很少稳定。 随着业务需求的变化或法规要求的发展,我们的系统和基于它们的数据结构也必须随之变化。

在项目的设计和开发阶段,数据模型总是会经过实质性的修订,并且在几乎每种情况下,这些修订都旨在满足模型的需求,以适应将要在生产中使用它的应用程序的需求。 这些最初的设计影响力如此之大,以至于在生产后就无法修改应用程序和模型以适应最初设计时无法做的事情。

我们将结构更改引入数据库的技术机制称为迁移(Migrations),这种迁移已被Rails等应用程序开发框架所普及。 迁移提供了一种结构化的,逐步的方法,可以将一组数据库重构应用到数据库,以便可以负责任地发展它以满足使用它的应用程序不断变化的需求。 但是,与我们通常在几秒钟或几分钟内完成的代码重构不同,数据库重构可能需要数周或数月的时间才能完成,而架构更改则需要停机时间。 数据库重构速度慢,风险大且昂贵。

因此,非规范化模型的问题在于其对系统业务需求的快速发展的抵制。正如我们在数据中心示例中看到的那样,在实施关系解决方案的过程中对白板模型施加的更改,在概念世界和数据物理布局方式之间产生了鸿沟。这种概念上的关系失调几乎阻止了业务利益相关者在系统的进一步发展中进行积极的协作。利益相关者的参与停止在关系大厦的门槛上。在开发方面,将已更改的业务需求转换为基础和根深蒂固的关系结构的困难使系统的发展滞后于业务的发展。没有专家的帮助和严格的计划,迁移非规范化的数据库会带来很多风险。如果迁移无法维持存储亲和性,则性能可能会受到影响。同样严重的是,如果在迁移后将故意重复的数据留为孤立,我们有可能危及整个数据的完整性。

4.2.系统管理域中的图形建模

我们已经看到了关系建模及其伴随的实现活动,如何使我们沿着将应用程序的基础存储模型与利益相关者的概念世界观分离开来的道路。 具有刚性模式和复杂建模特性的关系数据库并不是支持快速变化的特别好的工具。 我们需要一个与领域紧密相关的模型,但它不会牺牲性能,并且在数据进行快速更改和增长时,既支持演化又保持数据的完整性。 该模型是图模型。 那么,使用图形数据模型实现时,此过程有何不同?

在分析的早期阶段,我们所需的工作类似于关系方法:使用lo-fi方法(例如白板草图),我们描述并同意了该领域。 但是,此后方法有所不同。 我们没有丰富领域模型的图形表示形式,而是将其丰富化,目的是对领域中与我们的应用目标相关的部分进行准确的表示。 也就是说,对于我们域中的每个实体,我们都确保已捕获其相关角色(作为标签),其属性(作为属性)以及与相邻实体的关系(作为关系)。

记住,领域模型不是通向现实的透明,无上下文的窗口:相反,它是对领域中与我们的应用程序目标有关的那些方面的有目的抽象。 建立模型总是有动力。 通过使用其他属性和关联性来丰富我们的第一手域图,我们可以有效地生成与应用程序的数据需求相匹配的图模型。 也就是说,我们提供了回答我们的应用程序将询问其数据的各种问题。

有用的是,领域建模与图建模完全同构。 通过确保域模型的正确性,我们隐式改进了图形模型,因为在图形数据库中,您在白板上绘制的内容通常是您在数据库中存储的内容。

用图形表示,我们正在做的是确保每个节点具有适当的角色特定的标签和属性,以便其能够履行其以数据为中心的专用域职责。 但是,我们还要确保每个节点都位于正确的语义上下文中; 为此,我们通过在节点之间创建命名和定向(通常是属性)关系来捕获域的结构方面。 对于我们的数据中心场景,生成的图形模型如图3-5所示。

从逻辑上讲,这就是我们需要做的。 没有表,没有规范化,没有非规范化。 一旦我们有了域模型的准确表示,将其移到数据库中就变得微不足道了,我们很快就会看到。

请注意,这里的大多数节点都有两个标签:特定类型的标签(例如Database,App或Server)和更通用的Asset标签。 这使我们能够通过某些查询来定位特定类型的资产,并通过其他查询来定位所有资产,而不论类型如何。

4.3.测试模型

完善域模型后,下一步就是测试它是否适合回答实际查询。 尽管图形非常适合于支持不断发展的结构(因此可以纠正任何错误的早期设计决策),但是有许多设计决策一旦被应用到我们的应用程序中,便会进一步阻碍我们前进。 通过在此早期阶段回顾域模型和结果图模型,我们可以避免这些陷阱。 图结构的后续更改将仅由业务更改驱动,而不是由减轻不良设计决策的需求驱动。

实际上,我们可以在此处应用两种技术。 第一个,也是最简单的,只是检查图是否读得好。 我们选择一个开始节点,然后关注与其他节点的关系,并在进行过程中读取每个节点的标签和每个关系的名称。 这样做应该创造出明智的句子。 对于我们的数据中心示例,我们可以读取如下语句:“由应用程序实例1、2和3组成的应用程序使用驻留在数据库服务器1、2和3上的数据库,”和“服务器3运行承载应用程序实例3的VM31。”如果以这种方式读取图形是有意义的,那么我们可以合理地确信它对域是忠实的。

为了进一步提高信心,我们还需要考虑将在图(graph)上运行的查询。在这里,我们采用可查询性思维方式的设计。为了验证图是否支持我们期望在其上运行的查询,我们必须描述这些查询。这要求我们了解最终用户的目标;也就是说,将要应用图形的用例。例如,在我们的数据中心场景中,我们的一个使用案例涉及最终用户报告应用程序或服务无响应。为了帮助这些用户,我们必须找出无响应的原因,然后加以解决。为了确定可能出了什么问题,我们需要确定用户与应用程序之间路径上的内容,以及应用程序向用户交付功能所依赖的内容。给定数据中心域的特定图形表示形式,如果我们可以设计一个解决该用例的Cypher查询,则我们甚至可以确定该图形满足我们域的需求。

继续我们的示例用例,假设我们可以从常规的网络监控工具中更新图表,从而为我们提供网络状态的近实时视图。 对于大型物理网络,我们可能会使用复杂事件处理(CEP)处理低级网络事件流,仅在CEP解决方案引发重大领域事件时才更新图形。 当用户报告问题时,我们可以将物理故障查找限制在用户与应用程序之间以及应用程序及其依存关系之间的有问题的网络元素上。 在我们的图形中,可以通过以下查询找到有故障的设备:

MATCH (user:User)-[*1..5]-(asset:Asset)
WHERE user.name = 'User 3' AND asset.status = 'down'
RETURN DISTINCT asset

这里的MATCH子句描述了一个长度介于一到五个关系之间的可变长度路径。 关系是未命名和无方向的(方括号之间没有冒号或关系名称,也没有箭头指示方向)。这使我们可以匹配以下路径:

(user)-[:USER_OF]->(app)
(user)-[:USER_OF]->(app)-[:USES]->(database)
(user)-[:USER_OF]->(app)-[:USES]->(database)-[:SLAVE_OF]->(another-database)
(user)-[:USER_OF]->(app)-[:RUNS_ON]->(vm)
(user)-[:USER_OF]->(app)-[:RUNS_ON]->(vm)-[:HOSTED_BY]->(server)
(user)-[:USER_OF]->(app)-[:RUNS_ON]->(vm)-[:HOSTED_BY]->(server)-[:IN]->(rack)
(user)-[:USER_OF]->(app)-[:RUNS_ON]->(vm)-[:HOSTED_BY]->(server)-[:IN]->(rack)
<-[:IN]-(load-balancer)

就是说,从报告问题的用户开始,我们沿着长度为1到5的无向路径匹配图中的所有资产。我们添加了资产节点,这些资产节点的status属性的值小于我们的结果。 如果节点没有状态属性,则该节点不会包含在结果中。 RETURN DISTINCT资产可确保无论匹配多少次,结果中都将返回唯一有价值的事物。

鉴于我们的图形很容易支持这样的查询,因此我们可以确信该设计适合目标。

5.跨域模型

业务洞察力通常取决于我们了解复杂价值链中隐藏的网络效应。 为了产生这种理解,我们需要将域合并在一起,而不会扭曲或牺牲每个域特有的细节。 属性图在此处提供了解决方案。 使用属性图,我们可以将价值链建模为图的图,其中特定的关系连接并区分组成子域。

在图3-6中,我们看到了围绕莎士比亚文学的生产和消费的价值链的图形表示。 在这里,我们可以获得有关莎士比亚和他的一些戏剧的高质量信息,以及最近进行过戏剧表演的公司之一的详细信息,以及剧院场地和一些地理空间数据。 我们甚至添加了评论。 总之,该图描述并连接了三个不同的域。 在图表中,我们区分了这三种具有不同格式关系的域:点号代表文学领域,实线代表戏剧领域,点划线代表地理空间领域。

首先看一下文学领域,我们有一个代表莎士比亚本人的节点,带有一个标签Author和属性firstname:'William'和 lastname:'Shakespeare'。 该节点通过名为WROTE_PLAY的关系连接到一对节点,每个节点都标记为Play,分别代表戏剧Julius Caesar( title:'Julius Caesar')和The Tempest( title:'The Tempest')。

按照关系箭头的方向从左到右阅读该子图,告诉我们作家威廉·莎士比亚(William Shakespeare)创作了戏剧《凯撒大帝》和《暴风雨》。 如果我们对出处感兴趣,那么每个WROTE_PLAY关系都有一个date属性,它告诉我们Julius Caesar于1599年编写,而Tempest于1610年编写。了解如何添加莎士比亚的其他作品(戏剧)是一件微不足道的事情。 只需添加更多代表每个作品的节点,然后通过WROTE_PLAY和WROTE_POEM关系将它们加入莎士比亚节点,就可以将诗和诗歌插入图表中。

通过用手指跟踪WROTE_PLAY关系箭头,我们可以有效地完成图形数据库执行的工作,尽管是以人的速度而不是计算机的速度进行的。 稍后我们将看到,此简单的遍历操作是任意复杂的图形查询的基础。

接下来转到戏剧领域,我们添加了有关皇家莎士比亚剧团(通常简称为RSC)的一些信息,其形式为带有标签Company和属性键名称(其值为RSC)的节点。 毫无疑问,戏剧领域与文学领域息息相关。 在我们的图表中,RSC具有Julius Caesar和The Tempest的PRODUCED版本。 反过来,这些戏剧作品通过PRODUCTION_OF关系与文学领域的戏剧联系起来。

该图还捕获了特定性能的详细信息。 例如,作为RSC夏季巡回演出的一部分,RSC在2012年7月29日进行了Julius Caesar的制作。 如果我们对表演场地感兴趣,我们只需遵循表演节点的传出VENUE关系,即可发现该表演是在皇家剧院进行的,由一个标有Venue的节点表示。

该图还允许我们捕获对特定性能的评论。 在我们的示例图中,我们仅包含了用户Billy撰写的7月29日效果的评论。 我们可以在性能,评级和用户节点的相互作用中看到这一点。 在这种情况下,我们有一个标记为User的节点,表示Billy(属性name:'Billy'),其传出的WROTE_REVIEW关系连接到表示其评论的节点。 “Review”节点包含一个数字评级属性和一个自由文本审阅属性。 该评论通过传出的REVIEW_OF关系链接到特定的效果。 为了将其扩展到许多用户,许多评论和许多性能,我们只需向图添加更多带有适当标签和更同名关系的节点。

第三个域是地理空间数据域,它包含一个简单的位置层次树。 该地理空间域在图中的几个点连接到其他两个域。 雅芳的斯特拉特福市(财产名称:“雅芳河畔的斯特拉福德福特”)由于是莎士比亚的发源地而与文学领域息息相关(莎士比亚的名字是BORN_IN斯特拉特福德)。 只要它是RSC的所在地(RSC是BASED_IN Stratford),它就连接到戏剧域。 要根据雅芳的地理条件进一步了解斯特拉特福,我们可以通过与COUNTRY的往来COUNTRY关系来了解它是否位于名为England的国家/地区。

注意图如何减少跨域重复数据的实例。 例如,埃文河畔的斯特拉特福就参与了这三个领域。

该图可以捕获更复杂的地理空间数据。 例如,查看与Theatre Royal相连的节点上的标签,我们发现它位于Gray Street上,该大街位于Newcastle市中,该市位于Tyne and Wear县,而该市最终位于 英格兰国家-就像埃文河畔的斯特拉特福一样。

关系和标签

我们在这里使用关系名称和节点标签来构造图并为每个节点建立语义上下文。

关系的名称和方向通过以有意义的方式连接两个节点来帮助建立语义上下文。 例如,通过遵循传出的WROTE_REVIEW关系,我们了解到该关系末尾的节点表示评论。

关系有助于将图形划分到单独的域中并连接这些域。 从莎士比亚的例子中可以看出,通过属性图模型,可以轻松组合不同的域(每个域都具有自己的特定实体,标签,属性和关系),这种方式不仅使每个域都可以访问,而且可以产生洞察力 从域之间的连接。

标签代表每个节点在我们的域中扮演的角色。 因为一个节点可以连接到许多其他节点,其中一些节点可能来自非常不同的域,所以一个节点可以潜在地履行几个不同的角色。

标签是属性图模型的一等公民。 标签除了表明不同节点在我们的域中扮演的角色外,还允许我们将元数据与那些标签所附加的节点相关联。 例如,我们可以为所有带有用户标签的节点建立索引,或者要求所有带有客户标签的节点具有唯一的电子邮件属性值。

5.1.创建莎士比亚图

要创建如图3-6所示的莎士比亚图,我们使用CREATE来构建整体结构。 该语句由Cypher运行时在单个事务中执行,因此一旦执行了该语句,我们就可以确信该图完整地存在于数据库中。 如果事务失败,则数据库中将不包含任何图形。 如我们所料,Cypher具有人性化和可视化的图形生成方式:

CREATE (shakespeare:Author {firstname:'William', lastname:'Shakespeare'}),
(juliusCaesar:Play {title:'Julius Caesar'}),
(shakespeare)-[:WROTE_PLAY {year:1599}]->(juliusCaesar),
(theTempest:Play {title:'The Tempest'}),
(shakespeare)-[:WROTE_PLAY {year:1610}]->(theTempest),
(rsc:Company {name:'RSC'}),
(production1:Production {name:'Julius Caesar'}),
(rsc)-[:PRODUCED]->(production1),
(production1)-[:PRODUCTION_OF]->(juliusCaesar),
(performance1:Performance {date:20120729}),
(performance1)-[:PERFORMANCE_OF]->(production1),
(production2:Production {name:'The Tempest'}),
(rsc)-[:PRODUCED]->(production2),
(production2)-[:PRODUCTION_OF]->(theTempest),
(performance2:Performance {date:20061121}),
(performance2)-[:PERFORMANCE_OF]->(production2),
(performance3:Performance {date:20120730}),
(performance3)-[:PERFORMANCE_OF]->(production1),
(billy:User {name:'Billy'}),
(review:Review {rating:5, review:'This was awesome!'}),
(billy)-[:WROTE_REVIEW]->(review),
(review)-[:RATED]->(performance1),
(theatreRoyal:Venue {name:'Theatre Royal'}),
(performance1)-[:VENUE]->(theatreRoyal),
(performance2)-[:VENUE]->(theatreRoyal),
(performance3)-[:VENUE]->(theatreRoyal),
(greyStreet:Street {name:'Grey Street'}),
(theatreRoyal)-[:STREET]->(greyStreet),
(newcastle:City {name:'Newcastle'}),
(greyStreet)-[:CITY]->(newcastle),
(tyneAndWear:County {name:'Tyne and Wear'}),
(newcastle)-[:COUNTY]->(tyneAndWear),
(england:Country {name:'England'}),
(tyneAndWear)-[:COUNTRY]->(england),
(stratford:City {name:'Stratford upon Avon'}),
(stratford)-[:COUNTRY]->(england),
(rsc)-[:BASED_IN]->(stratford),
(shakespeare)-[:BORN_IN]->stratford

前面的Cypher代码有两个不同的作用。它创建带标签的节点(及其属性),然后将它们与关系(必要时及其关系属性)连接起来。

例如, CREATE (shakespeare:Author {firstname:'William', lastname:'Shakespeare'})  创建一个Author节点,表示威廉·莎士比亚。

新创建的节点被分配给标识符shakespeare。该shakespeare标识符稍后在代码中用于将关系附加到基础节点。

例如,(shakespeare)-[:WROTE_PLAY {year:1599}]->(juliusCaesar) 创建了从莎士比亚到剧本Julius Caesar的WROTE关系。

该关系的 year属性值为1599。

标识符在当前查询范围内一直可用,但不再可用。如果我们希望为节点提供长久的名称,我们只需为特定标签和键属性组合创建索引。 我们在“索引和约束”中讨论索引。

与关系模型不同,这些命令不会在图表中引入任何意外的复杂性。 信息元模型(即通过标签和关系建立节点的结构)与业务数据保持分离,业务数据仅作为属性存在。 我们再也不必担心外键和基数约束会污染我们的真实数据,因为在图模型中,这两者都是节点形式以及将它们互连的语义丰富的关系,是显式的。

我们可以在以后的某个时间以两种不同的方式修改图形。 当然,我们可以继续使用CREATE语句简单地添加到图形中。 但是我们也可以使用MERGE,它的语义是确保一旦执行命令,节点和关系的特定子图结构(其中一些可能已经存在,其中一些可能会丢失)已经存在。 在实践中,当我们添加到图表中时,我们倾向于使用CREATE,并且不介意重复;而在域中不允许重复时,我们倾向于使用MERGE。

5.2.开始查询

现在我们有了一个图,我们可以开始查询它了。 在Cypher中,我们总是从图中一个或多个众所周知的起点(称为绑定节点)开始查询。 Cypher使用MATCH和WHERE子句中提供的任何标签和属性谓词,以及索引和约束提供的元数据,来找到锚定我们的图形模式的起点。

例如,如果我们想了解有关皇家剧院表演的更多信息,我们将从皇家剧院节点开始查询,我们可以通过指定其Venue标签和name属性来查找。 但是,如果我们对某个人的评论更感兴趣,则可以使用该人的节点作为查询的起点,并在“User”标签和名称属性组合上进行匹配。

假设我们想了解在纽卡斯尔皇家剧院发生的所有莎士比亚活动。 这三件事—名为莎士比亚的作者,名为皇家剧院的场所和名为纽卡斯尔的城市—为我们的新查询提供了起点:

MATCH (theater:Venue {name:'Theatre Royal'}),
(newcastle:City {name:'Newcastle'}),
(bard:Author {lastname:'Shakespeare'})

该MATCH子句使用属性key名称和属性值Theatre Royal标识所有Venue节点,并将它们绑定到标识符Theater。(如果该图中有许多Theatre Royal节点,该怎么办?我们将尽快处理。)下一步,我们找到代表纽卡斯尔市的节点; 我们将此节点绑定到标识符newcastle。 最后,与先前的莎士比亚查询一样,要查找莎士比亚节点本身,我们将查找标签为Author且姓氏属性为Shakespeare的节点。 我们将此查询的结果绑定到bard。

从现在开始,在我们的查询中,无论我们在模式中使用标识符Theater,Newcastle和Bard的哪个位置,该模式都将锚定到与这三个标识符关联的实际节点上。 实际上,此信息将查询绑定到图形的特定部分,从而为我们提供了起点,以匹配紧邻的节点和关系中的模式。

索引与约束

索引有助于优化查找特定节点的过程。

在大多数情况下,查询图表时,我们很乐意让遍历过程发现满足我们信息目标的节点和关系。 通过遵循与特定图形模式匹配的关系,我们会遇到有助于查询结果的元素。 但是,在某些情况下,我们需要选择特定的节点,而不是遍历整个过程发现它们。 例如,确定遍历的起始节点需要我们根据标签和属性值的某种组合来找到一个或多个特定节点。

为了支持有效的节点查找,Cypher允许我们为每个标签和属性组合创建索引。 对于唯一属性值,我们还可以指定确保唯一性的约束。 在我们需要直接查找场所的莎士比亚图表中,我们可以选择基于标记为Venue的所有节点的名称属性值来为其编制索引。 为此的命令是:

CREATE INDEX ON :Venue(name)

为了确保所有国家/地区名称都是唯一的,我们可以添加唯一性约束:

CREATE CONSTRAINT ON (c:Country) ASSERT c.name IS UNIQUE

在现有数据库上,索引将在后台填充,并且在建立索引后即可使用。

查找不需要索引,但是可以通过添加索引来提高其性能。  MATCH (theater:Venue {name:'Theatre Royal'})无论有没有索引都可以使用。但是在拥有数千个场所的大型数据集中,索引将有助于提高性能。 如果没有索引,则选择Theatre Royal作为查询的起点,将使Neo4j扫描并过滤所有标记为Venue的节点。

5.3.声明要查找的信息模式

Cypher中的MATCH子句是神奇的地方。 由于CREATE子句试图使用ASCII艺术来传达意图以描述图的所需状态,因此MATCH子句使用相同的语法来描述要在数据库中发现的模式。 我们已经研究了一个非常简单的MATCH子句; 现在我们来看一个更复杂的模式,它可以找到纽卡斯尔皇家剧院所有莎士比亚的表演:

MATCH (theater:Venue {name:'Theatre Royal'}),
      (newcastle:City {name:'Newcastle'}),
      (bard:Author {lastname:'Shakespeare'}),
      (newcastle)<-[:STREET|CITY*1..2]-(theater)
        <-[:VENUE]-()-[:PERFORMANCE_OF]->()
        -[:PRODUCTION_OF]->(play)<-[:WROTE_PLAY]-(bard)
RETURN DISTINCT play.title AS play

这种MATCH模式使用了一些我们尚未遇到的语法元素。 除了我们前面讨论的锚定节点之外,它还使用模式节点,任意深度路径和匿名节点。 让我们依次看一下其中的每个:

  • 根据指定的标签和属性值,将标识符newcastle,Theater和bard锚定到图中的实际节点。
  • 如果我们的数据库中有几个皇家剧院(Theatre Royal)(例如,英国的普利茅斯,巴斯,温彻斯特和诺里奇等城市都拥有皇家剧院),那么剧院将绑定到所有这些节点。 为了将我们的模式限制为纽卡斯尔的皇家剧院(Theatre Royal),我们使用语法 <-[:STREET|CITY*1..2]-,这意味着剧院节点最多只能有两个传出的STREET and/or CITY关系 从代表泰恩河畔纽卡斯尔市(Newcastle-upon-Tyne)的节点出发。 通过提供可变的深度路径,我们允许使用相对较细粒度的地址层次结构(例如,包括街道,地区或自治市镇和城市)。
  • 语法  (theater)<-[:VENUE]-() 使用匿名节点,因此括号为空。 在了解数据的同时,我们希望匿名节点与性能匹配,但是由于我们对查询或结果中其他位置的各个性能的详细信息不感兴趣,因此我们不会命名该节点或将其绑定到 标识符。
  • 我们再次使用匿名节点将性能链接到生产 ( ()-[:PERFORMANCE_OF]->() ) 。 如果我们有兴趣返回表演和作品的详细信息,则可以将这些出现的匿名节点替换为标识符:(performance)-[:PERFORMANCE_OF]->(production)
  • MATCH的其余部分是简单的 (play)<-[:WROTE_PLAY]-(bard)  节点到关系到节点模式匹配。 这种模式确保我们只返回莎士比亚写的戏剧。 由于(play)已加入到匿名制作节点,并且通过该节点又加入了表演节点,因此我们可以安全地推断它已经在纽卡斯尔的皇家剧院进行了表演。 在命名播放节点时,我们将其带入范围,以便稍后在查询中使用它。

运行此查询将返回在纽卡斯尔皇家剧院进行的所有莎士比亚戏剧:

+-----------------+
| play |
+-----------------+
| "Julius Caesar" |
| "The Tempest" |
+-----------------+
2 rows

如果我们对莎士比亚在皇家剧院的整个历史感兴趣,那很好,但是如果我们仅对特定的戏剧,作品或表演感兴趣,我们就需要以某种方式来限制结果。

5.4.约束匹配

我们使用WHERE子句约束图匹配。 通过指定以下一项或多项,WHERE允许我们从结果中消除匹配的子图:

  • 在匹配的子图中必须存在(或不存在)某些路径。
  • 该节点必须具有某些标签或与某些名称的关系。
  • 无论值如何,匹配节点和关系上的特定属性都必须存在(或不存在)。
  • 匹配的节点和关系上的某些属性必须具有特定的值。
  • 必须满足其他谓词的要求(例如,表演必须在特定日期或之前进行)。

与描述结构关系并为模式的各个部分分配标识符的MATCH子句相比,WHERE约束了当前的模式匹配。 例如,让我们想象一下,我们希望将结果的播放范围限制在莎士比亚最后一个时期(通常被认为始于1608年)。我们通过过滤匹配的WROTE_PLAY关系的year属性来做到这一点。 为了启用此过滤,我们调整了MATCH子句,将WROTE_PLAY关系绑定到一个标识符,我们将其称为w(关系标识符在冒号之前加一个关系名称)。 然后,我们添加WHERE子句,以对该关系的year属性进行过滤:

MATCH (theater:Venue {name:'Theatre Royal'}),
      (newcastle:City {name:'Newcastle'}),
      (bard:Author {lastname:'Shakespeare'}),
      (newcastle)<-[:STREET|CITY*1..2]-(theater)
        <-[:VENUE]-()-[:PERFORMANCE_OF]->()
        -[:PRODUCTION_OF]->(play)<-[w:WROTE_PLAY]-(bard)
WHERE w.year > 1608
RETURN DISTINCT play.title AS play

添加此WHERE子句意味着,对于每个成功的匹配,数据库都会检查莎士比亚节点和匹配的剧本之间的WROTE_PLAY关系是否具有Year属性,其年份值大于1608。与WROTE_PLAY关系的匹配如果年份值大于1608,则将 通过测试; 这些戏剧将被包括在结果中。 未通过测试的比赛将不包括在结果中。 通过添加此子句,我们确保仅返回莎士比亚后期的演出:

+---------------+
| play |
+---------------+
| "The Tempest" |
+---------------+
1 row

5.5.处理结果

借助Cypher的RETURN子句,我们可以对匹配的图形数据执行一些处理,然后再将其返回给执行查询的用户(或应用程序)。

正如我们在之前的查询中所看到的,我们最简单的方法就是返回找到的剧本:

RETURN DISTINCT play.title AS play

DISTINCT确保我们返回独特的结果。 由于每个剧本可以在同一个剧院中多次执行,有时甚至在不同的作品中进行,因此我们可以得到重复的剧本标题。 DISTINCT过滤掉这些。

我们可以通过几种方式来丰富此结果,包括聚合,排序,过滤和限制返回的数据。 例如,如果我们只对符合条件的打法数量感兴趣,则可以应用count函数:

RETURN count(play)

如果要按演出次数对戏剧进行排名,首先需要将MATCH子句中的PERFORMANCE_OF关系绑定到一个名为p的标识符,然后我们可以对其进行计数和排序:

MATCH (theater:Venue {name:'Theatre Royal'}),
      (newcastle:City {name:'Newcastle'}),
      (bard:Author {lastname:'Shakespeare'}),
      (newcastle)<-[:STREET|CITY*1..2]-(theater)
        <-[:VENUE]-()-[p:PERFORMANCE_OF]->()
        -[:PRODUCTION_OF]->(play)<-[:WROTE_PLAY]-(bard)
RETURN play.title AS play, count(p) AS performance_count
ORDER BY performance_count DESC

这里的RETURN子句使用标识符p(绑定到MATCH子句中的PERFORMANCE_OF关系)对PERFORMANCE_OF关系的数量进行计数,并将结果别名为performance_count。 然后,它根据performance_count排序结果,并首先列出执行最频繁的播放:

+-------------------------------------+
| play | performance_count |
+-------------------------------------+
| "Julius Caesar" | 2 |
| "The Tempest" | 1 |
+-------------------------------------+
2 rows

5.6.查询链接

在结束对Cypher的简要介绍之前,需要了解另外一个有用的功能-WITH子句。 有时候,一次MATCH做所有您想做的事是不切实际(或不可能)的。 WITH子句允许我们将多个匹配项链接在一起,并将上一个查询部分的结果传递到下一个查询中。在以下示例中,我们找到了莎士比亚写的剧本,并根据写剧的年份对它们进行排序, 最新的优先。 然后使用WITH,将结果通过管道传递到RETURN子句,该子句使用collect函数生成有限的播放标题列表:

MATCH (bard:Author {lastname:'Shakespeare'})-[w:WROTE_PLAY]->(play)
WITH play
ORDER BY w.year DESC
RETURN collect(play.title) AS plays

对我们的样本图执行此查询将产生以下结果:

+---------------------------------+
| plays |
+---------------------------------+
| ["The Tempest","Julius Caesar"] |
+---------------------------------+
1 row

WITH可以用于将只读子句与以写为中心的SET操作分开。更普遍的是,WITH通过允许我们将单个复杂查询分解为几个更简单的模式来帮助划分和解决复杂查询。

5.7.常见的建模陷阱

尽管图建模是掌握问题域中复杂性的一种非常有表现力的方式,但是仅凭表现力并不能保证特定的图适合目的。 实际上,在有些情况下,甚至我们每天使用图形的人都会犯错。 在本部分中,我们将研究出现问题的模型。 这样,我们将学习如何在建模工作中及早发现问题,以及如何解决这些问题。

5.7.1.电子邮件出处问题域

本示例涉及电子邮件通信的分析。 沟通模式分析是一个经典的图形问题,涉及到查询图形以发现主题专家,关键影响者以及传播信息的通信渠道。 但是,在这种情况下,我们不是在寻找积极的榜样(以专家的形式),而是在寻找流氓:也就是说,可疑的电子邮件通信模式会违反公司治理,甚至会触犯法律。

5.7.2.明智的第一次迭代?

在分析领域时,我们了解了潜在的不法行为者用来掩盖其足迹的所有聪明模式:使用盲目复制(BCC),使用别名-甚至与这些别名进行对话以模仿实际业务涉众之间的合法交互。 基于此分析,我们生成了一个具有代表性的图形模型,该模型似乎捕获了所有相关实体及其活动。

为了说明这个早期模型,我们将使用Cypher的CREATE子句生成一些表示用户和别名的节点。 我们还将生成一个关系,该关系表明Alice是Bob的已知别名之一。 (我们假设底层图形数据库正在为这些节点建立索引,以便我们以后可以查找它们并将它们用作查询的起点。)以下是Cypher查询,用于创建我们的第一个图形:

CREATE (alice:User {username:'Alice'}),
       (bob:User {username:'Bob'}),
       (charlie:User {username:'Charlie'}),
       (davina:User {username:'Davina'}),
       (edward:User {username:'Edward'}),
       (alice)-[:ALIAS_OF]->(bob)

生成的图形模型使我们很容易观察到Alice是Bob的别名,如图3-7所示。

现在,我们通过用户交换的电子邮件将他们聚集在一起:

MATCH (bob:User {username:'Bob'}),
      (charlie:User {username:'Charlie'}),
      (davina:User {username:'Davina'}),
      (edward:User {username:'Edward'})
CREATE (bob)-[:EMAILED]->(charlie),
       (bob)-[:CC]->(davina),
       (bob)-[:BCC]->(edward)

乍一看,这似乎是对该域的合理忠实表示。 每个子句都易于从左到右阅读,从而通过了我们的一项非正式测试,以确保准确性。 例如,我们从图表中可以看到“Bob通过电子邮件发送给Charlie”。只有在有必要确切确定潜在的不法行为Bob(以及他的另一个自我是Alice)交换的内容时,该模型的局限性才会出现。 我们可以看到Bob CC’d或 BCC’d有一些人,但我们看不到最重要的东西:电子邮件本身。

第一次建模尝试产生了一个以Bob为中心的星形图形。 他的电子邮件,复制和盲目复制行为由从Bob延伸到代表他的邮件收件人的节点的关系表示。 但是,如图3-8所示,数据中最关键的元素,即实际的电子邮件,丢失了。

这种图结构是有损的,当我们提出以下内容时,这一事实变得明显查询:

MATCH (bob:User {username:'Bob'})-[e:EMAILED]->
      (charlie:User {username:'Charlie'})
RETURN e

此查询返回Bob和Charlie之间的EMAILED关系(Bob发送给Charlie的每封电子邮件可能都有一个)。 这告诉我们电子邮件已经交换过,但是却没有告诉我们有关电子邮件本身的信息:

+----------------+
| e |
+----------------+
| :EMAILED[1] {} |
+----------------+
1 row

我们可能认为可以通过在EMAILED关系中添加属性以表示电子邮件的属性来纠正这种情况,但这只是时间的作用。 即使将属性附加到每个EMAILED关系中,我们仍将无法在EMAILED,CC和BCC关系之间建立关联。 也就是说,我们将无法说出复制了哪些电子邮件,复制了哪些电子邮件以及复制给了谁。

事实是,我们不经意间犯了一个简单的建模错误,这主要是由于英语使用不多而不是图论的任何缺点造成的。 我们日常使用的语言使我们专注于动词“电子邮件”而不是电子邮件本身,因此,我们制作的模型缺乏真正的领域洞察力。

用英语,将短语“Bob sent an email to Charlie”缩写为“Bob emailed Charlie.”很容易和方便。在大多数情况下,名词(实际电子邮件)的丢失并不重要,因为意图仍然很清楚 。 但是,在我们的法证场景中,这些被遗忘的陈述是有问题的。 意图保持不变,但是Bob所发送的电子邮件的数量,内容和收件人的详细信息由于被折叠成EMAILED关系而丢失了,而不是被明确地建模为自身的节点。

5.7.3.第二次的魅力

要修复我们的有损模型,我们需要插入电子邮件节点以表示业务中交换的真实电子邮件,并扩展我们的关系名称集以涵盖电子邮件支持的整个寻址字段。 现在,而不是创建像这样的有损结构:

CREATE (bob)-[:EMAILED]->(charlie)

我们将改为创建更详细的结构,如下所示:

CREATE (email_1:Email {id:'1', content:'Hi Charlie, ... Kind regards, Bob'}),
       (bob)-[:SENT]->(email_1),
       (email_1)-[:TO]->(charlie),
       (email_1)-[:CC]->(davina),
       (email_1)-[:CC]->(alice),
       (email_1)-[:BCC]->(edward)

这导致了另一个星形图形结构,但是这次电子邮件处于中心位置,如图3-9所示。

当然,在真实的系统中,会有更多这样的电子邮件,每个电子邮件都有自己复杂的交互网络供我们探索。 很容易想象,随着时间的推移,随着电子邮件服务器记录交互,还会执行更多的CREATE语句,就像这样(为简洁起见,我们省略了锚节点):

CREATE (email_1:Email {id:'1', content:'email contents'}),
       (bob)-[:SENT]->(email_1),
       (email_1)-[:TO]->(charlie),
       (email_1)-[:CC]->(davina),
       (email_1)-[:CC]->(alice),
       (email_1)-[:BCC]->(edward);
CREATE (email_2:Email {id:'2', content:'email contents'}),
       (bob)-[:SENT]->(email_2),
       (email_2)-[:TO]->(davina),
       (email_2)-[:BCC]->(edward);
CREATE (email_3:Email {id:'3', content:'email contents'}),
       (davina)-[:SENT]->(email_3),
       (email_3)-[:TO]->(bob),
       (email_3)-[:CC]->(edward);
CREATE (email_4:Email {id:'4', content:'email contents'}),
       (charlie)-[:SENT]->(email_4),
       (email_4)-[:TO]->(bob),
       (email_4)-[:TO]->(davina),
       (email_4)-[:TO]->(edward);
CREATE (email_5:Email {id:'5', content:'email contents'}),
       (davina)-[:SENT]->(email_5),
       (email_5)-[:TO]->(alice),
       (email_5)-[:BCC]->(bob),
       (email_5)-[:BCC]->(edward);

这将导致我们在图3-10中看到更复杂,更有趣的图形。

现在,我们可以查询此图以识别潜在的可疑行为:

MATCH (bob:User {username:'Bob'})-[:SENT]->(email)-[:CC]->(alias),
      (alias)-[:ALIAS_OF]->(bob)
RETURN email.id

在这里,我们检索Bob抄送给他自己的别名之一的地方发送的所有电子邮件。 与此模式匹配的所有电子邮件都表明流氓行为。 而且由于Cypher和基础图形数据库都具有图形相似性,因此这些查询(即使是在大型数据集上)也可以非常快速地运行。 该查询返回以下结果:

+------------------------------------------+
| email |
+------------------------------------------+
| Node[6]{id:"1",content:"email contents"} |
+------------------------------------------+
1 row

5.7.4.不断发展的领域

与任何数据库一样,我们的图形服务于一个可能随着时间而发展的系统。 那么当图演化时我们该怎么办? 我们怎么知道什么坏了,或者实际上,我们怎么知道某件事已经坏了? 事实是,我们无法完全避免在图形数据库中进行迁移:就像任何数据存储一样,它们是生活中不可或缺的事实。 但是在图形数据库中,它们通常要简单得多。

在图中,要添加新的事实或构图,我们倾向于添加新的节点和关系,而不是更改模型。 使用新的关系添加到图形不会影响任何现有查询,并且是完全安全的。 使用现有的关系类型来更改图形,以及更改现有节点的属性(不仅是属性值)可能是安全的,但是我们需要运行一组代表性的查询,以确保在结构化之后图形仍然适合于目的 变化。但是,这些活动与我们在常规数据库操作期间执行的动作完全相同,因此在图形世界中,迁移实际上只是照常进行。

至此,我们有了一个图表,描述了谁发送和接收了电子邮件,以及电子邮件本身的内容。 但是,当然,电子邮件的乐趣之一是收件人可以转发或回复他们收到的电子邮件。 这样可以增加互动和知识共享,但是在某些情况下会泄漏重要的业务信息。由于我们正在寻找可疑的通信模式,因此我们也应该考虑转发和答复。

乍一看,似乎不需要使用数据库迁移来更新我们的图以支持我们的新用例。 我们可以做的最简单的添加就是将FORWARDED和EPLIED_TO关系添加到图中,如图3-11所示。 这样做不会影响任何先前存在的查询,因为它们没有经过编码以识别新的关系。

但是,这种方法很快被证明是不合适的。 与我们最初使用EMAILED关系的方式几乎相同,添加FORWARDED或REPLIED关系是幼稚和有损的。 为了说明这一点,请考虑以下CREATE语句:

...
MATCH (email:Email {id:'1234'})
CREATE (alice)-[:REPLIED_TO]->(email)
CREATE (davina)-[:FORWARDED]->(email)-[:TO]->(charlie)

在第一个CREATE语句中,我们试图记录Alice回复特定电子邮件的事实。 从左至右阅读该陈述是合乎逻辑的,但这种情绪是有损的-我们无法确定爱丽丝是回信给所有电子邮件收件人还是直接回信给作者。 我们所知道的是,已经发送了一些答复。 第二条语句从左到右也很读:达维娜将电子邮件转发给了查理。 但是我们已经使用TO关系来指示给定的电子邮件具有一个TO标头,用于标识主要收件人。 在这里重复使用TO使得无法分辨谁是收件人以及谁收到了电子邮件的转发版本。

要解决此问题,我们必须考虑领域的基础。 回复电子邮件本身就是新的电子邮件,但它也是回复。 换句话说,回复具有两个角色,在图中可以通过将两个标签Email和Reply附加到回复节点来表示。 无论回复是发给原始发件人,所有收件人还是子集,都可以使用相同的熟悉的TO,CC和BCC关系轻松建模,而原始电子邮件本身可以通过REPLY_TO关系进行引用。 这是一系列修改后的一系列写法,它们是由多个电子邮件操作产生的(再次,我们省略了必要的节点锚定):

CREATE (email_6:Email {id:'6', content:'email'}),
       (bob)-[:SENT]->(email_6),
       (email_6)-[:TO]->(charlie),
       (email_6)-[:TO]->(davina);
CREATE (reply_1:Email:Reply {id:'7', content:'response'}),
       (reply_1)-[:REPLY_TO]->(email_6),
       (davina)-[:SENT]->(reply_1),
       (reply_1)-[:TO]->(bob),
       (reply_1)-[:TO]->(charlie);
CREATE (reply_2:Email:Reply {id:'8', content:'response'}),
       (reply_2)-[:REPLY_TO]->(email_6),
       (bob)-[:SENT]->(reply_2),
       (reply_2)-[:TO]->(davina),
       (reply_2)-[:TO]->(charlie),
       (reply_2)-[:CC]->(alice);
CREATE (reply_3:Email:Reply {id:'9', content:'response'}),
       (reply_3)-[:REPLY_TO]->(reply_1),
       (charlie)-[:SENT]->(reply_3),
       (reply_3)-[:TO]->(bob),
       (reply_3)-[:TO]->(davina);
CREATE (reply_4:Email:Reply {id:'10', content:'response'}),
       (reply_4)-[:REPLY_TO]->(reply_3),
       (bob)-[:SENT]->(reply_4),
       (reply_4)-[:TO]->(charlie),
       (reply_4)-[:TO]->(davina);

这将在图3-12中创建该图,该图显示了众多答复和逐项答复。

现在,很容易看到谁回复了鲍勃的原始电子邮件。 首先,找到感兴趣的电子邮件,然后与所有传入的REPLY_TO关系(可能有多个答复)进行匹配,然后从此处与传入的SENT关系进行匹配:这揭示了发件人。 在Cypher中,这很容易表达。 实际上,Cypher使得查找回复到回复的过程变得很容易,依此类推可以任意深度(尽管在这里我们将深度限制为四级):

MATCH p=(email:Email {id:'6'})<-[:REPLY_TO*1..4]-(:Reply)<-[:SENT]-(replier)
RETURN replier.username AS replier, length(p) - 1 AS depth
ORDER BY depth

在这里,我们捕获每个匹配的路径,并将其绑定到标识符p。 然后,在RETURN子句中,计算答复链的长度(SENT关系减去1),并返回答复者的姓名和答复者的深度。此查询返回以下结果:

+-------------------+
| replier | depth |
+-------------------+
| "Davina" | 1 |
| "Bob" | 1 |
| "Charlie" | 2 |
| "Bob" | 3 |
+-------------------+
4 rows

我们看到Davina和Bob都直接回复了Bob的原始电子邮件; Charlie回答了其中一项答复; 然后Bob回复了其中一封回复。

转发电子邮件的方式与此类似,可以将其视为新电子邮件,恰好包含原始电子邮件的某些文本。 与回复情况一样,我们明确地对新电子邮件建模。 我们还会引用转发邮件中的原始电子邮件,以便始终提供详细而准确的出处数据。 如果转发的邮件本身是转发的,则同样适用,依此类推。 例如,如果爱丽丝(Alice)(鲍勃(Bob)的另一位自我)通过电子邮件向鲍勃(Bob)尝试建立单独的具体身份,然后Bob(希望进行一些变通)将其转发给查理(Charlie),然后查理(Charlie)将其转发给达维纳(Davina),我们实际上有三封电子邮件 考虑。 假设用户(及其别名)已经存在于数据库中,在Cypher中,我们将审核信息写入数据库,如下所示:

CREATE (email_11:Email {id:'11', content:'email'}),
       (alice)-[:SENT]->(email_11)-[:TO]->(bob);
CREATE (email_12:Email:Forward {id:'12', content:'email'}),
       (email_12)-[:FORWARD_OF]->(email_11),
       (bob)-[:SENT]->(email_12)-[:TO]->(charlie);
CREATE (email_13:Email:Forward {id:'13', content:'email'}),
       (email_13)-[:FORWARD_OF]->(email_12),
       (charlie)-[:SENT]->(email_13)-[:TO]->(davina);

完成这些写操作后,我们的数据库将包含图3-13所示的子图。

使用此图,我们可以确定转发的电子邮件链的各种路径。

MATCH (email:Email {id:'11'})<-[f:FORWARD_OF*]-(:Forward)
RETURN count(f)

此查询从给定的电子邮件开始,然后与转发的电子邮件树中所有传入的FOR WARD_OF关系进行任何深度的匹配。 这些关系绑定到标识符f。 为了计算电子邮件被转发的次数,我们使用Cypher的count函数计算与f绑定的FORWARD_OF关系的数量。 在此示例中,我们看到原始电子邮件已转发两次:

+----------+
| count(f) |
+----------+
| 2 |
+----------+
1 row

6.识别节点和关系

建模过程可以最好地概括为尝试创建一个图形结构,以表达我们要针对我们的领域提出的问题。 也就是说,设计可查询性:

  1. 描述激发我们模型的客户或最终用户目标。
  2. 将这些目标重写为我们的领域要问的问题。
  3. 确定出现在这些问题中的实体和关系。
  4. 将这些实体和关系转换为Cypher路径表达式。
  5. 使用类似于我们用来建模领域的路径表达式,以图形模式表达我们想以域模式问我们的领域的问题。

通过检查用于描述领域的语言,我们可以非常快速地确定图中的核心元素:

  • 普通名词成为标签:例如“ user”和“ email”成为标签User和Email。
  • 带对象的动词成为关系名称:例如,“sent”和“wrote”成为SENT和WROTE。
  • 专有名词(例如,人或公司的名称)是指事物的一个实例,我们使用一个或多个属性来捕获该事物的属性,将其建模为节点。

7.避免反模式

通常,不要将实体编码为关系。 使用关系来传达有关实体之间如何关联以及这些关系的质量的语义。

通常,不要将实体编码为关系。 使用关系来传达有关实体之间如何关联以及这些关系的质量的语义。域实体在语音中并不总是立即可见,因此我们必须仔细考虑我们实际处理的名词。 Verbing是名词将名词转换为动词的语言习惯,通常可以隐藏名词和相应域实体的存在。 技术和商业术语在此类新词中尤为流行:如我们所见,我们互相“email”,而不是发送电子邮件,“google”获取结果,而不是搜索Google。

同样重要的是要认识到图是自然可加的结构。 很自然地添加有关域实体及其使用新节点和新关系的相互关系方面的事实,即使感觉好像我们正在向数据库中充斥大量数据。 通常,在写入时尝试合并数据元素以保持查询时的效率是一种不好的做法。 如果我们按照我们要对数据提出的问题进行建模,则将出现域的准确表示。 有了这个数据模型,我们可以相信图形数据库在读取时表现良好。

图形数据库即使存储大量数据也能保持快速查询时间。 当学习构造我们的图而不对它们进行规范化时,学会信任我们的图数据库非常重要。

8.摘要

图形数据库使软件专业人员可以使用图形表示问题域,然后在运行时持久保存并查询该图形。 我们可以使用图来清楚地描述问题域; 图数据库然后允许我们以在域和数据之间保持高亲和力的方式存储此表示。 此外,图形建模消除了使用复杂数据管理代码对数据进行规范化和非规范化的需求。

但是,我们许多人对于使用图进行建模将是新手。 我们创建的图形应易于查询,同时避免混淆实体和操作-可能会丢失有用的领域知识的不良做法。 尽管图形建模没有绝对的对与错,但本章中的指南将帮助您创建可以在许多迭代中满足系统需求的图形数据,同时始终与代码演进保持同步。

了解了图形数据建模之后,您现在可以考虑进行图形数据库项目。 在下一章中,我们将研究规划和交付图形数据库解决方案所涉及的内容。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章