1 of 53

neolixir

A declarative ORM for the Neo4j graph database

Ivo Tzvetkov

2 of 53

Data stored in a graph instead of a table:

  • Nodes (vertices) and Relationships (edges)
  • Relationships are directed and have a “type”, Nodes can have labels
  • Dynamically-typed Properties on both Nodes and Relationships

:Person

:Person

:knows

since: 2012

name: ‘Bob’�born: 1978

name: ‘Alice’�born: 1980

3 of 53

Traditional table-based model (SQL / NoSQL)

Id

Name

Born

1

‘Bob’

1978

2

‘Alice’

1980

Id

PersonId

OtherId

Since

1

1

2

2012

Person

Knows

4 of 53

Cypher query language:

match (bob:Person {name: ‘Bob’})-[:knows]->(alice:Person {name: ‘Alice’})

5 of 53

Cypher query language:

match (bob:Person {name: ‘Bob’})-[:knows]->(alice:Person {name: ‘Alice’})

with bob, alice

match (bob)-[:knows*1..6]->(other:Person)-[rel:knows]->(alice)

where rel.since > 2000

6 of 53

Dynamic definition with no formal schema (limited indexing and constraints):

  • Very easy to prototype, modify and extend - no need for schema migrations!

:Person

:Person

:knows

since: 2012

name: ‘Bob’�born: 1978

name: ‘Alice’�born: 1980

7 of 53

Dynamic definition with no formal schema (limited indexing and constraints):

  • Very easy to prototype, modify and extend - no need for schema migrations!

:Person

:Person

:knows

since: 2012

name: ‘Bob’�born: 1978

name: ‘Alice’�born: 1980

>>> from py2neo import Node, Relationship�>>> a = Node("Person", name="Alice")�>>> b = Node("Person", name="Bob")�>>> ab = Relationship(a, "knows", b)�>>> ab�(alice)-[:knows]->(bob)

8 of 53

Dynamic definition with no formal schema (limited indexing and constraints):

  • Very easy to prototype, modify and extend - no need for schema migrations!
  • No validation! Very easy to have typos and other undetected mistakes!

:Person

:Person

:knows

since: 2012

name: ‘Bob’�born: 1978

name: ‘Alice’�born: 1980

>>> from py2neo import Node, Relationship�>>> a = Node("Person", name="Alice")�>>> b = Node("Person", name="Bob")�>>> ab = Relationship(a, "knows", b)�>>> ab�(alice)-[:knows]->(bob)

9 of 53

Dynamic definition with no formal schema (limited indexing and constraints):

  • Very easy to prototype, modify and extend - no need for schema migrations!
  • No validation! Very easy to have typos and other undetected mistakes!
  • Difficult to manage a large body of code without more formal abstractions

:Person

:Person

:knows

since: 2012

name: ‘Bob’�born: 1978

name: ‘Alice’�born: 1980

>>> from py2neo import Node, Relationship�>>> a = Node("Person", name="Alice")�>>> b = Node("Person", name="Bob")�>>> ab = Relationship(a, "knows", b)�>>> ab�(alice)-[:knows]->(bob)

10 of 53

A social platform to create and share rich learning activities:

  • Developed in Montréal in 2013
  • Mainly used for Primary, Secondary and CÉGEP content
  • Also professional training and adult education

Backend technology stack:

  • Python 2.7 backend with Neo4j as main database
  • MySQL and Redis where appropriate

11 of 53

12 of 53

13 of 53

A social platform to create and share rich learning activities:

  • 7k teachers
  • 150k students
  • 600+ schools
  • 270k users total

14 of 53

A social platform to create and share rich learning activities:

  • 7k teachers
  • 150k students
  • 600+ schools
  • 270k users total
  • 45 million Nodes
  • 76 million Relationships
  • 326 million Properties
  • 2k requests/min

15 of 53

neolixir

An Object-Relational Mapping (ORM) abstraction for Neo4j:

16 of 53

neolixir

An Object-Relational Mapping (ORM) abstraction for Neo4j:

  • Declarative model definition

17 of 53

Declarative model definition

