この記事は、Neo4j v4.0(2020-01)に合わせて更新しています!
Neo4jのクエリ言語であるCypherがなかなか上達しないという悩みを聞くと、僕は「グラフを忘れて思い切りRDB的な事をやってみたらどうですか」と答えます。大概、Cypherに十分慣れていない内に、パターンマッチのような新しい概念に取り組んでいるからです。
まず、Cypherのシンタクスに慣れることが大事です。数を数えてみたり、クロス集計をしてみたり、Cypherの構文に慣れてから、パターンマッチに入る作戦です。
この作戦を助けるために開発したのが、「Cypher Query演習用のグラフデータベース」です。中身は、架空のECサイトの販売履歴です(以下、販売履歴データベース)。実は、自分の悩みを解決するために開発したものです。
セットで開発したCypher Queryなどは、2015年10月29日、『Cypherクエリー言語の事例で学ぶグラブテータベースNeo4j』(著者:李 昌桓、監修:クリエーションライン株式会社、発刊:株式会社インプレスR&D)のようにリリースされました
# グラフDBのスキーマ
グラフDBでは、スキーマをみれば、データベースの構成が一目瞭然に把握できます。
著作権について
販売履歴データベースの著作権はクリエーションライン社にありますが、ご利用において特に制限はありません。ご自由に利用してください。
販売履歴データベースの構築
事前準備
- データベースをダウンロードしてください。
ファイルを解凍し、内容を確認してみてください。LOAD CSVでMy販売履歴データベースが構築できるようになっています。
csv/ec-users-10000.csv # ユーザーデータ
csv/ec-goods-10000.csv # 商品データ
csv/ec-sales-10000.csv # 販売履歴データ
create-salesdb-ddl.txt
- CSVファイルを${NEO4J_HOME}/import配下にコピー
- Neo4jサーバーを起動
- create-salesdb-ddlを参考にして実行
販売履歴データベースの構築開始
Neo4jブラウザーから、次のように実行します。
- 制約及びインデックス作成
後続のCypherにインデクスが必要なものがあります。
CREATE CONSTRAINT cnt_uid ON (u:User) ASSERT u.uid IS UNIQUE;
CREATE CONSTRAINT cnt_gid ON (g:Goods) ASSERT g.gid IS UNIQUE;
CREATE CONSTRAINT cnt_oid ON (o:Order) ASSERT o.oid IS UNIQUE;
CREATE INDEX idx_day FOR (d:Day) ON (d.day);
- ユーザーノードの作成
LOAD CSV WITH HEADERS FROM "file:///ec-users-10000.csv" AS line
CREATE (u:User { uid:line.uid, born:toInteger(line.born), gender:line.gender })
- 商品ノードの作成
LOAD CSV WITH HEADERS FROM "file:///ec-goods-10000.csv" AS line
CREATE (g:Goods { gid:line.gid, color:line.color })
- 販売履歴ノードの作成
//USING PERIODIC COMMIT 500
LOAD CSV WITH HEADERS FROM "file:///ec-sales-10000.csv" AS line
CREATE (o:Order{oid:line.oid, number:toInteger(line.number), price:toInteger(line.price), date:substring(line.datetime,0,10),datetime:line.datetime})
- ユーザーと販売履歴間の関係性の作成
殆どのグラフDBでは、RDBとは違ってデータの結合関係を永続化します。
内部的にリレーションシップ(PLACED)の前後に始点と終点のIDを持ちます。
//USING PERIODIC COMMIT 500
LOAD CSV WITH HEADERS FROM "file:///ec-sales-10000.csv" AS line
MATCH (u:User { uid:line.uid }), (o:Order { oid:line.oid})
CREATE (u)-[:PLACED]->(o)
- 販売履歴と商品間の関係性の作成
//USING PERIODIC COMMIT 500
LOAD CSV WITH HEADERS FROM "file:///ec-sales-10000.csv" AS line
MATCH (o:Order { oid: line.oid}), (g:Goods { gid:line.gid})
CREATE (o)-[:CONTAINS]->(g)
- 年月日時間ノードの作成
WITH range(9,11) AS months, range(0,23) AS hours
FOREACH(month IN months |
CREATE (y:Year {year: 2014})
CREATE (m:Month {month: month})
MERGE (y)-[:HAS_MONTH]->(m)
FOREACH(day IN (CASE
WHEN month IN [1,3,5,7,8,10,12] THEN range(1,31)
WHEN month = 2 THEN
CASE
WHEN y.year % 4 <> 0 THEN range(1,28)
WHEN y.year % 100 <> 0 THEN range(1,29)
WHEN y.year % 400 <> 0 THEN range(1,28)
ELSE range(1,29)
END
ELSE range(1,30)
END) |
CREATE (d:Day {day: day})
MERGE (m)-[:HAS_DAY]->(d)
FOREACH( hour IN hours |
CREATE (h:Hour {hour: hour})
MERGE (d)-[:HAS_HOUR]->(h))))
- 年月日時間と販売履歴間の関係性の作成
//USING PERIODIC COMMIT 500
LOAD CSV WITH HEADERS FROM "file:///ec-sales-10000.csv" AS l
MATCH (y:Year { year:toInteger(substring(l.datetime,0,4))})-->(m:Month { month:toInteger(substring(l.datetime,5,2))})-->(d:Day { day:toInteger(substring(l. datetime,8,2))})-->(h:Hour {hour:toInteger(substring(l.datetime,11,2))})
MATCH (o:Order { oid:l.oid } )
CREATE (h)-[r:HAS_ORDER]->(o)
- ユーザーの最新の販売履歴と過去の販売履歴間の関係性の作成
//2つ以上の販売履歴を持つユーザー
MATCH (u:User)-->(o)
WITH u.uid AS uname, count(o.oid) AS ocnt
WHERE ocnt > 1
//販売履歴を昇順に並べる
MATCH (u:User { uid:uname})-->(o)
WITH u.uid AS uname, o.oid AS oname
ORDER BY uname, oname
//ユーザー毎にオーダー番号リストを作成
WITH uname, collect(oname) AS oname
//RETURN uname, collect(oname) AS oname
//foreachの利用し、同ユーザーのすべの販売履歴の関係性を作成
FOREACH(i IN RANGE(0, size(oname)-2) |
FOREACH(order1 IN [oname[i]] |
FOREACH(order2 IN [oname[i+1]] |
MERGE (o1:Order {oid:order1})
MERGE (o2:Order {oid:order2})
MERGE (o1)<-[:PREVIOUS]-(o2))))
- Neo4jブラウザーから接続し、次のCypher構文でグラフを出力してみてください。
Cypherにおいて、アスキーアートの線はRDBで言えばジョイン(結合)を意味します。
MATCH (y:Year {year:2014})-->(m:Month {month:9})-->(d:Day {day:22})-->(h)-->(o)--(all)
RETURN *
LIMIT 25
- データベースのスキーマ確認
Neo4j v3.x
call db.schema
Neo4j v4.x
call db.schema.visualization
販売履歴データベースの利用方法
パターンマッチを意識せず、初歩的なデータ表示、集計、クロス集計などSQLを書くような感覚で、色々Cypherを書いてみることです。
Cypherは、汎用的なクエリ言語です。
- 2014年9月の総売上金額
MATCH (y:Year{year:2014})-->(m:Month{month:9})-->(d)-->(h)-->(o)
RETURN sum(o.number * o.price) AS 金額
金額
106485340
- 2014年9月の金曜日の「22時から翌日01時」までの売上金額
MATCH
(y:Year{year:2014})-->(m:Month{month:9})-->(d)-->(h)-->(o)
WHERE
(d.day IN [5,12,19,26] AND h.hour IN [22,23]) OR
(d.day IN [6,13,20,27] AND h.hour=0)
WITH y.year AS tyear, m.month AS tmonth, d.day AS tday, h.hour AS thour, sum(o.number * o.price) AS total
ORDER BY tday, thour
RETURN
tyear+"-"+tmonth+"-"+tday+" "+thour+"h" AS 日時,
total AS 金額
日時 金額
"2014-9-5 22h" 182824
"2014-9-5 23h" 223862
"2014-9-6 0h" 249152
"2014-9-12 22h" 91454
"2014-9-12 23h" 203124
"2014-9-13 0h" 122270
"2014-9-19 22h" 39428
"2014-9-19 23h" 36708
"2014-9-20 0h" 83610
"2014-9-26 22h" 138828
"2014-9-26 23h" 152066
"2014-9-27 0h" 160288
- 2014年9月の年齢別男女別の売上金額
かなり複雑なクロス集計が簡単に書けてます。
グラフDBは、結合済みのデータから処理を開始します。SQLのように結合関係を紐解く必要がありません。
だからこそ、複雑な処理を高速に行うことができます。
MATCH (y:Year {year:2014})-->(m:Month {month:9})-->(d)-->(h)-->(o)<-[:PLACED]-(u)
WITH u, sum(o.number * o.price) AS total
WITH
CASE WHEN u.born > 1994 AND u.gender="man" THEN sum(total) END AS lt20m,
CASE WHEN u.born > 1994 AND u.gender="woman" THEN sum(total) END AS lt20w,
CASE WHEN u.born > 1984 AND u.born <= 1994 AND u.gender="man" THEN sum(total) END AS lt30m,
CASE WHEN u.born > 1984 AND u.born <= 1994 AND u.gender="woman" THEN sum(total) END AS lt30w,
CASE WHEN u.born > 1974 AND u.born <= 1984 AND u.gender="man" THEN sum(total) END AS lt40m,
CASE WHEN u.born > 1974 AND u.born <= 1984 AND u.gender="woman" THEN sum(total) END AS lt40w,
CASE WHEN u.born > 1964 AND u.born <= 1974 AND u.gender="man" THEN sum(total) END AS lt50m,
CASE WHEN u.born > 1964 AND u.born <= 1974 AND u.gender="woman" THEN sum(total) END AS lt50w,
CASE WHEN u.born > 1954 AND u.born <= 1964 AND u.gender="man" THEN sum(total) END AS lt60m,
CASE WHEN u.born > 1954 AND u.born <= 1964 AND u.gender="woman" THEN sum(total) END AS lt60w,
CASE WHEN u.born <= 1954 AND u.gender="man" THEN sum(total) END AS gt60m,
CASE WHEN u.born <= 1954 AND u.gender="woman" THEN sum(total) END AS gt60w
WITH collect(lt20m) AS lt20am,
collect(lt20w) AS lt20aw,
collect(lt30m) AS lt30am,
collect(lt30w) AS lt30aw,
collect(lt40m) AS lt40am,
collect(lt40w) AS lt40aw,
collect(lt50m) AS lt50am,
collect(lt50w) AS lt50aw,
collect(lt60m) AS lt60am,
collect(lt60w) AS lt60aw,
collect(gt60m) AS gt60am,
collect(gt60w) AS gt60aw
RETURN
head(lt20am) AS 男年齢20以下,
head(lt20aw) AS 女年齢20以下,
head(lt30am) AS 男年齢30以下,
head(lt30aw) AS 女年齢30以下,
head(lt40am) AS 男年齢40以下,
head(lt40aw) AS 女年齢40以下,
head(lt50am) AS 男年齢50以下,
head(lt50aw) AS 女年齢50以下,
head(lt60am) AS 男年齢60以下,
head(lt60aw) AS 女年齢60以下,
head(gt60am) AS 男年齢60上,
head(gt60aw) AS 女年齢60上
男年齢20以下 女年齢20以下 男年齢30以下 女年齢30以下 男年齢40以下 女年齢40以下 男年齢50以下 女年齢50以下 男年齢60以下 女年齢60以下 男年齢60上 女年齢60上
2,697,130 3,381,830 5,130,839 5,522,689 5,471,666 3,766,844 5,734,831 4,794,380 5,682,058 4,630,580 3,285,838 3,143,985
上記のCypherは、「Cypherクエリー言語の事例で学ぶ グラフデータベースNeo4j」から抜粋しています
Cypherは、汎用性が高いクエリ言語です。論理構成力が優れているために、簡潔な構文で色んなことができます。色々シナリオを作り、チャレンジしてみたください!
[関連記事]