5

使用Py2neo查询Neo4j中的节点、关系和路径

 3 years ago
source link: https://segmentfault.com/a/1190000040558191
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

Neo4j是一款开源图数据库,Py2neo提供了使用Python语言访问Neo4j的接口。本文介绍了使用Py2neo的NodeMatcher和RelationshipMatcher查询图中的节点和关系,以及通过执行Cypher语句的查询方式。

​本文使用的Py2neo是2021.1之后的版本,手册请戳这里:
The Py2neo Handbook

连接Neo4j数据库

本文中会用到多种数据类型,在此一并引用

import numpy as np
import pandas as pd
from py2neo import Node,Relationship,Graph,Path,Subgraph
from py2neo import NodeMatcher,RelationshipMatcher

配置Neo4j数据库的访问地址、用户名和密码

neo4j_url = 'http://localhost:7474/'
user = 'neo4j'
pwd = 'admin'

2021.1之前访问数据库的方式为:

graph = Graph(neo4j_url, username=user, password=pwd)

2021.1之后的版本访问数据库的方式为:

graph = Graph(neo4j_url,  auth=(user, pwd))

以下图为例:

  • 图中包含一些Person节点,每个Person节点有name、age、work属性;
  • 其中“赵赵”节点是多label的节点,除了有Person标签,它还有Teacher标签;
  • Person和Person节点之间有同事、邻居、学生、老师等关系;
  • 图中还有一些Location节点,它们之间有包含关系;
  • Person节点和Location节点之间有“到访”关系,“到访”关系具有date和stay_hours两个属性。

1. 通过graph.schema查询图中节点和关系有哪些类型

查看节点的类型用graph.schema.node_labels,查看关系的类型用graph.schema.relationship_types,它们的返回值类型都是frozenset,是不能增删元素的集合。

>>>graph.schema.node_labels 
frozenset({'Location', 'Person', 'Teacher'})
>>>graph.schema.relationship_types
frozenset({'到访', '包含', '同事', '学生', '老师', '邻居'})

2.使用NodeMatcher查询节点

首先创建一个NodeMatcher对象,用match来指明要匹配哪种label的节点,用where来表示筛选条件(有两种方法)。需要注意的是,匹配成功返回的是NodeMatcher的对象,要转化成Node对象,可以用first取出符合条件的第一个节点,或者转化成节点的list。

>>>node_matcher = NodeMatcher(graph)
>>>node = node_matcher.match("Person").where(age=20).first()
>>>node
Node('Person', age=20, name='李李', work='宇宙电子厂')
>>>nodes = list(node_matcher.match("Person").where(age=35))
>>>nodes
[Node('Person', age=35, name='王王', work='宇宙电子厂')]

where条件有两种写法,一种是把要匹配的属性和值写成key=value的形式,例如上面的where(age=20),这种写法只能按照值是否完全一致来匹配,不能按照值的大小来筛选,如果写成下面这样是会报错的:

node = node_matcher.match("Person").where(age>20).first() # 错误

想要按照值的大小筛选或者做一些字符串的模糊匹配,可以把条件表达式写成一个字符串,整体放在where语句中,在这个字符串中,可以用 _ 来代指匹配到的节点。下面两个例子,第一个是匹配work属性为“月亮XX”模式的Person节点,另一个是匹配age大于20的Person节点。

>>>node = node_matcher.match("Person").where("_.work =~ '月亮.*'").first()
>>>node
Node('Person', 'Teacher', age=45, name='赵赵', work='月亮中学')
>>>nodes = list(node_matcher.match("Person").where("_.age > 20"))
>>>nodes
[Node('Person', age=35, name='王王', work='宇宙电子厂'),
 Node('Person', age=30, name='张张', work='宇宙电子厂'),
 Node('Person', 'Teacher', age=45, name='赵赵', work='月亮中学')]

将NodeMatcher返回的结果转化为Node数据类型或者Node的list之后,访问其中的属性也就十分简单了,如上面最后一例的结果,访问其中第一个节点的name属性:

>>>nodes[0]['name']
'王王'

3. 使用RelationshipMatcher查询关系

RelationshipMatcher的match方法有三个及以上参数:

  • 第一个参数是节点的序列或者set,可以为None,为None表示任意节点均可;
  • 第二个参数是关系的类型,可以为None,为None表示任意类型的关系均可;
  • 第三个参数开始是要匹配的属性,写成key=value的形式。

