エンジニアのソフトウェア的愛情

または私は如何にして心配するのを止めてプログラムを・愛する・ようになったか

Neo4j と Cypher に手を出す

仕事でグラフデータベースが利用される案件が目につくようになってきました。

というわけで。Neo4j に手を出してみた顛末の記録です。

ちなみに。プラットフォームは OS X El Capitan です。

Neo4j のインストー

横着して Homebrew でインストールしました。

$ brew install neo4j
==> Downloading http://dist.neo4j.org/neo4j-community-2.2.5-unix.tar.gz
######################################################################## 100.0%t
🍺  /usr/local/Cellar/neo4j/2.2.5: 92 files, 60M, built in 77 seconds
$ which neo4j
/usr/local/bin/neo4j

無事インストールできました。

サーバを起動する

$ neo4j start
Starting Neo4j Server...WARNING: not changing user
process [87422]... waiting for server to be ready..... OK.
http://localhost:7474/ is ready.

ブラウザで http://localhost:7474/ にアクセスすると、最初にログインが求められます。
表示されたページにパスワードの初期値が書かれていますのでそれでログインします。ログインしたらパスワード変更ページが表示されるのでパスワードを変更します。
これでウェブ管理ツールが使えるようになりました。


…なのですが。ここから先はウェブ管理ツールは使わずコマンドラインツールの neo4j-shell を使っていきます。


起動する。

$ neo4j-shell
Welcome to the Neo4j Shell! Enter 'help' for a list of commands
NOTE: Remote Neo4j graph database service 'shell' at port 1337

neo4j-sh (?)$

起動しました。

CRUD

Neo4j には Cypher というクエリ言語が用意されています。Neo4j Shell 上で Cypher を書いて CRUD を試みていきます。

ノードを作る

Neo4j のデータはノード (nodes) と関係 (relationships) から作られています。グラフ理論で言うところのノードとエッジです。Neo4j のグラフは有向グラフなので関係には方向があります。


まずノードの作成です。

$ CREATE (n:Writer {name: 'Eric'});
+-------------------+
| No data returned. |
+-------------------+
Nodes created: 1
Properties set: 1
Labels added: 1
24 ms

これでプロパティとして name (値は Eric)を持ち Writer というラベルがついたノードが作成できました。
が、値が返されていないので本当に作られているのかよくわかりません。
RETURN 節をつけて値を返すようにしてみます。

$ CREATE (n:Writer {name: 'Jim'}) RETURN n;
+----------------------+
| n                    |
+----------------------+
| Node[12]{name:"Jim"} |
+----------------------+
1 row
Nodes created: 1
Properties set: 1
Labels added: 1
34 ms

RETURN n としたことで n の値、つまり作成されたノードの値が表示されています。


別のラベルをつけたノードを作成してみます。

$ CREATE (n:Book {name: 'Seven Databases in Seven Weeks'}) RETURN n;
+-------------------------------------------------+
| n                                               |
+-------------------------------------------------+
| Node[13]{name:"Seven Databases in Seven Weeks"} |
+-------------------------------------------------+
1 row
Nodes created: 1
Properties set: 1
Labels added: 1
44 ms
ノードを取得する

ラベル Writer がついたノードを取得してみます。

$ MATCH (n:Writer) RETURN n;
+-----------------------+
| n                     |
+-----------------------+
| Node[11]{name:"Eric"} |
| Node[12]{name:"Jim"}  |
+-----------------------+
2 rows
59 ms

先ほど登録した2つのノードが表示されました。Node の後に表示されている数字は id ですのでデータベースの状態によって異なります。


次に name の値が Eric のノードを取得します。

$ MATCH (n {name: 'Eric'}) RETURN n;
+-----------------------+
| n                     |
+-----------------------+
| Node[11]{name:"Eric"} |
+-----------------------+
1 row
58 ms

この例ではノードのラベルを指定していません。このため他に {name: 'Eric'} というノードがあったばあいはラベルに関係なく取得されます。

WHERE 節を使ってもプロパティを指定することができます。

$ MATCH (n) WHERE n.name = 'Eric' RETURN n;
+-----------------------+
| n                     |
+-----------------------+
| Node[11]{name:"Eric"} |
+-----------------------+
1 row
52 ms

SQL と同じように WHERE 節は色々な条件を指定することができます。


条件を何も指定しなければ、すべてのノードを取得することができます。

$ MATCH (n) RETURN n;
+-------------------------------------------------+
| n                                               |
+-------------------------------------------------+
| Node[11]{name:"Eric"}                           |
| Node[12]{name:"Jim"}                            |
| Node[13]{name:"Seven Databases in Seven Weeks"} |
+-------------------------------------------------+
3 rows
29 ms


ノードのラベルを知りたいばあいは labels 関数が使えます。

$ MATCH (n) RETURN n, labels(n);
+--------------------------------------------------------------+
| n                                               | labels(n)  |
+--------------------------------------------------------------+
| Node[11]{name:"Eric"}                           | ["Writer"] |
| Node[12]{name:"Jim"}                            | ["Writer"] |
| Node[13]{name:"Seven Databases in Seven Weeks"} | ["Book"]   |
+--------------------------------------------------------------+
3 rows
96 ms
関係を作る