In [1]: from neolixir import *

In [2]: class Person(Node):

...: name = String()

...: born = Integer()

...: knows = RelOut('knows')

18 of 53

Declarative model definition

In [1]: from neolixir import *

In [2]: class Person(Node):

...: name = String()

...: born = Integer()

...: knows = RelOut('knows')

In [3]: bob = Person(name='Bob', born=1978)

In [4]: bob

Out[4]:

<Person (0x31073d0):

Id = None

Descriptors = ['born', 'knows', 'name']

Properties = {'born': 1978, 'name': u'Bob'}

>

19 of 53

neolixir

An Object-Relational Mapping (ORM) abstraction for Neo4j:

  • Declarative model definition
  • Polymorphic inheritance

20 of 53

Polymorphic inheritance

In [2]: class Person(Node):

...: name = String()

...: born = Integer()

...: knows = RelOut('knows')

In [5]: class Student(Person):

...: grade = Integer()

21 of 53

Polymorphic inheritance

In [2]: class Person(Node):

...: name = String()

...: born = Integer()

...: knows = RelOut('knows')

In [5]: class Student(Person):

...: grade = Integer()

In [6]: alice = Student(name='Alice', born=1980, grade=8)

In [7]: alice

Out[7]:

<Student (0x33eb890):

Id = None

Descriptors = ['born', 'grade', 'knows', 'name']

Properties = {'grade': 8, 'born': 1980, 'name': u'Alice'}

>

22 of 53

neolixir

An Object-Relational Mapping (ORM) abstraction for Neo4j:

  • Declarative model definition
  • Polymorphic inheritance
  • Relationship abstraction

23 of 53

Relationship abstraction

In [3]: class Person(Node):

...: name = String()

...: knows = RelOut('knows')

24 of 53

Relationship abstraction

In [2]: class KnowsRel(Relationship):

...: __rel_type__ = 'knows'

...: since = DateTime()

In [3]: class Person(Node):

...: name = String()

...: knows = RelOut(KnowsRel)

25 of 53

Relationship abstraction

In [2]: class KnowsRel(Relationship):

...: __rel_type__ = 'knows'

...: since = DateTime()

In [3]: class Person(Node):

...: name = String()

...: knows = RelOut(KnowsRel)

In [4]: bob = Person(name='Bob')

In [5]: alice = Person(name='Alice')

In [6]: bob.knows.append(alice)

Out[6]: <KnowsRel (0x27e7810): (None)-[None:knows]->(None) {}>

26 of 53

Relationship abstraction

In [2]: class KnowsRel(Relationship):

...: __rel_type__ = 'knows'

...: since = DateTime()

In [3]: class Person(Node):

...: name = String()

...: knows = RelOut(KnowsRel)

In [4]: bob = Person(name='Bob')

In [5]: alice = Person(name='Alice')

In [6]: bob.knows.append(alice)

Out[6]: <KnowsRel (0x27e7810): (None)-[None:knows]->(None) {}>

In [7]: alice in bob.knows

Out[7]: True

27 of 53

Relationship abstraction

In [7]: bob.knows

Out[7]:

[<Person (0x27a9350):

Id = None

Descriptors = ['knows', 'name']

Properties = {'name': u'Alice'}

>]

28 of 53

Relationship abstraction

In [7]: bob.knows

Out[7]:

[<Person (0x27a9350):

Id = None

Descriptors = ['knows', 'name']

Properties = {'name': u'Alice'}

>]

In [8]: bob.knows.rels()

Out[8]: [<KnowsRel (0x27e7810): (None)-[None:knows]->(None) {}>]

29 of 53

Relationship abstraction

In [7]: bob.knows

Out[7]:

[<Person (0x27a9350):

Id = None

Descriptors = ['knows', 'name']

Properties = {'name': u'Alice'}

>]

In [8]: bob.knows.rels()

Out[8]: [<KnowsRel (0x27e7810): (None)-[None:knows]->(None) {}>]

In [9]: bob.knows.rel(alice).since = '2012-01-01'

In [10]: bob.knows.rel(alice)

