摘要:本文以华为云图引擎 GES 为例,来介绍如何使用图查询语言 Cypher 表达一些需要做数据局部遍历的场景。

本文分享自华为云社区《使用 Cypher 子查询进行图探索 -- 以华为云图引擎 GES 为例》,作者:蜉蝣与海。

在图数据库/图计算领域,很多查询可以使用图查询语言Cypher、Gremlin或者指令式API进行表达,如多跳过滤、全局检索以及对过滤后的结果进行聚集排序等操作。然而有些查询不是那么容易表达,常常需要对图中的一组数据去做局部遍历,例如在社交网络(人-人,人-兴趣的关联网络)场景中,常常涉及以下场景:

  • 朋友推荐:看看小明的朋友的朋友中,哪些不是小明的朋友,进而推荐给小明。
  • 潜在二度人脉分析:选取一组点,每个点代表一个人,在他们朋友的朋友中,统计他们各自有多少不认识的男性朋友和女性朋友。
  • 兴趣推荐A:兴趣爱好也是社交网络中的点,看看小明的朋友有哪些兴趣爱好(人-INTEREST-兴趣),从每个朋友的兴趣爱好中选取至多N个兴趣爱好推荐给小明。
  • 兴趣推荐B:看小明有哪些朋友还没有录入兴趣爱好,允许小明把自己的兴趣爱好推荐给他们。

这些查询往往只关注图中的某个局部,对局部进行多跳查询,且局部上往往有类似下列限制:

  • 数量限制:例如兴趣推荐A场景中,限制了每个朋友的兴趣数目,而不是总数目。
  • 条件限制:例如朋友推荐场景中,“哪些不是小明的朋友”需要先查询小明和朋友的朋友间有没有边,并将结果作为查询条件输入用来过滤。

在查询语言Cypher中,常常使用子查询来解决这类问题。本文会以华为云图引擎GES为例(图引擎版本>=2.3.6),来介绍如何使用Cypher表达上述场景。

注: 本文同步发布至华为云AI Gallery,文中所有代码皆可以在AI Gallery上运行:【AI Gallery】使用Cypher子查询进行图探索 – 以华为云图引擎GES为例

阅读前准备

基础知识

阅读前需要了解如下基础知识

下方三个小节会指导如何配置一个GES实例并使用notebook连接GES服务进而做查询演示。如果你只想了解如何编写查询语句,对输入的Cypher查询获取返回结果没有需求,可以直接跳过下方三个小节。

本文使用的数据集

本教程使用LDBC-SF0.1社交数据集中截选的人物关系数据集,数据集可以从此处下载。下载后需要在GES中创建图并导入数据集,详细指导流程可参见华为图引擎文档-快速入门华为云图引擎服务 GES 实战——创图

如何调用GES的Cypher API

GES官网帮助文档上有GES Cypher的API,为了方便用户调用,API设计为基于http/https请求,响应体的设计也兼容的neo4j的json格式。这里放置一下链接执行Cypher查询。调用API时需要将Token输入请求头中进行鉴权,有关Token的获取问题请参考业务面API认证鉴权

本文会使用ges4jupyter工具脚本进行相关查询的演示,该脚本中封装了刚刚提到的鉴权&Cypher查询API,并对结果进行了一些处理,提供了相关可视化的能力。

本文使用的代码包

ges4jupyter是jupyter连接GES服务的工具文件。文件中封装了使用 GES 查询的预置条件,包括配置相关参数和对所调用 API 接口的封装,如果你对这些不感兴趣,可直接运行而不需要了解细节,这对理解后续具体查询没有影响。本文的所有语句请求都会访问一个GES实例并得到实际的响应。

import moxing as mox
mox.file.copy('obs://obs-aigallery-zc/GES/ges4jupyter/beta/ges4jupyter.py', 'ges4jupyter.py')
mox.file.copy('obs://obs-aigallery-zc/GES/ges4jupyter/beta/ges4jupyter.html', 'ges4jupyter.html')

GESConfig的参数都是与调用 GES 服务有关的参数,依次为“公网访问地址”、“项目ID”、“图名”、“终端节点”、“IAM 用户名”、“IAM 用户密码”、“IAM 用户所属账户名”、“所属项目”,其获取方式可参考调用 GES 服务业务面 API 相关参数的获取。这里通过read_csv_config方法从配置文件中读取这些信息。如果没有配置文件,可以根据自己的需要补充下列字段。对于开启了https安全模式的图实例,参数port的值为443。

