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

AgensGraph 完全指南 / 第 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 同一查询的两种写法

场景 Cypher Gremlin
查找所有人物 MATCH (p:Person) RETURN p g.V().hasLabel('Person')
Alice 的朋友 MATCH (a:Person {name:'Alice'})-[:KNOWS]->(f) RETURN f.name g.V().has('Person','name','Alice').out('KNOWS').values('name')
朋友的朋友 MATCH (a)-[:KNOWS*2]->(f) RETURN f g.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 扩展阅读