Leveraging the Power of Graph Databases in PHP

91
Leveraging the Power of Graph Databases in PHP Jeremy Kendall Atlanta PHP April 2015

Transcript of Leveraging the Power of Graph Databases in PHP

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 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

Neo4j + Cypher

Cypher

• Neo4j’s declarative query language

• Easy to pick up

• Some clauses and concepts familiar from SQL

Simple Example

Goal

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, lMATCH (p)-[:WORKS_AT]->(j)WITH p, l, jMATCH (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, lMATCH (p)-[:WORKS_AT]->(j)WITH p, l, jMATCH (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, lMATCH (p)-[:WORKS_AT]->(j)WITH p, l, jMATCH (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, lMATCH (p)-[:WORKS_AT]->(j)WITH p, l, jMATCH (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, lMATCH (p)-[:WORKS_AT]->(j)WITH p, l, jMATCH (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, lMATCH (p)-[:WORKS_AT]->(j)WITH p, l, jMATCH (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, lMATCH (p)-[:WORKS_AT]->(j)WITH p, l, jMATCH (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 \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

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();

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 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 =<<<CYPHERMATCH (user { username: {u}})OPTIONAL MATCH (user)-[r:LASTPOST]->(lastpost)DELETE rCREATE (user)-[:LASTPOST]->(p:Content { title:{title}, url:{url}, tagstr:{tagstr}, timestamp:{timestamp}, contentId:{contentId} })WITH p, collect(lastpost) as lastpostsFOREACH (x IN lastposts | CREATE p-[:NEXTPOST]->x)RETURN p, {u} as username, true as ownerCYPHER;

$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 =<<<CYPHERMATCH (user { username: {u}})OPTIONAL MATCH (user)-[r:LASTPOST]->(lastpost)DELETE rCREATE (user)-[:LASTPOST]->(p:Content { title:{title}, url:{url}, tagstr:{tagstr}, timestamp:{timestamp}, contentId:{contentId} })WITH p, collect(lastpost) as lastpostsFOREACH (x IN lastposts | CREATE p-[:NEXTPOST]->x)RETURN p, {u} as username, true as ownerCYPHER;

$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 =<<<CYPHERMATCH (user { username: {u}})OPTIONAL MATCH (user)-[r:LASTPOST]->(lastpost)DELETE rCREATE (user)-[:LASTPOST]->(p:Content { title:{title}, url:{url}, tagstr:{tagstr}, timestamp:{timestamp}, contentId:{contentId} })WITH p, collect(lastpost) as lastpostsFOREACH (x IN lastposts | CREATE p-[:NEXTPOST]->x)RETURN p, {u} as username, true as ownerCYPHER;

$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 rCREATE (user)-[:LASTPOST]->(p:Content { title:{title}, url:{url}, tagstr:{tagstr}, timestamp:{timestamp}, contentId:{contentId} })WITH p, collect(lastpost) as lastpostsFOREACH (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 rCREATE (user)-[:LASTPOST]->(p:Content { title:{title}, url:{url}, tagstr:{tagstr}, timestamp:{timestamp}, contentId:{contentId} })WITH p, collect(lastpost) as lastpostsFOREACH (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 rCREATE (user)-[:LASTPOST]->(p:Content { title:{title}, url:{url}, tagstr:{tagstr}, timestamp:{timestamp}, contentId:{contentId} })WITH p, collect(lastpost) as lastpostsFOREACH (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 rCREATE (user)-[:LASTPOST]->(p:Content { title:{title}, url:{url}, tagstr:{tagstr}, timestamp:{timestamp}, contentId:{contentId} })WITH p, collect(lastpost) as lastpostsFOREACH (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 rCREATE (user)-[:LASTPOST]->(p:Content { title:{title}, url:{url}, tagstr:{tagstr}, timestamp:{timestamp}, contentId:{contentId} })WITH p, collect(lastpost) as lastpostsFOREACH (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 rCREATE (user)-[:LASTPOST]->(p:Content { title:{title}, url:{url}, tagstr:{tagstr}, timestamp:{timestamp}, contentId:{contentId} })WITH p, collect(lastpost) as lastpostsFOREACH (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 = <<<CYPHERMATCH (u:User { username: { u }})-[:FOLLOWS*0..1]->fWITH DISTINCT f, uMATCH f-[:LASTPOST]-lp-[:NEXTPOST*0..]-pRETURN p, f.username as username, f = u as ownerORDER BY p.timestamp desc SKIP { skip } LIMIT 4CYPHER;

$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]->fWITH DISTINCT f, uMATCH f-[:LASTPOST]-lp-[:NEXTPOST*0..]-pRETURN p, f.username as username, f = u as ownerORDER BY p.timestamp desc SKIP { skip } LIMIT 4

Retrieving Content

MATCH (u:User { username: { u }})-[:FOLLOWS*0..1]->fWITH DISTINCT f, uMATCH f-[:LASTPOST]-lp-[:NEXTPOST*0..]-pRETURN p, f.username as username, f = u as ownerORDER BY p.timestamp desc SKIP { skip } LIMIT 4

Retrieving Content

MATCH (u:User { username: { u }})-[:FOLLOWS*0..1]->fWITH DISTINCT f, uMATCH f-[:LASTPOST]-lp-[:NEXTPOST*0..]-pRETURN p, f.username as username, f = u as ownerORDER BY p.timestamp desc SKIP { skip } LIMIT 4

Retrieving Content

MATCH (u:User { username: { u }})-[:FOLLOWS*0..1]->fWITH DISTINCT f, uMATCH f-[:LASTPOST]-lp-[:NEXTPOST*0..]-pRETURN p, f.username as username, f = u as ownerORDER BY p.timestamp desc SKIP { skip } LIMIT 4

Retrieving Content

MATCH (u:User { username: { u }})-[:FOLLOWS*0..1]->fWITH DISTINCT f, uMATCH f-[:LASTPOST]-lp-[:NEXTPOST*0..]-pRETURN p, f.username as username, f = u as ownerORDER BY p.timestamp desc SKIP { skip } LIMIT 4

Retrieving Content

MATCH (u:User { username: { u }})-[:FOLLOWS*0..1]->fWITH DISTINCT f, uMATCH f-[:LASTPOST]-lp-[:NEXTPOST*0..]-pRETURN p, f.username as username, f = u as ownerORDER 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 leafMATCH (u:User { username: { username }})-[:LASTPOST|NEXTPOST*0..]->(c:Content { contentId: { contentId }})WITH cMATCH (c)-[r]-()DELETE c, r

Deleting Content: Leaf

// If leafMATCH (u:User { username: { username }})-[:LASTPOST|NEXTPOST*0..]->(c:Content { contentId: { contentId }})WITH cMATCH (c)-[r]-()DELETE c, r

Deleting Content: Leaf

// If leafMATCH (u:User { username: { username }})-[:LASTPOST|NEXTPOST*0..]->(c:Content { contentId: { contentId }})WITH cMATCH (c)-[r]-()DELETE c, r

Deleting Content: Leaf

// If leafMATCH (u:User { username: { username }})-[:LASTPOST|NEXTPOST*0..]->(c:Content { contentId: { contentId }})WITH cMATCH (c)-[r]-()DELETE c, r

Deleting Content: Leaf

// If leafMATCH (u:User { username: { username }})-[:LASTPOST|NEXTPOST*0..]->(c:Content { contentId: { contentId }})WITH cMATCH (c)-[r]-()DELETE c, r

Deleting Content: LASTPOST

// If lastMATCH (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 lastMATCH (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 lastMATCH (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 lastMATCH (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 otherMATCH (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 otherMATCH (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 otherMATCH (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 otherMATCH (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 otherMATCH (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!

[email protected] @JeremyKendall

http://www.graphstory.com