19. Why Care?
• Relationships have 1st class status
• Just as important as the objects connecting them
• You can have properties & labels
20. Why Care?
• Relationships have 1st class status
• Just as important as the objects connecting them
• You can have properties & labels
• Multiple relationships
22. 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
23. 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
33. 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
34. 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
35. 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
36. 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
37. 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
38. 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
39. 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
42. 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
43. Also see: NeoClient
• Written by Neoxygen
• Alternative PHP wrapper for the Neo4j REST API
• Installable via Composer
• Under review for internal use at Graph Story
• Well tested
• https://packagist.org/packages/neoxygen/neoclient
55. The Content Model
class Content!
{!
public $node;!
public $nodeId;!
public $contentId;!
public $title;!
public $url;!
public $tagstr;!
public $timestamp;!
public $userNameForPost;!
public $owner = false;!
}
56. 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);!
}
57. 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);!
}
58. 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);!
}
59. 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
60. 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
61. 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
62. 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
63. 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
64. 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
66. 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);!
}
67. 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
68. 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
69. 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
70. 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
71. 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
72. 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