既存のノードに関係を作るばあい、MATCH で該当するノードを取得し、それに対して CREATE で関係を作成します。

$ MATCH (w:Writer), (b:Book) CREATE (w)-[r:Write]->(b) RETURN *;
+---------------------------------------------------------------------------------------+
| b                                               | r           | w                     |
+---------------------------------------------------------------------------------------+
| Node[13]{name:"Seven Databases in Seven Weeks"} | :Write[0]{} | Node[11]{name:"Eric"} |
| Node[13]{name:"Seven Databases in Seven Weeks"} | :Write[1]{} | Node[12]{name:"Jim"}  |
+---------------------------------------------------------------------------------------+
2 rows
Relationships created: 2
89 ms

Writer とラベルがついたノードと Book とラベルがついたノードに Write とラベルがついた関係が作成されました。
ここでは RETURN 節で * を指定しているのですべての変数の値を表示しています。


関係は有向なので、逆向きの WrittenBy という関係を作成してみます。

$ MATCH (w:Writer), (b:Book) CREATE (w)<-[r:WrittenBy]-(b) RETURN *;
+-------------------------------------------------------------------------------------------+
| b                                               | r               | w                     |
+-------------------------------------------------------------------------------------------+
| Node[13]{name:"Seven Databases in Seven Weeks"} | :WrittenBy[4]{} | Node[11]{name:"Eric"} |
| Node[13]{name:"Seven Databases in Seven Weeks"} | :WrittenBy[5]{} | Node[12]{name:"Jim"}  |
+-------------------------------------------------------------------------------------------+
2 rows
Relationships created: 2
34 ms

このように関係の向きは逆向きでも記述することができます。


すべてのノードと関係を表示してみます。

$ MATCH (n1)-[r]->(n2) RETURN n1, r, n2;
+---------------------------------------------------------------------------------------------------------------------+
| n1                                              | r               | n2                                              |
+---------------------------------------------------------------------------------------------------------------------+
| Node[13]{name:"Seven Databases in Seven Weeks"} | :WrittenBy[4]{} | Node[11]{name:"Eric"}                           |
| Node[13]{name:"Seven Databases in Seven Weeks"} | :WrittenBy[5]{} | Node[12]{name:"Jim"}                            |
| Node[12]{name:"Jim"}                            | :Write[1]{}     | Node[13]{name:"Seven Databases in Seven Weeks"} |
| Node[11]{name:"Eric"}                           | :Write[0]{}     | Node[13]{name:"Seven Databases in Seven Weeks"} |
+---------------------------------------------------------------------------------------------------------------------+
4 rows
36 ms


関係は向きを省略して指定することもできます。

$ MATCH (n1)-[r]-(n2) RETURN DISTINCT n1, r, n2;
+---------------------------------------------------------------------------------------------------------------------+
| n1                                              | r               | n2                                              |
+---------------------------------------------------------------------------------------------------------------------+
| Node[13]{name:"Seven Databases in Seven Weeks"} | :Write[0]{}     | Node[11]{name:"Eric"}                           |
| Node[13]{name:"Seven Databases in Seven Weeks"} | :WrittenBy[4]{} | Node[11]{name:"Eric"}                           |
| Node[13]{name:"Seven Databases in Seven Weeks"} | :Write[1]{}     | Node[12]{name:"Jim"}                            |
| Node[13]{name:"Seven Databases in Seven Weeks"} | :WrittenBy[5]{} | Node[12]{name:"Jim"}                            |
| Node[12]{name:"Jim"}                            | :Write[1]{}     | Node[13]{name:"Seven Databases in Seven Weeks"} |
| Node[11]{name:"Eric"}                           | :Write[0]{}     | Node[13]{name:"Seven Databases in Seven Weeks"} |
| Node[12]{name:"Jim"}                            | :WrittenBy[5]{} | Node[13]{name:"Seven Databases in Seven Weeks"} |
| Node[11]{name:"Eric"}                           | :WrittenBy[4]{} | Node[13]{name:"Seven Databases in Seven Weeks"} |
+---------------------------------------------------------------------------------------------------------------------+
8 rows
45 ms

向きを省略したので、同じ意味のものが (n1)-[r]->(n2)(n1)<-[r]-(n2) のそれぞれで取得されています。


関係をノードと同時作成する場合は、ノードを作成する CREATE と一緒に記述することができます。

$ CREATE (e:Writer {name: 'Eric'}), (j:Writer {name: 'Jim'}), (s:Book {name: 'Seven Databases in Seven Weeks'}), (e)-[:Write]->(s), (j)-[:Write]->(s), (s)-[:WrittenBy]->(e), (s)-[:WrittenBy]->(j) RETURN *;
+------------------------------------------------------------------------------------------------+
| e                     | j                    | s                                               |
+------------------------------------------------------------------------------------------------+
| Node[14]{name:"Eric"} | Node[15]{name:"Jim"} | Node[16]{name:"Seven Databases in Seven Weeks"} |
+------------------------------------------------------------------------------------------------+
1 row
Nodes created: 3
Relationships created: 4
Properties set: 3
Labels added: 3
143 ms