match方法的返回值是RelationshipMatcher类型,需要通过first转化成Relationship数据结构,或者转化为list。
举例说明,比如想要查询“李李”节点的所有关系。先查询出节点,再查询节点的关系,r_type=None表示任意类型的关系均可。返回的关系包括到访、同事。

>>>node1 = node_matcher.match("Person").where(name='李李').first()
>>>relationship = list(relationship_matcher.match([node1], r_type=None))
>>>relationship
[到访(Node('Person', age=20, name='李李', work='宇宙电子厂'), Node('Location', name='禄口机场'), date='2021/7/16', stay_hours=1),
 同事(Node('Person', age=20, name='李李', work='宇宙电子厂'), Node('Person', age=30, name='张张', work='宇宙电子厂')),
 同事(Node('Person', age=20, name='李李', work='宇宙电子厂'), Node('Person', age=35, name='王王', work='宇宙电子厂'))]

例2,查询“李李”和“张张”的关系,两个节点的顺序表示了要匹配的关系的方向。所以在整个图中“李李”和“张张”节点之间的同事关系是双向的,但是查询结果只给出了从“张张”节点到“李李”节点的一条关系。

>>>node1 = node_matcher.match("Person").where(name='李李').first()
>>>node2 = node_matcher.match("Person").where(name='张张').first()
>>>relationship = list(relationship_matcher.match((node2,node1), r_type=None))
>>>relationship
[同事(Node('Person', age=30, name='张张', work='宇宙电子厂'), Node('Person', age=20, name='李李', work='宇宙电子厂'))]

例3,查询图中某一类关系,第一个参数为None,第二个参数r_type指定关系类型,这里查询了图中所有的同事关系。

>>>relationship = list(relationship_matcher.match(None, r_type='同事'))
>>>relationship
[同事(Node('Person', age=20, name='李李', work='宇宙电子厂'), Node('Person', age=30, name='张张', work='宇宙电子厂')),
 同事(Node('Person', age=20, name='李李', work='宇宙电子厂'), Node('Person', age=35, name='王王', work='宇宙电子厂')),
 同事(Node('Person', age=35, name='王王', work='宇宙电子厂'), Node('Person', age=20, name='李李', work='宇宙电子厂')),
 同事(Node('Person', age=30, name='张张', work='宇宙电子厂'), Node('Person', age=20, name='李李', work='宇宙电子厂'))]

例4,在查询关系时按照属性的值筛选,可以将该属性写为key=value的形式作为match方法的第三个参数。这里,查询图中的到访关系,并且stay_hours属性为1。

>>>relationship = list(relationship_matcher.match(None, r_type='到访', stay_hours=1))
>>>relationship
[到访(Node('Person', age=20, name='李李', work='宇宙电子厂'), Node('Location', name='禄口机场'), date='2021/7/16', stay_hours=1)]

虽然Py2neo的手册上没有写,但其实RelationshipMatcher也可以接上where方法,按照属性的值筛选关系。上面这个例子也可以写作下面这种形式,效果是一样的。

relationship = list(relationship_matcher.match(None, r_type='到访').where(stay_hours=1))

同样,在where方法中也可以写一个字符串表达式,实现按值大小来筛选关系。例如要筛选出所有到访关系,且stay_hours>=1的关系时,可以这样写:

>>>relationship = list(relationship_matcher.match(None, r_type='到访').where("_.stay_hours>=1"))
>>>relationship
[到访(Node('Person', age=20, name='李李', work='宇宙电子厂'), Node('Location', name='禄口机场'), date='2021/7/16', stay_hours=1),
 到访(Node('Person', age=20, name='刘刘', work='地球电子商务公司'), Node('Location', name='禄口机场'), date='2021/7/19', stay_hours=4)]

如何访问返回的结果中的各个属性呢,Relationship其实是包含了一对起止节点:start_nodeend_node,包含了关系的类型,而关系的属性是以字典形式存在的,可以用get方法来获取属性的值。
获取关系的起止节点:

>>>print(relationship[0].start_node['name'])
>>>print(relationship[0].end_node['name'])
李李
禄口机场

获取关系的类型的文本字符串

>>>print(relationship[0])
>>>print(type(relationship[0]).__name__)
(李李)-[:到访 {date: '2021/7/16', stay_hours: 1}]->(禄口机场)
到访

获取关系中的属性和值