from ges4jupyter import GESConfig, GES4Jupyter, read_csv_config
eip = ''
project_id = ''
graph_name = ''
iam_url = ''
user_name = ''
password = ''
domain_name = ''
project_name = ''
port = 80
eip, project_id, graph_name, iam_url, user_name, password, domain_name, project_name, port = read_csv_config('cn_north_4_graph.csv')
config = GESConfig(eip, project_id, graph_name,
iam_url = iam_url,
user_name = user_name,
password = password,
domain_name = domain_name,
project_name = project_name,
port = port)
ges_util = GES4Jupyter(config, True);

首先在GES中创建索引,这有利于后续查询加速。

import time
def wait_job_finish(util, job_id, max_loop):
job_result = util.get_job(job_id)
if 'errorCode' not in job_result:
for i in range(max_loop):
if job_result['status'] == 'success':
break
else:
time.sleep(1)
job_result = util.get_job(job_id)
print(job_result) job_id = ges_util.build_vertex_index()
wait_job_finish(ges_util, job_id, 100)
job_id = ges_util.build_edge_index()
wait_job_finish(ges_util, job_id, 100)

可以使用下列语句查看schema信息:

import time
body = ges_util.generate_schema_structure()
job_id = body["jobId"]
print('开始构造schema结构:')
wait_job_finish(ges_util, job_id, 100)
print('schema结构构造完成')
cypher_result = ges_util.cypher_query("call db.schema()",formats=['row','graph']);
ges_util.format_cypher_result(cypher_result, candidate_title = ['description', 'name'])

如图是本文使用的数据集的schema,主要包括下列类型的点边:

使用子查询

一般来说,使用Cypher查询朋友的朋友是相对容易的,下列语句演示了如何查询顶点p367朋友的朋友。

match (n)-[:KNOWS]->(a)-[:KNOWS]->(b) where id(n)='p367' return distinct b

然而,使用一般的Cypher语义,从朋友的朋友中移除所有的朋友,表达朋友推荐场景中的“朋友的朋友而非我的朋友”却很困难。文章如何使用GES进行社交关系考据?—GES查询能力介绍中,描述了一种常规的查询语句写法:

match (n)-[:KNOWS]->(a) where id(n)='p367' with n, collect(a) as neighbor
match (n)-[:KNOWS]->(a)-[:KNOWS]->(b)
where not (b in neighbor)
return b

由于cypher的结果是使用行(Row)组织数据,所有的计算以“行”作为单元进行,如果要进行过滤,只能进行行内过滤。所以上述语句第一步,先通过collect(a),将“朋友”这个集合组织到了一行里,而后才能将collect(a)作为过滤条件,进行二次查询。

将子查询作为查询条件

在GES 2.3.6版本,实现了子查询能力,支持Neo4j中的SemiApply算子,该算子支持类似于下列语句的运行,使得查询更为简洁:

match (n) where id(n)='p367'
match (n)-[:KNOWS*2..2]->(b)
where not (n)-[:KNOWS]->(b)
return id(b) limit 10
cypher_result = ges_util.cypher_query("""
match (n) where id(n)='p367'
match (n)-[:KNOWS*2..2]->(b) where not (n)-[:KNOWS]->(b)
return id(b) limit 10""",formats=['row','graph']);
ges_util.format_cypher_result(cypher_result)

注意到这里where条件后面跟从的不是一个一般的条件表达式,不是大于小于这样的比较运算,在条件运算not后跟随了一个图模式(Graph Pattern),整个where条件表示“不存在从顶点n连向顶点b,且label为KNOWS的边”。这样的表达方式使得整条查询语句看起来更为简洁。

也可以使用explain查看其查询计划,可以看到是AntiSemiApply在发挥作用。这里条件查询主要包含两个算子:

  • SemiApply: 用于支撑“where (n)-[:KNOWS]->(b)”这样的条件,表示对应的查询模式存在。
  • AntiSemiApply:用于支撑“where not (n)-[:KNOWS]->(b)”这样的条件,表示对应的查询模式不存在。

这两个算子对每个左子树生成的结果,都去检查右子树是否会/不会产生满足条件的结果,并将右子树的结果作为过滤条件,辅助左子树的结果过滤。

通过这两个算子,即可实现简单的条件子查询。

