标签 图数据库 下的文章

在这个三篇文章系列中的第二篇中,安装 Neo4j 并通过网页客户端来在图中插入和查询数据。

在本系列的 第一篇 中,我们介绍了图数据库中的一些核心概念。在这篇,我们将安装 Neo4j 并通过网页客户端在图中插入并查询数据。

可通过 他们的网站 下载社区版的 Neo4j!你可以下载 Windows 或 OSX 版来测试,也有各 Linux 发行版对应的版本,还有 Docker 版。

我会在 Debian 9 (stretch) 上安装软件。你可在 这里 查看完整说明。如果你正在使用 Debian 8 (jessie) 或更老的版本,你可以安装当前的版本,但会出现的一点小问题是 jessie 中并没有安装 Neo4j 运行所需要的 Java 8 环境。

wget -O - https://debian.neo4j.org/neotechnology.gpg.key | sudo apt-key add - echo 'deb https://debian.neo4j.org/repo stable/' | sudo tee /etc/apt/sources.list.d/neo4j.list sudo apt-get update sudo apt-get install neo4j

在我的系统中,出于某些原因,我创建好 /var/run/neo4j 之后它就可以很轻松地开始了。Neo4j 给了一个“最大打开文件数”的警告,但因为是测试环境所以我不太需要关心这个问题。Neo4j 默认只会监听本机 localhost 上的连接。如果你的机器是 Debian ,那这很好,但是我的不是。我修改了 /etc/neo4j/neo4j.conf ,取消注释了下面这行:

dbms.connectors.default_listen_address=0.0.0.0

在重启 Neo4j 之后,我可以通过 7474 端口来访问服务器的 Neo4j 服务。默认的用户名和密码是 Neo4jneo4j; 你需要设置一个新密码,然后会出现初始页面:

 title=

让我们在 Neo4j 上创建上篇文章中使用过的图。如下图:

 title=

类似 MySQL 和其它的数据库系统,Neo4j 的各类操作也使用一套查询语句。Cypher,就是 Neo4j 使用的查询语句,但有一些语法区别需要去学习和注意。 节点 node 需要用圆括号表示,而 关系 relationship 需要放在方括号中。因为这是系统中唯二的数据类型,所以了解这些就够了。

首先,我们创建所有的节点。你需要将下面内容复制黏贴到浏览器顶部区域中,在那里运行查询语句。

CREATE (a:Person { name: 'Jane Doe', favorite_color: 'purple' }) CREATE (b:Person { name: 'John Doe' }) CREATE (c:Person { name: 'Mary Smith', favorite_color: 'red', dob: '1992-11-09' }) CREATE (d:Person { name: 'Robert Roe' }) CREATE (e:Person { name: 'Rhonda Roe' }) CREATE (f:Person { name: 'Ryan Roe' }) CREATE (t:City { name: 'Petaluma, CA' }) CREATE (u:City { name: 'Cypress, TX' }) CREATE (v:City { name: 'Grand Prairie, TX' }) CREATE (w:City { name: 'Houston, TX' })

注意,在标签前的字符就是变量。这些信息会在出现在各个地方,但我们在这里并不会用到。但你不能不指定相应信息就盲目创建,所以我们使用它们然后就忽略它们。

在上面一共创建了 10 个节点和 13 个属性。想查看它们? 通过下面语句来匹配查询所有节点:

MATCH (n) RETURN n

这条语句会返回一个可视化的图。(在应用内,你可以在返回的图中使用”全屏”按钮来查看大图)。你将会看到类似下面的图像:

 title=

添加关系需要一点技巧;你需要连接的节点必须是 “ 在限定范围内的 in scope ”,意思连接的节点是在当前查询语句所限定的范围内的。我们之前使用的查询语句范围太大,所以让我们找到 John 和 Jane 并让他们结婚:

MATCH (a:Person),(b:Person) WHERE a.name='Jane Doe' AND b.name='John Doe' CREATE (a)-[r:MARRIAGE {date: '2017-03-04', place: 'Houston, TX'}]->(b)

这条语句会创建一个关系并设置两个属性。重新运行该 MATCH 语句会显示那个关系。你可以通过鼠标点击任意的节点或关系来查看它们的属性。

我们来添加其它的关系。比起使用一些列的 MATCH 语句,我会一次性做完并从中 CREATE 创建多个关系。

MATCH (a:Person),(b:Person),(c:Person),(d:Person),(e:Person),(f:Person),(t:City),(u:City),(v:City),(w:City) WHERE a.name='Jane Doe' AND b.name='John Doe' AND c.name='Mary Smith' AND d.name='Robert Roe' AND e.name='Rhonda Roe' AND f.name='Ryan Roe' AND t.name='Petaluma, CA' AND u.name='Cypress, TX' AND v.name='Grand Prairie, TX' AND w.name='Houston, TX' CREATE (d)-[m2:MARRIAGE {date: '1990-12-01', place: 'Chicago, IL'}]->(e) CREATE (a)-[n:CHILD]->(c) CREATE (d)-[n2:CHILD]->(f) CREATE (e)-[n3:CHILD]->(f) CREATE (b)-[n4:STEPCHILD]->(c) CREATE (a)-[o:BORN_IN]->(v) CREATE (b)-[o2:BORN_IN]->(t) CREATE (c)-[p:DATING]->(f) CREATE (a)-[q:LIVES_IN]->(u) CREATE (b)-[q1:LIVES_IN]->(u) CREATE (a)-[r:WORKS_IN]->(w) CREATE (a)-[s:FRIEND]->(d) CREATE (a)-[s2:FRIEND]->(e)

重新运行该 MATCH 语句,你将会看到下面图像:

 title=

如果你喜欢,你可以将节点拖拉成像我之前画的图的样子。

在这个例子中,我们唯一使用的 MATCH 就是 MATCH 所有的东西。下面这个查询会返回两个结婚了的夫妻并显示他们之间的关系:

MATCH (a)-[b:MARRIAGE]->(c) RETURN a,b,c

在一个更复杂的图中,你可以做更多的细节查询。(LCTT 译注:此例子为 Neo4j 自带例子的)例如,你有关于电影和人的节点,还有像 ACTED INDIRECTEDWROTE SCREENPLAY 等属性的关系,你可以运行下面这个查询:

MATCH (p:Person { name: 'Mel Gibson' })--(m:Movie) RETURN m.title

……上述是查询和 Mel Gibson 相关的所有影片。但如果你想查询他演过的所有电影,下面这条语句会更有用:

MATCH (p:Person { name: 'Mel Gibson' })-[r:ACTED_IN]->(m:movie) RETURN m.title,r.role

还有更多更炫酷的 Cypher 语句可以使用,但我们就简单介绍这些。更详细完整的 Cypher 语句可以在 Neo4j 的官网上查看, 并且也有很多例子可以练习。

在此系列的下篇文章中,我们会通过写些 Perl 脚本来展示如何在应用中使用图数据库。


via: https://opensource.com/article/17/7/neo4j-graph-databases-getting-started

作者:Ruth Holloway 译者:happygeorge01 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

在这个三篇系列文章的第一篇文章中,我们将学习 图数据库 graph database 的基础知识,它支持了这地球上最大的一些数据池。

对于海量的各种非结构化信息来说,图数据库已经成为帮助收集、管理和搜索大量数据的技术。在这三篇系列文章中,我们将使用开源图数据库软件 Neo4j 来研究图数据库。

在本文中,我将向你展示图数据库的基础知识,帮助你快速了解概念模型。在第二篇中,我将向你展示如何启动 Neo4j 数据库,并使用内置的浏览器工具填充一些数据。而且,在本系列的最后一篇文章中,我们将探讨一些在开发工作中使用的 Neo4j 编程库。

掌握图数据库的概念模型是有用的,所以我们从那里开始。图数据库中只存储两种数据: 节点 node edge

  • 节点是实体:诸如人物、发票、电影、书籍或其他具体事物。这些有些等同于关系数据库中的记录或行。
  • 边名关系:连接节点的概念、事件或事物。在关系数据库中,这些关系通常存储在具有链接字段的数据库行中。在图数据库中,它们本身就是有用的,是可以以其自己的权限搜索的对象。

节点和边都可以拥有可搜索的属性。例如,如果你的节点代表人,他们可能拥有名字、性别、出生日期、身高等属性。而边的属性可能描述了两个人之间的关系何时建立,见面的情况或关系的性质。

这是一个帮助你可视化的图表:

 title=

在这张图中,你知道 Jane Doe 有一个新的丈夫 John;一个女儿(来自她以前的夫妻关系)Mary Smith 和朋友 Robert 和 Rhonda Roe。Roes 有一个儿子 Ryan,他正在与 Mary Smith 约会。

看看它怎么工作?每个节点代表一个独立于其他节点的人。你需要找到关于那个人的一切都可以存储在节点的属性中。边描述了人们之间的关系,这与你在程序中需要的一样多。

关系是单向的,且不能是无向的,但这没有问题。由于数据库可以以相同的速度遍历两个方向,并且方向可以忽略,你只需要定义一次此关系。如果你的程序需要定向关系,则可以自由使用它们,但如果双向性是暗含的,则不需要。

另外需要注意的是,图数据库本质上是无 schema 的。这与关系数据库不同,关系数据库每行都有一组列表,并且添加新的字段会给开发和升级带来很多工作。

每个节点都可以拥有一个 标签 label ;对于大多数程序你需要“输入”这个标签,是对典型的关系数据库中的表名的模拟。标签可以让你区分不同的节点类型。如果你需要添加新的标签或属性,修改程序来用它就行!

使用图数据库,你可以直接开始使用新的属性和标签,节点将在创建或编辑时获取它们。不需要转换东西;只需在你的代码中使用它们即可。在这里的例子中,你可以看到,我们知道 Jane 和 Mary 最喜欢的颜色和 Mary 的出生日期,但是别人没有(这些属性)。这个系统不需要知道它;用户可以在正常使用程序的过程中访问节点时为其添加信息(属性)。

作为一名开发人员,这是一个有用的特性。你可以将新的标签或属性添加到由节点处理的表单中并开始使用它,而不必进行数据库 schema 的修改。对于没有该属性的节点,将不显示任何内容。你可以使用任何一种类型的数据库来为表单进行编码,但是你可以放下在关系型数据库中要进行的许多后端工作了。

让我们添加一些新的信息:

 title=

这是一个新的节点类型,它代表一个位置,以及一些相关关系。现在我们看到 John Doe 出生在加利福尼亚州的 Petaluma,而他的妻子 Jane 则出生在德克萨斯州的 Grand Prairie。 他们现在住在得克萨斯州的赛普拉斯,因为 Jane 在附近的休斯顿工作。Ryan Roe 缺乏城市关系对数据库来说没有什么大不了的事情,我们不知道那些信息而已。当用户输入更多数据时,数据库可以轻松获取新数据并添加新数据,并根据需要创建新的节点和关系。

