neo4j
グラフデータベース

グラフDB - Neo4j のレファレンスをまとめる

この記事は

グラフDBをいじってみたくなったので、Neo4jを触ろうとした際の備忘録である。
レファレンスを読んでみた感じ面白そう。特に独自のQuery LanguageであるCypherはとても興味深い。
公式に載ってることしか書いてないので、そっちを読みましょう。
https://neo4j.com/docs/developer-manual/current/

必要となる概念

グラフDBはグラフ理論に基づいたデータ構造を扱うためのデータベースである。
そのため取り扱われる概念も基本的にグラフ理論によって導入されたものがほとんどである。
いくつかの概念はRDBにて用いられているものに呼び変えられているが、RDBを知らなくても(そんな人がグラフDBを扱おうとは思わないだろうが)十分理解ができる。

  • Node
    • Entityのこと。LabelとPropertyを持つ
  • Relationship
    • Node間(自己参照も含む)を結ぶ線のこと
  • Label
    • Entityの名前
  • Property
    • Entityの属性
    • 取りうる値
      • Numeric(integer/float)
      • Boolean
      • String
      • List(上記三種の)
    • Null(Property値ではなく、Propertyに値が入っていない状態を指す)
  • Paths
    • Nodeと(optionalで)Relationの単位
    • Relationの数がPathのLengthとなる。
      • 自己参照型のNodeであればLength=1
      • Node単体であればLength=0

CypherのSummary

Neo4j独自のQuery Language。影響を受けている言語は下記とのこと。

  • where(制約)とorderby(ソート)はSQLから
  • regex(パターンマッチ)はSPARQLから
  • list周りのsemanticsはHaskel/Pythonから

基本的なSyntaxは下記。

  • Node
    • (hoge)
  • Relationship

    • (a)--(b)
      • 無向性
    • (a)->(b)
      • 有向性
    • (a)-[rel]-(b)
      • 特定のrelationshipを指定する場合
    • (a)-[*..5]-(b)
      • a/b間でlengthが5以下のpathsを返す
  • Match

    • DB内の起点となるNode群を指定する
    • Where句やReturn句で使いたいNode群にはこのタイミングで参照を取る必要がある
  • With

    • Match句で取得した情報をどのように取得したいかを指定する
  • Where

    • Match句で取得したNode群に絞込を掛ける
  • Return

    • 実際にDBから取得したいProperty
  • Create[Delete]

    • Node/Relationを作成削除する
  • Set[Remove]

    • Label/Propertyを作成更新削除する
  • Merge

    • Node/PatternをRDBで言うところのUpsertする
      • PatternはMatch句で取得したNode群を指す
// Johnの友達の友達(friend of friend)の一覧を取得する// MATCH句の()内にLabelとその制約を与えるイメージ
MATCH (john {name: 'John'})-[:friend]->()-[:friend]->(fof)
RETURN john.name, fof.name
// 指定のnameを持つNodeがfollowしている、nameが'S'から始まるNodeの一覧を取得する
MATCH (user)-[:friend]->(follower)
WHERE user.name IN['Joe', 'John', 'Sara', 'Maria', 'Steve'] AND follower.name =~ 'S.*'
RETURN user.name, follower.name
// 友達が4人以上いるJohnを返す
MATCH (n {name: 'John'})-[:frined]-(friend)
WITH n, count(friend) AS friendsCount
WHERE friendsCount > 3
RETURN n, friendsCount
// 友達の数をJohnのfriendsCount propertyにSETし、それを返す
MATCH (n {name: 'John'})-[:friend]-(friend)
WITH n, count(friend) AS friendsCount
SET n.friendsCount = friendsCount
RETURN n.friendsCount
// User Entityを3つ作成、それらをfriend relationshipで結ぶ
CREATE (adam:User {name: 'Adam'}), (pernilla:User {name: 'Pernilla'}), (david:User {name: 'David')},
(adam)-[:friend]->(perniilla), (pernilla)-[:friend]->(david)
// User:Adamの友達の友達(David)を返す
MATCH (user:User {name: 'Adam'})-[r1:friend]-()-[r2:friend]-(fof)
RETURN fof.name AS fofName
// Case 
MATCH (n)
RETURN
CASE n.eyes
WHEN 'blue' THEN 1
WHEN 'brown' THEN 2
ELSE 3 END AS result

Parameterを利用したQuery

// parameter
{
  "name" : "Johan"
}

// query
MATCH (n:Person)
WHERE n.name = $name
RETURN n
// parameter
{
  "regex" : ".*h.*"
}

// query
MATCH (n:Person)
WHERE n.name =~ $regex
RETURN n.name
// parameter
{
  "props" : {
    "name" : "Andres",
    "position" : "Developer"
  }
}

