自己紹介
初めまして。加藤です。
名古屋の大学で情報系について勉強しています。
neo4jというグラフデータベースに触れる機会があったのでQiitaの練習がてらまとめてみました。
neo4j について
neo4j とは
データやデータ間の関係がデータレコードごとに定義されるネイティブグラフDBです。
neo4jではcypherというグラフクエリ言語を用いてデータを操作します。
グラフDB とは
テーブル単位でデータ間の関係を持つ従来のリレーショナルデータベースと違い、データ単位でデータ間の関係を持つネットワーク状のデータベースのことです。
neo4j のメリット
-
RDBではデータ間の関係をテーブルごとに定義された外部キーを参照しデータを操作したが、グラフDBではデータ間の関係(エッジ)がデータ(ノード)ごとに明示的に定義されているためエッジを辿るだけでよく、RDBで大量のJOIN文を用いて処理したデータでも計算が早い(join bombが発生しない)。
-
RDBではリレーションを考える必要があったが、グラフDBでは考えなくて良いので設計が簡単(な場合が多い)。
例
- cypher(深さ無制限)
MATCH (p:Product)-[:CATEGORY]->(l:ProductCategory)-[:PARENT*0..]->(:ProductCategory {name:"Dairy Products"})
RETURN p.name
- SQL(深さ3)
SELECT p.ProductName
FROM Product AS p
JOIN ProductCategory pc ON (p.CategoryID = pc.CategoryID AND pc.CategoryName = "Dairy Products")
JOIN ProductCategory pc1 ON (p.CategoryID = pc1.CategoryID)
JOIN ProductCategory pc2 ON (pc1.ParentID = pc2.CategoryID AND pc2.CategoryName = "Dairy Products")
JOIN ProductCategory pc3 ON (p.CategoryID = pc3.CategoryID)
JOIN ProductCategory pc4 ON (pc3.ParentID = pc4.CategoryID)
JOIN ProductCategory pc5 ON (pc4.ParentID = pc5.CategoryID AND pc5.CategoryName = "Dairy Products")
-
隣接ノードへのポインタを物理的に保持しているため隣接ノードを辿る計算量が $\mathcal{O}(1)$ (index-free-adjency)
-
.Net、Java、JavaScript、Go、Pythonのドライバーを公式にサポートしている。
-
コミュニティーが大規模である。
-
柔軟な構造のため、機能の追加などが容易。
neo4j のデメリット
- DBへの書き込みが1台のノードに制限されているため、大量のデータの書き込みには向かない。
- グラフをメモリ上にロードすることができるが、システムの耐久性の低下やメモリが足りない場合にパフォーマンスが落ちる。
neo4j を触ってみる
neo4j をdockerで立ち上げる
docker-composeを使ってneo4jを立ち上げるので、以下のようにdocker-compose.ymlを記述しましょう。
version: "3"
services:
neo4j:
image: neo4j:latest
container_name: neo4j
ports:
- 7474:7474
- 7687:7687
volumes:
- $PWD/data:/data
- $PWD/logs:/logs
docker-compose upで起動します(結構時間がかかる)。
% docker-compose up
Pulling neo4j (neo4j:latest)...
6f28985ad184: Pull complete
b3555c388585: Pull complete
42965fccf401: Pull complete
55a08f332394: Pull complete
777621e154b8: Pull complete
e0588cf29830: Pull complete
3f2ffef95d5c: Pull complete
de6bf150b1fd: Pull complete
Digest: sha256:e9cce6d1bd2289e6deab711a2e8930ea6105d3274bfbbd2a84a5b5b3b0036878
Status: Downloaded newer image for neo4j:latest
Creating neo4j ... done
Attaching to neo4j
neo4j | Warning: Folder mounted to "/logs" is not writable from inside container. Changing folder owner to neo4j.
neo4j | Warning: Folder mounted to "/data" is not writable from inside container. Changing folder owner to neo4j.
neo4j | Directories in use:
neo4j | home: /var/lib/neo4j
neo4j | config: /var/lib/neo4j/conf
neo4j | logs: /logs
neo4j | plugins: /var/lib/neo4j/plugins
neo4j | import: /var/lib/neo4j/import
neo4j | data: /var/lib/neo4j/data
neo4j | certificates: /var/lib/neo4j/certificates
neo4j | run: /var/lib/neo4j/run
neo4j | Starting Neo4j.
neo4j | 2021-03-24 07:00:00.465+0000 INFO Starting...
neo4j | 2021-03-24 07:00:05.648+0000 INFO ======== Neo4j 4.2.4 ========
neo4j | 2021-03-24 07:00:10.786+0000 INFO Initializing system graph model for component 'security-users' with version -1 and status UNINITIALIZED
neo4j | 2021-03-24 07:00:10.835+0000 INFO Setting up initial user from defaults: neo4j
neo4j | 2021-03-24 07:00:10.841+0000 INFO Creating new user 'neo4j' (passwordChangeRequired=true, suspended=false)
neo4j | 2021-03-24 07:00:10.893+0000 INFO Setting version for 'security-users' to 2
neo4j | 2021-03-24 07:00:10.922+0000 INFO After initialization of system graph model component 'security-users' have version 2 and status CURRENT
neo4j | 2021-03-24 07:00:10.951+0000 INFO Performing postInitialization step for component 'security-users' with version 2 and status CURRENT
neo4j | 2021-03-24 07:00:11.970+0000 INFO Bolt enabled on 0.0.0.0:7687.
neo4j | 2021-03-24 07:00:14.868+0000 INFO Remote interface available at http://localhost:7474/
neo4j | 2021-03-24 07:00:14.877+0000 INFO Started.
neo4j を開く
起動したら、ブラウザを開いてlocalhost:7474にアクセスしてみましょう(もしここでページが空白だったらキャッシュを消せば治るはず)。
アクセスすると以下のページが表示されます。
UsernameとPasswordを入力し、Connectをクリック。
Username: neo4j
Password: neo4j
Passwordの変更が求められるので変更してください。
Cypherクエリを触ってみる
グラフDBの世界ではノードとリレーションという概念が存在します。
ノードとは
ノードとは個々のデータを表すもので、「ラベル」と「プロパティ」という概念を持ちます。
ラベルとはそのノードが何であるか(どのグループに属すか)を表すRDBでいうテーブルのようなものです。
プロパティはそのノードが実際に保持する情報を表現します。
Cypherクエリではノードは以下のように記述します。
() #ラベルもプロパティも指定しないノード
(:User) #ラベルを付与したノード
(:User{name:"kato", email:"kato@xxx.com"}) #ラベルとプロパティを付与したノード
ノードを作成
ノードを作成するにはCREATE句を使います。以下のCypher クエリを実行してみましょう。
CREATE (:User{name:"kato", email:"kato@xxx.com"})
実行したら以下のように出力されます。
無事追加できていることが確認できました。しかし、これではデータの作成の際にどんなデータが入ったのか分かりづらいですよね。ここで活躍するのが「変数」です。変数にはノードやリレーションのデータを格納することができます。実際に変数を使って以下のように打ち込んでみましょう。
CREATE (u:User{name:"kato", email:"kato@xxx.com"})
RETURN u
ここでは、変数uに作成するノードの情報を格納し、RETURN uでその情報を表示しています。
実行すると以下のようになります。
これで追加したノードを出力することができました。
ノードを検索
ノードの検索にはMATCH句を使います。以下のクエリを実行してみましょう。
MATCH (u:User) RETURN u
このクエリでは:Userラベルがついているデータを変数uに格納し、それを表示しています。
実行すると以下のようになるはずです。
これで無事検索することができました。
ノードを更新する
ノードの更新にはSET句を使います。以下のクエリを実行してみましょう。
MATCH (u:User{name:"kato"})
SET
u.name="sato",
u.email="sato@xxx.com"
RETURN
u
ここでは、Userラベルのついたノードからプロパティを指定して、そのノードに対してノードを更新しています。
実行すると以下のようになります。
これでname属性にkatoを持つノードが更新できました。
ノードを削除する
ノードの削除にはDELETE句を使います。以下のクエリを実行してみましょう。
MATCH (u:User{name:"sato"}) DELETE u
ここでは、Userラベルのついたノードのうちname属性がsatoであるものを削除しています。
実行すると以下のようになります。
これで、ノードを削除することができました。
次に、リレーションについてもCRUD処理をしてみます。
リレーションとは
リレーションとは、ノード間の関係を表し、ノードと同じようにラベルとプロパティを持ちます。
リレーションはCypherクエリでは以下のように表します。
[] #ラベルもプロパティも指定しないリレーション
[:MEMBER_OF] #ラベルを付与したリレーション
[:MEMBER_OF{since:"xxxx"}] #ラベルとプロパティを付与したリレーション
リレーションの作成
リレーションはそれ単体では存在することができないので、ノードと一緒に作成します。
CREATE (u:User{name:"kato", email:"kato@xxx.com"}) -[r:MEMBER_OF{since:"xxxx"}]-> (c:Club{name:"xxxClub"})
RETURN u, r, c
リレーションの方向は-, ->, <-で表すことができます。これはkatoさんはxxxClubのメンバーであることを表しています。
実行すると以下のようになります。
無事にリレーションが作成できています。
リレーションの検索
何かしらのノードに対応するリレーションを検索する場合、以下のようにして検索します。
MATCH (n1)-[r:MEMBER_OF]->(n2) RETURN r, n1, n2
これは、全てのノードからリレーションにMEMBER_OFのラベルを持つものを表示します。
実行すると以下のようになります。
リレーションを更新する
リレーションの更新はリレーションを削除して作り直します。
リレーションを削除する
リレーションの削除も、ノードと同じようにしてできます。
MATCH (:User{name:"kato"}) -[r:MEMBER_OF]-> ()
DELETE r
ここでは、Userラベルのついたノードのうち、nameプロパティがkatoであるものに対して、MEMBER_OFのラベルを持つリレーションを全て消去しています。
実行するとリレーションが削除されます。
Tips
全てのノードを検索する場合、ラベルやプロパティを指定しないことで検索できます。
MATCH (n) RETURN n
実行結果は以下になります。
また、これを用いて全てのノードを削除することができます。
MATCH (n) DELETE n
しかし、リレーションが存在する場合にはこれだけでは削除できないので以下のようにして削除します。
MATCH (n) DETACH DELETE n
実践
AwesomePublicDatasetsをダウンロード
https://www.kaggle.com/startupsci/awesome-datasets-graph からデータセットをダウンロードします。
ダウンロードしたデータセットのうち、csvファイル3つをneo4j-sample/datasets/AwesomePublicDatasets/に入れます。
ノードの追加
出来たらそれぞれのノードを追加します。csvからのノードの追加はLOAD CSVモジュールを使います。今回はヘッダー付きのcsvなのでLOAD CSV WITH HEADERS で読み込みます。
LOAD CSV WITH HEADERS FROM "file:///AwesomePublicDataset/datasources.csv" AS line
CREATE (:DataSource { name: line.datasourceName, about: line.about, link: line.link, tool: line.tool, categoryName: line.categoryName, vintage: line.vintage})
LOAD CSV WITH HEADERS FROM "file:///AwesomePublicDataset/datasets.csv" AS line
CREATE (:DataSet { name: line.datasetName, about: line.about, link: line.link, categoryName: line.categoryName,cloud: line.cloud, vintage: line.vintage})
LOAD CSV WITH HEADERS FROM "file:///AwesomePublicDataset/categories.csv" AS line
CREATE (:Category { name: line.categoryName})
リレーションの追加
ノードを追加したらリレーションを追加します。
Dataset PART_OF Category
MATCH (c:Category), (dset:DataSet)
WHERE c.name=dset.categoryName
CREATE (dset) -[:PART_OF]-> (c)
DataSource PART_OF Category
MATCH (c:Category), (dsource:DataSource)
WHERE c.name=dsource.categoryName
CREATE (dsource) -[:PART_OF]-> (c)
cypherクエリではSQLと同じようにWHERE句を使うことができます。ここでは、Categoryラベルのついたノードのname属性とDataSourceラベルのついたノードのctegoryName属性が等しいノード間を、リレーションでつないでいます。
ノードを見てみる
無事インポートできたので、Biologyカテゴリに関連のあるノードを見てみましょう。
MATCH (n) -[p]-> (c:Category{name:"Biology"})
RETURN n, c, p
実行すると以下のようになります。