This document provides an overview of leveraging graph databases in PHP. It begins with an introduction to graph databases and their data model. It then discusses Neo4j, a popular graph database, and its query language Cypher. The document demonstrates connecting to Neo4j from PHP, creating and querying nodes and relationships, and provides an example of modeling content like a news feed as a graph using the LASTPOST and NEXTPOST relationships to link content in order.
7. Graph Databases
• Data Model
• Nodes with properties
• Typed relationships
• Strengths
• Highly connected data
• ACID
• Weaknesses
• Paradigm shift
• Examples
• Neo4j, Titan, OrientDB
8. Why Care?
• All the NoSQL Joy
• Schema-less
• Semi-structured data
• Escape from JOIN Hell
• Speed
9. Why Care?
• Relationships have 1st class status
• Just as important as the objects they connect
• You can have properties & labels
• Multiple relationships
11. 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
12. 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
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
36. 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
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
49. The Content Model
class Content
{
public $node;
public $nodeId;
public $contentId;
public $title;
public $url;
public $tagstr;
public $timestamp;
public $userNameForPost;
public $owner = false;
}
50. 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);
}
51. 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);
}
52. 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);
}
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
60. 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);
}
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
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