使用Neo4j和GraphQL来分析社交网络
一開始
我们在公司的项目中采用了Neo4j和GraphQL,并正在开发新产品。
这次我们想要分享一些关于使用Neo4j和GraphQL的感想和案例。
技术栈:
-
- Neo4j : 永続用のグラフデータベース
-
- GraphQL: バックエンド API を提供するためのクエリ言語
- Vue.js : フロントエンド
关于这个项目
我们目前正在进行的项目是建立一个名为miel的时尚AI平台,该平台是基于Instagram等社交媒体的帖子和社交网络图的获取和分析,调查和评估品牌和产品的影响力等。在本文中,我想写一下关于注册和评估想要分析的社交媒体账户(尤其是Instagram)和活动的功能。
选择GraphQL/Neo4J的原因
我们考虑使用GraphQL,并阅读了有关Neo4j的宣传文章,思考着使用图数据库是多么的美妙。当出现与这些技术完美匹配的新项目时,我们希望尝试一下。
社交网络可以帮助用户识别人与人、群体与群体以及它们之间直接或间接地互动的关系,并且用户可以相互评价、评论和发现感兴趣的事物。通过了解谁与谁进行交流,人们如何连接以及群体中的代表者可能根据群体的集体行动做出何种行动和选择,我们可以获得对个人行为产生影响的看不见力量的重要洞察。
由于社交网络已经以图形化的方式存在,因此可以使用Neo4j来创建数据模型,此举与领域模型直接匹配,并且可以更好地理解数据,避免不必要的工作。
GraphQL是一种天然适应于以图形数据库表示的数据模型的技术。通过用GraphQL对基本数据模型进行建模,可以将数据消费控制转移到前端应用,并能够从持久层查询所需的数据。GraphQL的系统可以保证查询的有效性,并显著减少后端和前端之间的通信开销,从而节省开发工作。
数据模型
Neo4j最大的卖点之一是数据建模,也就是数据模型的构建变得简单而直观。根据我们的经验,事实证明它确实如此。将像在白板上绘制的图像带入到Neo4j模型中,在讨论过程中变得更加容易。不需要标准化、主键/外键,只需要用圆圈和箭头表示。
Neo4j/Neomodel的意思是什么?
我们使用了Neo4j的对象图映射器(OGM)Neomodel来进行建模。Neomodel隐藏了Neo4j的Cypher查询语言的复杂性,虽然一开始有些令人生畏,但我们后端团队都喜欢使用Python。在Neomodel中,客户端和项目的对象及其关系如下所示。
class Client(StructuredNode):
uid = UniqueIdProperty()
name = StringProperty(required=True)
# relationships
instagram = RelationshipTo(InstaUser, 'HAS', cardinality=ZeroOrMore)
projects = RelationshipTo('Project', 'OWNS', cardinality=ZeroOrMore)
class Project(StructuredNode):
uid = UniqueIdProperty()
name = StringProperty(required=True)
# relationships
hashtags = RelationshipTo(Hashtag, 'PROMOTED_WITH', cardinality=ZeroOrMore)
campaigns = RelationshipTo('Campaign', 'CONTAINS', cardinality=ZeroOrMore)
每个客户可以拥有多个项目。每个项目可以拥有多个活动。项目通过多个哈希标签进行推广,这些关系是由基数规则ZeroOrMore进行强制。
客户运营的活动可以具有特定的哈希标签,并由注册在活动中的Instagram用户进行推广。活动通过标记、提及客户的Instagram账户、以及推广哈希标签等多种方式获得可见性。我们将其建模为关系AID。
class AidRel(StructuredRel):
AID_TYPE = {'M': 'mention', 'T': 'tagged', 'H': 'hashtag', 'O': 'manual'}
type = StringProperty(choices=AID_TYPE)
name = StringProperty()
class Campaign(StructuredNode):
uid = UniqueIdProperty()
name = StringProperty()
start = DateTimeProperty()
end = DateTimeProperty()
url = StringProperty()
# relationships
posts = RelationshipTo(InstaPost, 'AID', cardinality=ZeroOrMore, model=AidRel)
entry_users = RelationshipTo(InstaUser, 'ENTRY', cardinality=ZeroOrMore)
aid_users = RelationshipTo(InstaUser, 'PROMOTE_BY', cardinality=ZeroOrMore)
注意:为了简洁起见,InstaUser 和 InstaPost 模型在图中没有显示。
GraphQL和Ariadne
我们选择了Ariadne的GraphQL库来实现API。这是一种基于模式的方法,先用GraphQL查询语言对API进行建模,然后将数据解析器绑定到模式定义中。
模式
GraphQL API會對所有API實體強制使用類型。這樣一來,API變得更加健壯,用戶對於API的使用也更少出現錯誤。類似GraphQL playground這樣的IDE能夠讀取API模式,並在發送查詢之前對查詢進行驗證,這對於前端開發者來說,使用GraphQL API進行開發是非常舒適的體驗。
GraphQL API的設計目標是表示基礎數據模型,可以提高不同類型之間的關聯性,而不需要太在意消費者如何使用。最終,將數據模型的節點類型映射到一對一的GraphQL類型。
type Client {
uid : ID
name: String
user: User
instagram: InstaUser
projects: [Project]
}
type Project {
uid: ID
name: String
hashtags: [Hashtag]
campaigns: [Campaign]
}
为了获取关系数据(将数据放置在边而不是节点上),我们创建了一种新类型,名为AidRelInstaPost,以便聚合关系数据(AidRel)和对应的节点数据(InstaPost)。
type Campaign {
uid: ID
name: String
start: DateTime
end: DateTime
url: String
posts: [InstaPost]
aid_users: [InstaUser]
aid_rel_posts: [AidRelInstaPost]
}
type AidRelInstaPost {
aid: AidRel # edge data
post: InstaPost # node data
}
解决者
GraphQL提供了一种优雅且灵活的方式,通过使用称为解析器(reslolver)的机制来查询类型字段。对于大多数标量字段(如ID、String、Int等),无需准备解析器。因为Ariadne会自然地将Neomodel数据对象的字段名映射到相应的GraphQL类型字段名。对于数组字段,结果通常会少于90%。
@campaign.field("posts")
def reslove_campaign_posts(campaign, *_):
return campaign.posts.all()
对于包含边缘和节点信息的混合类型,我必须依赖Neo4j Cypher查询语言来获取构建数据所需的信息。
@campaign.field("aid_rel_posts")
def reslove_campaign_aid_rel_post(campaign, *_):
query = f"""
MATCH (c:Campaign)-[a:AID_BY]->(p:InstaPost)
WHERE c.uid = '{campaign.uid}'
RETURN a, p
"""
results, _ = db.cypher_query(query)
# Create AidRelInstaPost objects
res = [{'aid': AidRel.inflate(r[0]),
'post': InstaPost.inflate(r[1])} for r in results]
return res
使用GraphQL来表示数据模型时,可以通过简单的查询 getProjects 查询来检索与项目相关的所有数据。
type Query {
getProjects(project: ProjectQueryInput): [Project]
}
input ProjectQueryInput {
# project by uid
uid: ID
# project query by client.uid or .name and project.name
client_uid: ID
client_name: String
project_name: String
}
另外,通过使用GraphQL的选项字段,您可以在QueryInput字段中以不同条件执行查询。在上面的例子中,我们可以使用uid、client_id和project_name的组合,或者client_name和project_name的组合来查询项目。然而,这种方式的缺点是将查询验证留给了查询解析器。
数据查询
getProjects查询可以从这些节点的任何一个对象中请求所需字段。例如,生成项目报告的查询如下:
{
getProjects(project: {
# all projects or criteria
}) {
# Project attributes
uid
name
hashtags {
tag_id
name
}
campaigns { # Campaign attributes
uid
name
start
end
url
posts { # Post attributes
post_id
shortcode
media(first: 1) { # media selection: only first one
display_url
media_id
shortcode
}
}
entry_users { # entry user attributes
ig_id
username
}
}
}
}
前端可以获取任意数据元素,解析器仅对请求的元素执行。下图展示了在GraphQL Playground中提供的上述查询。这种工具可以简化前端开发人员的查询配置。自动补全和错误处理可以加快开发过程,并能够以较少的通信开销适应API规范的更改。
为了提供营销活动客户报告所需的查询数据的前端渲染如下所示。
最终/最后 (zuì
在新项目中使用Neo4j和GraphQL是一次愉快的经历。随着项目需求的变化,我们能够扩展模式并在不进行大规模重构的情况下向数据模型中添加新实体和关系。Neomodel在创建查询和明确描述实体关系的初期阶段是必不可少的,但后来我们不得不依赖Cypher查询语言以更好地控制。GraphQL的API反映了数据模型,因此我们能够在不破坏API的情况下添加新功能。即使是对API的重构,我们也可以有效地向前端传达GraphQL模式文件、查询验证以及在需要使用正确字段名称时提供方便的提示,如惊人的建议。
我們使用這個技術堆疊還不到幾個月,還有很多事情需要探索,但我們相信這個技術堆疊能夠持續滿足我們項目的期望。同時,提及GRANstack也是有價值的,這與JavaScript開發者社群有更緊密的合作關係。