cayley图数据库

郭柱明


图数据库背景

在此之前已经简单记录了一下图数据的背景和发展现状 图数据引擎那部分现在还是知之甚少 还没深入去了解
图数据库背景


cayley优点

  • go语言实现
  • 运行简单(三四条命令)
  • 支持http接口以及REPL交互式
  • 支持多种语言进行查询
    • Gizmo
    • MQL
    • GraphQL
  • 支持多种后端存储
    • KVs: Bolt, LevelDB
    • NoSQL: MongoDB, ElasticSearch, CouchDB/PouchDB
    • SQL: PostgreSQL, CockroachDB, MySQL
    • n-memory, ephemeral
  • 模块化编程 容易拓展
  • 良好的测试覆盖
  • 速度快
  • 免费

运行和应用

这里真的要非常注意 因为cayley提供的官方文档相当有限 会导致很多新手搞不清到底怎么使用这个图数据

cayley使用两种方式

  • 作为应用运行使用
  • 作为第三方库运行使用

作为应用运行使用

下面是官方文档中作为应用进行运行使用的部分
参考链接

  1. 下载二进制文件解压或者下载源码进行编译
  2. 配置
  3. 导入数据
  4. 运行

运行使用一下命令

./cayley http --config=cayley.yml --host=0.0.0.0:64210

或者在Linux系统下后台运行

./cayley http --config=cayley.yml --host=0.0.0.0:64210 &

可以看到已经在后台运行 监听64210端口
image_1ci3f3d0g1hg21bt411nh4f04r09.png-26.4kB

上面这种运行方式是使用一个go原生net/http架起一个web服务器 以方便开放人员作为管理员可以在web界面上使用UI进行数据库的增删查找 如下
image_1ci24pfveosv1hudifc10ivmcl9.png-149.3kB

但是 对于这个http接口 官方并没有提供进一步的封装 意味着如果你要使用这种方式运行cayley 并且在你的项目中连接这个数据库 那么你是需要自己拼凑我们用来增删查找的Gizmo语句(如果选择使用Gizmo) 类似以下

1
2
3
g.V().Has("<name>","Casablanca")
.Out("</film/film/starring>").Out("</film/performance/actor>")
.Out("<name>").All()

然后post给服务端上的cayley

这就意味着你需要进一步封装这个http接口 就跟写一个爬虫一样 虽然在Python上已经有人做好了这部分工作
pyley一个封装了cayley http接口的客户端

但是go语言我目前还没发现有类似的 官方亦然


作为第三方库运行使用

我个人推荐将cayley作为第三方库来进行运行 一个是go get安装cayley第三方库的时候就顺带安装了cayley 因为cayley本身就是go编写的 但是这方面的使用官方文档上简直坑得不行 仅仅提供了几个hello world 对刚接触的开发者来说真的非常不友好

官方提供的几个example

hello_worldtransaction都是使用

cayley.NewMemoryGraph()

这个api使用内存作为临时的存储位置进行存储的

hello_bolthello_schema都是使用一个叫bolt的键值对数据库进行存储的 这个需要一个路径来存存储着数据的文件

除此之外 官方文档就没提供太多 实际在go语言中链接cayley的方式了

但是我们小组的领导决定使用mongodb作为backend进行存储 上面的例子都没有太大的实际应用意义

我参照了bolt作为backend的例子修改了一下 获得了使用mongodb作为backend的方法

注释已经将每一部分讲的很清楚了 所以就不说其他什么了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
package main

import (
"context"
"fmt"
"log"
"github.com/cayleygraph/cayley"
"github.com/cayleygraph/cayley/graph"
//下面这个import必不可少 因为在这个import这个库的时候 它的init函数会注册这个类型的数据库 允许使用这个数据库进行存储
_ "github.com/cayleygraph/cayley/graph/nosql/mongo"
"github.com/cayleygraph/cayley/quad"
)

func main() {

// 初始化数据库
err := graph.InitQuadStore("mongo", "mongodb://120.92.100.60:27017", nil)
if err != nil {
log.Fatal(err)
}

//打开和使用数据库
store, err := cayley.NewGraph("mongo", "mongodb://120.92.100.60:27017", nil)
if err != nil {
log.Fatalln(err)
}

//给数据库添加一个quad实例 phrase of the day is of course Hello BoltDB! demo graph
//主语 phrase of the day
//谓语 is of course
//宾语 Hello BoltDB!
//标签 demo graph
store.AddQuad(quad.Make("phrase of the day", "is of course", "Hello BoltDB!", "demo graph"))

// 创建一个用于查找的path
p := cayley.StartPath(store, quad.String("phrase of the day")).Out(quad.String("is of course"))

//下面的查询是跟hello world里面是差不多的 但是更先进一点
//获取这个path的迭代器 并且优化它
it, _ := p.BuildIterator().Optimize()

//在quad层面进行优化 之后这个迭代器就被指定了backend的迭代器取代了
it, _ = store.OptimizeIterator(it)

// 关闭迭代器
defer it.Close()

ctx := context.TODO()
// 当path上还有下一个
for it.Next(ctx) {
token := it.Result() // 获取节点的一个引用
value := store.NameOf(token) // 获取节点的值
nativeValue := quad.NativeOf(value) // 将上面获取的节点的值转化为正常的数
fmt.Println(nativeValue)
}
if err := it.Err(); err != nil {
log.Fatalln(err)
}
}