了解节点和边应该足以让你开始使用图形数据库。如果你像我一样,已经在考虑如何在一个图中重组你的程序。在本系列的下一篇文章中,我将向你展示如何安装 Neo4j、插入数据,并进行一些基本的搜索。


via: https://opensource.com/article/17/7/fundamentals-graph-databases-neo4j

作者:Ruth Holloway 译者:geekpi 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

摘要

GraphQL 在生产环境中似乎难以使用:虽然对于建模功能来说图接口非常灵活,但是并不适用于关系型存储,不管是在实现还是性能方面。

在这篇博客中,我们会设计并实现一个简单的博客引擎 API,它支持以下功能:

  • 三种类型的资源(用户、博文以及评论)支持多种功能(创建用户、创建博文、给博文添加评论、关注其它用户的博文和评论,等等。)
  • 使用 PostgreSQL 作为后端数据存储(选择它因为它是一个流行的关系型数据库)。
  • 使用 Golang(开发 API 的一个流行语言)实现 API。

我们会比较简单的 GraphQL 实现和纯 REST 替代方案,在一种普通场景(呈现博客文章页面)下对比它们的实现复杂性和效率。

介绍

GraphQL 是一种 IDL( 接口定义语言 Interface Definition Language ),设计者定义数据类型和并把数据建模为一个 graph 。每个顶点都是一种数据类型的一个实例,边代表了节点之间的关系。这种方式非常灵活,能适应任何业务领域。然而,问题是设计过程更加复杂,而且传统的数据存储不能很好地映射到图模型。阅读附录1了解更多关于这个问题的详细信息。

GraphQL 在 2014 年由 Facebook 的工程师团队首次提出。尽管它的优点和功能非常有趣而且引人注目,但它并没有得到大规模应用。开发者需要权衡 REST 的设计简单性、熟悉性、丰富的工具和 GraphQL 不会受限于 CRUD(LCTT 译注:Create、Read、Update、Delete) 以及网络性能(它优化了往返服务器的网络)的灵活性。

大部分关于 GraphQL 的教程和指南都跳过了从数据存储获取数据以便解决查询的问题。也就是,如何使用通用目的、流行存储方案(例如关系型数据库)为 GraphQL API 设计一个支持高效数据提取的数据库。

这篇博客介绍构建一个博客引擎 GraphQL API 的流程。它的功能相当复杂。为了和基于 REST 的方法进行比较,它的范围被限制为一个熟悉的业务领域。

这篇博客的文章结构如下:

  • 第一部分我们会设计一个 GraphQL 模式并介绍所使用语言的一些功能。
  • 第二部分是 PostgreSQL 数据库的设计。
  • 第三部分介绍了使用 Golang 实现第一部分设计的 GraphQL 模式。
  • 第四部分我们以从后端获取所需数据的角度来比较呈现博客文章页面的任务。

相关阅读

在 GraphQL 中建模一个博客引擎

下述列表1包括了博客引擎 API 的全部模式。它显示了组成图的顶点的数据类型。顶点之间的关系,也就是边,被建模为指定类型的属性。

type User {
  id: ID
  email: String!
  post(id: ID!): Post
  posts: [Post!]!
  follower(id: ID!): User
  followers: [User!]!
  followee(id: ID!): User
  followees: [User!]!
}

type Post {
  id: ID
  user: User!
  title: String!
  body: String!
  comment(id: ID!): Comment
  comments: [Comment!]!
}

type Comment {
  id: ID
  user: User!
  post: Post!
  title: String
  body: String!
}

type Query {
  user(id: ID!): User
}

type Mutation {
  createUser(email: String!): User
  removeUser(id: ID!): Boolean
  follow(follower: ID!, followee: ID!): Boolean
  unfollow(follower: ID!, followee: ID!): Boolean
  createPost(user: ID!, title: String!, body: String!): Post
  removePost(id: ID!): Boolean
  createComment(user: ID!, post: ID!, title: String!, body: String!): Comment
  removeComment(id: ID!): Boolean
}

列表1

模式使用 GraphQL DSL 编写,它用于定义自定义数据类型,例如 UserPostComment。该语言也提供了一系列原始数据类型,例如 StringBooleanID(它是String 的别名,但是有顶点唯一标识符的额外语义)。

QueryMutation 是语法解析器能识别并用于查询图的可选类型。从 GraphQL API 读取数据等同于遍历图。需要提供这样一个起始顶点;该角色通过 Query 类型来实现。在这种情况中,所有图的查询都要从一个由 id user(id:ID!) 指定的用户开始。对于写数据,定义了 Mutation 顶点。它提供了一系列操作,建模为能遍历(并返回)新创建顶点类型的参数化属性。列表2是这些查询的一些例子。

顶点属性能被参数化,也就是能接受参数。在图遍历场景中,如果一个博文顶点有多个评论顶点,你可以通过指定 comment(id: ID) 只遍历其中的一个。所有这些都取决于设计,设计者可以选择不提供到每个独立顶点的直接路径。

! 字符是一个类型后缀,适用于原始类型和用户定义类型,它有两种语义:

  • 当被用于参数化属性的参数类型时,表示这个参数是必须的。
  • 当被用于一个属性的返回类型时,表示当顶点被获取时该属性不会为空。
  • 也可以把它们组合起来,例如 [Comment!]! 表示一个非空 Comment 顶点链表,其中 [][Comment] 是有效的,但 null, [null], [Comment, null] 就不是。

