尝试使用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.
$

图数据库的组成要素

生成图形后,会生成构成图形数据库的顶点和边。
图形数据库基本上由顶点和连接两个顶点的边构成。

figure.png

顶点或边上可以添加标签或属性值。

密码函数

如果要注册顶点和边并对图进行搜索的话,可以使用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的例子。
通过图来表示的话,就是这样的样子。

persons-graph.png

嗯,感觉有些爱恨交织的情况呢。

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。

现在,在这里,再次展示所创建的顶点和边的图。

persons-graph.png

顶点列表

首先,只需尝试搜索此图中存在的所有人物顶点。

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);
广告
将在 10 秒后关闭
bannerAds