1.前言
随着社交、电商、金融、零售、物联网等行业的快速发展,现实社会织起了了一张庞大而复杂的关系网,传统数据库很难处理关系运算。
大数据行业需要处理的数据之间的关系随数据量呈几何级数增长,亟需一种支持海量复杂数据关系运算的数据库,图数据库应运而生。
1.1、什么是图数据库
1.1.1、什么视图
图由两个元素组成:节点和关系。
每个节点代表一个实体(人,地,事物,类别或其他数据),每个关系代表两个节点的关联方式。这种通用结构可以对各种场景进行建模 - 从道路系统到设备网络,到人口的病史或由关系定义的任何其他事物。
1.1.2、什么是图数据库
图数据库(Graph database)并非指存储图片的数据库,而是以图这种数据结构存储和查询数据。
图形数据库是一种在线数据库管理系统,具有处理图形数据模型的创建,读取,更新和删除(CRUD)操作。
与其他数据库不同,关系在图数据库中占首要地位。这意味着应用程序不必使用外键或带外处理(如MapReduce)来推断数据连接。
与关系数据库或其他NoSQL数据库相比,图数据库的数据模型也更加简单,更具表现力。
图形数据库是为与事务(OLTP)系统一起使用而构建的,并且在设计时考虑了事务完整性和操作可用性。
1.1.3、两个重要属性
根据存储和处理模型不同,市面上图数据库也有一些区分。比如:
Neo4J就是属于原生图数据库,它使用的后端存储是专门为Neo4J这种图数据库定制和优化的,理论上说能更有利于发挥图数据库的性能。
而JanusGraph不是原生图数据库,而将数据存储在其他系统上,比如Hbase。
1.1.3.1、图存储
一些图数据库使用原生图存储,这类存储是经过优化的,并且是专门为了存储和管理图而设计的。并不是所有图数据库都是使用原生图存储,也有一些图数据库将图数据序列化,然后保存到关系型数据库或者面向对象数据库,或其他通用数据存储中。
1.1.3.2、图处理引擎
原生图处理(也称为无索引邻接)是处理图数据的最有效方法,因为连接的节点在数据库中物理地指向彼此。非本机图处理使用其他方法来处理CRUD操作。
1.2、对比
1.2.1、与NoSQL数据对比
| 分类 | 数据模型 | 优势 | 劣势 | 举例 |
|---|---|---|---|---|
| 键值数据库 | 哈希表 | 查找速度快 | 数据无结构化,通常只被当作字符串或者二进制数据 | Redis |
| 列存储数据库 | 列式数据存储 | 查找速度快; 支持分布横向扩展; 数据压缩率高 | 功能相对受限 | HBase |
| 文档型数据库 | 键值对扩展 | 数据结构要求不严格; 表结构可变; 不需要预先定义表结构 | 查询性能不高,缺乏统一的查询语法 | MongoDB |
| 图数据库 | 节点和关系组成的图 | 利用图结构相关算法(最短路径、节点度关系查找等) | 可能需要对整个图做计算,不利于图数据分布存储 | Neo4j、JanusGraph |
1.2.2、与关系型数据库对比
关系型数据库实际上是不擅长处理关系的。很多场景下,你的业务需求完全超出了当前的数据库架构。
当我们要查询:“用户购买了那些商品?” 或者 “该商品有哪些客户购买过?” 需要开发人员JOIN几张表,效率非常低下。
而“购买该产品的客户还购买了哪些商品?”类似的查询几乎不可能实现。
试图在一个社交网络里找到最大深度为5的朋友的朋友。他们的数据集包括100万人,每人约有50个朋友。实验结果如下:
| 深度 | MySQL执行时间(S) | Neo4J执行时间(S) | 返回记录数 |
|---|---|---|---|
| 2 | 0.016 | 0.01 | ~2500 |
| 3 | 30.267 | 0.168 | ~110 000 |
| 4 | 1543.505 | 1.359 | ~600 000 |
| 5 | 未完成 | 2.132 | ~800 000 |
1.3、 Neo4J和JanusGraph
1.3.1、Neo4J
Neo4J是由Java实现的开源图数据库。自2003年开始开发,直到2007年正式发布第一版,并托管于GitHub上。
目前Neo4J分为社区版和企业版,社区版只支持单机部署,功能受限。企业版支持主从复制和读写分离,包含可视化管理工具。
1.3.2、JanusGraph
JanusGraph是一个Linux基金会下的开源分布式图数据库。
JanusGraph支持多种储存后端(包括Apache Cassandra、Apache HBase、Bigtable、Berkeley DB)。JanusGraph的可扩展性取决于与JanusGraph一起使用的基础技术。例如,通过使用Apache Cassandra作为存储后端,可以将JanusGraph简单地扩展到多个数据中心。
JanusGraph通过与大数据平台(Apache Spark,Apache Giraph,Apache Hadoop)集成,支持全局图数据的分析、报告和ETL。
JanusGraph通过外部索引存储(Elasticsearch,Solr,Lucene)支持地理、数字范围和全文搜索。
1.4、Cypher图查询语言 和 Gremlin查询语言
Cypher是Neo4j的图形查询语言,允许用户存储和检索图形数据库中的数据。
Gremlin语言是图数据库查询语言,相当于SQL之于关系型数据库。
2.图基本概念与操作
Gremlin语言是图数据库查询语言,相当于SQL之于关系型数据库。
2.1、图剧本概念
-
图Graph:指关系图。比如:同学及朋友关系图、银行转账图等。
-
顶点Vertex:一般指实体。比如:人、账户等。
-
边Edge:一般指关系。比如:朋友关系、转账动作等。
-
属性Property:顶点或边可以包含属性,比如:人的姓名、人的年龄、转账的时间。
:查询顶点,一般作为图查询的第1步。
// 查询图中所有的顶点
// 注意:g 代表的是整个图
// 一切查询都是以图开始
g.V()
// 根据id查询顶点
g.V('4:Gremlin', '3:TinkerPop')
- E():查询边,一般作为图查询的第1步。
// 查询图中所有的边
g.E()
// 根据id查询边
g.E('S3:TinkerPop>4>>S4:Gremlin')
- id():获取顶点、边的id。
// 查询所有顶点的id
g.V().id()
类似的,通过
g.E().id()查询所有边的id。
注意:Gremlin Step是作用在上一步产生的结果集上,如果上一步的结果是多个元素,那么这里id()将返回多个元素的id
- label():获取顶点、边的label。
// 查询所有顶点的label
g.V().label()
类似的,通过g.E().label()查询所有边的label。
- properties():获取顶点、边的属性。
g.V().properties()
// 查询所有顶点的“lang”属性
// 如果无“lang”属性的顶点将跳过
g.V().properties('lang')
类似的,通过g.E().properties()查询所有边的属性。
此外 properties()还可以和 key()、value()搭配使用,以获取属性的名称或值。
// 查询所有顶点的属性名称
g.V().properties().key()
// 查询所有顶点的属性值
g.V().properties().value()
- valueMap():获取顶点、边的属性, valueMap()与 properties()不同的地方是:它们返回的结构不一样,后者将所有的属性扁平化到一个大列表里面,一个元素代表一个属性;前者保持一个顶点或一条边的属性作为一组,每一组由若干属性的键值对组成。
// 查询所有顶点的属性
g.V().valueMap()
类似的,通过g.E().valueMap()查询所有边的属性。
- values():获取顶点、边的属性值。
// 查询所有顶点的属性值
// 效果等同于:
// g.V().properties().value()
g.V().values()
// 查询所有顶点的“lang”属性
// 效果等同于:
// g.V().properties('lang').value()
g.V().values('lang')
类似的,通过g.E().values()查询所有边的属性值。
3.边的遍历操作
3.1、边遍历概念
边遍历是指通过顶点来访问与其有关联边的邻接顶点(或者仅访问邻接边),边遍历是图数据库与图计算的核心。