>>>print(relationship[0].keys())
>>>print(relationship[0].values())
>>>print(relationship[0].get('date'))
dict_keys(['date', 'stay_hours'])
dict_values(['2021/7/16', 1])
2021/7/16

4.通过执行Cypher语句查询

NodeMatcher和RelationshipMatcher能够表达的匹配条件相对简单,更加复杂的查询还是需要用Cypher语句来表达。Py2neo本身支持执行Cypher语句的执行,可以将复杂的查询写成Cypher语句,通过graph.run方法查询,返回的结果可以转化为pandas.DataFrame或者pandas.Series对象,从而和其他数据分析工具无缝衔接。

例如,要查询Person节点,并满足work属性为“宇宙电子厂”。Cypher语句中可以使用WHERE接条件表达式,使用AS将返回的属性改名,返回多个属性时,用xxx AS x, yyy AS y。graph.run方法之后再接to_data_frame()可以将返回的数据变成pandas的DataFrame对象,并且用AS改过的属性名即为DataFrame中的列名

cypher_ = "MATCH (n:Person) \
WHERE n.work='宇宙电子厂' \
RETURN n.name AS name, n.age AS age "

df = graph.run(cypher_).to_data_frame() # pd.DataFrame

例2,查询一个已知节点和其他哪些节点有关系,有什么样的关系。Cypher语言查询关系时用 < 或者 > 表示方向,这里需要返回type(r),直接返回r的话结果里是空值。

>>>cypher_ = "MATCH (n:Person)-[r]->(m:Person) \
WHERE n.name='李李' \
RETURN type(r) AS type,m.name AS name"
>>>df = graph.run(cypher_).to_data_frame() # pd.DataFrame

例3,Cypher语言还可以查询路径,因为不确定返回的路径数量,所以最好先将结果转化为pandas.Series,再遍历访问其中每条路径的节点和关系。
这里查询的是“赵赵”节点和“王王”节点之间的关系路径,关系指定为同事或邻居,关系不超过4层。

>>>cypher_ = "MATCH path=(m:Person)-[:同事|邻居*1..4]->(n:Person) \
WHERE m.name='赵赵' AND n.name='王王' \
RETURN path"
>>>s = graph.run(cypher_).to_series()
>>>print(len(s))
>>>s[0]
1
Path(Node('Person', 'Teacher', age=45, name='赵赵', work='月亮中学'),
邻居(Node('Person', 'Teacher', age=45, name='赵赵', work='月亮中学'), 
Node('Person', age=30, name='张张', work='宇宙电子厂')), 
同事(Node('Person', age=30, name='张张', work='宇宙电子厂'), 
Node('Person', age=20, name='李李', work='宇宙电子厂')), 
同事(Node('Person', age=20, name='李李', work='宇宙电子厂'), 
Node('Person', age=35, name='王王', work='宇宙电子厂')))

这里查询到的关系路径数量仅有1条。从上图的结果中也可以看出来,Path是一个比较复杂的结构,Path中的节点和关系分别用nodesrelationships表示,并且是按照路径上节点和关系的顺序分别存放的。这里给出一段示例代码,对每一个路径都做了直接打印path数据结构和自己组织路径文本。

for path in s:
    # 直接打印path
    print(path)
    # 获取路径中的节点和关系
    nodes = path.nodes
    relationshis = path.relationships   
    # 自己组织路径文本
    path_text = ""
    for n,r in zip(nodes, relationshis):
        # 每次加入一个节点和一个关系的类型
        path_text += "{} - {} - ".format(n['name'], type(r).__name__)
    # 别忘了最后一个节点
    path_text += nodes[-1]['name'] + '\n'
    print(path_text)

运行这段代码得的结果如下所示,上面一行是直接打印路径的结果,下面一行是自己组织文本得到的结果。

(赵赵)-[:邻居 {}]->(张张)-[:同事 {}]->(李李)-[:同事 {}]->(王王)
赵赵 - 邻居 - 张张 - 同事 - 李李 - 同事 - 王王

使用Py2neo查询Neo4j中的节点、关系和路径时,条件简单的查询可以通NodeMatcher和RelationshipMatcher来实现。而较为复杂的查询,可以写成Cypher语句来查询,查询结果可以转化为pandas的DataFrame或者Series数据类型,与其他数据分析工具结合。


我的Python版本

>>> import sys
>>> print(sys.version)
3.7.6 (default, Jan  8 2020, 13:42:34) 
[Clang 4.0.1 (tags/RELEASE_401/final)]

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK