====================================================
你最近在干嘛?写SQL,哦SQL_boy
====================================================
怎么说呢,最近遇到了需要在python中灵活地在cypher语句里面装载和弃用关系结构、聚合函数、条件筛选的需求,然后就开始着手考虑构建一系列地cypher算子,但是吧,感觉似乎又要用到正则表达式啥的老麻烦了,就这么想着想着今天灵感突然爆发,想起之前写的LinQ(C#)语句,那个让我在多少个夜里感慨:“既生SQL何生LinQ”的辣鸡语句竟然在跨语言灵活装载sql问题上能发挥这么大作用??抱着试一试的心态开始整理整个构造过程:
1.业务范围
鉴于是基于图数据库构建的,因此作为知识沉淀的数据库,其实更常用的业务是在各种搜索的问题上,因而本文所有的内容都聚焦于cypher的搜索算子的构建上;另外,鉴于双实体关系是最常见的搜索/推理关系,因而本文将最初目光锁定在双实体多关系的问题上。
2.业务痛点
①同一结果不同cypher造成代码可读性降低
每当遇到一个新的业务就需要从头构建cypher,并且构造过程中有多种完成方式,很容易造成同一任务细微不同格式却需要重新构造cypher数据,比方说以下两个cypher语句:
1. MATCH (p1:Person{name:'Ay'})-[r:knows]->(p2:Person{name:'John'}) RETURN p1,r,p2
2. MATCH (p1)-[r:knows]->(p2) WHERE p1.name="Ay" and p2.name="John" RETURN p1,r,p2
表达的都是查找属性name="Ay"的实体与属性name="John"的实体之间认识的关系,但是前者直接通过index查询后者则通过条件筛选,因而最终写出来的cypher语句也就不同,容易使得同类业务代码未被察觉。
②代码可扩展性差
若同样的业务有不同的需求,比方说,根据用户的输入,我们需要回复不同的回答时:
以:“John认识多少人?”和“John认识谁?”的python-py2neo为例:
myGraph=py2neo.Graph(host,user,pwd)
if "多少" in inputStr:#判断问的是不是数字
data=myGraph.run("MATCH (p1:Person{name:"John"})-[r:knows]->(p2) return COUNT(p2)").data()
elif "谁" in inputStr:#判断问的是不是具体的人
data=myGraph.run("MATCH (p1:Person{name:"John"})-[r:knows]->(p2) return p2").data()
根据上述代码可知,当inputStr为“John认识多少人?”时,data返回的内容为2,反之为具体的人的列表,但是这样的结果就造成了当条件变多时就需要写大量的cypher去完成业务,增加开发及后期维护的难度。
综上两点(还有别的再补充),我决定开发一个易读的可扩展的cypher算子工具。
3.业务场景分解
我们首先回顾一个比较复杂的cypher:
MATCH (p1)-[r:knows]->(p2)
WHERE p1.name="John"
return p1,r,p2,COUNT(p2)
我们可以看到一个比较复杂的cypher语句主要由Match/Where/Return三个部分构成,Match构造图数据库的子图,WHERE可以加设条件,return既可以返回所需的实体/关系也可以返回相应的聚合函数
4.产品需求
因此基于以上分解,我们可以将我们的算子输入工具分为以下几个部分:
{
"match":{
"sub":{},#{"IDName":"","IDValue":"","nickName":""}
"rel":{},#{"relName":"","nickName":""}
"obj":{},#{"IDName":"","IDValue":"","nickName":""}
},
"where":"",
"return":[],
}
即match中输入主谓宾,其中为了实现多关系,"rel"可以支持dict和list两种数据结构,
where为条件
return为返回的内容
基于以上数据,我们第一期希望能完成以下任务:
- 用户可通过更加pythonic的方式对不同的cypher业务进行区分;
- cypher细节函数可通过简单且明显的增删进行;
- 用户可查看各个算子最终生成的cypher也可直接运行
- 通过pipeline的方式构造算子
- 最终结果通过dataframe的形式展示
5.工具模块
当前工具暂定名为CqlFormer分如下几个模块,分别以函数形式表现出来,相应功能一并列出:
CqlFormer:
__init__ #初始化实例及neo4j数据库
getSub #获取关系起始实体
getObj #获取关系终点实体
getRel #获取关系
getTri #获取三元组
getCon #获取筛选条件(待开发)
getReturn #获取返回内容
outputJson #以json格式输出
outputCypher #以Cypher格式输出
run #运行算子结果
相关参数如下表所示:
8.产品基本使用方式
STEP 1:构建CqlFormer对象
myCF=CqlFormer()
STEP 2:构建基本查询图
myCF.getRel(["knows"]).getObj(name="Amy")
上图表示查询认识Amy的人,相对应的查询图大致形状如下:
根据上述基本查询图可推理大致的查询内容,于是,我们需要在getReturn中确定下来我们需要返回的是什么(例如:认识Amy的人有谁?认识Amy的人有多少个?认识Amy的人平均多少岁?认识Amy的人都来自哪些国家?等)。
STEP 3:确定返回内容
myCF.getRel(["knows"]).getObj(name="Amy").getReturn()
以上默认返回主谓宾
STEP 4:检查查询内容
可选择输出json查看是否所有对象都已就为,也可输出Cypher确定语法是否正确
myCF.getRel(["knows"]).getObj(name="Amy").getReturn().outputJson() #获取json
myCF.getRel(["knows"]).getObj(name="Amy").getReturn().outputCypher() #获取Cypher
所得结果分别如下:
Json:
{
'match':
{'sub': {'nickName': 'sub8643853489963341526', 'IDName': '', 'IDValue': ''},
'rel': {'relName': 'knows', 'nickName': 'rel7201487519220280229'},
'obj': {'IDName': 'name', 'IDValue': 'Amy', 'nickName': 'obj2517711945240361104'}},
'where': '',
'return': ['sub8643853489963341526', 'rel7201487519220280229', 'obj2517711945240361104']
}
Cypher:
MATCH (sub8643853489963341526)-[rel7201487519220280229:knows]->(obj2517711945240361104)
WHERE obj2517711945240361104.name='Amy'
RETURN sub8643853489963341526,rel7201487519220280229,obj2517711945240361104
STEP 5:运行Cypher(run)
myCF.getRel(["knows"]).getObj(name="Amy").getReturn().run()
所得结果如下:
9.产品功能演示
相关代码已上传至github:
https://github.com/Timaos123/CypherFormergithub.com首先把github的文件中的CqlFormer.py下载至自己的项目目录中,并在代码中调用该module:
from CqlFormer import CqlFormer
实体化CqlFormer,可以在这里输入自己的host/user/pwd:
myCF=CqlFormer(host,user,pwd)
假设我们的图数据库结构图下图所示:
接下来以几个案例验证CqlFormer的功能(见github中的example.py):
1.查询Amy的年龄:
myCF=CqlFormer()
print("How old is Amy:n{}".format(myCF.getSub(name="Amy").getReturn(sro=[],att=["s.Age"]).run()))
所得结果如下:
1.查询认识Amy的人详细信息:
myCF=CqlFormer()
print("who knows Amy:n{}".format(myCF.getRel(["knows"]).getObj(name="Amy").getReturn().run()))
所得结果如下:
2.查询Amy认识的人有哪些:
myCF=CqlFormer()
print("Amyknows who:n{}".format(myCF.getSub(name="Amy").getRel(["knows"]).getReturn().run()))
所得结果如下:
3.查询认识Amy的有多少人(注意把sro调整为[]否则会依赖sro进行输出):
myCF=CqlFormer()
print("How many people know Amy:n{}".format(myCF.getRel(["knows"]).getObj(name="Amy").getReturn(sro=[],agg=[("count","s")]).run()))
所得结果如下:
4.查询认识Amy的人的年龄:
myCF=CqlFormer()
print("what are the ages of people who know Alex:n{}".format(myCF.getRel(["knows"]).getObj(name="Alex").getReturn(sro=[],att=["s.name","s.Age"]).run()))
所得结果如下:
5.查询认识Amy的人的最大年龄:
myCF=CqlFormer()
print("what is the largest age of people who know Alex:n{}".format(myCF.getRel(["knows"]).getObj(name="Amy").getReturn(sro=[],agg=[("max","s.Age")]).run()))
所得结果如下:
6.查询认识Amy的人所处国家(区域)(注意att中若有和agg共现的属性,该语句将会变为统计语句,若没有共现则为罗列):
myCF=CqlFormer()
print("what is the countries/districts of people who know Amy:n{}".format(myCF.getRel(["knows"]).getObj(name="Amy").getReturn(sro=[],att=["s.country"]).run()))
所得结果如下:
7.查询认识Amy的人都来自哪些国家:
myCF=CqlFormer()
print("what is the numbers of the countries/districts of people who know Amy:n{}".format(myCF.getRel(["knows"]).getObj(name="Amy").getReturn(sro=[],att=["s.country"],agg=[("count","s.country")]).run()))
所得结果如下:
10.注意:
- 每次构建了查询图后记得设定getReturn;
- 每次调用前需要通过CqlFormer进行一次初始化,否则容易出现数据紊乱;
今天就先更到这啦~回头发现问题再做补充,也欢迎大家多多交流~