cypher_result = ges_util.cypher_query("""explain
match (n) where id(n)='p367'
match (n)-[:KNOWS*2..2]->(b) where not (n)-[:KNOWS]->(b)
return id(b) limit 10""",formats=['row','graph']);
ges_util.format_cypher_result(cypher_result)

子查询作为条件,也可以用来描述兴趣推荐B场景:看小明有哪些朋友还没有录入兴趣爱好,允许小明把自己的兴趣爱好推荐给他们。

match (n:Person) where id(n)='p933'
match (n)-[r]->(m) where not (m)-[:HAS_INTEREST]-() return id(m)

将子查询作为中间结果

此外,还可以将子查询作为中间结果,朋友推荐场景下,cypher语句还可以这么写:

match (n) where id(n)='p367'
with [(n)-[:KNOWS*2..2]->(b)|id(b)] as hop2, [(n)-[:KNOWS]->(b)|id(b)] as hop1
return [x in hop2 where not x in hop1|x] limit 10

在这条查询语句中,Graph Pattern出现在了with子句中,用于收集某个点的多跳结果。

另外采用类似的写法还可以筛选三度好友中“我不认识的人”的数目,示例如下:

match (n) where id(n)='p367'
with [(n)-[:KNOWS*3..3]->(b)|id(b)] as hop3, [(n)-[:KNOWS*1..2]->(b)|id(b)] as hop2
return size([x in hop3 where not x in hop2|x])
cypher_result = ges_util.cypher_query("""
match (n) where id(n)='p367'
with [(n)-[:KNOWS*3..3]->(b)|id(b)] as hop3, [(n)-[:KNOWS*1..2]->(b)|id(b)] as hop2
return size([x in hop3 where not x in hop2|x])""",formats=['row','graph']);
ges_util.format_cypher_result(cypher_result, boxHeight=200)

同时这种子查询后续步骤也可以跟随一些过滤条件,进行各类统计操作,如上述提到的潜在二度人脉分析

match (n:Person) where id(n) in ['p367','p13194139534836','p932','p4398046512206','p6597069767359']
with n, [(n)-[:KNOWS*2..2]->(m) where not (n)-->(m)|m] as recSet
return id(n) as key,
size([x in recSet where x.gender='male']) as maleNumber,
size([x in recSet where x.gender='female']) as femaleNumber
cypher_result = ges_util.cypher_query("""
match (n:Person) where id(n) in ['p367','p13194139534836','p932','p4398046512206','p6597069767359']
with n, [(n)-[:KNOWS*2..2]->(m) where not (n)-->(m)|m] as recSet
return id(n), size([x in recSet where x.gender='male']),size([x in recSet where x.gender='female'])
""",formats=['row','graph']);
ges_util.format_cypher_result(cypher_result, boxHeight=200)

下列元素出现在with子句中,描述了一个子查询:

[(n)-[:KNOWS*2..2]->(m) where some-condition|m] as recSet

这里会对每个遍历到的n,都进行二跳查询, 取二跳查询的末端节点m,然后组装成一个列表。

注意到where条件中,使用了刚刚提到的条件子查询:

where not (n)-->(m)

这里条件使用where条件,对子查询的结果进行了过滤,且过滤时,是将一个Graph Pattern作为的过滤条件,最后使用竖线进行投影。

在return子句中,使用了Cypher中List Comprehension的语法,进行列表过滤,并获取大小:

return id(n) as key,
size([x in recSet where x.gender='male']) as maleNumber,
size([x in recSet where x.gender='female']) as femaleNumber

支撑子查询结果作为中间结果的,是RollUpApply算子,可以通过explain看到其在查询计划中发挥价值:

cypher_result = ges_util.cypher_query("""explain
match (n:Person) where id(n) in ['p367','p13194139534836','p932','p4398046512206','p6597069767359']
return n, [(n)-[:KNOWS*2..2]->(m) where not (n)-->(m)|m] as recSet""",formats=['row','graph']);
ges_util.format_cypher_result(cypher_result, boxHeight=200)

对每个左子树生成的结果(这里是 (n:Person))都会作为变量输入,并执行右子树,将右子树的结果打包返回为 list。

此外还可以限制子查询的数目,对查询进行 PerNodeLimit(单点跳出限制:每个点每层只能向外跳出限定个数的顶点)。