3.1.1、顶点为基准
- out(label): 根据指定的EdgeLabel来访问顶点的OUT方向邻接点(可以是零个EdgeLabel,代表所有类型边;也可以一个或多个EdgeLabel,代表任意给定EdgeLabel的边,下同)
- in(label): 根据指定的EdgeLabel来访问顶点的IN方向邻接点
- both(label): 根据指定的EdgeLabel来访问顶点的双向邻接点
- outE(label): 根据指定的EdgeLabel来访问顶点的OUT方向邻接边
- inE(label): 根据指定的EdgeLabel来访问顶点的IN方向邻接边
- bothE(label): 根据指定的EdgeLabel来访问顶点的双向邻接边
3.1.2、边为基准
- outV(): 访问边的出顶点(注意:这里是以边为基准,上述Step均以顶点为基准),出顶点是指边的起始顶点
- inV(): 访问边的入顶点,入顶点是指边的目标顶点,也就是箭头指向的顶点
- bothV(): 访问边的双向顶点
- otherV(): 访问边的伙伴顶点,即相对于基准顶点而言的另一端的顶点
3.2、实例讲解
- out():访问顶点的OUT方向邻接点
// 先查询图中所有的顶点
// 然后访问顶点的OUT方向邻接点
// 注意:out()的基准必须是顶点
g.V().out()
// 访问某个顶点的OUT方向邻接点
// 注意'3:TinkerPop'是顶点的id
// 该id是插入顶点时自动生成的
g.V('3:TinkerPop').out()
目前讲解过的Gremlin Steps中,顶点的id可通过g.V()来获取,也可通过即将讲解的has()来获取(根据属性查询顶点)。
// 访问某个顶点的OUT方向邻接点
// 且限制仅“define”类型的边相连的顶点
g.V('3:TinkerPop').out('define')
- in():访问顶点的IN方向邻接点
// 访问某个顶点的IN方向邻接点
g.V('3:TinkerPop').in()
// 访问某个顶点的IN方向邻接点
// 且限制了关联边的类型
g.V('3:TinkerPop').in('implements')
- both():访问顶点的双向邻接点
// 访问某个顶点的双向邻接点
g.V('3:TinkerPop').both()
// 访问某个顶点的双向邻接点
// 且限制了关联边的类型
g.V('3:TinkerPop').both('implements', 'define')
- outE(): 访问顶点的OUT方向邻接边
// 访问某个顶点的OUT方向邻接边
g.V('3:TinkerPop').outE()
// 访问某个顶点的OUT方向邻接边
// 且限制了关联边的类型
g.V('3:TinkerPop').outE('define')
- inE(): 访问顶点的IN方向邻接边
// 访问某个顶点的IN方向邻接边
g.V('3:TinkerPop').inE()
// 访问某个顶点的IN方向邻接边
// 且限制了关联边的类型
g.V('3:TinkerPop').inE('implements')
- bothE(): 访问顶点的双向邻接边
// 访问某个顶点的双向邻接边
g.V('3:TinkerPop').bothE()
// 访问某个顶点的双向邻接边
// 且限制了关联边的类型
g.V('3:TinkerPop').bothE('define', 'implements')
- outV(): 访问边的出顶点
// 访问某个顶点的IN邻接边
// 然后获取边的出顶点
g.V('3:TinkerPop').inE().outV()
一般情况下,inE().outV()等价于in()
- inV(): 访问边的入顶点
// 访问某个顶点的OUT邻接边
// 然后获取边的入顶点
g.V('3:TinkerPop').outE().inV()
一般情况下,outE().inV()等价于out()
- bothV(): 访问边的双向顶点
// 访问某个顶点的OUT邻接边
// 然后获取边的双向顶点
g.V('3:TinkerPop').outE().bothV()
注意:bothV()会把源顶点也一起返回,因此只要源顶点有多少条出边,结果集中就会出现多少次源顶点
- otherV() : 访问边的伙伴顶点
// 访问某个顶点的OUT邻接边
// 然后获取边的伙伴顶点
g.V('3:TinkerPop').outE().otherV()
一般情况下,outE().otherV()等价于out(),inE().otherV()等价于in()
// 访问某个顶点的双向邻接边
// 然后获取边的伙伴顶点
g.V('3:TinkerPop').bothE().otherV()
一般情况下,bothE().otherV()等价于both()
3.3、综合运用
- 多度查询
// 4度out()查询
// 通过id找到“javeme”作者顶点
// 通过out()访问其创建的软件
// 继续通过out()访问软件实现的框架
// 继续通过out()访问框架包含的软件
// 继续通过out()访问软件支持的语言
g.V('javeme').out('created').out('implements').out('contains').out('supports')
- 查询支持Gremlin语言的软件的作者
// 通过id找到“Gremlin”语言顶点
// 通过in()访问支持Gremlin的软件
// 继续通过in()访问软件的作者
g.V('4:Gremlin').in('supports').in('created')
- 查询某个作者的共同作者
// 通过id找到“javeme”作者顶点
// 通过out()访问其创建的软件
// 通过in()访问软件的所有作者
g.V('javeme').out('created').in('created')
4.Has条件过滤
4.1、说明
在众多Gremlin的语句中,有一大类是filter类型,顾名思义,就是对输入的对象进行条件判断,只有满足过滤条件的对象才可以通过filter进入下一步。
has语句是filter类型语句的代表,能够以顶点和边的属性作为过滤条件,决定哪些对象可以通过。has语句包括很多变种:
- hasLabel(labels…): object的label与labels列表中任何一个匹配就可以通过
- hasId(ids…): object的id满足ids列表中的任何一个就可以通过
- has(key, value): 包含属性“key=value”的object通过,作用于顶点或者边
- has(label, key, value): 包含属性“key=value”且label值匹配的object通过,作用于顶点或者边
- has(key, predicate): 包含键为key且对应的value满足predicate的object通过,作用于顶点或者边
- hasKey(keys…): object的属性键包含所有的keys列表成员才能通过,作用于顶点属性
- hasValue(values…): object的属性值包含所有的values列表成员才能通过,作用于顶点属性
- has(key): 包含键为key的属性的object通过,作用于顶点或者边
- hasNot(key): 不包含键为key的属性的object通过,作用于顶点或者边
4.2、实例讲解
在JanusGraph中,按property的值查询之前,应该对property建立索引,否则将无法查到结果并引发异常。
4.2.1、hasLabel(label...),通过label来过滤顶点或边,满足label列表中一个即可通过
// 查询label为"person"的顶点
g.V().hasLabel('person')
// 查询label为"person"或者"software"的顶点
g.V().hasLabel('person', 'software')
4.2.2、hasId(ids…),通过id来过滤顶点或者边,满足id列表中的一个即可通过
// 查询id为"zhoney"的顶点
g.V().hasId('zhoney')
// 查询id为“zhoney”或者“3:HugeGraph”的顶点
g.V().hasId('zhoney', '3:HugeGraph')
4.2.3、has(key, value),通过属性的名字和值来过滤顶点或边
// 查询“addr”属性值为“Beijing”的顶点
g.V().has('addr', 'Beijing')
4.2.4、has(label, key, value),通过label和属性的名字和值过滤顶点和边
// 查询label为“person”且“addr”属性值为“Beijing”的顶点
g.V().has('person', 'addr', 'Beijing')
4.2.5、has(key, predicate),通过对指定属性用条件过滤顶点和边
// 查询“addr”属性值为“Beijing”的顶点
g.V().has('age', gt(20))
4.2.6、hasKey(keys…): properties包含所有的key才能通过
// 查询包含属性“lang”的顶点
g.V().properties().hasKey('lang')
特殊用法:直接将hasKey()作用于顶点,仅后端是Cassandra时支持
// 查询包含属性“age”的顶点
g.V().hasKey('age')
4.2.7、hasValue(values…): properties包含所有的value才能通过
// 查询包含属性值“Beijing”的顶点
g.V().properties().hasValue('Beijing')
特殊用法:直接将hasValue()作用于顶点,仅后端是Cassandra时支持
// 查询包含属性值“Beijing”的顶点
g.V().hasValue('Beijing')
4.2.8、has(key): 有这个属性的通过
// 查询包含属性“age”的顶点
g.V().has('age')
4.2.9、hasNot(key): 没有这个属性的通过
// 查询没有属性“age”的顶点
g.V().hasNot('age')
5.图查询返回结果数限制
5.1、说明

