强曰为道

与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

第 06 章:Gremlin 图遍历

第 06 章:Gremlin 图遍历

6.1 Apache TinkerPop 概述

6.1.1 TinkerPop 是什么?

Apache TinkerPop 是 Apache 基金会下的图计算框架(Graph Computing Framework),它定义了一套标准化的图遍历语言 —— Gremlin。TinkerPop 的目标是为图数据库提供统一的查询接口。

TinkerPop 生态:

  应用层:    Java App │ Python App │ JS App
                       │
  语言层:    ┌────────────────────────────┐
             │      Gremlin Language      │
             └────────────┬───────────────┘
                          │
  传输层:    ┌────────────────────────────┐
             │    Gremlin Server / Driver  │
             └────────────┬───────────────┘
                          │
  存储层:    ┌──────┬──────┬──────┬───────┐
             │AgensGraph│Neo4j│JanusGraph│...│
             └──────┴──────┴──────┴───────┘

6.1.2 TinkerPop 核心概念

概念说明
Graph图的抽象接口
Vertex顶点
Edge
Property属性
Traversal遍历(Gremlin 查询的核心)
Step遍历步骤(遍历的原子操作)
Traverser遍历器(跟踪当前位置和状态)

6.2 Gremlin 基础

6.2.1 Gremlin 的设计哲学

Cypher vs Gremlin:

  Cypher(声明式):
    "给我 Alice 的所有朋友"
    MATCH (a:Person {name:'Alice'})-[:KNOWS]->(f:Person)
    RETURN f.name;

  Gremlin(命令式/遍历式):
    "从 Alice 出发,沿着 KNOWS 边走到朋友,返回他们的名字"
    g.V().has('Person','name','Alice').out('KNOWS').values('name')

  核心区别:
    Cypher  → 告诉数据库 "要什么模式"
    Gremlin → 告诉数据库 "怎么一步步走"

6.2.2 基本遍历步骤

// 从所有顶点开始
g.V()

// 从特定顶点开始
g.V().has('Person', 'name', 'Alice')

// 沿出边遍历
g.V().has('Person', 'name', 'Alice').out('KNOWS')

// 沿入边遍历
g.V().has('Person', 'name', 'Alice').in('KNOWS')

// 沿任意边遍历
g.V().has('Person', 'name', 'Alice').both('KNOWS')

// 获取边
g.V().has('Person', 'name', 'Alice').outE('KNOWS')

// 获取属性值
g.V().has('Person', 'name', 'Alice').values('age')

// 获取标签
g.V().has('Person', 'name', 'Alice').label()

6.2.3 Gremlin 核心步骤速查

步骤类型说明示例
V()Vertex获取顶点g.V()
E()Edge获取边g.E()
out()Map沿出边到邻接顶点.out('KNOWS')
in()Map沿入边到邻接顶点.in('KNOWS')
both()Map沿任意边到邻接顶点.both('KNOWS')
outE()Map获取出边.outE('KNOWS')
inE()Map获取入边.inE('KNOWS')
bothE()Map获取所有边.bothE('KNOWS')
outV()Map边的起始顶点.outV()
inV()Map边的终止顶点.inV()
has()Filter属性过滤.has('age', gt(25))
filter()Filter自定义过滤.filter {it.get().value('age') > 25}
values()Map获取属性值.values('name')
valueMap()Map获取属性映射.valueMap(true)
select()Map选择指定遍历变量.select('a', 'b')
where()Filter条件过滤.where(out('KNOWS').count().is(gt(2)))
dedup()Filter去重.dedup()
order()Order排序.order().by('age', desc)
limit()Filter限制数量.limit(10)
count()Map计数.count()
group()Map分组.group().by(label)
path()Map获取遍历路径.path()
repeat()Branch循环遍历.repeat(out()).times(3)
union()Branch合并遍历.union(out(), in())
coalesce()Branch尝试多个遍历.coalesce(out('KNOWS'), out('FOLLOWS'))
addV()Mutation添加顶点.addV('Person').property('name', 'Alice')
addE()Mutation添加边.addE('KNOWS').to(otherV)
drop()Mutation删除.drop()

6.3 Gremlin 详细操作

6.3.1 创建数据

// 创建顶点
g.addV('Person').property('name', 'Alice').property('age', 30)
g.addV('Person').property('name', 'Bob').property('age', 28)
g.addV('Company').property('name', 'TechCorp')

// 创建边
g.V().has('Person', 'name', 'Alice')
  .addE('KNOWS')
  .to(g.V().has('Person', 'name', 'Bob'))
  .property('since', 2020)

// 批量创建
g.addV('Person').property('name', 'Carol').property('age', 32).as('c')
 .addV('Person').property('name', 'Dave').property('age', 25).as('d')
 .addE('KNOWS').from('c').to('d').property('since', 2022)