Out[10]: <KnowsRel (0x27e7810): (None)-[None:knows]->(None) {'since': '2012-01-01 00:00:00'}>

30 of 53

neolixir

An Object-Relational Mapping (ORM) abstraction for Neo4j:

  • Declarative model definition
  • Polymorphic inheritance
  • Relationship abstraction
  • Strongly-typed properties

31 of 53

Strongly-typed properties

Types: Boolean, String, Enum, Integer, Float, Numeric, DateTime

32 of 53

Strongly-typed properties

Types: Boolean, String, Enum, Integer, Float, Numeric, DateTime

In [2]: class Student(Node):

...: name = String()

...: born = DateTime()

...: gpa = Numeric()

...: graduated = Boolean(default=False)

33 of 53

Strongly-typed properties

Types: Boolean, String, Enum, Integer, Float, Numeric, DateTime

In [2]: class Student(Node):

...: name = String()

...: born = DateTime()

...: gpa = Numeric()

...: graduated = Boolean(default=False)

In [3]: bob = Student(name='Bob', born='1970-01-01', gpa='3.0')

In [4]: bob.name, bob.born, bob.gpa, bob.graduated

Out[4]: (u'Bob', datetime.datetime(1970, 1, 1, 0, 0), Decimal('3.0'), False)

34 of 53

Strongly-typed properties

Types: Boolean, String, Enum, Integer, Float, Numeric, DateTime

In [2]: class Student(Node):

...: name = String()

...: born = DateTime()

...: gpa = Numeric()

...: graduated = Boolean(default=False)

In [3]: bob = Student(name='Bob', born='1970-01-01', gpa='3.0')

In [4]: bob.name, bob.born, bob.gpa, bob.graduated

Out[4]: (u'Bob', datetime.datetime(1970, 1, 1, 0, 0), Decimal('3.0'), False)

In [5]: bob.name = 123; bob.name

Out[5]: u'123'

35 of 53

Strongly-typed properties

Types: Boolean, String, Enum, Integer, Float, Numeric, DateTime

In [2]: class Student(Node):

...: name = String()

...: born = DateTime()

...: gpa = Numeric()

...: graduated = Boolean(default=False)

In [3]: bob = Student(name='Bob', born='1970-01-01', gpa='3.0')

In [4]: bob.name, bob.born, bob.gpa, bob.graduated

Out[4]: (u'Bob', datetime.datetime(1970, 1, 1, 0, 0), Decimal('3.0'), False)

In [5]: bob.name = 123; bob.name

Out[5]: u'123'

In [8]: bob.gpa = 'foo'

ValueError: Invalid value for Numeric: foo

36 of 53

neolixir

An Object-Relational Mapping (ORM) abstraction for Neo4j:

  • Declarative model definition
  • Polymorphic inheritance
  • Relationship abstraction
  • Strongly-typed properties
  • Session management

37 of 53

Session management

In [6]: bob

Out[6]:

<Person (0x22f5050):

Id = None

Descriptors = ['knows', 'name']

Properties = {'name': u'Bob'}

>

38 of 53

Session management

In [6]: bob

Out[6]:

<Person (0x22f5050):

Id = None

Descriptors = ['knows', 'name']

Properties = {'name': u'Bob'}

>

In [7]: metadata.session.commit()

In [8]: bob

Out[8]:

<Person (0x22f5050):

Id = 5433

Descriptors = ['knows', 'name']

Properties = {'name': u'Bob', '__class__': 'Person'}

>

39 of 53

Session management

In [9]: bob.knows.append(Person(name='Alice'))

Out[9]: <Relationship (0x237ccd0): (5433)-[None:knows]->(None) {}>

40 of 53

Session management

In [9]: bob.knows.append(Person(name='Alice'))

Out[9]: <Relationship (0x237ccd0): (5433)-[None:knows]->(None) {}>

In [10]: metadata.session.commit()

In [11]: bob.knows.rels()

Out[11]: [<Relationship (0x237ccd0): (5433)-[2706:knows]->(5434) {'__class__': 'Relationship'}>]

41 of 53

Session management

