请注意本文档是官方gremlin文档的中文翻译。对于操作阿里云GDB图数据库而言,是在gremlin语言框架下的实现, 这就意味着很多单步具有特殊的实现和约定。使用特定的语义和方式能够让图遍历更有效率。

阿里云GDB团队翻译,如有疑问,订正,和转载需求,请发信至 nosql@list.alibaba-inc.com

图的遍历(Traversal)

gremlin running

在顶层设计上,`Traversal<S,E>`是`Iterator<E>`接口的实现类,`S`和`E`代表着出发点和结束。图的遍历(Traversal)有四种基本类型:

  1. Step<S,E>:单步(Step)。一个从开始(S)向结束(E)遍历过程中的一个独立的函数,一次图的遍历由很多单步游走连接而成

  2. TraversalStrategy:拦截器(interceptor)方法,用户更换遍历的执行步骤(比如,拦截并重写查询的方式)

  3. TraversalSideEffects:一个键值对(key/value pairs),用于存储图的遍历中的全局信息

  4. Traverser<T>:在当前遍历中生成的类型为`T`的对象

GraphTraversal<S,E> 是图遍历的一个经典实现,它继承自 Traversal<S,E> 类。 GraphTraversal 提供了一个图内部数据(包含顶点,边等)组织方式的深层次解释和实现,和由此而来的图的遍历 DSL

在TinkerPop的实现里,大量单步(Step)的实现已经可以满足DSL使用者的绝大多数的需要。 DSL使用者也可以充分利用这些已经实现好的单步,来优化遍历策略或者将其作为优化策略的装饰类,来调整图遍历的策略顺序。 值得注意的是,如果新的“单步”被引入,整个图的遍历策略可能会工作的不正常,或者不正确

单步(Traversal-steps)

step types

GraphTraversalSource(遍历实现)生成一个 GraphTraversal <S,E> 。 同样作为匿名 __(另外一种Traversal的实现,空实现)也可以生成一个 GraphTraversal <S,E> 。 一次图的遍历是由一组单步组成的。 所有 GraphTraversal 提供的单步都可以认为继承自上图的几种基本类型。 Tinkerpop提供的所有单步可以在 GraphTraversal JavaDoc 查到。 以下的章节将使用Gremlin Console,展示GraphTraversal提供的标准单步,

开始一个最基本的图的遍历已经在The Graph Process 这章描述了, 也可以在Getting Started 查到
简单点的话,可以直接引入: +import static org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.__.*+ , 这样,匿名的遍历就可以直接写 inE(),不用再调用 +__.inE()+ 这样了。 但是需要注意有些语言里关键字的冲突,如在Groovy中,inas 是关键字,所以就得使用复杂语义 +__.in()++__.as()+
【中文添加,方便读者理解】GraphTraversal <S,E> 是一个接口类,提供了所有单步的接口。 而每一个接口又大都返回一个 GraphTraversal <S,E>,这样就可以把整个遍历连接起来。 这更像是一个流处理的流程,熟悉函数式程序设计也可以按此思考方式掌握

通用单步(抽象)

一共有5种通用的单步(general-step),每个都有遍历功能或者使用lambda的表现形式(Traversal Representation后称为 形式遍历 )。其他的单步都是这些 形式遍历 的扩展,见后文

Step Description

map(Traversal<S, E>) map(Function<Traverser<S>, E>)

映射(map)遍历到类型为 E 的对象,用于下步处理

flatMap(Traversal<S, E>) flatMap(Function<Traverser<S>, Iterator<E>>)

映射(map)遍历到类型为 E 的迭代器(Iterator),流式的传递到下一步

filter(Traversal<?, ?>) filter(Predicate<Traverser<S>>)

映射(map)遍历为true或者false,其中如果结果是false的,就不会传递到下一步

sideEffect(Traversal<S, S>) sideEffect(Consumer<Traverser<S>>)

在遍历上做一些操作,并且传递到下一步

branch(Traversal<S, M>) branch(Function<Traverser<S>,M>)

将遍历分拆成多个遍历,这些遍历都以按照 M 标记(token)被索引到

文中Lambda(单步中使用lambda)的展示是以教育和引导为目的,主要用于解释Gremlin语言的基础构造。 在实践中,应当在遍历的描述和遍历的验证策略中避免使用Lambda。当然除非显式的声明"turned off",Lambda是仍旧生效的。 关于更多的Lambda在使用中的问题,请阅读 A Note on Lambdas 章节
【中文添加,方便读者理解】形式遍历 (Traversal Representation)就是一种单步的规则的抽象,更类似函数式编程中functor的概念。 在一般的单步中,很多都可以传入一个Lambda或者functor(如Predictable),或者依赖一个迭代器(Iterator),非常类似高阶函数。 在Tinkerpop后面的提供的很多 单步 都属于 一种或者多种 形式遍历。可以认为是功能上的一种归纳或者抽象。 但在tinkerpop具体实现中并不是依赖接口继承关系的。之所以引入 形式遍历 这个名词是作为习语帮助理解

Traverser<S> 对象提供:

  1. 当前遍历 S 对象(开始) — Traverser.get()

  2. 当前遍历器遍历过的路径 — Traverser.path()

    1. 辅助理解:获取指定历史路径 — Traverser.path(String) == Traverser.path().get(String)

  3. 获取遍历器当前循环的次数 — Traverser.loops()

  4. 遍历器中维护的对象的个数 — Traverser.bulk()

  5. 遍历器相关联的局部数据结构 — Traverser.sack()

  6. 遍历器相关联的side-effects — Traverser.sideEffects().

    1. 辅助理解:获取一个指定的side-effect — Traverser.path(String) == Traverser.path().get(String)

map lambda
g.V(1).out().values('name') (1)
g.V(1).out().map {it.get().value('name')} (2)
g.V(1).out().map(values('name')) (3)
1 一次从顶点1向外向(从顶点1沿外向边进行一步游走)的获取相邻接点name的遍历
2 相同的操作,使用Lambda来获取name属性值
3 使用 map() 的相同操作的 形式遍历
filter lambda
g.V().filter {it.get().label() == 'person'} (1)
g.V().filter(label().is('person')) (2)
g.V().hasLabel('person') (3)
1 一个过滤器(filter)操作,过滤条件是顶点的标签是"person"
2 相同的操作,使用 filter() 作为 形式遍历
3 更加特殊的 has() 单步: 是 filter() 加上一个描述性谓词的实现
side effect lambda
g.V().hasLabel('person').sideEffect(System.out.&println) (1)
g.V().sideEffect(outE().count().store("o")).
      sideEffect(inE().count().store("i")).cap("o","i") (2)
1 任何进入 sideEffect() 并到下一步的间隔中,其他的事情(副作用)可能发生
2 对于每个顶点计算 入度(in-degree,入边的数目) 和 出度(out-degree,出边的数目),这两个统计数字的 sideEffect() 都赋给了相同的顶点
branch lambda
g.V().branch {it.get().value('name')}.
      option('marko', values('age')).
      option(none, values('name')) (1)
g.V().branch(values('name')).
      option('marko', values('age')).
      option(none, values('name')) (2)
g.V().choose(has('name','marko'),
             values('age'),
             values('name')) (3)
1 遍历图,如果顶点name是"marko",得到他的age,否则获取其他顶点的name
2 相同的操作,但是使用 branch() 这个 形式遍历
3 更特殊的true/false判断:choose() 单步是一个具体的 branch() 的实现

结束性单步

一般来说, 每一个单步接受一个遍历对象,返回执行后的遍历对象结果,所以下一个单步操作同样使用这个遍历结果,再次返回一个遍历结果。 这样图的遍历过程就能如流式接口, 幺半群运算这样的方式持续进行下去。 但是,有一些单步操作不返回遍历对象,而是执行遍历并返回一个结果(类型不同或者不可迭代,所以遍历没法继续,从而终止)。 这些特殊的单步游就叫做 结束性单步 (Terminal Steps),以下是一些例子

g.V().out('created').hasNext() (1)
g.V().out('created').next() (2)
g.V().out('created').next(2) (3)
g.V().out('nothing').tryNext() (4)
g.V().out('created').toList() (5)
g.V().out('created').toSet() (6)
g.V().out('created').toBulkSet() (7)
results = ['blah',3]
g.V().out('created').fill(results) (8)
g.addV('person').iterate() (9)
1 hasNext() 判断结果是否可用(gremlin-javascript 不支持)
2 next() 将返回下一个结果
3 next(n) 将返回后面 n 个结果返回一个List(gremlin-javascript 和 Gremlin.NET 不支持)
4 tryNext() 返回一个 Optional;因此,它是一个 hasNext()/next() 的组合 (只支持 JVM 相关的语言)
5 toList() 返回所有的结果到一个List中
6 toSet() 返回所有结果到一个Set里,因此具备排重功能(gremlin-javascript 不支持)
7 toBulkSet() 返回所有结果到一个带权重的集合(Weighted Set)里,具备同权重的排重功能,不同权重的相同元素将被保留(只支持 JVM 相关的语言)
8 fill(collection) 将所有的返回结果放到一个提供的Collection中,当结果完全后返回此collection(只支持 JVM 相关的语言)
9 iterate() 并不完全符合 结束性单步 不返回(可迭代)结果的定义, 因为它返回一个遍历的执行——而它的表象更像是 结束性单步 :进行一次遍历迭代,生成“副作用”而非真正返回值

promise() 也是一个 结束性单步,它只用来执行到Gremlin Server 或者 RGPs的远端遍历。 它承诺(promise)在当前遍历 Traversal 的执行在将来(future)会完成

最后,explain() 单步也是一个 结束性单步,请看后续章节描述

阿里巴巴GDB的图元素id

在阿里云GDB图数据库的实现中,对于边和顶点元素,提供了自动id。简而言之,如果用户在创建点和属性时,如果不创建id,那么 GDB图数据库就会自动为用户创建一个id,目前默认是一串uuid,如 5c06721a-1fdc-42bc-b063-ac4178ee95a0, 如果用户想自定义id,那么就需要使用`property(id, "user-defined-id")` 来添加。注意id不使用引号。 同样,如果需要引用顶点,也需要使用g.V(user-defined-id).如g.V(1),而非g.V(user-defined-id),如g.V(1)

g.addV('product').property('name', 'GDB').property(id, "1") (1)
g.addV('entity').property('name', 'aliyun')
g.V().has('entity','name','aliyun').id() (2)
g.addV('product').property('name', 'gremlin').property('id', "25") (3)
1 指定GDB顶点的id为1(注意不需要双引号)。如果增加双引号,那么只不过是增加了一个’id’的属性,而非顶点id
2 默认的,输出GDB默认生成的uuid,如 5c06721a-1fdc-42bc-b063-ac4178ee95a0
3 与1不同,这个id同样是自动生成的,通过property传入带有引号id的是一个属性,而非顶点id

AddEdge Step

自动推理 实际上是将数据中隐式的关系显式化的过程。 在图里面,所谓(数据)已经显式的东西就是图中的对象,比如顶点和边。图中“隐式”的关系体现在图的遍历执行过程中。 换句话说,图的遍历过程揭示的意义(关系),由本遍历定义(的操作)来决定。 举例说明:拿“协同开发者”(co-developer)的定义来说,两个工作在一个工程项目的人就可以称为“协同开发者”。 这个定义就可以定义一个遍历过程来表示(用图遍历语句就可以关联两个人是“协同开发者”co-developer的关系), 同样,这种“协同开发者”的定义同样可以被继承和演绎(到更高抽象层面)。 再多说一句,通过 addE() 这个操作,就可以把这个过去隐式的关系显性化。

addE() 这个单步可能属于(map/sideEffect) 两种 形式遍历 之一 ,后续文档用 addE() (map/sideEffect) 表示

【中文添加,方便读者理解】Traversal 本身在tinkerpop文档里非常抽象,Traversal本身直面翻译代表着遍历,和图的遍历。 首先来讲,Traversal“遍历”并不是在图中从头到尾走一遍,它具体代表了在图中游走或者操作的一串流程。 比如 Traverser<S>, E> 就比较清楚的代表了S到E的一个遍历器。 因为 addE() 这个单步,本身属于图中流程和游走的一部分,按规则添加一条边,返回一个Traversal串式链接给后续Traversal使用。 Tinkerpop文档中的 Traversal 更加广义,需要注意
addedge step
g.V(1).as('a').out('created').in('created').where(neq('a')).
  addE('co-developer').from('a').property('year',2009) (1)
g.V(3,4,5).aggregate('x').has('name','josh').as('a').
  select('x').unfold().hasLabel('software').addE('createdBy').to('a') (2)
g.V().as('a').out('created').addE('createdBy').to('a').property('acl','public') (3)
g.V(1).as('a').out('knows').
  addE('livesNear').from('a').property('year',2009).
  inV().inE('livesNear').values('year') (4)
g.V().match(
        __.as('a').out('knows').as('b'),
        __.as('a').out('created').as('c'),
        __.as('b').out('created').as('c')).
      addE('friendlyCollaborator').from('a').to('b').
        property(id,23).property('project',select('c').values('name')) (5)
g.E(23).valueMap()
marko = g.V().has('name','marko').next()
peter = g.V().has('name','peter').next()
g.V(marko).addE('knows').to(peter) (6)
g.addE('knows').from(marko).to(peter) (7)
1 在marko和他的同伴中间,增加一条带着年份属性(year-property)的“co-developer”边(注意是双向的)
2 接下来从lop和ripple顶点增加createdBy的入向边到josh顶点
3 向所有的created的边,增加一条createdBy的反向边
4 新创建的边是一个可遍历对象
5 遍历过程中任意两个绑定可以用 from()to() 来连接, id 可以用于让用户传入相应的id
6 直接使用引用顶点的方式,从marko到peter添加一条边
7 直接使用引用顶点的方式,从marko到peter添加一条边
【GDB特性】图元素的遍历在GDB中使用g.V(1)的形式,而非g.V(1)。另外,请参考GDB的id来使用GDB中的图元素id

更多参考

AddVertex Step

addV() (map/sideEffect) 用于给图添加顶点。每传入一个对象,就会创建一个顶点。 另外,GraphTraversalSource 的实现中维护着 addV() 方法.

g.addV('person').property('name','stephen')
g.V().values('name')
g.V().outE('knows').addV().property('name','nothing')
g.V().has('name','nothing')
g.V().has('name','nothing').bothE()
【GDB特性】图元素的遍历在GDB中使用g.V(1)的形式,而非g.V(1)。另外,请参考GDB的id来使用GDB中的图元素id

更多参考

AddProperty Step

property() (sideEffect) 单步用于给图的基本元素添加属性。 与 addV()addE() 不同,property() 一定是sideEffect的一个单步,它并不返回创建的属性,而是返回流式传入给他的元素。 (所以是在遍历中做了一些操作,再把遍历传给后面的单步,属于sideEffect 这个形式遍历)。 另外,如果 addV() 或者 addE() 单步紧接着 property() 单步,这些单步会被“折叠”到顶点和边的创建阶段,同时创建所有的属性

g.V(1).property('country','usa')
g.V(1).property('city','santa fe').property('state','new mexico').valueMap()
g.V(1).property(list,'age',35)  (1)
g.V(1).valueMap()
g.V(1).property('friendWeight',outE('knows').values('weight').sum(),'acl','private') (2)
g.V(1).properties('friendWeight').valueMap() (3)
1 对顶点来说,可以进行属性标定: vertex properties.
2 在遍历中,可以选择属性的value,key也可以
3 对顶点来说,property() 单步可以添加元数据属性(meta-properties)
【GDB特性】对于GDB添加图元素的id需要使用特殊形式,请参考GDB的id来使用GDB中的图元素id

更多参考

Aggregate Step

aggregate step

aggregate()(sideEffect) 单步用于在遍历过程中一个特殊点上,将所有对象聚合成一个collection 。 直到所有之前的对象都被充分聚合完成,之后的遍历将没有对象的持续流入,此单步属于 贪婪求值法 。 这正好与 store() 单步填充collection的 惰性求值 的方式相反。 贪婪求值的特性对于一些场景十分关键,比如在某个特殊点上有将所有数据(聚合)进行下一步计算的需要。

g.V(1).out('created') (1)
g.V(1).out('created').aggregate('x') (2)
g.V(1).out('created').aggregate('x').in('created') (3)
g.V(1).out('created').aggregate('x').in('created').out('created') (4)
g.V(1).out('created').aggregate('x').in('created').out('created').
       where(without('x')).values('name') (5)
1 marko created了什么?
2 将所有他created的聚合到x中
3 marko的协作者有谁?
4 marko的协作者created了什么东西?
5 marko的协作者created了什么marko自己没有created的东西?

推荐系统中,上述模式就经常被用到:

"用户A关注(like)什么?谁还关注这些东西?哪些是别人关注但是A还没关注的?"

最后说一下,aggregate() 单步可以被 by() 这个调整单步来调整输出

g.V().out('knows').aggregate('x').cap('x')
g.V().out('knows').aggregate('x').by('name').cap('x')

更多参考

And Step

and() (filter) 单步用于保证所有提供的遍历过程都产生结果,请对比看 or() 的语义

and 在Python中是保留字,所以在Gremlin要使用 and_()

g.V().and(
   outE('knows'),
   values('age').is(lt(30))).
     values('name')

and() 单步可以传入不定长个数的遍历,但在这些遍历中,至少一个产生输出后才能让原来的遍历进入下一单步。