例如兴趣推荐 A 场景中,看看小明的朋友有哪些兴趣爱好(人 - INTEREST - 兴趣),从每个朋友的兴趣爱好中选取至多 N 个兴趣爱好推荐给小明。

match (n:Person) where id(n)='p367'
match (n)-[r]->(m)
return [(m)-[:HAS_INTEREST]-(a)|a][0..3]

为了可视化演示效果,可视化时同步打印了“朋友”和“INTEREST”边。

同样的,也可以使用RollUpApply+Limit对每跳做PerNodeLimit,例如统计和小明的朋友有共同兴趣爱好的朋友,每个顶点每跳最多找3个点,最后一跳每个点最多找1个点:

match (n:Person) where id(n)='p367'
match (n)-[r]->(m) with m limit 3
with m,[(m)<-[r1:HAS_INTEREST]-(a)|a][0..3] as interests
unwind interests as interest
with interest, [(interest)-[r1:HAS_INTEREST]->(a) where not (a)--(m)|[r1,a]][0..1] as soulMate
return *

其他子查询

使用with也可以实现其他子查询任务,例如上一跳的查询结果经过limit限制后输入下一跳,成为查询条件:

match (n:Person) where id(n) in ['p367','p13194139534836','p932','p4398046512206','p6597069767359']
with n limit 10
match (m:Person{lastName:n.lastName}) return n.lastName, m.firstName

使用explain也可以看到其查询计划:

cypher_result = ges_util.cypher_query("""explain
match (n:Person) where id(n) in ['p367','p13194139534836','p932','p4398046512206','p6597069767359']
with n limit 10
match (m:Person{lastName:n.lastName}) return n.lastName, m.firstName""",formats=['row','graph']);
ges_util.format_cypher_result(cypher_result)

由于不同的n,其n.lastName的值是不固定的,所以需要针对每个n,去做match (m:Person{lastName:n.lastName})这样的查询,因此需要使用Apply子查询算子支撑这样的语句。

总结借助子查询进行局部遍历是图查询中的常用操作,将子查询作为过滤条件或者中间结果辅助查询,可以满足某些业务场景下对查询局部有限制的诉求,

如文中提到的社交网络分析,再如股权关系中穿透层数分析、装备制造和配置管理(IT设备管理)领域依赖识别和变更影响分析等。

此外,由于Cypher以行的形式组织数据,某些情况下使用子查询可以节省中间结果产生,加速Cypher查询的执行。

当然,使用更高效的API(如GES产品中有多跳过滤API)或者使用非行存的查询执行引擎也是可选的解决方案。

点击关注,第一时间了解华为云新鲜技术~