6.3.2 属性过滤

// 精确匹配
g.V().has('Person', 'name', 'Alice')

// 比较谓词
g.V().hasLabel('Person').has('age', gt(25))       // > 25
g.V().hasLabel('Person').has('age', gte(25))      // >= 25
g.V().hasLabel('Person').has('age', lt(30))       // < 30
g.V().hasLabel('Person').has('age', lte(30))      // <= 30
g.V().hasLabel('Person').has('age', between(25, 35)) // 25 <= age < 35
g.V().hasLabel('Person').has('age', inside(25, 35))  // 25 < age < 35
g.V().hasLabel('Person').has('age', outside(25, 35)) // age < 25 || age >= 35

// 字符串操作
g.V().has('Person', 'name', containing('li'))     // 包含
g.V().has('Person', 'name', startingWith('A'))    // 前缀
g.V().has('Person', 'name', endingWith('e'))      // 后缀
g.V().has('Person', 'name', notContaining('x'))   // 不包含

// 存在性检查
g.V().has('Person', 'email')                      // email 属性存在
g.V().hasNot('email')                             // email 属性不存在

// 多条件
g.V().hasLabel('Person')
  .has('age', gt(25))
  .has('name', startingWith('A'))

6.3.3 遍历与导航

// 一层出遍历
g.V().has('Person', 'name', 'Alice').out('KNOWS').values('name')

// 多层遍历
g.V().has('Person', 'name', 'Alice')
  .out('KNOWS')
  .out('KNOWS')
  .values('name')

// 获取边的属性
g.V().has('Person', 'name', 'Alice')
  .outE('KNOWS')
  .valueMap()

// 获取完整的三元组
g.V().has('Person', 'name', 'Alice')
  .outE('KNOWS').as('r')
  .inV().as('friend')
  .select('r', 'friend')
  .by('since')
  .by('name')

6.3.4 循环与重复遍历

// 重复遍历 3 次
g.V().has('Person', 'name', 'Alice')
  .repeat(out('KNOWS'))
  .times(3)
  .values('name')

// 带终止条件的重复
g.V().has('Person', 'name', 'Alice')
  .repeat(out('KNOWS'))
  .until(has('name', 'Dave'))
  .values('name')

// emit 中间结果
g.V().has('Person', 'name', 'Alice')
  .repeat(out('KNOWS'))
  .times(3)
  .emit()
  .values('name')

// 路径去重(防止循环)
g.V().has('Person', 'name', 'Alice')
  .repeat(out('KNOWS').simplePath())
  .times(5)
  .values('name')

6.3.5 聚合与分组

// 计数
g.V().hasLabel('Person').count()

// 分组计数
g.V().hasLabel('Person')
  .groupCount().by('city')

// 分组收集
g.V().hasLabel('Person')
  .group().by(label).by(values('name').fold())

// 求和与平均值
g.V().hasLabel('Person').values('age').sum()
g.V().hasLabel('Person').values('age').mean()
g.V().hasLabel('Person').values('age').min()
g.V().hasLabel('Person').values('age').max()

// 排序
g.V().hasLabel('Person')
  .order().by('age', desc)
  .valueMap('name', 'age')

6.3.6 条件分支

// union 合并多个遍历
g.V().has('Person', 'name', 'Alice')
  .union(
    out('KNOWS').values('name'),
    out('WORKS_AT').values('name')
  )

// coalesce 尝试多个遍历(返回第一个有结果的)
g.V().hasLabel('Person')
  .coalesce(
    values('nickname'),
    values('name')
  )

// choose 条件分支
g.V().hasLabel('Person')
  .choose(
    values('age'),
    choose(gt(30))
      .option(true, constant('senior'))
      .option(false, constant('junior'))
  )

6.3.7 修改操作

// 更新属性
g.V().has('Person', 'name', 'Alice').property('age', 31)

// 删除顶点
g.V().has('Person', 'name', 'Alice').drop()

// 删除边
g.V().has('Person', 'name', 'Alice').outE('KNOWS')
  .where(inV().has('name', 'Bob'))
  .drop()

// 删除属性
g.V().has('Person', 'name', 'Alice').properties('age').drop()

6.4 Gremlin vs Cypher 对比

6.4.1 同一查询的两种写法

场景CypherGremlin
查找所有人物MATCH (p:Person) RETURN pg.V().hasLabel('Person')
Alice 的朋友MATCH (a:Person {name:'Alice'})-[:KNOWS]->(f) RETURN f.nameg.V().has('Person','name','Alice').out('KNOWS').values('name')
朋友的朋友MATCH (a)-[:KNOWS*2]->(f) RETURN fg.V().has('Person','name','Alice').repeat(out('KNOWS')).times(2)
最短路径shortestPath((a)-[:KNOWS*]-(b))g.V().has('name','Alice').repeat(out('KNOWS').simplePath()).until(has('name','Dave')).path().limit(1)
计数分组RETURN city, count(*)g.V().hasLabel('Person').groupCount().by('city')

