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 练习
- 用 Gremlin 编写查询:找出所有 30 岁以上的人物及其朋友。
- 用 Gremlin 实现"从某节点出发,3 跳内可达的所有节点"。
- 分别用 Cypher 和 Gremlin 实现同一个查询,对比两者写法。
- 使用
repeat().until() 实现带条件的广度优先搜索。
6.10 扩展阅读