本身也可以当成 中缀表示法 的型式来使用

g.V().where(outE('created').and().outE('knows')).values('name')

更多参考

As Step

as() 单步并不是一个真正的单步执行操作,它更像是一个“调整步”(step modulator),类似by()option()。 使用 as() 可以为一个单步提供标签,从而让后面的诸多单步和数据结构访问到。——比如 select()match(), 和路径.

as 是Groovy的保留字,因此在Gremlin语法匿名遍历时需要使用 __.as().

as 是Python的保留字,因此在Gremlin语法使用中要使用 as_()

g.V().as('a').out('created').as('b').select('a','b')            (1)
g.V().as('a').out('created').as('b').select('a','b').by('name') (2)
1 在路径中选取带有"a" 和 "b"标签的对象
2 在路径中选取带有"a" 和 "b"标签的对象,并且对于每个对象,投射name的值

一个单步可以有任意多个标签与其相关联。这在后面多步操作中通过标签引用这相同的一步很有用

g.V().hasLabel('software').as('a','b','c').
   select('a','b','c').
     by('name').
     by('lang').
     by(__.in('created').values('name').fold())

更多参考

Barrier Step

barrier()(barrier) 单步将惰性(lazy)遍历的管道队列(pipeline)接到一个批量同步(bulk-synchronous)管道队列上。 这一步在以下场景下很有用:

  • 所有在 barrier() 前的要执行完后,才开始执行 barrier() 后面的,例如排序

  • 相同的元素(elements)在执行中可能被多次涉及。因此,就可以“拖住”一个遍历,这样就可以做遍历的批量优化,诸如此例

g.V().sideEffect{println "first: ${it}"}.sideEffect{println "second: ${it}"}.iterate()
g.V().sideEffect{println "first: ${it}"}.barrier().sideEffect{println "second: ${it}"}.iterate()

“批量优化”背后的原理很简单。如果有一百万个在顶点1的遍历器,就需要计算一百万次 both() 单步。 然而,把这一百万次遍历映射为一个遍历器使用 Traverser.bulk() 一次就可以了。 批量优化的示例对于“大图”来说显著。因此下面的例子使用 Grateful Dead graph (字面翻译为优雅渐亡图,指查询快速有效收敛)

graph = TinkerGraph.open()
g = graph.traversal()
g.io('data/grateful-dead.xml').read().iterate()
g = graph.traversal().withoutStrategies(LazyBarrierStrategy) (1)
clockWithResult(1){g.V().both().both().both().count().next()} (2)
clockWithResult(1){g.V().repeat(both()).times(3).count().next()} (3)
clockWithResult(1){g.V().both().barrier().both().barrier().both().barrier().count().next()} (4)
1 显式的删除生成进行批量优化的 LazyBarrierStrategy
2 一个非批量的遍历,每个遍历流程都被执行了
3 每一个进入的遍历都在批量递归
4 隐式的诸多遍历都没有被执行,只有一个批量的递归

如果 barrier() 被传入了一个数字参数(n),则barrier在这些遍历流程流入下一个单步前,只存放 n 个的独一无二(unique)的遍历过程。 这对于前面提到的批量优化的场景非常有用,可以减少由于高并行度遍历导致内存用光(Out-Of-Memory)的风险

LazyBarrierStrategy 会在迭代过程中适当的地方插入 barrier() 来获得批量优化

graph = TinkerGraph.open()
g = graph.traversal()  (1)
g.io('data/grateful-dead.xml').read().iterate()
clockWithResult(1){g.V().both().both().both().count().next()}
g.V().both().both().both().count().iterate().toString()  (2)
1 LazyBarrierStrategy 是一个默认的策略,因此不需要显式的去激活它
2 LazyBarrierStrategy 激活后, barrier() 会在合适的地方自动被插入

更多参考

By Step

by() 单步并不是一个真正的单步执行操作,它是一个“调整步”(step modulator),类似as()option()。 如果一个单步可以接受遍历,函数(functions),比较器(comparators)等, by() 就是用来告诉单步,这些(遍历,函数,比较器…​)是怎样被添加的。 最通用的形式就是 step().by()...by()。有一些单步只能够接受一个 by() ,也有一些单步可以接受任意个 by()

g.V().group().by(bothE().count()) (1)
g.V().group().by(bothE().count()).by('name') (2)
g.V().group().by(bothE().count()).by(count())  (3)
1 by(outE().count()) 将以元素的边的个数来分组,是一个遍历过程 (traversal).
2 by('name') 将获取这些已经分组好的元素的名字,是一个元素属性的投射 (element property projection).
3 by(count()) 将计算每个分组中元素的个数,是一个遍历过程 (traversal).

下面的这些单步操作都支持 by() 调整(modulation)。注意每个调整的语义都需要一步一步理解,或者到解释它们的章节去了解

  • dedup(): 将结果按照 by() 调整步(modulation)来去重

  • cyclicPath(): 按照 by() 来过滤保留循环路径

  • simplePath(): 按照 by() 来过滤保留简单路径(非循环路径即成为简单路径)

  • sample():按照 by() 返回的value来采样

  • where(): 按照 by() 给出的测试条件来判断分支

  • groupCount(): 按照 by() 给出的结果当做分组的键(key)来计算分组的个数

  • group(): 根据 by() 来创建分组的键和值

  • order(): 将对象按照 by() 给的结果进行排序

  • path(): 按照 by() 给出的元素路径来获取遍历器的路径

  • project(): 根据 by() 提供的各种条件来映射(project)一组当前的对象(map)

  • select(): 根据 by() 来选择和转换路径元素(path elements)

  • tree(): 获取一颗按照 by() 对象的遍历树

  • aggregate():聚合对象到一个集合中,仅存储 by() 的传入(的条件)

  • store(): 存储对象到一个集合中,仅存储 by() 的传入(的条件)

更多参考

Cap Step

cap() (barrier) 迭代遍历本身,并且按照提供的key来存储引发的sideEffect。 (这样理解比较容易:很多迭代本身是会引发sideEffects的,而cap则只把sideEffects存到一个以key索引的对象里) 如果cap提供了多个keys,那么就把发生的sideEffects存到 Map<String,Object> 结构里。

g.V().groupCount('a').by(label).cap('a')      (1)
g.V().groupCount('a').by(label).groupCount('b').by(outE().count()).cap('a','b')   (2)
1 按照label来分组并统计顶点的个数,将所生成的“按label的分组个数”这个sideEffect存到以a为键的对象里。
2 和1一样,同时将按照边数目分组顶点这个sideEffect存到b里(注意有两次groupCount,引发两次带有sideEffect的单步)

更多参考

Choose Step

choose step

choose()(branch) 单步提供了遍历中的择路。 使用 choose() 就可以实现 if/then/else 这样的语义,甚至更复杂的情况

g.V().hasLabel('person').
      choose(values('age').is(lte(30)),
        __.in(),
        __.out()).values('name') (1)
g.V().hasLabel('person').
      choose(values('age')).
        option(27, __.in()).
        option(32, __.out()).values('name') (2)
1 如果遍历中有元素符合要求,就执行 in ,否则执行 out (一个基于true/false的择路)
2 在遍历中判断结果当做一个key,使用它判断择路(同样也是基于true/false的择路)

如果false的分支没有指定,那么choose就是一个if/then的语义

g.V().choose(hasLabel('person'), out('created')).values('name') (1)
g.V().choose(hasLabel('person'), out('created'), identity()).values('name') (2)
1 如果顶点是一个person,则发送到它created的顶点,否则就发送顶点本身
2 If/then/else 在false分支使用一个 identity() 就相当于没有false分支的 if/then 语义

请注意 choose() 可以接受任意个参数的选择项,也可以接受选择函数使用的匿名遍历

g.V().hasLabel('person').
      choose(values('name')).
        option('marko', values('age')).
        option('josh', values('name')).
        option('vadas', valueMap()).
        option('peter', label())

choose() 单步可以利用 Pick.none (什么都不选)这个选项来匹配。 如果在各个选项都精确匹配不上,那么 none 这个选项就可以匹配上了

g.V().hasLabel('person').
      choose(values('name')).
        option('marko', values('age')).
        option(none, values('name'))

更多参考

Coalesce Step

coalesce() 单步按顺序执行并对提供的多个遍历做评估,返回第一个能够返回最少一个元素的遍历(在提供的多个遍历中成功的第一个)。

g.V(1).coalesce(outE('knows'), outE('created')).inV().path().by('name').by(label)
g.V(1).coalesce(outE('created'), outE('knows')).inV().path().by('name').by(label)
g.V(1).property('nickname', 'okram')
g.V().hasLabel('person').coalesce(values('nickname'), values('name'))

更多参考

Coin Step

要想随机的过滤掉一些遍历,使用 coin() (filter) 单步就可以。参数是double类型,意思是扔硬币("coin toss")的概率(获取正面)。

g.V().coin(0.5)
g.V().coin(0.0)
g.V().coin(1.0)

更多参考

ConnectedComponent Step

connectedComponent() 单步执行一次计算来识别图中的 图元件 实例。 当单步完成,每个顶点将被打上一个“元件标签(component identifier)”用于标识它属于哪个图元件

connectedComponent() 单步是一个 VertexComputing 的步骤,所以它只有在支持 GraphComputer (OLAP) 的系统中使用
g = graph.traversal().withComputer()
g.V().
  connectedComponent().
    with(ConnectedComponent.propertyName, 'component').
  project('name','component').
    by('name').
    by('component')
g.V().hasLabel('person').
  connectedComponent().
    with(ConnectedComponent.propertyName, 'component').
    with(ConnectedComponent.edges, outE('knows')).
  project('name','component').
    by('name').
    by('component')

使用上要注意 with() 这个调整步,它用于来给算法增加配置选项。 它需要在 ConnectedComponent 这个类中获取配置的keys,并且会被自动的导入到Gremlin控制台上

更多参考

Constant Step

constant() (map) 单步直接为遍历产生一个“立即值”(constant value), 这个在条件性选择单步时特别有用,比如choose()-step 或者 coalesce()-step

g.V().choose(hasLabel('person'),
    values('name'),
    constant('inhuman')) (1)
g.V().coalesce(
    hasLabel('person').values('name'),
    constant('inhuman')) (2)
1 显示顶点是person的name,对于其他顶点,显示"inhuman"
2 和1一样(除非有一个person的顶点没有name)

更多参考

Count Step

count step

count() (map) 单步在当前遍历器流中计算总数 (比如批量计数)

g.V().count()
g.V().hasLabel('person').count()
g.V().hasLabel('person').outE('created').count().path()  (1)
g.V().hasLabel('person').outE('created').count().map {it.get() * 10}.path() (2)
1 count() 单步是一个 reducing barrier step ,这意味着所有单步之前的遍历器都被“折叠”(fold)到一个新遍历器中
2 count() 单步后的遍历都是由 count() 发起的
count(local) 计数当前的局部对象(而不是整个遍历中的流式对象) 这对 CollectionMap 类型的对象也生效。其他对象就返回个数1

更多参考

CyclicPath Step

cyclicpath step

每一个遍历器都维护了它的在图中的游走历史——比如,它的路径。 如果遍历的重复游走很重要的话,那么就可以使用 cyclic() (filter) 单步。 这个单步分析遍历器运行到当前的历史路径,如果有回环,遍历器就运算一下,把这些给过滤出来。 如果期望输出没有回环(non-cyclic)的,请查看simplePath()

g.V(1).both().both()
g.V(1).both().both().cyclicPath()
g.V(1).both().both().cyclicPath().path()
g.V(1).as('a').out('created').as('b').
  in('created').as('c').
  cyclicPath().
  path()
g.V(1).as('a').out('created').as('b').
  in('created').as('c').
  cyclicPath().from('a').to('b').
  path()

更多参考

Dedup Step

dedup() (filter) 单步可以用来在整个遍历流中去重。 注意如果一个遍历的批量如果大于1,那么它在发出前就被设置成1了

g.V().values('lang')
g.V().values('lang').dedup()
g.V(1).repeat(bothE('created').dedup().otherV()).emit().path() (1)
1 遍历所有 created 的边,但只会访问一次

如果 dedup() 依赖了 by 这个协调步,那么对象先被做相应的处理,然后决定是否可见

g.V().valueMap('name').with(WithOptions.tokens)
g.V().dedup().by(label).values('name')

最后,如果向 dedup() 传了一串字符串(array of strings)那么它将保证这些去重操作不仅作用于当前遍历对象, 而是作用在整个遍历器的历史路径(的多个遍历对象)上

g.V().as('a').out('created').as('b').in('created').as('c').select('a','b','c')
g.V().as('a').out('created').as('b').in('created').as('c').dedup('a','b').select('a','b','c') (1)
1 If the current a and b combination has been seen previously, then filter the traverser.

更多参考

Drop Step

drop() (filter/sideEffect) 单步用于在图上删除元素和属性。 它本身也是一个过滤性(filter)的单步操作,因为它并不引入外部对象

g.V().outE().drop()
g.E()
g.V().properties('name').drop()
g.V().valueMap()
g.V().drop()
g.V()

更多参考

Emit Step

emit 也不是一个真正意义上的单步,它是 <<repeat-step,repeat()>> 的协调步。(查看更多 emit() 的信息)

更多参考

Explain Step

explain() (terminal) 是一个 结束性单步,它返回一个 TraversalExplanation 结构。 一个“遍历的解释”详细的描述了遍历(先于 explain())是怎样根据给定的traversal strategies(遍历策略)解析的。 TraversalExplanation 有一个 toString() 方法,生成一个三栏的陈述:第一栏是应用的遍历规则, 第二栏是遍历规则的分类:[D]ecoration,[O]ptimization,[P]rovider optimization,[F]inalization和[V]erification。 最后一栏是应用策略后,遍历器的当前状态。“最终遍历”是整个执行计划的结果

g.V().hasLabel('person').outE().identity().inV().count().is(gt(5)).explain()

查看更多遍历的特征分析(profiling)信息,请阅读profile() 这个单步

Fold Step

有些场景下,遍历流需要一个“栅栏”来聚合所有的对象, 然后对这个聚合的对象使用一个函数进行计算 fold() 单步就是这样一个特殊的例子,请查阅 unfold() 看相反的操作

g.V(1).out('knows').values('name')
g.V(1).out('knows').values('name').fold() (1)
g.V(1).out('knows').values('name').fold().next().getClass() (2)
g.V(1).out('knows').values('name').fold(0) {a,b -> a + b.length()} (3)
g.V().values('age').fold(0) {a,b -> a + b} (4)
g.V().values('age').fold(0, sum) (5)
g.V().values('age').sum() (6)
1 一个无参数的 fold() 将聚合所有的对象到一个链表里(list),然后发送到遍历器下一个单步
2 检验一下返回链表的类型
3 fold() 能够接受两个参数,一个是种子值,一个是做reduce的bi-function。("vadas" 有5个字母 + "josh" 有4个).
4 图中所有人age的和
5 和前面一样,不过是使用内建的bi-function
6 和前面一样,不过使用了sum() 单步
【中文添加,方便读者理解】由于tinkerpop的每一个单步非常像函数式编程,在很多操作中, 可以直接传递一个functor或者lambda进行运算衔接。上文中reduce和BiFunction可以参考Java8的FunctionalInterface来理解

更多参考

From Step

from() 单步也不是一个真正意义上的单步,和as()by()一样,也是一个 调整步。 如果一个单步能够接受遍历(traversals)或者字符串(strings),那么 from() 就是告诉他们从哪添加的。 通用的模式就是 step().from()。 同样可以看to()调整步

可以接受 from() 的单步操作有:simplePath()cyclicPath()path(),和addE()

from 在Javascript是关键字,所以引用Gremlin时使用 from_()

from 在Python是保留字,所以引用Gremlin时使用 from_()

更多参考

Graph Step

V() 单步一般用来发起一个 GraphTraversal,也可以用于中间遍历(mid-traversal)

g.V().has('name', within('marko', 'vadas', 'josh')).as('person').
  V().has('name', within('lop', 'ripple')).addE('uses').from('person')
中间遍历`V()` 是否能够使用使用索引,依赖于:a) 是否有合适的索引 b) 特殊实现的图系统师是否提供这个功能
g.V().has('name', within('marko', 'vadas', 'josh')).as('person').
  V().has('name', within('lop', 'ripple')).addE('uses').from('person').toString() (1)
g.V().has('name', within('marko', 'vadas', 'josh')).as('person').
  V().has('name', within('lop', 'ripple')).addE('uses').from('person').iterate().toString() (2)
1 一般来说,V() 单步将遍历所有的顶点。 但同时,图策略可以将能够满足 HasContainer 过滤条件的放到 Graph-Step 单步中,就可以使用索引来快速查找
2 在具体的图系统中,是否中间遍历 V() 支持快速索引与否,是不能简单的通过在当前迭代中使用 toString() 输出的结果来判断的。 但如果有 has 这样的条件使用在了 V() 单步中,如果索引存在,它就会被使用(即使部分索引)

更多参考

Group Step

当遍历器在执行(执行图遍历所在的子图)时,有时候“副作用”是需要进行计算的。 即当前遍历器的位置或者已执行的路径都不是最后计算的结果,而仅仅是这个遍历的一种另外的表示形式。 group() (map/sideEffect) 单步就是一种sideEffect单步,它能够按照某函数将对象组织并分组。 然后接下来,被组织的分组(一个链表)就可以进行reduce操作。下面是一个例子