如何更好的分析潜在人脉?聊聊华为云图引擎GES的Cypher子查询的更多相关文章

  1. sql 语句 嵌套子查询 执行顺序分析

    --创建测试数据create table Student(S# varchar(10),Sname nvarchar(10),Sage datetime,Ssex nvarchar(10))inser ...

  2. hive 子查询特别分析

      Hive只支持在FROM子句中使用子查询,子查询必须有名字,并且列必须唯一:SELECT ... FROM(subquery) name ... 确认下是否一定要求列必须唯一?      建表语句 ...

  3. mysql(4)—— 表连接查询与where后使用子查询的性能分析。

    子查询就是在一条查询语句中还有其它的查询语句,主查询得到的结果依赖于子查询的结果. 子查询的子语句可以在一条sql语句的FROM,JOIN,和WHERE后面,本文主要针对在WHERE后面使用子查询与表 ...

  4. MySQL 使用profile分析慢sql,group left join效率高于子查询

    MySQL 使用profile分析慢sql,group left join效率高于子查询 http://blog.csdn.net/mchdba/article/details/54380221 -- ...

  5. 夯实Java基础系列3:一文搞懂String常见面试题,从基础到实战,更有原理分析和源码解析!

    目录 目录 string基础 Java String 类 创建字符串 StringDemo.java 文件代码: String基本用法 创建String对象的常用方法 String中常用的方法,用法如 ...

  6. jQuery-1.9.1源码分析系列(三) Sizzle选择器引擎——编译原理

    这一节要分析的东东比较复杂,篇幅会比较大,也不知道我描述后能不能让人看明白.这部分的源码我第一次看的时候也比较吃力,现在重头看一遍,再分析一遍,看能否查缺补漏. 看这一部分的源码需要有一个完整的概念后 ...

  7. jQuery-1.9.1源码分析系列(三) Sizzle选择器引擎——总结与性能分析

    Sizzle引擎的主体部分已经分析完毕了,今天为这部分划一个句号. a. Sizzle解析流程总结 是时候该做一个总结了.Sizzle解析的流程已经一目了然了. 1.选择器进入Sizzle( sele ...

  8. jQuery源码分析系列(三)Sizzle选择器引擎-下

    选择函数:select() 看到select()函数,if(match.length === 1){}存在的意义是尽量简化执行步骤,避免compile()函数的调用. 简化操作同样根据tokenize ...

  9. jQuery-1.9.1源码分析系列(三) Sizzle选择器引擎——一些有用的Sizzle API

    说一下Sizzle中零碎的API.这些API有的被jQuery接管,直接使用jQuery.xxx就可以使用,有的没有被接管,如果要在jQuery中使用,使用方法是jQuery.find.xxx. 具体 ...

  10. jQuery-1.9.1源码分析系列(三) Sizzle选择器引擎——词法解析

    jQuery源码9600多行,而Sizzle引擎就独占近2000行,占了1/5.Sizzle引擎.jQuery事件机制.ajax是整个jQuery的核心,也是jQuery技术精华的体现.里面的有些策略 ...

随机推荐

  1. salesforce零基础学习(一百三十三)ListView的button思考

    本篇参考: salesforce零基础学习(九十五)lightning out salesforce零基础学习(一百一十)list button实现的一些有趣事情 https://help.sales ...

  2. AI图形算法的应用之一:仪表识别

    目前AI智能算法如火如荼,各大型厂商对此趋之若鹜般加大开发力度,比如大华.海康这些视频处理类,以及百度.腾讯这些IT软件厂商,因为业务开展需要,我也把研发方向转向于此,小有成绩,在此展示一下. 最近研 ...

  3. 随身wifi 救砖过程记录

    7,8块钱买了个随身wifi,准备刷机玩的,后来不知道刷错了boot还是啥,加电后灯都不亮了,前期没备份,于是网上找了各种教程,下面记录下: 变砖后有个底层的9008驱动协议可以刷机,下面的过程都是基 ...

  4. FFT & NTT 及其简单优化

    FFT FFT 是一种高效实现 DFT 和 IDFT 的方式,可以在 \(O(n \log n)\) 的时间内求多项式的乘法. 多项式的点值表示 不同于用每项的系数来表示一个多项式,我们知道对于给定的 ...

  5. MongoDB 中的锁分析

    MongoDB 中的锁 前言 MongoDB 中锁的类型 锁的让渡释放 常见操作使用的锁类型 如果定位 MongoDB 中锁操作 1.查询运行超过20S 的请求 2.批量删除请求大于 20s 的请求 ...

  6. 如何使用Python将PDF转为Excel

    PDF文件是一种静态文档格式,通常难以编辑,而Excel则是一个灵活的表格工具.如果你需要处理PDF表格中的数据,那么将其导出为Excel文件可以大大节省工作时间和精力.Excel提供的强大数据编辑和 ...

  7. Python 利用pandas和matplotlib绘制堆叠柱状图

    在数据可视化中,堆叠柱状图是一种常用的图表类型,它能够清晰地展示多个类别的数据,并突出显示每个类别中各部分的总量和组成比例.本文将演示如何使用 Python 的 pandas 和 matplotlib ...

  8. Electron原生菜单

    .markdown-body { color: rgba(56, 56, 56, 1); font-size: 15px; line-height: 30px; letter-spacing: 2px ...

  9. Vivado生成bitstream时报错[Opt 31-67] Problem: A LUT3 cell in the design is missing a connection on input pin I1, which is used by the LUT equation

    这个原因主要是因为有一个引脚没有用到,解决方法. 1.打开Schematic. 2.根据提示的模块去找,比如说我的报错. [Opt 31-67] Problem: A LUT3 cell in the ...

  10. C语言编写两个函数,分别求两个整数的最大公约数和最小公倍数,并用主函数调用这两个函数,然后输出结果。两个整数由键盘输入。约定最大公约数为正整数,两数里有负的则最小公倍数就为负的。

    /* 开发者:慢蜗牛 开发时间:2020.5.28 程序功能:计算最大公约数和最小公倍数 */ #include<stdio.h> #include<math.h> int m ...