第 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 扩展阅读