输出
image_1ci3gj9n4178dooh1a7g166fsbbm.png-24.1kB


事务

  1. 初始化数据库获取一个handle
  2. 获取一个事务
  3. 添加多个四元组到事务中
  4. 将事务添加到handle中
  5. 查询跟之前没什么区别

image_1ci8s50oq9jqrrt82c1hqk19nq19.png-107.8kB


数据结构与api

1.添加四元组的方式
image_1ci99nqqkm61l077bd12fj1ng1m.png-113.8kB

2.更新或者删除四元组
cayley的设计中是没有更新四元组这个api 如果想要更新一个四元组只能先删除这个四元组 然后再重新添加一条新的四元组

image_1ci9habhi1iob1b5094s14k518t52j.png-105kB


数据集

image_1ci3jdcegdal17q8tga180t8at13.png-135.6kB

cayley api

四元组 含有主谓宾标签

1
2
3
func Quad(subject, predicate, object, label interface{}) quad.Quad {
return quad.Make(subject, predicate, object, label)
}

三元组 含有主谓宾 没有标签(标签为nil)

1
2
3
func Triple(subject, predicate, object interface{}) quad.Quad {
return Quad(subject, predicate, object, nil)
}

处理器handle

1
type Handle = graph.Handle

一个handle的结构是

1
2
3
4
type Handle struct {
QuadStore
QuadWriter
}

包含一个匿名的四元组存储成员 一个匿名的四元组写入成员

新建一个处理器

1
func NewGraph(name, dbpath string, opts graph.Options) (*Handle, error)

返回一个handle

新建一个内存存储的处理器

1
func NewMemoryGraph() (*Handle, error)

从它的具体实现 可以知道这基于memstore内存进行存储的处理器

1
2
3
func NewMemoryGraph() (*Handle, error) {
return NewGraph("memstore", "", nil)
}

迭代器

1
type Iterator = graph.Iterator

query path

1
type Path = path.Path


Gizmo

因为我们上面都是使用Gizmo作为查询语言进行查询的 所以很有必要记录一下Gizmo的基本语法

基本语句太多了 所以只简要记录一下常用的语句

1.
graph.M()

graph.Morphism()

的缩写 Morphism创建一条路径 这样允许Gizmo的路径可以被复用
类似如下

var shorterPath = graph.Morphism().Out("foo").Out("bar")

2.

graph.V(*)

graph.Vertex([nodeId],[nodeId]...)

的缩写 这个语句返回一个根据给定的点作为始点的query path 没有nodeID的话以为着所有的点


3.
Path object .Morphism()和.Vertex()和这个函数都可以创建一个query path对象


4.

path.And(path)

path.Intersect(path)

的缩写 就是逻辑与的意思
例子

1
g.V("<charlie>").Out("<follows>").And(g.V("<dani>").Out("<follows>")).All()


5.

path.As(tags)

path.Tag(tags)

的缩写
Tag保存一个list的nodes存到给定的tag 主要是为了保存我们的工作或者为了了解一个path是怎样到达终点的 我们需要这个tag来进行标志一下


6.

path.Back(tag)

跟tag来后退到tag上面去


7.

path.Both([predicatePath], [tags])

获取同时进来和出去的节点
例子

1
g.V("<fred>").Both("<follows>").All()


8.

path.Count()

返回结果的数量


9.

path.Difference(path)

path.Except(path)

的别名
Except移除当前path上匹配了这个query path删的所有paths


10.

path.Follow(path)

是一种使用.Morphism()准备好的query path的方式


11.

path.ForEach(callback) or (limit, callback)

遍历query path


12.

path.Has(predicate, object)

一般应用于所有节点或者一个堆节点按照某个谓语进行查询 例子

1
g.V().Has("<follows>", "<bob>").All()


13.

path.In([predicatePath], [tags])

in就比较直观了 就是按照这个谓语进来到这个节点

path.Out([predicatePath], [tags])

类似 就是按照这个谓语出来这个节点


14.

path.Or(path)

path.Union(path)

别名
这个也比较直观 就是逻辑或


15.

path.Save(predicate, tag)

保存所有使用这个谓语进入这个tag的节点 并且不遍历