In [9]: bob.knows.append(Person(name='Alice'))

Out[9]: <Relationship (0x237ccd0): (5433)-[None:knows]->(None) {}>

In [10]: metadata.session.commit()

In [11]: bob.knows.rels()

Out[11]: [<Relationship (0x237ccd0): (5433)-[2706:knows]->(5434) {'__class__': 'Relationship'}>]

In [12]: bob.knows

Out[12]:

[<Person (0x23851d0):

Id = 5434

Descriptors = ['knows', 'name']

Properties = {'name': u'Alice', '__class__': 'Person'}

>]

42 of 53

Session management

In [15]: bob

Out[15]:

<Person (0x22f5050):

Id = 5433

Descriptors = ['knows', 'name']

Properties = {'name': u'Bob', '__class__': 'Person'}

>

43 of 53

Session management

In [15]: bob

Out[15]:

<Person (0x22f5050):

Id = 5433

Descriptors = ['knows', 'name']

Properties = {'name': u'Bob', '__class__': 'Person'}

>

In [16]: Node.get(5433)

Out[16]:

<Person (0x22f5050):

Id = 5433

Descriptors = ['knows', 'name']

Properties = {'name': u'Bob', '__class__': 'Person'}

>

44 of 53

Session management

In [16]: Node.get(5433)

Out[16]:

<Person (0x22f5050):

Id = 5433

Descriptors = ['knows', 'name']

Properties = {'name': u'Bob', '__class__': 'Person'}

>

In [17]: metadata.session.clear()

45 of 53

Session management

In [16]: Node.get(5433)

Out[16]:

<Person (0x22f5050):

Id = 5433

Descriptors = ['knows', 'name']

Properties = {'name': u'Bob', '__class__': 'Person'}

>

In [17]: metadata.session.clear()

In [18]: Node.get(5433)

Out[18]:

<Person (0x23ad990):

Id = 5433

Descriptors = ['born', 'knows', 'name']

Properties = {u'name': u'Bob', u'__class__': u'Person'}

>

46 of 53

neolixir

An Object-Relational Mapping (ORM) abstraction for Neo4j:

  • Declarative model definition
  • Polymorphic inheritance
  • Relationship abstraction
  • Strongly-typed properties
  • Session management
  • Query abstraction

47 of 53

Query abstraction

In [21]: Person.query.count()

Out[21]: 2

48 of 53

Query abstraction

In [21]: Person.query.count()

Out[21]: 2

In [22]: Person.query.string

Out[22]: 'match (instance:Person) return instance'

49 of 53

Query abstraction

In [21]: Person.query.count()

Out[21]: 2

In [22]: Person.query.string

Out[22]: 'match (instance:Person) return instance'

In [23]: Person.query.append('where instance.name = "Bob" return instance').string

Out[23]: 'match (instance:Person) where instance.name = "Bob" return instance'

50 of 53

Query abstraction

In [21]: Person.query.count()

Out[21]: 2

In [22]: Person.query.string

Out[22]: 'match (instance:Person) return instance'

In [23]: Person.query.append('where instance.name = "Bob" return instance').string

Out[23]: 'match (instance:Person) where instance.name = "Bob" return instance'

In [24]: Person.query.append('where instance.name = "Bob" return instance').execute()

Out[24]:

[[<Person (0x23ad990):

Id = 5433

Descriptors = ['born', 'knows', 'name']

Properties = {u'name': u'Bob', u'__class__': u'Person'}

>]]

51 of 53

neolixir

An Object-Relational Mapping (ORM) abstraction for Neo4j:

  • Declarative model definition
  • Polymorphic inheritance
  • Relationship abstraction
  • Strongly-typed properties
  • Session management
  • Query abstraction
  • Easy to use!

52 of 53

neolixir

Current state:

  • Stable Python 2.7 code base
  • Used in production for over 3 years
  • Comprehensive test suite

In the near future:

  • Complete documentation, PyPI release
  • Python 3 support and tests
  • Neo4j 3.0 support, new features, etc...

53 of 53

https://github.com/ivotkv/neolixir

ivotkv@gmail.com