写点什么

基于华为云图引擎 GES,使用 Cypher 子查询进行图探索

  • 2023-05-10
    广东
  • 本文字数:6142 字

    阅读完需:约 20 分钟

基于华为云图引擎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 moxmox.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_configeip = ''project_id = ''graph_name = ''iam_url = ''user_name = ''password = ''domain_name = ''project_name = ''port = 80eip, 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 timedef 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 timebody = 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 neighbormatch (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 hop1return [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 hop2return 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 recSetreturn 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 soulMatereturn *
复制代码


其他子查询


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


match (n:Person) where id(n) in ['p367','p13194139534836','p932','p4398046512206','p6597069767359'] with n limit 10match (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 10match (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)或者使用非行存的查询执行引擎也是可选的解决方案。


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

发布于: 刚刚阅读数: 5
用户头像

提供全面深入的云计算技术干货 2020-07-14 加入

生于云,长于云,让开发者成为决定性力量

评论

发布
暂无评论
基于华为云图引擎GES,使用Cypher子查询进行图探索_人工智能_华为云开发者联盟_InfoQ写作社区