像许多软件工程师一样,我喜欢玩一场精彩的龙与地下城(Dungeons & Dragons)游戏。我喜欢提升我的角色能力,并面对越来越强大的敌人。要做到这一点,需要金币和经验。而获取这些东西的最佳途径就是传统的地下城探索。
如果你需要关于 D&D 的背景信息,这个视频或许有帮助。简短但过于简化的版本是:在 D&D 中,你探索地下建筑群,与其中的怪物战斗,并拿走它们的金币。
不幸的是,地下城并非为了方便我们这些冒险者而设计。有时怪物很有挑战性,但没有宝藏。有时宝藏就在那里摆着。有时房间里什么都没有。那么我们如何找出地下城中哪里能获得我们想要的宝藏呢?
解决这个问题的一种有趣方法是使用图数据库(如RedisGraph)将地下城表示为图。从数据的角度来看,地下城是实体(房间、怪物和宝藏)及其关系的集合。图非常擅长建模这类数据。通过图数据库,我们可以查询地下城的图,找到那些可怕的生物和闪闪发光的宝物,以便升级我们的角色。
也许你不熟悉图和图数据库?那么,和我一起踏上冒险之旅,探索图数据库是什么、为什么以及如何使用。
图数据库实际上相当容易理解。我认为它们比关系型数据库更容易理解。但如果你在关系型数据库领域花费了大量时间(我们很多人都是如此,包括我自己),你可能需要忘掉一两件事。只需将所有关系型知识都放在你头脑的另一个部分,腾出空间让新的想法涌入。
搞定了吗?好的。让我们深入了解图数据库!
一个图数据库包含一个图(graph),由节点(nodes)和边(edges)组成。对图、节点和边的解释可能会有点抽象,因为从根本上说,它们都是相当抽象的概念。因此,我将使用例子来使它们更具体一些。
节点是数据的名词或事物。它们有一个标签(label),告诉你它们是什么类型的事物。它们也可以有属性(attributes),提供关于节点的额外信息。让我们看几个带标签和属性的节点:
下面有两个节点。第一个节点的标签是“room”,并有一个属性告诉我们关于这个房间的一些信息。在这个例子中,它的名字是:“食人魔王巢穴”。第二个节点的标签是“monster”,有两个属性,一个告诉我们怪物的名字是“食人魔王拉尔夫”,另一个是杀死他值得 1200 点经验值。这相当直接。节点有点像 Java 或 C# 等编程语言中的对象。它们有一个类型和属性。
现在,让我们添加一条边(edge),看看会发生什么:这条边的类型(type)是“contains”,方向(direction)从房间指向怪物。它的目的是建立房间和怪物之间的关系(relationship)。类型是这种关系的性质,在很多方面类似于节点的标签。我喜欢认为边是动词——特别是及物动词——因为它们连接了名词:这个房间包含一个怪物。这在节点之间增加了关系。
边的方向是任意的。无论哪种方式,它都建立了关系。我完全可以创建一个类型为“is_contained_by”,指向相反方向的边。但那样我的句子就成了:怪物被房间包含。这是被动语态。正如多年前我的英语老师所教导的,被动语态应避免,因为它更冗长且更难理解。
总的来说,这些节点和边被称为一个图(graph)。最简单(也可能最不有趣)的图没有任何节点。当然,没有节点就没有边。
另一方面,图可以变得非常复杂。节点可以有多条进入和离开的边。一对节点之间甚至可以有多条边。节点也可以是孤立的,没有任何边!看那个怪兽般的图!它展示了三个房间,一扇秘密的门,以及一堆宝藏。还有一个名叫拉尔夫的守卫。
我一直在忙着为我那个荒谬的地下城示例建模,包括所有相连的房间、秘密、怪物和宝藏。如果你正在构建一个基于文本的在线游戏,比如MUD,这将是建模其状态的好方法。
但大多数开发者并不是在构建 80 年代的基于文本的游戏。我们大多数人正在构建更实用的东西。图数据库可以解决哪些实际问题?各种各样的。这里有一些例子:
这些都是图数据库的良好候选应用,因为它们具有复杂的关系,而讽刺的是,这些关系用关系型数据库难以建模。这是图数据库的主要优势之一:它们能很好地建模关系。
但图数据库还有另一个重要优势:它们没有模式(schema)。这使得它们一旦投入生产后更容易使用。例如:
你能想象使用关系型数据库将一对多关系转换为多对多关系吗?使用图数据库,你无需关心一对多和多对多。事物只是相互关联。如果需要关联,就添加一条边。仅此而已。
是的。是的,可以。你可能熟悉Redis 模块(modules)。模块是你可以安装的扩展,用于扩展 Redis 的功能——通常通过添加新命令和数据结构,但有时更多。Redis 创建了几个模块,它们增加了各种功能。其中一个,RedisGraph,提供了一个作为图数据库的数据结构。
如果不包含一些代码,写一篇博客感觉不对,所以我要展示一些使用Cypher与图交互的例子,Cypher 是 RedisGraph 使用的查询语言。我将使用RedisInsight来完成此操作,因为它有一个很酷的可视化工具值得一看,但你也可以使用 redis-cli。
注意:如果你使用 redis-cli,请务必在你所有的 Cypher 查询前加上GRAPH.QUERY key “your cypher query here”。
> CREATE (:monster { name: 'Ralph the Ogre King', xp: 1200 })
括号内的内容是要创建的节点,冒号后面的monster是该节点的标签。节点的属性紧随标签之后,格式非常类似于 JavaScript。
现在我们创建了一个怪物,让我们查询它
> MATCH (m:monster) RETURN m
这里的MATCH匹配所有标签为monster的节点,并将它们赋值给m。在本例中,m只包含一个节点。RETURN,不出所料,返回我们指定的内容。
这本可以更简单。由于我们的图中只有一个节点,我们无需检查标签
> MATCH (n) RETURN n
这个查询返回所有节点。
Cypher 很巧妙,因为它的查询语句看起来有点像图。由于节点表示为圆圈,当我们CREATE或MATCH它们时,我们将它们括在括号中以暗示圆圈。这个想法在我们创建带有边的节点时得到了延续。
让我们从头开始,创建一个空图,然后创建一个包含拉尔夫及其巢穴的图
> CREATE (:room { name: 'The Den of the Ogre King' })-[:contains]->(:monster { name: 'Ralph the Ogre King', xp: 1200 })
这里我们正在创建两个节点:拉尔夫和他的巢穴。但我们同时也在它们之间定义了一个关系,一条边,看起来像一个带标签的箭头。箭头指向关系的方向,方括号中包含关系的类型。在本例中,房间包含一个怪物。
我们可以像之前一样查询这个结构,现在也可以包含边
> MATCH (r:room)-[c:contains]->(m:monster) RETURN r, c, m
而且,由于我们的图只包含两个节点和一个关系,我们可以使用更简单的查询
> MATCH (n1)-[e]->(n2) RETURN n1, e, n2
这个查询返回所有节点和所有边。
RedisGraph 还有很多内容,包括更多复杂的查询。你应该深入研究文档,那里有很多有趣的东西。然而,对于我们寻宝者来说,这是一个有用的查询:
> MATCH (r:room)-[:contains]->(:monster)-[:guards]->(:treasure) RETURN r
我们了解了图数据库可以解决的问题类型,也了解了如何创建和查找节点和边。但是它内部是如何工作的呢?这是一个大问题,我不会在这里回答。相反,我建议你查阅文档,在那里你可以了解稀疏邻接矩阵、矩阵乘法和GraphBLAS。
最后,我鼓励你更深入地探索 RedisGraph 这个充满宝藏的领域。安装它。构建一些很酷的东西。与世界分享。如果你构建了一个 MUD,我可以玩吗?