6.4.2 选择建议

维度Cypher 优势Gremlin 优势
学习曲线SQL 用户友好,声明式命令式思维,程序员熟悉
模式匹配原生支持,简洁直观需要手动构建遍历
灵活性固定模式查询任意复杂遍历逻辑
可组合性中等高(步骤可任意组合)
生态openCypher 标准TinkerPop 生态(30+ 数据库)
调试执行计划可视化遍历步骤可逐个调试
推荐场景关系查询、报表复杂遍历、算法实现

6.5 AgensGraph 中的 Gremlin 使用

6.5.1 Gremlin 查询接口

在 AgensGraph 中,Gremlin 查询通过特定的 SQL 函数或 Gremlin Console 接口执行:

-- 通过 SQL 函数执行 Gremlin(概念示意)
SELECT * FROM gremlin('demo', $$
  g.V().hasLabel('Person').has('age', gt(25)).values('name')
$$);

6.5.2 混合使用场景

-- 在同一个应用中,根据查询复杂度选择语言

-- 简单模式匹配 → Cypher
SET graph_path = social_network;
MATCH (p:Person)-[:KNOWS]->(f:Person)
RETURN p.name, f.name;

-- 复杂遍历逻辑 → Gremlin
-- (通过客户端驱动执行)

6.6 业务场景:供应链追溯

场景描述

一个产品从原材料到最终消费者的供应链可以用图建模:

// 创建供应链图
g.addV('Company').property('name', '原材料供应商A').property('tier', 1).as('a')
g.addV('Company').property('name', '零部件厂商B').property('tier', 2).as('b')
g.addV('Company').property('name', '组装厂C').property('tier', 3).as('c')
g.addV('Company').property('name', '品牌商D').property('tier', 4).as('d')
g.addV('Company').property('name', '零售商E').property('tier', 5).as('e')

g.V().has('Company', 'name', '原材料供应商A')
  .addE('SUPPLIES').to(g.V().has('Company', 'name', '零部件厂商B'))
  .property('quantity', 10000).property('lead_time_days', 7)

g.V().has('Company', 'name', '零部件厂商B')
  .addE('SUPPLIES').to(g.V().has('Company', 'name', '组装厂C'))
  .property('quantity', 5000).property('lead_time_days', 14)

// ... 类似创建更多供应链关系

Gremlin 查询:供应链上游追溯

// 从组装厂出发,向上追溯所有供应商
g.V().has('Company', 'name', '组装厂C')
  .repeat(in('SUPPLIES').simplePath())
  .emit()
  .path().by('name')

// 找出供应链中所有 Tier 1 供应商
g.V().has('Company', 'name', '组装厂C')
  .repeat(in('SUPPLIES').simplePath())
  .until(has('tier', 1))
  .values('name')

Cypher 等价查询

-- 从组装厂出发向上追溯
MATCH path = (c:Company {name: '组装厂C'})<-[:SUPPLIES*]-(supplier:Company)
RETURN [n IN nodes(path) | n.name] AS supply_chain,
       length(path) AS tiers;

-- 找出 Tier 1 供应商
MATCH (c:Company {name: '组装厂C'})<-[:SUPPLIES*]-(supplier:Company {tier: 1})
RETURN supplier.name AS tier1_supplier;

6.7 Gremlin 性能优化建议

优化策略说明示例
尽早过滤在遍历早期使用 has() 过滤g.V().hasLabel('Person').has('age', gt(25)).out('KNOWS')
限制范围使用 limit() 减少结果集.limit(100)
避免全图扫描总是从特定顶点开始g.V().has('name', 'Alice') 而非 g.V()
使用索引确保 has() 使用的属性有索引确保 name 属性有索引
simplePath()防止遍历循环.repeat(out().simplePath())
dedup()去重避免重复处理.dedup()
profile()分析遍历性能g.V().has('name','Alice').out().profile()

6.8 本章小结

要点说明
Gremlin 类型命令式/遍历式查询语言
TinkerPop图计算标准框架
核心步骤V(), out(), in(), has(), values(), repeat()
与 Cypher 对比Gremlin 更灵活,Cypher 更直观
AgensGraph同时支持 Cypher 和 Gremlin
最佳实践尽早过滤、使用索引、限制遍历深度

6.9 练习

  1. 用 Gremlin 编写查询:找出所有 30 岁以上的人物及其朋友。
  2. 用 Gremlin 实现"从某节点出发,3 跳内可达的所有节点"。
  3. 分别用 Cypher 和 Gremlin 实现同一个查询,对比两者写法。
  4. 使用 repeat().until() 实现带条件的广度优先搜索。

6.10 扩展阅读