尝试使用Apache AGE (1)
首先
喵。
最近在看Twitter的时候,看到了一个有趣的扩展程序叫做Apache AGE,所以我试着用了一下。
相关推文 → https://twitter.com/DataAugmented/status/1520449666222989314
Apache AGE(孵化中)是一个为 PostgreSQL 提供图数据库功能的扩展。
我这次先安装了软件,试着生成了一些简单的图形,并使用OpenCypher进行了简单的搜索。
Apache AGE是什么?
据说,Apache AGE是作为PostgreSQL的扩展功能(SQL函数)提供的,它提供了与图数据库查询语言OpenCypher的接口。OpenCypher是基于Cypher开发的查询语言,在图数据库Neo4j中也使用过一些。尽管目前还处于实验阶段,并且标有(正在孵化)的标记,但如果能够在PostgreSQL上管理图数据库并能够使用SQL进行操作,那么我可以尝试一些有趣的事情。所以我决定立即进行尝试。
由于文件似乎已经齐全,所以只要是简单的操作,即使是像我这样的图数据库初学者,通过查看文件,也能够找到解决的办法。[链接](https://age.apache.org/docs/master/index.html)
安装和设置
获取安装材料
Apache AGE从以下网站获取了源代码集。
https://www.apache.org/dyn/closer.lua/incubator/age/1.0.0/apache-age-1.0.0-incubating-src.tar.gz
目前已支持PostgreSQL 11版本。预计将来还会支持PostgreSQL 12和13版本。这次是在我手头有的PostgreSQL 11.6环境上进行安装。
创建并安装
只需解压源代码,并进入解压后的目录,然后执行make命令。
$ make USE_PGXS=1
编译完成后,执行make install。
这将安装PostgreSQL 11在指定的目录下的库文件(*.so)以及用于CREATE EXTENSION命令的脚本。
$ ls -l lib/age.so
-rwxr-xr-x 1 ec2-user ec2-user 519792 May 3 15:20 lib/age.so
$ $ ls -l share/extension/age*
-rw-r--r-- 1 ec2-user ec2-user 77061 May 3 15:20 share/extension/age--1.0.0.sql
-rw-r--r-- 1 ec2-user ec2-user 900 May 3 15:20 share/extension/age.control
$
将信息录入数据库
要使用Apache AGE,需要通过CREATE EXTENSION命令将其注册到数据库中。
下面是创建一个名为sample的数据库,并将AGE扩展功能注册到其中的示例。
$ createdb sample
$ psql sample -c "CREATE EXTENSION age"
CREATE EXTENSION
$ psql sample -c "\dx"
List of installed extensions
Name | Version | Schema | Description
---------+---------+------------+------------------------------
age | 1.0.0 | ag_catalog | AGE database extension
plpgsql | 1.0 | pg_catalog | PL/pgSQL procedural language
(2 rows)
$
使用dx元命令,如果显示age 1.0.0,则表示已成功注册。
PostgreSQL的配置设置
当使用CREATE EXTENSION命令完成注册后,将在目标数据库中注册一个名为ag_catalog的模式。
$ psql sample -c "\dn"
List of schemas
Name | Owner
------------+----------
ag_catalog | postgres
public | postgres
(2 rows)
$
AGE提供的SQL函数也存储在ag_catalog模式中。
SELECT * FROM ag_catalog.cypher( ... )
如果添加模式修饰符的话,可以使用。但如果觉得添加模式修饰符麻烦的话,可以在postgresql.conf的search_path变量中进行以下设置,并重新启动,以便无需模式修饰符即可使用AGE提供的函数。
search_path = ag_catalog, "$user", public
生成图表 tú
AGE为存储顶点与边的区域的结构称为”图”。因此,为了创建”图”,首先需执行AGE提供的专用SQL函数create_graph()。
create_graph()接受一个字符串参数,该参数指示图的名称,并在以后执行OpenCypher命令的cypher()函数中传递create_graph()指定的图名称是必要的。
换句话说,可以说在一个数据库上可以布置多个图。
以下是 create_graph() 的示例(已经在 age 数据库中注册了 age 扩展)。
$ psql age -c "SELECT create_graph('sample');"
NOTICE: graph "sample" has been created
create_graph
--------------
(1 row)
$
删除图表
如果要删除创建的图表,则需要使用AGE提供的专用SQL函数drop_graph()来执行。
drop_graph()函数接受两个参数。
第一个字符串参数表示图表的名称,指定要删除的图表名称。
第二个布尔参数(级联)是用于确定是否级联删除依赖对象的标志。
以下是drop_graph()的示例执行。
$ psql age -c "SELECT drop_graph('sample',true);"
NOTICE: drop cascades to 2 other objects
DETAIL: drop cascades to table sample._ag_label_vertex
drop cascades to table sample._ag_label_edge
NOTICE: graph "sample" has been dropped
drop_graph
------------
(1 row)
$
另外,目前情况下似乎只能在cascade中指定为true。
如果在create_graph之后尝试删除图表,并在第二个参数中指定为false,或者省略第二个参数,似乎会导致删除失败。下面是指定为false时的行为示例。
$ psql age -c "SELECT drop_graph('sample',false);"
ERROR: cannot drop schema sample because other objects depend on it
DETAIL: table sample._ag_label_vertex depends on schema sample
table sample._ag_label_edge depends on schema sample
HINT: Use DROP ... CASCADE to drop the dependent objects too.
$
图数据库的组成要素
生成图形后,会生成构成图形数据库的顶点和边。
图形数据库基本上由顶点和连接两个顶点的边构成。
顶点或边上可以添加标签或属性值。
密码函数
如果要注册顶点和边并对图进行搜索的话,可以使用AGE提供的SQL函数Cypher。
虽然语法有些特点,但习惯之后并不难。
Cypher函数的基本结构如下所示。
SELECT * FROM cypher(<graph_name>, $$
/* Cypher Query Here */
$$) AS (result agtype [,resule agtype]... );
最初的参数为,在生成图时指定的图名,以字符串形式给出。
第二个参数的开头有一个陌生的描述$$,实际上是指定PostgreSQL的引号。
在PostgreSQL中,可以使用任意引号。
在这个例子中,我们使用$$作为引号,因为在编写PostgreSQL的SQL函数或PL/pgSQL函数时,惯例是使用$$作为引号,但实际上可以指定任意字符(例如$quote$)。
那么为什么要这样指定呢?因为在/* Cypher Query Here */中的Cypher查询中,使用单引号作为引号。如果不使用$$指定引号,那么在Cypher查询中的单引号需要逐个转义,这样就会变得麻烦且不可读。
以下是实际的Cypher函数执行示例的图像。
SELECT * FROM cypher('sample', $$
CREATE p = ( :Person {id: 101, name:'Alice', gender:'Fmale'} )
RETURN p
$$) as (p agtype);
通过Cyper进行注册和搜索的示例
不知道是谁首先说出了喜欢和讨厌这样的话。
这里以示例为基础,展示了有关Vertex、Edge的注册以及注册后对图进行简单搜索的例子。
作为示例,我会举出以Person标签为例的五个Vertex以及以喜欢(Like)/讨厌(Dislike)为标签的两种Edge的例子。
通过图来表示的话,就是这样的样子。
嗯,感觉有些爱恨交织的情况呢。
Vertex的注册
在注册Vertex时,使用Cypher的CREATE命令。
CREATE命令的基本格式是
CREATE 生成したVertex = ( [:ラベル名] [ {属性名:属性値[, 属性名:属性値]... RETURN 生成したVertex
在SQL函数内编写上述内容,并将Cypher函数的返回值描述为`(变量名 类型)`,作为SQL进行执行。尽管这是一个有两个阶段的结构,刚开始可能会感到有些困惑,但我们决定接受并使用它。
以下に示すように、Personラベルを持つVertexの登録例(例えば、Alice=サンの登録例)があります。
-
- ラベルはPerson
-
- 属性として以下の3つをもつ
id属性。値は101
name属性。値は’Alice’
gender属性。値は’Female’
SELECT * FROM cypher('sample', $$
CREATE p = ( :Person {id: 101, name:'Alice', gender:'Female'} )
RETURN p
$$) as (p agtype);
p
---------------------------------------------------------------------------------------------------------------------------
[{"id": 844424930131969, "label": "Person", "properties": {"id": 101, "name": "Alice", "gender": "Fmale"}}::vertex]::path
(1 row)
执行上述的SELECT语句后,会返回一个类似JSON的值。这个值被称为路径,其中包含了生成的顶点的元数据和属性值集合。
既然完成了Alice的注册,那么我们也要同样地注册其他四个人。关于其他四个人的注册示例,可以在本页的“参考信息”部分找到。
Edge的注册
如果要注册一个Edge,最重要的是”在Edge的两端存在一个Vertex”。
简而言之,如果要注册一个Edge,必须先存在Vertex,不存在没有Vertex的Edge。
所以,在这个例子中,首先要注册Vertex,然后选择已存在的Vertex。
在之前的章节中已注册了Vertex,所以要创建连接Vertex的Edge。
创建边缘对象与创建顶点对象一样都使用CREATE语句。不同之处在于在创建时使用MATCH子句指定边缘的两个端点。
以下是一个例子,Alice=サン对Bob=サン生成了一个标记为Like的边。
SELECT * FROM cypher('sample', $$
MATCH (a:Person), (b:Person)
WHERE a.name = 'Alice' AND b.name = 'Bob'
CREATE (a)-[e:Like{level: 'high'}]->(b)
RETURN e
$$) as (p agtype);
在MATCH句中,设置两个标签为Person的标签,并将第一个Person分配给变量a,将下一个Person分配给变量b。然后,在下一个WHERE句中,将a的Person条件设置为名字为”Alice”,将b的Person条件设置为名字为”Bob”。这样就指定了Alice=サン到Bob=サン之间的关系。然后,使用CREATE语句创建所确定的关系,并将其标记为Like(”好き”)。Edge与Vertex一样可以设置任意属性(properties)。在上述示例中,设置了名为level的属性,其值为”high”(”好き”级别高的感觉)。
我们将设置具有“喜欢/不喜欢”标签的边,如图所示。
使用Cypher进行搜索
在使用Cypher进行搜索时,需要像注册时一样使用Cypher函数。
Cypher查询的基本结构是MATCH→WHERE→RETURN。
WHERE条件不是必需的,所以在简单搜索的情况下,可以只使用MATCH→RETURN。
现在,在这里,再次展示所创建的顶点和边的图。
顶点列表
首先,只需尝试搜索此图中存在的所有人物顶点。
SELECT * FROM cypher('sample', $$
MATCH (a:Person) RETURN a
$$ ) as ("Person" agtype);
运行此命令后,将返回以下结果。
Person
--------------------------------------------------------------------------------------------------------------------
{"id": 844424930131969, "label": "Person", "properties": {"id": 101, "name": "Alice", "gender": "Female"}}::vertex
{"id": 844424930131970, "label": "Person", "properties": {"id": 102, "name": "Bob", "gender": "Male"}}::vertex
{"id": 844424930131971, "label": "Person", "properties": {"id": 103, "name": "Casey", "gender": "Female"}}::vertex
{"id": 844424930131972, "label": "Person", "properties": {"id": 104, "name": "Dave", "gender": "Male"}}::vertex
{"id": 844424930131973, "label": "Person", "properties": {"id": 105, "name": "Erin", "gender": "Female"}}::vertex
(5 rows)
只显示特定的属性值
这里同样将所有Vertex作为搜索对象,但仅限定返回信息为姓名和性别。
SELECT * FROM cypher('sample', $$
MATCH (a:Person) RETURN a.name, a.gender
$$ ) as (name text, gender text);
在RETURN子句中,不再使用a全体来指定,而是使用属性列表,如a.name, a.gender。同时,相应地将cypher函数的返回类型更改为(name text, gender text)。
当执行此操作时,仅返回名称和属性作为结果。
name | gender
-------+--------
Alice | Female
Bob | Male
Casey | Female
Dave | Male
Erin | Female
(5 rows)
显示与“讨厌”连接的Vertex的名称。
最后,我来试试类似图形数据库的搜索。
SELECT * FROM cypher('sample', $$
MATCH (a:Person)-[e:Dislike]->(b:Person) RETURN a.name, e.level, b.name
$$ ) as ("from" text, level text, "to" text);
这是一个通过MATCH方法连接“嫌弃”(Dislike)的边缘与人的顶点进行搜索的命令。
返回搜索结果中人的顶点名称以及边缘上设置的级别属性的值。
执行这个操作将会得到以下结果。
from | level | to
-------+-------+-------
Alice | high | Erin
Bob | mid | Dave
Casey | high | Alice
Dave | low | Bob
(4 rows)
這一節的結果與所示的混亂的愛恨圖相符。
最后
本次活動着重介紹了最基礎的內容,包括安裝、基本設定、邊/點的註冊以及簡單的搜索示例。當然,Cypher和AGE的功能不僅限於此,我們將繼續深入研究相關應用功能。
赠品
目前的AGE还有一些不稳定的地方。
目前最困扰的问题是,在会话中首次执行的Cypher查询总是出现一个名为”ERROR: unhandled cypher(cstring) function call” 的神秘错误。而且,如果重新执行相同的查询,则能够正常执行。为什么会这样呢。
$ psql age
psql (11.6)
Type "help" for help.
age=# SELECT * FROM cypher('sample', $$
MATCH (a:Person)-[e:Dislike]->(b:Person) RETURN a.name, e.level, b.name
$$ ) as ("from" text, level text, "to" text);
ERROR: unhandled cypher(cstring) function call
DETAIL: sample
age=# SELECT * FROM cypher('sample', $$
MATCH (a:Person)-[e:Dislike]->(b:Person) RETURN a.name, e.level, b.name
$$ ) as ("from" text, level text, "to" text);
from | level | to
-------+-------+-------
Alice | high | Erin
Bob | mid | Dave
Casey | high | Alice
Dave | low | Bob
(4 rows)
请提供相关信息
用于注册Vertex和Edge的所有查询,在样本中使用。
-- create Person Vertex
SELECT * FROM cypher('sample', $$
CREATE p = ( :Person {id: 101, name:'Alice', gender:'Female'} )
RETURN p
$$) as (p agtype);
SELECT * FROM cypher('sample', $$
CREATE p = ( :Person {id: 102, name:'Bob', gender:'Male'} )
RETURN p
$$) as (p agtype);
SELECT * FROM cypher('sample', $$
CREATE p = ( :Person {id: 103, name:'Casey', gender:'Female'} )
RETURN p
$$) as (p agtype);
SELECT * FROM cypher('sample', $$
CREATE p = ( :Person {id: 104, name:'Dave', gender:'Male'} )
RETURN p
$$) as (p agtype);
SELECT * FROM cypher('sample', $$
CREATE p = ( :Person {id: 105, name:'Erin', gender:'Female'} )
RETURN p
$$) as (p agtype);
-- Create Edge
-- From Alice
SELECT * FROM cypher('sample', $$
MATCH (a:Person), (b:Person)
WHERE a.name = 'Alice' AND b.name = 'Bob'
CREATE (a)-[e:Like{level: 'high'}]->(b)
RETURN e
$$) as (p agtype);
SELECT * FROM cypher('sample', $$
MATCH (a:Person), (b:Person)
WHERE a.name = 'Alice' AND b.name = 'Erin'
CREATE (a)-[e:Dislike{level: 'high'}]->(b)
RETURN e
$$) as (p agtype);
-- From Bob
SELECT * FROM cypher('sample', $$
MATCH (a:Person), (b:Person)
WHERE a.name = 'Bob' AND b.name = 'Erin'
CREATE (a)-[e:Like{level: 'high'}]->(b)
RETURN e
$$) as (p agtype);
SELECT * FROM cypher('sample', $$
MATCH (a:Person), (b:Person)
WHERE a.name = 'Bob' AND b.name = 'Dave'
CREATE (a)-[e:Dislike{level: 'mid'}]->(b)
RETURN e
$$) as (p agtype);
-- From Casey
SELECT * FROM cypher('sample', $$
MATCH (a:Person), (b:Person)
WHERE a.name = 'Casey' AND b.name = 'Dave'
CREATE (a)-[e:Like{level: 'high'}]->(b)
RETURN e
$$) as (p agtype);
SELECT * FROM cypher('sample', $$
MATCH (a:Person), (b:Person)
WHERE a.name = 'Casey' AND b.name = 'Alice'
CREATE (a)-[e:Dislike{level: 'high'}]->(b)
RETURN e
$$) as (p agtype);
-- Dave
SELECT * FROM cypher('sample', $$
MATCH (a:Person), (b:Person)
WHERE a.name = 'Dave' AND b.name = 'Alice'
CREATE (a)-[e:Like{level: 'high'}]->(b)
RETURN e
$$) as (p agtype);
SELECT * FROM cypher('sample', $$
MATCH (a:Person), (b:Person)
WHERE a.name = 'Dave' AND b.name = 'Bob'
CREATE (a)-[e:Dislike{level: 'low'}]->(b)
RETURN e
$$) as (p agtype);
-- From Erin
SELECT * FROM cypher('sample', $$
MATCH (a:Person), (b:Person)
WHERE a.name = 'Erin' AND b.name = 'Dave'
CREATE (a)-[e:Like{level: 'mid'}]->(b)
RETURN e
$$) as (p agtype);
SELECT * FROM cypher('sample', $$
MATCH (a:Person), (b:Person)
WHERE a.name = 'Erin' AND b.name = 'Alice'
CREATE (a)-[e:Like{level: 'mid'}]->(b)
RETURN e
$$) as (p agtype);