GraphDB - Apache AGE 1.5.0
Getting Started with Postgres AGE GraphDB
Graph Databases are very interesting technologies for managing social networks, recommendation engines, etc.
In this article, I want to play with the new Postgres Apache AGE GraphDB since I primarily Postgres already and using Neo4j can be quite expensive for real-world applications.
Unfortunately, Apache AGE is not directly supported the Ruby world. This article plays with integrating AGE into rails using the PG gem.
(Note: PostgreSQL versions 11, 12, 13, 14, 15 & 16 are supported.)
Getting Started - Install Apache Age
Compile the Apache AGE extension
Compile and install the AGE extension into Postgres if you are already running postgres on your computer.
- Download from: https://age.apache.org/download/ or clone using:
git clone https://github.com/apache/age.git
- Go into the source code directory of Apache AGE, something like
cd age
if using the Git Repo - Run
make install
If the path to your Postgres installation is not in the PATH variable, then add the path using:make PG_CONFIG=/path/to/postgres/bin/pg_config install
Postgres Apache AGE
If you are not already running Postgres on you computer then using a docker image is probably the easiest way forward:
- Get the docker image with:
docker pull apache/age
- Create AGE docker container with:
docker run \
--name age \
-p 5455:5432 \
-e POSTGRES_USER=pgUser \
-e POSTGRES_PASSWORD=pgPassword \
-e POSTGRES_DB=ageDB \
-d \
apache/age
- now you can enter postgresql using:
docker exec -it age psql -d ageDB -U ageUser
although I tend to use:
export PGPASSWORD=agePassword
psql -d ageDB -U ageUser -h localhost -p 5455
Post Install
No matter how you installed Apache Age, you now need to do the following withing postgres:
CREATE EXTENSION IF NOT EXISTS age;
LOAD 'age';
SET search_path = ag_catalog, "$user", public;
Exploring Cypher SQL
Cypher SQL is be the basis for interacting with Apache AGE - so let’s be sure the basics work within a psql session:
First we need to make an Apache AGE schema for us to use (which is cool, because that means we can within one app use both Apache AGE and normal SQL records)
Create a Schema
To create a graph, use the create_graph function located in the ag_catalog namespace using - you can use whatever name you want here I am using age_schema
:
SELECT create_graph('age_schema');
now you can view your new schema with \dn
ageDB=# \dn
List of schemas
Name | Owner
------------+-------------------
ag_catalog | pgUser
age_schema | postgresUser
public | pg_database_owner
Basic Cypher Syntax
The AGE cypher syntax is: cypher(graph_name, query_string, parameters)
graph_name
is the graph for the Cypher query (in Postgres it is aschema
).query_string
is the Cypher query the graph equivalent of SQL.parameters
OPTIONAL - (default is NULL) map of parameters for Prepared Statements - I have no need for this (so far), so will ignore it.
NOTES:
- Every Cypher query starts with the SQL
SELECT * FROM
- The SQL that ends with
AS (result1 agtype)
- which defines the (columns) returned. This is important to include when the cypher query includes aRETURN
Thus all the queries in this article will have the format of something like:
SELECT * FROM cypher('age_schema', $$
CREATE (f:Person {first_name: "Fred", last_name: "Flintstone", given_name: "Flintstone", gender: "male"})
return f
$$) as (person agtype);
or
SELECT * FROM cypher('age_schema', $$
CREATE (f:Person {first_name: "Fred", last_name: "Flintstone", given_name: "Flintstone", gender: "male"})
$$);
I generally like to return something, so will almost exclusively use the above format.
QUICK GraphDB
A GraphDB has two main parts:
- Nodes - (a vertex) a person, place, thing, etc. (usually noun is used)
- Edges - (a relationship) connects Nodes with a relationship (usually a verb or connection type -
purchased
,likes
,married_to
,child_of
,works_at
) - EDGES CONTAIN A DIRECTION (in some DBs they can be bi-directional) - Paths a combination of Nodes, Edges and their relationship directions / pattern (patterns are mostly show via examples)
All Graph Components can have:
- id - unique identifier
- label - (type) the node or edge type, i.e.
:person
,:works_at
, etc - properties - a hash of attributes, i.e.
{ name: 'Bill', role: 'author' }
,{ role: 'employee' }
QUICK Cypher
- Nodes are found within
()
- Edges are found within
[]
(in AGE Edges are uni-directional, a bi-directional relationship thus requires to edges - one in each direction) - Labels start with
:
- Paths are a combination of Nodes, Edges and their relationship directions / pattern
BASIC AGE Cypher KEYWORDS are:
- MATCH - describes types and relationship patterns to use. A very basic match would be:
(:Person)
match all Person Nodes or(:Likes)
match all Likes Edges(boss:Person { role: 'manager' })-[:WorkFor]->(firm:Company { industry: 'technology'})
this matches all People who work_for technology Companies - MERGE - ensures that a pattern exists in the graph. It either matches existing nodes, or creates new data. It’s a combination of
MATCH
andCREATE
. - WHERE - similar to an SQL WHERE
- CREATE - creates a new edge or node
- SET - used to update labels and properties on vertices and edges
- REMOVE - removes properties from nodes and edges.
- ORDER BY - like in SQL determine the sort order to be returned
- SKIP - SKIP defines from which record to start including the records in the output, ie
SKIP 3
will return starting with the 4th result of the query. - LIMIT - like SQL limit
- WITH - Using WITH, you can manipulate the output before it is passed on to the next query part.
- DELETE (generally use
DETACH DELETE
which will automatically delete edges along with a node - otherwise, explicitly delete associated edges, then a node) - RETURN - describe what to return (similar to a SQL SELECT)
ALIASES: in cypher there is no AS
like in SQL, there are 2 ways for alias/ naming:
- within a MATCH the name alias is made directly BEFORE the Label, ie:
(boss:Person { role: 'manager' })-[r:WorkFor]->(firm:Company { industry: 'technology'})
boss
is an alias of all managers,r
is an alias of WorkFor edges andfirm
is an alias of tech companies - with a CREATE we use
=
, ie:CREATE bill = (:Person {name: 'Bill', role: 'employee'})
allowsbill
to be the alias of the created Node.
Now with this Lightning Intro - on to examples.
This article uses the Flintstone Characters, their workplaces and the associated relationships.
Create a Node (Vertex)
To create a single vertex (node) with label and properties, use the CREATE clause. Let’s enter Fred Flintstone
. Given our schema age_schema
the CREATE
command in psql
looks like:
SELECT * FROM cypher('age_schema', $$
CREATE (f:Person {first_name: "Fred", last_name: "Flintstone", given_name: "Flintstone", gender: "male"})
return f
$$) as (person agtype);
person
---------------------------------------------
{"id": 844424930131969, "label": "Person", "properties": {"gender": "male", "last_name": "Flintstone", "first_name": "Fred", "given_name": "Flintstone"}}::vertex
(1 row)
This creates a type vertex
(node) with a person
label and the properties (attributes) of { first_name: "Fred", last_name: "Flintstone", given_name: "Flintstone", gender: "male" }
to verify this lets find our record using a MATCH
instead of CREATE
command:
SELECT *
FROM cypher('age_schema', $$
MATCH (p:Person)
WHERE p.first_name = "Fred" AND p.last_name = "Flintstone"
RETURN p
$$) as (person agtype);
person
-----------------------------------------------
{"id": 844424930131969, "label": "Person", "properties": {"gender": "male", "last_name": "Flintstone", "first_name": "Fred", "given_name": "Flintstone"}}::vertex
(1 row)
To match by ID we need a slightly different query we need to drop the prefix p
which contains the properties so now the query looks like:
SELECT *
FROM cypher('age_schema', $$
MATCH (p:Person)
WHERE id(p) = 844424930131969
RETURN p
$$) as (person agtype);
If we want the persons first and last name we can change the return (like adding a select):
SELECT *
FROM cypher('age_schema', $$
MATCH (p:Person)
WHERE id(p) = 844424930131969
RETURN p.first_name, p.last_name
$$) as (FirstName agtype, LastName agtype);
firstname | lastname
-----------+--------------
"Fred" | "Flintstone"
(1 row)
Now let’s enter Wilma Flintstone with:
SELECT *
FROM cypher('age_schema', $$
CREATE (w:Person {first_name: "Wilma", last_name: "Flintstone", given_name: "Slaghoople", gender: "female"})
return w
$$) as (person agtype);
person
--------------------------------------------------
{"id": 844424930131970, "label": "Person", "properties": {"gender": "female", "last_name": "Flintstone", "first_name": "Wilma", "given_name": "Slaghoople"}}::vertex
(1 row)
Create an Relationship (Edge)
Now that we have two vertex we can connect them with an edge
with a married_to
label (usually a verb likes
, or married_to
, works_at
, etc) we can do that with the command and the family_name
as the edge property:
SELECT *
FROM cypher('age_schema', $$
MATCH (f:Person), (w:Person)
WHERE f.first_name = 'Fred' AND w.first_name = 'Wilma'
CREATE
(f)-[e:MarriedTo {role: "husband", family_name:f.given_name + '-' + w.given_name}]->(w)
RETURN e
$$) as (married_to agtype);
married_to
-----------------------------------------------
{"id": 1125899906842625, "label": "MarriedTo", "end_id": 844424930131970, "start_id": 844424930131969, "properties": {"role": "husband", "family_name": "Flintstone-Slaghoople"}}::edge
(1 row)
Note this time it returned a type edge
and also not that the family_name
operation worked and resulted in: Flintstone-Slaghoople
! There are lots of other operators documented at:
Note: we can use an operation to use known properties to create a new property: family_name:f.given_name + '-' + w.given_name
Since AGE Cypher only allows uni-directional relationships, we can also have Wilma married to fred with:
SELECT *
FROM cypher('age_schema', $$
MATCH (f:Person), (w:Person)
WHERE f.first_name = 'Fred' AND w.first_name = 'Wilma'
CREATE
path = (w)-[edge:MarriedTo {role: "wife", family_name:f.given_name + '-' + w.given_name}]->(f)
RETURN edge, path
$$) as (Relationship agtype, Path agtype);
relationship | path
--------------------------------------------------------
{"id": 1125899906842626, "label": "MarriedTo", "end_id": 844424930131969, "start_id": 844424930131970, "properties": {"role": "wife", "family_name": "Flintstone-Slaghoople"}}::edge | [{"id": 844424930131970, "label": "Person", "properties": {"gender": "female", "last_name": "Flintstone", "first_name": "Wilma", "given_name": "Slaghoople"}}::vertex, {"id": 1125899906842626, "label": "MarriedTo", "end_id": 844424930131969, "start_id": 844424930131970, "properties": {"role": "wife", "family_name": "Flintstone-Slaghoople"}}::edge, {"id": 844424930131969, "label": "Person", "properties": {"gender": "male", "last_name": "Flintstone", "first_name": "Fred", "given_name": "Flintstone"}}::vertex]::path
(1 row)
Note: in this case I asked it to return both the edge and the full path we created. (sorry, its hard to read, but wanted to demonstrate they type path
exists too - which contains both vertices
and edges
)
Create a Path (Nodes and Edges together)
Let’s make a full path - with this we can create edges and nodes if they don’t already exist. For example we have Fred, but not Barney nor their workplace.
First we find Fred, then we create the Company and Barney and their relationships to each other – all in one go. The cypher command looks like:
SELECT *
FROM cypher('age_schema', $$
MATCH (f:Person)
WHERE f.first_name = 'Fred'
CREATE p = (f)-[:WorksAt {role: "Crane Operator"}]->(:Company {name: "Bedrock Quarry"})<-[:WorksAt]-(:Person {first_name: 'Barney', last_name: "Rubble", given_name: "Rubble", gender: "male"})
RETURN p
$$) as (path agtype);
path
----------------------------------------------------------
[{"id": 844424930131969, "label": "Person", "properties": {"gender": "male", "last_name": "Flintstone", "first_name": "Fred", "given_name": "Flintstone"}}::vertex, {"id": 1407374883553282, "label": "WorksAt", "end_id": 1688849860263937, "start_id": 844424930131969, "properties": {"role": "Crane Operator"}}::edge, {"id": 1688849860263937, "label": "company", "properties": {"name": "Bedrock Quarry"}}::vertex, {"id": 1407374883553281, "label": "WorksAt", "end_id": 1688849860263937, "start_id": 844424930131971, "properties": {}}::edge, {"id": 844424930131971, "label": "Person", "properties": {"gender": "male", "last_name": "Rubble", "first_name": "Barney", "given_name": "Rubble"}}::vertex]::path
(1 row)
Note: we returned the full path: Fred his role at work, barney and his role at work. Pretty cool when only Fred existed before this command. (You can also do this without a match - where all aspects are new!)
Multiple Creates in one Command
Let’s create Betty Rubble and her marriage with Barney. first_name: ‘Betty’, last_name: ‘Rubble’, given_name: ‘McBricker’, gender: ‘female’.
The trick to multiple creates in one command is to separate them with a comma. So the command looks like:
SELECT *
FROM cypher('age_schema', $$
MATCH (ba:Person)
WHERE ba.first_name = 'Barney'
CREATE p1 = (ba)-[:MarriedTo {role: "husband"}]->(be:Person {first_name: 'Betty', last_name: 'Rubble', given_name: 'McBricker', gender: 'female'}),
p2 = (be)-[:MarriedTo {role: 'wife'}]->(ba)
RETURN p1, p2
$$) as (path1 agtype, path2 agtype);
path1 | path2
----------------------------------------------------
[{"id": 844424930131971, "label": "Person", "properties": {"gender": "male", "last_name": "Rubble", "first_name": "Barney", "given_name": "Rubble"}}::vertex, {"id": 1125899906842627, "label": "MarriedTo", "end_id": 844424930131972, "start_id": 844424930131971, "properties": {"role": "husband"}}::edge, {"id": 844424930131972, "label": "Person", "properties": {"gender": "female", "last_name": "Rubble", "first_name": "Betty", "given_name": "McBricker"}}::vertex]::path | [{"id": 844424930131972, "label": "Person", "properties": {"gender": "female", "last_name": "Rubble", "first_name": "Betty", "given_name": "McBricker"}}::vertex, {"id": 1125899906842628, "label": "MarriedTo", "end_id": 844424930131971, "start_id": 844424930131972, "properties": {"role": "wife"}}::edge, {"id": 844424930131971, "label": "Person", "properties": {"gender": "male", "last_name": "Rubble", "first_name": "Barney", "given_name": "Rubble"}}::vertex]::path
(1 row)
Alternatively, and probably more efficiently we could have created all paths together using:
SELECT *
FROM cypher('age_schema', $$
MATCH (barney:Person)
WHERE barney.first_name = 'Barney'
CREATE path = (barney)-[:MarriedTo {role: "husband"}]->(betty:Person {first_name: 'Betty', last_name: 'Rubble', given_name: 'McBricker', gender: 'female'})-[:MarriedTo {role: 'wife'}]->(barney)
RETURN path
$$) as (path agtype);
I just wanted demonstrate two creates in one command.
Let’s also add the work place of Wilma and Betty:
SELECT *
FROM cypher('age_schema', $$
MATCH (b:Person), (w:Person)
WHERE b.first_name = 'Betty' AND w.first_name = "Wilma"
CREATE p = (b)-[:WorksAt {role: "Reporter"}]->(:Company {name: "Bedrock News"})<-[:WorksAt {role: "News Anchor"}]-(w)
RETURN p
$$) as (path agtype);
path
-----------------------------------------------------
[{"id": 844424930131972, "label": "Person", "properties": {"gender": "female", "last_name": "Rubble", "first_name": "Betty", "given_name": "McBricker"}}::vertex, {"id": 1407374883553284, "label": "WorksAt", "end_id": 1688849860263938, "start_id": 844424930131972, "properties": {"role": "Reporter"}}::edge, {"id": 1688849860263938, "label": "company", "properties": {"name": "Bedrock News"}}::vertex, {"id": 1407374883553283, "label": "WorksAt", "end_id": 1688849860263938, "start_id": 844424930131970, "properties": {"role": "News Anchor"}}::edge, {"id": 844424930131970, "label": "Person", "properties": {"gender": "female", "last_name": "Flintstone", "first_name": "Wilma", "given_name": "Slaghoople"}}::vertex]::path
(1 row)
Query Relations
We will start with a very generic query to find any nodes connected with a relationship:
SELECT * from cypher('age_schema', $$
MATCH (N)-[R]-(N2)
RETURN N,R,N2
$$) as (StartNode agtype, Relationship agtype, EndNode agtype);
startnode | relationship | endnode
---------------------------------------------------
{"id": 844424930131971, "label": "Person", "properties": {"gender": "male", "last_name": "Rubble", "first_name": "Barney", "given_name": "Rubble"}}::vertex | {"id": 1407374883553281, "label": "WorksAt", "end_id": 1688849860263937, "start_id": 844424930131971, "properties": {}}::edge | {"id": 1688849860263937, "label": "company", "properties": {"name": "Bedrock Quarry"}}::vertex
...
you can see we can return 3 columns and describe these columns in as
statement.
Now you can see we have a big jumble multiple types of relationships and entities returned in one query. If we want to return just marriage relations we can write:
SELECT * from cypher('age_schema', $$
MATCH p = (c1:Person)-[r:MarriedTo]-(c2:Person)
RETURN p
$$) as (Marriages agtype);
marriages
--------------------------------------------------------
[{"id": 844424930131969, "label": "Person", "properties": {"gender": "male", "last_name": "Flintstone
", "first_name": "Fred", "given_name": "Flintstone"}}::vertex, {"id": 1125899906842625, "label": "Marr
iedTo", "end_id": 844424930131970, "start_id": 844424930131969, "properties": {"role": "husband", "fam
ily_name": "Flintstone-Slaghoople"}}::edge, {"id": 844424930131970, "label": "Person", "properties": {
"gender": "female", "last_name": "Flintstone", "first_name": "Wilma", "given_name": "Slaghoople"}}::ve
rtex]::path
[{"id": 844424930131969, "label": "Person", "properties": {"gender": "male", "last_name": "Flintstone", "first_name": "Fred", "given_name": "Flintstone"}}::vertex, {"id": 1125899906842626, "label": "MarriedTo", "end_id": 844424930131969, "start_id": 844424930131970, "properties": {"role": "wife", "family_name": "Flintstone-Slaghoople"}}::edge, {"id": 844424930131970, "label": "Person", "properties": {"gender": "female", "last_name": "Flintstone", "first_name": "Wilma", "given_name": "Slaghoople"}}::vertex]::path
...
And if we just want to see who works where:
SELECT * from cypher('age_schema', $$
MATCH p = (c1:Person)-[r:WorksAt]-(c2:Company)
RETURN p
$$) as (Employees agtype);
employees
----------------------------------------------------------
[{"id": 844424930131969, "label": "Person", "properties": {"gender": "male", "last_name": "Flintstone", "first_name": "Fred", "given_name": "Flintstone"}}::vertex, {"id": 1407374883553282, "label": "WorksAt", "end_id": 1688849860263937, "start_id": 844424930131969, "properties": {"role": "Crane Operator"}}::edge, {"id": 1688849860263937, "label": "company", "properties": {"name": "Bedrock Quarry"}}::vertex]::path
[{"id": 844424930131970, "label": "Person", "properties": {"gender": "female", "last_name": "Flintstone", "first_name": "Wilma", "given_name": "Slaghoople"}}::vertex, {"id": 1407374883553283, "label": "WorksAt", "end_id": 1688849860263938, "start_id": 844424930131970, "properties": {"role": "News Anchor"}}::edge, {"id": 1688849860263938, "label": "company", "properties": {"name": "Bedrock News"}}::vertex]::path
...
(4 rows)
Deep Relationship Paths
Let’s create a few more family relationships so that we can build a family tree a few levels deep. We will build the children and grand-children from the Flintstones
and Rubbles
Let’s start with Bamm-Bamm
SELECT *
FROM cypher('age_schema', $$
MATCH (father:Person), (mother:Person)
WHERE father.first_name = 'Barney' and mother.first_name = "Betty"
CREATE
parenthood = (father)-[:ParentOf {role: "Father"}]->(child:Person {first_name: "Bamm-Bamm", last_name: "Rubble", given_name: "Rubble", gender: "male"})<-[:ParentOf {role: "Mother"}]-(mother),
childhood = (father)<-[:ChildOf {role: "Son"}]-(child)-[:ChildOf {role: "Son"}]->(mother)
RETURN parenthood, childhood
$$) as (Parenthood agtype, Childhood agtype);
parenthood | childhood
----------------------------------------------------
[{"id": 844424930131971, "label": "Person", "properties": {"gender": "male", "last_name": "Rubble", "first_name": "Barney", "given_name": "Rubble"}}::vertex, {"id": 1970324836974594, "label": "parent_of", "end_id": 844424930131973, "start_id": 844424930131971, "properties": {"role": "Father"}}::edge, {"id": 844424930131973, "label": "Person", "properties": {"gender": "male", "last_name": "Rubble", "first_name": "Bamm-Bamm", "given_name": "Rubble"}}::vertex, {"id": 1970324836974593, "label": "parent_of", "end_id": 844424930131973, "start_id": 844424930131972, "properties": {"role": "Mother"}}::edge, {"id": 844424930131972, "label": "Person", "properties": {"gender": "female", "last_name": "Rubble", "first_name": "Betty", "given_name": "McBricker"}}::vertex]::path | [{"id": 844424930131971, "label": "Person", "properties": {"gender": "male", "last_name": "Rubble", "first_name": "Barney", "given_name": "Rubble"}}::vertex, {"id": 2251799813685250, "label": "child_of", "end_id": 844424930131971, "start_id": 844424930131973, "properties": {"role": "Son"}}::edge, {"id": 844424930131973, "label": "Person", "properties": {"gender": "male", "last_name": "Rubble", "first_name": "Bamm-Bamm", "given_name": "Rubble"}}::vertex, {"id": 2251799813685249, "label": "child_of", "end_id": 844424930131972, "start_id": 844424930131973, "properties": {"role": "Son"}}::edge, {"id": 844424930131972, "label": "Person", "properties": {"gender": "female", "last_name": "Rubble", "first_name": "Betty", "given_name": "McBricker"}}::vertex]::path
(1 row)
And now Pebbles
SELECT *
FROM cypher('age_schema', $$
MATCH (f:Person), (m:Person)
WHERE f.first_name = 'Fred' and m.first_name = "Wilma"
CREATE
pp = (f)-[:ParentOf {role: "Father"}]->(c:Person {first_name: "Pebbles", last_name: "Rubble", given_name: "Rubble", gender: "male"})<-[:ParentOf {role: "Mother"}]-(m),
cp = (f)<-[:ChildOf {role: "Daughter"}]-(c)-[:ChildOf {role: "Daughter"}]->(m)
RETURN pp, cp
$$) as (ParentalPaths agtype, ChildPaths agtype);
parenthood | childhood
----------------------------------------------------
[{"id": 844424930131969, "label": "Person", "properties": {"gender": "male", "last_name": "Flintstone", "first_name": "Fred", "given_name": "Flintstone"}}::vertex, {"id": 1970324836974596, "label": "parent_of", "end_id": 844424930131974, "start_id": 844424930131969, "properties": {"role": "Father"}}::edge, {"id": 844424930131974, "label": "Person", "properties": {"gender": "male", "last_name": "Rubble", "first_name": "Pebbles", "given_name": "Rubble"}}::vertex, {"id": 1970324836974595, "label": "parent_of", "end_id": 844424930131974, "start_id": 844424930131970, "properties": {"role": "Mother"}}::edge, {"id": 844424930131970, "label": "Person", "properties": {"gender": "female", "last_name": "Flintstone", "first_name": "Wilma", "given_name": "Slaghoople"}}::vertex]::path | [{"id": 844424930131969, "label": "Person", "properties": {"gender": "male", "last_name": "Flintstone", "first_name": "Fred", "given_name": "Flintstone"}}::vertex, {"id": 2251799813685252, "label": "child_of", "end_id": 844424930131969, "start_id": 844424930131974, "properties": {"role": "Daughter"}}::edge, {"id": 844424930131974, "label": "Person", "properties": {"gender": "male", "last_name": "Rubble", "first_name": "Pebbles", "given_name": "Rubble"}}::vertex, {"id": 2251799813685251, "label": "child_of", "end_id": 844424930131970, "start_id": 844424930131974, "properties": {"role": "Daughter"}}::edge, {"id": 844424930131970, "label": "Person", "properties": {"gender": "female", "last_name": "Flintstone", "first_name": "Wilma", "given_name": "Slaghoople"}}::vertex]::path
(1 row)
son of bamm-bamm & pebbles chip = Character.create!(species: human, first_name: ‘Charleston Frederick’, nick_name: ‘Chip’, last_name: ‘Rubble’, gender: ‘male’)
SELECT *
FROM cypher('age_schema', $$
MATCH (f:Person), (m:Person)
WHERE f.first_name = 'Bamm-Bamm' and m.first_name = "Pebbles"
CREATE
pp = (f)-[:ParentOf {role: "Father"}]->(c:Person {first_name: "Chip", last_name: "Rubble", given_name: "Rubble", gender: "male"})<-[:ParentOf {role: "Mother"}]-(m),
cp = (f)<-[:ChildOf {role: "Son"}]-(c)-[:ChildOf {role: "Son"}]->(m)
RETURN pp, cp
$$) as (ParentalPaths agtype, ChildPaths agtype);
parenthood | childhood
----------------------------------------------------
[{"id": 844424930131973, "label": "Person", "properties": {"gender": "male", "last_name": "Rubble", "first_name": "Bamm-Bamm", "given_name": "Rubble"}}::vertex, {"id": 1970324836974598, "label": "parent_of", "end_id": 844424930131975, "start_id": 844424930131973, "properties": {"role": "Father"}}::edge, {"id": 844424930131975, "label": "Person", "properties": {"gender": "male", "last_name": "Rubble", "first_name": "Chip", "given_name": "Rubble"}}::vertex, {"id": 1970324836974597, "label": "parent_of", "end_id": 844424930131975, "start_id": 844424930131974, "properties": {"role": "Mother"}}::edge, {"id": 844424930131974, "label": "Person", "properties": {"gender": "male", "last_name": "Rubble", "first_name": "Pebbles", "given_name": "Rubble"}}::vertex]::path | [{"id": 844424930131973, "label": "Person", "properties": {"gender": "male", "last_name": "Rubble", "first_name": "Bamm-Bamm", "given_name": "Rubble"}}::vertex, {"id": 2251799813685254, "label": "child_of", "end_id": 844424930131973, "start_id": 844424930131975, "properties": {"role": "Son"}}::edge, {"id": 844424930131975, "label": "Person", "properties": {"gender": "male", "last_name": "Rubble", "first_name": "Chip", "given_name": "Rubble"}}::vertex, {"id": 2251799813685253, "label": "child_of", "end_id": 844424930131974, "start_id": 844424930131975, "properties": {"role": "Son"}}::edge, {"id": 844424930131974, "label": "Person", "properties": {"gender": "male", "last_name": "Rubble", "first_name": "Pebbles", "given_name": "Rubble"}}::vertex]::path
(1 row)
daughter of bamm-bamm & pebbles roxy = Character.create!(species: human, first_name: ‘Roxann Elisabeth’, nick_name: ‘Roxy’, last_name: ‘Rubble’, gender: ‘female’)
SELECT *
FROM cypher('age_schema', $$
MATCH (f:Person), (m:Person)
WHERE f.first_name = 'Bamm-Bamm' and m.first_name = "Pebbles"
CREATE
pp = (f)-[:ParentOf {role: "Father"}]->(c:Person {first_name: "Roxann", last_name: "Rubble", given_name: "Rubble", gender: "female"})<-[:ParentOf {role: "Mother"}]-(m),
cp = (f)<-[:ChildOf {role: "Daughter"}]-(c)-[:ChildOf {role: "Daughter"}]->(m)
RETURN pp, cp
$$) as (ParentalPaths agtype, ChildPaths agtype);
parenthood | childhood
----------------------------------------------------
[{"id": 844424930131973, "label": "Person", "properties": {"gender": "male", "last_name": "Rubble", "first_name": "Bamm-Bamm", "given_name": "Rubble"}}::vertex, {"id": 1970324836974600, "label": "parent_of", "end_id": 844424930131976, "start_id": 844424930131973, "properties": {"role": "Father"}}::edge, {"id": 844424930131976, "label": "Person", "properties": {"gender": "female", "last_name": "Rubble", "first_name": "Roxann", "given_name": "Rubble"}}::vertex, {"id": 1970324836974599, "label": "parent_of", "end_id": 844424930131976, "start_id": 844424930131974, "properties": {"role": "Mother"}}::edge, {"id": 844424930131974, "label": "Person", "properties": {"gender": "male", "last_name": "Rubble", "first_name": "Pebbles", "given_name": "Rubble"}}::vertex]::path | [{"id": 844424930131973, "label": "Person", "properties": {"gender": "male", "last_name": "Rubble", "first_name": "Bamm-Bamm", "given_name": "Rubble"}}::vertex, {"id": 2251799813685256, "label": "child_of", "end_id": 844424930131973, "start_id": 844424930131976, "properties": {"role": "Daughter"}}::edge, {"id": 844424930131976, "label": "Person", "properties": {"gender": "female", "last_name": "Rubble", "first_name": "Roxann", "given_name": "Rubble"}}::vertex, {"id": 2251799813685255, "label": "child_of", "end_id": 844424930131974, "start_id": 844424930131976, "properties": {"role": "Daughter"}}::edge, {"id": 844424930131974, "label": "Person", "properties": {"gender": "male", "last_name": "Rubble", "first_name": "Pebbles", "given_name": "Rubble"}}::vertex]::path
Now we can find all person relations [*]
finds any paths between persons
SELECT *
FROM cypher('age_schema', $$
MATCH p = (p1:Person)-[*]->(p2:Person)
RETURN p
$$) as (PeoplePaths agtype);
Unfortunately, [*]
this is very SLOW!
To control performance we can limit the path depths.
We can also explicitly list a path length: (u)-[]->()-[]->(v)
which is the same as (u)-[*2]->(v)
We can allow a path match range the following allows a relationship path lenght of 1-5: (u)-[*..5]->(v)
.
If want matches with a minimum path length of three or more we can do: (u)-[*3..]->(v)
.
Finally, we can limit matches between 3 and 5 with: (u)-[*3..5]->(v)
.
Finally, we can also add a match using a label for example let’s list all the parents of Chip
SELECT *
FROM cypher('age_schema', $$
MATCH path = (child:Person {first_name: 'Chip'})-[:ChildOf]->(:Person)
RETURN path
$$) as (FamilyPath agtype);
familypath
----------------------------------------------------------
[{"id": 844424930131975, "label": "Person", "properties": {"gender": "male", "last_name": "Rubble", "first_name": "Chip", "given_name": "Rubble"}}::vertex, {"id": 2251799813685254, "label": "child_of", "end_id": 844424930131973, "start_id": 844424930131975, "properties": {"role": "Son"}}::edge, {"id": 844424930131973, "label": "Person", "properties": {"gender": "male", "last_name": "Rubble", "first_name": "Bamm-Bamm", "given_name": "Rubble"}}::vertex]::path
[{"id": 844424930131975, "label": "Person", "properties": {"gender": "male", "last_name": "Rubble", "first_name": "Chip", "given_name": "Rubble"}}::vertex, {"id": 2251799813685253, "label": "child_of", "end_id": 844424930131974, "start_id": 844424930131975, "properties": {"role": "Son"}}::edge, {"id": 844424930131974, "label": "Person", "properties": {"gender": "male", "last_name": "Rubble", "first_name": "Pebbles", "given_name": "Rubble"}}::vertex]::path
(2 rows)
Cool now we have the two parents Chip - might be nicer to just list the parents and not the paths with:
SELECT *
FROM cypher('age_schema', $$
MATCH (child:Person {first_name: 'Chip'})-[:ChildOf]->(parent:Person)
RETURN parent.first_name
$$) as (ParentName agtype);
parentname
-------------
"Bamm-Bamm"
"Pebbles"
(2 rows)
now to query parents of parents (family tree)
SELECT *
FROM cypher('age_schema', $$
MATCH (child:Person {first_name: 'Chip'})-[:ChildOf]->(:Person)-[:ChildOf]->(parent:Person)
RETURN parent.first_name
$$) as (Grandparents agtype);
grandparents
------------
"Fred"
"Wilma"
"Barney"
"Betty"
(4 rows)
as described earlier a nicer way to rewrite this is as:
SELECT *
FROM cypher('age_schema', $$
MATCH (child:Person {first_name: 'Chip'})-[:ChildOf *2]->(parent:Person)
RETURN parent.first_name
$$) as (Grandparents agtype);
grandparents
--------------
"Fred"
"Wilma"
"Barney"
"Betty"
(4 rows)
to get both parents and grandparent we query using:
FROM cypher('age_schema', $$
MATCH (child:Person {first_name: 'Chip'})-[:ChildOf *1..2]->(parent:Person)
RETURN parent.first_name
$$) as (Relatives agtype);
relatives
-------------
"Fred"
"Wilma"
"Barney"
"Betty"
"Bamm-Bamm"
"Pebbles"
(6 rows)
Multiple Path Matching
Let’s say we want to match all who have the same boss:
mr_slate = Character.create!(species: human, first_name: ‘George’, nick_name: ‘Mr.’, last_name: ‘Slate’, gender: ‘male’)
let’s add Mr Slate
to our data:
SELECT * FROM cypher('age_schema', $$
MATCH (firm:Company {name: 'Bedrock Quarry'})
CREATE path = (boss:Person {first_name: 'George', last_name: 'Slate', given_name: 'Slate', gender: 'male'})-[:WorksAt {role: "Manager"}]->(firm)
RETURN path
$$) as (Boss agtype);
boss
-------------------------------------------------------
[{"id": 844424930131977, "label": "Person", "properties": {"gender": "male", "last_name": "Slate", "first_name": "George", "given_name": "Slate"}}::vertex, {"id": 1407374883553285, "label": "WorksAt", "end_id": 1688849860263937, "start_id": 844424930131977, "properties": {"role": "Manager"}}::edge, {"id": 1688849860263937, "label": "company", "properties": {"name": "Bedrock Quarry"}}::vertex]::path
(1 row)
So to find people with known bosses:
SELECT * FROM cypher('age_schema', $$
MATCH (employee:Person)-[job:WorksAt]->(firm:Company)<-[:WorksAt {role: "Manager"}]-(boss:Person)
RETURN employee.first_name, job.role, firm.name, boss.last_name
$$) as (employee agtype, job agtype, firm agtype, boss agtype);
employee | job | firm | boss
----------+------------------+------------------+---------
"Fred" | "Crane Operator" | "Bedrock Quarry" | "Slate"
"Barney" | | "Bedrock Quarry" | "Slate"
(2 rows)
OOPS - we forgot to add Barney’s role. We can fix this with:
SELECT * FROM cypher('age_schema', $$
MATCH (employee:Person {first_name: "Barney"})-[job:WorksAt]->(firm:Company)
SET job.role = 'Crane Operator'
RETURN employee.first_name, job.role, firm.name
$$) as (employee agtype, job agtype, firm agtype);
employee | job | firm
----------+------------------+------------------
"Barney" | "Crane Operator" | "Bedrock Quarry"
PS we can remove a property with: SET job.role = NULL
Many Other Features
This is hopefully, enough of an intro to inspire exploring Apache AGE. It’s complete features all well documented at: https://age.apache.org/age-manual/master/
Resources
Usage Example Resources
Resources AGE
Apache AGE Installation Tutorial for MacOS - compile extension
APACHE AGE: Getting Started Part 3 (AGE Installation via Docker)