教程:LDBC Gremlin¶
在开始之前,需要说明的是LDBC在图数据库/系统基准测试标准和审计测试领域享有盛誉。LDBC开发了多套基准测试工具集,其中GIE主要针对社交网络基准测试(SNB)。该基准测试专门为数据库管理系统定义了图工作负载,包括交互式(高QPS)工作负载和商业智能工作负载。
在本教程中,我们将引导您了解查询SNB模拟社交网络时的各种应用场景。通过本教程的学习,我们相信您将能够熟练构建与SNB商业智能工作负载同样复杂的Gremlin查询。
加载LDBC图数据¶
在本教程中,我们将使用LDBC社交网络作为图数据,它包含以下实体:
图中的顶点/边标签(类型)
标签之间的连接和关系
与各类标签相关的属性
LDBC图是GraphScope内置的数据集之一,因此可以通过以下方式轻松加载:
from graphscope.dataset.ldbc import load_ldbc
graph = load_ldbc()
这将加载比例为1的LDBC社交网络。
提示
我们确实允许加载更大的LDBC图数据,例如sf 3k规模。 为了处理如此大规模的图数据,建议您在大型集群中使用GIE的独立部署模式。
目前,GIE支持Gremlin作为其查询语言。
在加载LDBC图数据并初始化引擎后,我们可以通过g.execute(GREMLIN_QUERIES)轻松地向GIE提交Gremlin查询。
例如,如果我们想统计LDBC图中有多少个顶点和边,只需编写以下Python代码:
import graphscope as gs
from graphscope.dataset.ldbc import load_ldbc
# load the ldbc graph
graph = load_ldbc()
# Hereafter, you can use the `graph` object to create an `gremlin` query session
g = gs.gremlin(graph)
# then `execute` any supported gremlin query.
# count vertices
q1 = g.execute('g.V().count()')
print(q1.all().result())
# count edges
q2 = g.execute('g.E().count()')
print(q2.all().result())
那么输出应该是:
[190376]
[787074]
这表明LDBC sf1图中有190376个顶点和787074条边。
基础顶点/边查询¶
使用SQL语句从关系型数据库中检索数据是非常常见的操作。在图数据中,顶点可视为实体,边则代表实体间的连接关系。类似地,借助GIE和相应的图查询语句,我们也能轻松地从图中检索数据。
检索顶点和边¶
如上一个教程所示,我们可以通过g.V()和g.E() Gremlin步骤轻松地从图中检索顶点和边。
# then `execute` any supported gremlin query.
# Retrieve all vertices
q1 = g.execute('g.V()')
print(q1.all().result())
# Retrieve all edges
q2 = g.execute('g.E()')
print(q2.all().result())
上述代码的输出应类似于:
# All vertices
[v[432345564227583365], ......, v[504403158265495622]]
# All edges
[e[576460752303435306][432345564227579434-hasType->504403158265495612], ......, e[144115188075855941][504403158265495622-isSubclassOf->504403158265495553]]
对于顶点,[]中的数字代表其ID。但这些ID看起来有些令人困惑:为什么它们这么大?实际上,这种大ID是由于图数据库与关系型数据库的存储机制不同造成的。
在关系型数据库中,不同类型的实例被分别存储在不同的表中。因此,我们通常使用本地ID从表中检索特定实例。例如,我们可以使用select * from Person where id=0从Person表中检索ID为0的人员。由于ID是本地化的,属于不同表的实例可能具有相同的ID(例如person(id=0)和message(id=0))。
在图存储中,各类顶点和边被集中存储。如果仍使用本地ID来定位顶点和边,将会产生大量冲突。因此,每个顶点和边都需要一个特定的全局标识符,称为global id,以区别于其他元素。
GIE使用全局ID来标识每个顶点和边。这些ID看起来很大的主要原因是底层存储通过位运算(移位和加法)从顶点的局部ID和标签生成其全局ID。毕竟,全局ID只是标识符,我们不需要过多关注它的具体形式。
对于边而言,第一个[]中的数字仍表示其ID。第二个[]中的内容格式为source vertex id -edge label(type)-> destination vertex id,表示该边两端顶点的ID及连接类型(边标签)。
应用一些过滤器¶
在大多数情况下,我们不需要从图中检索所有顶点和边,因为我们可能只关心其中的一小部分。因此,GIE通过Gremlin提供了一些过滤操作,帮助您找到感兴趣的数据。
如果您只想提取具有给定ID的顶点/边,可以在GIE中编写Gremlin语句g.V(id)。(未来将支持g.E(id))
# Retrieve vertex with id 1
q1 = g.execute('g.V(1)')
print(q1.all().result())
输出应如下所示:
[v[1]]
有时,您可能需要检索具有特定标签的顶点/边,这时可以在Gremlin中使用haveLabel(label)步骤。例如,以下代码展示了如何查找所有person类型的顶点。
# Retrieve vertices having label 'person'
q1 = g.execute('g.V().hasLabel("person")')
print(q1.all().result())
输出应如下所示:
# All person vertices
[v[216172782113783808], ......, v[216172782113784710]]
如果您想提取多种类型(标签)的顶点,可以在步骤中写入多个标签,例如haveLabel(label1, label2...)。例如,以下代码提取所有具有'person'或'forum'标签的顶点。
# Retrieve vertices having label 'person' or 'forum'
q1 = g.execute('g.V().hasLabel("person", "forum")')
print(q1.all().result())
输出应如下所示:
# All person vertices and forum vertices
[v[216172782113783808], ......, v[72057594037936036]]
如上一教程所述,GraphScope将数据图建模为属性图,因此GIE允许用户通过has(...)步骤根据某些属性筛选输出结果。
has(...)步骤的使用非常灵活。首先,它允许用户查找具有某些所需属性的顶点。例如,以下代码旨在提取具有'creationDate'属性的顶点
# Retrieve vertices having property 'creationDate'
q1 = g.execute('g.V().has("creationDate")')
print(q1.all().result())
输出应如下所示:
# All vertices having property 'creationDate'
v[360287970189718653], ......, v[360287970189718655]]
从上面展示的LDBC模式中可以看到,带有标签'person'、'forum'、'post'和'comment'的顶点都具有'creationDate'属性。因此,上述查询语句等价于:
# Retrieve vertices having label 'person' or 'forum' or 'message'
q1 = g.execute('g.V().hasLabel("person", "forum", "comment", "post")')
print(q1.all().result())
此外,您可能还需要提取满足某些条件的属性。例如,如果您想提取名字为'Joseph'的人员,可以编写以下代码:
# Retrieve person vertices whose first name is 'Joseph'
q1 = g.execute('g.V().hasLabel("person").has("firstName", "Joseph")')
print(q1.all().result())
你也可以在has(...)步骤中编写更复杂的谓词,例如:
# Retrieve person vertices whose first name is not 'Joseph'
q1 = g.execute('g.V().hasLabel("person").has("firstName", not(eq("Joseph")))')
# Retrieve person vertices whose first name is either 'Joseph' or 'Yacine'
q2 = g.execute('g.V().hasLabel("person").has("firstName", within("Joseph", "Yacine"))')
# Retrieve comment vertices created after the very beginning of year 2011
q3 = g.execute('g.V().hasLabel("comment").has("creationDate", gt( "2011-01-01T00:00:00.000+0000"))')
print(q1.all().result())
print(q2.all().result())
print(q3.all().result())
以下是关于Gremlin中has(...)步骤的更多参考资料。
提取属性值¶
有时,您可能对顶点/边的属性值更感兴趣。借助GIE,您可以轻松使用values(PROPERTY_NAME) Gremlin步骤来提取顶点/边的属性值。
例如,您已经知道顶点(id=38416)是一条评论,并且对其内容感兴趣,那么您可以在GIE中编写以下代码:
# Extract comment vertex(id=38416)'s content
q1 = g.execute('g.V(38416).values("content")')
print(q1.all().result())
输出应该类似于:
# The content of the comment(id=38416)
['maybe']
实际应用¶
由于GIE提供了Python接口,对检索到的数据进行进一步分析非常方便。
以下是一个关于LDBC图中评论内容长度(单词数)统计的示例。
import matplotlib.pyplot as plt
import graphscope as gs
from graphscope.dataset.ldbc import load_ldbc
# load the modern graph as example.
graph = load_ldbc()
# Hereafter, you can use the `graph` object to create an `gremlin` query session
g = gs.gremlin(graph)
# Extract all comments' contents
q1 = g.execute('g.V().hasLabel("comment").values("content")')
comment_contents = q1.all().result()
comment_length = [len(comment_content.split()) for comment_content in comment_contents];
# Draw Histogram
plt.hist(x=list(filter(lambda x:(x< 50), comment_length)), bins='auto', color='#607c8e',alpha=0.75)
plt.grid(axis='y', alpha=0.75)
plt.xlabel('Comments Length')
plt.ylabel('Comments Count')
plt.title('Comment Length Histogram')
plt.show()
图2. ldbc社交网络中评论长度的直方图分布。¶
我们还可以绘制一个关于社交网络中性别比例的饼图。
import matplotlib.pyplot as plt
import graphscope as gs
from graphscope.dataset.ldbc import load_ldbc
# load the modern graph as example.
graph = load_ldbc()
# Hereafter, you can use the `graph` object to create an `gremlin` query session
g = gs.gremlin(graph)
# Extract all person' gender property value
q1 = g.execute('g.V().hasLabel("person").values("gender")')
person_genders = q1.all().result()
# Count male and female
male_count = 0
female_count = 0
for gender in person_genders:
if gender == "male":
male_count += 1
else:
female_count += 1
# Draw pie chart
plt.pie(x=[male_count, female_count], labels=["Male", "Female"])
plt.show()
图3. LDBC社交网络中人物性别的饼状图。¶
基础遍历查询¶
属性图与关系型数据库的主要区别在于,属性图将实体(顶点)之间的关系(边)视为一等公民。因此,在属性图中按照预定义的路径遍历顶点和边非常容易。
在GIE中,我们将这种遍历称为图遍历:这类工作负载涉及从一组源顶点出发遍历图,同时满足遍历路径上顶点和边的约束条件。图遍历与分析型工作负载不同,因为它通常只访问图的一小部分而非整个图。
在本教程中,我们将探讨如何使用Gremlin步骤遍历属性图。此外,我们还将展示一些在实际数据分析中应用图遍历的示例。
扩展¶
在图遍历中最基本的单元是扩展(Expansion),即从一个顶点/边出发到达其相邻元素。GIE目前支持以下Gremlin扩展步骤:
out(): 根据给定的边标签,将顶点映射到其出向相邻顶点。in(): 根据给定的边标签,将顶点映射到其入方向的相邻顶点。both(): 根据给定的边标签,将顶点映射到其相邻顶点。outE(): 根据给定的边标签,将顶点映射到其出射边。inE(): 将顶点映射到其入方向的关联边,需指定边标签。bothE(): 根据给定的边标签,将顶点映射到其关联的边。outV(): 将边映射到其出向/尾部关联顶点。inV(): 将边映射到其入边/头顶点。otherV(): 将边映射到路径历史中未被遍历的关联顶点。bothV():将边映射到其关联的顶点。
这里是从LDBC图中提取的一个局部子图,它由一个中心人物顶点(id=216172782113784483)及其所有相邻边和顶点组成。接下来我们将使用这个子图来解释所有的扩展步骤。
图5. 围绕给定人物顶点的局部图。¶
out()、in() 和 both()¶
这三个步骤都从源顶点开始遍历到其相邻顶点。它们之间的区别在于:
out()通过出边遍历到相邻顶点in()遍历指向相邻顶点的入边both()遍历出边和入边到相邻顶点
例如,如果您想通过三步查找顶点(id=216172782113784483)的相邻顶点,可以在GIE中编写如下语句:
# Traverse from the vertex to its adjacent vertices through its outgoing edges
q1 = g.execute('g.V(216172782113784483).out()')
# Traverse from the vertex to its adjacent vertices through its incoming edges
q2 = g.execute('g.V(216172782113784483).in()')
# Traverse from the vertex to its adjacent vertices through all of its incident edges
q3 = g.execute('g.V(216172782113784483).both()')
print(q1.all().result())
print(q2.all().result())
print(q3.all().result())
该图展示了q1、q2和q3的执行过程:
图6. out/in/both步骤的示例。¶
因此,上述代码的输出结果应如下所示:
# q1: vertex's adjacent vertices through outgoing edges
[v[432345564227569033], v[288230376151712472], v[144115188075856168], v[144115188075860911]]
# q2: vertex's adjacent vertices through incoming edges
[v[72057594037934114]]
# q3: vertex's adjacent vertices through all of its incident edges
[v[432345564227569033], v[288230376151712472], v[144115188075856168], v[144115188075860911], v[72057594037934114]]
此外,这三个步骤都支持使用边标签作为参数来进一步限制遍历的边,例如out/in/both(label1, label2, ...),这意味着它只能遍历标签为{label 1, label 2, ..., label n}之一的边。例如:
# Traverse from the vertex to its adjacent vertices through its incoming edges, and the edge label should be 'hasModerator'
q1 = g.execute('g.V(216172782113784483).in("hasModerator")')
# Traverse from the vertex to its adjacent vertices through its outgoing edges, and the edge label should be either 'studyAt' or 'workAt'
q2 = g.execute('g.V(216172782113784483).out("studyAt", "workAt")')
# Traverse from the vertex to its adjacent vertices through all of its incident edges, and the edge label should be either 'isLocatedIn' or 'hasModerator'
q3 = g.execute('g.V(216172782113784483).both("isLocatedIn", "hasModerator")')
print(q1.all().result())
print(q2.all().result())
print(q3.all().result())
该图展示了q1、q2和q3的执行过程:
图7. 从给定标签出发的out/in/both步骤示例。¶
因此,上述代码的输出结果应如下所示:
# q1: vertex's adjacent vertices through incoming 'hasModerator' edges
[v[72057594037934114]]
# q1: vertex's adjacent vertices through outgoing 'studyAt' or 'workAt' edges
[v[144115188075860911], v[144115188075856168]]
# q1: vertex's adjacent vertices through 'isLocatedIn' or 'hasModerator' edges
[v[288230376151712472], v[72057594037934114]]
outE()、inE() 和 bothE()¶
这三个步骤都从源顶点开始遍历到它们的相邻边。与out()、in()和both()类似,它们之间的区别在于:
outE()只能遍历到源顶点的出向邻接边inE()只能遍历到源顶点的入向邻接边bothE()可以遍历源顶点的出边和入边相邻边
例如,如果您想通过三个步骤找到顶点(id=216172782113784483)的相邻边,可以在GIE中这样编写语句:
# Traverse from the vertex to its outgoing adjacent edges
q1 = g.execute('g.V(216172782113784483).outE()')
# Traverse from the vertex to its incoming adjacent edges
q2 = g.execute('g.V(216172782113784483).inE()')
# Traverse from the vertex to all of its adjacent edges
q3 = g.execute('g.V(216172782113784483).bothE()')
print(q1.all().result())
print(q2.all().result())
print(q3.all().result())
该图展示了q1、q2和q3的执行过程:
图8. outE/inE/bothE步骤的示例。¶
因此,上述代码的输出结果应如下所示:
# q1: vertex's incident outgoing edges
[e[432345564227582847][216172782113784483-hasInterest->432345564227569033], e[504403158265496227][216172782113784483-isLocatedIn->288230376151712472], e[864691128455136658][216172782113784483-workAt->144115188075856168], e[1008806316530991636][216172782113784483-studyAt->144115188075860911]]
# q2: vertex's incident incoming edges
[e[360287970189645858][72057594037934114-hasModerator->216172782113784483]]
# q3: vertex's incident edges
[e[360287970189645858][72057594037934114-hasModerator->216172782113784483], e[432345564227582847][216172782113784483-hasInterest->432345564227569033], e[504403158265496227][216172782113784483-isLocatedIn->288230376151712472], e[864691128455136658][216172782113784483-workAt->144115188075856168], e[1008806316530991636][216172782113784483-studyAt->144115188075860911]]
类似地,这三个步骤也支持使用边标签作为参数来进一步限制遍历的边,例如outE/inE.bothE(label1, label2, ...),表示只能遍历标签为{label 1, label 2, ..., label n}之一的边。例如:
# Traverse from the vertex to its incident incoming edges, and the edge label should be 'hasModerator'
q1 = g.execute('g.V(216172782113784483).inE("hasModerator")')
# Traverse from the vertex to its incident outgoing edges, and the edge label should be either 'studyAt' or 'workAt'
q2 = g.execute('g.V(216172782113784483).outE("studyAt", "workAt")')
# Traverse from the vertex to its incident edges, and the edge label should be either 'isLocatedIn' or 'hasModerator'
q3 = g.execute('g.V(216172782113784483).bothE("isLocatedIn", "hasModerator")')
print(q1.all().result())
print(q2.all().result())
print(q3.all().result())
该图展示了q1、q2和q3的执行过程:
图9. 从给定标签出发的outE/inE/bothE步骤示例。¶
因此,上述代码的输出应类似于:
# q1: vertex's incident incoming 'hasModerator' edges
[e[360287970189645858][72057594037934114-hasModerator->216172782113784483]]
# q2: vertex's incident outgoing 'studyAt' or 'workAt' edges
[e[1008806316530991636][216172782113784483-studyAt->144115188075860911], e[864691128455136658][216172782113784483-workAt->144115188075856168]]
# q3: vertex's incident 'isLocatedIn' or 'hasModerator' edges
[e[360287970189645858][72057594037934114-hasModerator->216172782113784483], e[504403158265496227][216172782113784483-isLocatedIn->288230376151712472]]
outV()、inV()、bothV() 和 otherV()¶
在遍历过程中到达某条边时,您可能对其关联顶点感兴趣。因此,GIE支持从源边遍历到其关联顶点的Gremlin步骤。
图10. 一条边的出/入顶点示例。¶
对于一条从v1指向v2的边e1,我们称v1为"出点顶点",v2为"入点顶点"。
outV(): 从源边遍历到出方向的顶点inV(): 从源边遍历到入方向的顶点bothV(): 从源边同时遍历出边和入边的顶点otherV(): 这一步有点特殊,它从源边遍历到尚未出现在遍历历史中的关联顶点。稍后将详细解释。
在本地子图中,假设当前我们位于'isLocatedIn'边。如果想通过outV()/inV()/bothV()步骤获取其关联顶点,可以在GIE中编写如下语句:
# Traverse from the edge to its outgoing incident vertex
q1 = g.execute('g.V(216172782113784483).outE("isLocatedIn").outV()')
# Traverse from the edge to its incoming incident vertex
q2 = g.execute('g.V(216172782113784483).outE("isLocatedIn").inV()')
# Traverse from the edge to both of its incident vertex
q3 = g.execute('g.V(216172782113784483).outE("isLocatedIn").bothV()')
print(q1.all().result())
print(q2.all().result())
print(q3.all().result())
该图展示了q1、q2和q3的执行过程:
图11. outV/inV/bothV步骤的示例。¶
因此,上述代码的输出应类似于:
# q1: the edge's outgoing vertex
[v[216172782113784483]]
# q2: the edge's incoming vertex
[v[288230376151712472]]
# q3: both of the edge's outgoing and incoming vertices
[v[216172782113784483], v[288230376151712472]]
对于otherV()函数,它只会遍历到在遍历历史中尚未到达的顶点。例如,'isLocatedIn'边是从出顶点'Person'到达的,那么该边的otherV()就是其入顶点'Place',因为之前未被访问过。
# Traverse from the edge to its incident vertex that hasn't been reached
q1 = g.execute('g.V(216172782113784483).outE("isLocatedIn").otherV()')
print(q1.all().result())
输出应如下所示:
# q1: 'place' vertex, which has not been reached before
[v[288230376151712472]]
多步扩展¶
你可能已经注意到,图遍历支持多步扩展。例如,在Gremlin语句g.V(216172782113784483).outE('isLocatedIn').inV()中,outE('isLocatedIn')是第一步扩展,而inV()是第二步扩展。
该图展示了这条Gremlin语句的执行过程:
图12. outE后接inV的示例。¶
在遍历过程中,首先从源顶点'person'出发,到达'isLocatedIn'边。然后从'isLocatedIn'边开始,到达'place'顶点。至此遍历结束,'place'顶点是本次遍历的终点,而'isLocatedIn'边可视为遍历的中间过程。
当GIE在处理遍历过程中连续的多个扩展步骤时,下一个扩展步骤会使用前一个扩展步骤的输出作为其起点。基于这一特性,我们可以总结出以下等价规则:
out() = outE().inV() = outE().otherV()in() = inE().outV() = inE().otherV()both() = bothE().otherV()
可以肯定的是,GIE支持多步顶点扩展。例如,一个人所在的城市(city)通过'isPartOf'关系属于一个更大的地点(国家)。如果您想查找更大的地点,可以在GIE中编写多步顶点扩展查询如下:
# Traverse from the vertex to the larger place it is located in by two vertex expansions
q1 = g.execute('g.V(216172782113784483).out("isLocatedIn").out("isPartOf")')
print(q1.all().result())
该图展示了执行流程:
图13. 两跳出边顶点的示例。¶
因此,Gremlin语句的输出应如下所示:
# The larger place(country) that the person is located in
[v[288230376151711797]]
当前介绍的Gremlin遍历语句存在一个问题:只保留最后一步的结果。有时您可能需要记录遍历的中间结果以便进一步分析,但该如何实现呢?实际上,您可以使用as(TAG)和select(TAG 1, TAG 2, ..., TAG N)步骤来保留中间结果:
as(TAG): 为它跟随的步骤赋予一个标签,之后可以通过该标签访问前一步骤的值。select(TAG 1, TAG 2, ..., TAG N): 选择给定标签所指向步骤的所有值。
在前一个示例的基础上,如果您希望在输出中同时保留人物、较小地点(城市)和较大地点(国家),可以在GIE中编写如下Gremlin语句:
# a: person, b: city, c: country
q1 = g.execute('g.V(216172782113784483).as("a")\
.out("isLocatedIn").as("b")\
.out("isPartOf").as("c")\
.select("a", "b", "c")')
print(q1.all().result())
其中标签 'a' 指向 'person' 顶点,标签 'b' 指向 'city' 顶点,标签 'c' 指向 'country' 顶点。输出结果应类似:
[{'a': v[216172782113784483], 'b': v[288230376151712472], 'c': v[288230376151711797]}]
从多个起点进行扩展¶
目前我们仅讨论了从单个顶点/边扩展的情况。然而在GIE中,编写类似g.V().out()的Gremlin语句非常常见。通过之前的教程我们已经知道,g.V()会获取图中的所有顶点。那么该如何理解g.V().out()的含义呢?
为了更清楚地解释,让我们首先看一个更简单的情况:从仅两个顶点开始。这是由两个人顶点局部图组成的LDBC图子图,其中两个人顶点的ID分别为216172782113784483和216172782113784555。
图14. 两个局部图的示例。¶
此外,我们进一步将遍历的起点限制为恰好是这两个人物顶点。因此,我们可以在GIE中编写以下Gremlin语句:
# Traverse from the two source vertices to their adjacent vertices through their incident outgoing edges
q1 = g.execute('g.V(216172782113784483, 216172782113784555).out()')
print(q1.all().result())
该图展示了q1的执行过程:
图15. 从两个给定顶点出发的出边邻居结果。¶
遍历从两个人顶点开始,然后out()步骤将两个源顶点都映射到它们的出边相邻顶点。因此,输出应该类似于:
[v[432345564227569033], v[288230376151712472], v[144115188075856168], v[144115188075860911], v[432345564227569357], v[432345564227570524], v[288230376151712984], v[144115188075861043]]
这两个'person'顶点的出边相邻顶点是哪些。
如果在最终输出中保留person顶点的信息,理解起来会更清晰:
q1 = g.execute('g.V(216172782113784483, 216172782113784555).as("a")\
.out().as("b")\
.select("a", "b")')
print(q1.all().result())
# the first 4 elements are tuple of vertex(id=216172782113784483) and its outgoing adjacent vertex
# the last 4 elements are tuple of vertex(id=216172782113784555) and its outgoing adjacent vertex
[{'a': v[216172782113784483], 'b': v[432345564227569033]},
{'a': v[216172782113784483], 'b': v[288230376151712472]},
{'a': v[216172782113784483], 'b': v[144115188075856168]},
{'a': v[216172782113784483], 'b': v[144115188075860911]},
{'a': v[216172782113784555], 'b': v[432345564227569357]},
{'a': v[216172782113784555], 'b': v[432345564227570524]},
{'a': v[216172782113784555], 'b': v[288230376151712984]},
{'a': v[216172782113784555], 'b': v[144115188075861043]}]
通常来说,从遍历器(traverser)的角度来理解扩展和遍历步骤会更好。每个扩展步骤实际上是将当前遍历器映射到另一个系列遍历器,完整的遍历过程可以视为遍历器的转换过程。我们以g.V().out().in()为例进行说明。
最初,
g.V()从图中提取所有顶点,这些顶点构成了初始遍历器:每个遍历器包含对应的顶点。然后
out()将每个遍历器(顶点)映射到其出方向的相邻顶点,这些出方向的相邻顶点将形成下一系列的遍历器。最后,
in()将每个遍历器(顶点)映射到其入方向的相邻顶点,这些入方向的相邻顶点形成下一组遍历器,这也是该Gremlin语句的最终输出结果。
该图展示了在遍历g.V().out().in()过程中遍历器转换的概览。
图16. 遍历器转换的概览。¶
此外,该图展示了在执行Gremlin语句g.V().as('a').out().as('b').in().as('c')过程中,遍历器在每次扩展步骤和as(TAG)步骤后的变化细节。
图17. 上述遍历器转换的映射细节。¶
筛选与扩展¶
扩展后应用过滤器¶
在上一个教程中,我们讨论了可以使用has(...)系列步骤对提取的顶点和边应用过滤器。实际上,这些步骤也可以在扩展步骤之后应用,用于筛选当前遍历器。
仍以g.V().out().in()为例,如果我们希望:
g.V()操作后的遍历器(顶点)具有'person'标签第一次扩展
out()后的遍历器(顶点)具有属性 'browserUsed' 且值为 'Chrome'第二次扩展
in()后的遍历器(顶点)具有'length'属性且长度小于5,且扩展边的标签为'replyOf'
# Traversers(vertices) after g.V() has label 'person'
# Traversers(vertices) after out() has property 'browserUsed' and the value is 'Chrome'
# Traversers(vertices) after in('replyOf') has property 'length' and the length is < 5
q1 = g.execute('g.V().hasLabel("person")\
.out().has("browserUsed", "Chrome")\
.in("replyOf").has("length", lt(5))')
print(q1.all().result())
输出应如下所示:
[v[54336], ..., v[33411]]
此外,对于包含as(TAG)步骤的Gremlin语句,我们可以引入where()步骤来基于标记值对遍历器应用过滤器。
在前一个示例的基础上,我们在两个has(...)步骤后添加了标签'a'和'b':
q1 = g.execute('g.V().hasLabel("person")\
.out().has("browserUsed", "Chrome").as("a")\
.in("replyOf").has("length", lt(5)).as("b")')
如果我们希望标签为‘a’和‘b’的顶点在属性‘browserUsed’上具有相同的属性值,可以在GIE中添加一个where步骤并编写以下Gremlin语句:
# Traversers(vertices) after g.V() has label 'person'
# Traversers(vertices) after out() has property 'browserUsed' and the value is 'Chrome'
# Traversers(vertices) after in('replyOf') has property 'length' and the length is < 5
# vertex a and b have the same value of 'browserUsed' property
q1 = g.execute('g.V().hasLabel("person")\
.out().has("browserUsed", "Chrome").as("a")\
.in("replyOf").has("length", lt(5)).as("b")\
.where("b", eq("a")).by("browserUsed")\
.select("a", "b")')
print(q1.all().result())
print(q1.all().result())
输出应如下所示:
[{'a': v[360287970189700805], 'b': v[59465]}, ..., {'a': v[33403], 'b': v[33411]}]
扩展作为过滤器¶
此外,我们不仅可以在扩展步骤后添加过滤器,还可以使用扩展步骤作为where(...)步骤的过滤谓词。例如,如果我们想提取所有拥有至少5条出边的顶点,可以编写如下使用where(...)步骤的Gremlin语句:
# Retrieve vertices having at least 5 outgoing incident edges
q1 = g.execute('g.V().where(outE().count().is(gte(5)))')
print(q1.all().result())
路径扩展(语法糖)¶
Until now, we can use expansion steps, filter steps and auxiliary steps like as(TAG) to write complicated Gremlin sentences to traverse the graph. However, there are still two shortcomings:
如果我们想找到一个距离当前顶点10跳(边)远的顶点,需要重复编写扩展步骤10次,这种做法非常低效。
从源顶点到目标顶点的跳数可以是任意的。例如,如果我们想找到所有在3跳内可以从源顶点到达的顶点,仅靠已介绍的步骤无法解决这个问题。
因此,我们希望通过引入路径扩展来解决这两个问题,它将扩展步骤out()、in()和both()作为语法糖来实现。
在这三个扩展步骤中,您现在可以写成out/in/both(lower..upper, label1, label2...),其中lower表示路径中的最小跳数,而upper-1表示路径中的最大跳数。例如,out(3..5)表示通过出边扩展3次或4次。因此,lower <= upper - 1,否则该语句是非法的。
路径扩展非常灵活。因此,我们设计了with()步骤后接路径扩展步骤,为用户提供两种选项来配置路径扩展步骤的对应行为:
PATH_OPT: 由于路径扩展允许多次扩展同一路径步骤,路径中可能会出现重复顶点。若将PATH_OPT设为ARBITRARY,则包含重复顶点的路径将保留在遍历器中;反之,若设为SIMPLE_PATH,则包含重复顶点的路径将被过滤。RESULT_OPT: 有时,您可能对路径中的所有顶点感兴趣,但有时您可能只关注路径的最后一个顶点。因此,当RESULT_OPT设置为ALL_V时,路径中的所有顶点都会被保留到输出中;而当RESULT_OPT设置为END_V时,则仅保留路径的最后一个顶点。
最后,我们想介绍路径扩展中的endV()步骤。由于一条路径可能包含多个顶点,默认情况下所有保留的顶点都存储在路径集合中。因此,endV()用于展开路径集合。例如,如果路径扩展后的遍历器是[[v[1], v[2], v[3]],其中内部的[v[1], v[2], v[3]]就是路径集合。使用endV()后,v[1]、v[2]和v[3]会从路径集合中提取出来,遍历器就变成了[v[1], v[2], v[3]]
图18. 路径扩展的示例。¶
实际应用¶
图遍历使我们能够在图上进行更复杂的数据分析。
例如,路径 comment-hasCreator->person->isLocatedIn->city-isPartOf->country 可以帮助确定 comments 的国籍。因此,我们可以轻松地使用 GIE 计算来自中国的评论长度的直方图:
import matplotlib.pyplot as plt
import numpy as np
import graphscope as gs
from graphscope.dataset.ldbc import load_ldbc
# load the modern graph as example.
graph = load_ldbc()
# Hereafter, you can use the `graph` object to create an `gremlin` query session
g = gs.gremlin(graph)
# Extract all comments' contents in China
q1 = g.execute('g.V().hasLabel("person").where(out("isLocatedIn").out("isPartOf").values("name").is(eq("China"))).in("hasCreator").values("content")')
comment_contents = q1.all()
comment_length = [len(comment_content.split()) for comment_content in comment_contents];
plt.hist(x=list(filter(lambda x:(x< 50), comment_length)), bins='auto', color='#607c8e',alpha=0.75)
plt.grid(axis='y', alpha=0.75)
plt.xlabel('Comments Length')
plt.ylabel('Comments Count')
plt.title('Comment Length Histogram for comments from China')
plt.show()
图19. 中国地区评论长度分布直方图。¶
或者我们可以估算日本市场的浏览器份额:
import matplotlib.pyplot as plt
import graphscope as gs
from graphscope.dataset.ldbc import load_ldbc
from collections import Counter
# load the modern graph as example.
graph = load_ldbc()
# Hereafter, you can use the `graph` object to create an `gremlin` query session
g = gs.gremlin(graph)
# Extract all person' gender property value
q1 = g.execute('g.V().hasLabel("person").where(out("isLocatedIn").out("isPartOf").values("name").is(eq("Japan"))).in("hasCreator").values("browserUsed")')
browsers_used = q1.all()
browser_list = ['Firefox', 'Chrome', 'Internet Explorer', 'Safari']
# Count Firefox, Chrome, Internet Explorer and Safari
browser_counts = Counter(browsers_used)
# Draw pie chart
plt.pie(x=[browser_counts[browser] for browser in browser_list], labels=browser_list, autopct='%1.1f%%')
plt.show()
图20. 日本浏览器市场份额预估¶
模式匹配¶
假设你想在图中找到两个persons以及他们studyAt的universities,满足以下条件:
这两个
personknow彼此认识这两个
personstudyAt同一所university
通过之前教授的方法,您可以在GIE中编写以下代码来实现此目的:
q1 = g.execute('g.V().hasLabel("person").as("person1").both("knows").as("person2")\
.select("person1").out("studyAt").as("university1")\
.select("person2").out("studyAt").as("university2")\
.where("university1", eq("university2"))\
.select("person1", "person2", "university1")')
print(q1.all().result())
首先,你需要找出所有相互认识的
person1和person2组合。接着,您找到
person1的大学是university1,而person2的大学是university2。接下来,您使用
where(...)步骤来检查university1是否等于university2,并过滤掉不相等的项。最后,您使用
select(...)步骤来投影person1、person2和university1(现在university1与university2相同)。
输出结果应类似于:
# Person1 and 2 know each other and study at the same university
[{'person2': v[216172782113784481],
'person1': v[216172782113784279],
'university1': v[144115188075858884]},
{'person2': v[216172782113784361],
'person1': v[216172782113784291],
'university1': v[144115188075858879]},
{'person2': v[216172782113784642],
'person1': v[216172782113784291],
'university1': v[144115188075858879]},
{'person2': v[216172782113784473],
'person1': v[216172782113784328],
'university1': v[144115188075858700]},
{'person2': v[216172782113784700],
'person1': v[216172782113784331],
'university1': v[144115188075860619]},
{'person2': v[216172782113784375],
'person1': v[216172782113784333],
'university1': v[144115188075858813]},
{'person2': v[216172782113784593],
'person1': v[216172782113784361],
'university1': v[144115188075858879]},
{'person2': v[216172782113784642],
'person1': v[216172782113784361],
'university1': v[144115188075858879]},
{'person2': v[216172782113784291],
...
'person1': v[216172782113784688],
'university1': v[144115188075860870]},
{'person2': v[216172782113784047],
'person1': v[216172782113784692],
'university1': v[144115188075862429]}]
实际上,这类问题被称为图模式匹配。这意味着,您首先需要定义一个模式。该模式既可以用自然语言描述:
"在图中查找两个相互
know(认识)的person(人)以及他们共同studyAt(就读)的university(大学)"
或者可以表示为如下所示的图模式。
图21. 模式:两人相互认识并在同一所大学学习。¶
然后你的目标是找到所有与模式同构的匹配子图。在图同构条件下,如果模式中顶点之间存在边,则子图中两个匹配顶点之间也必须存在等效边,这样该子图与模式才被视为匹配。此外,子图中的单个顶点不能匹配模式中的多个顶点。
例如,这是LDBC图中大学顶点(id = 144115188075858884)的局部图:
图22. 大学顶点的局部图。¶
以下是该局部图中匹配的子图:
图23. 局部图中的三个匹配子图。¶
本地图中匹配的实例为:
{person1: v[216172782113784192], person2: v[216172782113784107], university: v[144115188075858884]}{person1: v[216172782113784107], person2: v[216172782113784192], university: v[144115188075858884]}{person1: v[216172782113784107], person2: v[216172782113784279], university: v[144115188075858884]}{person1: v[216172782113784279], person2: v[216172782113784107], university: v[144115188075858884]}{person1: v[216172782113784279], person2: v[216172782113784171], university: v[144115188075858884]}{person1: v[216172782113784171], person2: v[216172782113784279], university: v[144115188075858884]}
总共有6个匹配实例(相比3个子图),因为改变person1和person2的位置会产生新的匹配实例。
如本节开头所示,您可以通过编写常规Gremlin查询来进行模式匹配。但该方法存在两个缺点:
查询语句过于复杂。针对如此简单的三角形模式就编写如此冗长的查询语句,那么对于更复杂的模式(如方形、蝴蝶形或团状结构)又该如何处理呢?
几乎没有优化。模式匹配的速度很大程度上取决于您如何编写查询语句。
为解决这两个问题,GIE支持Gremlin中的match(...)步骤,该步骤专为模式匹配而设计。
match()步骤基于模式匹配的概念,提供了一种更具声明性的图查询方式:
每个
match()步骤都是一组sentences,而每个sentence都是常规的Gremlin遍历语句,但以给定的标签开始和结束。接着,GIE利用底层的MatchAnalyticsAlgorithm将所有
sentences连接起来,基于这些起始和结束标签形成一个模式结构。最后,GIE应用其匹配优化算法为形成的模式生成优化的匹配策略并执行模式匹配。
因此,使用Gremlin内置的match()步骤来解决模式匹配问题效率要高得多。
对于本节开头展示的模式(两个互相认识并在同一所大学学习的人),您可以在GIE中使用match()步骤编写以下代码:
# Person1 and 2 know each other and study at the same university
q1 = g.execute('g.V().match(__.as("person1").both("knows").as("person2"),\
__.as("person1").out("studyAt").as("university"),\
__.as("person2").out("studyAt").as("university"))')
print(q1.all().result())
输出应与之前自行编写的模式匹配语句相同:
[{'person2': v[216172782113784361],
'person1': v[216172782113784291],
'university': v[144115188075858879]},
{'person2': v[216172782113784642],
'person1': v[216172782113784291],
'university': v[144115188075858879]},
{'person2': v[216172782113784375],
'person1': v[216172782113784333],
'university': v[144115188075858813]},
{'person2': v[216172782113784593],
'person1': v[216172782113784361],
'university': v[144115188075858879]},
{'person2': v[216172782113784642],
'person1': v[216172782113784361],
'university': v[144115188075858879]},
{'person2': v[216172782113784587],
'person1': v[216172782113784363],
'university': v[144115188075860919]},
{'person2': v[216172782113784532],
'person1': v[216172782113784400],
'university': v[144115188075861858]},
{'person2': v[216172782113784491],
'person1': v[216172782113784401],
'university': v[144115188075858086]},
{'person2': v[216172782113784598],
...
'person1': v[216172782113784629],
'university': v[144115188075858881]},
{'person2': v[216172782113783931],
'person1': v[216172782113784657],
'university': v[144115188075858708]}]
让我们展示一个更复杂的模式匹配示例:我们希望找到两个persons A和B,满足以下条件:
他们彼此认识
personA 创建了一条message,随后由personB 创建的comment进行了回复
该图展示了这种模式:
图24. 模式:两人互相认识,其中一人创建评论回复另一人发布的消息。¶
我们可以轻松使用Gremlin的match(...)步骤来匹配这个模式:
# pA and pB know each other
# pB create a comment that reply a message created by pA
q1 = g.execute('g.V().match(__.as("pA").both("knows").as("pB"),\
__.as("pA").in("hasCreator").as("m"),\
__.as("pB").in("hasCreator").as("c"),\
__.as("c").out("replyOf").as("m"))')
print(q1.all().result())
输出应如下所示:
[{'pA': v[216172782113783809], 'pB': v[216172782113784011], 'c': v[3], 'm': v[360287970189640007]}, {'pA': v[216172782113783809], 'pB': v[216172782113784011], 'c': v[4], 'm': v[360287970189640007]}, {'pA': v[216172782113783809], 'pB': v[216172782113784011], 'c': v[5], 'm': v[360287970189640007]}, {'pA': v[216172782113783809], 'pB': v[216172782113784011], 'c': v[13], 'm': v[360287970189640007]}, {'pA': v[216172782113783809], 'pB': v[216172782113784011], 'c': v[18], 'm': v[360287970189640007]}, {'pA': v[216172782113783809], 'pB': v[216172782113784011], 'c': v[19], 'm': v[360287970189640007]}, {'pA': v[216172782113783809], 'pB': v[216172782113784011], 'c': v[22], 'm': v[360287970189640008]}, {'pA': v[216172782113783809], 'pB': v[216172782113784011], 'c': v[27], 'm': v[360287970189640008]}, {'pA': v[216172782113783809], 'pB': v[216172782113784011], 'c': v[29], 'm': v[360287970189640009]}, {'pA': v[216172782113783809], 'pB': v[216172782113784011], 'c': v[34], 'm': v[360287970189640009], ......, }]
请注意,match(...)步骤中的每个匹配语句都支持过滤步骤和路径扩展。例如,如果您希望knows边是1..3多跳,并且message和comment是在2012-01-01之后创建的,您可以这样编写:
# pA and pB know each other
# pB create a comment that reply a message created by pA
q1 = g.execute('g.V().match(__.as("pA").both("1..3", "knows").as("pB"),\
__.as("pA").in("hasCreator").has("creationDate", gt("2012-01-01")).as("m"),\
__.as("pB").in("hasCreator").has("creationDate", gt("2012-01-01")).as("c"),\
__.as("c").out("replyOf").as("m"))')
print(q1.all().result())
输出应如下所示:
[{'pA': v[216172782113783812], 'pB': v[216172782113783882], 'c': v[36], 'm': v[360287970189640010]}, {'pA': v[216172782113783812], 'pB': v[216172782113783882], 'c': v[37], 'm': v[360287970189640010]}, {'pA': v[216172782113783812], 'pB': v[216172782113784105], 'c': v[38], 'm': v[360287970189640010]}, {'pA': v[216172782113783812], 'pB': v[216172782113783882], 'c': v[41], 'm': v[360287970189640010]}, {'pA': v[216172782113784105], 'pB': v[216172782113783882], 'c': v[43], 'm': v[42]}, {'pA': v[216172782113783814], 'pB': v[216172782113783962], 'c': v[50], 'm': v[360287970189640135]}, {'pA': v[216172782113783814], 'pB': v[216172782113784171], 'c': v[52], 'm': v[360287970189640135]}, {'pA': v[216172782113784481], 'pB': v[216172782113784199], 'c': v[54], 'm': v[49]}, {'pA': v[216172782113783814], 'pB': v[216172782113784038], 'c': v[56], 'm': v[360287970189640135]}, {'pA': v[216172782113783816], 'pB': v[216172782113784144], 'c': v[175], 'm': v[360287970189640462], ......, }]
关系操作¶
接下来我们将介绍GIE支持的更多Gremlin步骤的关系操作。
筛选步骤¶
hasId()¶
在之前的教程中,我们已经介绍过您可以使用g.V(id)从图中筛选出具有特定全局ID的顶点。但是,如果我们想在遍历过程中基于全局ID应用过滤器该怎么办呢?
你可以使用hasId(id)步骤来实现这个目的。例如,如果你想获取全局ID恰好为144115188075861858的扩展顶点,可以在GIE中编写以下代码:
q1 = g.execute('g.V().out().hasId(144115188075861858)')
print(q1)
输出应如下所示:
[v[144115188075861858], v[144115188075861858], v[144115188075861858]]
请注意hasId(id)与has('id', id)是不同的!如前所述,hasId(id)是基于全局ID进行过滤,而has('id', id)是基于局部ID进行过滤。例如,如果您编写:
q1 = g.execute('g.V().has("id", 0)')
print(q1.all().result())
输出结果如下:
[v[72057594037927936],
v[144115188075855872],
v[288230376151711744],
v[432345564227567616],
v[504403158265495555]]
这些顶点的本地ID均为0。
where()¶
在之前的教程中,我们已经介绍过,Gremlin语句中带有as()步骤的where()步骤可用于基于标记值来过滤遍历器:
# vertex tagged as 'a' should be the same as vertex 'tagged' as b
q1 = g.execute('g.V(216172782113783808).as("a")\
.out("knows").in("knows").as("b").where("b", P.eq("a"))')
print(q1.all().result())
输出应如下所示:
[v[216172782113783808], v[216172782113783808], v[216172782113783808]]
此外,where()步骤后可以接一个by(...)步骤来提供更强大的功能:被选中的实体可能不需要相互比较,而是比较它们的属性。
例如,以下Gremlin语句要求顶点'a'和'b'在相同的'firstName'属性上具有相同值:
# vertex tagged as 'a' and 'b' should have the same property 'name'
q1 = g.execute('g.V(216172782113783808).as("a").out("knows").in("knows").as("b")\
.where("b", P.eq("a")).by("firstName")')
print(q1.all().result())
输出结果也应如下所示:
[v[216172782113783808], v[216172782113783808], v[216172782113783808]]
此外,如果您希望顶点'a'和'b'具有相同的输出度,可以这样编写:
# vertex tagged as 'a' and 'b' should have the output degree
q1 = g.execute('g.V(216172782113783808).as("a").out("knows").in("knows").as("b")\
.where("b", P.eq("a")).by(out().count())')
print(q1.all().result())
输出应如下所示:
# v[216172782113784171] is not v[216172782113783808](the source vertex),
# but have the same output degree
[v[216172782113783808], v[216172782113783808], v[216172782113783808], v[216172782113784171]]
dedup()¶
假设您想了解有多少论坛拥有来自印度的成员,您可以在GIE中编写以下Gremlin语句:
# Aim to count how may forums have members from India
q1 = g.execute('g.V().hasLabel("place").has("name", "India")\
.in("isPartOf").in("isLocatedIn").in("hasMember").count()')
print(q1.all().result())
输出是[8248]。看起来没有问题。但是,图中有多少个论坛?
# Count how many forums are there in the graph
q1 = g.execute('g.V().hasLabel("forum").count()')
print(q1.all().result())
输出结果为[8101]。这里出现了一个问题:拥有印度会员的论坛数量竟然超过了论坛总数。这显然不可能!实际上,当我们通过之前的图遍历统计拥有印度会员的论坛数量时,最后一步.in(\'hasMember\')会导致大量重复计数,因为不同用户加入同一个论坛的情况非常普遍。
因此,dedup()步骤被设计用来去除遍历器中的重复项。当我们在count()之前添加dedup()步骤后,统计结果就恢复正常了:
# Count how may forums have members from India
q1 = g.execute('g.V().hasLabel("place").has("name", "India")\
.in("isPartOf").in("isLocatedIn").in("hasMember").dedup().count()')
print(q1.all().result())
输出结果为 [2822]。
对于包含as()步骤的Gremlin语句,我们还可以通过dedup(TAG)根据特定标记的实体来去重。例如,以下Gremlin语句同样统计有多少论坛拥有来自印度的成员,但遍历器是从forum开始的。为了去除重复项,我们使用dedup('a'),其中'a'是标记的forum顶点。
# Count how may forums have members from India
q1 = g.execute('g.V().hasLabel("forum").as("a").out("hasMember")\
.out("isLocatedIn").out("isPartOf").has("name", "India")\
.dedup("a").count()')
print(q1)
输出结果也是 [2822]。
此外,我们可以使用dedup(TAG1, TAG2, ...)来通过多个标记条目的组合去除重复项。例如,下面的Gremlin语句用于统计图中存在多少不同的关联(country, forum)对。
# Count how many different related (country, forum) pairs in the graph
q1 = g.execute('g.V().hasLabel("place").as("a")\
.in("isPartOf").in("isLocatedIn")\
.in("hasMember").as("b").dedup("a", "b").count()')
print(q1.all().result())
输出结果为 [37164]
此外,就像之前介绍的where()步骤一样,我们可以在dedup(...)步骤后添加by(...)来根据属性值去重。例如,如果您想统计图中persons有多少个不同的firstNames,可以在GIE中这样编写:
# Count how many different firstName are there in the graph
q1 = g.execute('g.V().hasLabel("person").dedup().by("firstName").count()')
print(q1)
输出应为 [432]。
项目步骤¶
id() 和 label()¶
在之前的教程中,我们已经介绍了如何使用values(PROPERTY)将实体(顶点或边)投影到其属性值。然而,要提取全局ID和标签,我们可能需要分步操作:id()和label():
# Get vertex(id=72057594037927936) ’s id
q1 = g.execute('g.V(72057594037927936).id()')
# Get vertices(label='person')'s label
q2 = g.execute('g.V().hasLabel("person").label()')
print(q1.all().result())
print(q2.all().result())
输出应如下所示:
# ID
[72057594037927936]
# Labels
[3, ..., 3]
请注意,id()步骤并不等同于values("id"),后者是将实体(顶点或边)投影到其本地ID!例如:
# Get vertex(id=72057594037927936) ’s local id
q1 = g.execute('g.V(72057594037927936).values("id")')
print(q1.all().result())
输出结果为 [0]
constant()¶
constant() 步骤用于将任何对象映射到一个固定的对象值。例如:
# Map all the vertices to 1
q1 = g.execute('g.V().constant(1)')
# Map all the vertices to "marko"
q2 = g.execute('g.V().constant("marko")')
# Map all the vertices to 1.0
q3 = g.execute('g.V().constant(1.0)')
print(q1.all().result())
print(q2.all().result())
print(q3.all().result())
输出应如下所示:
# 1s
[1, ..., 1]
# "markos"
["marko", ..., "marko"]
# 1.0s
[1.0, ..., 1.0]
valueMap()¶
value(PROPERTY) 步骤只能将实体(顶点或边)映射到其某个属性的值。如果我们想同时提取多个属性的值该怎么办?
valueMap(PROPERTY1, PROPERTY2, ...) 步骤提供了这种能力。例如,以下代码通过valueMap(...)步骤同时提取person顶点的firstName和lastName属性。
# Extract person vertices' firstName and lastName
q1 = g.execute('g.V().hasLabel("person").valueMap("firstName", "lastName")')
print(q1.all().result())
输出应如下所示:
[{'lastName': ['Donati'], 'firstName': ['Luigi']}, {'lastName': ['Hall'], 'firstName': ['S. N.']},
......
, {'lastName': ['Costa'], 'firstName': ['Carlos']}, {'lastName': ['Khan'], 'firstName': ['Meera']}]
select()¶
如前所述,对于包含as()步骤的Gremlin语句,我们可以使用select(TAG1, TAG2, ...)步骤来提取所需的标记实体。就像where()和dedup()步骤一样,我们可以在其后添加by(...)步骤来进一步提取标记实体的属性值。
例如,以下Gremlin语句选取了标记为'a'的顶点的firstName属性。
# Extract tagged 'a' vertex's firstName
q1 = g.execute('g.V().hasLabel("person").as("a").select("a").by("firstName")')
print(q1.all().result())
输出应如下所示:
['Mahinda', 'Eli', 'Joseph',... ]
实际上,by(...)步骤内的内容可以是一个子遍历。例如,以下Gremlin语句提取标记为'a'的顶点的出度。
# Extract tagged 'a' vertex's out degree
q1 = g.execute('g.V().hasLabel("person").as("a").select("a").by(out().count())')
print(q1.all().result())
输出应如下所示:
[36, 12, 94, 228, ...]
此外,如果您想提取标记顶点的多个属性值,可以在by(...)步骤中嵌入valueMap(TAG1, TAG2, ...)步骤。例如:
# Extract tagged 'a' vertex's firstName and lastName
q1 = g.execute('g.V().hasLabel("person").as("a").select("a").by(valueMap("firstName", "lastName"))')
print(q1.all().result())
输出应如下所示:
[{'lastName': ['Donati'], 'firstName': ['Luigi']}, {'lastName': ['Hall'], 'firstName': ['S. N.']}, ..., ]
此外,如果您想在包含多个标签的select(TAG1, TAG2, ...)步骤后添加by()步骤,假设标签数量为\(n\),您应该在select(...)步骤后添加\(n\)个by()步骤,其中第\(i^{th}\)个by()步骤对应第\(i^{th}\)个标签。例如,如果您想提取顶点'a'的firstName和顶点'b'的lastName,可以这样编写:
# extract vertex 'a's firstName and vertex 'b's lastName
q1 = g.execute('g.V().hasLabel("person").as("a").out("knows").as("b")\
.select("a", "b").by("firstName").by("lastName")')
print(q1.all().result())
输出应如下所示:
[{'a': 'Luigi', 'b': 'Dom Pedro II'}, {'a': 'Luigi', 'b': 'Condariuc'}, {'a': 'Luigi', 'b': 'Bonomi'}, ..., ]
请注意,如果select(TAG1, TAG2...)步骤包含\(n\)个标签且后面跟随by()步骤,那么必须有\(n\)个by()步骤。对于某些标记实体,即使我们不想将它们投影到任何内容,也必须为它们提供一个对应的空by()步骤。例如,以下代码仅将顶点'b'投影到其lastName属性,而保持顶点'a'不变。
# extract vertex 'a' and vertex 'b's lastName
q1 = g.execute('g.V().hasLabel("person").as("a").out("knows").as("b")\
.select("a", "b").by().by("lastName")')
print(q1.all().result())
输出应如下所示:
[{'a': v[216172782113783808], 'b': 'David'}, {'a': v[216172782113783809], 'b': 'Silva'}, {'a': v[216172782113783809], 'b': 'Guliyev'},..., ]
聚合步骤¶
fold()¶
Gremlin语句总是生成包含多个遍历器的输出。fold()步骤可以将这些遍历器汇总成一个聚合列表。例如:
# Roll up the TagClass vertices into an aggregate list
q1 = g.execute('g.V().hasLabel("tagClass").fold()')
print(q1.all().result())
输出应如下所示:
[[v[504403158265495552], v[504403158265495553], v[504403158265495554], v[504403158265495555], v[504403158265495556], v[504403158265495557], v[504403158265495558], v[504403158265495559], v[504403158265495560], v[504403158265495561], v[504403158265495562], v[504403158265495563], v[504403158265495564], v[504403158265495565], v[504403158265495566], v[504403158265495567], v[504403158265495568], v[504403158265495569], v[504403158265495570], v[504403158265495571], v[504403158265495572], v[504403158265495573], v[504403158265495574], v[504403158265495575], v[504403158265495576], v[504403158265495577], v[504403158265495578], v[504403158265495579], v[504403158265495580], v[504403158265495581], v[504403158265495582], v[504403158265495583], v[504403158265495584], v[504403158265495585], v[504403158265495586], v[504403158265495587], v[504403158265495588], v[504403158265495589], v[504403158265495590], v[504403158265495591], v[504403158265495592], v[504403158265495593], v[504403158265495594], v[504403158265495595], v[504403158265495596], v[504403158265495597], v[504403158265495598], v[504403158265495599], v[504403158265495600], v[504403158265495601], v[504403158265495602], v[504403158265495603], v[504403158265495604], v[504403158265495605], v[504403158265495606], v[504403158265495607], v[504403158265495608], v[504403158265495609], v[504403158265495610], v[504403158265495611], v[504403158265495612], v[504403158265495613], v[504403158265495614], v[504403158265495615], v[504403158265495616], v[504403158265495617], v[504403158265495618], v[504403158265495619], v[504403158265495620], v[504403158265495621], v[504403158265495622]]]
sum(), min(), max() 和 mean()¶
假设您已经将遍历器投影为一系列数字,您可以使用sum()、min()、max()和mean()步骤对这些值进行进一步分析。例如:
# Get the sum of all vertex's out degree
q1 = g.execute('g.V().as("a").select("a").by(out().count()).sum()')
# Get the minimum of vertex's out degree
q2 = g.execute('g.V().as("a").select("a").by(out().count()).min()')
# Get the maximum of vertex's out degree
q3 = g.execute('g.V().as("a").select("a").by(out().count()).max()')
# Get the mean of vertex's out degree
q4 = g.execute('g.V().as("a").select("a").by(out().count()).mean()')
print(q1.all().result())
print(q2.all().result())
print(q3.all().result())
print(q4.all().result())
输出应如下所示:
[787074]
[0]
[690]
[4.134313148716225]
group()¶
根据某些值或条件将遍历器划分为不同组是一个非常常见的需求,这些分组依据称为组键。Gremlin提供了group()步骤来实现这一目的。
分组键应包含在group()步骤之后的by()步骤中。分组键可以是以下任意一种:
属性名称
生成单个值的子遍历语句
例如,如果你想根据person顶点的gender属性将它们划分为male组和female组,可以这样编写:
# Divide person vertices into male group and female group according to their gender
q1 = g.execute('g.V().hasLabel("person").group().by("gender")')
print(q1.all().result())
输出应如下所示:
[{'male': [v[216172782113783808], v[216172782113783811], v[216172782113783813], ...]
{'female': [..., v[216172782113784707], v[216172782113784708], v[216172782113784709]]]
您还可以根据顶点的出度将它们分组,使具有相同出度的顶点属于同一组:
# divide the vertices into groups according to their out degrees
q1 = g.execute('g.V().hasLabel("person").group().by(out().count())')
print(q1.all().result())
[{11: [v[216172782113783910], v[216172782113784104], v[216172782113784207], v[216172782113784318]],
24: [v[216172782113784305], v[216172782113784597], v[216172782113784693], v[216172782113784018], v[216172782113784092], v[216172782113784108], v[216172782113784161], v[216172782113784162]],
73: [v[216172782113783875], v[216172782113783932], v[216172782113784057], v[216172782113784068] ......}]
如果在group()步骤后没有跟随by()步骤,或者by()步骤的内容为空会怎样?面对这种情况,GIE会按当前值(例如顶点本身)对遍历器进行分组:
# Group the vertices by the vertex itself
q1 = g.execute('g.V().hasLabel("person").group()') # same for 'g.V().hasLabel("person").group().by()'
print(q1.all().result())
输出应如下所示:
[{v[216172782113784065]: [v[216172782113784065]], v[216172782113784235]: [v[216172782113784235]], v[216172782113784247]: [v[216172782113784247]], ..., }]
因此,强烈建议在group()步骤后,于by()步骤中添加有意义的组键。
groupCount()¶
有时,我们只关心组内有多少个实体。因此,没有必要使用group()步骤来获取包含所有实体的完整分组。
我们可以简单地使用groupCount()步骤来实现这个目的。groupCount()步骤的用法与group()步骤几乎相同,但它返回的是计数而不是完整的分组。例如,以下代码计算图中有多少男性和女性。
# Count the number of males and females
q1 = g.execute('g.V().hasLabel("person").groupCount().by("gender")')
print(q1.all().result())
输出应如下所示:
[{'male': 449, 'female': 454}]
排序步骤¶
默认情况下,GIE中Gremlin语句的输出是无序的。因此,Gremlin提供了order()步骤来对遍历到当前点的所有对象进行排序,然后按排序后的顺序逐个输出。
与group()步骤相同,排序键应放置在order()步骤之后的by()步骤中。如果没有by()步骤或by()步骤内容为空,则将使用默认的排序设置:
使用当前值作为排序键
升序排列
例如:
# Order person vertices by default
q1 = g.execute('g.V().hasLabel("person").order()')
print(q1.all().result())
输出应如下所示:
[v[216172782113783808], v[216172782113783809], v[216172782113783810], v[216172782113783811], v[216172782113783812], ......, ]
与 group() 步骤相同,您可以添加
属性名称或
生成单个值的子遍历语句
作为排序键传递给by()步骤。此外,您可以在by()步骤的第二个参数中指定排序是ascending(升序)还是descending(降序)。
例如:
# Order person vertices by their first names, ascending
q1 = g.execute('g.V().hasLabel("person").order().by("firstName", asc)') # asc is optional
# Order person vertices by their first names, descending
q2 = g.execute('g.V().hasLabel("person").order().by("firstName",desc)')
# Order person vertices by their out degree, ascending
q3 = g.execute('g.V().hasLabel("person").order().by(out().count(), asc)') #asc is optional
# Order person vertices by their out degree, descending
q4 = g.execute('g.V().hasLabel("person").order().by(out().count(), desc)')
print(q1.all().result())
print(q2.all().result())
print(q3.all().result())
print(q4.all().result())
输出应如下所示:
[v[216172782113784082], v[216172782113784169], v[216172782113784267], v[216172782113784368], ..., ]
[v[216172782113784376], v[216172782113783938], v[216172782113784405], v[216172782113783980], ..., ]
[v[216172782113783844], v[216172782113783901], v[216172782113783935], v[216172782113784439], ..., ]
[v[216172782113784315], v[216172782113784374], v[216172782113784601], v[216172782113784431], ..., ]
限制步骤¶
Gremlin语句总是会生成大量遍历器,但有时我们只需要其中的一部分。因此,Gremlin提供了limit()步骤来根据通过流的对象数量筛选遍历中的对象,其中只允许保留由limit参数定义的前n个对象。
例如,如果你想从图中提取10个人物顶点,可以这样编写:
# Extract 10 person vertices
q1 = g.execute('g.V().hasLabel("person").limit(10)')
print(q1.all().result())
输出应如下所示:
[v[216172782113783808], v[216172782113783809], v[216172782113783810], v[216172782113783811], v[216172782113783812], v[216172782113783813], v[216172782113783814], v[216172782113783815], v[216172782113783816], v[216172782113783817]]
limit() 步骤通常与 order() 步骤一起使用来选择 top(k) 顶点。例如,如果你想选择度数最大的10个人,可以在GIE中这样编写:
# Extract 10 person vertices with the largest number of degrees
q1 = g.execute('g.V().hasLabel("person").order().by(both().count(), desc).limit(10)')
print(q1.all().result())
输出应如下所示:
[v[216172782113784601], v[216172782113784315], v[216172782113784011], v[216172782113784374], v[216172782113783971], v[216172782113784431], v[216172782113784333], v[216172782113784154], v[216172782113784381], v[216172782113783933]]
表达式(语法糖)¶
目前,当我们需要将多个谓词同时应用于遍历器时,操作仍然较为复杂。
例如,如果你想查找所有在2012-01-01之后创建的male性别persons,可能需要使用两个has(...)步骤。第一个has()步骤保留所有gender属性值为male的persons,第二个has()步骤则过滤掉所有creationDate属性值早于2012-01-01的persons。
# Find all male persons that are created after 2012-01-01
q1 = g.execute('g.V().hasLabel("person").has("gender", "male").has("creationDate", gt("2012-01-01"))')
print(q1.all().result())
如何将多个谓词组合在一个过滤操作中?GIE支持一种语法糖,**直接在过滤操作符中编写表达式**,来解决这个问题!
表达式用于表示基于属性的计算或过滤,由以下基本条目组成:
@: 当前条目的值@.name: 当前条目的name属性值@a: 条目a的值@a.name: 条目a的name属性值
并且可以基于这些条目执行相关操作,包括:
算术运算
@.age + 10 @.age * 10 (@.age + 4) / 10 + (@.age - 5)
逻辑比较
@.name == "marko" @.age != 10 @.age > 10 @.age < 10 @.age >= 10 @.weight <= 10.0
逻辑连接符
@.age > 10 && @.age < 20 @.age < 10 || @.age > 20
位操作
@.num | 2 @.num & 2 @.num ^ 2 @.num >> 2 @.num << 2
幂运算
@.num ^^ 3 @.num ^^ -3
我们可以在project(select)或filter(where)操作符中编写表达式:
过滤器:
where(expr(“…”)), 例如:g.V().where(expr("@.name == \"marko\"")) # 等同于 g.V().has("name", "marko") g.V().where(expr("@.age > 10")) # 等同于 g.V().has("age", P.gt(10)) g.V().as("a").out().where(expr("@.name == \"marko\" || (@a.age > 10)"))
项目:
select(expr(“…”))g.V().select(expr("@.name")) # 等同于 g.V().values("name")
现在,要查找所有在2012-01-01之后创建的male类型persons,您只需编写:
# Find all male persons that are created after 2012-01-01
q1 = g.execute("g.V().hasLabel('person').where(expr('@.gender == \"male\" && @.creationDate > \"2012-01-01\"'))")
print(q1.all().result())
输出应如下所示:
[v[216172782113783813], v[216172782113783819], v[216172782113783826], v[216172782113783836], ..., ]
复杂查询¶
在本教程中,我们将主要讨论如何使用GIE与Gremlin语句进行一些复杂查询。我们选择的查询主要来自LDBC BI工作负载。
LDBC BI2¶
图25. LDBC BI查询2.¶
该图展示了LDBC BI2查询。此查询旨在找出在给定TagClass下,在起始日期开始的100天时间窗口内被用于Messages的Tags,并将其与随后的100天时间窗口进行比较。对于Tags和两个时间窗口,计算Messages的数量。
该查询的关键是创建两个子执行分支来分别统计两个时间窗口。我们可以使用Gremlin的by()步骤来实现这一目的。假设TagClass's name='Actor' $date='2012-01-01'、$date+100='2012-04-09'$和$date+200='2012-07-18':
首先,我们需要找到与给定
TagClass相关的所有Tagsg.V().has("tagclass", "name", "Actor").in("hasType").as("tag")接下来,我们需要统计第一个时间窗口内的数据:所有在
2012-01-01至2012-04-09期间创建且带有特定Tag的消息。由于存在两个独立的计数任务,我们必须使用select()后接by(...)步骤来创建新的子分支执行该任务。.select("tag").by(__.in("hasTag").hasLabel("comment").has("creationDate", inside("2012-01-01", "2012-04-09")).dedup().count()).as("count1")接下来,我们需要统计第二个时间窗口的数据:所有在
2012-04-09至2012-07-18期间创建且带有特定Tag的消息。我们仍然使用select()后接by(...)步骤来创建一个新的子分支执行该任务。.select("tag").by(__.in("hasTag").hasLabel("comment").has("creationDate", inside("2012-04-09", "2012-07-18")).dedup().count()).as("count2")\最后,我们从遍历器中选择
tag的 名称、count1和count2作为输出.select("tag", "count1", "count2").by("name").by().by()')
将这些步骤结合起来,我们可以在GIE中编写以下代码来实现LDBC BI2查询:
q1 = g.execute('g.V().has("tagclass", "name", "Actor").in("hasType").as("tag")\
.select("tag").by(__.in("hasTag").hasLabel("comment")\
.has("creationDate", inside("2012-01-01", "2012-04-09"))\
.dedup().count()).as("count1")\
.select("tag").by(__.in("hasTag").hasLabel("comment")\
.has("creationDate", inside("2012-04-09", "2012-07-18"))\
.dedup().count()).as("count2")\
.select("tag", "count1", "count2")\
.by("name").by().by()')
print(q1.all().result())
输出应如下所示:
# Query results for ldbc bi2 query
[{'count1': 0, 'count2': 0, 'tag': 'Jet_Li'},
{'count1': 0, 'count2': 0, 'tag': 'Zhang_Yimou'},
{'count1': 0, 'count2': 0, 'tag': 'Faye_Wong'},
{'count1': 0, 'count2': 0, 'tag': 'Tsui_Hark'},
{'count1': 3, 'count2': 7, 'tag': 'Bruce_Lee'},
{'count1': 12, 'count2': 20, 'tag': 'Johnny_Depp'},
{'count1': 6, 'count2': 4, 'tag': 'Tom_Cruise'},
{'count1': 4, 'count2': 7, 'tag': 'Jackie_Chan'}]
LDBC BI3¶
图26. LDBC BI查询3.¶
该图展示了LDBC BI3查询。给定一个TagClass和Country,此查询旨在查找在指定Country中创建的所有Forums,这些论坛至少包含一条带有直接属于给定TagClass的Tags的Message,并按包含这些消息的Forum统计Messages数量。
Forum的位置由其版主的位置决定。这里又引出一个问题:如何判断论坛是否包含与给定TagClass直接相关的消息?我们假设在以下情况下消息属于某个论坛:
它回复了论坛中的一个帖子
它回复了论坛中包含的一条消息
因此,我们可以使用Gremlin中的out(1..)路径扩展步骤来查找论坛包含的所有消息。然而,无限路径长度可能导致严重的计算开销。因此,路径扩展的上限被设置为6。接着我们可以再使用两个out('hasTag')和out('hasType')步骤,并通过过滤器has('name', TAGCLASS)来确定消息是否具有所需的标签。
假设 Country's name = 'China' 且 TagClass's name = 'Song',我们可以在GIE中这样编写:
q1 = g.execute('g.V().has("place", "name", "China").in("isPartOf").in("isLocatedIn").as("person")\
.in("hasModerator").as("forum").select("forum")\
.by(out("containerOf").in("1..6", "replyOf").endV().as("message")\
.out("hasTag").out("hasType").has("name", "Song")\
.select("msg").dedup().count()).as("message_count")\
.select("person", "forum", "message_count")\
.by("id").by(valueMap("id", "title", "creationDate")).by())')
print(q1.all().result())
输出应如下所示:
# Query results for ldbc bi3 query
[{'forum': {'id': [824633725780],
'creationDate': ['2012-01-08T02:52:34.334+0000'],
'title': ['Album 3 of Hao Wang']},
'person': 19791209300479,
'message': 0},
{'forum': {'id': [755914248304],
'creationDate': ['2012-01-02T20:35:03.344+0000'],
'title': ['Wall of Lin Lei']},
'person': 24189255811275,
'message': 0},
{'forum': {'id': [824633725045],
'creationDate': ['2012-02-03T18:15:52.633+0000'],
'title': ['Album 4 of Lin Lei']},
'person': 24189255811275,
'message': 0},
{'forum': {'id': [893353201782],
'creationDate': ['2012-03-28T23:02:53.251+0000'],
'title': ['Album 5 of Lin Lei']},
'person': 24189255811275,
'message': 0},
{'forum': {'id': [1030792152809],
'creationDate': ['2012-09-03T09:47:01.414+0000'],
'title': ['Wall of Lei Chen']},
'person': 32985348833887,
'message': 0},
...
'creationDate': ['2012-03-07T07:30:01.038+0000'],
'title': ['Album 0 of Zhang Yang']},
'person': 15393162789707,
'message': 0},
...]
LDBC BI4(左半部分)¶
图27. LDBC BI查询4(左半部分).¶
该图展示了LDBC BI4查询的左半部分。该查询旨在根据成员数量找出每个国家的前100个论坛,且这些论坛需在指定日期之后创建。
由于GIE目前不提供全局存储功能,我们无法直接使用Gremlin语句筛选出每个国家的前100个论坛。我们可以先获取一个(country, forum, country_count)的元组。
假设 Forum's creationDate > '2012-01-01',我们可以在GIE中这样编写
q1 = g.execute('g.V().hasLabel("place").as("country").in("isPartOf").in("isLocatedIn")\
.in("hasMember").as("forum").dedup("country","forum")\
.select("forum").by(out("hasMember").as("person").out("isLocatedIn")\
.out("isPartOf").where(eq("country")).select("person").dedup().count())\
.as("personCount").select("country", "forum", "personCount")')
print(q1.all().result())
输出应如下所示:
[{'forum': v[72057594037932503],
'country': v[288230376151711797],
'personCount': 2},
{'forum': v[72057594037932473],
'country': v[288230376151711797],
'personCount': 1},
{'forum': v[72057594037932489],
'country': v[288230376151711797],
'personCount': 1},
{'forum': v[72057594037932494],
'country': v[288230376151711797],
'personCount': 1},
{'forum': v[72057594037932501],
'country': v[288230376151711797],
'personCount': 2},
{'forum': v[72057594037934803],
'country': v[288230376151711797],
'personCount': 1},
{'forum': v[72057594037934804],
'country': v[288230376151711797],
'personCount': 1},
{'forum': v[72057594037935005],
'country': v[288230376151711797],
'personCount': 2},
{'forum': v[72057594037928132],
...
'personCount': 1},
{'forum': v[72057594037935879],
'country': v[288230376151711799],
'personCount': 1},
...]
由于GIE提供了Python接口,我们可以非常方便地编写代码来计算每个国家的前100个论坛:
country_top100_forums_dict = {}
for traverser in q1.all().result():
country = traverser['country']
forum = traverser['forum']
personCount = traverser['personCount']
if country in country_top100_forums_dict:
country_top100_forums_dict[country].append((forum, personCount))
else:
country_top100_forums_dict[country] = [(forum, personCount)]
for personCountForums in country_top100_forums_dict.values():
personCountForums.sort(reverse=True, key=lambda x: x[1])
if len(personCountForums) > 100:
personCountForums = personCountForums[:100]
print(country_top100_forums_dict)
LDBC BI查询4(左侧部分)的结果应如下所示:
{v[288230376151711797]: [(v[72057594037935702], 7), (v[72057594037930685], 7), (v[72057594037932114], 6), (v[72057594037934231], 5), (v[72057594037935703], 5), (v[72057594037935730], 5), (v[72057594037935734], 5), (v[72057594037932509], 4), (v[72057594037935707], 4), (v[72057594037935708], 4), (v[72057594037935711], 4), (v[72057594037935714], 4), (v[72057594037935719], 4), (v[72057594037935729], 4), (v[72057594037935731], 4), (v[72057594037935724], 4), (v[72057594037929560], 4), (v[72057594037931546], 4), (v[72057594037931545], 4), (v[72057594037932479], 3), (v[72057594037932485], 3), (v[72057594037932505], 3), (v[72057594037932511], 3), (v[72057594037935710], 3), (v[72057594037935722], 3), (v[72057594037929246], 3), (v[72057594037928266], 3), (v[72057594037935713], 3), (v[72057594037934308], 3), (v[72057594037934313], 3), (v[72057594037934302], 3), (v[72057594037930430], 3), (v[72057594037934303], 3), (v[72057594037932503], 2), (v[72057594037932482], 2), (v[72057594037935147], 2), (v[72057594037935148], 2), (v[72057594037935157], 2), (v[72057594037927973], 2), (v[72057594037929386], 2), (v[72057594037934315], 2), (v[72057594037932501], 2), (v[72057594037935005], 2), (v[72057594037935720], 2), (v[72057594037935015], 2), (v[72057594037934300], 2), (v[72057594037930861], 2), (v[72057594037934319], 2), (v[72057594037931542], 2), (v[72057594037929583], 2), (v[72057594037933324], 2), (v[72057594037929182], 2), (v[72057594037929739], 2), (v[72057594037934829], 2), (v[72057594037935658], 2), (v[72057594037935660], 2), (v[72057594037935674], 2), (v[72057594037935682], 2), (v[72057594037935683], 2), (v[72057594037935672], 2), (v[72057594037927978], 2), (v[72057594037935163], 2), (v[72057594037935014], 2), (v[72057594037931544], 2), (v[72057594037934297], 2), (v[72057594037927979], 1), (v[72057594037932473], 1), (v[72057594037932474], 1), (v[72057594037934384], 1), (v[72057594037932639], 1), (v[72057594037934380], 1), (v[72057594037935146], 1), (v[72057594037935152], 1), (v[72057594037934804], 1), (v[72057594037928134], 1), (v[72057594037928674], 1), (v[72057594037929248], 1), (v[72057594037931240], 1), (v[72057594037931247], 1), (v[72057594037931256], 1), (v[72057594037930958], 1), (v[72057594037930963], 1), (v[72057594037931259], 1), (v[72057594037931260], 1), (v[72057594037931262], 1), (v[72057594037929365], 1), (v[72057594037929419], 1), (v[72057594037929421], 1), (v[72057594037929431], 1), (v[72057594037929434], 1), (v[72057594037929437], 1), (v[72057594037929446], 1), (v[72057594037929447], 1), (v[72057594037929449], 1), (v[72057594037929450], 1), (v[72057594037935166], 1), (v[72057594037929519], 1), (v[72057594037929451], 1), (v[72057594037929452], 1), (v[72057594037935046], 1), (v[72057594037932339], 1), (v[72057594037932481], 1), (v[72057594037934294], 1), (v[72057594037932389], 1), (v[72057594037932976], 1), (v[72057594037930870], 1), (v[72057594037931266], 1), (v[72057594037928092], 1), (v[72057594037928495], 1), (v[72057594037929510], 1), (v[72057594037929517], 1), (v[72057594037929961], 1), (v[72057594037929965], 1), (v[72057594037930242], 1), (v[72057594037930248], 1), (v[72057594037930250], 1), (v[72057594037930313], 1), (v[72057594037931547], 1), (v[72057594037935686], 1), (v[72057594037929052], 1), (v[72057594037929053], 1), (v[72057594037929055], 1), (v[72057594037929057], 1), (v[72057594037929060], 1), (v[72057594037934811], 1), (v[72057594037934813], 1), (v[72057594037934816], 1), (v[72057594037934821], 1), (v[72057594037934822], 1), (v[72057594037934823], 1), (v[72057594037934828], 1), (v[72057594037934835], 1), (v[72057594037934837], 1), (v[72057594037934975], 1), (v[72057594037934976], 1), (v[72057594037935032], 1), (v[72057594037935038], 1), (v[72057594037935045], 1), (v[72057594037935664], 1), (v[72057594037935673], 1), (v[72057594037935678], 1), (v[72057594037935685], 1), (v[72057594037931549], 1), (v[72057594037931550], 1), (v[72057594037931552], 1), (v[72057594037931554], 1), (v[72057594037931557], 1), (v[72057594037931560], 1), (v[72057594037931566], 1), (v[72057594037935671], 1), (v[72057594037935670], 1), (v[72057594037934977], 1), (v[72057594037931543], 1), (v[72057594037929518], 1), (v[72057594037935679], 1), (v[72057594037931575], 1), (v[72057594037932438], 1), (v[72057594037932878], 1), (v[72057594037932886], 1), (v[72057594037932888], 1), (v[72057594037927982], 1), (v[72057594037930526], 1), (v[72057594037930535], 1), (v[72057594037930546], 1), (v[72057594037930976], 1), (v[72057594037930536], 1), (v[72057594037931358], 1), (v[72057594037930366], 1), (v[72057594037931574], 1), (v[72057594037932435], 1), (v[72057594037935020], 1), (v[72057594037935864], 1), (v[72057594037933861], 1), (v[72057594037932440], 1), (v[72057594037927981], 1), (v[72057594037931349], 1), (v[72057594037931366], 1), (v[72057594037932885], 1), (v[72057594037927980], 1), (v[72057594037930529], 1), (v[72057594037930532], 1), (v[72057594037931304], 1), (v[72057594037932433], 1), (v[72057594037932441], 1), (v[72057594037934547], 1)], ......
LDBC BI11¶
图28. LDBC BI查询11.¶
该图展示了LDBC BI11查询。此查询旨在查找被标记为a、b、c的三个人物,其中:
a、b、c 彼此认识
a、b、c 居住在同一个国家
他们的关系('knows')是在[
startDate,endDate]时间范围内创建的
使用Gremlin的match(...)步骤来描述这个查询非常简单。假设Country's name=China,startDate='2012-01-01',endDate='2012-07-01':
q1 = g.execute('g.V().match(__.as("a").bothE("knows")\
.has("creationDate", inside("2012-01-01", "2012-07-01"))\
.otherV().as("b"),\
__.as("a").bothE("knows")\
.has("creationDate", inside("2012-01-01", "2012-07-01"))\
.otherV().as("c"),\
__.as("a").bothE("knows")\
.has("creationDate", inside("2012-01-01", "2012-07-01"))\
.otherV().as("c"),\
__.as("a").out("isLocatedIn").out("isPartOf").as("d"),\
__.as("b").out("isLocatedIn").out("isPartOf").as("d"),\
__.as("c").out("isLocatedIn").out("isPartOf").as("d"),\
__.as("d").has("name", "China").as("d"))\
.select("a", "b", "c")')
print(q1.all().result())
输出应如下所示:
# Query results for ldbc bi11 query
[{'a': v[216172782113784091], 'b': v[216172782113783882], 'c': v[216172782113784250]}, ......,
{'a': v[216172782113784403], 'b': v[216172782113784537], 'c': v[216172782113784122]}]
LDBC BI14¶
图29. LDBC BI查询14.¶
该图展示了LDBC BI14查询。以下是其用途:
考虑所有满足以下条件的人对 (person1, person2):
他们彼此认识,
一个位于
country1国家的城市中,另一个位于
country2国家的某个城市。
对于国家country1的每个城市,返回得分最高的配对。配对的得分定义为以下交互类型的子分数总和。初始值为score = 0。
person1对person2发布的至少一条Message创建了回复Comment:分数 += 4person1创建了至少一条Message且person2对其进行了回复:分数 += 1person1至少点赞过一条person2发布的Message:分数 += 10person1创建了至少一条被person2点赞的Message:分数 += 1
假设 country1's name = "India" 且 country2's name = "Chile",我们可以在GIE中这样编写:
q1 = g.execute('g.V().has("place", "name", "India")\
.in("isPartOf").in("isLocatedIn").as("p1")\
.both("knows").as("p2").out("isLocatedIn").out("isPartOf")\
.has("name", "Chile")\
.select("p1").by(in("hasCreator").hasLabel("comment").out("replyOf")\
.out("hasCreator").where(eq("p2")).select("p1").dedup().count()).as("case1")\
.select("p1").by(in("hasCreator").in("replyOf").hasLabel("comment")\
.out("hasCreator").where(eq("p2")).select("p1").dedup().count()).as("case2")\
.select("p1").by(out("likes").hasLabel("post", "comment")\.out("hasCreator")\\
.where(eq("p2"))\.select("p1").dedup().count()).as("case3")\
.select("p1").by(in("hasCreator").hasLabel("post", "comment").in("likes")\
.where(eq("p2")).select("p1").dedup().count()).as("case4")\
.select(expr("@case1 * 4 + @case2 * 1 + @case3 * 10 + @case4 * 1")).as("score")\
.select("p1", "p2", "score")')
print(q1.all().result())
LDBC BI14查询的输出结果应如下所示:
# Query results for ldbc bi14 query
[{'p1': v[216172782113784153], 'score': 10, 'p2': v[216172782113784100]},
{'p1': v[216172782113783956], 'score': 10, 'p2': v[216172782113784673]},
{'p1': v[216172782113784252], 'score': 10, 'p2': v[216172782113784263]},
{'p1': v[216172782113784193], 'score': 0, 'p2': v[216172782113784100]},
{'p1': v[216172782113784242], 'score': 6, 'p2': v[216172782113784263]},
{'p1': v[216172782113784623], 'score': 11, 'p2': v[216172782113784498]},
{'p1': v[216172782113784251], 'score': 5, 'p2': v[216172782113784538]},
{'p1': v[216172782113784167], 'score': 11, 'p2': v[216172782113784100]},
{'p1': v[216172782113783864], 'score': 12, 'p2': v[216172782113784100]},
{'p1': v[216172782113784613], 'score': 5, 'p2': v[216172782113784116]},
{'p1': v[216172782113784481], 'score': 16, 'p2': v[216172782113784498]},
{'p1': v[216172782113784481], 'score': 16, 'p2': v[216172782113784100]}]