-
Gremlin能统计查询结果集中元素的个数,且允许从结果集中做范围截取。假设某个查询操作(如:
g.V())的结果集包含8个元素,我们可以从这8个元素中截取指定部分。主要包括: -
count(): 统计查询结果集中元素的个数;
-
range(m, n): 指定下界和上界的截取,左闭右开。比如range(2, 5)能获取第2个到第4个元素(0作为首个元素,上界为-1时表示剩余全部);
-
limit(n): 下界固定为0,指定上界的截取,等效于range(0, n),语义是“获取前n个元素”。比
-
limit(3)能获取前3个元素;
-
tail(n): 上界固定为-1,指定下界的截取,等效于range(count - n, -1),语义是“获取后n个元素”。比如tail(2)能获取最后的2个元素;
-
skip(n): 上界固定为-1,指定下界的截取,等效于range(n, -1),语义是“跳过前n个元素,获取剩余的元素”。比如skip(6)能跳过前6个元素,获取最后2个元素。
5.2、实例讲解
5.2.1、 count():查询当前traverser中的元素的个数,元素可以是顶点、边、属性、路径等。
//查询图中所有顶点的个数
g.V().count()
//查询图中类型为“人person”的顶点数
g.V().hasLabel('person').count()
//查询图中所有的 “人创建created” 的边数
g.V().hasLabel('person').outE('created').count()
//查询图中所有顶点的属性数
g.V().properties().count()
5.2.2、 range():限定查询返回的元素的范围,上下界表示元素的偏移量,左闭右开。下界以“0”作为第一个元素,上界为“-1”时表示取到最后的元素。
//不加限制地查询所有类型为“人person”的顶点
g.V().hasLabel('person').range(0, -1)
//查询类型为“人person”的顶点中的第2个到第5个
g.V().hasLabel('person').range(2, 5)
//查询类型为“人person”的顶点中的第5个到最后一个
g.V().hasLabel('person').range(5, -1)
5.2.3、 limit():查询前“n”个元素,相当于range(0, n)
//查询前两个顶点
g.V().limit(2)
//查询前三条边
g.E().limit(3)
5.2.4、 tail():与limit()相反,它查询的是后“n”个元素,相当于range(count - n, -1)
//查询后两个顶点
g.V().tail(2)
//查询后三条边
g.E().tail(3)
5.2.5、 skip():跳过前“n”个元素,获取剩余的全部元素
// 跳过前5个,skip(5)等价于range(5, -1)
g.V().hasLabel('person').skip(5)
6.查询路径path
6.1、说明
在使用Gremlin对图进行分析时,关注点有时并不仅仅在最终到达的顶点、边或者属性上,通过什么样的路径到达最终的顶点、边和属性同样重要。此时可以借助path()来获取经过的路径信息。
path()返回当前遍历过的所有路径。有时需要对路径进行过滤,只选择没有环路的路径或者选择包含环路的路径,Gremlin针对这种需求提供了两种过滤路径的step:simplePath()和cyclicPath()。
6.2、实例讲解
6.2.1、path(),获取当前遍历过的所有路径
// “JanusGraph”顶点到与其有直接关联的顶点的路径(仅包含顶点)
g.V().hasLabel('software').has('name','HugeGraph').both().path()
如果想要同时获得经过的边的信息,可以用bothE().otherV()替换both()
// “JanusGraph”顶点到与其有直接关联的顶点的路径(包含顶点和边)
g.V().hasLabel('software')
.has('name','HugeGraph')
.bothE()
.otherV()
.path()
输出路径的时候,通过by(property)语句可以指定对象的某个属性代替对象,且连续的多个by()是循环应用到路径中的对象,例如路径中有3个对象[A, B, C],by(X).by(Y)语句指定两个属性[X Y],代表用“用A的X属性代表A,用B的Y属性代表B,用C的X属性代表C”,具体可参考如下例子:
// “HugeGraph”顶点到与其有直接关联的顶点的路径(包含顶点和边)
// 用“name”属性代表person和software顶点,用“weight”属性代表边
g.V().hasLabel('software')
.has('name','HugeGraph')
.bothE()
.otherV()
.path()
.by('name')
.by('weight')
路径分为两种:有环路径和无环路径。
- 有环路径是指路径中至少有一个对象出现的次数大于等于两次。
- 无环路径是指路径中所有的对象只出现一次。
path()返回所有路径,包含有环路径和无环路径,例如:
// “HugeGraph”顶点到与其有两层关系的顶点的所有路径(只包含顶点)
g.V().hasLabel('software')
.has('name','HugeGraph')
.both()
.both()
.path()
6.2.2、simplePath(),过滤掉路径中含有环路的对象,只保留路径中不含有环路的对象
// “HugeGraph”顶点到与其有两层关系的顶点的不含环路的路径(只包含顶点)
g.V().hasLabel('software')
.has('name','HugeGraph')
.both()
.both()
.simplePath()
.path()
6.2.3、cyclicPath(),过滤掉路径中不含有环路的对象,只保留路径中含有环路的对象
// “HugeGraph”顶点到与其有两层关系的顶点的包含环路的路径(只包含顶点)
g.V().hasLabel('software')
.has('name','HugeGraph')
.both()
.both()
.cyclicPath()
.path()
7.循环操作
7.1、说明
循环操作是指多次执行某一部分语句,用于语句需要重复运行的场景,比如“查找朋友的朋友的朋友”,可以直接使用循环操作来完成即“查找3层朋友”,下面对具体的循环相关的Step进行说明:
- repeat()
: 指定要重复执行的语句,如repeat(out(‘friend’)) - times()
: 指定要重复执行的次数,如执行3次repeat(out(‘friend’)).times(3) - until(): 指定循环终止的条件,如一直找到某个名字的朋友为止
- repeat(out(‘friend’)).until(has(‘name’,‘xiaofang’))
- emit(): 指定循环语句的执行过程中收集数据的条件,每一步的结果只要符合条件则被收集,不指定条件时收集所有结果
- loops(): 当前循环的次数,可用于控制最大循环次数等,如最多执行3次
- repeat(out(‘friend’)).until(loops().is(3))
7.2、实例讲解
7.2.1、 repeat() + times():按照指定的次数重复执行语句
// 访问某个顶点的OUT邻接点(1次)
// 注意'okram'是顶点的id
g.V('okram').repeat(out()).times(1)
// 访问某个顶点的2度双向邻接点
// 访问第1个顶点的所有邻接点(第1层)
// 再访问第1层结果顶点的邻接点(第2层)
g.V('okram').repeat(both()).times(2)
// 访问某个顶点的3度OUT邻接点
// 访问第1个顶点的所有邻接点(第1层)
// 再访问第1层结果顶点的邻接点(第2层)
// 再访问第2层结果顶点的邻接点(第3层)
g.V('okram').repeat(out()).times(3)
7.2.2、 repeat() + until():根据条件来重复执行语句
// 查询顶点'okram'到顶点'Gremlin'之间的路径
// 循环的终止条件是遇到名称是'Gremlin'的顶点
g.V('okram')
.repeat(out())
.until(has('name', 'Gremlin'))
.path()
注意1:这里用到了path()来获取经过的路径,path的讲解请参考上一期。
注意2:until()与 times()是互斥的,两个语句无法同时存在于同一个循环中。
注意3:until()放在repeat()之前或之后的顺序是会影响逻辑的,放前面表示先判断再执行,放后面表示先执行后判断。请对比如下两个语句的执行结果:
g.V(‘okram’).repeat(out()).until(hasLabel(‘person’)).path()
g.V(‘okram’).until(hasLabel(‘person’)).repeat(out()).path()
7.2.3、 repeat() + emit():收集执行过程中的数据
// 查询顶点'okram'的所有OUT可达点的路径
g.V('okram')
.repeat(out())
.emit()
.path()
// 查询顶点'okram'的所有OUT可达点的路径
// 且必须满足是'person'类型的点
g.V('okram')
.repeat(out())
.emit(hasLabel('person'))
.path()
注意:emit()放在repeat()之前或之后的顺序是会影响结果的,放前面表示先收集再执行,放后面表示先执行后收集。请对比如下两个语句的执行结果:
g.V(‘okram’).repeat(out()).emit(hasLabel(‘person’)).path()
g.V(‘okram’).emit(hasLabel(‘person’)).repeat(out()).path()
// 查询顶点'okram'到顶点'Gremlin'之间的路径
// 此外还收集过程中的'person'类型的顶点
g.V('okram')
.repeat(out())
.until(has('name', 'Gremlin'))
.emit(hasLabel('person'))
.path()
注意:
emit()与until()搭配使用时,是“或”的关系而不是“与”的关系,满足两者间任意一个即可。
// 查询顶点'okram'的2度OUT可达点的路径
// 此外还收集'person'类型的顶点
g.V('okram')
.repeat(out()).times(2)
.emit(hasLabel('person'))
.path()
注意:
emit()与times()搭配使用时,是“或”的关系而不是“与”的关系,满足两者间任意一个即可。
7.2.4、 repeat() + loops():根据最大次数限制来重复执行语句
// 查询顶点'okram'的3度OUT可达点路径
g.V('okram')
.repeat(out())
.until(loops().is(3))
.path()
// 查询顶点'okram'到顶点'Gremlin'之间的路径
// 且之间只相差2跳的距离
// 其中的and()是指两个条件都满足
g.V('okram')
.repeat(out())
.until(has('name', 'Gremlin')
.and().loops().is(2))
.path()
7.3、综合运用
7.3.1、 查找子树
// 查找从一个节点出发,到
// 叶子节点结束的所有路径
// 这些路径的集合为一颗子树(子图)
g.V('okram')
.repeat(out())
.until(outE().count().is(0))
.path()
7.3.2、 查找两点之间的最短路径
// 已知两个顶点'okram'和'javeme',
// 通过任意关系来找到这两点之间的路径
// 且限制了最大深度为3
// 若存在那么第一条结果即是最短路径
g.V('okram')
.repeat(bothE().otherV().simplePath())
.until(hasId('javeme').and().loops().is(lte(3)))
.hasId('javeme')
.path()
注意:
bothE().otherV()一般等价于both(),但是在这里有一些差别,后者仅仅返回路径中的顶点信息,前者会把路径中的边信息也返回。
8.查询结果排序
8.1、说明
Gremlin允许对查询的结果进行排序输出,可以指定按某个属性的升序、降序或是随机序的方式输出。排序方式可以通过单独的order()或者order().by(...)指定,而by() step又有一些变种,下面分别讲解order()和order().by(...)的用法。
8.1.1、 单独使用order() Step,一般用于遍历器中的元素是属性时:
- order()会将结果以升序输出;
order()单独使用时,必须保证遍历器(traverser)中的元素是可排序的,在 java 里就是必须实现
java.lang.Comparable接口,否则会抛出异常。
8.1.2、 联合使用order().by(...) Step,传入排序方式,一般用于遍历器中的元素是属性时:
order().by(incr): 将结果以升序输出,这也是默认的排序方式;order().by(decr): 将结果以降序输出;order().by(shuffle): 将结果以随机序输出,每次执行结果顺序都可能不一样。
使用 order().by(…) step 但是 by() 传递的仅是一个排序方式的参数时,也必须保证遍历器(traverser)中的元素是可排序的。
8.1.3、 联合使用order().by(...) Step,传入属性和排序方式,用于遍历器中的元素是顶点或边时:
- order().by(key): 将结果按照元素属性key的值升序排列,与order().by(key, incr)等效;
- order().by(key, incr): 将结果按照元素属性key的值升序排列;
- order().by(key, decr): 将结果按照元素属性key的值降序排列;
- order().by(key, shuffle): 将结果按照元素属性key的值随机序排列,每次执行结果顺序都可能不一样。
by()step不是一个真正的step,而是一个“step modulator”,与此类似的还有as()和option()step。通过by()step可以为某些step添加traversal、function、comparator等,通常的使用方式是step().by()…by(),某些step只能添加一个by(),而有一些可以添加任意数量的by()step。
8.2、实例讲解
8.2.1、 order(),使用默认的排序(升序)输出
// 以默认排序输出所有顶点的"name"属性值
g.V().values('name').order()
8.2.2、 order().by(incr),指定以升序输出
// 以升序输出所有顶点的"name"属性值
g.V().values('name').order().by(incr)
8.2.3、 order().by(decr),指定以降序输出
// 以降序输出所有顶点的"name"属性值
g.V().values('name').order().by(decr)
8.2.4、 order().by(shuffle),指定以随机序输出
// 以随机序输出所有顶点的"name"属性值
g.V().values('name').order().by(shuffle)
8.2.5、 order().by(key),按照元素属性key的值升序(默认)排列
// 将"person"类型的顶点按照"age"升序(默认)排列输出
g.V().hasLabel('person').order().by('age')
为了使"age"属性排列显示得更清晰,我们取出顶点的"age"属性
// 将"person"类型的顶点按照"age"升序(默认)排列,并获取"age"属性
g.V().hasLabel('person').order().by('age').values('age')
8.2.6、 order().by(key, incr),按照元素属性key的值升序排列
// 将"person"类型的顶点按照"age"升序排列,并获取"age"属性
g.V().hasLabel('person').order().by('age', incr).values('age')
8.2.7、 order().by(key, desc),按照元素属性key的值降序排列
// 将"person"类型的顶点按照"age"降序排列输出,并获取"age"属性
g.V().hasLabel('person').order().by('age', decr).values('age')
8.2.8、 order().by(key, shuffle),按照元素属性key的值随机序排列
// 将"person"类型的顶点按照"age"随机序排列输出,并获取"age"属性
g.V().hasLabel('person').order().by('age', shuffle).values('age')
9.数据分组与去重
Gremlin支持对数据进行分组和去重。
数据分组是指:从某个维度上对拥有相同点的数据进行分组,比如根据年龄分组、根据出生省份分组等。
数据去重是指:去除结果集中相同的元素,或者去除在某个维度上具有相同点的数据,比如根据年龄选出一些代表,每个年龄最多只能有一个人。
下面讲解实现上述功能的具体Step:
- group(): 对结果集进行分组,可通过by(property)来指定根据什么维度进行分组,可称维度为分组键;如果不指定维度则以元素id作为分组键,相当于重复的元素被分为一组。每一组由分组键+组内元素列表构成。如果有需要也可对每一组的元素列表进行reduce操作,依然使用by()语句,如by(count())对组内元素计数。
- groupCount(): 对结果集进行分组,并统计每一组的元素个数。每一组由分组键+组内元素数量构成。
- dedup(): 去除结果集中相同的元素,可通过by(property)来指定根据什么维度进行去重。
- by(): 语义上一般指“根据什么维度”,与上述语句配合使用,如group().by()、dedup().by()等。也可与其它语句配合,如前面讲到的排序order().by()及路径path().by()等。
9.1、实例讲解
group():对结果集进行分组
// 不指定任何维度进行分组
g.V().hasLabel('person').group()
// 不指定任何维度进行分组
// 但数据集中有重复的元素
// 重复的元素将会被分为一组
g.V().both().hasLabel('person').group()
// 根据年龄进行分组
g.V().hasLabel('person').group().by('age')
// 根据年龄进行分组
// 并统计各个年龄的人数
g.V().hasLabel('person')
.group()
.by('age')
.by(count())
// 根据顶点类别进行分组
// 并统计各个类别的数量
g.V().group().by(label).by(count())
groupCount():对结果集进行分组计数
// 不指定任何维度进行分组计数
g.V().hasLabel('person').groupCount()
// 不指定任何维度进行分组计数
// 但数据集中有重复的元素
// 重复的元素将会被分为一组
g.V().both().hasLabel('person').groupCount()
// 根据年龄进行分组计数
g.V().hasLabel('person')
.groupCount()
.by('age')
dedup():去除结果集中重复的元素
// 对一组含有重复顶点的数据进行去重
g.V().both().hasLabel('person').dedup()
// 查看所有人当中有哪几种年龄
// 人之间的年龄是可能有重复的,
// 通过dedup去除掉重复的年龄
g.V().hasLabel('person')
.values('age')
.dedup()
// 从各个年龄的人中选出一个代表
g.V().hasLabel('person').dedup().by('age')
9.2、综合运用
- 获得各个地方人们的平均年龄
// 根据地域分组,并得到各个组的平均年龄
g.V().hasLabel('person')
.group()
.by('addr')
.by(values('age').mean())
- 统计顶点的边数量的分布情况
// 拥有相同数量边的顶点作为一组
// 并获取每一组的顶点数量
// 结果相当于:拥有m条边的顶点有n个
g.V().groupCount().by(bothE().count())
10.条件和过滤
10.1、条件和过滤操作说明
在对图进行遍历分析时,经常需要对满足一定条件的对象进行过滤。where()就是用来过滤遍历过程中当前阶段的对象。另一方面,predicate就是过滤时使用的判断条件,包括关系运算和区间判断等,只有满足判断条件的对象才能通过进入下一轮或者作为结果。
where()常与select()或者match()配合使用,也可以单独使用。
以下是predicate的说明:
| 谓语 | 说明 |
|---|---|
| eq(object) | 传入的对象等于目标object |
| neq(object) | 传入的对象不等于目标object |
| lt(number) | 传入的数字小于目标number |
| lte(number) | 传入的数字小于或等于目标number |
| gt(number) | 传入的数字大于目标number |
| gte(number) | 传入的数字大于或等于目标number |
| inside(low,high) | 传入的数字大于low且小于high |
| outside(low,high) | 传入的数字小于low或者大于high |
| between(low,high) | 传入的数字大于等于low且小于high |
| within(objects…) | 传入的对象等于目标对象列表objects中的任意一个 |
| without(objects…) | 传入的对象不等于目标对象列表objects中的任何一个 |
逻辑运算and()、or()或者not()作用于
predicate会产生一个新的predicate
在众多的Gremlin steps中,有一大类是filter step,通过判断是否满足predicate来决定对象能否通过filter step。filter()语句是filter step的基础,较为抽象,而更加具体的where()语句就是一个典型的filter step。
10.2、实例说明
10.2.1、predicate可以通过test()来获得boolean值
可以用test()测试value是否满足predicate,以下是一些例子:
// (3 == 2)
eq(2).test(3)
// ('d' == 'a' || 'd' == 'b' || 'd' == 'c')
within('a','b','c').test('d')
// (3 > 1 && 3 < 4)
inside(1,4).test(3)
10.2.2、and/or/not作用于predicate之后成为新的predicate
// not()作用于neq(),等价于eq()
not(neq(2))
// and()连接的predicate,是一个新的predicate
within(1,2,3).and(not(eq(2))).test(3)
// or()连接的predicate,是一个新的predicate
inside(1,4).or(eq(5)).test(3)
10.2.3、where()单独使用
where()有三种使用方式:
where(P)where(String, P)where(Traversal)
// 查看“zhoney”的合作伙伴
// where(P)方式
g.V('zhoney').as('a')
.out('created').in('created')
.where(neq('a'))
// 查看“zhoney”的合作伙伴
// where(String, P)方式
g.V('zhoney').as('a')
.out('created').in('created').as('b')
.where('a',neq('b'))
// “spmallette”开发过不止一个软件的合作伙伴
// where(Traversal)方式
g.V('spmallette').out('created').in('created')
.where(out('created').count().is(gt(1)))
.values('name')
// 查询”被别人认识“
// 且认识自己的人的年龄大于自己的年龄的人
g.V().as('a')
.out('knows').as('b')
.where('a', gt('b')).by('age')
10.2.4、where()与as()+select()配合使用
as()可以为某一阶段的对象添加标签,select()则可以通过标签获取对象。因此as()+select()可以在某个step处得到历史信息
// 查看“zhoney”的合作伙伴,并将“zhoney”及其合作伙伴的名字以map输出
// select().where()方式
g.V('zhoney').as('a')
.out('created').in('created').as('b')
.select('a','b').by('name')
.where('a',neq('b'))
10.2.5、where()与match()配合使用
match()可以保证满足某种模式的对象通过
// 查看“zhoney”的合作伙伴,并将“zhoney”及其合作伙伴的名字以map输出
// match().where()方式
g.V('zhoney').match(
__.as('a').out('created').as('b'),
__.as('b').in('created').as('c')).where('a', neq('c')
).select('a','c').by('name')
10.2.6、filter()
filter()有三种用法:
- lambda方式,filter{it.get()…}
- Traversal方式,filter(Traversal)
- 特定filter step方式
// 查找图中的“person”顶点
// lambda方式
g.V().filter {it.get().label() == 'person'}
// 查找图中的“person”顶点
// Traversal方式
g.V().filter(label().is('person'))
// 查找图中的“person”顶点
// 特定filter step方式
g.V().hasLabel('person')
11.逻辑运算
11.1、说明
Gremlin支持在遍历器上加上逻辑运算进行过滤,只有满足该逻辑条件的元素才会进入下一个遍历器中。
下面讲解实现上述功能的具体Step:
is():可以接受一个对象(能判断相等)或一个判断语句(如:P.gt()、P.lt()、P.inside()等),当接受的是对象时,原遍历器中的元素必须与对象相等才会保留;当接受的是判断语句时,原遍历器中的元素满足判断才会保留,其实接受一个对象相当于P.eq();and():可以接受任意数量的遍历器(traversal),原遍历器中的元素,只有在每个新遍历器中都能生成至少一个输出的情况下才会保留,相当于过滤器组合的与条件;or():可以接受任意数量的遍历器(traversal),原遍历器中的元素,只要在全部新遍历器中能生成至少一个输出的情况下就会保留,相当于过滤器组合的或条件;not():仅能接受一个遍历器(traversal),原遍历器中的元素,在新遍历器中能生成输出时会被移除,不能生成输出时则会保留,相当于过滤器的非条件。
这四种逻辑运算Step除了像一般的Step写法以外,
and()和or()还可以放在where()中以中缀符的形式出现。
11.2、实例讲解
11.2.1、 is()
// 筛选出顶点属性“age”等于28的属性值,与`is(P.eq(28))`等效
g.V().values('age').is(28)
当没有任何一个顶点的属性“age”为28时,输出为空。
// 筛选出顶点属性“age”大于等于28的属性值
g.V().values('age').is(gte(28))
// 筛选出顶点属性“age”属于区间(27,29)的属性值
g.V().values('age').is(inside(27, 29))
P.inside(a, b)是左开右开区间(a,b)
// 筛选出由两个或两个以上的人参与创建(“created”)的顶点
// 注意:这里筛选的是顶点
g.V().where(__.in('created').count().is(gt(2))).values('name')
where()Step可以接受一些过滤条件
// 筛选出有创建者(“created”)的年龄(“age”)在20~29之间的顶点
g.V().where(__.in('created').values('age').is(between(20, 29))).values('name')
11.2.2、 and(),逻辑与
// 所有包含出边“supports”的顶点的名字“name”
g.V().and(outE('supports')).values('name')
// 所有包含出边“supports”和“implements”的顶点的名字“name”
g.V().and(outE('supports'), outE('implements')).values('name')
// 包含边“created”并且属性“age”为28的顶点的名字“name”
g.V().and(outE('created'), values('age').is(28)).values('name')
上面的缀符写法
// 包含边“created”并且属性“age”为28的顶点的名字“name”
g.V().where(outE('created')
.and()
.values('age').is(28))
.values('name')
11.2.3、 or(),逻辑或
// 所有包含出边“supports”的顶点的名字“name”
g.V().or(outE('supports')).values('name')
只有一个条件时,
and()与or()的效果一样的。
// 所有包含出边“supports”或“implements”的顶点的名字“name”
g.V().or(outE('supports'), outE('implements')).values('name')
注意对比与
g.V().and(outE('supports'), outE('implements')).values('name')的差别
// 包含边“created”或属性“age”为28的顶点的名字“name”
g.V().or(outE('created'), values('age').is(28)).values('name')
注意对比与
g.V().and(outE('created'), values('age').is(28)).values('name')的差别
上面的 缀符写法
// 包含边“created”或属性“age”为28的顶点的名字“name”
g.V().where(outE('created')
.or()
.values('age').is(28))
.values('name')
11.2.4、 not(),逻辑非
// 筛选出所有不是“person”的顶点的“label”
g.V().not(hasLabel('person')).label()
// 筛选出所有包含不少于两条(大于等于两条)“created”边的“person”的名字“name”
g.V().hasLabel('person').not(out('created').count().is(lt(2))).values('name')
11.3、综合运用
目标:获取所有最多只有一条“created”边并且年龄不等于28的“person”顶点
// 与(含有小于等于一条“created”边,年龄不等于28)
g.V().hasLabel('person')
.and(outE('created').count().is(lte(1)),
values("age").is(P.not(P.eq(28))))
.values('name')
// 非(或(含有多于一条“created”边,年龄等于28))
g.V().hasLabel('person')
.not(or(out('created').count().is(gt(1)),
values('age').is(28)))
.values('name')
12.统计运算
12.1、说明
Gremlin可以在Number类型的流(遍历器)上做简单的统计运算,包括计算总和、最大值、最小值、均值。
下面讲解实现上述功能的具体Step:
sum():将流上的所有的数字求和;max():对流上的所有的数字求最大值;min():对流上的所有的数字求最小值;mean():将流上的所有的数字求均值;
这四种Step只能作用在Number类型的流上,在java里就是继承自
java.lang.Number类。
12.2、实例讲解
12.2.1、 sum()
// 计算所有“person”的“age”的总和
g.V().hasLabel('person').values('age').sum()
// 计算所有“person”的“created”出边数的总和
g.V().hasLabel('person').map(outE('created').count()).sum()
12.2.2、 max()
// 计算所有“person”的“age”中的最大值
g.V().hasLabel('person').values('age').max()
// 计算所有“person”的“created”出边数的最大值
g.V().hasLabel('person').map(outE('created').count()).max()
12.2.3、 min()
// 计算所有“person”的“age”中的最小值
g.V().hasLabel('person').values('age').min()
// 计算所有“person”的“created”出边数的最小值
g.V().hasLabel('person').map(outE('created').count()).min()
12.2.4、 mean()
// 计算所有“person”的“age”的均值
g.V().hasLabel('person').values('age').mean()
// 计算所有“person”的“created”出边数的均值
g.V().hasLabel('person').map(outE('created').count()).mean()
13.数学运算
13.1、说明
在Gremlin中有一个专门负责科学计算功能的step math()。 math() 不同于常见的函数组合和嵌套形式,提供了一种易于读取的基于字符串的数学处理器。
-
math()支持by(),其中多个by()按照在math()运算表达式中首次引用变量的顺序应用。 -
保留变量
_是指传入math()的当前遍历器对象。
math()支持的运算符包括:+,-,*,/,%,^
math()支持的内嵌函数包括:abs: absolute value,绝对值acos: arc cosine,反余弦asin: arc sine,反正弦atan: arc tangent,反正切cbrt: cubic root,立方根ceil: nearest upper integer,向上最接近的整数cos: cosine,余弦cosh: hyperbolic cosine,双曲余弦exp: euler’s number raised to the power (e^x),以e为底的指数floor: nearest lower integer,向下最近接的整数log: logarithmus naturalis (base e),以e为底的对数log10: logarithm (base 10),以10为底的对数log2: logarithm (base 2),以2为底的对数sin: sine,正弦sinh: hyperbolic sine,双曲正弦sqrt: square root,平方根tan: tangent,正切tanh: hyperbolic tangent,双曲正切signum: signum function,签名功能
13.2、实例讲解
(待补充…)
14.路径选取与过滤
14.1、说明
Gremlin支持从走过的路径里选取部分数据作为结果,并且可以在选取时进行条件过滤。
下面讲解实现上述功能的具体Step:
- as()+select(): 对路径中结果进行选取,首先通过as(label)对任意步骤打上标签,然后使用select(label)来选取若干历史步骤的结果作为新结果。此外还可通过select().by(property)来指定根据什么维度进行选取。
- as()+where(): 以条件匹配的方式进行路径结果选取,只有符合条件的路径才能被选取出来。
- as()+match(): 以模式匹配的方式进行路径结果选取,只有符合模式的路径才能被选取出来。
- as()+dedup(): 根据路径中的若干步骤的结果进行去重,只有首次出现的路径段才能被选取出来。
14.2、实例讲解
14.2.1、 as()...select():对路径中结果进行选取
// 从路径中选取第1步和第3步的结果作为最终结果
g.V('2:HugeGraph').as('a')
.out().as('b')
.out().as('c')
.select('a', 'c')
// 从集合中选择最后一个元素
g.V('2:HugeGraph').as("a")
.repeat(out().as("a")).times(2)
.select(last, "a")
// 通过by()来指定选取的维度
g.V('2:HugeGraph').as('a')
.out().as('b')
.out().as('c')
.select('a', 'c')
.by('name').by('name')
// 从map中选择指定key的值
g.V().valueMap().select('tag').dedup()
14.2.2、 as()...where():以条件匹配的方式进行路径结果选取
// 选取满足第1步和第3步“lang”属性相等的路径
g.V('2:HugeGraph').as('a')
.out().as('b').out().as('c')
.where('a', eq('c')).by('lang')
.select('a', 'b', 'c').by(id)
14.2.3、 as()+match():以模式匹配的方式进行路径结果选取
// 选取满足两个模式的路径:
// 1.第3步有OUT节点
// 2.第3步的OUT节点的指定路径不允许回到第二步的节点
g.V('2:HugeGraph').as('a').out().as('b')
.match(__.as('b').out().as('c'),
__.not(__.as('c').out().in('define').as('b')))
.select('a','b','c').by(id)
14.2.4、 as()+debup():路径去重
// 以路径中的前3步作为去重依据,对路径进行去重
g.V('2:HugeGraph').as('a')
.out().as('b').out().as('c').in()
.dedup('a', 'b', 'c').path()
14.3、综合运用
14.3.1、 查询支持Gremlin语言的软件,至少由2个相互认识的且在北京的作者完成
// 获取支持Gremlin语言的软件,
// 并且作者是至少为2个相互认识的人,
// 且这两个作者都在北京
g.V('3:Gremlin').in('supports').as('software')
.match(
__.as('software').in('created').as('person1'),
__.as('person1').both('knows').as('person2'),
__.as('person2').out('created').as('software'),
__.as('person1').has('addr', 'Beijing'),
__.as('person2').has('addr', 'Beijing'))
.select('software').dedup()
14.3.2、 查询支持Gremlin语言的软件的作者,并按边权重排序
// 获取支持Gremlin语言的软件,
// 并查找其作者,并对中间经过的边打标签
// 按照边的权重进行排序
// 选取软件、权重、作者作为结果
g.V('3:Gremlin').in('supports').as('s')
.inE('created').as('e').outV().as('t')
.order().by(select('e').by('weight'), decr)
.select('s', 'e', 't').by('name').by('weight')
15.分支
15.1、说明
在对图进行遍历分析时,有时需要根据某些条件对当前的对象集合进行不同的操作,也就是if-then-else语法结构。Gremlin中有一类step可以满足这种分支需求,这组step叫做branch step。branch()是这类step的基础,比较抽象,而choose()是典型的branch step。
choose()的基本使用方法有两类:
-
单独使用,choose(predicate, true-traversal, false-traversal):根据predicate判断,当前对象满足时,继续true-traversal,否则继续false-traversal
-
与option配合使用,choose(traversal).option(value1, traversal1).option(…)…:根据对象通过traversal的结果决定后续操作,如果结果是value1,则该对象继续traversal1,以此类推
option()不是一种Gremlin step,只是一种辅助语法,可与choose()配合使用
15.2、实例讲解
15.2.1、if-then-else型choose()语句
// 查找所有的“person”类型的顶点
// 如果“age”属性小于等于20,输出他的朋友的名字
// 如果“age”属性大于20,输出他开发的软件的名字
// choose(condition, true-action, false-action)
g.V().hasLabel('person')
.choose(values('age').is(lte(20)),
__.in('knows'),
__.out('created'))
.values('name')
15.2.2、option()型choose()语句
// 查找所有的“person”类型的顶点
// 如果“age”属性等于0,输出名字
// 如果“age”属性等于28,输出年龄
// 如果“age”属性等于29,输出他开发的软件的名字
// choose(predicate).option().option()...
g.V().hasLabel('person')
.choose(values('age'))
.option(0, values('name'))
.option(28, values('age'))
.option(29, __.out('created').values('name'))
15.2.3、如果choose(predicate, true-traversal, false-traversal)中false-traversal为空或者是identity(),则不满足条件的对象直接通过choose()
// 查找所有顶点,
// 类型为“person”的顶点输出其创建的软件的“name”属性
// 否则输出顶点自身的“name”属性
g.V().choose(
hasLabel('person'),
out('created'))
.values('name')
// 查找所有顶点,
// 类型为“person”的顶点输出其创建的软件的“name”属性
// 否则输出顶点自身的“name”属性
g.V().choose(
hasLabel('person'),
out('created'),
identity())
.values('name')
15.2.4、choose()和option()配合使用时,还提供了一个none,不满足其他选项的对象,执行none选项的traversal
// 查找所有类型为“person”的顶点,
// “name”属性为“Zhoney Zhang”的输出其“age”属性
// 否则输出顶点的“name”属性
g.V().hasLabel('person')
.choose(values('name'))
.option('Zhoney Zhang', values('age'))
.option(none, values('name'))
15.2.5、branch()
branch()有三种用法:
- lambda方式,filter{it.get()…}
- Traversal方式,filter(Traversal)
- 特定branch step方式
// 查询所有顶点
// “name”属性值为“HugeGraph”的顶点输出其“lang”属性
// “name”属性值不为“HugeGraph”的顶点输出其“name”属性
// lambda方式
g.V().branch {it.get().value('name')}
.option('HugeGraph', values('lang'))
.option(none, values('name'))
// 查询所有顶点
// “name”属性值为“HugeGraph”的顶点输出其“lang”属性
// “name”属性值不为“HugeGraph”的顶点输出其“name”属性
// traversal方式
g.V().branch(values('name'))
.option('HugeGraph', values('lang'))
.option(none, values('name'))
// 查询所有顶点
// “name”属性值为“HugeGraph”的顶点输出其“lang”属性
// “name”属性值不为“HugeGraph”的顶点输出其“name”属性
// 特定branch step方式
g.V().choose(
has('name','HugeGraph'),
values('lang'),
values('name'))
16.合并
16.1、说明
- coalesce: 可以接受任意数量的遍历器(traversal),按顺序执行,并返回第一个能产生输出的遍历器的结果;
- optional: 只能接受一个遍历器(traversal),如果该遍历器能产生一个结果,则返回该结果,否则返回调用optionalStep的元素本身。当连续使用.optional()时,如果在某一步返回了调用元素本身,则后续的.optional()不会继续执行;
- union: 可以接受任意数量的遍历器(traversal),并能够将各个遍历器的输出合并到一起;
16.2、实例讲解
16.2.1、 coalesce()
// 按优先级寻找到顶点“HugeGraph”的以下边和邻接点,找到一个就停止
// 1、“implements”出边和邻接点
// 2、“supports”出边和邻接点
// 3、“created”入边和邻接点
g.V('3:HugeGraph')
.coalesce(
outE('implements'),
outE('supports'),
inE('created'))
.inV()
.path()
.by('name')
.by(label)
JanusGraph这三类边都是存在的,按照优先级,返回了“implements”出边和邻接点。
// 按优先级寻找到顶点“HugeGraph”的以下边和邻接点,找到一个就停止(调换了示例1中的1和2的顺序)
// 1、“supports”出边和邻接点
// 2、“implements”出边和邻接点
// 3、“created”入边和邻接点
g.V('3:HugeGraph')
.coalesce(
outE('supports'),
outE('implements'),
inE('created'))
.inV()
.path()
.by('name')
.by(label)
这次由于“supports”放在了“implements”的前面,所以返回了“supports”出边和邻接点。
16.2.2、 optional()
// 查找顶点"linary"的“created”出顶点,如果没有就返回"linary"自己
g.V('linary').optional(out('created'))
// 查找顶点"linary"的“knows”出顶点,如果没有就返回"linary"自己
g.V('linary').optional(out('knows'))
// 查找每个“person”顶点的出“knows”顶点,如果存在,然后以出“knows”顶点为起点,继续寻找其出“created”顶点,最后打印路径
g.V().hasLabel('person')
.optional(out('knows').optional(out('created')))
.path()
结果中的后面四个顶点因为没有出“knows”顶点,所以在第一步返回了自身后就停止了。
16.2.3、 union()
// 寻找顶点“linary”的出“created”顶点,邻接“knows”顶点,并将结果合并
g.V('linary').union(out('created'), both('knows')).path()
// 寻找顶点“HugeGraph”的入“created”顶点(创作者),出“implements”和出“supports”顶点,并将结果合并
g.V('3:HugeGraph')
.union(
__.in('created'),
out('implements'),
out('supports'),
out('contains'))
.path()
17.结果聚集与展开
17.1、说明
Gremlin在路径游走的时候,可以将某一步的所有结果收集到一个集合里面(我们称之为结果聚集),以备在后续步骤中使用;此外还可在需要的时候将聚集的结果展开。
下面讲解实现上述功能的具体Step:
aggregate(): 聚集路径中指定步骤的所有结果,通过aggregate(label)对任意步骤打上标签,在此之前的步骤的结果均会被收集到此标签所代表的集合中(但并不会影响路径的游走),可配合by及cap一起使用,通过cap(label)来获取该结果集合,此外还可通过select(label)或without(label)等其它方式读取。
-
store(): 类似aggregate(),只是以Lazy的方式来收集。
-
unfold(): 将集合展开平铺,路径将扩张。
-
fold(): 将多个元素折叠为一个集合,路径将收缩。
17.2、实例讲解
17.2.1、 aggregate():聚集路径中的结果
// 收集第1步的结果到集合'x'中
// 注意:不影响后续结果
g.V('2:HugeGraph').out().aggregate('x')
// 收集第1步的结果到集合'x'中
// 并通过cap取出结果
// 与示例1比较,结果的层次更深了
g.V('2:HugeGraph').out()
.aggregate('x').cap('x')
// 通过by()来指定聚集的维度
g.V('2:HugeGraph').out()
.aggregate('x').by('name')
.cap('x')
17.2.2、 store():以Lazy的方式来收集结果
// 以Lazy方式收集,后续步骤使用limit限制时,
// 路径中取到第2个结果时将会停止,
// 因此集合中有2个元素。
g.V().store('x').by('name').limit(1).cap('x')
17.2.3、 unfold():以把集合展开、平铺
// 将集合‘x’展开(层级变少了)
g.V('2:HugeGraph').out()
.aggregate('x').by('name')
.cap('x').unfold()
17.2.4、 fold():将元素折叠为集合
// 将属性折叠起来(层级变深)
g.V('2:HugeGraph').out()
.values('name').fold()
// 统计所有'name'属性的长度
// 其中通过lambuda表达式累加字符串长度
g.V('2:HugeGraph').out().values('name')
.fold(0) {a,b -> a + b.length()}
17.3、综合运用
17.3.1、 查询一个软件的同类别软件,但不包括自身在内
// 查询与HugeGraph类似支持Gremlin语言的软件
// 但不包含自身和一步邻居
// 比较:请看看去除where语句的效果
g.V('2:HugeGraph').aggregate('x')
.out().aggregate('x')
.out().in()
.where(without('x'))
17.3.2、 查询2度之内的所有邻居的名称
// 查
询与HugeGraph的两度OUT邻居
// 并收集这些到‘a’集合里面,
// 最终以‘name’属性展示其邻居
g.V('2:HugeGraph').out().aggregate('a')
.out().aggregate('a').cap('a')
.unfold().values('name')
17.3.3、 查询由多人合作的软件及其各作者的名称
// 查询所有由3个以上作者完成的软件
// 并显示它的名称及其作者
g.V().as('software', 'authors')
.where(__.in('created').count().is(gte(3)))
.select('software', 'authors')
.by('name')
.by(__.in('created').values('name').fold())
18.模式匹配
18.1、说明
Gremlin中的match()语句为图查询提供了一种基于“模式匹配”的方式,以便用更具描述性的方式进行图查询。match()语句通过多个模式片段traversal fragments来进行模式匹配。这些traversal fragments中会定义一些变量,只有满足所有用变量表示的约束的对象才能够通过,并被放到一个Map<String, Object>中,其中map的key为变量名(label),value为顶点、边、路径或者属性。match()语句的格式为:match(Traversal…)。其中可以有任意多个Traversal,每一个Traversal就是一个“匹配模式”traversal fragment。
match()语句中的“模式”通过MatchAlgorithm来选择匹配顺序,默认的MatchAlgorithm是CountMatchAlgorithm。CountMatchAlgorithm根据过滤强度动态调整“模式匹配”的执行计划(最能够减少规模的“模式”优先匹配),从而优化执行减少资源消耗。因此,当图的规模比较大且用户不知道满足特定模式的数据规模时,使用match()可以自动进行优化,减小操作规模。另外,对于一些图查询场景,相较于单路径遍历,match()语句的“模式匹配”更容易表达需求。
18.2、实例说明
match()语句通过模式匹配生成map
// 对每一个顶点,用以下模式去匹配,满足则生成一个map<String, Object>,不满足则过滤掉
// 模式1:“a”对应当前顶点,且创建了软件“HugeGraph”
// 模式2:“b”对应顶点软件“HugeGraph”
// 模式3:“c”对应创建软件“HugeGraph”的年龄为29的person顶点
g.V().match(__.as('a').out('created').has('name', 'HugeGraph').as('b'),
__.as('b').in('created').has('age', 29).as('c'))
match()语句可以与select()语句配合使用,从Map中选取部分结果
// 对每一个顶点,用以下模式去匹配,满足则生成一个map<String, Object>,不满足则过滤掉
// 模式1:“a”对应当前顶点,且创建了软件“HugeGraph”
// 模式2:“b”对应顶点软件“HugeGraph”
// 模式3:“c”对应创建软件“HugeGraph”的年龄为29的person顶点
// 并选取map中的“a"和”c",对应的对象以”name“属性的值代替
g.V().match(__.as('a').out('created').has('name', 'HugeGraph').as('b'),
__.as('b').in('created').has('age', 29).as('c'))
.select('a', 'c').by('name')
match()语句可以与where()语句配合使用,过滤结果
// 对每一个顶点,用以下模式去匹配,满足则生成一个map<String, Object>,不满足则过滤掉
// 模式1:“a”对应当前顶点,且创建了软件“HugeGraph”
// 模式2:“b”对应顶点软件“HugeGraph”
// 模式3:“c”对应创建软件“HugeGraph”的年龄为29的person顶点
// 模式4:”a“和”c“对应的对象不相等
// 并选取map中的“a"和”c",对应的对象以”name“属性的值代替
g.V().match(__.as('a').out('created').has('name', 'HugeGraph').as('b'),
__.as('b').in('created').has('age', 29).as('c'))
.where('a', neq('c'))
.select('a', 'c').by('name')
match()语句中可以使用外部的label
// 对每一个顶点打标签”a“,"a"经过一条OUT方向的”knows“边到达的顶点打标签”b“
// 对”b“中的每一个顶点用以下模式去匹配,满足则生成一个map<String, Object>,不满足则过滤掉
// 模式1:”b“通过一条OUT方向的”created“边到达顶点”c“
// 模式2:”c“不能通过一条IN方向的”created“边到达”a“
// 选取map中的“a",”b“,”c",对应的对象以”name“属性的值代替
g.V().as('a').out('knows').as('b')
.match(__.as('b').out('created').as('c'),
__.not(__.as('c').in('created').as('a')))
.select('a','b','c').by('name')
注意
match(__.as('b').out('created').as('c')中的__.as(b)是读取label标识的对象,as('c')是为新的对象打上label标识
19.随机过滤与注入
19.1、随机过滤说明
Gremlin支持对遍历器(traversal)上的结果进行采样或者做随机过滤。
- sample: 接受一个整数值,从前一步的遍历器中采样(随机)出最多指定数目的结果;
- coin: 字面意思是抛硬币过滤,接受一个浮点值,该浮点值表示硬币出现正面的概率。coin Step 对前一步的遍历器中的每个元素都抛一次硬币,出现正面则可以通过,反面则被拦截。
sampleStep后能接上byStep,能以指定的属性为判断依据进行随机过滤。
19.2、注入说明
Gremlin允许在遍历器中注入一些默认值或自定义值,比如在分支 Step 中给 else 路径的元素一个默认值,又或者在遍历器过程中人为地加上一些额外的元素。
Inject Step
- constant: 通常用在choose或coalesceStep中做辅助输出,为那些不满足条件的元素提供一个默认值;
- inject: 能够在流(遍历器)的任何位置注入与当前遍历器同输出类型的对象,当然,也可以作为流的起始 Step 产生数据;
inject只是在查询过程中添加一些额外的元素,并没有把数据真正地插入到图中
19.3、实例讲解
19.3.1、 sample()
// 从所有顶点的出边中随机选择2条
g.V().outE().sample(2)
由于
sample是随机采样,所以运行结果每次都可能不一样。另外,sample(n)表示最多采样n个,如果上一步不够n个元素自然结果是会小于n的。
// 从所以顶点的“name”属性中随机选取3个
g.V().values('name').sample(3)
// 从所有的“person”中根据“age”随机选择3个
g.V().hasLabel('person').sample(3).by('age')
与local联合使用做随机漫游(从某个顶点出发,随机选一条边,走到边上的邻接点;再以该点为起点,继续随机选择边,走到邻接点…)
// 从顶点“HugeGraph”出发做3次随机漫游
g.V('3:HugeGraph')
.repeat(local(bothE().sample(1).otherV()))
.times(3)
.path()
19.3.2、 coin()
// 每个顶点按0.5的概率过滤
g.V().coin(0.5)
// 每个顶点按0.0的概率过滤
g.V().coin(0.0)
// 每个顶点按1.0的概率过滤
g.V().coin(1.0).count()
避免输出太长,加上
count。
19.3.3、 constant()
// 输出所有“person”类顶点的“name”属性,否则输出“inhuman”(非人类)
g.V().choose(hasLabel('person'),
values('name'),
constant('inhuman'))
// 与示例1功能相同,使用“coalesce”Step 实现
g.V().coalesce(hasLabel('person').values('name'),
constant('inhuman'))
19.3.4、 inject()
// 给顶点“HugeGraph”的作者添加一个叫“Tom”的人
g.V('3:HugeGraph').in('created').values('name').inject('Tom')
// 在示例1的基础上计算每个元素的长度(“name”属性值的长度)
g.V('3:HugeGraph').in('created').values('name').inject('Tom')
.map {it.get().length()}
// 在示例2的基础上计算走过的路径
g.V('3:HugeGraph').in('created').values('name').inject('Tom')
.map {it.get().length()}.path()
// 使用inject创建出两个元素(顶点的id),并使用该元素作为id获取顶点及其属性“name”
inject('javeme', 'linary', 'zhoney').map {g.V(it.get()).next()}.values('name')
// 使用inject创建出一个“person”(顶点label),并使用该元素作为label获取顶点及其属性“name”
inject('person').flatMap {g.V().hasLabel(it.get())}.values('name')
20.结果存取口袋sack
20.1、说明
Gremlin在路径遍历的时候,可以将中间结果存放到一个叫口袋(sack)的结构里面,以备在后续步骤中使用;此外在放入数据到口袋的时候,还可以做一些灵活的操作比如:分裂(split)、合并(merge)等。sack相关step属于Gremlin语言里面的高级操作,在处理较为复杂的任务时可以灵活的实现一些特殊功能。
下面讲解实现上述功能的具体Step:
- withSack(): 创建一个口袋,并给定一个初始结构,比如可以是一个返回Map或者随机数的lambda函数,也可以是一个对象或常数;另外还可以提供lambda分裂函数,以指定当traverser分裂时的行为,比如进行clone操作。
- sack(): 将数据放入口袋,或者从口袋取出数据。当传入lambda合并函数作为参数时,可指定放入口袋的行为如何执行;当不传入参数时表示读取口袋中的内容。
20.2、实例讲解
withSack()…sack(): 利用口袋来存取结果
// 创建一个包含常数1的口袋,
// 并且在最终取出口袋中的值
g.withSack(1).V().sack()
// 创建一个包含随机常数的口袋,
// 并且在最终取出口袋中的值
g.withSack{new Random().nextFloat()}
.V().sack()
// 通过sum求和的方式把数据放入口袋
g.withSack(0).V()
.repeat(outE().sack(sum).by('weight').inV())
.times(3).sack()
// 通过以下gremlin查看路径及其权重:
g.withSack(0).V()
.repeat(outE().sack(sum).by('weight').inV())
.times(3).path().by().by('weigh')
// 通过lambda函数来指定放入口袋的行为
// 注意:提供的初始值为Map类型,而且
// 当traverser分裂时会拷贝Map
g.withSack{[:]}{it.clone()}
.V().out().out().dedup()
.sack{m,v -> m[v.value('name')] = v.value('lang'); m}
.sack()
// 平均获取口袋中的值
g.withSack(1.0).V('javeme')
.out('knows').out('created')
.barrier(normSack).sack()
20.3、综合应用
20.3.1、 获取路径并计算路径权重之和
// 获取路径的同时通过sack(sum)计算权重之和
// 最终通过select把权重和路径选取出来
g.withSack(0).V()
.repeat(outE().sack(sum).by('weight').inV().as('p'))
.times(3).sack().as('w')
.select('w', 'p').by().by{p->p.toString()}.limit(3)
20.3.2、 获取路径并根据路径权重之和排序
// 获取路径的同时通过sack(sum)计算权重之和
// 最终通过order().by(sack())根据总权重排序
g.withSack(0).V()
.repeat(outE().sack(sum).by('weight').inV())
.times(3).order().by(sack(),decr)
.path().limit(3)
21.遍历栅栏barrier
21.1、说明
Gremlin在路径遍历的时候,可以将栅栏barrier插入到懒加载的遍历流水线中,以使得barrier之前的步骤都执行完成之后再继续往下执行。barrier主要有2个好处:1、可以强制改变深度优先搜索为广度优先搜索,2、由于通过层的bulking模式可以优化大量重复的数据访问。
下面讲解实现上述功能的具体Step:
- barrier(): 在某个位置插入一个栅栏,以强制该位置之前的步骤必须都执行完成才可以继续往后执行,比如g.V().both().barrier().both()只有在第一个both()全部完成之后才会执行第二个both()。
21.2、实例讲解
21.2.1、 遍历时设置栅栏
// 将所有顶点打印出来
// 打印完一轮之后再打印一轮
def list=[]
g.V().sideEffect{list.add("first: "+it)}
.barrier()
.sideEffect{list.add("second: "+it)}
.iterate()
list
// 打印first后打印second,
// 直到一轮所有的顶点都完成
def list=[]
g.V().sideEffect{list.add("first: "+it)}
.sideEffect{list.add("second: "+it)}
.iterate()
list
// 禁用自动barrier策略
g = g.withoutStrategies(LazyBarrierStrategy)
g.V()
.both().barrier()
.both().barrier()
.both().barrier()
.both().barrier()
.both().barrier()
.groupCount()
.order(local).by(values, decr)
// 通过lambda函数来指定放入口袋的行为
// 注意:提供的初始值为Map类型,而且
// 当traverser分裂时会拷贝Map
g = g.withoutStrategies(LazyBarrierStrategy)
g.V()
.both()
.both()
.both()
.both()
.both()
.groupCount()
.order(local).by(values, decr)
注意:LazyBarrierStrategy是默认策略,该策略会在合适的地方插入barrier,因此这里先禁用了该策略。
22.2.2、 barrier()相关Step:
事实上除了可显示的插入barrier栅栏外,还有不少Step会隐式插入barrier,包括 order(), sample(), dedup(), aggregate(), fold(), count(), sum(), max(), min(), group(), groupCount(), cap()等。
21.3、综合运用
21.3.1、 计算特征向量中心性(Eigenvector Centrality)
// 利用隐式barrier计算特征向量中心性
// 包括groupCount、cap,按照降序排序
g.V().repeat(both().groupCount('m'))
.times(5).cap('m')
.order(local).by(values, decr)
22.局部操作local
22.1、说明
通过Gremlin进行图遍历通常是当前step处理前一step传递过来的对象流。很多操作是针对传递过来的对象流中的全部对象进行操作,但也有很多时候需要针对对象流中的单个对象而非对象流中的全部对象进行一些操作。这种对单个对象的局部操作,可以使用local()语句实现。
另外,有一些step默认的操作是针对对象流中的全部对象,但也可以通过参数来改变默认操作,允许针对对象流中的单个对象进行操作,包括count(),max(),mean(),min(),sum(),order(),tail(),limit(),range(),sample(),skip()和dedup()等。
22.2、实例说明
22.2.1、local的作用说明
是否使用local()的区别可以参见如下示意图:

// 不使用local()
g.V().hasLabel('person').as('person')
.properties('age').order().by(value).limit(2)
.value().as('age')
.select('person','age').by('name').by()
// 使用local()
g.V().hasLabel('person').as('person')
.local(properties('age').order().by(value).limit(2))
.value().as('age')
.select('person','age').by('name').by()
22.2.2、以参数的形式指定局部操作
// 查询软件HugeGraph的属性Map
g.V().hasLabel('software').has('name', 'HugeGraph')
.propertyMap()
// 查询软件HugeGraph的属性个数
g.V().hasLabel('software').has('name', 'HugeGraph')
.propertyMap().count(local)
// 数目最多的顶点类型的顶点数目
g.V().groupCount().by(label).select(values).max(local)
min()、mean()、sum()等与max()使用local参数的方法基本一致,不再赘述
// 所有顶点的属性列表中的第一个属性
g.V().valueMap().limit(local, 1)
tail()、range()、skip()与limit()使用local参数的方法基本一致,不再赘述
// 所有顶点一步邻居中所有的software
g.V().both().group().by(label).select('software').dedup(local)
// 所有顶点按类型计数并按数目由多到少排序
g.V().groupCount().by(label).order(local).by(values, decr)
// 所有顶点作为一个集合,从中采样2个
g.V().fold().sample(local,2)
23.遍历终止terminal
23.1、说明
Gremlin 中有一类特殊的操作,它能够终止遍历器的“遍历”行为,使其执行并返回结果。在这里要强调的一点:原生的 Gremlin 语句通常都是用遍历器连接起来的,但其实这些连接的过程并不会执行 Gremlin 语句,只有走到了terminalStep 时才会执行。这个模式类似于 Spark 中对RDD的map和action操作。
-
hasNext: 判断遍历器是否含有元素(结果),返回布尔值;
-
next: 不传参数时获取遍历器的下一个元素,也可以传入一个整数 n,则获取后面 n 个元素;
-
tryNext: hasNext和next的结合版,返回一个Optional对象,如果有结果还需要调用get()方法才能拿到;
-
toList: 将所有的元素放到一个List中返回;
-
toSet: 将所有的元素放到一个Set中返回,会去除重复元素;
-
toBulkSet: 将所有的元素放到一个能排序的List中返回,重复元素也会保留;
-
fill: 传入一个集合对象,将所有的元素放入该集合并返回,其实toList、toSet和toBulkSet就是通过fillStep实现的;
-
iterate: 这个 Step 在终止操作里面有点特殊,它并不完全符合终止操作的定义。它会在内部迭代完整个遍历器但是不返回结果。
前面我们介绍了那么多的 Step 很多都没有加terminalStep 啊,为什么也能返回结果呢?其实这是 Tinkerpop 的 Gremlin 解析引擎对遍历器对象调用了一个IteratorUtils.asList()方法,又调用了它内部的fill()方法(注意:不是上面讲到的fill()Step)。
23.2、实例讲解
23.2.1、 hasNext()
// 判断顶点“linary”是否包含“created”出顶点
g.V('linary').out('created').hasNext()
// 判断顶点“linary”是否包含“knows”出顶点
g.V('linary').out('knows').hasNext()
23.2.2、 next()
// 获取顶点“javeme”的“knows”出顶点集合的下一个(第1个)
g.V('javeme').out('knows').next()
g.V('javeme').out('knows')返回的是一个遍历器(迭代器),每次执行这句话实际上都是获取的迭代器的第一个元素,那如果想获取第二个元素该怎么写呢?很简单,执行两次next()即可,但是这里的前提条件是遍历器中确实存在多个元素。
// 获取顶点“javeme”的“knows”出顶点集合的下一个(第2个)
it = g.V('javeme').out('knows')
it.next()
it.next()
// 获取顶点“javeme”的“knows”出顶点集合的前两个
g.V('javeme').out('knows').next(2)
next()与next(n)使用中有一点小小的区别,就是当没有元素或者没有足够多的元素时,执行next()会报错,但是执行next(n)则是返回一个空集合(List)。
23.2.3、 tryNext()
// 试图获取顶点“javeme”的“created”出顶点集合中的下一个
g.V('javeme').out('created').tryNext()
这里会发现结果与前面概述中说的有些不同。概述中说的是返回一个
Optional对象,要获取Optional对象里的值是需要调用它的get()方法的,怎么这里直接就把值给返回了呢?大家先别着急,我们再看一个例子。
// 试图获取顶点“javeme”的“created”入顶点集合中的下一个
g.V('javeme').in('created').tryNext()
这里更加令人费解,没有“created”入顶点时竟然直接报错了,其实这是HugeGraph的实现中关于Optional的序列化所致。HugeGraph序列化Optional对象时会判断该对象内的值是否存在,如果存在则取出来序列化该值,否则填入一个null。详细代码见HugeGraphSONModule.java中关于OptionalSerializer的实现。
本文的重点在于学习Gremlin语法本身,下面给出上述两个示例的预期结果:
Optional[v[3:HugeGraph]]
Optional.empty
23.2.4、 toList()
// 获取所有“person”顶点的“created”出顶点集合,放入List中,允许包含重复结果
g.V().hasLabel('person').out('created').toList()
// 获取所有“person”顶点的“created”入顶点集合,放入List中,允许包含重复结果
g.V().hasLabel('person').in('created').toList()
结果与
next(n)有些类似。
23.2.5、 toSet()
// 获取所有“person”顶点的“created”出顶点集合,放入Set中,不允许包含重复结果
g.V().hasLabel('person').out('created').toSet()
相比于
toList,toSet去除了重复元素。
// 获取所有“person”顶点的“created”入顶点集合,放入Set中,不允许包含重复结果
g.V().hasLabel('person').in('created').toSet()
23.2.6、 toBulkSet()
// 获取所有“person”顶点的“created”出顶点集合,放入BulkSet中,允许包含重复结果,排序
g.V().hasLabel('person').out('created').toBulkSet()
所谓的
BulkSet虽然名字上带有"Set",但还是更像一个List,对比toList的结果,它实际上是把所有元素排了个序。
23.2.7、 fill()
// 创建一个List,获取所有“person”顶点的“created”出顶点,并放入该List中
results = []
g.V().hasLabel('person').out('created').fill(results)
results
23.2.8、 iterate()
// 迭代所有“person”顶点
it = g.V().hasLabel('person').iterate()
it.hasNext()
调用了
iterate()后遍历器内部的元素就已经全部迭代过了,所以再调用hasNext()返回false。
24.转换操作map/flatMap
24.1、说明
-
map: 可以接受一个遍历器 Step 或 Lamda 表达式,将遍历器中的元素映射(转换)成另一个类型的某个对象(一对一),以便进行下一步处理;
-
flatMap: 可以接受一个遍历器 Step 或 Lamda 表达式,将遍历器中的元素映射(转换)成另一个类型的某个对象流或迭代器(一对多)。
24.2、操作实例
24.2.1、 map()
// 获取顶点“3:HugeGraph”的入“created”顶点的“name”属性,其实可以理解为顶点对象转化成了属性值对象
g.V('3:HugeGraph').in('created').map(values('name'))
// g.V('3:HugeGraph').in('created').map {it.get().value('name')}
// 先获取顶点“3:HugeGraph”的入“created”顶点,再将每个顶点转化为出边(一条)
g.V('3:HugeGraph').in('created').map(outE())
注意:顶点“javeme”其实是有三条边的,但是这里只打印出了一条。因为
mapStep是一对一的转换,要想获取所有的边可以使用flatMap。
24.2.2、 flatMap()
// 先获取顶点“3:HugeGraph”的入“created”顶点,再将每个顶点转化为出边(多条)
g.V('3:HugeGraph').in('created').flatMap(outE())
注意:这一次就能打印出顶点“javeme”的全部三条边了。
25.附加操作sideEffect
25.1、说明
Gremlin在路径遍历的时候,可以在路径中做一些额外的附加操作,这个附加操作不会改变上一步的结果,会原封不动的传递到下一步去。附加操作看起来就像透明的,但实际上可以将附加操作的处理结果存储到外部变量中去。
下面讲解实现上述功能的具体Step:
-
sideEffect(): 在某个位置插入一个附加操作,以执行额外的操作,通常可与store、sack等配合使用。另外如下一些Step本质上也是sideEffect:
group(string)、groupCount(string)、subgraph(string)、aggregate(string)、inject(string)、profile(string)等。
-
withSideEffect():绑定初始值到变量上,等价于sideEffect的效果。
25.2、实例讲解
25.2.1、 sideEffect(): 附加操作
// 将所有顶点打印出来
// sideEffect本身不影响结果
def list=[]
g.V().hasLabel('person')
.sideEffect{list.add("vertex:"+it)}
.toList()
// 将sideEffect处理的结果打印出来
def list=[]
g.V().hasLabel('person')
.sideEffect{list.add("vertex:"+it)}
.toList()
list
注意:Gremlin中的最后一行内容表示输出的结果
// 将sideEffect结果存到变量中
g.V().hasLabel('person')
.sideEffect(outE().count().store("o"))
.sideEffect(inE().count().store("i"))
.cap("o","i")
25.2.2、 withSideEffect(): 绑定变量初始值
// 初始化一个变量以供后续条件判断中使用
// 查找javeme的共同作者,且名称在初始集合中
g.withSideEffect('p',['Linary Li','Zhoney Zhang','Tom'])
.V('javeme').out('created').in('created')
.values('name').where(within('p'))
25.3、综合运用
25.3.1、 计算度中心性(Degree Centrality)
// 利用sideEffect计算3种度中心性
g.V().group('both').by().by(bothE().count())
.group('out').by().by(outE().count())
.group('in').by().by(inE().count())
.cap('both', 'out', 'in')
26.执行统计和分析
26.1、说明
Gremlin提供了两种语句来帮助用户对执行的查询语句进行统计和分析工作:
-
explain(),详细描述原始的Gremlin语句在编译期是如何转变为最终要执行的step集合的
-
profile(),统计Gremlin语句执行过程中的每个step消耗的时间和通过的对象等统计信息
TraversalStrategy是“遍历策略”,可以在编译期分析遍历(Traversal)的组成,并在遍历满足TraversalStrategy的条件时对遍历进行修改。这些修改往往都是为了能够更加高效的执行遍历。
遍历策略有5类:
-
Decoration,应用程序级别的策略
-
Optimization,TinkerPop3级别的策略
-
Provider optimization,图数据库实现级别的策略
-
Finalization,遍历执行前的调整和清理策略
-
Verification,判断遍历是否合法的验证策略
26.2、实例讲解
26.2.1、 explain()
g.V().hasLabel('person').outE().identity().inV().count().is(gt(5)).explain()
结果中:
-
original,表示Gremlin语句等价的最初的step列表
-
intermediate,表示original转化在TraversalStrategy作用下的转化过程
strategy,表示作用于上一轮的step列表的TraversalStrategy
category,表示strategy中的TraversalStrategy所属的级别,参见说明部分
traversal,表示上一轮的step列表经过strategy中的TraversalStrategy处理之后的新的step列表
- final,表示经过所有TraversalStrategy处理后的最终要执行的step列表
26.2.2、 profile()
g.V().out('created').profile()
返回的结果中,metrics中每一条是一个执行的step,其中:
- name是step的名字,例如"HugeGraphStep(vertex,[])"
- dur是step执行的时间,单位是毫秒
- annotations中的percentDur是当前step消耗的时间在总的执行时间中的比例
- counts中的traverserCount是当前step中的traverser的数目
- counts中的elementCount是当前step中的element的数目
traverserCount和elementCount的区别在于: traverserCount是同一step中相同的对象合并之后的数目,对象相同是指当前的对象是一样的,并不代表对象的path等其他数据也相同;相同的对象合并为bulk,可以减少重复工作,提高效率。elementCount是同一step中所有对象展开bulk之后的数目之和,即未去重的对象数目。因此,traverserCount总是小于等于elementCount。
profile()语句是一个“副作用”(side effect)语句,并非立刻执行。profile()语句还可以指定一个key,形式为profile(String),在执行完要统计的Gremlin语句后,通过key获取统计信息。例如:
t=g.V().out('created').profile('metrics')
t.iterate()
t.getSideEffects().get('metrics')
27. Gremlin Java API
27.1、Maven配置
在Maven的pom.xml配置文件中增加如下引用:
<dependency>
<groupId>org.janusgraph</groupId>
<artifactId>janusgraph-core</artifactId>
<version>0.4.0</version>
</dependency>
<dependency>
<groupId>org.janusgraph</groupId>
<artifactId>janusgraph-cql</artifactId>
<version>0.4.0</version>
</dependency>
<dependency>
<groupId>org.janusgraph</groupId>
<artifactId>janusgraph-es</artifactId>
<version>0.4.0</version>
</dependency>
<dependency>
<groupId>org.apache.tinkerpop</groupId>
<artifactId>gremlin-driver</artifactId>
<version>3.4.1</version>
</dependency>
27.2、配置JanusGraph配置文件janusgraph-cql-es.properties
janusgraph-cql-es.properties文件配置示例如下:
gremlin.graph=org.janusgraph.core.JanusGraphFactory
storage.backend=cql
storage.hostname=192.156.193.222
storage.cql.keyspace=janusgraphdev
index.search.backend=elasticsearch
index.search.hostname=192.156.193.222
27.3、创建Spring Bean文件JanusgraphBean.java
JanusgraphBean.java内容如下:
import com.esint.api.utils.PropertiesUtil;
import org.janusgraph.core.JanusGraph;
import org.janusgraph.core.JanusGraphFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class JanusgraphBean {
private String confFile = PropertiesUtil.getPropValue("janusgraph.conf.file");
public @Bean
JanusGraph client(){
return JanusGraphFactory.open(confFile);
}
}
27.4、一些Java使用JanusGraph的示例
27.4.1、查询节点的周边的边与节点
/**
* 根据身份证号找到节点并且查询该节点的周边节点.
*/
@Test
public void queryVertexByIdCardTest() throws Exception {
JanusGraph graph = JanusgraphBean.getJanusGraph();
JanusGraphTransaction transaction = graph.newTransaction();
GraphTraversalSource g = transaction.traversal();
GraphTraversal<Vertex, Path> traversal = g.V()
.has("personId", "120221198209293016")
.outE().inV().path()
.by(__.valueMap().with(WithOptions.tokens))
.by(__.valueMap().with(WithOptions.tokens));
while (traversal.hasNext()) {
Path thisobj = traversal.next();
// 分别截取图库查询返回结果中的三部分,左节点、边、右节点
Map<String, List> lv = thisobj.get(0);
Map<String, List> relation = thisobj.get(1);
Map<String, List> rv = thisobj.get(2);
System.out.println(JSON.toJSONString(lv));
System.out.println(JSON.toJSONString(relation));
System.out.println(JSON.toJSONString(rv));
}
//关闭查询
g.close();
//关闭查询事务
transaction.close();
}
27.4.2、查询节点
/**
* 如果节点不存在则创建节点并返回
* @param graph JanusGraph实例对象
* @param key 节点key
* @param value 节点value
* @return 节点
*/
public static Vertex getVertex(String key, String value) {
JanusGraph graph = JanusgraphBean.getJanusGraph();
JanusGraphTransaction transaction = graph.newTransaction();
GraphTraversalSource g = transaction.traversal();
Vertex v;
GraphTraversal<?, Vertex> graphTraversal = graph.traversal().V().has(key, value);
if (graphTraversal.hasNext()) {
v = graphTraversal.next();
} else {
v = graph.addVertex("person");
v.property(key, value);
}
return v;
}
27.4.2、查询边
/**
* 获得边
* @param key 边的key
* @param value 边的value
* @return 边
*/
public static Edge GetEdge(String key, String value){
JanusGraph graph = JanusgraphBean.getJanusGraph();
JanusGraphTransaction transaction = graph.newTransaction();
GraphTraversalSource g = transaction.traversal();
Edge e = null;
GraphTraversal<?, Edge> graphTraversal = graph.traversal().E().has(key, value);
if (graphTraversal.hasNext()){
e = graphTraversal.next();
}
return e;
}
27.4.3、判断边是否存在
/**
* 判断边是否存在
* @param key 边的key
* @param value 边的value
* @return 是否存在 true:存在 false:不存在
*/
public static boolean edgeIsExist(String key, String value) {
JanusGraph graph = JanusgraphBean.getJanusGraph();
JanusGraphTransaction transaction = graph.newTransaction();
GraphTraversalSource g = transaction.traversal();
boolean exist = false;
GraphTraversal<?, Edge> graphTraversal = graph.traversal().E().has(key, value);
if (graphTraversal.hasNext()) {
exist = true;
}
return exist;
}
27.4.4、删除节点
/**
* 删除节点
* @param graph JanusGraph实例对象
* @param key 节点的key
* @param value 节点的value
*/
public static void deleteVertex(JanusGraph graph,String key, String value){
GraphTraversal<Vertex, Vertex> traversal = graph.traversal().V()
.has(key, value);
if(traversal.hasNext()){
Vertex thisobj = traversal.next();
thisobj.remove();
}
}
27.4.5、删除边
/**
* 删除边
* @param graph JanusGraph实例对象
* @param key 边的key
* @param value 边的value
*/
public static void deleteEdge(JanusGraph graph,String key, String value){
GraphTraversal<Edge, Edge> traversal = graph.traversal().E()
.has(key, value);
if(traversal.hasNext()){
Edge thisobj = traversal.next();
thisobj.remove();
}
}
27.4.6、获得节点双向链接所有边的数量
/**
* 获得节点双向链接所有边的数量
* @param graph JanusGraph实例对象
* @param key 节点的key
* @param value 节点的value
* @return 边的数量
*/
public static long getVertexBothEgdeCount(JanusGraph graph,String key, String value){
long number = 0L;
GraphTraversal<Vertex, Long> traversal = graph.traversal().V()
.has(key, value)
.bothE().count();
if (traversal.hasNext()) {
number = traversal.next();
}
return number;
}
27.4.7、获得节点in方向所有边的数量
/**
* 获得节点in方向所有边的数量
* @param graph JanusGraph实例对象
* @param key 节点的key
* @param value 节点的value
* @return 节点的数量
*/
public static long getVertexInEgdeCount(JanusGraph graph,String key, String value){
long number = 0L;
GraphTraversal<Vertex, Long> traversal = graph.traversal().V()
.has(key, value)
.inE().count();
if (traversal.hasNext()) {
number = traversal.next();
}
return number;
}
27.4.8、获得节点Out方向所有边的数量
/**
* 获得节点Out方向所有边的数量
* @param graph JanusGraph实例对象
* @param key 节点的key
* @param value 节点的value
* @return 节点的数量
*/
public static long getVertexOutEgdeCount(JanusGraph graph,String key, String value){
long number = 0L;
GraphTraversal<Vertex, Long> traversal = graph.traversal().V()
.has(key, value)
.outE().count();
if (traversal.hasNext()) {
number = traversal.next();
}
return number;
}
27.4.9、根据边的属性获得节点信息
/**
* 根据边的属性获得节点信息
* @param graph JanusGraph实例对象
* @param ikey 边的index
* @param value 边的value
* @return 节点集合
*/
public static List<Vertex> getVertexByEgdeProperty(JanusGraph graph,String ikey, String value){
List<Vertex> vertexList = graph.traversal().E()
.has(ikey, value)
.bothV()
.dedup()
.toList();
return vertexList;
}
27.4.10、根据边的属性获得边左侧的节点
/**
* 根据边的属性获得边左侧的节点.
* @param graph JanusGraph实例对象
* @param ikey 边的index
* @param value 边的value
* @return 边的左侧节点
*/
public static Vertex getLeftVertexByEdge(JanusGraph graph, String ikey, String value){
GraphTraversal<Edge, Vertex> traversal = graph.traversal().E()
.has(ikey, value)
.outV();
Vertex v = null;
if (traversal.hasNext()){
v = traversal.next();
}
return v;
}
27.4.11、根据边的属性获得右侧的节点
/**
* 根据边的属性获得右侧的节点
* @param graph JanusGraph实例对象
* @param ikey 边的index
* @param value 边的value
* @return 边的右侧节点
*/
public static Vertex getRightVertexByEdge(JanusGraph graph, String ikey, String value){
GraphTraversal<Edge, Vertex> traversal = graph.traversal().E()
.has(ikey, value)
.inV();
Vertex v = null;
if (traversal.hasNext()){
v = traversal.next();
}
return v;
}
27.4.12、获得节点属性的值
/**
* 获得节点属性的值.
* @param vmap 节点的属性结合对象
* @param key 节点的key
* @return 值
*/
public static String getVertexPropertie(Map<String, List> vmap, String key) {
String value = "";
if(vmap == null) {
return value;
}
//是否包含key值
if(vmap.containsKey(key)){
List list = vmap.get(key);
if(list.size() > 0){
value = list.get(0).toString();
}
}
return value;
}
27.4.13、获得节点属性的值-多态1
/**
* 获得节点属性的值.
* @param vmap 节点的属性结合对象
* @param key 节点的key
* @param defaultStr 默认值
* @return 值
*/
public static String getVertexPropertie(Map<String, List> vmap, String key,String defaultStr) {
String value = defaultStr;
if(vmap == null){
return value;
}
//是否包含key值
if(vmap.containsKey(key)){
List list = vmap.get(key);
if(list.size() > 0){
Object obj = list.get(0);
if(obj instanceof Date){
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
Date date = (Date)obj;
if(date.getTime() != DateUtil.getMinDate().getTime()){
value = sdf.format(date);
}
}else if(!"".equals(obj)){
value = obj.toString();
}
}
}
return value;
}
27.4.14、获得节点属性的值-多态2
/**
* 获得节点属性的值.
* @param vmap 节点的属性结合对象
* @param key 节点的key
* @return 值
*/
public static String getVertexPropertieObject(Map<Object, List> vmap, String key) {
String value = "";
if(vmap == null){
return value;
}
//是否包含key值
if(vmap.containsKey(key)){
List list = vmap.get(key);
if(list.size() > 0){
value = list.get(0).toString();
}
}
return value;
}

2106

被折叠的 条评论
为什么被折叠?



