はじめに
勤務先では、truncusというマイクロサービスプラットフォームを開発していますが、このプラットフォームは組み込みのデータベースとして独自のグラフデータベースを持っています。
現在、代表的なグラフデータベースといえばNeo4jだと思いますが、一度も触ったことがありません。
勤務先がグラフデータベースを扱っているのに、社員としてNeo4jを使ったことがないのはいかがなものかということで、触れてみることにしました。
Neo4j公式にSandboxがあるので、まずはこれを試しました。
やってみて、何となくNeo4jのクエリ言語であるCypherの使い方が分かったので、自分でデータをインポートして色々やってみたくなりました。
何か使えそうなデータがないか探してみたところ、kaggleにオリンピックのアスリートデータがあったので、これを使ってみることにしました。
本記事では、このCSVデータをNeo4jに格納して、Cypherを使っていくつかの視点でデータを参照する手順を示します。
歴代オリンピックのアスリートデータ
歴代オリンピックのアスリートデータ(CSV)は、1896年アテネオリンピック〜2016年リオデジャネイロオリンピックに参加した全てのアスリートの基本的な情報からなるものです(レコード数:271116件)。データは、下記のページからダウンロードできます。
120 years of Olympic history: athletes and results
ダウンロードしたCSVは、以下の項目を持っています。
- ID: 各アスリートに割り当てた一意な数値
- Name: アスリートの氏名
- Sex: 性別(M/F)
- Age: 年齢
- Height: 身長(cm)
- Weight: 体重(kg)
- Team: チーム名
- NOC: 国際オリンピック委員会で定められた3文字の国コード(JPNなど)
- Games: オリンピックの大会名(開催年+季節)
- Year: 開催年
- Season: 季節
- City: 開催都市
- Sport: 競技
- Event: 種目
- Medal: メダル(Gold/Silver/Bronze/NA)
エンティティと関連
Neo4jでは、データはエンティティと関連として格納されるため、このアスリートデータを、以下のようなエンティティと関連に分解します。
エンティティ
エンティティは、次の6個です。
- Olimpic: オリンピック
- id: エンティティのID('o'+連番)
- name: オリンピックの大会名(開催年+季節)。CSVの「Games」。
- year: 開催年。CSVの「Year」。
- season: 季節。CSVの「Season」。
- city: 開催都市。CSVの「City」。
- Game: 試合
- id: エンティティのID('g'+連番)
- Sport: 競技
- id: エンティティのID('s'+連番)
- name: 競技名。CSVの「Sport」。
- Event: 種目
- id: エンティティのID('e'+連番)
- name: 種目名。CSVの「Event」。
- Team: チーム
- id: エンティティのID('t'+連番)
- name: チーム名。CSVの「Team」。
- noc: 国際オリンピック委員会での国の3文字コード。CSVの「NOC」。
- Person: 人物
- id: ('p'+連番)。連番はCSVの「ID」。
- name: アスリートの氏名。CSVの「Name」。
- sex: 性別(M/F)。CSVの「Sex」。
- age: 年齢。CSVの「Age」。
- height: 身長(cm)。CSVの「Height」。
- weight: 体重(kg)。CSVの「Weight」。
関連
関連は、次の5個です。
- HOLD: 開催する
- オリンピック(Olimpic)と、そこで開催される試合(Game)との間の関連
- GAME_EVENT: 試合の種目
- 試合(Game)と、その試合の種目(Event)との間の関連
- PLAY: 参加する
- 人物(Person)と、その人が参加する試合(Game)との間の関連
- 以下の属性を持つ。
- age: 年齢(CSVのAge)
- medal: 獲得したメダル(CSVのMedal)
- INCLUDE: 含む
- 競技(Sport)と、その競技の種目(Event)との間の関連
- MEMBER: チームのメンバー
- チーム(Team)と、そのチームに所属する人物(Person)との間の関連
CSVインポート
今回は、Neo4j Desktopを使って、CSVのインポート、Cypherの実行を行います。
CSVの作成
Cypherを駆使すれば、ダウンロードしたCSVからエンティティ、関連を直接作れると思いますが、まだそこまでのCypher達人にはなっていないので、泥臭くExcelを使って、エンティティ、関連を表すCSVを作ります(手順は省略)。
作成するCSVの名称は、エンティティ名と同じです(Olimpic.csv、HOLD.csvなど)。
CSVのインポート
準備
CSVのインポートは、Neo4jのDEVELOPER GUIDESにある、以下のページにしたがって行います。
How-To: Import CSV Data with Neo4j Desktop
このページのうち必要となる手順は、「1. Creating and starting the Neo4j instance」、「2. Putting CSV files in the import folder」の2つだけです。
上記のページの手順に従うと、CSVをインポートする準備が整います。
では、次からCypherを使ってCSVをインポートしていきます。
Neo4j Browser
Neo4j Desktopの「Open」をクリックして、Neo4j Browserを開きます。
Neo4jブラウザは下図の様になっていて、赤枠の部分にCyperクエリを入力して実行します。
では、Cypherを使って、CSVをインポートしていきます。
1. インデックス作成
まず、各エンティティのインデックスを作成します。
話を簡単にするために、インデックスは作らなくても良いかと思いましたが、関連のインポートでCypherの実行が終わらなかったので、作成しました。
CREATE CONSTRAINT UniqueOlimpic ON (o: Olimpic) ASSERT o.id IS UNIQUE;
CREATE CONSTRAINT UniqueSport ON (s:Sport) ASSERT s.id IS UNIQUE;
CREATE CONSTRAINT UniqueEvent ON (e:Event) ASSERT e.id IS UNIQUE;
CREATE CONSTRAINT UniqueTeam ON (t:Team) ASSERT t.id IS UNIQUE;
CREATE CONSTRAINT UniquePerson ON (p:Person) ASSERT p.id IS UNIQUE;
CREATE CONSTRAINT UniqueGame ON (g:Game) ASSERT g.id IS UNIQUE;
2. エンティティのインポート
次に、エンティティをインポートします。
Cypherでは「()」はグラフのノード(エンティティ)を表しています。
以下では、エンティティをインポートするためのクエリを示しますが、直感的でわかりやすいと思いますので、詳細な説明は省きます。
- Olimpic
LOAD CSV WITH HEADERS FROM 'file:///Olimpic.csv' AS row
CREATE (o:Olimpic {id: row.id, name: row.name, year: toInteger(row.year), season: row.season, city: row.city})
RETURN count(o);
- Sport
LOAD CSV WITH HEADERS FROM 'file:///Sport.csv' AS row
CREATE (s:Sport {id: row.id, name: row.name})
RETURN count(s);
- Event
LOAD CSV WITH HEADERS FROM 'file:///Event.csv' AS row
CREATE (e:Event {id: row.id, name: row.name})
RETURN count(e);
- Team
LOAD CSV WITH HEADERS FROM 'file:///Team.csv' AS row
CREATE (t:Team {id: row.id, name: row.name, noc: row.noc})
RETURN count(t);
- Person
LOAD CSV WITH HEADERS FROM 'file:///Person.csv' AS row
CREATE (p:Person {id: row.id, name: row.name, sex: row.sex, weight: toFloat(row.weight), height: toFloat(row.height)})
RETURN count(p);
- Game
LOAD CSV WITH HEADERS FROM 'file:///Game.csv' AS row
CREATE (g:Game {id: row.id})
RETURN count(g);
3. 関連のインポート
最後に、関連をインポートします。
Cypherでは「[]」でグラフのエッジ(関連)を表現します。
エンティティ間の関連は、「(エンティティ)-[関連]->(エンティティ)」の形で表現され、非常に直感的です。
- HOLD
LOAD CSV WITH HEADERS FROM 'file:///HOLD.csv' AS row
MATCH (o:Olimpic {id: row.olimpic})
MATCH (g:Game {id: row.game})
CREATE (o)-[rel:HOLD]->(g)
RETURN count(rel);
- GAME_EVENT
LOAD CSV WITH HEADERS FROM 'file:///GAME_EVENT.csv' AS row
MATCH (g:Game {id: row.game})
MATCH (e:Event {id: row.event})
CREATE (g)-[rel:GAME_EVENT]->(e)
RETURN count(rel);
- INCLUDE
LOAD CSV WITH HEADERS FROM 'file:///INCLUDE.csv' AS row
MATCH (s:Sport {id: row.sport})
MATCH (e:Event {id: row.event})
CREATE (s)-[rel:INCLUDE]->(e)
RETURN count(rel);
- PLAY
LOAD CSV WITH HEADERS FROM 'file:///PLAY.csv' AS row
MATCH (p:Person {id: row.person})
MATCH (g:Game {id: row.game})
CREATE (p)-[rel:PLAY {age: toInteger(row.age), medal: row.medal}]->(g)
RETURN count(rel);
- MEMBER
LOAD CSV WITH HEADERS FROM 'file:///MEMBER.csv' AS row
MATCH (t:Team {id: row.team})
MATCH (p:Person {id: row.person})
CREATE (t)-[rel: MEMBER]->(p)
RETURN count(rel);
Cypherでアスリートデータを見る
ここまでで、CSVをインポートすることができたので、Cypherを使ってアスリートデータをいくつかの視点で見ていきます。
オリンピックのエンティティ一覧
Cypherを実行して、オリンピックの一覧を参照します。
MATCH (o:Olimpic) RETURN o
結果はこの様に表示されます。
ノードをクリックするとこの様になります。
下の部分をクリックすると、そのエンティティと関連で紐づいた全てのエンティティが表示されます。どんどん関連を辿っていくと、こんな感じになります。
陸上競技の種目一覧
せっかくグラフデータベースを使っているので、関連も使ってみます。
まずは簡単に、陸上競技に含まれる種目の一覧を表示してみましょう。
使用するエンティティ、関連は、下図のオレンジ色のものです。
MATCH (s:Sport)-[r:INCLUDE]->(e:Event)
WHERE s.name='Athletics'
RETURN s,r,e
次のように書いても同じです。
MATCH (s:Sport {name:'Athletics'})-[r:INCLUDE]->(e:Event)
RETURN s,r,e
結果はこうなります。
種目名が全て「Athletics」になっているように見えますが、途中で見えなくなっているだけで、実際には「Athletics Women's 100 metres」のようになっています。
吉田沙保里の戦歴
×××最強女子と言われた吉田沙保里さんの戦歴を拾ってみましょう。
使うエンティティ、関連は、下図のオレンジ色のものです。
図を見てわかるように、一本の関連では表現できません。
この様な場合は、複数のMATCHを使います。注意すべき点は、関連の合流点である
「試合(Game)」の変数を同じにすることです(ここでは「g」)。
MATCH (o:Olimpic)-[:HOLD]->(g:Game)-[:GAME_EVENT]->(e:Event)
MATCH (:Person {name: 'Saori Yoshida'})-[rp:PLAY]->(g:Game)
RETURN o.name AS olimpic, o.city AS city, e.name AS event, rp.medal AS medal, rp.age AS age
ORDER BY o.year
これが実行結果です。やはり吉田さん最強です。
╒═════════════╤════════════════╤════════════════════════════════════════════╤════════╤═════╕
│"olimpic" │"city" │"event" │"medal" │"age"│
╞═════════════╪════════════════╪════════════════════════════════════════════╪════════╪═════╡
│"2004 Summer"│"Athina" │"Wrestling Women's Lightweight, Freestyle" │"Gold" │21 │
├─────────────┼────────────────┼────────────────────────────────────────────┼────────┼─────┤
│"2008 Summer"│"Beijing" │"Wrestling Women's Lightweight, Freestyle" │"Gold" │25 │
├─────────────┼────────────────┼────────────────────────────────────────────┼────────┼─────┤
│"2012 Summer"│"London" │"Wrestling Women's Lightweight, Freestyle" │"Gold" │29 │
├─────────────┼────────────────┼────────────────────────────────────────────┼────────┼─────┤
│"2016 Summer"│"Rio de Janeiro"│"Wrestling Women's Featherweight, Freestyle"│"Silver"│33 │
└─────────────┴────────────────┴────────────────────────────────────────────┴────────┴─────┘
オリンピックの参加国数
最後に、歴代オリンピックの参加国数を見てみます。
使うエンティティ、関連は、下図のオレンジ色のものです。
リレーショナルデータベースで扱うには少し厳しそうな距離の関連です。
MATCH (o:Olimpic)-[:HOLD]->(:Game)<-[:PLAY]-(:Person)<-[:MEMBER]-(t:Team)
RETURN o.name AS name, o.city AS city, count(DISTINCT t.noc) AS noc
ORDER BY o.name
実行結果はこちらです。わずか407msで実行できました。グラフデータベースの威力発揮ですね。
╒═════════════╤════════════════════════╤═════╕
│"name" │"city" │"noc"│
╞═════════════╪════════════════════════╪═════╡
│"1896 Summer"│"Athina" │12 │
├─────────────┼────────────────────────┼─────┤
│"1900 Summer"│"Paris" │30 │
├─────────────┼────────────────────────┼─────┤
│"1904 Summer"│"St. Louis" │15 │
├─────────────┼────────────────────────┼─────┤
│"1906 Summer"│"Athina" │24 │
├─────────────┼────────────────────────┼─────┤
│"1908 Summer"│"London" │24 │
├─────────────┼────────────────────────┼─────┤
│"1912 Summer"│"Stockholm" │34 │
├─────────────┼────────────────────────┼─────┤
│"1920 Summer"│"Antwerpen" │33 │
├─────────────┼────────────────────────┼─────┤
│"1924 Summer"│"Paris" │48 │
├─────────────┼────────────────────────┼─────┤
│"1924 Winter"│"Chamonix" │20 │
├─────────────┼────────────────────────┼─────┤
│"1928 Summer"│"Amsterdam" │48 │
├─────────────┼────────────────────────┼─────┤
│"1928 Winter"│"Sankt Moritz" │25 │
├─────────────┼────────────────────────┼─────┤
│"1932 Summer"│"Los Angeles" │47 │
├─────────────┼────────────────────────┼─────┤
│"1932 Winter"│"Lake Placid" │17 │
├─────────────┼────────────────────────┼─────┤
│"1936 Summer"│"Berlin" │52 │
├─────────────┼────────────────────────┼─────┤
│"1936 Winter"│"Garmisch-Partenkirchen"│28 │
├─────────────┼────────────────────────┼─────┤
│"1948 Summer"│"London" │63 │
├─────────────┼────────────────────────┼─────┤
│"1948 Winter"│"Sankt Moritz" │28 │
├─────────────┼────────────────────────┼─────┤
│"1952 Summer"│"Helsinki" │72 │
├─────────────┼────────────────────────┼─────┤
│"1952 Winter"│"Oslo" │30 │
├─────────────┼────────────────────────┼─────┤
│"1956 Summer"│"Melbourne" │73 │
├─────────────┼────────────────────────┼─────┤
│"1956 Summer"│"Stockholm" │31 │
├─────────────┼────────────────────────┼─────┤
│"1956 Winter"│"Cortina d'Ampezzo" │33 │
├─────────────┼────────────────────────┼─────┤
│"1960 Summer"│"Roma" │94 │
├─────────────┼────────────────────────┼─────┤
│"1960 Winter"│"Squaw Valley" │32 │
├─────────────┼────────────────────────┼─────┤
│"1964 Summer"│"Tokyo" │101 │
├─────────────┼────────────────────────┼─────┤
│"1964 Winter"│"Innsbruck" │38 │
├─────────────┼────────────────────────┼─────┤
│"1968 Summer"│"Mexico City" │116 │
├─────────────┼────────────────────────┼─────┤
│"1968 Winter"│"Grenoble" │38 │
├─────────────┼────────────────────────┼─────┤
│"1972 Summer"│"Munich" │123 │
├─────────────┼────────────────────────┼─────┤
│"1972 Winter"│"Sapporo" │36 │
├─────────────┼────────────────────────┼─────┤
│"1976 Summer"│"Montreal" │98 │
├─────────────┼────────────────────────┼─────┤
│"1976 Winter"│"Innsbruck" │39 │
├─────────────┼────────────────────────┼─────┤
│"1980 Summer"│"Moskva" │93 │
├─────────────┼────────────────────────┼─────┤
│"1980 Winter"│"Lake Placid" │41 │
├─────────────┼────────────────────────┼─────┤
│"1984 Summer"│"Los Angeles" │151 │
├─────────────┼────────────────────────┼─────┤
│"1984 Winter"│"Sarajevo" │59 │
├─────────────┼────────────────────────┼─────┤
│"1988 Summer"│"Seoul" │183 │
├─────────────┼────────────────────────┼─────┤
│"1988 Winter"│"Calgary" │70 │
├─────────────┼────────────────────────┼─────┤
│"1992 Summer"│"Barcelona" │188 │
├─────────────┼────────────────────────┼─────┤
│"1992 Winter"│"Albertville" │80 │
├─────────────┼────────────────────────┼─────┤
│"1994 Winter"│"Lillehammer" │74 │
├─────────────┼────────────────────────┼─────┤
│"1996 Summer"│"Atlanta" │206 │
├─────────────┼────────────────────────┼─────┤
│"1998 Winter"│"Nagano" │79 │
├─────────────┼────────────────────────┼─────┤
│"2000 Summer"│"Sydney" │209 │
├─────────────┼────────────────────────┼─────┤
│"2002 Winter"│"Salt Lake City" │85 │
├─────────────┼────────────────────────┼─────┤
│"2004 Summer"│"Athina" │210 │
├─────────────┼────────────────────────┼─────┤
│"2006 Winter"│"Torino" │87 │
├─────────────┼────────────────────────┼─────┤
│"2008 Summer"│"Beijing" │212 │
├─────────────┼────────────────────────┼─────┤
│"2010 Winter"│"Vancouver" │85 │
├─────────────┼────────────────────────┼─────┤
│"2012 Summer"│"London" │215 │
├─────────────┼────────────────────────┼─────┤
│"2014 Winter"│"Sochi" │91 │
├─────────────┼────────────────────────┼─────┤
│"2016 Summer"│"Rio de Janeiro" │213 │
└─────────────┴────────────────────────┴─────┘
最後に
今回は、比較的簡単なことしか試していませんが、関連を扱うのに向いているというグラフデータベースの長所を垣間見ることができました。
今後は、さらに関連が特徴的なデータを探してみて、Neo4jで色々と試してみたいと思います。