Leveraging the Power of
Graph Databases
in PHP
Jeremy Kendall
Atlanta PHP
April 2015
Obligatory Intro Slide
Also - New Father
What Kind of
Database?
Graphs != Charts
https://www.flickr.com/photos/markgroves/3065192499/
Graphs != Charts
http://stephenwildish.tumblr.com/post/101408321763/friday-project-witch-moral-compass
Graph Databases
• Data Model
• Nodes with properties
• Typed relationships
• Strengths
• Highly connected data
• ACID
• Weaknesses
• Paradigm shift
• Examples
• Neo4j, Titan, OrientDB
Why Care?
• All the NoSQL Joy
• Schema-less
• Semi-structured data
• Escape from JOIN Hell
• Speed
Why Care?
• Relationships have 1st class status
• Just as important as the objects they connect
• You can have properties & labels
• Multiple relationships
Why Care?
Speed
Depth MySQL Query Time Neo4j Query Time Records Returned
2 0.028 (28 MS) 0.04 ~900
3 0.213 0.06 ~999
4 10.273 0.07 ~999
5 92.613 0.07 ~999
1,000 people with an average 50 friends each
Crazy Speed
Depth MySQL Query Time Neo4j Query Time Records Returned
2 0.016 (16 MS) 0.01 ~2500
3 30.27 0.168 ~125,000
4 1543.505 1.359 ~600,000
5 Stopped after 1 hour 2.132 ~800,000
1,000,000 people with an average 50 friends each
Neo4j + Cypher
Cypher
• Neo4j’s declarative query language
• Easy to pick up
• Some clauses and concepts familiar from SQL
Simple Example
Goal
Create Some Nodes
CREATE (jk:Person { name: "Jeremy Kendall" })
CREATE (gs:Company { name: "Graph Story" })
CREATE (tn:State { name: "Tennessee" })
CREATE (memphis:City { name: "Memphis" })
CREATE (nashville:City { name: "Nashville" })
CREATE (hotchicken:Food { name: "Hot Chicken" })
CREATE (bbq:Food { name: "Barbecue" })
CREATE (photography:Hobby { name: "Photography" })
CREATE (language:Language { name: "PHP" })
// . . . snip . . .
Create Some Relationships
// . . . snip . . .
CREATE (jk)-[:WORKS_AT { title: {"CTO"}]->(gs),
(jk)-[:LIVES_IN]->(memphis)-[:LIVED_IN]->(nashville),
(hotchicken)-[:ONLY_IN]->(nashville),
(bbq)-[:ONLY_IN]->(memphis),
(jk)-[:LOVES]->(hotchicken),
// . . . snip . . .
Create Some Relationships
// . . . snip . . .
CREATE (jk)-[:WORKS_AT { title: {"CTO"}]->(gs),
(jk)-[:LIVES_IN]->(memphis)-[:LIVED_IN]->(nashville),
(hotchicken)-[:ONLY_IN]->(nashville),
(bbq)-[:ONLY_IN]->(memphis),
(jk)-[:LOVES]->(hotchicken),
// . . . snip . . .
Create Some Relationships
// . . . snip . . .
CREATE (jk)-[:WORKS_AT { title: {"CTO"}]->(gs),
(jk)-[:LIVES_IN]->(memphis)-[:LIVED_IN]->(nashville),
(hotchicken)-[:ONLY_IN]->(nashville),
(bbq)-[:ONLY_IN]->(memphis),
(jk)-[:LOVES]->(hotchicken),
// . . . snip . . .
Create Some Relationships
// . . . snip . . .
CREATE (jk)-[:WORKS_AT { title: {"CTO"}]->(gs),
(jk)-[:LIVES_IN]->(memphis)-[:LIVED_IN]->(nashville),
(hotchicken)-[:ONLY_IN]->(nashville),
(bbq)-[:ONLY_IN]->(memphis),
(jk)-[:LOVES]->(hotchicken),
// . . . snip . . .
Example Cypher Query
MATCH (p:Person { name: "Jeremy Kendall" })-[:LOVES]->(l)
WITH p, l
MATCH (p)-[:WORKS_AT]->(j)
WITH p, l, j
MATCH (p)-[:LIVES_IN]->(c:City)-[:LIVED_IN*0..]->(o:City)
RETURN p, l, j, o
Example Cypher Query
MATCH (p:Person { name: "Jeremy Kendall" })-[:LOVES]->(l)
WITH p, l
MATCH (p)-[:WORKS_AT]->(j)
WITH p, l, j
MATCH (p)-[:LIVES_IN]->(c:City)-[:LIVED_IN*0..]->(o:City)
RETURN p, l, j, o
Example Cypher Query
MATCH (p:Person { name: "Jeremy Kendall" })-[:LOVES]->(l)
WITH p, l
MATCH (p)-[:WORKS_AT]->(j)
WITH p, l, j
MATCH (p)-[:LIVES_IN]->(c:City)-[:LIVED_IN*0..]->(o:City)
RETURN p, l, j, o
Example Cypher Query
MATCH (p:Person { name: "Jeremy Kendall" })-[:LOVES]->(l)
WITH p, l
MATCH (p)-[:WORKS_AT]->(j)
WITH p, l, j
MATCH (p)-[:LIVES_IN]->(c:City)-[:LIVED_IN*0..]->(o:City)
RETURN p, l, j, o
Example Cypher Query
MATCH (p:Person { name: "Jeremy Kendall" })-[:LOVES]->(l)
WITH p, l
MATCH (p)-[:WORKS_AT]->(j)
WITH p, l, j
MATCH (p)-[:LIVES_IN]->(c:City)-[:LIVED_IN*0..]->(o:City)
RETURN p, l, j, o
Example Cypher Query
MATCH (p:Person { name: "Jeremy Kendall" })-[:LOVES]->(l)
WITH p, l
MATCH (p)-[:WORKS_AT]->(j)
WITH p, l, j
MATCH (p)-[:LIVES_IN]->(c:City)-[:LIVED_IN*0..]->(o:City)
RETURN p, l, j, o
Example Cypher Query
MATCH (p:Person { name: "Jeremy Kendall" })-[:LOVES]->(l)
WITH p, l
MATCH (p)-[:WORKS_AT]->(j)
WITH p, l, j
MATCH (p)-[:LIVES_IN]->(c:City)-[:LIVED_IN*0..]->(o:City)
RETURN p, l, j, o
Query Result
However!
Easy to Model,
Challenging to Master
Subtle(-ish) Bug
Subtle(-ish) Bug
Neo4j + PHP
Neo4jPHP
• PHP wrapper for the Neo4j REST API
• Installable via Composer
• Used internally at Graph Story
• Used in this presentation
• Well tested
• https://packagist.org/packages/everyman/
neo4jphp
Also see: NeoClient
• Written by Neoxygen
• Alternative PHP wrapper for the Neo4j REST API
• Installable via Composer
• Accepted for internal use at Graph Story
• Well tested
• https://packagist.org/packages/neoxygen/neoclient
Connecting
$neo4jClient = new EverymanNeo4jClient(
‘yourgraph.example.com’,
7473
);
$neo4jClient->getTransport()
->setAuth('username', 'password')
->getTransport()->useHttps();
Creating a Node and Label
$node = new Node($neo4jClient);
$label = $neo4jClient->makeLabel('Person');
$node->setProperty('name', ‘Jeremy Kendall');
$node->save()->addLabels(array($label));
Searching
// Searching for a label by property
$label = $neo4jClient->makeLabel('Person');
$nodes = $label->getNodes('name', $name);
Querying (Cypher)
$queryString =
'MATCH (p:Person { name: { name }}) RETURN p';
$query = new EverymanNeo4jCypherQuery(
$neo4jClient,
$queryString,
['name' => ‘Jeremy Kendall']
);
$result = $query->getResultSet();
Named Parameters
Named Parameters
$queryString =
'MATCH (p:Person { name: { name }}) RETURN p';
$query = new EverymanNeo4jCypherQuery(
$neo4jClient,
$queryString,
['name' => ‘Jeremy Kendall']
);
$result = $query->getResultSet();
Named Parameters
$queryString =
'MATCH (p:Person { name: { name }}) RETURN p';
$query = new EverymanNeo4jCypherQuery(
$neo4jClient,
$queryString,
['name' => ‘Jeremy Kendall']
);
$result = $query->getResultSet();
Content Modeling:
News Feeds
Graph Kit for PHP
https://github.com/GraphStory/graph-kit-php
News Feed
• Modeled as a list of posts
• Newest post first
• All subsequent posts follow
• Relationships: LASTPOST and NEXTPOST
LASTPOST
NEXTPOST
The Content Model
class Content
{
public $node;
public $nodeId;
public $contentId;
public $title;
public $url;
public $tagstr;
public $timestamp;
public $userNameForPost;
public $owner = false;
}
Adding Content
public static function add($username, Content $content)
{
$queryString =<<<CYPHER
MATCH (user { username: {u}})
OPTIONAL MATCH (user)-[r:LASTPOST]->(lastpost)
DELETE r
CREATE (user)-[:LASTPOST]->(p:Content { title:{title}, url:{url}, tagstr:{tagstr}, timestamp:{timestamp}, contentId:
{contentId} })
WITH p, collect(lastpost) as lastposts
FOREACH (x IN lastposts | CREATE p-[:NEXTPOST]->x)
RETURN p, {u} as username, true as owner
CYPHER;
$query = new Query(
Neo4jClient::client(),
$queryString,
array(
'u' => $username,
'title' => $content->title,
'url' => $content->url,
'tagstr' => $content->tagstr,
'timestamp' => time(),
'contentId' => uniqid()
)
);
$result = $query->getResultSet();
return self::returnMappedContent($result);
}
Adding Content
public static function add($username, Content $content)
{
$queryString =<<<CYPHER
MATCH (user { username: {u}})
OPTIONAL MATCH (user)-[r:LASTPOST]->(lastpost)
DELETE r
CREATE (user)-[:LASTPOST]->(p:Content { title:{title}, url:{url}, tagstr:{tagstr}, timestamp:{timestamp}, contentId:
{contentId} })
WITH p, collect(lastpost) as lastposts
FOREACH (x IN lastposts | CREATE p-[:NEXTPOST]->x)
RETURN p, {u} as username, true as owner
CYPHER;
$query = new Query(
Neo4jClient::client(),
$queryString,
array(
'u' => $username,
'title' => $content->title,
'url' => $content->url,
'tagstr' => $content->tagstr,
'timestamp' => time(),
'contentId' => uniqid()
)
);
$result = $query->getResultSet();
return self::returnMappedContent($result);
}
Adding Content
public static function add($username, Content $content)
{
$queryString =<<<CYPHER
MATCH (user { username: {u}})
OPTIONAL MATCH (user)-[r:LASTPOST]->(lastpost)
DELETE r
CREATE (user)-[:LASTPOST]->(p:Content { title:{title}, url:{url}, tagstr:{tagstr}, timestamp:{timestamp}, contentId:
{contentId} })
WITH p, collect(lastpost) as lastposts
FOREACH (x IN lastposts | CREATE p-[:NEXTPOST]->x)
RETURN p, {u} as username, true as owner
CYPHER;
$query = new Query(
Neo4jClient::client(),
$queryString,
array(
'u' => $username,
'title' => $content->title,
'url' => $content->url,
'tagstr' => $content->tagstr,
'timestamp' => time(),
'contentId' => uniqid()
)
);
$result = $query->getResultSet();
return self::returnMappedContent($result);
}
Adding Content
MATCH (user { username: {u}})
OPTIONAL MATCH (user)-[r:LASTPOST]->(lastpost)
DELETE r
CREATE (user)-[:LASTPOST]->(p:Content { title:{title}, url:
{url}, tagstr:{tagstr}, timestamp:{timestamp}, contentId:
{contentId} })
WITH p, collect(lastpost) as lastposts
FOREACH (x IN lastposts | CREATE p-[:NEXTPOST]->x)
RETURN p, {u} as username, true as owner
Adding Content
MATCH (user { username: {u}})
OPTIONAL MATCH (user)-[r:LASTPOST]->(lastpost)
DELETE r
CREATE (user)-[:LASTPOST]->(p:Content { title:{title}, url:
{url}, tagstr:{tagstr}, timestamp:{timestamp}, contentId:
{contentId} })
WITH p, collect(lastpost) as lastposts
FOREACH (x IN lastposts | CREATE p-[:NEXTPOST]->x)
RETURN p, {u} as username, true as owner
Adding Content
MATCH (user { username: {u}})
OPTIONAL MATCH (user)-[r:LASTPOST]->(lastpost)
DELETE r
CREATE (user)-[:LASTPOST]->(p:Content { title:{title}, url:
{url}, tagstr:{tagstr}, timestamp:{timestamp}, contentId:
{contentId} })
WITH p, collect(lastpost) as lastposts
FOREACH (x IN lastposts | CREATE p-[:NEXTPOST]->x)
RETURN p, {u} as username, true as owner
Adding Content
MATCH (user { username: {u}})
OPTIONAL MATCH (user)-[r:LASTPOST]->(lastpost)
DELETE r
CREATE (user)-[:LASTPOST]->(p:Content { title:{title}, url:
{url}, tagstr:{tagstr}, timestamp:{timestamp}, contentId:
{contentId} })
WITH p, collect(lastpost) as lastposts
FOREACH (x IN lastposts | CREATE p-[:NEXTPOST]->x)
RETURN p, {u} as username, true as owner
Adding Content
MATCH (user { username: {u}})
OPTIONAL MATCH (user)-[r:LASTPOST]->(lastpost)
DELETE r
CREATE (user)-[:LASTPOST]->(p:Content { title:{title}, url:
{url}, tagstr:{tagstr}, timestamp:{timestamp}, contentId:
{contentId} })
WITH p, collect(lastpost) as lastposts
FOREACH (x IN lastposts | CREATE p-[:NEXTPOST]->x)
RETURN p, {u} as username, true as owner
Adding Content
MATCH (user { username: {u}})
OPTIONAL MATCH (user)-[r:LASTPOST]->(lastpost)
DELETE r
CREATE (user)-[:LASTPOST]->(p:Content { title:{title}, url:
{url}, tagstr:{tagstr}, timestamp:{timestamp}, contentId:
{contentId} })
WITH p, collect(lastpost) as lastposts
FOREACH (x IN lastposts | CREATE p-[:NEXTPOST]->x)
RETURN p, {u} as username, true as owner
Adding Content
$query = new Query(
$neo4jClient,
$queryString,
array(
'u' => $username,
'title' => $content->title,
'url' => $content->url,
'tagstr' => $content->tagstr,
'timestamp' => time(),
'contentId' => uniqid()
)
);
$result = $query->getResultSet();
Retrieving Content
public static function getContent($username, $skip)
{
$queryString = <<<CYPHER
MATCH (u:User { username: { u }})-[:FOLLOWS*0..1]->f
WITH DISTINCT f, u
MATCH f-[:LASTPOST]-lp-[:NEXTPOST*0..]-p
RETURN p, f.username as username, f = u as owner
ORDER BY p.timestamp desc SKIP { skip } LIMIT 4
CYPHER;
$query = new Query(
Neo4jClient::client(),
$queryString,
array(
'u' => $username,
'skip' => $skip,
)
);
$result = $query->getResultSet();
return self::returnMappedContent($result);
}
Retrieving Content
MATCH (u:User { username: { u }})-[:FOLLOWS*0..1]->f
WITH DISTINCT f, u
MATCH f-[:LASTPOST]-lp-[:NEXTPOST*0..]-p
RETURN p, f.username as username, f = u as owner
ORDER BY p.timestamp desc SKIP { skip } LIMIT 4
Retrieving Content
MATCH (u:User { username: { u }})-[:FOLLOWS*0..1]->f
WITH DISTINCT f, u
MATCH f-[:LASTPOST]-lp-[:NEXTPOST*0..]-p
RETURN p, f.username as username, f = u as owner
ORDER BY p.timestamp desc SKIP { skip } LIMIT 4
Retrieving Content
MATCH (u:User { username: { u }})-[:FOLLOWS*0..1]->f
WITH DISTINCT f, u
MATCH f-[:LASTPOST]-lp-[:NEXTPOST*0..]-p
RETURN p, f.username as username, f = u as owner
ORDER BY p.timestamp desc SKIP { skip } LIMIT 4
Retrieving Content
MATCH (u:User { username: { u }})-[:FOLLOWS*0..1]->f
WITH DISTINCT f, u
MATCH f-[:LASTPOST]-lp-[:NEXTPOST*0..]-p
RETURN p, f.username as username, f = u as owner
ORDER BY p.timestamp desc SKIP { skip } LIMIT 4
Retrieving Content
MATCH (u:User { username: { u }})-[:FOLLOWS*0..1]->f
WITH DISTINCT f, u
MATCH f-[:LASTPOST]-lp-[:NEXTPOST*0..]-p
RETURN p, f.username as username, f = u as owner
ORDER BY p.timestamp desc SKIP { skip } LIMIT 4
Retrieving Content
MATCH (u:User { username: { u }})-[:FOLLOWS*0..1]->f
WITH DISTINCT f, u
MATCH f-[:LASTPOST]-lp-[:NEXTPOST*0..]-p
RETURN p, f.username as username, f = u as owner
ORDER BY p.timestamp desc SKIP { skip } LIMIT 4
Editing Content
public static function edit(Content $content)
{
$updatedAt = time();
$node = $content->node;
$node->setProperty('title', $content->title);
$node->setProperty('url', $content->url);
$node->setProperty('tagstr', $content->tagstr);
$node->setProperty('updated', $updatedAt);
$node->save();
$content->updated = $updatedAt;
return $content;
}
Editing Content
public static function edit(Content $content)
{
$updatedAt = time();
$node = $content->node;
$node->setProperty('title', $content->title);
$node->setProperty('url', $content->url);
$node->setProperty('tagstr', $content->tagstr);
$node->setProperty('updated', $updatedAt);
$node->save();
$content->updated = $updatedAt;
return $content;
}
Editing Content
public static function edit(Content $content)
{
$updatedAt = time();
$node = $content->node;
$node->setProperty('title', $content->title);
$node->setProperty('url', $content->url);
$node->setProperty('tagstr', $content->tagstr);
$node->setProperty('updated', $updatedAt);
$node->save();
$content->updated = $updatedAt;
return $content;
}
Editing Content
public static function edit(Content $content)
{
$updatedAt = time();
$node = $content->node;
$node->setProperty('title', $content->title);
$node->setProperty('url', $content->url);
$node->setProperty('tagstr', $content->tagstr);
$node->setProperty('updated', $updatedAt);
$node->save();
$content->updated = $updatedAt;
return $content;
}
Editing Content
public static function edit(Content $content)
{
$updatedAt = time();
$node = $content->node;
$node->setProperty('title', $content->title);
$node->setProperty('url', $content->url);
$node->setProperty('tagstr', $content->tagstr);
$node->setProperty('updated', $updatedAt);
$node->save();
$content->updated = $updatedAt;
return $content;
}
Deleting Content
public static function delete($username, $contentId)
{
$queryString = self::getDeleteQueryString(
$username,
$contentId
);
$params = array(
'username' => $username,
'contentId' => $contentId,
);
$query = new Query(
$neo4jClient,
$queryString,
$params
);
$query->getResultSet();
}
Deleting Content
public static function delete($username, $contentId)
{
$queryString = self::getDeleteQueryString(
$username,
$contentId
);
$params = array(
'username' => $username,
'contentId' => $contentId,
);
$query = new Query(
$neo4jClient,
$queryString,
$params
);
$query->getResultSet();
}
Deleting Content
public static function delete($username, $contentId)
{
$queryString = self::getDeleteQueryString(
$username,
$contentId
);
$params = array(
'username' => $username,
'contentId' => $contentId,
);
$query = new Query(
$neo4jClient,
$queryString,
$params
);
$query->getResultSet();
}
Deleting Content
public static function delete($username, $contentId)
{
$queryString = self::getDeleteQueryString(
$username,
$contentId
);
$params = array(
'username' => $username,
'contentId' => $contentId,
);
$query = new Query(
$neo4jClient,
$queryString,
$params
);
$query->getResultSet();
}
Deleting Content: Leaf
// If leaf
MATCH (u:User { username: { username }})-[:LASTPOST|
NEXTPOST*0..]->(c:Content { contentId: { contentId }})
WITH c
MATCH (c)-[r]-()
DELETE c, r
Deleting Content: Leaf
// If leaf
MATCH (u:User { username: { username }})-[:LASTPOST|
NEXTPOST*0..]->(c:Content { contentId: { contentId }})
WITH c
MATCH (c)-[r]-()
DELETE c, r
Deleting Content: Leaf
// If leaf
MATCH (u:User { username: { username }})-[:LASTPOST|
NEXTPOST*0..]->(c:Content { contentId: { contentId }})
WITH c
MATCH (c)-[r]-()
DELETE c, r
Deleting Content: Leaf
// If leaf
MATCH (u:User { username: { username }})-[:LASTPOST|
NEXTPOST*0..]->(c:Content { contentId: { contentId }})
WITH c
MATCH (c)-[r]-()
DELETE c, r
Deleting Content: Leaf
// If leaf
MATCH (u:User { username: { username }})-[:LASTPOST|
NEXTPOST*0..]->(c:Content { contentId: { contentId }})
WITH c
MATCH (c)-[r]-()
DELETE c, r
Deleting Content: LASTPOST
// If last
MATCH (u:User { username: { username }})-[lp:LASTPOST]-
>(del:Content { contentId: { contentId }})-[np:NEXTPOST]-
>(nextPost)
CREATE UNIQUE (u)-[:LASTPOST]->(nextPost)
DELETE lp, del, np
Deleting Content: LASTPOST
// If last
MATCH (u:User { username: { username }})-[lp:LASTPOST]-
>(del:Content { contentId: { contentId }})-[np:NEXTPOST]-
>(nextPost)
CREATE UNIQUE (u)-[:LASTPOST]->(nextPost)
DELETE lp, del, np
Deleting Content: LASTPOST
// If last
MATCH (u:User { username: { username }})-[lp:LASTPOST]-
>(del:Content { contentId: { contentId }})-[np:NEXTPOST]-
>(nextPost)
CREATE UNIQUE (u)-[:LASTPOST]->(nextPost)
DELETE lp, del, np
Deleting Content: LASTPOST
// If last
MATCH (u:User { username: { username }})-[lp:LASTPOST]-
>(del:Content { contentId: { contentId }})-[np:NEXTPOST]-
>(nextPost)
CREATE UNIQUE (u)-[:LASTPOST]->(nextPost)
DELETE lp, del, np
Deleting Content: Other
// All other
MATCH (u:User { username: { username }})-[:LASTPOST|
NEXTPOST*0..]->(before),
(before)-[delBefore]->(del:Content { contentId:
{ contentId }})-[delAfter]->(after)
CREATE UNIQUE (before)-[:NEXTPOST]->(after)
DELETE del, delBefore, delAfter
Deleting Content: Other
// All other
MATCH (u:User { username: { username }})-[:LASTPOST|
NEXTPOST*0..]->(before),
(before)-[delBefore]->(del:Content { contentId:
{ contentId }})-[delAfter]->(after)
CREATE UNIQUE (before)-[:NEXTPOST]->(after)
DELETE del, delBefore, delAfter
Deleting Content: Other
// All other
MATCH (u:User { username: { username }})-[:LASTPOST|
NEXTPOST*0..]->(before),
(before)-[delBefore]->(del:Content { contentId:
{ contentId }})-[delAfter]->(after)
CREATE UNIQUE (before)-[:NEXTPOST]->(after)
DELETE del, delBefore, delAfter
Deleting Content: Other
// All other
MATCH (u:User { username: { username }})-[:LASTPOST|
NEXTPOST*0..]->(before),
(before)-[delBefore]->(del:Content { contentId:
{ contentId }})-[delAfter]->(after)
CREATE UNIQUE (before)-[:NEXTPOST]->(after)
DELETE del, delBefore, delAfter
Deleting Content: Other
// All other
MATCH (u:User { username: { username }})-[:LASTPOST|
NEXTPOST*0..]->(before),
(before)-[delBefore]->(del:Content { contentId:
{ contentId }})-[delAfter]->(after)
CREATE UNIQUE (before)-[:NEXTPOST]->(after)
DELETE del, delBefore, delAfter
Questions?
Thanks!
jeremy.kendall@graphstory.com
@JeremyKendall
http://www.graphstory.com

Leveraging the Power of Graph Databases in PHP

  • 1.
    Leveraging the Powerof Graph Databases in PHP Jeremy Kendall Atlanta PHP April 2015
  • 2.
  • 3.
    Also - NewFather
  • 4.
  • 5.
  • 6.
  • 7.
    Graph Databases • DataModel • Nodes with properties • Typed relationships • Strengths • Highly connected data • ACID • Weaknesses • Paradigm shift • Examples • Neo4j, Titan, OrientDB
  • 8.
    Why Care? • Allthe NoSQL Joy • Schema-less • Semi-structured data • Escape from JOIN Hell • Speed
  • 9.
    Why Care? • Relationshipshave 1st class status • Just as important as the objects they connect • You can have properties & labels • Multiple relationships
  • 10.
  • 11.
    Speed Depth MySQL QueryTime Neo4j Query Time Records Returned 2 0.028 (28 MS) 0.04 ~900 3 0.213 0.06 ~999 4 10.273 0.07 ~999 5 92.613 0.07 ~999 1,000 people with an average 50 friends each
  • 12.
    Crazy Speed Depth MySQLQuery Time Neo4j Query Time Records Returned 2 0.016 (16 MS) 0.01 ~2500 3 30.27 0.168 ~125,000 4 1543.505 1.359 ~600,000 5 Stopped after 1 hour 2.132 ~800,000 1,000,000 people with an average 50 friends each
  • 14.
  • 15.
    Cypher • Neo4j’s declarativequery language • Easy to pick up • Some clauses and concepts familiar from SQL
  • 16.
  • 17.
  • 18.
    Create Some Nodes CREATE(jk:Person { name: "Jeremy Kendall" }) CREATE (gs:Company { name: "Graph Story" }) CREATE (tn:State { name: "Tennessee" }) CREATE (memphis:City { name: "Memphis" }) CREATE (nashville:City { name: "Nashville" }) CREATE (hotchicken:Food { name: "Hot Chicken" }) CREATE (bbq:Food { name: "Barbecue" }) CREATE (photography:Hobby { name: "Photography" }) CREATE (language:Language { name: "PHP" }) // . . . snip . . .
  • 19.
    Create Some Relationships //. . . snip . . . CREATE (jk)-[:WORKS_AT { title: {"CTO"}]->(gs), (jk)-[:LIVES_IN]->(memphis)-[:LIVED_IN]->(nashville), (hotchicken)-[:ONLY_IN]->(nashville), (bbq)-[:ONLY_IN]->(memphis), (jk)-[:LOVES]->(hotchicken), // . . . snip . . .
  • 20.
    Create Some Relationships //. . . snip . . . CREATE (jk)-[:WORKS_AT { title: {"CTO"}]->(gs), (jk)-[:LIVES_IN]->(memphis)-[:LIVED_IN]->(nashville), (hotchicken)-[:ONLY_IN]->(nashville), (bbq)-[:ONLY_IN]->(memphis), (jk)-[:LOVES]->(hotchicken), // . . . snip . . .
  • 21.
    Create Some Relationships //. . . snip . . . CREATE (jk)-[:WORKS_AT { title: {"CTO"}]->(gs), (jk)-[:LIVES_IN]->(memphis)-[:LIVED_IN]->(nashville), (hotchicken)-[:ONLY_IN]->(nashville), (bbq)-[:ONLY_IN]->(memphis), (jk)-[:LOVES]->(hotchicken), // . . . snip . . .
  • 22.
    Create Some Relationships //. . . snip . . . CREATE (jk)-[:WORKS_AT { title: {"CTO"}]->(gs), (jk)-[:LIVES_IN]->(memphis)-[:LIVED_IN]->(nashville), (hotchicken)-[:ONLY_IN]->(nashville), (bbq)-[:ONLY_IN]->(memphis), (jk)-[:LOVES]->(hotchicken), // . . . snip . . .
  • 23.
    Example Cypher Query MATCH(p:Person { name: "Jeremy Kendall" })-[:LOVES]->(l) WITH p, l MATCH (p)-[:WORKS_AT]->(j) WITH p, l, j MATCH (p)-[:LIVES_IN]->(c:City)-[:LIVED_IN*0..]->(o:City) RETURN p, l, j, o
  • 24.
    Example Cypher Query MATCH(p:Person { name: "Jeremy Kendall" })-[:LOVES]->(l) WITH p, l MATCH (p)-[:WORKS_AT]->(j) WITH p, l, j MATCH (p)-[:LIVES_IN]->(c:City)-[:LIVED_IN*0..]->(o:City) RETURN p, l, j, o
  • 25.
    Example Cypher Query MATCH(p:Person { name: "Jeremy Kendall" })-[:LOVES]->(l) WITH p, l MATCH (p)-[:WORKS_AT]->(j) WITH p, l, j MATCH (p)-[:LIVES_IN]->(c:City)-[:LIVED_IN*0..]->(o:City) RETURN p, l, j, o
  • 26.
    Example Cypher Query MATCH(p:Person { name: "Jeremy Kendall" })-[:LOVES]->(l) WITH p, l MATCH (p)-[:WORKS_AT]->(j) WITH p, l, j MATCH (p)-[:LIVES_IN]->(c:City)-[:LIVED_IN*0..]->(o:City) RETURN p, l, j, o
  • 27.
    Example Cypher Query MATCH(p:Person { name: "Jeremy Kendall" })-[:LOVES]->(l) WITH p, l MATCH (p)-[:WORKS_AT]->(j) WITH p, l, j MATCH (p)-[:LIVES_IN]->(c:City)-[:LIVED_IN*0..]->(o:City) RETURN p, l, j, o
  • 28.
    Example Cypher Query MATCH(p:Person { name: "Jeremy Kendall" })-[:LOVES]->(l) WITH p, l MATCH (p)-[:WORKS_AT]->(j) WITH p, l, j MATCH (p)-[:LIVES_IN]->(c:City)-[:LIVED_IN*0..]->(o:City) RETURN p, l, j, o
  • 29.
    Example Cypher Query MATCH(p:Person { name: "Jeremy Kendall" })-[:LOVES]->(l) WITH p, l MATCH (p)-[:WORKS_AT]->(j) WITH p, l, j MATCH (p)-[:LIVES_IN]->(c:City)-[:LIVED_IN*0..]->(o:City) RETURN p, l, j, o
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
    Neo4jPHP • PHP wrapperfor the Neo4j REST API • Installable via Composer • Used internally at Graph Story • Used in this presentation • Well tested • https://packagist.org/packages/everyman/ neo4jphp
  • 37.
    Also see: NeoClient •Written by Neoxygen • Alternative PHP wrapper for the Neo4j REST API • Installable via Composer • Accepted for internal use at Graph Story • Well tested • https://packagist.org/packages/neoxygen/neoclient
  • 38.
    Connecting $neo4jClient = newEverymanNeo4jClient( ‘yourgraph.example.com’, 7473 ); $neo4jClient->getTransport() ->setAuth('username', 'password') ->getTransport()->useHttps();
  • 39.
    Creating a Nodeand Label $node = new Node($neo4jClient); $label = $neo4jClient->makeLabel('Person'); $node->setProperty('name', ‘Jeremy Kendall'); $node->save()->addLabels(array($label));
  • 40.
    Searching // Searching fora label by property $label = $neo4jClient->makeLabel('Person'); $nodes = $label->getNodes('name', $name);
  • 41.
    Querying (Cypher) $queryString = 'MATCH(p:Person { name: { name }}) RETURN p'; $query = new EverymanNeo4jCypherQuery( $neo4jClient, $queryString, ['name' => ‘Jeremy Kendall'] ); $result = $query->getResultSet();
  • 42.
  • 43.
    Named Parameters $queryString = 'MATCH(p:Person { name: { name }}) RETURN p'; $query = new EverymanNeo4jCypherQuery( $neo4jClient, $queryString, ['name' => ‘Jeremy Kendall'] ); $result = $query->getResultSet();
  • 44.
    Named Parameters $queryString = 'MATCH(p:Person { name: { name }}) RETURN p'; $query = new EverymanNeo4jCypherQuery( $neo4jClient, $queryString, ['name' => ‘Jeremy Kendall'] ); $result = $query->getResultSet();
  • 45.
    Content Modeling: News Feeds GraphKit for PHP https://github.com/GraphStory/graph-kit-php
  • 46.
    News Feed • Modeledas a list of posts • Newest post first • All subsequent posts follow • Relationships: LASTPOST and NEXTPOST
  • 47.
  • 48.
  • 49.
    The Content Model classContent { public $node; public $nodeId; public $contentId; public $title; public $url; public $tagstr; public $timestamp; public $userNameForPost; public $owner = false; }
  • 50.
    Adding Content public staticfunction add($username, Content $content) { $queryString =<<<CYPHER MATCH (user { username: {u}}) OPTIONAL MATCH (user)-[r:LASTPOST]->(lastpost) DELETE r CREATE (user)-[:LASTPOST]->(p:Content { title:{title}, url:{url}, tagstr:{tagstr}, timestamp:{timestamp}, contentId: {contentId} }) WITH p, collect(lastpost) as lastposts FOREACH (x IN lastposts | CREATE p-[:NEXTPOST]->x) RETURN p, {u} as username, true as owner CYPHER; $query = new Query( Neo4jClient::client(), $queryString, array( 'u' => $username, 'title' => $content->title, 'url' => $content->url, 'tagstr' => $content->tagstr, 'timestamp' => time(), 'contentId' => uniqid() ) ); $result = $query->getResultSet(); return self::returnMappedContent($result); }
  • 51.
    Adding Content public staticfunction add($username, Content $content) { $queryString =<<<CYPHER MATCH (user { username: {u}}) OPTIONAL MATCH (user)-[r:LASTPOST]->(lastpost) DELETE r CREATE (user)-[:LASTPOST]->(p:Content { title:{title}, url:{url}, tagstr:{tagstr}, timestamp:{timestamp}, contentId: {contentId} }) WITH p, collect(lastpost) as lastposts FOREACH (x IN lastposts | CREATE p-[:NEXTPOST]->x) RETURN p, {u} as username, true as owner CYPHER; $query = new Query( Neo4jClient::client(), $queryString, array( 'u' => $username, 'title' => $content->title, 'url' => $content->url, 'tagstr' => $content->tagstr, 'timestamp' => time(), 'contentId' => uniqid() ) ); $result = $query->getResultSet(); return self::returnMappedContent($result); }
  • 52.
    Adding Content public staticfunction add($username, Content $content) { $queryString =<<<CYPHER MATCH (user { username: {u}}) OPTIONAL MATCH (user)-[r:LASTPOST]->(lastpost) DELETE r CREATE (user)-[:LASTPOST]->(p:Content { title:{title}, url:{url}, tagstr:{tagstr}, timestamp:{timestamp}, contentId: {contentId} }) WITH p, collect(lastpost) as lastposts FOREACH (x IN lastposts | CREATE p-[:NEXTPOST]->x) RETURN p, {u} as username, true as owner CYPHER; $query = new Query( Neo4jClient::client(), $queryString, array( 'u' => $username, 'title' => $content->title, 'url' => $content->url, 'tagstr' => $content->tagstr, 'timestamp' => time(), 'contentId' => uniqid() ) ); $result = $query->getResultSet(); return self::returnMappedContent($result); }
  • 53.
    Adding Content MATCH (user{ username: {u}}) OPTIONAL MATCH (user)-[r:LASTPOST]->(lastpost) DELETE r CREATE (user)-[:LASTPOST]->(p:Content { title:{title}, url: {url}, tagstr:{tagstr}, timestamp:{timestamp}, contentId: {contentId} }) WITH p, collect(lastpost) as lastposts FOREACH (x IN lastposts | CREATE p-[:NEXTPOST]->x) RETURN p, {u} as username, true as owner
  • 54.
    Adding Content MATCH (user{ username: {u}}) OPTIONAL MATCH (user)-[r:LASTPOST]->(lastpost) DELETE r CREATE (user)-[:LASTPOST]->(p:Content { title:{title}, url: {url}, tagstr:{tagstr}, timestamp:{timestamp}, contentId: {contentId} }) WITH p, collect(lastpost) as lastposts FOREACH (x IN lastposts | CREATE p-[:NEXTPOST]->x) RETURN p, {u} as username, true as owner
  • 55.
    Adding Content MATCH (user{ username: {u}}) OPTIONAL MATCH (user)-[r:LASTPOST]->(lastpost) DELETE r CREATE (user)-[:LASTPOST]->(p:Content { title:{title}, url: {url}, tagstr:{tagstr}, timestamp:{timestamp}, contentId: {contentId} }) WITH p, collect(lastpost) as lastposts FOREACH (x IN lastposts | CREATE p-[:NEXTPOST]->x) RETURN p, {u} as username, true as owner
  • 56.
    Adding Content MATCH (user{ username: {u}}) OPTIONAL MATCH (user)-[r:LASTPOST]->(lastpost) DELETE r CREATE (user)-[:LASTPOST]->(p:Content { title:{title}, url: {url}, tagstr:{tagstr}, timestamp:{timestamp}, contentId: {contentId} }) WITH p, collect(lastpost) as lastposts FOREACH (x IN lastposts | CREATE p-[:NEXTPOST]->x) RETURN p, {u} as username, true as owner
  • 57.
    Adding Content MATCH (user{ username: {u}}) OPTIONAL MATCH (user)-[r:LASTPOST]->(lastpost) DELETE r CREATE (user)-[:LASTPOST]->(p:Content { title:{title}, url: {url}, tagstr:{tagstr}, timestamp:{timestamp}, contentId: {contentId} }) WITH p, collect(lastpost) as lastposts FOREACH (x IN lastposts | CREATE p-[:NEXTPOST]->x) RETURN p, {u} as username, true as owner
  • 58.
    Adding Content MATCH (user{ username: {u}}) OPTIONAL MATCH (user)-[r:LASTPOST]->(lastpost) DELETE r CREATE (user)-[:LASTPOST]->(p:Content { title:{title}, url: {url}, tagstr:{tagstr}, timestamp:{timestamp}, contentId: {contentId} }) WITH p, collect(lastpost) as lastposts FOREACH (x IN lastposts | CREATE p-[:NEXTPOST]->x) RETURN p, {u} as username, true as owner
  • 59.
    Adding Content $query =new Query( $neo4jClient, $queryString, array( 'u' => $username, 'title' => $content->title, 'url' => $content->url, 'tagstr' => $content->tagstr, 'timestamp' => time(), 'contentId' => uniqid() ) ); $result = $query->getResultSet();
  • 60.
    Retrieving Content public staticfunction getContent($username, $skip) { $queryString = <<<CYPHER MATCH (u:User { username: { u }})-[:FOLLOWS*0..1]->f WITH DISTINCT f, u MATCH f-[:LASTPOST]-lp-[:NEXTPOST*0..]-p RETURN p, f.username as username, f = u as owner ORDER BY p.timestamp desc SKIP { skip } LIMIT 4 CYPHER; $query = new Query( Neo4jClient::client(), $queryString, array( 'u' => $username, 'skip' => $skip, ) ); $result = $query->getResultSet(); return self::returnMappedContent($result); }
  • 61.
    Retrieving Content MATCH (u:User{ username: { u }})-[:FOLLOWS*0..1]->f WITH DISTINCT f, u MATCH f-[:LASTPOST]-lp-[:NEXTPOST*0..]-p RETURN p, f.username as username, f = u as owner ORDER BY p.timestamp desc SKIP { skip } LIMIT 4
  • 62.
    Retrieving Content MATCH (u:User{ username: { u }})-[:FOLLOWS*0..1]->f WITH DISTINCT f, u MATCH f-[:LASTPOST]-lp-[:NEXTPOST*0..]-p RETURN p, f.username as username, f = u as owner ORDER BY p.timestamp desc SKIP { skip } LIMIT 4
  • 63.
    Retrieving Content MATCH (u:User{ username: { u }})-[:FOLLOWS*0..1]->f WITH DISTINCT f, u MATCH f-[:LASTPOST]-lp-[:NEXTPOST*0..]-p RETURN p, f.username as username, f = u as owner ORDER BY p.timestamp desc SKIP { skip } LIMIT 4
  • 64.
    Retrieving Content MATCH (u:User{ username: { u }})-[:FOLLOWS*0..1]->f WITH DISTINCT f, u MATCH f-[:LASTPOST]-lp-[:NEXTPOST*0..]-p RETURN p, f.username as username, f = u as owner ORDER BY p.timestamp desc SKIP { skip } LIMIT 4
  • 65.
    Retrieving Content MATCH (u:User{ username: { u }})-[:FOLLOWS*0..1]->f WITH DISTINCT f, u MATCH f-[:LASTPOST]-lp-[:NEXTPOST*0..]-p RETURN p, f.username as username, f = u as owner ORDER BY p.timestamp desc SKIP { skip } LIMIT 4
  • 66.
    Retrieving Content MATCH (u:User{ username: { u }})-[:FOLLOWS*0..1]->f WITH DISTINCT f, u MATCH f-[:LASTPOST]-lp-[:NEXTPOST*0..]-p RETURN p, f.username as username, f = u as owner ORDER BY p.timestamp desc SKIP { skip } LIMIT 4
  • 67.
    Editing Content public staticfunction edit(Content $content) { $updatedAt = time(); $node = $content->node; $node->setProperty('title', $content->title); $node->setProperty('url', $content->url); $node->setProperty('tagstr', $content->tagstr); $node->setProperty('updated', $updatedAt); $node->save(); $content->updated = $updatedAt; return $content; }
  • 68.
    Editing Content public staticfunction edit(Content $content) { $updatedAt = time(); $node = $content->node; $node->setProperty('title', $content->title); $node->setProperty('url', $content->url); $node->setProperty('tagstr', $content->tagstr); $node->setProperty('updated', $updatedAt); $node->save(); $content->updated = $updatedAt; return $content; }
  • 69.
    Editing Content public staticfunction edit(Content $content) { $updatedAt = time(); $node = $content->node; $node->setProperty('title', $content->title); $node->setProperty('url', $content->url); $node->setProperty('tagstr', $content->tagstr); $node->setProperty('updated', $updatedAt); $node->save(); $content->updated = $updatedAt; return $content; }
  • 70.
    Editing Content public staticfunction edit(Content $content) { $updatedAt = time(); $node = $content->node; $node->setProperty('title', $content->title); $node->setProperty('url', $content->url); $node->setProperty('tagstr', $content->tagstr); $node->setProperty('updated', $updatedAt); $node->save(); $content->updated = $updatedAt; return $content; }
  • 71.
    Editing Content public staticfunction edit(Content $content) { $updatedAt = time(); $node = $content->node; $node->setProperty('title', $content->title); $node->setProperty('url', $content->url); $node->setProperty('tagstr', $content->tagstr); $node->setProperty('updated', $updatedAt); $node->save(); $content->updated = $updatedAt; return $content; }
  • 72.
    Deleting Content public staticfunction delete($username, $contentId) { $queryString = self::getDeleteQueryString( $username, $contentId ); $params = array( 'username' => $username, 'contentId' => $contentId, ); $query = new Query( $neo4jClient, $queryString, $params ); $query->getResultSet(); }
  • 73.
    Deleting Content public staticfunction delete($username, $contentId) { $queryString = self::getDeleteQueryString( $username, $contentId ); $params = array( 'username' => $username, 'contentId' => $contentId, ); $query = new Query( $neo4jClient, $queryString, $params ); $query->getResultSet(); }
  • 74.
    Deleting Content public staticfunction delete($username, $contentId) { $queryString = self::getDeleteQueryString( $username, $contentId ); $params = array( 'username' => $username, 'contentId' => $contentId, ); $query = new Query( $neo4jClient, $queryString, $params ); $query->getResultSet(); }
  • 75.
    Deleting Content public staticfunction delete($username, $contentId) { $queryString = self::getDeleteQueryString( $username, $contentId ); $params = array( 'username' => $username, 'contentId' => $contentId, ); $query = new Query( $neo4jClient, $queryString, $params ); $query->getResultSet(); }
  • 76.
    Deleting Content: Leaf //If leaf MATCH (u:User { username: { username }})-[:LASTPOST| NEXTPOST*0..]->(c:Content { contentId: { contentId }}) WITH c MATCH (c)-[r]-() DELETE c, r
  • 77.
    Deleting Content: Leaf //If leaf MATCH (u:User { username: { username }})-[:LASTPOST| NEXTPOST*0..]->(c:Content { contentId: { contentId }}) WITH c MATCH (c)-[r]-() DELETE c, r
  • 78.
    Deleting Content: Leaf //If leaf MATCH (u:User { username: { username }})-[:LASTPOST| NEXTPOST*0..]->(c:Content { contentId: { contentId }}) WITH c MATCH (c)-[r]-() DELETE c, r
  • 79.
    Deleting Content: Leaf //If leaf MATCH (u:User { username: { username }})-[:LASTPOST| NEXTPOST*0..]->(c:Content { contentId: { contentId }}) WITH c MATCH (c)-[r]-() DELETE c, r
  • 80.
    Deleting Content: Leaf //If leaf MATCH (u:User { username: { username }})-[:LASTPOST| NEXTPOST*0..]->(c:Content { contentId: { contentId }}) WITH c MATCH (c)-[r]-() DELETE c, r
  • 81.
    Deleting Content: LASTPOST //If last MATCH (u:User { username: { username }})-[lp:LASTPOST]- >(del:Content { contentId: { contentId }})-[np:NEXTPOST]- >(nextPost) CREATE UNIQUE (u)-[:LASTPOST]->(nextPost) DELETE lp, del, np
  • 82.
    Deleting Content: LASTPOST //If last MATCH (u:User { username: { username }})-[lp:LASTPOST]- >(del:Content { contentId: { contentId }})-[np:NEXTPOST]- >(nextPost) CREATE UNIQUE (u)-[:LASTPOST]->(nextPost) DELETE lp, del, np
  • 83.
    Deleting Content: LASTPOST //If last MATCH (u:User { username: { username }})-[lp:LASTPOST]- >(del:Content { contentId: { contentId }})-[np:NEXTPOST]- >(nextPost) CREATE UNIQUE (u)-[:LASTPOST]->(nextPost) DELETE lp, del, np
  • 84.
    Deleting Content: LASTPOST //If last MATCH (u:User { username: { username }})-[lp:LASTPOST]- >(del:Content { contentId: { contentId }})-[np:NEXTPOST]- >(nextPost) CREATE UNIQUE (u)-[:LASTPOST]->(nextPost) DELETE lp, del, np
  • 85.
    Deleting Content: Other //All other MATCH (u:User { username: { username }})-[:LASTPOST| NEXTPOST*0..]->(before), (before)-[delBefore]->(del:Content { contentId: { contentId }})-[delAfter]->(after) CREATE UNIQUE (before)-[:NEXTPOST]->(after) DELETE del, delBefore, delAfter
  • 86.
    Deleting Content: Other //All other MATCH (u:User { username: { username }})-[:LASTPOST| NEXTPOST*0..]->(before), (before)-[delBefore]->(del:Content { contentId: { contentId }})-[delAfter]->(after) CREATE UNIQUE (before)-[:NEXTPOST]->(after) DELETE del, delBefore, delAfter
  • 87.
    Deleting Content: Other //All other MATCH (u:User { username: { username }})-[:LASTPOST| NEXTPOST*0..]->(before), (before)-[delBefore]->(del:Content { contentId: { contentId }})-[delAfter]->(after) CREATE UNIQUE (before)-[:NEXTPOST]->(after) DELETE del, delBefore, delAfter
  • 88.
    Deleting Content: Other //All other MATCH (u:User { username: { username }})-[:LASTPOST| NEXTPOST*0..]->(before), (before)-[delBefore]->(del:Content { contentId: { contentId }})-[delAfter]->(after) CREATE UNIQUE (before)-[:NEXTPOST]->(after) DELETE del, delBefore, delAfter
  • 89.
    Deleting Content: Other //All other MATCH (u:User { username: { username }})-[:LASTPOST| NEXTPOST*0..]->(before), (before)-[delBefore]->(del:Content { contentId: { contentId }})-[delAfter]->(after) CREATE UNIQUE (before)-[:NEXTPOST]->(after) DELETE del, delBefore, delAfter
  • 90.
  • 91.