// query
CREATE ($props)

List型

RETURN [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
//=> [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

RETURN range(0, 10)[3]
//=> 3

RETURN range(0, 10)[0..3]
//=> [0, 1, 2]

Map型

RETURN { key: 'Value', listKey: [{ inner: 'Map1' }, { inner: 'Map2' }]}
//=> {listKey -> [{inner -> "Map1"},{inner -> "Map2"}], key -> "Value"}

MATCH (actor:Person)-[:ACTED_IN]->(movie:Movie)
WITH actor, count(movie) AS nrOfMovies
RETURN actor { .name, nrOfMovies }
//=> {name -> "Martin Sheen", nrOfMovies -> 2}

Match Statement

Aggregate元の指定。SQLではFromに該当する。

// Oliver Stoneと隣接する全てのmovie
MATCH (director { name: 'Oliver Stone' })--(movie)
RETURN movie.title
// Person labelのOliver Stoneと隣接する全てのMovie labelを持つnodeを返す
MATCH (:Person { name: 'Oliver Stone' })--(movie:Movie)
RETURN movie.title
// Movie LabelのWall Streetにacted_inなnodeを返す
MATCH (wallstreet:Movie { title: 'Wall Street' })<-[:acted_in]-(actor)
RETURN actor.name
// wallstreetにacted_inまたはdirectedなnodeを返す
MATCH (wallstreet { title: 'Wall Street' })<-[:acted_in|:directed]-(person)
RETURN person.name
// 特定のProperty値を持つ経路を返す
MATCH p =(charlie:Person)-[* { blocked:false }]-(martin:Person)
WHERE charlie.name = 'Charlie Sheen' AND martin.name = 'Martin Sheen'
RETURN p
//=> [Node[0]{name:"Charlie Sheen"},:X[20]{blocked:false},Node[20]{},:X[21]{blocked:false},Node[1]{name:"Martin Sheen"}]
// MartinとOliverとの最短経路を返す
MATCH (martin:Person { name: 'Martin Sheen' }),(oliver:Person { name: 'Oliver Stone' }), p = shortestPath((martin)-[*..15]-(oliver))
RETURN p
//=> [Node[1]{name:"Martin Sheen"},:ACTED_IN[1]{role:"Carl Fox"},Node[5]{title:"Wall Street"},:DIRECTED[3]{},Node[3]{name:"Oliver Stone"}]

Optional Match Statement

Matchと異なり、一致するパターンが見つからなかった時にnullを返す。
SQLで言うところの、Outer JoinのようなStatement。

MATCH (a:Movie { title: 'Wall Street' })
OPTIONAL MATCH (a)-->(x)
RETURN x

Return Statement

実際に返す値を指定する。SQLで言うところのSelect。

MATCH (n { name: 'A' })-[r:KNOWS]->(c)
RETURN r

With Statement

MatchでAggregateしたものに集約関数等を用いる際には、With Statementを用いる。

MATCH (david { name: 'David' })--(otherPerson)-->()
WITH otherPerson, count(*) AS foaf
WHERE foaf > 1
RETURN otherPerson.name
MATCH (n)
WITH n
ORDER BY n.name DESC LIMIT 3
RETURN collect(n.name)

Unwind Statement

Listをrowとして変形する。

UNWIND [1, 2, 3] AS x
RETURN x
// Parameter
{
  "events" : [ {
    "year" : 2014,
    "id" : 1
  }, {
    "year" : 2014,
    "id" : 2
  } ]
}

// Query
UNWIND $events AS event
MERGE (y:Year { year: event.year })
MERGE (y)<-[:IN]-(e:Event { id: event.id })
RETURN e.id AS x
ORDER BY x

Where Statement

SQLでのWhereと同様の役割を果たす。
AND/OR/XOR/NOT演算子が使える。

MATCH (n)
WHERE n.name = 'Peter' XOR (n.age < 30 AND n.name = 'Tobias') OR NOT (n.name = 'Tobias' OR n.name = 'Peter')
RETURN n.name, n.age

Label型によるfileter

MATCH (n)
WHERE n:Swedish
RETURN n.name, n.age

Propertyの存在有無でfilter

MATCH (n)
WHERE exists(n.belt)
RETURN n.name, n.belt

string matchによるfilter

MATCH (n)
WHERE n.name STARTS WITH 'Pet'
RETURN n.name, n.age

正規表現matchによるfilter

MATCH (n)
WHERE n.name =~ 'Tob.*'
RETURN n.name, n.age

path patternによるfilter

MATCH (tobias { name: 'Tobias' }),(others)
WHERE others.name IN ['Andres', 'Peter'] AND (tobias)<--(others)
RETURN others.name, others.age

Order By Statement

MATCH (n)
RETURN n.name, n.age
ORDER BY n.age, n.name

Create Statement

Nodeの作成

// Label = Person, Property = name/title
CREATE (n:Person { name: 'Andres', title: 'Developer' })

Relationshipの作成

// :RELTYPEというRelationshipをa/b間に作成する
MATCH (a:Person),(b:Person)
WHERE a.name = 'Node A' AND b.name = 'Node B'
CREATE (a)-[r:RELTYPE]->(b)
RETURN r

Pathの作成

CREATE p =(andres { name:'Andres' })-[:WORKS_AT]->(neo)<-[:WORKS_AT]-(michael { name: 'Michael' })
RETURN p

Parameterによる作成

// Parameter
{
  "props" : [ {
    "name" : "Andres",
    "position" : "Developer"
  }, {
    "name" : "Michael",
    "position" : "Developer"
  } ]
}

// Query
UNWIND $props AS map
CREATE (n)
SET n = map

Delete Statement

Nodeの削除

MATCH (n { name: 'Andres' })
DETACH DELETE n

Relationshipの削除

MATCH (n { name: 'Andres' })-[r:KNOWS]->()
DELETE r

Set Statement

Set Property

MATCH (n { name: 'Andres' })
SET n.surname = 'Taylor'
RETURN n

Remove Property

MATCH (n { name: 'Andres' })
SET n.name = NULL RETURN n

Parameterを用いてPropertyを追加

MATCH (peter { name: 'Peter' })
SET peter += { hungry: TRUE , position: 'Entrepreneur' }

Parameterを用いてPropertyを更新

// Parameters
{
  "props" : {
    "name" : "Andres",
    "position" : "Developer"
  }
}

// Query
MATCH (n { name: 'Andres' })
SET n = $props
RETURN n

LabelをSet

MATCH (n { name: 'Stefan' })
SET n:German
RETURN n

Remove Statement

Nodeを削除

MATCH (andres { name: 'Andres' })
REMOVE andres.age
RETURN andres

Labelを削除

MATCH (n { name: 'Peter' })
REMOVE n:German
RETURN n

Foreach Statement

List型を更新する際などに用いる。

MATCH p =(begin)-[*]->(END)
WHERE begin.name = 'A' AND END.name = 'D'
FOREACH (n IN nodes(p)| SET n.marked = TRUE )

Merge Statement

与えた引数をUpsertするイメージ

LabelのMerge

MERGE (robert:Critic)
RETURN robert, labels(robert)

NodeのMerge

MERGE (charlie { name: 'Charlie Sheen', age: 10 })
RETURN charlie

ON CREATEによるフック

MERGE (keanu:Person { name: 'Keanu Reeves' })
ON CREATE SET keanu.created = timestamp()
RETURN keanu.name, keanu.created

ON MATCHによるフック

MERGE (person:Person)
ON MATCH SET person.found = TRUE
RETURN person.name, person.found

Union Statement

Union Allは重複を許す。

MATCH (n:Actor)
RETURN n.name AS name
UNION ALL
MATCH (n:Movie)
RETURN n.title AS name

Unionは重複を許さない。

MATCH (n:Actor)
RETURN n.name AS name
UNION
MATCH (n:Movie)
RETURN n.title AS name

CSV

"1","ABBA","1992"
"2","Roxette","1986"
"3","Europe","1979"
"4","The Cardigans","1992"

LOAD CSV FROM 'https://neo4j.com/docs/developer-manual/3.3/csv/artists.csv' AS line
CREATE (:Artist { name: line[1], year: toInteger(line[2])})
"Id","Name","Year"
"1","ABBA","1992"
"2","Roxette","1986"
"3","Europe","1979"
"4","The Cardigans","1992"

LOAD CSV WITH HEADERS FROM 'https://neo4j.com/docs/developer-manual/3.3/csv/artists-with-headers.csv' AS line
CREATE (:Artist { name: line.Name, year: toInteger(line.Year)})

Index

single-property index

CREATE INDEX ON :Person(firstname)

composite-property index

CREATE INDEX ON :Person(firstname, surname)

Constraints

CREATE CONSTRAINT ON (book:Book) ASSERT book.isbn IS UNIQUE
DROP CONSTRAINT ON (book:Book) ASSERT book.isbn IS UNIQUE

Driver

JSのクライアントが公式で用意されている。

npm install neo4j-driver

実行計画やクエリチューニングに関しても記載があったが、今回はスキップということで。
とりあえずサンプルアプリを作ってみて感想を別途書きたい所存。