Nigel Small

Geoff

Geoff is a text-based interchange format for Neo4j graph data that should be instantly readable to anyone familiar with Cypher, on which its syntax is based.

Implementation

The reference Geoff parser implementation is a Neo4j server extension called load2neo. This works with Neo4j 2.0 and upwards and is incompatible with earlier versions.

A client side parser-loader also exists within py2neo. This has no label support and, due to its use of the REST API, is somewhat less scalable than the server extensions. A demo of the XML to Geoff conversion tool, distributed with py2neo, can be seen at http://api.nigelsmall.com/xml-geoff.

Syntax

A Geoff document is composed of one or more subgraphs, each of which contains one or more paths made up from alternating nodes and relationships. For example:

(alice {"name":"Alice"})
(bob {"name":"Bob"})
(carol {"name":"Carol"})
(alice)<-[:KNOWS]->(bob)<-[:KNOWS]->(carol)<-[:KNOWS]->(alice)

Multiple subgraphs may exist within a document and are separated by a sequence of four tildes: ~~~~.

Nodes

Each node can have an identifier, a set of labels and a set of properties. None of these attributes are mandatory, and in the simplest case, a node may just be an empty set of parentheses: (). Node identifiers are local only to that subgraph and may be reused without overlap in other subgraphs, even in the same document. Identifiers are not carried over into Neo4j on a load operation.

The examples below show some of the combinations possible:

(a)                          /* a simple named node */
(b {"name":"Bob"})           /* a node with one property */
(cat:Animal)                 /* a node with one label and no properties */
(dog:Animal {"name":"dog"})  /* a node with both label and property */
(dinner:Spam:Egg:Chips {"foo":"bar","answer":42})  /* several of each */

Properties

Properties are represented in JSON format and may be provided in single nodes, in longer paths or in both:

(alice {"name":"Alice"})
(bob {"name":"Bob"})
(alice {"age":33})<-[:KNOWS]->(bob {"age":44})

Relationships

Relationships are drawn using ASCII art and look very similar to those in Cypher. These contain a type and optional properties and may point forwards, backwards or in both directions; undirected relationships are not permitted. When loaded, two-way relationships will create two unidirectional relationships, both containing any properties specified.

Some relationship examples are below:

(alice)-[:KNOWS]->(bob)
(alice)-[:KNOWS {"since":1999}]->(bob)
(alice)<-[:KNOWS {"since":1999}]-(bob)
(alice)<-[:KNOWS {"since":1999}]->(bob)

Relationships may be listed separately or chained together to form longer paths, such as:

(a {"name":"Alice"})<-[:KNOWS]->(b {"name":"Bob"})<-[:KNOWS]->(c {"name":"Carol"})<-[:KNOWS]->(a) 

Uniqueness

Nodes and relationships can also be designated as unique. This means that when they are loaded, a check is done for existing nodes or relationships and a new one is only created if no such entity is found. Uniqueness is denotated by an exclamation mark immediately following a node label or relationship type which may itself also be followed by a property key name.

Unique Node by Label and Property

(alice:Person!name {"name":"Alice","age":33})

On load, if a Person node exists with a name property value of "Alice", then update that node's properties. Otherwise, create a new node with the properties specified.

Unique Relationship by Type

(alice)-[:KNOWS! {"since":1999}]->(bob)

On load, if a KNOWS relationship exists between alice and bob, then update its properties. Otherwise, create it.

Unique Relationship by Type and Property

(alice)-[:KNOWS!since {"since":1999}]->(bob)

On load, if a KNOWS relationship exists between alice and bob with a since property value of 1999, then update its properties. Otherwise, create it.

Full Syntax Specification

A full specification for the Geoff syntax is described below:

document       := subgraph (_ boundary _ subgraph)*
boundary       := "~~~~"

subgraph       := [element (_ element)*]
element        := comment | hook | path

comment        := "/*" <<any text excluding sequence "*/">> "*/"

hook           := ":" ~ label [~ ":" ~ key] ~ ":" "=>" node

path           := node (forward_path | reverse_path | two_way_path)*
forward_path   := "-" relationship "->" node
reverse_path   := "<-" relationship "-" node
two_way_path   := "<-" relationship "->" node

node           := named_node | anonymous_node
named_node     := "(" ~ node_name [label_list] [_ property_map] ~ ")"
anonymous_node := "(" ~ [label_list] [_ property_map] ~ ")"
relationship   := "[" ~ ":" type ["!" [key]] [_ property_map] ~ "]"
label_list     := ":" label ["!" key] (":" label)*
label          := name | JSON_STRING
property_map   := "{" ~ [key_value (~ "," ~ key_value)* ~] "}"
node_name      := name | JSON_STRING
name           := (ALPHA | DIGIT | "_")+
type           := name | JSON_STRING
key_value      := key ~ ":" ~ value
key            := name | JSON_STRING
value          := array | JSON_STRING | JSON_NUMBER | JSON_BOOLEAN | JSON_NULL

array          := empty_array | string_array | numeric_array | boolean_array
empty_array    := "[" ~ "]"
string_array   := "[" ~ JSON_STRING (~ "," ~ JSON_STRING)* ~ "]"
numeric_array  := "[" ~ JSON_NUMBER (~ "," ~ JSON_NUMBER)* ~ "]"
boolean_array  := "[" ~ JSON_BOOLEAN (~ "," ~ JSON_BOOLEAN)* ~ "]"

* Mandatory whitespace is represented by "_" and optional whitespace by "~"