関係にもプロパティを設定することができます。
ここではもう一つ Book ラベルのついたノードを作成し、同時にラベルが Translate でプロパティとして {language: 'Japanese'} を設定した関係を作成しています。

$ MATCH (e:Book {name: 'Seven Databases in Seven Weeks'}) CREATE (j:Book {name: '7つのデータベース 7つの世界'}), (e)-[r:Translate {language: 'Japanese'}]->(j) RETURN *;
+--------------------------------------------------------------------------------------------------------------------------+
| e                                               | j                                | r                                   |
+--------------------------------------------------------------------------------------------------------------------------+
| Node[13]{name:"Seven Databases in Seven Weeks"} | Node[56]{name:"7つのデータベース 7つの世界"} | :Translate[13]{language:"Japanese"} |
+--------------------------------------------------------------------------------------------------------------------------+
1 row
Nodes created: 1
Relationships created: 1
Properties set: 2
Labels added: 1
48 ms


値の取得では関係のプロパティで指定することもできます。

$ MATCH (n)-[{language: 'Japanese'}]->(m) RETURN n, m;
+------------------------------------------------------------------------------------+
| n                                               | m                                |
+------------------------------------------------------------------------------------+
| Node[13]{name:"Seven Databases in Seven Weeks"} | Node[56]{name:"7つのデータベース 7つの世界"} |
+------------------------------------------------------------------------------------+
1 row
28 ms
プロパティの追加と削除

SET 節で追加が、REMOVE 節で削除ができます。

$ MATCH (n {name: 'Eric'}) SET n.surname = 'Redmond' RETURN n;
+-----------------------------------------+
| n                                       |
+-----------------------------------------+
| Node[11]{name:"Eric",surname:"Redmond"} |
+-----------------------------------------+
1 row
Properties set: 1
40 ms
$ MATCH (n {name: 'Eric'}) REMOVE n.surname RETURN n;         
+-----------------------+
| n                     |
+-----------------------+
| Node[11]{name:"Eric"} |
+-----------------------+
1 row
Properties set: 1
81 ms


+= 演算子を使って追加することもできます。一度に複数のプロパティを追加するときに便利です。

$ MATCH (n {name: 'Eric'}) SET n += {surname: 'Redmond'} RETURN n;
+-----------------------------------------+
| n                                       |
+-----------------------------------------+
| Node[11]{name:"Eric",surname:"Redmond"} |
+-----------------------------------------+
1 row
Properties set: 1
77 ms


ここまで作成したノードと関係を確認します。

$ MATCH (n) RETURN n;                              
+-------------------------------------------------+
| n                                               |
+-------------------------------------------------+
| Node[11]{name:"Eric",surname:"Redmond"}         |
| Node[12]{name:"Jim"}                            |
| Node[13]{name:"Seven Databases in Seven Weeks"} |
| Node[56]{name:"7つのデータベース 7つの世界"}                |
+-------------------------------------------------+
4 rows
31 ms
$ MATCH (n)-[r]->(m) RETURN r;
+-------------------------------------+
| r                                   |
+-------------------------------------+
| :Write[0]{}                         |
| :Write[1]{}                         |
| :WrittenBy[5]{}                     |
| :WrittenBy[4]{}                     |
| :Translate[13]{language:"Japanese"} |
+-------------------------------------+
5 rows
22 ms
削除

ノードあるいは関係を取得したのちに DELETE することで削除することができます。

$ MATCH (e)-[{language: 'Japanese'}]->(j) DELETE j;
+-------------------+
| No data returned. |
+-------------------+
Nodes deleted: 1
36 ms
TransactionFailureException: Transaction was marked as successful, but unable to commit transaction so rolled back.

ここでノードを削除しようとしていますが、失敗しました。先に関係を削除する必要があるようです。
関係とノードを一緒に削除してみます。

$ MATCH (e)-[r {language: 'Japanese'}]->(j) DELETE r, j;
+-------------------+
| No data returned. |
+-------------------+
Nodes deleted: 1
Relationships deleted: 1
47 ms
$ MATCH (n) RETURN n;
+-------------------------------------------------+
| n                                               |
+-------------------------------------------------+
| Node[11]{name:"Eric",surname:"Redmond"}         |
| Node[12]{name:"Jim"}                            |
| Node[13]{name:"Seven Databases in Seven Weeks"} |
+-------------------------------------------------+
3 rows
16 ms

無事削除できました。

いつか読むはずっと読まない:また別のクエリ言語

待望の SPARQL 本です。以前仕事で SPARQL に触れた時にこの本があったらと思わずにはいられないのですが、執筆陣はその仕事でお世話になった先生方。つまりその仕事の結果としてこの本があるわけで…。
わたし自身は現在はその仕事からはずれていますが、またいつ加わることになるかもしれないので、今のうちに読んで勉強しておきます(幾分業務連絡)。


オープンデータ時代の標準Web API SPARQL (NextPublishing)

オープンデータ時代の標準Web API SPARQL (NextPublishing)