本文将介绍几种常用的图查询命令,展示如何用编写其 UQL 语句以及它们在 Ultipa Manager 中的查询结果样式。
文本使用的图集:
对于没有 Ultipa 服务器环境的用户:
- 点击代码框上的 Run 按钮,在弹窗中运行并查看当前 UQL 命令的查询结果
- 点击 Copy 按钮复制代码,并在 Ultipa Playground 中运行(注意选择图集 Quick Start)
基本查询
查找点
点的查找类似于在关系型数据库中对某张表进行查询。尝试理解下边的 UQL 语句:
find().nodes() as myFirstQuery
return myFirstQuery{*} limit 10
该语句的字面含义为:查找点,将它们作为 myFirstQuery,返回 myFirstQuery 并限制 10 条记录。
以上的描述已经相当接近了,以下是更多的解释:
- 链式语句
find().nodes()
发起了点查询 - 这些点的别名 myFirstQuery 被后面的
return
子句调用 - 点别名后紧跟着的
{*}
携带了这些点的全部属性
所以这句 UQL 的目的是 “找到 10 个不同的点,返回它们的全部属性”。在 Ultipa Manager 中的执行结果为:
返回的 10 个点均属于 schema
@customer
,这种巧合取决于数据插入的先后顺序,同时也会受到并发计算的影响。
如果要查找某个特定 schema 的点,比如 merchant,可以在 nodes()
中添加描述:
find().nodes({@merchant}) as mySecondQuery
return mySecondQuery{*} limit 10
- 参数
nodes()
中的花括号{}
及其内容被称作 过滤器(图数据 中介绍的其他用来描述点的参数也如是)
上面的 UQL 语句的执行结果如下:
查找边
在当前的图模型中,@customer
节点通过@transfer
边向@merchant
节点进行转账:
边的查找与点极为类似,只需替换使用 edges()
传入边的过滤条件:
find().edges({_from == "60017791850"}) as payment
return payment{*} limit 10
返回的 10 个 payment 全部由顾客 Chen**(ID 为 60017791850)发起,payment 的 _id
表示接收转账的商家的 ID。现在,用一个点查询语句调用这些商家的 ID:
find().edges({_from == "60017791850"}) as payment
find().nodes({_id == payment._to}) as merchant
return merchant{*} limit 10
- 点别名后面紧跟着
._id
表示调用这些点的属性_id
续写后的 UQL 语句返回 “10 个接收了顾客 Chen**(ID 为 60017791850)的转账的商家的所有属性”:
返回的 10 个商家中有两个商家重复出现了,即
_uuid
为 117 和 119 的商家各出现了两次,原因是这两个商家各收到两笔来自 Chen** 的付款。
这向我们展示了一个典型的复杂图(多边图)的例子,即两个节点之间有多条边存在。想要更好的观察这一现象,可以更换查询命令并用 Ultipa Manager 的2D 视图进行观测。
展开
spread().src({_id == "60017791850"}).depth(1) as transaction
return transaction{*} limit 10
该语句的字面意思:展开自 ID 为 60017791850 的源头,深度为 1,返回 10 条记录的所有属性。
- 命令
spread()
发起了以节点src()
为源头的边查询,并以 BFS 方式进行搜索 - 参数
depth()
限制了该 BFS 搜索的最大深度 - 别名 transaction 为找到的边的一步路径形式,即 “起点-边-终点”
- 路径别名后面紧跟着的
{*}
携带了路径中点、边的全部属性
所以这句 UQL 的目的是 “查找 10 笔由顾客 Chen** 发起的转账,返回顾客 Chen**、所有转账边、所有收款商家的全部属性”:
Ultipa Manager 将返回的路径自动显示在 2D 视图中,从图中能清楚的看到商家 117 和 119 与顾客 Chen** 之间的多笔转账。
如果要指定 spread()
命令所返回的路径终点的不重复的数量,可以适当引入去重操作再进行返回。另一种思路是改用另一种查询命令,同样使用 BFS 的方式进行搜索,但搜索的目标是点,而非边。
K邻
khop().src({_id == "60017791850"}).depth(1) as merchant
return merchant{*} limit 10
该语句的字面意思:跳跃 K 步自 ID 为 60017791850 的源头,深度为 1,返回 10 条记录的所有属性。
- 命令
khop()
发起了以src()
为源头的点查询,并以 BFS 方式进行搜索 - 别名 merchant 为找到的点
所以这句 UQL 的目的是 “查找 10 个接收顾客 Chen** 的转账的商家,返回商家的全部属性”:
khop()
命令找到了 10 个不同的收款商家,对比之前使用spread()
命令找到的 10 笔付款中的 8 个收款商家,新发现了 110 和 118 两个商家。
模板的思维方式
模板查询是一种高级的图查询方法,通过使用 n()
、e()
、nf()
对路径中的每一个点、边进行精确描述。(前文 图数据 中有提到。)
链路
n({_id == "60017791850"}).e().n() as transaction
return transaction{*} limit 10
该语句的字面意思:查找从 ID 为 60017791850 的点开始的一步路径,返回 10 条路径的所有属性:
返回的结果和之前
spread()
示例的结果完全一致,因为它们全都是在查找从顾客 Chen** 出发的一步转账路径。
现在考虑这样一种路径:从 Chen** 出发,经过三条边,最终到达某个商家:
n({_id == "60017791850"}).e({tran_amount > 70000})[3].n() as transChain
return transChain{*} limit 10
e()
后面紧跟着的[3]
表示 3 条连续的边(作用类似于之前提到的depth()
)
10 条路径从 Chen** 出发,到达第二个节点(商家 111)后兵分三路,再从 Zheng**、Qian**、Chu** 分散为 10 条路径、到达 8 个不同的商家。
图中所示的 Chen** 和 Zheng**、Qian**、Chu** 均购买过商家 111 的商品,在特定场景下可能意味着这 4 个顾客特征相似,因此便有理由将 Zheng**、Qian**、Chu** 光顾过的其他 8 个商家(路径终点)推荐给 Chen**。这便是一种典型的商品、商家推荐模型。
当返回结果中有点、边重复出现时,Manager 的 2D 视图不会对其进行重复渲染。例如上面查到的 10 条路径,每一条都以 Chen** 为起点,以 “转账 60” 为第一条边,以 “商家 111” 为第二个点,而 2D 视图中只渲染一个 Chen**、一条 “转账 60” 边、一个 “商家 111”。如需了解每一条路径的具体信息,可切换至列表视图:
环路
路径中不允许有边重复出现,但允许有重复的点,于是便有了环路。
n({@customer} as start).e({tran_date > "2020-1-1 0:0:0"})[4].n(start) as transRing
return transRing{*} limit 10
- 最后一个
n()
调用了的第一个n()
的别名 start,表示路径的首尾节点相同(即形成环路)
所以这句 UQL 的目的是 “查找 10 条从某个顾客出发的四步路径,并最终回到该顾客,要求每一步转账都晚于 2020-1-1 0:0:0,返回这些路径中的所有点、边的全部属性”:
切换到列表视图为:
顾客 Chu**、Ou** 均从商家 110、108 处购买商品,这种结构在特定的场景下意味着 Chu** 和 Ou** 具有相似性。
最短路
n({_id == "60017791850"}).e()[:5].n(115) as transRange
return transRange{*} limit 1
- 中括号中的
:5
也可写作1:5
,表示边数为 1~5,而非固定值 - 最后一个
n()
中的数字115
是过滤器{_uuid == 115}
的简写形式
所以这句 UQL 的目的是 “查找 1 条从 Chen** 出发的、到达 UUID 为 115 的节点的路径,要求边数不超过 5,返回这条路径中的所有点、边的全部属性”:
碰巧,返回的这条路径刚好有 5 条边。
对路径中的边数稍加修改,则可能完全改变查询的目标:
n({_id == "60017791850"}).e()[*:5].n(115) as transShortest
return transShortest{*} limit 1
- 在边数中添加星号
*
后得到*:5
,模板变成了边数不超过 5 的最短路径
所以这句 UQL 的目的是 “查找 1 条从 Chen** 出发的、到达 UUID 为 115 的节点的最短路径,要求边数不超过 5,返回这条路径中的所有点、边的全部属性”:
最短路径体现的是两点之间最直接的联系。一般情况下,路径越短,路径中数据的关联性就越大,路径就越值得被研究。但也正因如此,真实世界中的某些实体会故意将自身隐藏在很长(20~30步)的数据链路末端。为了能挖掘出这类可疑实体,要求数据库拥有超深遍历的能力和快速响应的高性能。
常用运算
UQL 查到点、边、路径以后,可以进行很多运算。这些运算是通过函数、子句进行的。运算的种类非常多,具体可参看 UQL 文档。在本节中仅挑选一些较常用的运算进行介绍。
去重
修改前面链路一节中的第一个例子,将 10 条路径的终点去重后再返回(8 个商家):
n({_id == "60017791850"}).e().n(as payee) limit 10
with distinct payee as payeeDedup
return payeeDedup{*}
- 操作符
distinct
对 payee 进行去重 - 去重操作写在
with
子句中 - UQL 语句按顺序执行,
limit 10
位于with
之前则先查找 10 条结果,再进行去重
计数
修改上面的示例,计算去重后的终点个数:
n({_id == "60017791850"}).e().n(as payee) limit 10
with count(distinct payee) as cardinality
return cardinality
- 函数
count()
对distinct payee
进行数量统计,count()
和distinct
经常这样套用在一起
排序
修改前面查找边一节中的第一个例子,将找到的边按转账金额降序排列后再返回:
find().edges({_from == "60017791850"}) as payment limit 10
order by payment.tran_amount desc
return payment{*}
- 子句关键词
order by
对 payment 按照其属性 tran_amount 进行排序 - 位于子句
order by
末尾的关键词desc
表示 “降序”
分组
修改前面链路一节中的第二个例子,将找到的三步路径按照路径中的第三个点进行分组,并统计每组中的路径数量:
n({_id == "60017791850"}).e({tran_amount > 70000})[2].n(as third).e({tran_amount > 70000}).n() limit 10
group by third
return table(third.cust_name, count(third))
- 为了在模板中体现第三个点,将原先的结构
n().e()[3].n()
改写为n().e()[2].n().e().n()
- 子句关键词
group by
对 third 进行分组 - 函数
count()
对每组中的 third 进行数量统计 - 函数
table()
将third.cust_name
和count(third)
合并到一个表格中,使结果更加一目了然