g.V().group().by(label) (1)
g.V().group().by(label).by('name') (2)
g.V().group().by(label).by(count()) (3)
1 将顶点按它们各自的label分组
2 将顶点分组后,获取他们的name
3 看下每个分组的大小是多少?

group() 一共有两个可以用 by() 投射(projection)的参数:

  1. 键投射:使用什么样的方式来归类对象?(一个能用于生成Key的功能)

  2. 值投射:存放在链表中的对象,使用了分组中什么能力?

更多参考

GroupCount Step

groupCount() (map/sideEffect) 单步可以用于在特定的点上按需查看聚合对象的个数

"图中age的分布情况是什么?"
g.V().hasLabel('person').values('age').groupCount()
g.V().hasLabel('person').groupCount().by('age') (1)
1 也可以在分组前,使用by() 投射的方法来将分组要流入的对象

例中有一个人32岁,一个人35岁,一个27岁,一个29岁

"进行图的游走,并对每个名字的第二个字母的个数进行统计"
groupcount step
g.V().repeat(both().groupCount('m').by(label)).times(10).cap('m')

上面的实例非常有趣,实际上它深刻的展示了 groupCount() 怎样使用一个string类型的变量来引用其内部的 Map<Object,Long>。 考虑到 groupCount() 一个sideEffect类型的 通用单步 ,它传入的对象适用于接收对象流的输出。 groupCount() 内部,对象计数器递增(按object对象)

更多参考

Has Step

has step

使用 has() (filter) 单步就可以来筛选过滤边,顶点和顶点的属性。 has() 本身也有一些变形使用,包含:

  • has(key,value):如果遍历器里的元素没有提供key/value属性,直接删除遍历器

  • has(label, key, value):如果遍历器中的元素没有指定的label和key/value属性,直接删除遍历器

  • has(key,predicate):如果遍历器中的元素的key/value不能满足bi-predicate(二元谓词函子),则删除遍历器。对于predicates更多信息,请查看A Note on Predicates

  • hasLabel(labels...):如果遍历器中的元素没有任何一个传入的label,那就删除遍历器

  • hasId(ids...):如果遍历器中的元素没有任何一个传入的ids,那就删除遍历器

  • hasKey(keys...):如果遍历器属性中没有所有提供的keys,那就删除遍历器

  • hasValue(values...):如果遍历器属性中没有所有提供的values,那就删除遍历器

  • has(key):如果遍历器中没有元素具有所提供的key,那就删除遍历器

  • hasNot(key):如果遍历器中有元素具有所提供的key,那就删除遍历器

  • has(key, traversal): 如果遍历(内部的traversal)中属性没有按照key生成结果,则删除遍历器

g.V().hasLabel('person')
g.V().hasLabel('person').out().has('name',within('vadas','josh'))
g.V().hasLabel('person').out().has('name',within('vadas','josh')).
      outE().hasLabel('created')
g.V().has('age',inside(20,30)).values('age') (1)
g.V().has('age',outside(20,30)).values('age') (2)
g.V().has('name',within('josh','marko')).valueMap() (3)
g.V().has('name',without('josh','marko')).valueMap() (4)
g.V().has('name',not(within('josh','marko'))).valueMap() (5)
g.V().properties().hasKey('age').value() (6)
g.V().hasNot('age').values('name') (7)
1 找到所有age在20(不包含)到30(不包含)中间的顶点。换句话说,age必须在大于20,且小于30
2 找到所有age不在20(包含)和30(包含)中间的顶点。换句话说,age必须在小于20,或者大于30
3 找到所有name精确匹配 [josh,marko] 集合内(josh和marko)的顶点,并且以键值对方式显示这些顶点
4 找到所有name不在集合 [josh,marko] 内的顶点,并且以键值对方式显示这些顶点
5 一个相同的例子,用 not 修饰 within 来生成 without 语义
6 找到图中所有age的属性,并且获取他们的value
7 找到所有不含age属性的顶点,并且获取他们的name

TinkerPop并不提供正则表达式的predicate运算,尽管一些使用Tinkerpop的实现可能提供了一些部分匹配的语义扩展

更多参考

Id Step

id() (map) 单步传入一个元素(Element),并获得它的id

g.V().id()
g.V(1).out().id().is(2)
g.V(1).outE().id()
g.V(1).properties().id()
【GDB特性】请参考GDB的id来使用GDB中的图元素id

更多参考

Identity Step

identity() (map) 单步是一个映射对象到自己的恒等函数

【中文添加,方便读者理解】identity()id() 的区别就是返回一个函数,而这个函数可以作为下一步运算。 比如如果用户使用id()获取到一个唯一标识后,如果想继续运算就得把它as()一下
g.V().identity()

更多参考

Index Step

index() (map) 单步为当前的集合每个元素创建索引。如果当前遍历器值不是一个value,那就被认为是个单元素集合。 目前有两种索引器,它们可以用 with() 协调步来选择。一种是List索引器(默认索引器),它为这个集合创建一个链表,第一个是原有的元素,第二个是索引,以此类推。 第二种Map索引器创建一个LinkedHashMap,索引作为键,原有的元素作为值

g.V().hasLabel("software").index()  (1)
g.V().hasLabel("software").values("name").fold().
  order(Scope.local).
  index().
  unfold().
  order().
    by(__.tail(Scope.local, 1))     (2)
g.V().hasLabel("software").values("name").fold().
  order(Scope.local).
  index().
    with(WithOptions.indexer, WithOptions.list).
  unfold().
  order().
    by(__.tail(Scope.local, 1))     (3)
g.V().hasLabel("person").values("name").fold().
  order(Scope.local).
  index().
    with(WithOptions.indexer, WithOptions.map)  (4)
1 在非集合对象上做索引将生成一个多元索引的单对象集合(相同的元素,因为查到的元素相同,所以对应了相同的索引0)
2 对所有“software”的name做索引,按字母序
3 和1相同,显式的选择List索引器
4 对所有“person”的name做索引,按字母排序,且选择Map索引器

更多参考

Inject Step

inject step

“可插入单步”的理念其实是让“能够任意的在遍历流中插入对象”变得可能。总体来说 inject() (sideEffect) 单步是存在的,下面是一些例子

g.V(4).out().values('name').inject('daniel')
g.V(4).out().values('name').inject('daniel').map {it.get().length()}
g.V(4).out().values('name').inject('daniel').map {it.get().length()}.path()

在上面的例子中,请注意从 daniel 开始的路径深度只有2。这是因为 daniel 这个字符串在遍历的中间步骤(half-way)被插入。 最后,下面是一个典型的用法——当图遍历的起点不是一个图对象

inject(1,2)
inject(1,2).map {it.get() + 1}
inject(1,2).map {it.get() + 1}.map {g.V(it.get()).next()}.values('name')

更多参考

IO Step

gremlin io

在图的实例上导入导出数据是 io() 单步的工作。 默认的,TinkerPop支持三种导入导出图数据的格式,分别是 GraphMLGraphSON,和 Gryo.

附加的关于TinkerPop IO 格式的文档在 IO 参考.

io() 单步本身只是导入和导出的配置,表明其的工作模式,后续在调用 read() 或者 write() 时才真正执行(导入或导出)。 因此,典型的 io() 应该类似:

g.io(someInputFile).read().iterate()
g.io(someOutputFile).write().iterate()
上面的命令仍旧是一个遍历操作,所以需要一个迭代来执行,正如这里的 iterate() 这个 结束性单步

默认的,io() 单步将通过文件的扩展名来检测是否是使用了正确的格式。 要去获得更高级格式控制的能力,应通过 with() 单步来提供更多的信息给 io(),比如:

g.io(someInputFile).
    with(IO.reader, IO.graphson).
  read().iterate()
g.io(someOutputFile).
    with(IO.writer,IO.graphml).
  write().iterate()

IO 类是一个实现 io() 单步的helper类,它提供了可以帮助(用户)配置的表达式和供"reader" 和 "writer"使用的直接规范。 "reader" 其实可以参考 GraphReader 的实现,同样,"writer" 的一个实现是 GraphWriter。这两个接口类的默认实现是标准的tinkerpop实现的一部分

这个默认的实现对用户来说也是很重要的、要考虑的一点。Tinkerpop的实现并不是以“超大规模、复杂、大并发负载”为目的来思考设计的。 它是围绕着“单线程,OLTP类型的数据负载”这种常见类型来设计的,因为这样可以适配较多的图数据库类应用。 因此,从导入角度,这样的设计对一些小数据量(对时间不敏感的中型数据图也是足够的)的空图导入是最好的 —— 不支持增量数据导入。 对导出来说也是同样的,整个过程中没有并行操作。存储流式数据到磁盘是一个单通道的过程,所以对大数据量来说并不需要特别多内存

总体来说,TinkerPop建议用户在选择图实现时,测试一下这些它自带的导入导出工具。这些工具一般比 io() 单步强很多,功能很多也很容易上手。 也就是说,图服务的提供方确实是可以优化 io() 的,一般可以用那些(他们提供的)导入导出工具的实现来覆盖TinkerPop上面描述的实现即可。

一个极好的实践是在HadoopGraph中,SparkGraphComputer内部使用CloneVertexProgram 覆盖掉了原有的单线程实现,提供了高级的OLAP类型的批量导入导出功能。使用这个模块,Hadoop的 InputFormat 或者 OutputFormat 就可以支持 任意规格的图的导入导出。

远程Gremlin控制台或者使用Gremlin语言变种(Gremlin Language Variant, GLV)的用户(比如gremlin-python),这些集成了 io() 单步的,需要记得,这些 read()write() 操作都是不是在本地发生而是在远端的服务器上。所以制定的导入导出文件必须能够 被服务器访问到

GraphSON和Gryo 格式可以为用户和图服务提供商提供串行化的扩展。可以通过 IoRegistry 的来实现这些扩展。IoRegistry 依赖使用 with() 选项和 IO.registry 键才能被使用。IO.registry 键对应的值可以在 IoRegistry 的实例或 实现类的类名这二者之一选择

g.io(someInputFile).
    with(IO.reader, IO.gryo).
    with(IO.registry, TinkerIoRegistryV3d0.instance())
  read().iterate()
g.io(someOutputFile).
    with(IO.writer,IO.graphson).
    with(IO.registry, "org.apache.tinkerpop.gremlin.tinkergraph.structure.TinkerIoRegistryV3d0")
  write().iterate()

明显的,GLVs(Gremlin语言变种)一般都会选择后者(传入类名的方式)因为它们没法创建一个 IoRegistry 实例并传递到服务器。 (IoRegistry 也不一定是按需要串行化的)

io() 集成的这些类型的版本号(例如GraphSON 2.0或3.0)是在早期 IO.readerIO.writer 的配置或者默认选项决定的。 默认的版本号永远是当前TinkerPop最新的版本。当然,图服务提供商也能够直接覆盖这些默认值,要获取详细的配置信息,需要查阅相应的图数据库文档

关于 GraphReaderGraphWriter 的更多配置信息(比如标准化GraphSON的输出,或者禁止Gryo的类注册):在创建相应的 GraphReaderGraphWriter 对象时使用 build() 方法,并直接将其应用上去就可以了。IO.readerIO.writer 的参数都可以直接接受这些对象。当然,这些基于JVM的操作对于GLVs(Gremlin语言变种)来说不可用

GraphML

gremlin graphml

GraphML 类型的文件是一种常用的基于xml图的描述方式。 它也是TinkerPop内部的一种可靠的可插拔的格式,也被各种图相关的工具和库所支持。 这样说,如果想要去让TinkerPop和外部的图数据源来连接,GraphML可能是最好的选择了。典型应用有:

  • 使用NetworkX来生成一张图,用GraphML输出并导入到TinkerPop

  • 生成一个子图,导出GraphML,并使用Gephi来生成可视化的图标.

  • 将整个图数据迁移到一个不支持TinkerPop的图数据库系统

GraphML是一种“有损”的格式,因为它只支持一些属性的原语值(primitive values),并不支持所有 Graph 结构的变量。它对原语外哪些“不认识”的将使用 toString 来串行化属性
GraphML只是一种规范,它允许 <edge> 元素(边) 和 <node> 元素(顶点)以任意顺序呈现。 很多软件系统(包括TinkerPop的 GraphMLWriter )在写GraphML时,都把 <node> 写在了 <edge> 前。 但是需要着重注意的是,GraphMLReader 读数据的顺序是很关键的。如果一个 <edge> 元素在 <node> 顶点元素前出现, 那么顶点(label)的标签就被忽略了。所以如果标签对图很重要,那么所有的 <node> 元素 必须排序在 <edge> 元素前
g.io("graph.xml").read().iterate()
g.io("graph.xml").write().iterate()
如果使用了TinkerPop 2.x 生成的GraphML,需要查阅这些不兼容的条目 升级指南.

GraphSON

gremlin graphson

GraphSON 是TinkerPop早期版本 JSON 对JSON格式的扩展 请注意TinkerPop的GraphSON是不向前兼容的。GraphSON也有一些非TinkerPop的图相关系统的支持,但它比较适合两种场景:

  • 倾向于实用文本的方式来描述图和它的元素(比如调试,代码管理等)

  • 这些图和图的元素不是被基于JVM的代码所使用(比如JavaScript, Python, .NET等)

g.io("graph.json").read().iterate()
g.io("graph.json").write().iterate()
Additional documentation for GraphSON can be found in the IO Reference.

Gryo

gremlin kryo