列表2 包括一系列用于博客 API 的 curl 命令,它们会使用 mutation 填充图然后查询图以便获取数据。要运行它们,按照 topliceanu/graphql-go-example 仓库中的指令编译并运行服务。

# 创建用户 1、2 和 3 的更改。更改和查询类似,在该情景中我们检索新创建用户的 id 和 email。
curl -XPOST http://vm:8080/graphql -d 'mutation {createUser(email:"[email protected]"){id, email}}'
curl -XPOST http://vm:8080/graphql -d 'mutation {createUser(email:"[email protected]"){id, email}}'
curl -XPOST http://vm:8080/graphql -d 'mutation {createUser(email:"[email protected]"){id, email}}'
# 为用户添加博文的更改。为了和模式匹配我们需要检索他们的 id,否则会出现错误。
curl -XPOST http://vm:8080/graphql -d 'mutation {createPost(user:1,title:"post1",body:"body1"){id}}'
curl -XPOST http://vm:8080/graphql -d 'mutation {createPost(user:1,title:"post2",body:"body2"){id}}'
curl -XPOST http://vm:8080/graphql -d 'mutation {createPost(user:2,title:"post3",body:"body3"){id}}'
# 博文所有评论的更改。`createComment` 需要用户 id,标题和正文。看列表 1 的模式。
curl -XPOST http://vm:8080/graphql -d 'mutation {createComment(user:2,post:1,title:"comment1",body:"comment1"){id}}'
curl -XPOST http://vm:8080/graphql -d 'mutation {createComment(user:1,post:3,title:"comment2",body:"comment2"){id}}'
curl -XPOST http://vm:8080/graphql -d 'mutation {createComment(user:3,post:3,title:"comment3",body:"comment3"){id}}'
# 让用户 3 关注用户 1 和用户 2 的更改。注意 `follow` 更改只返回一个布尔值而不需要指定。
curl -XPOST http://vm:8080/graphql -d 'mutation {follow(follower:3, followee:1)}'
curl -XPOST http://vm:8080/graphql -d 'mutation {follow(follower:3, followee:2)}'

# 用户获取用户 1 所有数据的查询。
curl -XPOST http://vm:8080/graphql -d '{user(id:1)}'
# 用户获取用户 2 和用户 1 的关注者的查询。
curl -XPOST http://vm:8080/graphql -d '{user(id:2){followers{id, email}}}'
curl -XPOST http://vm:8080/graphql -d '{user(id:1){followers{id, email}}}'
# 检测用户 2 是否被用户 1 关注的查询。如果是,检索用户 1 的 email,否则返回空。
curl -XPOST http://vm:8080/graphql -d '{user(id:2){follower(id:1){email}}}'
# 返回用户 3 关注的所有用户 id 和 email 的查询。
curl -XPOST http://vm:8080/graphql -d '{user(id:3){followees{id, email}}}'
# 如果用户 3 被用户 1 关注,就获取用户 3 email 的查询。
curl -XPOST http://vm:8080/graphql -d '{user(id:1){followee(id:3){email}}}'
# 获取用户 1 的第二篇博文的查询,检索它的标题和正文。如果博文 2 不是由用户 1 创建的,就会返回空。
curl -XPOST http://vm:8080/graphql -d '{user(id:1){post(id:2){title,body}}}'
# 获取用户 1 的所有博文的所有数据的查询。
curl -XPOST http://vm:8080/graphql -d '{user(id:1){posts{id,title,body}}}'
# 获取写博文 2 用户的查询,如果博文 2 是由 用户 1 撰写;一个现实语言灵活性的例证。
curl -XPOST http://vm:8080/graphql -d '{user(id:1){post(id:2){user{id,email}}}}'

列表2

通过仔细设计 mutation 和类型属性,可以实现强大而富有表达力的查询。

设计 PostgreSQL 数据库

关系型数据库的设计,一如以往,由避免数据冗余的需求驱动。选择该方式有两个原因:

  1. 表明实现 GraphQL API 不需要定制化的数据库技术或者学习和使用新的设计技巧。
  2. 表明 GraphQL API 能在现有的数据库之上创建,更具体地说,最初设计用于 REST 后端甚至传统的呈现 HTML 站点的服务器端数据库。

阅读 附录1 了解关于关系型和图数据库在构建 GraphQL API 方面的区别。列表3 显示了用于创建新数据库的 SQL 命令。数据库模式和 GraphQL 模式相对应。为了支持 follow/unfollow 更改,需要添加 followers 关系。

