leveraging the power of graph databases in php
TRANSCRIPT
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
Graph Databases• Data Model!
• Nodes with properties
• Typed relationships
• Strengths!
• Highly connected data
• ACID
• Weaknesses!
• Paradigm shift
Graph Databases• Data Model!
• Nodes with properties
• Typed relationships
• Strengths!
• Highly connected data
• ACID
• Weaknesses!
• Paradigm shift
• Examples!
• Neo4j, Titan, OrientDB
Why Care?
• Relationships have 1st class status
• Just as important as the objects connecting them
• You can have properties & labels
Why Care?
• Relationships have 1st class status
• Just as important as the objects connecting them
• You can have properties & labels
• Multiple relationships
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 SpeedDepth 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
Cypher
• Neo4j’s declarative query language
• Easy to pick up
• Some clauses and concepts familiar from SQL
Create Some NodesCREATE (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
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
• Under review for internal use at Graph Story
• Well tested
• https://packagist.org/packages/neoxygen/neoclient
Connecting$neo4jClient = new \Everyman\Neo4j\Client(! ‘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 \Everyman\Neo4j\Cypher\Query(! $neo4jClient,! $queryString,! ['name' => ‘Jeremy Kendall']!);!!$result = $query->getResultSet();
Named Parameters
$queryString = ! 'MATCH (p:Person { name: { name }}) RETURN p';!!$query = new \Everyman\Neo4j\Cypher\Query(! $neo4jClient,! $queryString,! ['name' => ‘Jeremy Kendall']!);!!$result = $query->getResultSet();
Named Parameters
$queryString = ! 'MATCH (p:Person { name: { name }}) RETURN p';!!$query = new \Everyman\Neo4j\Cypher\Query(! $neo4jClient,! $queryString,! ['name' => ‘Jeremy Kendall']!);!!$result = $query->getResultSet();
News Feed
• Modeled as a list of posts
• Newest post first
• All subsequent posts follow
• Relationships: LASTPOST and NEXTPOST
The Content Modelclass Content!{! public $node;! public $nodeId;! public $contentId;! public $title;! public $url;! public $tagstr;! public $timestamp;! public $userNameForPost;! public $owner = false;!}
Adding Contentpublic 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 Contentpublic 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 Contentpublic 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 Contentpublic 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 Contentpublic 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 Contentpublic 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 Contentpublic 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 Contentpublic 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 Contentpublic 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 Contentpublic 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 Contentpublic 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 Contentpublic 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 Contentpublic 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