Kryo 是在JVM上的一个常用的串行化包。 Gremlin-Kryo 可以以二进制串行化 Graph ,并应用在可以在JVM之上运行的语言中。 它设计时考虑到了空间的优化、无损的格式转换,所以是TinkerPop图数据技术栈中的标准格式。 下面列表是一些常用的场景:

  • 将Gremlin结构体迁移到其他系统(比如从 TinkerGraphNeo4jGraph

  • 将图中单独的元素串行化,并通过网络发送到其他JVM系统中

  • 内存(in-memory)图或者子图的备份

Kryo在迁移Gremlin结构时并不损失数据,但是更重要的是要考虑 Graph 的各个功能在另外迁移到的系统中是否支持。 如果不是这样(迁移至的系统不支持),那么就会导致错误的发生
g.io("graph.kryo").read().iterate()
g.io("graph.kryo").write().iterate()

更多参考

Is Step

is() (filter) 单步可以用于使用标量条件进行过滤

is 是Python的保留字,所以要使用 is_() 来描述Gremlin

g.V().values('age').is(32)
g.V().values('age').is(lte(30))
g.V().values('age').is(inside(30, 40))
g.V().where(__.in('created').count().is(1)).values('name') (1)
g.V().where(__.in('created').count().is(gte(2))).values('name') (2)
g.V().where(__.in('created').values('age').
                           mean().is(inside(30d, 35d))).values('name') (3)
1 找到只有一个作者的项目工程
2 找到那些有大于两个作者的项目工程
3 找到作者的age在30到35之间的项目工程

更多参考

Key Step

key()(map) 单步接受一个 Property 并且从中获取键

g.V(1).properties().key()
g.V(1).properties().properties().key()

更多参考

Label Step

label()(map) 单步接受一个 Element 并且从中获取标签

g.V().label()
g.V(1).outE().label()
g.V(1).properties().label()

更多参考

Limit Step

limit() 单步是一个类似 `range()`单步 的实现,不过它的取值下界为0

g.V().limit(2)
g.V().range(0, 2)

limit() 单步同样可以使用 Scope.local,这样它就用来操作后面传入的集合 下面的例子使用 The Crew 这个模型集合

g.V().valueMap().select('location').limit(local,2) (1)
g.V().valueMap().limit(local, 1) (2)
1 使用 List<String> 来存储顶点,每个保存2个location
2 Map<String, Object> 来存储顶点,但只保存第一个属性

更多参考

Local Step

local step

GraphTraversal 操作连续的对象流。很多场景下,在对象流中操作一个元素是很关键的。所以 local() (branch) 单步就用来 做这样一个“局部对象”的遍历计算。 下面的例子使用 The Crew 这个模型集合

g.V().as('person').
      properties('location').order().by('startTime',asc).limit(2).value().as('location').
      select('person','location').by('name').by() (1)
g.V().as('person').
      local(properties('location').order().by('startTime',asc).limit(2)).value().as('location').
      select('person','location').by('name').by() (2)
1 根据startTime的历史,找到两个人和他们的相应的location
2 对每个人都找到他们出现的两个最早的出现点

除了 local() 包含起来的使用局部对象的部分遍历之外,这两个遍历看起来几乎相同。 local单步在功能上非常像Flat Map Step所以比较难以理解。 local() 将通过内部遍历将迭代器进行下去,而不是使用切分(splitting)或者克隆(cloning)来进行分发。 所以它是一个没有局部处理的“全局遍历”。它的使用很微妙,主要是用来做应用的编译优化(比如实现 TraversalStrategy)。 另外的一个例子是:

g.V().both().barrier().flatMap(groupCount().by("name"))
g.V().both().barrier().local(groupCount().by("name"))
匿名的 local() 遍历“局部的”处理当前的对象。在OLAP系统中当原子计算一个“星型图”(图单元)时, 匿名遍历本身不能离开这个局部的“星型图”的界线本身。也就说,计算不能到其他非(“星型图”)的顶点和边上去
【中文添加,方便读者理解】local 这个的解释翻译过来很拗口,主要是理解遍历本身在local中的运行。 遍历流本身一旦遇到local,看起来就是并行的执行了。内部并不是自己将自己clone一份传递到多个local遍历中,这也是文献要传达的意思, 一个是表现起来像一个flat map,其实内部实现还是一个全局的执行

更多参考

Loops Step

loops() (map) 单步提取遍历器(Traverser)当前循环的数目

g.V().emit(__.has("name", "marko").or().loops().is(2)).repeat(__.out()).values("name")

更多参考

Match Step

match()(map) 单步提供一个描述性编程类型的基于 模式匹配语义的图查询方式。 用户使用 match() 来提供一组“遍历片段”,统称为模式。这些定义的变量在执行 match() 的整个过程中都为真。 当遍历器在进行 match() 单步时,一个注册的 MatchAlgorithm 的算法将用于分析当前遍历器的状态(比如它的基于path data的历史), 运行态遍历状态的统计信息,并返回一个遍历模式,用于遍历器接下来执行。 默认的 MatchAlgorithm 算法叫做 CountMatchAlgorithm,它根据对过滤功能的模式排序来动态的调整模式匹配的执行计划。 (比如,优先执行大集合的reduce操作)。 对于一些特定的特大图,程序员也不清楚图的状态统计(比如有多少条 knows 和多少个 worksFor 的边)。这样使用 match() 操作就 很有好处,因为特定的优化执行方案就会被自动执行。再者,有些查询用 match() 来表达也比使用单路遍历容易得多。

"谁created了'lop'这个工程?并且这个created这个工程的人中,谁的age是29?返回2个创建者"
match step
g.V().match(
        __.as('a').out('created').as('b'),
        __.as('b').has('name', 'lop'),
        __.as('b').in('created').as('c'),
        __.as('c').has('age', 29)).
      select('a','c').by('name')

注意上例可以用如下这种简洁的方式来写,也表明了标准的内部遍历可以随意的被定义

g.V().match(
        __.as('a').out('created').has('name', 'lop').as('b'),
        __.as('b').in('created').has('age', 29).as('c')).
      select('a','c').by('name')

为了提高可读性,as() 单步可以提供有意义的标签来更好的映射数据域。前面的查询用下面的展示更具有描述性

g.V().match(
        __.as('creators').out('created').has('name', 'lop').as('projects'), (1)
        __.as('projects').in('created').has('age', 29).as('cocreators')). (2)
      select('creators','cocreators').by('name') (3)
1 找到“created”东西的顶点,作为“creators”,然后找到它们name为lop的顶点,作为“projects”
2 使用这些“projects”的顶点,找到created它们的29岁的成员,作为“cocreators”
3 返回“creators”和“cocreators”的name
grateful dead schema
Figure 1. Grateful Dead

Match 单步可以使Gremlin具有SPARQL类似的功能。 正如SPARQL,单步可以链接一组模式,然后应用于图上。比如说,下面的遍历过程精确查找那些Jerry Garcia创作且演唱的歌。 (使用分布在 data/ 目录的渐亡图)

g = graph.traversal()
g.io('data/grateful-dead.xml').read().iterate()
g.V().match(
        __.as('a').has('name', 'Garcia'),
        __.as('a').in('writtenBy').as('b'),
        __.as('a').in('sungBy').as('b')).
      select('b').values('name')

match() 有一些功能是与SPARQL不同的,例如:

g.V().match(
        __.as('a').out('created').has('name','lop').as('b'), (1)
        __.as('b').in('created').has('age', 29).as('c'),
        __.as('c').repeat(out()).times(2)). (2)
      select('c').out('knows').dedup().values('name') (3)
1 任意复杂度的模式match() 并不严格要求模式不超过三个,或者仅限于属性路径
2 支持递归match() 支持在模式内使用单步进行判断择路,包含使用 repeat()
3 命令式/声明式语义的混合:无论是在 `match()`单步前后都可以使用经典的Gremlin遍历

对#3的引申理解:可以将指令式语义转换为声明式语义,或者指令式本身,无限种可能

g.V().match(
        __.as('a').out('knows').as('b'),
        __.as('b').out('created').has('name','lop')).
      select('b').out('created').
        match(
          __.as('x').in('created').as('y'),
          __.as('y').out('knows').as('z')).
      select('z').values('name')
match() 单步是无状态的。遍历模式相关联的变量都存在遍历器的历史路径里。所以, 这些用于遍历中 match() 单步使用的变量,全局来说是唯一的。这样的好处是随后的 where()select()match() 等单步就可以使用一个相同的变量作分析

正如Gremlin所有的单步一样,match() 是个函数, 所以在 match() 函数能有内部的 match() ,这种使用方式是由Gremlin函数式运算的本质设计的自然结果。 (比如递归匹配)

g.V().match(
        __.as('a').out('knows').as('b'),
        __.as('b').out('created').has('name','lop'),
        __.as('b').match(
                     __.as('b').out('created').as('c'),
                     __.as('c').has('name','ripple')).
                   select('c').as('c')).
      select('a','c').by('name')

如果一个使用了 match() 单步的遍历被某一步打了标签(如as),那么使用了 match() 的遍历器肯定也注定要绑一个特殊变量, 那么前面的单步就应该相应的打好标签

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() 遍历模式

  1. as('a')...as('b'):遍历的开始和结束都有声明变量

  2. as('a')...:只有遍历开始有声明的变量

  3. ...:没有生命的变量

如果在遍历模式的开头有一个变量那么它就 必须 当成一个在遍历历史路径中的标签而存在,否则遍历器就没法按照这个路径走下去。 如果遍历模式的后面有一个变量,那么情况是:如果变量已经存在于遍历的历史路径里,当前遍历器的位置 必须 与处于相同标签的历史位置匹配, (比如相等)。然而,如果遍历器历史路径中不存在变量,那么当前的位置就被当成变量打上标签,变成一个为后续模式匹配而存在的相关变量。 如果模式匹配最后没有跟随一个结束标签,那么遍历器必须要在模式匹配中“幸存”(比如不因为没有匹配结果而被过滤掉)才能执行下一个模式匹配。 如果遍历模式没有开始标签,那么遍历器就能够在任意点沿路径执行,但只在这个模式上执行一次。因为遍历模式只执行一次且在遍历器的历史记录上也是只执行一次。 一个典型的用法是没有开始和结束标签的遍历模式一般和 and()or()where() 相连接。 一旦遍历器在所有的模式匹配中“幸存”(或者最少有一个 or() 存在),match() 将分析遍历器历史路径并生成一个 Map<String,Object> 绑定到遍历的下个单步操作

【中文添加,方便读者理解】这段话相当晦涩。其实要传达的意思是遍历中的变量的传递和自动绑定关系。一个as(label)实际上在一个Map结构中创建了 一个以label为key的对象,这个Map在这次遍历中是全局的(如上文)。这样,match()中的匿名遍历"__"引用的as就会尝试做自动绑定。 如果Map中有,就会按照label为key取出值,如果没有就会自动创建——所以区分了在遍历模式前标签和后标签的行为。另外的,匿名遍历其实是相当于回到遍历的开头。
g.V().as('a').out().as('b'). (1)
    match( (2)
      __.as('a').out().count().as('c'), (3)
      __.not(__.as('a').in().as('b')), (4)
      or( (5)
        __.as('a').out('knows').as('b'),
        __.as('b').in().count().as('c').and().as('c').is(gt(2)))).  (6)
    dedup('a','c'). (7)
    select('a','b','c').by('name').by('name').by() (8)
1 一个标准的在 match() 前对遍历打标签的操作
2 如果遍历器在进入 match() 的路径前有必须的标签,那么这些历史值就会被绑定
3 可以使用barrier单步尽管它对模式进行本地计算(正如期望结果)
4 也可以使用 not() 来对模式取非
5 可以嵌套 and()or() 单步来连接匹配过程
6 中缀和前缀连接符号都可以支持
7 可以“区分”指定的标签组合
8 绑定的值可以是不同的类型 —— 顶点a,顶点b,和long型的值c

使用 Where 来 Match

Match单步通常与 select() (前文已述)和 where() 来连接,接下来讨论 where() 单步允许用户来对 match() 提供的结果集合进行进一步约束

g.V().match(
        __.as('a').out('created').as('b'),
        __.as('b').in('created').as('c')).
        where('a', neq('c')).
      select('a','c').by('name')

where() 单步可以传入一个 P 谓词函数(如上例)或者一个遍历本身,如下。 使用 MatchPredicateStrategy 时,where() 语句自动被折叠到 match() 中所以它同样可以被 match() 中的优化器覆盖到。

traversal = g.V().match(
                    __.as('a').has(label,'person'), (1)
                    __.as('a').out('created').as('b'),
                    __.as('b').in('created').as('c')).
                    where(__.as('a').out('knows').as('c')). (2)
                  select('a','c').by('name'); null (3)
traversal.toString() (4)
traversal (5) (6)
traversal.toString() (7)
1 所有用于key匹配的 has() 单步遍历模式都会被拉到 match() 外面,让图系统能够利用索引查找来筛选过滤
2 where() 单步传入了一个遍历,其中使用了 match() 单步中使用的绑定的变量
3 一个有用的技巧,保证遍历不被Gremlin控制台进行迭代
4 在计划执行前查看遍历的文本描述
5 Gremlin 控制台将自动迭代执行迭代器,或者可迭代执行的对象
6 marko 和 josh 都是协同开发者并且 marko 认识 josh
7 在执行计划被应用后,查看遍历的文本描述(从而 where() 被叠到 match() 里面)
where() 单步是一个过滤步,所以 where() 语句中的变量并不全局绑定到 match() 的遍历器路径里去。

更多参考

Math Step

math() (math) 单步为Gremlin提供了科学计算功能。与普通的数学函数组合和嵌套的形式稍有不同,单步提供了一种可读性更好的 基于文本描述的数学处理器。 表达式中的变量被映射到Gremlin的作用域中——比如路径标签,副作用,或传入的映射的key等。 这个单步支持使用 by() 来按照变量首次在算式中被引用的顺序来进行调整。 注意“__”这个保留变量指的是当前传入 math() 单步的数字遍历器对象

g.V().as('a').out('knows').as('b').math('a + b').by('age')
g.V().as('a').out('created').as('b').
  math('b + a').
    by(both().count().math('_ + 100')).
    by('age')
g.withSideEffect('x',10).V().values('age').math('_ / x')
g.withSack(1).V(1).repeat(sack(sum).by(constant(1))).times(10).emit().sack().math('sin _')

运算其中支持的运算符包括:*+\^%。 另外,也支持下面的内置函数:

  • abs: 绝对值

  • acos: 反余弦

  • asin: 反正弦

  • atan: 反正切

  • cbrt: 立方根

  • ceil: 向上取整

  • cos: 余弦

  • cosh: 双曲余弦

  • exp: 自然指数 (e^x

  • floor: 向下取整

  • log: 自然对数(注意不是ln)

  • log10: 对数(10为底)

  • log2: 对数(2为底)

  • sin: 正弦

  • sinh: 双曲正弦

  • sqrt: 平方根

  • tan: 正切

  • tanh: 双曲正切

  • signum: 符号函数

更多参考

Max Step

max() (map) 单步找到可对比对象流中按序后的最后一个对象。

g.V().values('age').max()
g.V().repeat(both()).times(3).values('age').max()
g.V().values('name').max()
max(local) 找到当前局部对象的最大值(而不是遍历流的最大值)。 这对于 CollectionComparable 类型对象生效

更多参考

Mean Step

mean() (map) 单步从一串流数字中计算出平均值

g.V().values('age').mean()
g.V().repeat(both()).times(3).values('age').mean() (1)
g.V().repeat(both()).times(3).values('age').dedup().mean()
1 要注意遍历器被 repeat() 批量化了,所以有一些数字比其余的多,从而改变了平均值
mean(local) 计算的是当前的局部变量,而不是遍历流中的对象。 这对于 CollectionNumber 类型对象生效

更多参考

Min Step

The min() (map) 单步找到可对比对象流中按序后的第一个对象。

g.V().values('age').min()
g.V().repeat(both()).times(3).values('age').min()
g.V().values('name').min()
min(local) 找到当前局部对象的最小值(而不是遍历流的最小值)。 这对于 CollectionComparable 类型对象生效

更多参考

None Step

none() (filter) 把遍历流中所有的对象都过滤掉。它在两种场景下非常有用,一种是过滤远程执行的无用结果,一种是遍历器本身也不想产生什么副作用。 选择不返回对象的好处就是在服务端过滤掉结果,从而在客户端测上节省串行化和网络传输的开销。 通常这个单步不直接使用,iterate() 这个终止性单步在实际迭代结果前,遍历将被静默被添加一个 none()

更多参考

Not Step

not() (filter) 单步首先确认传入遍历返回的结果对象,然后在遍历流中删除这些对象

not 在Groovy中是个保留字,所以在用作匿名遍历一部分来使用Gremlin时,必须增加双下划线来引用 __.not()

not 在Python中也是个保留字,所以如果使用Gremlin应该用 not_()

g.V().not(hasLabel('person')).valueMap().with(WithOptions.tokens)
g.V().hasLabel('person').
  not(out('created').count().is(gt(1))).values('name')   (1)
1 josh 创建了两个工程,但vadas 没有创建任何的工程

更多参考

Option Step

branch()choose() 单步的选择项

更多参考

Optional Step

optional() (branch/flatMap) 单步如果特定的遍历返回结果,那么将结果返回,否则就返回调用元素(比如 identity())。

g.V(2).optional(out('knows')) (1)
g.V(2).optional(__.in('knows')) (2)
1 vadas 没有外向的knows边,所以返回vadas本身
2 vadas 的确有一条knows的in边,所以返回了marko

当与 path 或者 tree 连接使用时, optional 在拉出整个图时特别有用

g.V().hasLabel('person').optional(out('knows').optional(out('created'))).path() (1)
1 找到所有人中谁认识谁,然后接下来显示他创建的项目的每条路径

更多参考

Or Step

or() (filter) 单步保证最少一个提供的遍历能返回结果。请查阅and() 语句

or 在Python中是个保留字,所以如果使用Gremlin应该用 or_().

g.V().or(
   __.outE('created'),
   __.inE('created').count().is(gt(1))).
     values('name')

or() 可以接受任意多个遍历。最少一个遍历产生至少一个结果才能让原来的遍历进行到下一步

同样可以使用中缀标识法

g.V().where(outE('created').or().outE('knows')).values('name')

更多参考

Order Step

如果遍历流中的对象需要排序,那么就可以利用 order() (map) 单步来做

g.V().values('name').order()
g.V().values('name').order().by(desc)
g.V().hasLabel('person').order().by('age', asc).values('name')

元素 Element 是遍历中最多被访问的对象。元素可以有很多个相关的属性(比如键值对)。 在很多场景下,一般都倾向于使用属性的对比来对遍历流中的对象来进行排序

g.V().values('name')
g.V().order().by('name',asc).values('name')
g.V().order().by('name',desc).values('name')

order() 单步允许用户提供任意多个应用于主和次的对比器(排序中使用)。 正如下例所示,主排序是根据外向的created的边的总数,而次排序是根据每个人的年龄

g.V().hasLabel('person').order().by(outE('created').count(), asc).
                                 by('age', asc).values('name')
g.V().hasLabel('person').order().by(outE('created').count(), asc).
                                 by('age', desc).values('name')

可以 Order.shuffle 来在为遍历的特定的点上来进行随机排序

g.V().hasLabel('person').order().by(shuffle)
g.V().hasLabel('person').order().by(shuffle)

order(local) 排序的也是当前的局部变量,而不是遍历流中的对象。 这对于 CollectionMap 类型对象生效。遇到其他类型对象就原样地被返回。

g.V().values('age').fold().order(local).by(desc) (1)
g.V().values('age').order(local).by(desc) (2)
g.V().groupCount().by(inE().count()).order(local).by(values, desc) (3)
g.V().groupCount().by(inE().count()).order(local).by(keys, asc) (4)
1 将年龄收集到链表中,然后按降序对该列表进行排序
2 这些年龄,由于没有收集起来,所以 order(local) 相当于对单个数字的集合排序,什么也不做
3 groupCount() 集合中元素按照降序对values排序(见后面的NOTE,keys是Column这个枚举中的一个值)
4 groupCount() 集合中元素按照升序对keys进行排序(同见后面的NOTE)
valueskeys 都是 Column 这个枚举中的元素(内部实现),用于从 MapMap.EntryPath 中获取 "columns"
在3.3.4版本之前,排序是依靠 Order.incr,降序靠 Order.decr。这些实现已经废弃,建议使用示例中的查询样式, 这样的格式更常见。

更多参考

PageRank Step

pageRank() (map/sideEffect) 使用PageRankVertexProgram 来计算PageRank

pageRank() 单步是一个 VertexComputing(顶点计算)的单步,所以只有支持 GraphComputer (OLAP)的图系统才能使用。
g = graph.traversal().withComputer()
g.V().pageRank().by('pageRank').values('pageRank')
g.V().hasLabel('person').
  pageRank().
    with(PageRank.edges, __.outE('knows')).
    with(PageRank.propertyName, 'friendRank').
  order().by('friendRank',desc).valueMap('name','friendRank')

注意可以使用 with() 来调整算法的配置参数, 它使用 PageRank 这个结构提供的keys就可以自动的导入到Gremlin控制台上

explain() 单步也能够帮助来理解遍历器怎样编译和分发 GraphComputer 任务

g = graph.traversal().withComputer()
g.V().hasLabel('person').
  pageRank().
    with(PageRank.edges, __.outE('knows')).
    with(PageRank.propertyName, 'friendRank').
  order().by('friendRank',desc).valueMap('name','friendRank').explain()

更多参考

Path Step

在遍历中,遍历器当进行一些单步后,就会发生变化。这些遍历器的历史记录就可以用 path() 单步来检测它的路径实现。

path step
g.V().out().out().values('name')
g.V().out().out().values('name').path()

如果遍历器需要展示边,那么遍历器就要显式的遍历过这些边(如下面实例的outE)

g.V().outE().inV().outE().inV().path()

遍历路径的元素,也能延迟的被 by() 来以round-robin的形式处理

g.V().out().out().path().by('name').by('age')

最后,因为 by() 的延迟处理导致没法避免触发下一次遍历。 下面的遍历中,对每一个路径中便利过得对象,如果是一个person,那么就获取他created的东西;如果是一个创作品(工程), 那么就获取到所有创建它的人。

g.V().out().out().path().by(
                   choose(hasLabel('person'),
                                 out('created').values('name'),
                                 __.in('created').values('name')).fold())
遍历器的历史路径都存在Java的List结构中,所以生成路径信息本身这个操作就非常昂贵。遍历中有多少个遍历器,就有多少个List结构。 多说一句,在一个OLAP GraphComputer环境里,由于有太多顶点的并发访问,列举路径基本是不可能的。 在OLAP系统中的确也存在一些供便利其选择的优化方式,但当路径本身在计算的时(并且遍历器由于它的历史路径唯一时),优化本身也是无效的

Path Data Structure

Path 数据结构是一个有序对象的List链表结构。而每个对象又有一个 Set<String> 的标签项和其绑定。下面的给出的示例展示了 Path API的使用和一个遍历如何生成带标签的路径

path data structure
path = g.V(1).as('a').has('name').as('b').
              out('knows').out('created').as('c').
              has('name','ripple').values('name').as('d').
              identity().as('e').path().next()
path.size()
path.objects()
path.labels()
path.a
path.b
path.c
path.d == path.e

更多参考

PeerPressure Step

peerPressure() (map/sideEffect) 单步使用 PeerPressureVertexProgram 来对顶点进行归类

peerPressure() 单步属于 VertexComputing 步骤之一,所以它只能在支持 GraphComputer (OLAP) 的图系统中使用
g = graph.traversal().withComputer()
g.V().peerPressure().by('cluster').values('cluster')
g.V().hasLabel('person').
  peerPressure().
    with(PeerPressure.propertyName, 'cluster').
  group().
    by('cluster').
    by('name')

注意可以使用 with() 来调整算法的配置参数, 它使用 PeerPressure 这个类结构提供的keys就可以自动的导入到Gremlin控制台上

更多参考

Profile Step

profile() (sideEffect) 单步是用来允许开发者来采样分析遍历的统计信息,比如单步执行,计数器等

对编译器进行采样分析会对遍历本身造成性能影响。这些额外开销大多数可以从采样分析结果中去除,但统计时间并不精确。 所以,最好通过两个时间对比来看(性能)
g.V().out('created').repeat(both()).times(3).hasLabel('person').values('age').sum().profile()

profile() 单步生成一个 TraversalMetrics “副作用”对象,它包含如下信息:

  • Step:当前在遍历中被分析的单步

  • Count:通过该单步的 呈现 的遍历器的计数

  • Traversers:通过单步的遍历器的数目

  • Time (ms):发生在此单步的总执行时间

  • % Dur:当前单步耗时比例

gremlin exercise

理解 CountTraversers 这两栏的区别很重要。遍历器是可以合并的,所以当两个遍历器走“相同”路时他们就被聚合成一个了。 这个新的遍历器有一个 Traverser.bulk() 来计算两个合并遍历器的bulk批量。另外的一点是,Count 代表的是这些 Traverser.bulk() 的和所以 呈现 而非列举了所有遍历器。所以 Traversers 栏将永远小于等于 Count

同样可以提供一个保存“副作用”的键给 profile() 单步,这样可以在一些场景下用来先执行遍历的迭代,后续步骤来获取 TraversalMetrics, 如下所示:

t = g.V().out('created').profile('metrics')
t.iterate()
metrics = t.getSideEffects().get('metrics')

For traversal compilation information, please see explain()-step.

更多参考

Project Step

project() (map) 单步可以把当前对象映射成一个用提供的标签来索引的 Map<String,Object> 结构里。 它很像select() 单步,只保存而不是获取并调整遍历器历史状态,但调整当前遍历器的状态。

g.V().out('created').
  project('a','b').
    by('name').
    by(__.in('created').count()).
  order().by(select('b'),desc).
  select('a')
g.V().has('name','marko').
               project('out','in').
                 by(outE().count()).
                 by(inE().count())
【中文添加,方便读者理解】Project 可以理解为一个格式化输出接口:它可以将数据组织下,然后传递到后面的单步。但是,在组织数据的时候, 如果后面的格式化输出同样操作了遍历器,那么它就被执行了,发生了内部状态变化。这就是“但调整当前遍历器的状态”的含义。就好比格式化输出一个字符串, 而打印这个字符串的时候,同时打印函数改变了内部变量一样。只能说明是通过引用来实现project,克隆

更多参考

Program Step

program() (map/sideEffect) 单步是用于在 GraphComputer 执行lambda表达式的单步。 传入一个VertexProgram 参数,且用于后来的相应的图处理。 所以,用户可以创建一个 VertexProgram 的实现并在遍历中执行,VertexProgram提供的配置参数有:

  • gremlin.vertexProgramStep.rootTraversal 是一个 PureTraversal 类型的根遍历的串行化

  • gremlin.vertexProgramStep.stepIdprogram() 这个单步的执行时字符串类型的id

用户提供的 VertexProgram 能有效利用他们顶点的相关联信息。例如以下的使用示例。

开发 VertexProgram 是一个专家级行为。另外,做好一个能够高效在遍历中使用的 VertexProgram 也需要很多实践来佐证。 强烈推荐高级用户来阅读并深层次理解 Gremlin OLAP (GraphComputer)机制
private TraverserSet<Object> haltedTraversers;

public void loadState(Graph graph, Configuration configuration) {
  VertexProgram.super.loadState(graph, configuration);
  this.traversal = PureTraversal.loadState(configuration, VertexProgramStep.ROOT_TRAVERSAL, graph);
  this.programStep = new TraversalMatrix<>(this.traversal.get()).getStepById(configuration.getString(ProgramVertexProgramStep.STEP_ID));
  // 如果遍历的“副作用”在计算中将被使用,那么将其添加为一个内存计算key
  this.memoryComputeKeys.addAll(MemoryTraversalSideEffects.getMemoryComputeKeys(this.traversal.get()));
  // 如果主遍历要被复制,那么也创建一个内存计算的key
  this.memoryComputeKeys.add(MemoryComputeKey.of(TraversalVertexProgram.HALTED_TRAVERSERS, Operator.addAll, false, false));
  // 如果没有停止的遍历器,那么就返回一个空遍历器
  this.haltedTraversers = TraversalVertexProgram.loadHaltedTraversers(configuration);
}

public void storeState(Configuration configuration) {
  VertexProgram.super.storeState(configuration);
  // 如果停止的遍历器为空,那么什么也不做
  TraversalVertexProgram.storeHaltedTraversers(configuration, this.haltedTraversers);
}

public void setup(Memory memory) {
  if(!this.haltedTraversers.isEmpty()) {
    // 这里做你想对哪些停止的主遍历做的事情
  }
  // 一旦做了操作,也就不用在主遍历器里保留这些信息了
  this.haltedTraversers = null;
}

public void execute(Vertex vertex, Messenger messenger, Memory memory) {
  // 一旦做了操作,也就不用在工作线程里保留这些信息了
  once used, no need to keep that information around (workers)
  if(null != this.haltedTraversers)
    this.haltedTraversers = null;
  if(vertex.property(TraversalVertexProgram.HALTED_TRAVERSERS).isPresent()) {
    // 在 execute() 中的haltedTraversers 代表那些正在进行遍历的遍历器(worker-traversal)
    // 比如,从g.V().out().program(...) 而来的遍历
    TraverserSet<Object> haltedTraversers = vertex.value(TraversalVertexProgram.HALTED_TRAVERSERS);
    // 创建一个被停止的遍历器集合,一边在后续OLAP 任务链中使用
    // 这些正在工作的遍历器可能分布在整个图中
    TraverserSet<Object> newHaltedTraversers = new TraverserSet<>();
    haltedTraversers.forEach(traverser -> {
       newHaltedTraversers.add(traverser.split(traverser.get().toString(), this.programStep));
    });
    vertex.property(VertexProperty.Cardinality.single, TraversalVertexProgram.HALTED_TRAVERSERS, newHaltedTraversers);
    // 可以在主遍历中,创建一些主遍历器(这也是将最终结果返回给用户的方法)
    memory.add(TraversalVertexProgram.HALTED_TRAVERSERS,
               new TraverserSet<>(this.traversal().get().getTraverserGenerator().generate("an example", this.programStep, 1l)));
  }

public boolean terminate(Memory memory) {
  // 主遍历器将包含很多停止的遍历器
  assert memory.exists(TraversalVertexProgram.HALTED_TRAVERSERS);
  TraverserSet<String> haltedTraversers = memory.get(TraversalVertexProgram.HALTED_TRAVERSERS);
  // 这里只有通过memory内存将这些遍历器发送给主遍历
  assert haltedTraversers.stream().map(Traverser::get).filter(s -> s.equals("an example")).findAny().isPresent();
  // 这里将不包含分布在各个顶点上的工作遍历器
  assert !haltedTraversers.stream().map(Traverser::get).filter(s -> !s.equals("an example")).findAny().isPresent();
  return true;
}
gremlin-test 中的 ProgramTest 测试实例中有一个叫做 TestProgram 的示例程序。它展示了各种在顶点程序中 复制遍历和遍历器信息到后续顶点OLAP计算链的方法

最后,一个使用 PageRankVertexProgram 而不是用pageRank() 单步的例子:

g = graph.traversal().withComputer()
g.V().hasLabel('person').
  program(PageRankVertexProgram.build().property('rank').create(graph)).
    order().by('rank', asc).
  valueMap('name', 'rank')

Properties Step

properties() (map) 单步从遍历流中提取单个 Element 的属性

g.V(1).properties()
g.V(1).properties('location').valueMap()
g.V(1).properties('location').has('endTime').valueMap()

更多参考

PropertyMap Step

propertiesMap() 单步生成一个用于描述元素诸多属性的Map结构数据

g.V().propertyMap()
g.V().propertyMap('age')
g.V().propertyMap('age','blah')
g.E().propertyMap()

更多参考

Range Step

遍历过程中会生成很多遍历器执行,那么就可以使用 range() (filter) 单步来传入参数来允许特定的一部分遍历器执行。 当区间下界没有满足时,对象仍旧在被迭代。但在区间内(前闭后开)里是迭代器才被发出。当大于区间上界时,遍历将直接跳出迭代。 最后,如果上界值是 -1,那么所有在下界往上的迭代器都会被发出

g.V().range(0,3)
g.V().range(1,3)
g.V().range(1, -1)
g.V().repeat(both()).times(1000000).emit().range(6,10)

range() 单步也可以结合 Scope.local 来试用,这种情况下就是操作接下来的集合。下例就是可以为每条遍历路径生成一个 Map<String, String> 结构,但只包含第二个属性值(b这一步)

g.V().as('a').out().as('b').in().as('c').select('a','b','c').by('name').range(local,1,2)

接下来的例子就是使用 The Crew 这个数据模型,它生成一个只包含顶点第二和第三位置信息的 List<String>

g.V().valueMap().select('location').range(local, 1, 3)

更多参考

Repeat Step

gremlin fade

repeat() (branch) 单步用于按退出条件(predicate)来循环执行遍历。下面是使用 repeat() 的例子

g.V(1).repeat(out()).times(2).path().by('name') (1)
g.V().until(has('name','ripple')).
      repeat(out()).path().by('name') (2)
1 do-while 语义,连续执行 out 两次
2 while-do 语义,如果遍历器遇到顶点name是ripple就退出
repeat() 有两个调整器:until()emit()。 如果 until()repeat() 后面出现那就是do/while循环。 如果 until()repeat() 前面就是while/do循环。 如果 emit() 是在 repeat() 之后,那么就用于评估遍历器如何离开这个循环遍历。 如果 emit() 是在 repeat() 之前,那么就用于评估遍历器进入循环遍历之前对其进行评估。

repeat() 单步同样支持“发出谓词”的操作,如果 emit() 中传入空参数就是认为是 true 所以无条件发出, (emit() == emit{true})。有了 emit() 后遍历器就被分成两步了,遍历器将退出代码段后再执行代码段(假如 until() 一直为真)

g.V(1).repeat(out()).times(2).emit().path().by('name') (1)
g.V(1).emit().repeat(out()).times(2).path().by('name') (2)
1 emit()repeat() 后,所以发出操作在 repeat() 遍历执行后才开始。所以没有单顶点的路径存在。
2 emit()repeat() 前,那么所有发出操作在 repeat() 遍历执行前就开始了,所以存在单顶点路径

emit() 调节器可以接受任意多个谓词参数

g.V(1).repeat(out()).times(2).emit(has('lang')).path().by('name')
repeat step
g.V(1).repeat(out()).times(2).emit().path().by('name')

第一次执行 repeat() 后,顶点lop,vadas 和 josh 就可见了。这时 loops==1 所以遍历器继续。 但是因为发出谓词函数此处为真,所以这些顶点被发出(到遍历器)了。当执行下次 repeat() 时,遍历走过的顶点是ripple和lop (如图,Josh created了两个项目,而lop和vadas没有出边),这时 loops==2,那么until 判断就失败,所以ripple和lop也被发出了。 最终的遍历器可以看到的顶点有lop,vadas,josh,ripple和lop.

repeat() 单步也可以被嵌套到其他repeat中,或者 emit() until() 这些谓词中也都可以。 可以向它传递一个字符串,作为第一个参数来命名这个 repeat() 。在“命名”循环中, 可以使用 loopName (就是创建 repeat() 时传递的那个字符串)传递到 loops(loopName) 单步中,从而获取循环的次数

g.V(1).
  repeat(out("knows")).
    until(repeat(out("created")).emit(has("name", "lop"))) (1)
g.V(6).
  repeat('a', both('created').simplePath()).
    emit(repeat('b', both('knows')).
           until(loops('b').as('b').where(loops('a').as('b'))).
  hasId(2)).dedup() (2)
1 从顶点1开始,一直沿着knows的外向边游走,知道找到有创建lop顶点的顶点
2 从顶点6开始,双向的游走created的边,找到那些顶点(与顶点6)有着与顶点2 knows的顶点同距离的那些顶点

最后注意 emit()until() 都可以接受一个traversal

最后,注意 emit()until() 都可以传入一个遍历,在这种情况下,判断的谓词就是 traversal.hasNext() 下面有几个例子

g.V(1).repeat(out()).until(hasLabel('software')).path().by('name') (1)
g.V(1).emit(hasLabel('person')).repeat(out()).path().by('name') (2)
g.V(1).repeat(out()).until(outE().count().is(0)).path().by('name') (3)
1 从顶点1开始,沿向外边游走,直到遇到一个software顶点
2 从顶点1开始的无限循环里,判断如果是person的顶点就发出,并沿外向边游走
3 从顶点1开始,一直沿外向边游走,直到找到没有边的顶点
emit()until() (非 repeat())的匿名遍历本地的处理他们的当前对象。 在OLAP系统中,计算的原子单元是顶点本身和它相关联的“星型图”,匿名遍历不离开星型图本身的界限很重要。 换句话说,它们不能游走到其他相邻的顶点或者边。

更多参考

Sack Step

gremlin sacks running

遍历器可以携带一个内部结构,叫做“sack”。sack() (sideEffectmap) 单步就用来读写这些sacks。 每个遍历器可以使用 GraphTraversal.withSack(initialValueSupplier,splitOperator?,mergeOperator?) 来创建sack。

  • Initial value supplier: 一个用于提供遍历器sack初始值的 Supplier

  • Split operator: 一个当遍历器分列时克隆sack的 UnaryOperator (一元描述符,java functional接口) , 如果没有给定分裂操作描述符,那么就使用 UnaryOperator.identity()

  • Merge operator: 一个能够在遍历器归并时将sack合并的 BinaryOperator ,如果没有提供合并运算符, 那么两个有sack的遍历器则不能合并

下面两个日常的例子来说明这些 initial value supplier。在下面最前的例子,在图(g.V())中每个顶点创建一个遍历器,携带着一个 值为1.0的sack,并且想去获取这些sack(sack())。下面的例子是,使用一个float类型的随机数生成器来填充sack

g.withSack(1.0f).V().sack()
rand = new Random()
g.withSack {rand.nextFloat()}.V().sack()

下面是一个比较复杂的 initial value supplier 例子,sack本身被用于一个运行的计算中,并到遍历最后被发出。 当边在遍历时将与权值sack相乘。注意by() 协调步可以是任意的遍历。

more complicated initial value supplier example is presented below where the sack values are used in a running computation and then emitted at the end of the traversal. When an edge is traversed, the edge weight is multiplied by the sack value (sack(mult).by('weight')). Note that the by()-modulator can be any arbitrary traversal.

g.withSack(1.0f).V().repeat(outE().sack(mult).by('weight').inV()).times(2)
g.withSack(1.0f).V().repeat(outE().sack(mult).by('weight').inV()).times(2).sack()
g.withSack(1.0f).V().repeat(outE().sack(mult).by('weight').inV()).times(2).path().
      by().by('weight')
gremlin sacks standing

当使用复杂对象时(比如非原语变量)时,为了保证每个遍历器能够克隆一份父遍历器的sack,split operator 是需要定义的。 第一个示例不使用拆分运算符,因此相同的map将被传播到所有遍历器(全局数据结构)。第二个例子展示了 Map.clone() 是如何 保证每个遍历器拿到一个独一无二的局部的sack

g.withSack {[:]}.V().out().out().
      sack {m,v -> m[v.value('name')] = v.value('lang'); m}.sack() // BAD: single map
g.withSack {[:]}{it.clone()}.V().out().out().
      sack {m,v -> m[v.value('name')] = v.value('lang'); m}.sack() // GOOD: cloned map
对于原语类型变量(即整数,长整数,浮点数等),不需要拆分运算符,因为它们在sack中被编码成地址,而不是作为对象引用

如果没有提供 merge operator ,那么有sack的那些遍历器就不能被批量(归并)。但是在很多场景下,在两个遍历器同一个位置上对sack进行归并 是合理的,并能够进行批量优化。下面的示例中就是用 Operator.sum 这个归并运算符,所以当两个遍历器归并时,它们响应的sack直接相加

g.withSack(1.0d).V(1).out('knows').in('knows') (1)
g.withSack(1.0d).V(1).out('knows').in('knows').sack() (2)
g.withSack(1.0d, sum).V(1).out('knows').in('knows').sack() (3)
g.withSack(1.0d).V(1).local(outE('knows').barrier(normSack).inV()).in('knows').barrier() (4)
g.withSack(1.0d).V(1).local(outE('knows').barrier(normSack).inV()).in('knows').barrier().sack() (5)
g.withSack(1.0d,sum).V(1).local(outE('knows').barrier(normSack).inV()).in('knows').barrier().sack() (6)
g.withBulk(false).withSack(1.0f,sum).V(1).local(outE('knows').barrier(normSack).inV()).in('knows').barrier().sack() (7)
g.withBulk(false).withSack(1.0f).V(1).local(outE('knows').barrier(normSack).inV()).in('knows').barrier().sack()(8)
1 顶点1被输出两次,因为它认识两个其他人
2 没有归并的操作,两个sack的值都是1.0
3 当在归并操作中指定了 sum,sack的值因为批量归并所以变成2.0
4 和1相同,但是内部使用栅栏(barrier)
5 local(...barrier(normSack)...) 保证所有离开顶点1的遍历器均分原始的1.0 “能量” (50-50均分),比如每个结果都是0.5的sack
6 和3相同,但是用 sum 归并了,导致结果是1.0
7 这里是一个打包后为为2的遍历器但sack为1.0的遍历器,通过withBulk(false)后结果是1.0
8 与7相同,但是没有sum这个操作符

更多参考

Sample Step

sample() 单步的主要作用是采样某个数目的前遍历中的遍历器

g.V().outE().sample(1).values('weight')
g.V().outE().sample(1).by('weight').values('weight')
g.V().outE().sample(2).by('weight').values('weight')

sample() 的一个比较有意思的例子是用于和local() 连接。 这两个单步联合起来可以用于执行随机游走 在下例中,遍历从顶点1来时,根据每条边的权值的概率分布来选择游走边。因为只选择一条边,所以输出永远是一条单一路径。 遍历器不会分裂,只会沿着一条路径走

g.V(1).repeat(local(
         bothE().sample(1).by('weight').otherV()
       )).times(5)
g.V(1).repeat(local(
         bothE().sample(1).by('weight').otherV()
       )).times(5).path()
g.V(1).repeat(local(
         bothE().sample(1).by('weight').otherV()
       )).times(10).path()

声明注意,上例因为只是一个顶点的随机遍历,local() 并不是一定必须的,但注意如果对于多个顶点遍历的情况,没有它会发生:

g.V().repeat(bothE().sample(1).by('weight').otherV()).times(5).path()

使用 local() 能够保证遍历器没经过一个顶点,就通过 bothE() 遍历一次,并且只允许每顶点只游走一步

g.V().repeat(local(bothE().sample(1).by('weight').otherV())).times(5).path()

所以,虽然不是严格要求,但最好显式地使用 local() ,这样可以更正确地描述遍历的意图

更多参考

Select Step

函数式编程 使用函数的组合和缓式求值(lazy evaluation)将元操作组合成复杂的计算形式。 正如 Traversal 遍历本身。但从一个不同的角度来说,Gremlin数据流的实现与图的处理的区别有差异,主要是数据流并不一定总是“向前”的,实际上, 也能够向后看到先前已经计算的区域。例子包含path() 也包含 select() (map) 单步。select() 单步有两种通用使用方式:

  1. 选取路径中打标签的单步(比如在遍历中用 as() 定义的)

  2. 在一个 Map<String,Object> 流中将对象滤出(比如,子图)

第一种情况可以见下例

g.V().as('a').out().as('b').out().as('c') // no select
g.V().as('a').out().as('b').out().as('c').select('a','b','c')
g.V().as('a').out().as('b').out().as('c').select('a','b')
g.V().as('a').out().as('b').out().as('c').select('a','b').by('name')
g.V().as('a').out().as('b').out().as('c').select('a') (1)
1 如果选择只有一个,那么就不返回map类型了

如果只选择一个标签,那么就返回一个单独对象(而不是map)。针对于退出计算并根据返回的对象再次向前遍历很有用

g.V().out().out()
g.V().out().out().path()
g.V().as('x').out().out().select('x')
g.V().out().as('x').out().select('x')
g.V().out().out().as('x').select('x') // pointless
当在一个标准的遍历引擎(比如OLTP)里使用 select() 进行遍历时,select() 将尽力避免计算历史路径,而是依赖一个 全局数据结构来存储当前选择的对象。所以,如果只需要一个走过路径的一个子集,那么 select() 应当优于 path() 使用, 因为后者属于资源密集型

如果需要一组路径键或值(比如columns),或者map本身,可以相应的使用 select(keys)select(values)。 如果只对 groupCount() 排名中的top N元素感兴趣,那么这样使用就很有用

g = graph.traversal()
g.io('data/grateful-dead.xml').read().iterate()
g.V().hasLabel('song').out('followedBy').groupCount().by('name').
      order(local).by(values,desc).limit(local, 5)
g.V().hasLabel('song').out('followedBy').groupCount().by('name').
      order(local).by(values,desc).limit(local, 5).select(keys)
g.V().hasLabel('song').out('followedBy').groupCount().by('name').
      order(local).by(values,desc).limit(local, 5).select(keys).unfold()

相同的,从一个路径或者map中提取数据

g = graph.traversal()
g.io('data/grateful-dead.xml').read().iterate()
g.V().hasLabel('song').out('sungBy').groupCount().by('name') (1)
g.V().hasLabel('song').out('sungBy').groupCount().by('name').select(values) (2)
g.V().hasLabel('song').out('sungBy').groupCount().by('name').select(values).unfold().
      groupCount().order(local).by(values,desc).limit(local, 5) (3)
1 哪位艺术家唱了多少歌
2 获得一组歌单的曲目数目,不显示名字
3 5个最常见的歌单曲目大小
select(keys)select(values) 不能用 by() 来调整

在遍历器 Traverser 中的 List 对象在使用 select() 操作时,还有一个选项是提供 Pop 操作。

g.V(1).as("a").repeat(out().as("a")).times(2).select(first, "a")
g.V(1).as("a").repeat(out().as("a")).times(2).select(last, "a")
g.V(1).as("a").repeat(out().as("a")).times(2).select(all, "a")

之前的示例都是 select() 来根据一个静态的key来选取元素,除此之外,select() 也能够传入一个能够生成一个key的遍历结构

因为 select(<traversal>) 不能在编译时确定,所以 TraversalSelectStep 启用全路径跟踪
g.withSideEffect("alias", ["marko":"okram"]).V().  (1)
  values("name").sack(assign).                     (2)
  optional(select("alias").select(sack()))         (3)
1 插入一个存名字的alias,并且对所有顶点开始遍历
2 选取所有 name 值并且当做当前遍历器的sack来存放
3 从插入的alias中可以选择当前的名字

Using Where with Select

match() 单步相同,可以使用 where(), 它是一个处理 Map<String,Object> 对象流的过滤器

g.V().as('a').out('created').in('created').as('b').select('a','b').by('name') (1)
g.V().as('a').out('created').in('created').as('b').
      select('a','b').by('name').where('a',neq('b')) (2)
g.V().as('a').out('created').in('created').as('b').
      select('a','b'). (3)
      where('a',neq('b')).
      where(__.as('a').out('knows').as('b')).
      select('a','b').by('name')
1 一个标准的 select() 使用,在例子中,它在路径中(比如 a and b)生成一个 Map<String,Object> 对象的绑定
2 select().by('name') 将绑定的顶点按name的属性值投放,同时 where() 操作保证 ab 字符串不相同
3 第一个 select() 投放一个顶点绑定集合,那些 ab 中顶点相同的被过滤掉了。如果 ab 没有knows也被过滤掉了。 第二个也就是最后一个 select() 投放这些顶点的name

更多参考

ShortestPath step

shortestPath() 单步提供一个简单的查找图最短非循环路径方法。它通过 with() 可以按照以下选项进行配置

Key Type Description Default

target

Traversal

为结束顶点设置过滤器遍历(比如 +__.has('name','marko')+

所有顶点 (+__.identity()+

edges

TraversalDirection

设置 Traversal,让它从当前顶点的边发出,或者用 Direction 做最短路径发现

Direction.BOTH

distance

TraversalString

设置 Traversal 用于计算当前边的距离,或者设置边属性的标识,用于距离计算中使用

__.constant(1)

maxDistance

Number

设置最短路径的距离限制

none

includeEdges

Boolean

结果中是否包含边

false

g = g.withComputer()
g.V().shortestPath() (1)
g.V().has('person','name','marko').shortestPath() (2)
g.V().shortestPath().with(ShortestPath.target, __.has('name','peter')) (3)
g.V().shortestPath().
        with(ShortestPath.edges, Direction.IN).
        with(ShortestPath.target, __.has('name','josh')) (4)
g.V().has('person','name','marko').
      shortestPath().
        with(ShortestPath.target, __.has('name','josh')) (5)
g.V().has('person','name','marko').
      shortestPath().
        with(ShortestPath.target, __.has('name','josh')).
        with(ShortestPath.distance, 'weight') (6)
g.V().has('person','name','marko').
      shortestPath().
        with(ShortestPath.target, __.has('name','josh')).
        with(ShortestPath.includeEdges, true) (7)
1 找到所有最短路径
2 找到从 marko 开始的最短路径
3 找到终点为 peter 的最短路径
4 找到所有入向到 josh 的最短路径
5 找到所有从 markojosh 的最短路径
6 使用定义的距离属性,找到所有从 markojosh 的最短路径
7 找到所有从 markojosh 的最短路径,结果中包含边的信息
g.inject(g.withComputer().V().shortestPath().
           with(ShortestPath.distance, 'weight').
           with(ShortestPath.includeEdges, true).
           with(ShortestPath.maxDistance, 1).toList().toArray()).
  map(unfold().values('name','weight').fold()) (1)
1 使用定义的距离属性和距离为1的条件来找到所有最短路径,在OLTP的 GraphTraversal 中注入结果,用于在以后所有的路径里都能够在元素中选择属性

更多参考

SimplePath Step

simplepath step

当遍历器游走路径不回环这个条件很重要时,可以使用 simplePath() (filter) 单步。 遍历器的路径 信息如果被分析出有重复的对象,那么这个遍历器就被滤掉了。 如果需要带有回环的,可以参考 cyclicPath().

g.V(1).both().both()
g.V(1).both().both().simplePath()
g.V(1).both().both().simplePath().path()
g.V().out().as('a').out().as('b').out().as('c').
  simplePath().by(label).
  path()
g.V().out().as('a').out().as('b').out().as('c').
  simplePath().
    by(label).
    from('b').
    to('c').
  path().
    by('name')

遍历器可以使用 from()to() 来保证只有路径的特定部分是非循环的

g.addV().property(id, 'A').as('a').
  addV().property(id, 'B').as('b').
  addV().property(id, 'C').as('c').
  addV().property(id, 'D').as('d').
  addE('link').from('a').to('b').
  addE('link').from('b').to('c').
  addE('link').from('c').to('d').iterate()
g.V('A').repeat(both().simplePath()).times(3).path()  (1)
g.V('D').repeat(both().simplePath()).times(3).path()  (2)
g.V('A').as('a').
  repeat(both().simplePath().from('a')).times(3).as('b').
  repeat(both().simplePath().from('b')).times(3).path()  (3)
1 从顶点 A 开始遍历游走所有非循环的3跳的路径
2 从顶点 D 开始遍历游走所有非循环的3跳的路径
3 从顶点 A 开始遍历游走所有非循环的3跳的路径,再从那里开始(遍历)非循环3跳的路径。第二条路径可能穿越第一条路径的顶点

更多参考

Skip Step

skip() 单步相当于在 range()里把其上届设置成 -1。

g.V().values('age').order()
g.V().values('age').order().skip(2)
g.V().values('age').order().range(2, -1)

skip() 单步也可以结合 Scope.local 来使用,那么它就操作接下来的集合

g.V().hasLabel('person').filter(outE('created')).as('p'). (1)
  map(out('created').values('name').fold()).
  project('person','primary','other').
    by(select('p').by('name')).
    by(limit(local, 1)). (2)
    by(skip(local, 1)) (3)
1 找到created什么东西的person
2 …​选取第一个工程(随机顺序)作为 primary 并…​
3 …​选取其他工程作为 other

更多参考

Store Step

当需要 惰性 聚合时,要使用 store() (sideEffect) 单步而不是 aggregate()。 这两步所不同的是 store() 并不阻塞,并只在走过时将对象存在它的side-effect对象集合里

g.V().aggregate('x').limit(1).cap('x')
g.V().store('x').limit(1).cap('x')

有趣的是,即使选择区间只有1,store() 的副作用集合也会有两个结果。其实当第二个对象在到 range() (limit是 [0..1])过滤之前, 它经过了 store() 单步,所以存了下来

g.E().store('x').by('weight').cap('x')

更多参考

Subgraph Step

subgraph logo

从大图中提取小图做分析和可视化等其他目的,是图分析中常见的操作。subgraph() (sideEffect) 单步提供了一种通过 边导出子图,这样几乎可以在任何遍历中生成子图。 下例展示了如何生成 "knows" 子图:

subGraph = g.E().hasLabel('knows').subgraph('subGraph').cap('subGraph').next() (1)
sg = subGraph.traversal()
sg.E() (2)
1 因为函数生成“边导出子图”,必须在边操作单步上使用 subgraph()
2 子图只包含 "knows" 子图

常用的获取子图的例子还包括比如获取所有与一个顶点相邻的图结构:

subGraph = g.V(3).repeat(__.inE().subgraph('subGraph').outV()).times(3).cap('subGraph').next()  (1)
sg = subGraph.traversal()
sg.E()
1 从顶点 3 开始,向入边游走三步,将其全部输出到

一次完整的遍历中可能哟多次 subgraph() 调用。每一次可以或者针对同一个图(携带相同的副作用key),也可以不相同(不同的副作用key)

t = g.V().outE('knows').subgraph('knowsG').inV().outE('created').subgraph('createdG').
          inV().inE('created').subgraph('createdG').iterate()
t.sideEffects.get('knowsG').traversal().E()
t.sideEffects.get('createdG').traversal().E()

TinkerGraph一个理想(默认)Graph 图,子图可以在内存中快速提取,也可以支持用户标识符(可以是任何Java对象)。 这也是最后需要关注的一个功能,因为很多基于TinkerPop的图实现都有很复杂的标识符类型,TinkerGraph能够消费这些对象的能力 可以让其变成一个完美的处理传入子图的寄宿(host)。但当使用TinkerGraph里面的元素时候要很小心。原图中的标识符可能已经被预留了, 但在TinkerGraph中的元素如顶点和边的对象却是 TinkerVertexTinkerEdge 类型。所以,它们没法直接在原图里用Gremlin运行, 以下的遍历操作就大概率返回一个错误:

Vertex v = sg.V().has('name','marko').next();  (1)
List<Vertex> vertices = g.V(v).out().toList(); (2)
1 这里"sg" 是一个TinkerGraph的子图,"v" 是一个 TinkerVertex 类型对象
2 这里 g.V(v) 就有可能失败,因为 "g" 是原始的 Graph 实例,而不是一个inkerGraph,因为不认识 TinkerVertex 实例所以可能会拒绝掉

TinkerVertex 包在 ReferenceVertex 对象里就很安全,或者直接用 id() 来引用,如下:

Vertex v = sg.V().has('name','marko').next();
List<Vertex> vertices = g.V(v.id()).out().toList();

// OR

Vertex v = new ReferenceVertex(sg.V().has('name','marko').next());
List<Vertex> vertices = g.V(v).out().toList();

更多参考

Sum Step

sum() (map) 单步将流中的数字进行求和操作,并且生成结果。 注意遍历器数字乘以遍历器批量才表示最后的数字

g.V().values('age').sum()
g.V().repeat(both()).times(3).values('age').sum()
sum(local) 决定的是当前的局部变量的和(并不是遍历流中的),这对于 Collection 类型对象生效

更多参考

Tail Step

tail step

tail() 单步等同于 limit() 单步,但它发出最后 n 个对象,而不是前面 n

g.V().values('name').order()
g.V().values('name').order().tail() (1)
g.V().values('name').order().tail(1) (2)
g.V().values('name').order().tail(3) (3)
1 姓(字母序)
2 与1相同
3 最后三个名字

tail() 也可以与 Scope.local 公用,那么就是操作传入的集合

g.V().as('a').out().as('a').out().as('a').select('a').by(tail(local)).values('name') (1)
g.V().as('a').out().as('a').out().as('a').select('a').by(unfold().values('name').fold()).tail(local) (2)
g.V().as('a').out().as('a').out().as('a').select('a').by(unfold().values('name').fold()).tail(local, 2) (3)
g.V().valueMap().tail(local) (4)
1 只去处理最近的一个 aList<Vertex> 变成 Vertex
2 和1相同(List<String> 变成 String
3 a 中包含最后两个name的 List<String> 结构
4 每顶点的 Map<String, Object>,但是只包含最后的属性值

更多参考

TimeLimit Step

很多情况下,图遍历只想获得相对排名,而不是一个精确答案,经典的例子是推荐系统。 需要的是一个顶点的相对排名而非绝对排名。期望的执行结果可能需要不超过2毫秒。在这种场景下,可以使用 timeLimit() (filter) 单步

timelimit step
clock(int runs, Closure code) 是一个预装在 Gremlin Console 的程序,它能够统计代码的执行时间
g.V().repeat(both().groupCount('m')).times(16).cap('m').order(local).by(values,desc).next()
clock(1) {g.V().repeat(both().groupCount('m')).times(16).cap('m').order(local).by(values,desc).next()}
g.V().repeat(timeLimit(2).both().groupCount('m')).times(16).cap('m').order(local).by(values,desc).next()
clock(1) {g.V().repeat(timeLimit(2).both().groupCount('m')).times(16).cap('m').order(local).by(values,desc).next()}

本质上说,即使要游走所有顶点的数目,也不会考虑相对顺序。这样做的主要好处就是能够保证计算能在指定时间内(毫秒)完成。 最后,注意 timeLimit() 内部的计时器从第一个进入这个单步的遍历器开始计时。当达到时间上限后,任何有 next() 评估单步将生成一个 NoSuchElementException 异常,每个 hasNext() 预判将返回 false

更多参考

To Step

to() 并不是一个真正的单步执行操作,它更像是一个“调整步”(step modulator),类似 as()by()。 如果一个单步能够接受遍历或者字符串,那么 to() 就是告诉它们从哪添加的。 通用的模式就是 step().to()。 同样可以看from()

simplePath()cyclicPath()path(),和 addE()可以接受 to()

更多参考

Tree Step

对于任意一个图元素(比如顶点和边),从它发出的路径可以聚合为一个。 Gremlin 提供 tree() (sideEffect) 单步来对这种情况进行处理

tree step
tree = g.V().out().out().tree().next()

能够看到所有遍历器是怎么通过路径联动来生成树的,这很重要

tree step2

生成的tree的对象结果可以被后续操作(见JavaDoc的 Tree

tree = g.V().out().out().tree().by('name').next()
tree['marko']
tree['marko']['josh']
tree.getObjectsAtDepth(3)

注意当使用 by() 调整时,tree的节点是根据投射的唯一性,而不是根据原始投射的对象的唯一性来组合的,比如:

g.V().has('name','josh').out('created').values('name').tree() (1)
g.V().has('name','josh').out('created').values('name').
  tree().by('name').by(label).by() (2)
1 tree() 被创建时,顶点3和5是唯一的,所以生成了单独的分支
2 tree()by()label 来调整时,顶点3和5都是"software",所以在树中被合并成了一个单独的节点

更多参考

Unfold Step

当进入 unfold() (flatMap) 里的对象是迭代器,可迭代的类,或者map等,它将被展开成一个线性结构。 如果不是,那对象就被直接发出,可以查看 fold() 的反向操作

g.V(1).out().fold().inject('gremlin',[1.23,2.34])
g.V(1).out().fold().inject('gremlin',[1.23,2.34]).unfold()

注意 unfold() 单步并不递归展开迭代器。而 repeat() 可以用于递归展开

inject(1,[2,3,[4,5,[6]]])
inject(1,[2,3,[4,5,[6]]]).unfold()
inject(1,[2,3,[4,5,[6]]]).repeat(unfold()).until(count(local).is(1)).unfold()

更多参考

Union Step

union step

union() (branch) 支持合并任意多个迭代的结果。 当一个遍历器到达 union() 单步他就被拷贝到内部的单步里。而 union() 发出的遍历器则是相应的内部遍历生成的输出

g.V(4).union(
         __.in().values('age'),
         out().values('lang'))
g.V(4).union(
         __.in().values('age'),
         out().values('lang')).path()

更多参考

Until Step

until 不是一个独立的单步,它是 <<repeat-step,repeat()>> 的一个协调器(可以在 until() 里找到更多使用信息)

更多参考

Value Step

value() (map) 传入一个 Property 并且提取它的值

g.V(1).properties().value()
g.V(1).properties().properties().value()

更多参考

ValueMap Step

valueMap() 单步生成描述元素诸多属性的Map结构

g.V().valueMap()
g.V().valueMap('age')
g.V().valueMap('age','blah')
g.E().valueMap()

一个重要提示是一个顶点的Map为每key维护着一个值的list,而一条边或一个定点的属性只有一个单一属性(而不是一个list)。 原因是TinkerPop的顶点利用vertex properties来支持每key多值结构,使用"The Crew" 数据模型,可以显式的展示这一点。

g.V().valueMap()
g.V().has('name','marko').properties('location')
g.V().has('name','marko').properties('location').valueMap()

将值的list转换为一个单一值,可以按下例使用 by() 来协调

g.V().valueMap().by(unfold())
g.V().valueMap('name','location').by().by(unfold())

如果需要元素 Element 中的 idlabelkeyvalue,则可以使用 with() 来协调将其插入到返回的map结构中

g.V().hasLabel('person').valueMap().with(WithOptions.tokens)
g.V().hasLabel('person').valueMap('name').with(WithOptions.tokens, WithOptions.labels)
g.V().hasLabel('person').properties('location').valueMap().with(WithOptions.tokens, WithOptions.values)

更多参考

Values Step

values() (map) 在遍历数据流中,从一个元素 Element 中提取所有属性的值

g.V(1).values()
g.V(1).values('location')
g.V(1).properties('location').values()

更多参考

Vertex Steps

vertex steps

vertex (flatMap) 单步是Gremlin语言的基本操作,可以用来“游走”图(比如遍历)

  • out(string...):根据给定的边标签来沿外向游走到相邻的那些顶点

  • in(string...):根据给定的边标签来沿内向游走到相邻的那些顶点

  • both(string...):根据给定的边标签来双向游走到相邻的那些顶点

  • outE(string...):根据给定的边标签来沿外向游走到相邻的那些边

  • inE(string...):根据给定的边标签来沿内向游走到相邻的那些边

  • bothE(string...):根据给定的边标签来双向游走到相邻的那些边

  • outV():游走到外向顶点

  • inV():游走到内向顶点

  • bothV():游走到双向定点

  • otherV():游走到其他顶点,这些顶点不包含此顶点从哪移动来的那些顶点

in 在Groovy中是一个关键字,在Gremlin中使用必须使用双下划线的匿名引用如 __.in().

in 在JavaScript中是一个保留字,所以如果要使用Gremlin就需要用:in_()

in 在Python中是一个保留字,所以如果要使用Gremlin就需要用:in_()

g.V(4)
g.V(4).outE() (1)
g.V(4).inE('knows') (2)
g.V(4).inE('created') (3)
g.V(4).bothE('knows','created','blah')
g.V(4).bothE('knows','created','blah').otherV()
g.V(4).both('knows','created','blah')
g.V(4).outE().inV() (4)
g.V(4).out() (5)
g.V(4).inE().outV()
g.V(4).inE().bothV()
1 所有出边
2 所有knows的入边
3 所有created的入边
4 向外游走,并引入边和顶点
5 向外游走,但只引入顶点

更多参考

Where Step

where() (filter) 单步根据对象本身(Scope.local)或者对象的历史路径(Scope.global)来对当前对象进行过滤。 它主要用于链接match()select() 单步,但也可以独立使用

g.V(1).as('a').out('created').in('created').where(neq('a')) (1)
g.withSideEffect('a',['josh','peter']).V(1).out('created').in('created').values('name').where(within('a')) (2)
g.V(1).out('created').in('created').where(out('created').count().is(gt(1))).values('name') (3)
1 谁是marko的协作者,并且marko不能作为自己的协作者(传入predicate谓词函数)
2 在marko的协作者中,只包含那些名字为josh和peter的(使用一个副作用sideEffect)
3 在marko的协作者中有谁创建了多于一个工程?(使用遍历做参数)
请查阅 match().where()select().where() 来 查阅 where() 怎样用于链接 Map<String,Object> 映射的对象,比如 Scope.local

下面的一些实例描述了怎样基于匿名遍历过滤任意对象

g.V().where(out('created')).values('name') (1)
g.V().out('knows').where(out('created')).values('name') (2)
g.V().where(out('created').count().is(gte(2))).values('name') (3)
g.V().where(out('knows').where(out('created'))).values('name') (4)
g.V().where(__.not(out('created'))).where(__.in('knows')).values('name') (5)
g.V().where(__.not(out('created')).and().in('knows')).values('name') (6)
g.V().as('a').out('knows').as('b').
  where('a',gt('b')).
    by('age').
  select('a','b').
    by('name') (7)
g.V().as('a').out('knows').as('b').
  where('a',gt('b').or(eq('b'))).
    by('age').
    by('age').
    by(__.in('knows').values('age')).
  select('a','b').
    by('name') (8)
1 那些创建了工程的人的名字是什么?
2 不但创建了工程,而且还被别人认识的人名字是什么?
3 那些创建了2个及多个工程的人叫什么?
4 那些认识创建工程的人的人的名字是什么?(仅对OLTP系统生效,见下面 WARNING
5 那些虽然没有创建任何工程,但是被认识的人的名字是?
6 使用 where() 级联和使用单个 where() 与and从句连接的意义相同
7 Marko认识josh和vadas但是只比vadas年龄大
8 Marko比josh年轻,但josh认识某些和marko同龄的人(也是marko)
The anonymous traversal of where() processes the current object "locally". In OLAP, where the atomic unit of computing is the vertex and its local "star graph," it is important that the anonymous traversal does not leave the confines of the vertex’s star graph. In other words, it can not traverse to an adjacent vertex’s properties or edges.

更多参考

With Step

with() 单步也不是一个真正的独立单步,而是一个“协调步”来改变前一步的行为。 with() 也提供了一些额外的“配置”信息,这些都是实现了 Configuring 这个接口。 那些允许使用with来协调的单步都在文档里说明了怎样显式的使用

Predicates介绍

P 是一个 Function<Object,Boolean> 类型的谓词函数(Java 函数类)。也就是说,给定一些对象,返回ture/false。 截止到TinkerPop 3.4.0,Gremlin也支持文本谓词函数,只对 String 类型值做处理。TextP 文本谓词是 P 的子类,但特定了是 Function<String,Boolean>。 下面展示了提供的谓词列表,可以在has()where()is()中使用

谓词函数 描述

P.eq(object)

传入对象是否等于提供对象?

P.neq(object)

传入对象是否不等于提供对象?

P.lt(number)

传入数是否小于提供数?

P.lte(number)

传入数是否小于等于提供数?

P.gt(number)

传入数是否大于提供数?

P.gte(number)

传入数是否大于等于提供数?

P.inside(number,number)

传入数字是否在大于提供的第一个并小于第二个数字?

P.outside(number,number)

传入数字是否在小于提供的第一个或大于第二个?

P.between(number,number)

传入数字是否在大于等于提供的第一个并小于第二个数字?

P.within(objects...)

传入对象是否是提供列表中的一个?

P.without(objects...)

传入对象是否不在提供列表中?

TextP.startingWith(string)

传入string以提供的string为开始?

TextP.endingWith(string)

传入string以提供的string为结束?

TextP.containing(string)

传入string是否包含提供的string?

TextP.notStartingWith(string)

传入string是否不以提供的string为开始?

TextP.notEndingWith(string)

传入string是否不以提供的string为结束?

TextP.notContaining(string)

传入string是否不包含提供的string?

eq(2)
not(neq(2)) (1)
not(within('a','b','c'))
not(within('a','b','c')).test('d') (2)
not(within('a','b','c')).test('a')
within(1,2,3).and(not(eq(2))).test(3) (3)
inside(1,4).or(eq(5)).test(3) (4)
inside(1,4).or(eq(5)).test(5)
between(1,2) (5)
not(between(1,2))
1 not() 加一个 P 谓词函数是一个新的 P 谓词
2 P 谓词函数中参数的很多部可以内部的使用 test() 来测试传入
3 P 谓词函数可以用and连接
4 P 谓词函数可以用or连接(生成新谓词)
5 and() 是一个 P 谓词所以 P 谓词可以由很多谓词函数组成
为了让谓词表达更加清晰,最好可以 import static org.apache.tinkerpop.gremlin.process.traversal.P.*.

最后,注意where() 接受一个 P<String>。提供的字符串值引用一个变量绑定,并不是字符串呈现的显式的值

g.V().as('a').both().both().as('b').count()
g.V().as('a').both().both().as('b').where('a',neq('b')).count()
图服务提供商或者用户也可以扩展 P 来提供新的谓词函数,比如 regex(pattern) 就可以当成一个特定图系统的 P

Barrier的解释

barrier

Gremlin本质上还是一个惰性, 基于流处理的语言。 这意味着Gremlin先完整处理(尽其所能)当前遍历管道中的所有遍历器,然后再从遍历的头尾中获得跟多的数据。 但是,仍旧有一些场景下是不可能(或者不能实现)进行这种惰性计算。所以当计算本身不能进行惰性延迟,那么就需要一些“Barrier”。 一共有三种不同的Barrier:

  1. CollectingBarrierStep:所有在这一步之前的遍历器先放入一个集合里,然后后续在一个一个“耗完”传入下一步之前做一些处理(比如排序)。 例子有order()sample()aggregate()barrier()

  2. ReducingBarrierStep:所有前面的遍历器都在被一个reduce函数处理,当所有的遍历器都被处理完成后,然后发出一个“reduce计算值”的遍历器到下一步。 注意路径历史当到达ReducingBarrierStep时因为它的多对一的特性将被销毁。例子诸如:fold()count()sum()max()min()

  3. SupplyingBarrierStep:所有在这一步前的遍历器在这一步前被迭代(但不处理),随后一个提供的supplier发出一个独立的遍历器传向下一步。例子如cap()

在Gremlin OLAP(见TraversalVertexProgram)中,在每一个顶点操作步后都会插入一个barrier。 这意味着遍历将尽可能在当前局部顶点做运算。那些除非拿到相邻顶点才能计算的会被聚合到一个barrier集合里。 当局部顶点上没有遍历器后,被“栅栏”阻挡的遍历器被当做消息发给远端顶点进行下一步处理。

关于Scopes的说明

Scope 是个枚举,有两个常量:Scope.localScope.global。Scope的选择决定了一些特定的单步处理范围是当前对象(local) 或者是整个对象流(global

global 在Python是个关键字,所以要使用 Scope 的话要用 global_

g.V().has('name','marko').out('knows').count() (1)
g.V().has('name','marko').out('knows').fold().count() (2)
g.V().has('name','marko').out('knows').fold().count(local) (3)
g.V().has('name','marko').out('knows').fold().count(global) (4)
1 Marko 认识2个人
2 创建一个Marko朋友的列表,然后计算总数(list结构个数是1)
3 创建一个Marko朋友的列表,但 local 计算将生成list内部的个数
4 count(global)count() 相同,因为大多数单步的默认scope是 global

支持设置scope的单步有:

  • count():计算局部集合,还是全局数据流

  • dedup():对局部对象还是全局数据流来去重

  • max():对局部对象还是全局数据流来计算最大值

  • mean():对局部对象还是全局数据流来计算平均值

  • min():对局部对象还是全局数据流来计算最小值

  • order():对局部对象还是全局数据流来进行排序

  • range():对局部对象还是全局数据流来进行截取

  • limit():对局部对象还是全局数据流来进行截取

  • sample():对局部对象还是全局数据流来进行采样

  • tail():获取局部对象还是全局数据流尾部值

Scope 例子如下:

g.V().both().group().by(label).select('software').dedup(local)
g.V().groupCount().by(label).select(values).min(local)
g.V().groupCount().by(label).order(local).by(values,desc)
g.V().fold().sample(local,2)

最后,注意local() 这个单步是一个“硬局部限制”的单步,它会将所有内部遍历变成局部的操作。 下面的例子就是有意图的:

g.V().fold().local(unfold().count())
g.V().fold().count(local)

Lambdas说明

lambda

lambda 是一种可以接受引用或者传入的匿名函数。 在Gremlin, lambdas使得用户能(在运行态)创建一个特定目的的单步。但是建议尽量避免使用它。

g.V().filter{it.get().value('name') == 'marko'}.
      flatMap{it.get().vertices(OUT,'created')}.
      map {it.get().value('name')} (1)
g.V().has('name','marko').out('created').values('name') (2)
1 携带lambda的Gremlin遍历,可以避免也应该避免(不建议使用
2 相同的遍历,但不携带lambda (建议使用

Gremlin还是尽量去提供一个详尽完备的单步操作集合,用户这样就可以不需要在实际中使用lambda。 也只建议用户想要一个功能而没有这个单步的情况下使用lambda。原因是在Gremlin编译阶段lambda没法被插入到编译中,也就没法优化 (见traversal strategies),当前也不支持写一个原生的lambda发送到远程去执行,驱动也不支持。

很多在使用lambda的地方,都可以使用一个相应的单步操作,或者直接提供一个遍历来替代。 TraversalLambda 行为和典型的lambda类似,但它是可以参与优化的,因为它比纯正的lambda生成较少的对象

g.V().out().out().path().by {it.value('name')}.
                         by {it.value('name')}.
                         by {g.V(it).in('created').values('name').fold().next()} (1)
g.V().out().out().path().by('name').
                         by('name').
                         by(__.in('created').values('name').fold()) (2)
1 三步游走的路径的每一个对象都被lambda转换(不建议使用
2 三步游走的路径的每一个对象被单步和遍历(当做lambda)替代(建议使用

TraversalStrategy

traversal strategy

TraversalStrategy 分析 Traversal 并且一旦符合条件,就对其进行相应的变换。 遍历策略在编译阶段就参与了,这也是整个Gremlin遍历编译器的根基。下面有5种类型的遍历策略

  • 有一个用户层面的应用可以被嵌入到遍历逻辑中 (decoration).

  • 在TinkerPop层面,有一种更有效地描述遍历的方法 (optimization).

  • 在图系统/语言/驱动层面,有更有效的描述遍历的方式 (provider optimization).

  • 在遍历执行之前,需要有一些的最终调整/清理/分析工作

  • 对应用和遍历引擎来说,某些遍历器是不合法的 (verification).

explain() 单步告诉用户已经注册的策略是如何调整遍历的

一个简单的 OptimizationStrategy 实现是 IdentityRemovalStrategy.

public final class IdentityRemovalStrategy extends AbstractTraversalStrategy<TraversalStrategy.OptimizationStrategy> implements TraversalStrategy.OptimizationStrategy {

    private static final IdentityRemovalStrategy INSTANCE = new IdentityRemovalStrategy();

    private IdentityRemovalStrategy() {
    }

    @Override
    public void apply(Traversal.Admin<?, ?> traversal) {
        if (traversal.getSteps().size() <= 1)
            return;

        for (IdentityStep<?> identityStep : TraversalHelper.getStepsOfClass(IdentityStep.class, traversal)) {
            if (identityStep.getLabels().isEmpty() || !(identityStep.getPreviousStep() instanceof EmptyStep)) {
                TraversalHelper.copyLabels(identityStep, identityStep.getPreviousStep(), false);
                traversal.removeStep(identityStep);
            }
        }
    }

    public static IdentityRemovalStrategy instance() {
        return INSTANCE;
    }
}

策略很简单,就是直接在遍历中删除 IdentityStep 单步,因为 aStep().identity().identity().bStep() 等同于 aStep().bStep()。 有些遍历策略需要在执行前或执行后使用其他策略,那么就可以在 TraversalStrategy 实现里实现这两个相对应的方法(默认是没有实现的)。 当然,如果一个 TraversalStrategy 实现属于上述的五种分类中(decoration, optimization, provider-optimization, finalization,or verification), 那么它priors和posts也只能在相应的分类中选。

public Set<Class<? extends S>> applyPrior();
public Set<Class<? extends S>> applyPost();
TraversalStrategy 使用起来是按照其内部排序进行的,顺序是:decoration, optimization, provider optimization, finalization然后verification。 如果设计新的新策略并不是完全符合这5种分类,那么实现 TraversalStrategy 和其prior,posts方法也可以在任何分类中选,但不推荐这样用

下面是一个 GraphSystemOptimizationStrategy 的实现例子

g.V().has('name','marko')

如上的语句,就可以让TinkerGraphO(|V|)O(log(|V|) 的时间复杂度内实现, 具体依赖了是否有"name"的索引。

public final class TinkerGraphStepStrategy extends AbstractTraversalStrategy<TraversalStrategy.ProviderOptimizationStrategy> implements TraversalStrategy.ProviderOptimizationStrategy {

    private static final TinkerGraphStepStrategy INSTANCE = new TinkerGraphStepStrategy();

    private TinkerGraphStepStrategy() {
    }

    @Override
    public void apply(Traversal.Admin<?, ?> traversal) {
        if (TraversalHelper.onGraphComputer(traversal))
            return;

        for (GraphStep originalGraphStep : TraversalHelper.getStepsOfClass(GraphStep.class, traversal)) {
            TinkerGraphStep<?, ?> tinkerGraphStep = new TinkerGraphStep<>(originalGraphStep);
            TraversalHelper.replaceStep(originalGraphStep, tinkerGraphStep, traversal);
            Step<?, ?> currentStep = tinkerGraphStep.getNextStep();
            while (currentStep instanceof HasStep || currentStep instanceof NoOpBarrierStep) {
                if (currentStep instanceof HasStep) {
                    for (HasContainer hasContainer : ((HasContainerHolder) currentStep).getHasContainers()) {
                        if (!GraphStep.processHasContainerIds(tinkerGraphStep, hasContainer))
                            tinkerGraphStep.addHasContainer(hasContainer);
                    }
                    TraversalHelper.copyLabels(currentStep, currentStep.getPreviousStep(), false);
                    traversal.removeStep(currentStep);
                }
                currentStep = currentStep.getNextStep();
            }
        }
    }

    public static TinkerGraphStepStrategy instance() {
        return INSTANCE;
    }
}

遍历器的重定义很简单,比如 TinkerGraphStep 直接把 g.V() 后面接上一串 has() 单步,并且提供 HasContainersTinkerGraphStep。 这样 TinkerGraphStep 就可以判断是否有相应的索引。因为这是一个非TinkerPop提供的单步,所以应该归类到 ProviderOptimizationStrategy 类别 以免干扰到 OptimizationStrategy 策略的假设。

t = g.V().has('name','marko'); null
t.toString()
t.iterate(); null
t.toString()
OptimizationStrategyProviderOptimizationStrategy 是两个不同分组的原因是因为优化的策略只能用 TinkerPop提供的单步来重写遍历。这也保证了到优化策略执行到最后最终优化结果也是TinkerPop兼容的。就这而说,服务商提供的优化策略可以分析遍历并 使用图系统特殊的单步来重写遍历本身(比如将 GraphStep.HasStep...HasStep 替换成 TinkerGraphStep)。如果服务商为图系统实现了 OptimizationStrategy 并提供了特殊优化, 那么原有的TinkerPop优化在执行遍历优化可能失败,或者对图系统特殊单步行为(比如 ProviderVertexStep 扩展了 VertexStep)理解错误并生成错误语义。

最后,比如下例是一个复杂的遍历,包含多种复杂组件,就可以被TinkerPop默认策略优化

g.V().hasLabel('person'). (1)
        and(has('name'),  (2)
            has('name','marko'),
            filter(has('age',gt(20)))). (3)
  match(__.as('a').has('age',lt(32)), (4)
        __.as('a').repeat(outE().inV()).times(2).as('b')). (5)
    where('a',neq('b')). (6)
    where(__.as('b').both().count().is(gt(1))). (7)
  select('b'). (8)
  groupCount().
    by(out().count()). (9)
  explain()
1 TinkerGraphStepStrategyhas() 单步提供了可以使用全局图维度的索引查找的谓词
2 FilterRankStrategy 为过滤单步的空间时间代价进行排序
3 InlineFilterStrategy 去掉回环调用,用于增加相似过滤器的连接性,或者聚合它们
4 InlineFilterStrategymatch() 中提取名称谓词,这样就很容易让用户自定义的策略来使用索引
5 RepeatUnrollStrategy 将展开循环,IncidentToAdjacentStrategyoutE().inV() 类型变成 out()
6 MatchPredicateStrategy 将引入 where() 单步,所以可以接受 match() 单步的运行态优化器
7 CountStrategy 将在遍历上通过 count().is(x) 检查,来限制遍历器的数目
8 PathRetractionStrategy 将删除遍历器中的路径,并增加批量的相似性,因为 select('b') 并不需要路径数据
9 AdjacentToIncidentStrategy 讲把 out() 变成 outE() 来增加数据操作的局部性

TinkerPop提供了对使用者很有用的 DecorationStrategy 策略集合,下面子章节描述了这些策略的详细信息

ElementIdStrategy

ElementIdStrategy 提供了元素标识ID的控制能力。比如有些图实现,比如TinkerGraph,允许用户在创建元素时指定其ID:

g = TinkerGraph.open().traversal()
v = g.addV().property(id,'42a').next()
g.V('42a')

其他的图实现如Neo4j,自动生成元素ID且不能被指定。ElementIdStrategy 可以用于帮助用于按照边和定点的索引来分配标识ID

graph = Neo4jGraph.open('/tmp/neo4j')
strategy = ElementIdStrategy.build().create()
g = graph.traversal().withStrategies(strategy)
g.addV().property(id, '42a').id()
在图底层的存储系统里,用于存储标识ID的key应该有索引。如果没有的话,那么所有元素的查找都会变成全量扫描操作

EventStrategy

EventStrategy 策略的目的是在在遍历时,当图底层变化时一个或者多个 MutationListener 对象发送事件通知。 这种策略用于记录变化,根据变化触发特定的行为,或者在遍历变化时通知相关应用。当操作事务被回滚时,通知事件队列也被清空。

MutationListener 将发送下列通知:

  • 新顶点创建

  • 新边创建

  • 顶点属性变化

  • 边属性变化

  • 移除顶点属性

  • 移除边属性

  • 移除顶点

  • 移除边

Traversal 要处理事件,首先就要实现 MutationListener 接口。一个实现的例子是 ConsoleMutationListener, 它在控制台上输出这些事件。下面控制台输出是一些基本使用方法

import org.apache.tinkerpop.gremlin.process.traversal.step.util.event.*
graph = TinkerFactory.createModern()
l = new ConsoleMutationListener(graph)
strategy = EventStrategy.build().addListener(l).create()
g = graph.traversal().withStrategies(strategy)
g.addV().property('name','stephen')
g.E().drop()

EventStrategy 默认的配置了一个 EventQueue,用于在执行单步时可能生成通知事件。 因此,在Gremlin执行最后一行,来删掉所有的边的时候有一些数字不一致的情况,因为删掉边的总数是在消息发出后统计的。 策略也可以配置一个 TransactionalEventQueue 队列,它会将所有的变化存下来,直到提交阶段才发出所有的通知。

EventStrategy 不适用于追踪跨进程的全局变化。换句话说,一个JVM进程中的变化不会发送到另一个JVM进程。 实际上,变化的通知消息的发送不会超出 Traversal 的上下文。

另外一个 EventStrategy 的默认配置是围绕着“分离”的概念展开的。图元素作为通知事件的相关引用,是作为拷贝分理出图的。 所以当在TinkerGraph中增加一个新顶点,通知事件不是包含一个 TinkerVertex,而是一个 DetachedVertex。 这种行为,可以修改 EventStrategy.Builder 中的 detach() 方法,它可以接受一个 null 输入来表明不需要分离,那么就返回原始的元素。 DetachedFactory 是默认配置行为,而 ReferenceFactory 将返回引用,但只对没有属性的元素生效。

如果设置 detach() 配置为 null,要小心事务性的图会在提交后立即创建一个新的事务用于生成通知事件。 通知事件里面的图元素并不是一个事件发生时实际数据的“快照”,而是一个“现场”的数据库元素的引用

PartitionStrategy

partition graph

PartitionStrategy 将图的诸多顶点和边分区成以 String 字符串命名的分区(比如bucket,子图等) 在上图中,每个元素都有颜色的意义就是 PartitionStrategy 背后所描述的意义。 可以进行跨分区读写,也可以按边将其合并,或者分裂成两个新分区(比如一个头顶点在一个分区里,一个尾顶点在另一个)

PartitionStrategy 有三个主要配置项:

  1. 分区Key - 标识分区String字符串值的属性key

  2. 写分区 - 一个 String 字符串,表明未来写入的元素在哪个分区

  3. 读分区 - Set<String> 集合的可读到数据的分区

下例可以更好的理解 PartitionStrategy 分区策略

graph = TinkerFactory.createModern()
strategyA = PartitionStrategy.build().partitionKey("_partition").writePartition("a").readPartitions("a").create()
strategyB = PartitionStrategy.build().partitionKey("_partition").writePartition("b").readPartitions("b").create()
gA = graph.traversal().withStrategies(strategyA)
gA.addV() // this vertex has a property of {_partition:"a"}
gB = graph.traversal().withStrategies(strategyB)
gB.addV() // this vertex has a property of {_partition:"b"}
gA.V()
gB.V()

如果图支持元属性(meta-properties)且当构建 PartitionStrategy 时,将 includeMetaProperties 设置为 true, 那么分区可以扩展 VertexProperty 元素。partitionKey 会被存在 VertexProperty 的元属性内,并无条件遍历这些属性。 请注意 VertexProperty 只在 Traversal 自己内封闭。比如调用 Vertex.property(k) 可以绕过 PartitionStrategy 的 上下文,所以可以直接访问所有属性

将图元素写入特殊的分区,然后指定好读分区,开发者就能在一个空间里创建多张图。 另外,为了支持跨单元引用,也可以合并多张图(合并分区)

ReadOnlyStrategy

ReadOnlyStrategy 是很容易理解的策略。如果遍历内部有步骤做了任何改动,那么使用了这个策略的遍历器就会抛出 IllegalStateException 异常

SubgraphStrategy

SubgraphStrategyPartitionStrategy 很相似,它将遍历约束到某些顶点和边、和顶点属性。 这个范围由每个单独的基于遍历的条件决定

graph = TinkerFactory.createTheCrew()
g = graph.traversal()
g.V().as('a').values('location').as('b').  (1)
  select('a','b').by('name').by()
g = g.withStrategies(SubgraphStrategy.build().vertexProperties(hasNot('endTime')).create()) (2)
g.V().as('a').values('location').as('b').  (3)
  select('a','b').by('name').by()
g.V().as('a').values('location').as('b').
  select('a','b').by('name').by().explain()
1 获取所有的顶点和location顶点属性
2 创建一个 SubgraphStrategy,每个顶点都不能含有 endTime 属性 (所以,当前位置)
3 获取所有顶点和当前的locations顶点属性
这个策略的实现是将附加到边的顶点都必须符合顶点标准(如果存在),这样边本身也能够作为子图的一部分而存在

下例使用所有的三种过滤器,顶点,边,和顶点属性。people的顶点必须曾经在三个location出现过,边必须含有"develops", 并且顶点属性必须是person当前的位置,或者一个非location属性

graph = TinkerFactory.createTheCrew()
g = graph.traversal().withStrategies(SubgraphStrategy.build().
  vertices(or(hasNot('location'),properties('location').count().is(gt(3)))).
  edges(hasLabel('develops')).
  vertexProperties(or(hasLabel(neq('location')),hasNot('endTime'))).create())
g.V().valueMap().with(WithOptions.tokens)
g.E().valueMap().with(WithOptions.tokens)
g.V().outE().inV().
  path().
    by('name').
    by().
    by('name')

Domain Specific Languages

Gremlin是遍历图的一种DSL 在语言里他用来操作顶点,边,和属性。典型地说,应用Gremlin的应用并不是整个图域,而是它将自己的数据域在图内进行建模。 举例说,"modern" toy graph 将“软件”和“人” 的想换关系的对象域建模成了图。(比如一个人“认识”另外一个人,而一个人“创建”了软件,之类的关系)

如果要分析出"marko"是否认识"josh"可以写以下的Gremlin:

g.V().hasLabel('person').has('name','marko').
  out('knows').hasLabel('person').has('name','josh').hasNext()

这样的方法可以达到目的,它要求大家来图上写DSL语句,而不是用社交网络的语言来描述。如果更自然的语句表达,那就应该这样写:

g.persons('marko').knows('josh').hasNext()

在上述描述中,遍历写成的DSL语句都是在不同域定义的,从底层抽象出图结构。 这两个遍历的结果是等同的,实际上这个“社交DSL”与“图DSL”生成相同的结果,从而产生等效的策略应用和动态性能

下列是一些社交DSL更深层的例子

// Graph DSL - find the number of persons who created at least 2 projects
g.V().hasLabel('person').
  where(outE("created").count().is(P.gte(2))).count()

// Social DSL - find the number of persons who created at least 2 projects
social.persons().where(createdAtLeast(2)).count()

// Graph DSL - determine the age of the youngest friend "marko" has
g.V().hasLabel('person').has('name','marko').
  out("knows").hasLabel("person").values("age").min()

// Social DSL - determine the age of the youngest friend "marko" has
social.persons("marko").youngestFriendsAge()

如果对编程语言有兴趣,可以在 Gremlin Language Variants 章节学到如何实现这些DSL