几十年来,关系数据库一直主导着数据管理,但它们最近已经失去了NoSQL的替代品。虽然NoSQL数据存储不适合每一个用例,但它们一般更适合html
本文介绍Neo4j,它是用于与java
顾名思义,图形数据库擅长表示数据图形。这对社交软件特别有用,每次与某人联系时,大家之间就会创建关系。可能在你上次求职时,你选择了一些你感兴趣的公司,而后搜索你的社交网络以获取与他们的联系。虽然你可能不知道有那些人为这些公司工做,但你的社交网络中的某些人可能会这样作。很容易在一个或两个分离度(你的朋友或朋友的朋友)内解决这样的问题,但当你开始在网络中扩展搜索时会发生什么?node
在他们的书中,Neo4j In Action,Aleksa Vukotic和Nicki Watt探讨了关系数据库和图形数据库之间的差别,以解决社交网络问题。为了向你展现为何图形数据库正成为关系数据库日益流行的替代方案,我将在接下来的几个示例中使用它们的工做。docker
从计算机科学的角度来看,当咱们考虑在社交网络中建模用户之间的关系时,咱们可能会绘制如图1所示的图形。数据库
用户与其余用户有IS_FRIEND_OF
关系,这些用户与其余用户也有IS_FRIEND_OF
关系,等等。图2显示了咱们如何在关系数据库中表示这一点。编程
USER
表与USER_FRIEND
表具备一对多的关系,USER_FRIEND
表模拟两个用户之间的“朋友”关系。如今咱们已经创建了关系模型,咱们将如何查询数据?Vukotic和Watt测量了查询性能,用于计算出五个级别深度的不一样朋友的数量(朋友的朋友的朋友的朋友)。在关系数据库中,查询看起来以下:浏览器
# Depth 1
select count(distinct uf.*) from user_friend uf where uf.user_1 = ?
# Depth 2
select count(distinct uf2.*) from user_friend uf1
inner join user_friend uf2 on uf1.user_1 = uf2.user_2
where uf1.user_1 = ?
# Depth 3
select count(distinct uf3.*) from t_user_friend uf1
inner join t_user_friend uf2 on uf1.user_1 = uf2.user_2
inner join t_user_friend uf3 on uf2.user_1 = uf3.user_2
where uf1.user_1 = ?
# And so on...
复制代码
这些查询的有趣之处在于,每次咱们再出一个级别时,咱们都须要本身加入USER_FRIEND
表格。表1显示了研究人员Vukotic和Watt在插入1,000个用户时发现了什么,每一个用户大约有50个关系(50,000个关系)并运行查询。bash
深度执行时间(秒)计数结果网络
2 0.028〜900架构
3 0.213〜999
4 10.273〜999
5 92.613〜999
MySQL能够很好地将数据链接到三个级别,但以后性能会迅速降低。缘由是每次USER_FRIEND
表与自身链接时,MySQL必须计算表的笛卡尔积,即便大部分数据将被丢弃。例如,当执行该链接五次时,笛卡尔积产生50,000 ^ 5行,或102.4 * 10 ^ 21行。当咱们只对其中的1000个感兴趣时,这是一种浪费!
接下来,Vukotic和Watt尝试对Neo4j执行相同类型的查询。这些彻底不一样的结果如表2所示。
深度执行时间(秒)计数结果
2 0.04〜900
3 0.06〜999
4 0.07〜999
5 0.07〜999
从这些执行比较中得出的结论
Neo4j比MySQL更好。相反,当遍历这些类型的关系时,Neo4j的性能取决于检索的记录数,而MySQL的性能取决于USER_FRIEND
表中的记录数。所以,随着关系数量的增长,MySQL查询的响应时间也会增长,而Neo4j查询的响应时间将保持不变。这是由于Neo4j的响应时间取决于特定查询的关系数,而不取决于关系总数。
将这个思想项目进一步扩展,Vukotic和Watt接下来建立了一百万用户,他们之间有5000万个关系。表3显示了该数据集的结果。
深度执行时间(秒)计数结果
2 0.01〜2500
3 0.168〜11万
4 1.359〜60万
5 2.132〜80万
毋庸置疑,我很是感谢Aleksa Vukotic和Nicki Watt,并强烈建议您查看他们的做品。我从本书的第一章
提取了本节中的全部测试。
您已经看到Neo4j可以很是快速地执行大量高度相关的数据,毫无疑问,它比MySQL(或任何关系数据库)更适合某些类型的问题。若是您想了解有关Neo4j如何工做的更多信息,最简单的方法是经过Web控制台与其进行交互。
首先下载Neo4j。对于本文,您将须要Community Edition,在撰写本文时版本为3.2.3。
安装Neo4j后,启动它并打开浏览器窗口到如下URL:
http://127.0.0.1:7474/browser/复制代码
使用默认用户名neo4j
和默认密码登陆neo4j
。您应该看到相似于图3的屏幕。
Neo4j是围绕节点和关系的概念设计的:
举个例子,咱们能够定义像钢铁侠和美国队长这样的角色节点; 定义一个名为“复仇者”的电影节点; 而后定义APPEARS_IN
为钢铁侠和复仇者之间以及美国队长和复仇者之间的关系。全部这些都显示在图4中。
图4显示了三个节点(两个Character节点和一个Movie节点)和两个关系(两种类型APPEARS_IN
)。
与关系数据库如何使用结构化查询语言(SQL)与数据交互相似,Neo4j使用Cypher查询语言与节点和关系进行交互。
让咱们使用Cypher建立一个简单的家庭表示。在Web界面的顶部,查找美圆符号。这表示容许您直接对Neo4j执行Cypher查询的字段。在该字段中输入如下Cypher查询(我以个人家人为例,但若是您愿意,能够随意更改细节以建模您本身的家庭):
CREATE (person:Person {name: "Steven", age: 45}) RETURN person复制代码
结果如图5所示。
在图5中,您能够看到一个标记为Person且名称为Steven的新节点。若是将鼠标悬停在Web控制台中的节点上,您将在底部看到其属性。在这种状况下,属性是ID:19,名称:Steven,年龄:45。如今让咱们分解Cypher查询:
CREATE
关键字用于建立节点和关系。在这种状况下,咱们传递一个参数,它Person
括在括号中,所以它意味着建立一个单独的节点。person
”是一个变量名称,经过它咱们能够访问正在建立的人,而大写“ Person
”是标签。请注意,冒号将变量名称与标签分开。该CREATE
命令(不区分大小写)用于建立节点,能够按以下方式读取:
。
接下来咱们想尝试一下Cypher的查询。首先,咱们须要建立更多人,以便咱们能够定义它们之间的关系。
CREATE (person:Person {name: "Michael", age: 16}) RETURN person
CREATE (person:Person {name: "Rebecca", age: 7}) RETURN person
CREATE (person:Person {name: "Linda"}) RETURN person
复制代码
建立四我的后,您能够单击节点标签下的“ 人员”按钮(若是单击网页左上角的数据库图标,则可见)或执行如下Cypher查询:
MATCH (person: Person) RETURN person复制代码
Cypher使用MATCH
关键字在Neo4j中查找内容。在此示例中,咱们要求Cypher匹配全部标记为Person的节点,将这些节点分配给
变量,并返回与该变量关联的值。所以,你应该看到您建立的四个节点。若是将鼠标悬停在Web控制台中的每一个节点上,你将看到每一个人的属性。(你可能会注意到我将我妻子的年龄排除在她的节点以外,说明属性不须要在节点之间保持一致,即便是相同的标签。我也不会愚蠢地公布我妻子的年龄。)
咱们能够经过MATCH
向咱们想要返回的节点添加条件来进一步扩展此示例。例如,若是咱们只想要“Steven”节点,咱们能够经过匹配name属性来检索它:
MATCH (person: Person {name: "Steven"}) RETURN person复制代码
或者,若是咱们想要归还全部孩子,咱们能够要求全部18岁如下的人:
MATCH (person: Person) WHERE person.age < 18 RETURN person复制代码
在此示例中,咱们WHERE
在查询中添加了子句以缩小结果范围。WHERE
与其SQL等价物很是类似:MATCH (person: Person)
查找具备Person标签的全部节点,而后该WHERE
子句过滤结果集中的值。
咱们有四个节点,因此让咱们建立一些关系。首先,让咱们建立史蒂文和琳达之间的IS_MARRIED_TO
关系:
MATCH (steven:Person {name: "Steven"}), (linda:Person {name: "Linda"}) CREATE (steven)-[:IS_MARRIED_TO]->(linda) return steven, linda复制代码
在这个例子中,咱们匹配标记为Steven和Linda的两个Person节点,而且咱们建立了一个从Steven到Linda 的IS_MARRIED_TO
类型关系。建立关系的格式以下:
(node1)-[relationshipVariable:RELATIONSHIP_TYPE->(node2)复制代码
这relationshipVariable
是可选的,但若是您但愿可以在RETURN语句(或WHERE子句)中访问它,则须要它。箭头()-[]->()
表示Cypher要求的关系方向。若是你想表达Linda与Steven结婚,那么你能够按照如下方式在另外一个方向写下这段关系:()<-[]-()
。若是你想建立一个双向关系,代表Linda和Steve彼此结婚,那么你须要建立两个独立的关系。虽然Cypher要求您定义关系的方向,但您可使用方向查询,也能够不使用方向查询。
如下查询查找此系列中已结婚的全部人(请注意查询中缺乏任何方向):
MATCH (p1:Person)-[:IS_MARRIED_TO]-(p2:Person) RETURN p1, p2复制代码
结果如图6所示。
如今让咱们建立一些关系:
MATCH (michael:Person {name: "Michael"}), (rebecca:Person {name: "Rebecca"}) CREATE (michael)-[:IS_SIBLILNG]->(rebecca) return michael, rebecca
MATCH (steven:Person {name: "Steven"}), (michael:Person {name: "Michael"}) CREATE (steven)-[:HAS_CHILD]->(michael) return steven, michael
MATCH (steven:Person {name: "Steven"}), (rebecca:Person {name: "Rebecca"}) CREATE (steven)-[:HAS_CHILD]->(rebecca) return steven, rebecca
MATCH (linda:Person {name: "Linda"}), (michael:Person {name: "Michael"}) CREATE (linda)-[:HAS_CHILD]->(michael) return linda, michael
MATCH (linda:Person {name: "Linda"}), (rebecca:Person {name: "Rebecca"}) CREATE (linda)-[:HAS_CHILD]->(rebecca) return linda, rebecca
复制代码
咱们如今能够经过如下查询查看全部人及其关系:
MATCH (p:Person) RETURN p复制代码
结果如图7所示。
要真正探索图数据库的力量,咱们须要扩展咱们的社交图。首先,让咱们添加一些FRIEND
关系:
MATCH (michael:Person {name: "Michael"}) CREATE (michael)-[:FRIEND]->(charlie:Person {name: "Charlie", age: 16}) RETURN michael, charlie
MATCH (michael:Person {name: "Michael"}) CREATE (michael)-[:FRIEND]->(koby:Person {name: "Koby"}) RETURN michael, koby
MATCH (michael:Person {name: "Michael"}) CREATE (michael)-[:FRIEND]->(grant:Person {name: "Grant"}) RETURN michael, grant
MATCH (rebecca:Person {name: "Rebecca"}) CREATE (rebecca)-[:FRIEND]->(jordyn:Person {name: "Jordyn"}) RETURN rebecca, jordyn
MATCH (rebecca:Person {name: "Rebecca"}) CREATE (rebecca)-[:FRIEND]->(katie:Person {name: "Katie"}) RETURN rebecca, katie
复制代码
关于这些关系的一些有趣的事情是朋友节点与FRIEND
关系同时建立。例如,执行第一个语句时,“Charlie”Person节点不存在,但该语句建立了从现有“Michael”Person节点到名为“Charlie”的新Person节点的FRIEND
关系。您能够拉出全部Person节点并验证节点是否已建立,如图8所示。
咱们已经启动了一个很是好的社交图,因此让咱们尝试编写一个更复杂的查询来查找我孩子的全部朋友:
MATCH (steven:Person {name:"Steven"})-[:HAS_CHILD]-(:Person)-[:FRIEND]-(friend:Person) RETURN friend复制代码
结果如图9所示。
在此查询中,咱们从名为“Steven”的HAS_CHILD
Person节点开始,遍历全部与Person节点的FRIEND
关系,遍历全部Person节点的关系,并返回朋友列表。咱们能够包含方向关系,但省略箭头可让咱们遍历两个方向。
除了定义两个节点之间的关系以外,关系自己能够具备键/值对。例如,咱们可能决定建立Movie节点,而后HAS_SEEN
在他们看到的人和电影之间建立关系。在这些HAS_SEEN
关系中,咱们还能够添加“评级”属性。下面的代码建立一个标题为Avengers的电影,而后HAS_SEEN
在Michael和电影复仇者之间建立一个关系,评级为5。
CREATE (movie:Movie {title:"Avengers"}) RETURN movie
MATCH (michael:Person {name:"Michael"}), (avengers:Movie {title:"Avengers"}) CREATE (michael)-[:HAS_SEEN {rating:5}]->(avengers) return michael, avengers
复制代码
图10显示告终果。
Java中的图形分析对于咱们在进入Java代码以前的最后一个例子,让咱们尝试使用图形分析进行简单的实验。咱们会给孩子们的朋友添加一些电影,设置我孩子的性别,而后查询个人一个孩子(迈克尔)可能想要看的电影。结果如图11所示。
CREATE (movie:Movie {title:"Batman"}) RETURN movie
CREATE (movie:Movie {title:"Gone with the Wind"}) RETURN movie
CREATE (movie:Movie {title:"Spongebob Square Pants"}) RETURN movie
CREATE (movie:Movie {title:"Avengers 2"}) RETURN movie
MATCH (charlie:Person {name:"Charlie"}), (movie:Movie {title:"Batman"}) CREATE (charlie)-[:HAS_SEEN {rating:4}]->(movie) return charlie, movie
MATCH (charlie:Person {name:"Charlie"}), (movie:Movie {title:"Gone with the Wind"}) CREATE (charlie)-[:HAS_SEEN {rating:0}]->(movie) return charlie, movie
MATCH (koby:Person {name:"Koby"}), (movie:Movie {title:"Batman"}) CREATE (koby)-[:HAS_SEEN {rating:4}]->(movie) return koby, movie
MATCH (koby:Person {name:"Koby"}), (movie:Movie {title:"Avengers 2"}) CREATE (koby)-[:HAS_SEEN {rating:5}]->(movie) return koby, movie
MATCH (grant:Person {name:"Grant"}), (movie:Movie {title:"Spongebob Square Pants"}) CREATE (grant)-[:HAS_SEEN {rating:1}]->(movie) return grant, movie
MATCH (jordyn:Person {name:"Jordyn"}), (movie:Movie {title:"Spongebob Square Pants"}) CREATE (jordyn)-[:HAS_SEEN {rating:5}]->(movie) return jordyn, movie
MATCH (michael:Person {name: "Michael"}) SET michael.gender = "male" RETURN michael
MATCH (rebecca:Person {name: "Rebecca"}) SET rebecca.gender = "female" RETURN rebecca
MATCH (steven:Person {name:"Steven"})-[:HAS_CHILD]-(child:Person)-[:FRIEND]-(friend:Person)-[hasSeen:HAS_SEEN]-(movie:Movie) WHERE child.gender = "male" AND hasSeen.rating > 3 RETURN DISTINCT movie.title
复制代码
上面的前四个陈述创造了四部电影。接下来的六个陈述创造了个人孩子的朋友和他们看过的电影之间的HAS_SEEN
关系,具备不一样的评级。接下来的两个语句为个人孩子添加了一个性别,这是经过按名称查找Person节点而后调用来完成的SET childName.gender = "male|female"
。在Cypher中,该SET
语句容许您经过将值设置为更改现有属性,添加新属性或删除属性NULL
。最后的查询须要一些工做才能理解。咱们从名为“Steven”的Person开始,跟随他与子节点Person关系的HAS_CHILD
关系,跟随那些Person节点到FRIEND
Person节点,经过HAS_SEEN
关系跟随那些朋友Person节点到Movie节点,而后添加一个WHERE
检查二者性别的子句史蒂文的孩子和评级属性的HAS_SEEN
价值。最后,由于有些孩子看过同一部电影(蝙蝠侠),咱们只想要回归DISTINCT
电影片头。在这种状况下,咱们不返回电影节点,而是返回电影的标题属性,这就是输出显示在表格中的缘由。对于聪明的观察者,咱们能够经过将性别添加到子节点查询来简化这一点,以下所示:
MATCH (steven:Person {name:"Steven"})-[:HAS_CHILD]-(child:Person {gender:"male"})-[:FRIEND]-(friend:Person)-[hasSeen:HAS_SEEN]<-(movie:Movie) WHERE hasSeen.rating > 3 RETURN DISTINCT movie.title
复制代码
Cypher是一种考虑编写查询的不一样方式,我鼓励您阅读正式文档以了解更多信息。一旦掌握了编写Cypher查询的过程,Java编程将是最简单的部分!咱们将在本简介的后半部分中对图形数据和与Neo4j的关系进行选择。
英文原文:www.javaworld.com/article/325…
更多文章欢迎访问: http://www.apexyun.com
公众号:银河系1号
联系邮箱:public@space-explore.com
(未经赞成,请勿转载)