CREATE TABLE IF NOT EXISTS users (
  id SERIAL PRIMARY KEY,
  email VARCHAR(100) NOT NULL
);
CREATE TABLE IF NOT EXISTS posts (
  id SERIAL PRIMARY KEY,
  user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
  title VARCHAR(200) NOT NULL,
  body TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS comments (
  id SERIAL PRIMARY KEY,
  user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
  post_id INTEGER NOT NULL REFERENCES posts(id) ON DELETE CASCADE,
  title VARCHAR(200) NOT NULL,
  body TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS followers (
  follower_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
  followee_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
  PRIMARY KEY(follower_id, followee_id)
);

列表3

Golang API 实现

本项目使用的用 Go 实现的 GraphQL 语法解析器是 github.com/graphql-go/graphql。它包括一个查询解析器,但不包括模式解析器。这要求开发者利用库提供的结构使用 Go 构建 GraphQL 模式。这和 nodejs 实现 不同,后者提供了一个模式解析器并为数据获取暴露了钩子。因此 列表1 中的模式只是作为指导使用,需要转化为 Golang 代码。然而,这个“限制”提供了与抽象级别对等的机会,并且了解模式如何和用于检索数据的图遍历模型相关。列表4 显示了 Comment 顶点类型的实现:

var CommentType = graphql.NewObject(graphql.ObjectConfig{
    Name: "Comment",
    Fields: graphql.Fields{
        "id": &graphql.Field{
            Type: graphql.NewNonNull(graphql.ID),
            Resolve: func(p graphql.ResolveParams) (interface{}, error) {
                if comment, ok := p.Source.(*Comment); ok == true {
                    return comment.ID, nil
                }
                return nil, nil
            },
        },
        "title": &graphql.Field{
            Type: graphql.NewNonNull(graphql.String),
            Resolve: func(p graphql.ResolveParams) (interface{}, error) {
                if comment, ok := p.Source.(*Comment); ok == true {
                    return comment.Title, nil
                }
                return nil, nil
            },
        },
        "body": &graphql.Field{
            Type: graphql.NewNonNull(graphql.ID),
            Resolve: func(p graphql.ResolveParams) (interface{}, error) {
                if comment, ok := p.Source.(*Comment); ok == true {
                    return comment.Body, nil
                }
                return nil, nil
            },
        },
    },
})
func init() {
    CommentType.AddFieldConfig("user", &graphql.Field{
        Type: UserType,
        Resolve: func(p graphql.ResolveParams) (interface{}, error) {
            if comment, ok := p.Source.(*Comment); ok == true {
                return GetUserByID(comment.UserID)
            }
            return nil, nil
        },
    })
    CommentType.AddFieldConfig("post", &graphql.Field{
        Type: PostType,
        Args: graphql.FieldConfigArgument{
            "id": &graphql.ArgumentConfig{
                Description: "Post ID",
                Type:        graphql.NewNonNull(graphql.ID),
            },
        },
        Resolve: func(p graphql.ResolveParams) (interface{}, error) {
            i := p.Args["id"].(string)
            id, err := strconv.Atoi(i)
            if err != nil {
                return nil, err
            }
            return GetPostByID(id)
        },
    })
}

列表4

正如 列表1 中的模式,Comment 类型是静态定义的一个有三个属性的结构体:idtitlebody。为了避免循环依赖,动态定义了 userpost 两个其它属性。

Go 并不适用于这种动态建模,它只支持一些类型检查,代码中大部分变量都是 interface{} 类型,在使用之前都需要进行类型断言。CommentType 是一个 graphql.Object 类型的变量,它的属性是 graphql.Field 类型。因此,GraphQL DSL 和 Go 中使用的数据结构并没有直接的转换。

每个字段的 resolve 函数暴露了 Source 参数,它是表示遍历时前一个节点的数据类型顶点。Comment 的所有属性都有作为 source 的当前 CommentType 顶点。检索idtitlebody 是一个直接属性访问,而检索 userpost 要求图遍历,也需要数据库查询。由于它们非常简单,这篇文章并没有介绍这些 SQL 查询,但在参考文献部分列出的 github 仓库中有。

普通场景下和 REST 的对比

在这一部分,我们会展示一个普通的博客文章呈现场景,并比较 REST 和 GraphQL 的实现。关注重点会放在入站/出站请求数量,因为这些是造成页面呈现延迟的最主要原因。

场景:呈现一个博客文章页面。它应该包含关于作者(email)、博客文章(标题、正文)、所有评论(标题、正文)以及评论人是否关注博客文章作者的信息。图1图2 显示了客户端 SPA、API 服务器以及数据库之间的交互,一个是 REST API、另一个对应是 GraphQL API。

+------+                  +------+                  +--------+
|client|                  |server|                  |database|
+--+---+                  +--+---+                  +----+---+
   |      GET /blogs/:id     |                           |
1\. +------------------------->  SELECT * FROM blogs...   |
   |                         +--------------------------->
   |                         <---------------------------+
   <-------------------------+                           |
   |                         |                           |
   |     GET /users/:id      |                           |
2\. +------------------------->  SELECT * FROM users...   |
   |                         +--------------------------->
   |                         <---------------------------+
   <-------------------------+                           |
   |                         |                           |
   | GET /blogs/:id/comments |                           |
3\. +-------------------------> SELECT * FROM comments... |
   |                         +--------------------------->
   |                         <---------------------------+
   <-------------------------+                           |
   |                         |                           |
   | GET /users/:id/followers|                           |
4\. +-------------------------> SELECT * FROM followers.. |
   |                         +--------------------------->
   |                         <---------------------------+
   <-------------------------+                           |
   |                         |                           |
   +                         +                           +

图1

+------+                  +------+                  +--------+
|client|                  |server|                  |database|
+--+---+                  +--+---+                  +----+---+
   |      GET /graphql       |                           |
1\. +------------------------->  SELECT * FROM blogs...   |
   |                         +--------------------------->
   |                         <---------------------------+
   |                         |                           |
   |                         |                           |
   |                         |                           |
2\. |                         |  SELECT * FROM users...   |
   |                         +--------------------------->
   |                         <---------------------------+
   |                         |                           |
   |                         |                           |
   |                         |                           |
3\. |                         | SELECT * FROM comments... |
   |                         +--------------------------->
   |                         <---------------------------+
   |                         |                           |
   |                         |                           |
   |                         |                           |
4\. |                         | SELECT * FROM followers.. |
   |                         +--------------------------->
   |                         <---------------------------+
   <-------------------------+                           |
   |                         |                           |
   +                         +                           +

图2

列表5 是一条用于获取所有呈现博文所需数据的简单 GraphQL 查询。

{
  user(id: 1) {
    email
    followers
    post(id: 1) {
      title
      body
      comments {
        id
        title
        user {
          id
          email
        }
      }
    }
  }
}

列表5

对于这种情况,对数据库的查询次数是故意相同的,但是到 API 服务器的 HTTP 请求已经减少到只有一个。我们认为在这种类型的应用程序中通过互联网的 HTTP 请求是最昂贵的。

为了利用 GraphQL 的优势,后端并不需要进行特别设计,从 REST 到 GraphQL 的转换可以逐步完成。这使得可以测量性能提升和优化。从这一点,API 设计者可以开始优化(潜在的合并) SQL 查询从而提高性能。缓存的机会在数据库和 API 级别都大大增加。

SQL 之上的抽象(例如 ORM 层)通常会和 n+1 问题相抵触。在 REST 示例的步骤 4 中,客户端可能不得不在单独的请求中为每个评论的作者请求关注状态。这是因为在 REST 中没有标准的方式来表达两个以上资源之间的关系,而 GraphQL 旨在通过使用嵌套查询来防止这类问题。这里我们通过获取用户的所有关注者来作弊。我们向客户提出了如何确定评论并关注了作者的用户的逻辑。

另一个区别是获取比客户端所需更多的数据,以免破坏 REST 资源抽象。这对于用于解析和存储不需要数据的带宽消耗和电池寿命非常重要。

总结

GraphQL 是 REST 的一个可用替代方案,因为:

  • 尽管设计 API 更加困难,但该过程可以逐步完成。也是由于这个原因,从 REST 转换到 GraphQL 非常容易,两个流程可以没有任何问题地共存。
  • 在网络请求方面更加高效,即使是类似本博客中的简单实现。它还提供了更多查询优化和结果缓存的机会。
  • 在用于解析结果的带宽消耗和 CPU 周期方面它更加高效,因为它只返回呈现页面所需的数据。

REST 仍然非常有用,如果:

  • 你的 API 非常简单,只有少量的资源或者资源之间关系简单。
  • 在你的组织中已经在使用 REST API,而且你已经配置好了所有工具,或者你的客户希望获取 REST API。
  • 你有复杂的 ACL(LCTT 译注:Access Control List) 策略。在博客例子中,可能的功能是允许用户良好地控制谁能查看他们的电子邮箱、博客、特定博客的评论、他们关注了谁,等等。优化数据获取同时检查复杂的业务规则可能会更加困难。

附录1:图数据库和高效数据存储

尽管将其应用领域数据想象为一个图非常直观,正如这篇博文介绍的那样,但是支持这种接口的高效数据存储问题仍然没有解决。

近年来图数据库变得越来越流行。通过将 GraphQL 查询转换为特定的图数据库查询语言从而延迟解决请求的复杂性似乎是一种可行的方案。

问题是和关系型数据库相比,图并不是一种高效的数据结构。图中一个顶点可能有到任何其它顶点的连接,访问模式比较难以预测因此提供了较少的优化机会。

例如缓存的问题,为了快速访问需要将哪些顶点保存在内存中?通用缓存算法在图遍历场景中可能没那么高效。

数据库分片问题:把数据库切分为更小、没有交叉的数据库并保存到独立的硬件。在学术上,最小切割的图划分问题已经得到了很好的理解,但可能是次优的,而且由于病态的最坏情况可能导致高度不平衡切割。

在关系型数据库中,数据被建模为记录(行或者元组)和列,表和数据库名称都只是简单的命名空间。大部分数据库都是面向行的,意味着每个记录都是一个连续的内存块,一个表中的所有记录在磁盘上一个接一个地整齐地打包(通常按照某个关键列排序)。这非常高效,因为这是物理存储最优的工作方式。HDD 最昂贵的操作是将磁头移动到磁盘上的另一个扇区,因此最小化此类访问非常重要。

很有可能如果应用程序对一条特定记录感兴趣,它需要获取整条记录,而不仅仅是记录中的其中一列。也很有可能如果应用程序对一条记录感兴趣,它也会对该记录周围的记录感兴趣,例如全表扫描。这两点使得关系型数据库相当高效。然而,也是因为这个原因,关系型数据库的最差使用场景就是总是随机访问所有数据。图数据库正是如此。

随着支持更快随机访问的 SSD 驱动器的出现,更便宜的内存使得缓存大部分图数据库成为可能,更好的优化图缓存和分区的技术,图数据库开始成为可选的存储解决方案。大部分大公司也使用它:Facebook 有 Social Graph,Google 有 Knowledge Graph。


via: http://alexandrutopliceanu.ro/post/graphql-with-go-and-postgresql

作者:Alexandru Topliceanu 译者:ictlyh 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

编者注:本文是 2016 年 4 月 Nicole Whilte 在欧洲 GraphConnect 时所作。这儿我们快速回顾一下她所涉及的内容:

  • 图数据库推荐基础
  • 社会化推荐
  • 相似性推荐
  • 集群推荐

今天我们将要讨论的内容是数据科学和 图推荐 graph recommendations

我在 Neo4j 任职已经两年了,但实际上我已经使用 Neo4j 和 Cypher 工作三年了。当我首次发现这个特别的 图数据库 graph database 的时候,我还是一个研究生,那时候我在奥斯丁的德克萨斯大学攻读关于社交网络的统计学硕士学位。

实时推荐引擎是 Neo4j 中最广泛的用途之一,也是使它如此强大并且容易使用的原因之一。为了探索这个东西,我将通过使用示例数据集来阐述如何将统计学方法并入这些引擎中。

第一个很简单 - 将 Cypher 用于社交推荐。接下来,我们将看一看相似性推荐,这涉及到可被计算的相似性度量,最后探索的是集群推荐。

图数据库推荐基础

下面的数据集包含所有达拉斯 Fort Worth 国际机场的餐饮场所,达拉斯 Fort Worth 国际机场是美国主要的机场枢纽之一:

我们把节点标记成黄色并按照出入口和航站楼给它们的位置建模。同时我们也按照食物和饮料的主类别将地点分类,其中一些包括墨西哥食物、三明治、酒吧和烤肉。

让我们做一个简单的推荐。我们想要在机场的某一确定地点找到一种特定食物,大括号中的内容表示是的用户输入,它将进入我们的假想应用程序中。

这个英文句子表示成 Cypher 查询:

这将提取出该类别中用户所请求的所有地点、航站楼和出入口。然后我们可以计算出用户所在位置到出入口的准确距离,并以升序返回结果。再次说明,这个非常简单的 Cypher 推荐仅仅依据的是用户在机场中的位置。

社交推荐 Social Recommendations

让我们来看一下社交推荐。在我们的假想应用程序中,用户可以登录并且可以用和 Facebook 类似的方式标记自己“喜好”的地点,也可以在某地签到。

考虑位于我们所研究的第一个模型之上的数据模型,现在让我们在下面的分类中找到用户的朋友喜好的航站楼里面离出入口最近的餐饮场所:

MATCH 子句和我们第一次 Cypher 查询的 MATCH 子句相似,只是现在我们依据喜好和朋友来匹配:

前三行是完全一样的,但是现在要考虑的是那些登录的用户,我们想要通过 :FRIENDS_WITH 这一关系来找到他们的朋友。仅需通过在 Cypher 中增加一些行内容,我们现在已经把社交层面考虑到了我们的推荐引擎中。

再次说明,我们仅仅显示了用户明确请求的类别,并且这些类别中的地点与用户进入的地方是相同的航站楼。当然,我们希望按照登录并做出请求的用户来滤过这些目录,然后返回地点的名字、位置以及所在目录。我们也要显示出有多少朋友已经“喜好”那个地点以及那个地点到出入口的确切距离,然后在 RETURN 子句中同时返回所有这些内容。

相似性推荐 Similarity Recommendations

现在,让我们看一看相似性推荐引擎:

和前面的数据模型相似,用户可以标记“喜好”的地点,但是这一次他们可以用 1 到 10 的整数给地点评分。这是通过前期在 Neo4j 中增加一些属性到关系中建模实现的。

这将允许我们找到其他相似的用户,比如以上面的 Greta 和 Alice 为例,我们已经查询了他们共同喜好的地点,并且对于每一个地点,我们可以看到他们所设定的权重。大概地,我们可以通过他们的评分来确定他们之间的相似性大小。

现在我们有两个向量:

现在让我们按照 欧几里得距离 Euclidean distance 的定义来计算这两个向量之间的距离:

我们把所有的数字带入公式中计算,然后得到下面的相似度,这就是两个用户之间的“距离”:

你可以很容易地在 Cypher 中计算两个特定用户的“距离”,特别是如果他们仅仅同时“喜好”一个很小的地点子集。再次说明,这儿我们依据两个用户 Alice 和 Greta 来进行匹配,并尝试去找到他们同时“喜好”的地点:

他们都有对最后找到的地点的 :LIKES 关系,然后我们可以在 Cypher 中很容易的计算出他们之间的欧几里得距离,计算方法为他们对各个地点评分差的平方求和再开平方根。

在两个特定用户的例子中上面这个方法或许能够工作。但是,在实时情况下,当你想要通过和实时数据库中的其他用户比较,从而由一架飞机上的一个用户推断相似用户时,这个方法就不一定能够工作。不用说,至少它不能够很好的工作。

为了找到解决这个问题的好方法,我们可以预先计算好距离并存入实际关系中:

当遇到一个很大的数据集时,我们需要成批处理这件事,在这个很小的示例数据集中,我们可以按照所有用户的 迪卡尔乘积 Cartesian product 和他们共同“喜好”的地点来进行匹配。当我们使用 WHERE id(u1) < id(u2) 作为 Cypher 询问的一部分时,它只是来确定我们在左边和右边没有找到相同的对的一个技巧。

通过用户之间的欧几里得距离,我们创建了他们之间的一种关系,叫做 :DISTANCE,并且设置了一个叫做 euclidean 的欧几里得属性。理论上,我们可以也通过用户间的一些关系来存储其他相似度从而获取不同的相似度,因为在确定的环境下某些相似度可能比其他相似度更有用。

在 Neo4j 中,的确是对关系属性建模的能力使得完成像这样的事情无比简单。然而,实际上,你不会希望存储每一个可能存在的单一关系,因为你仅仅希望返回离他们“最近”的一些人。

因此你可以根据一些临界值来存入前几个,从而你不需要构建完整的连通图。这允许你完成一些像下面这样的实时的数据库查询,因为我们已经预先计算好了“距离”并存储在了关系中,在 Cypher 中,我们能够很快的攫取出数据。

在这个查询中,我们依据地点和类别来进行匹配:

再次说明,前三行是相同的,除了登录用户以外,我们找出了和他们有 :DISTANCE 关系的用户。这是我们前面查看的关系产生的作用 - 实际上,你只需要存储处于前几位的相似用户 :DISTANCE 关系,因此你不需要在 MATCH 子句中攫取大量用户。相反,我们只攫取和那些用户“喜好”的地方有 :DISTANCE 关系的用户。

这允许我们用少许几行内容表达较为复杂的模型。我们也可以攫取 :LIKES 关系并把它放入到变量中,因为后面我们将使用这些权重来评分。

在这儿重要的是,我们可以依据“距离”大小将用户按照升序进行排序,因为这是一个距离测度。同时,我们想要找到用户间的最小距离因为距离越小表明他们的相似度最大。

通过其他按照欧几里得距离大小排序好的用户,我们得到用户评分最高的三个地点并按照用户的平均评分高低来推荐这些地点。换句话说,我们先找出一个活跃用户,然后依据其他用户“喜好”的地点找出和他最相似的其他用户,接下来按照这些相似用户的平均评分把那些地点排序在结果的集合中。

本质上,我们通过把所有评分相加然后除以收集的用户数目来计算出平均分,然后按照平均评分的升序进行排序。其次,我们按照出入口距离排序。假想地,我猜测应该会有交接点,因此你可以按照出入口距离排序然后再返回名字、类别、出入口和航站楼。

集群推荐 Cluster Recommendations

我们最后要讲的一个例子是集群推荐,在 Cypher 中,这可以被想像成一个作为临时解决方案的离线计算工作流。这可能完全基于在欧洲 GraphConnect 上宣布的新方法,但是有时你必须进行一些 Cypher 2.3 版本所没有的算法逼近。

在这儿你可以使用一些统计软件,把数据从 Neo4j 取出然后放入像 Apache Spark、R 或者 Python 这样的软件中。下面是一段把数据从 Neo4j 中取出的 R 代码,运行该程序,如果正确,写下程序返回结果的给 Neo4j,可以是一个属性、节点、关系或者一个新的标签。

通过持续把程序运行结果放入到图表中,你可以在一个和我们刚刚看到的查询相似的实时查询中使用它:

下面是用 R 来完成这件事的一些示例代码,但是你可以使用任何你最喜欢的软件来做这件事,比如 Python 或 Spark。你需要做的只是登录并连接到图表。

在下面的例子中,我基于用户的相似性把他们聚合起来。每个用户作为一个观察点,然后得到他们对每一个目录评分的平均值。

假定用户对酒吧类评分的方式和一般的评分方式相似。然后我攫取出喜欢相同类别中的地点的用户名、类别名、“喜好”关系的平均权重,比如平均权重这些信息,从而我可以得到下面这样一个表格:

因为我们把每一个用户都作为一个观察点,所以我们必须巧妙的处理每一个类别中的数据,这些数据的每一个特性都是用户对该类中餐厅评分的平均权重。接下来,我们将使用这些数据来确定用户的相似性,然后我将使用 聚类 clustering 算法来确定在不同集群中的用户。

在 R 中这很直接:

在这个示例中我们使用 K-均值 k-means 聚类算法,这将使你很容易攫取集群分配。总之,我通过运行聚类算法然后分别得到每一个用户的集群分配。

Bob 和 David 在一个相同的集群中 - 他们在集群二中 - 现在我可以实时查看哪些用户被放在了相同的集群中。

接下来我把集群分配写入 CSV 文件中,然后存入图数据库:

我们只有用户和集群分配,因此 CSV 文件只有两列。 LOAD CSV 是 Cypher 中的内建语法,它允许你从一些其他文件路径或者 URL 调用 CSV ,并给它一个别名。接下来,我们将匹配图数据库中存在的用户,从 CSV 文件中攫取用户列然后合并到集群中。

我们在图表中创建了一个新的标签节点:Cluster ID, 这是由 K-平均聚类算法给出的。接下来我们创建用户和集群间的关系,通过创建这个关系,当我们想要找到在相同集群中的实际推荐用户时,就会很容易进行查询。

我们现在有了一个新的集群标签,在相同集群中的用户和那个集群存在关系。新的数据模型看起来像下面这样,它比我们前面探索的其他数据模型要更好:

现在让我们考虑下面的查询:

通过这个 Cypher 查询,我们在更远处找到了在同一个集群中的相似用户。由于这个原因,我们删除了“距离”关系:

在这个查询中,我们取出已经登录的用户,根据用户-集群关系找到他们所在的集群,找到他们附近和他们在相同集群中的用户。

我们把这些用户分配到变量 c1 中,然后我们得到其他被我取别名为 neighbor 变量的用户,这些用户和那个相同集群存在着用户-集群关系,最后我们得到这些附近用户“喜好”的地点。再次说明,我把“喜好”放入了变量 r 中,因为我们需要从关系中攫取权重来对结果进行排序。

在这个查询中,我们所做的改变是,不使用相似性距离,而是攫取在相同集群中的用户,然后对类别、航站楼以及我们所攫取的登录用户进行声明。我们收集所有的权重:来自附近用户“喜好”地点的“喜好”关系,得到的类别,确定的距离值,然后把它们按升序进行排序并返回结果。

在这些例子中,我们可以进行一个相当复杂的处理并且将其放到图数据库中,然后我们就可以使用实时算法结果-聚类算法和集群分配的结果。

我们更喜欢的工作流程是更新这些集群分配,更新频率适合你自己就可以,比如每晚一次或每小时一次。当然,你可以根据直觉来决定多久更新一次这些集群分配是可接受的。


via: https://neo4j.com/blog/real-time-recommendation-engine-data-science/

作者:Nicole White 译者:ucasFL 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出