Neo4jにCSVロードする方法は少なくとも4つはあるようです。
- CSV LOAD
- apoc.load.csv
- apoc.import.csv
- neo4j-admin import
この記事ではこの4つの方法について、pythonから実行する具体的な実装を説明したいと思います。
ロードするデータ
サンプル実装でロードするデータはこちらです。リレーションLINKED
にはscoreというリンクの強さを表すプロパティが設定されています。
検証環境
今回は、docker-hubのneo4j:latest
イメージで、docker-composeで検証します。docker-compose.yamlの設定は基本的には以下のような感じです。
version: '3'
services:
neo4j:
image: neo4j:latest
ports:
- "7474:7474" # 管理画面用
- "7687:7687" # bolt用
volumes:
- ${HOME}/neo4j/data:/data
- ${HOME}/neo4j/logs:/logs
- ${HOME}/neo4j/conf:/conf
- ${HOME}/neo4j/import:/import
environment:
- NEO4J_AUTH=neo4j/password
# admin memrecに従って設定
- NEO4J_dbms_memory_heap_max__size=4G
- NEO4J_dbms_memory_heap_initial__size=4G
- NEO4J_dbms_memory_pagecache_size=454900k
- NEO4J_dbms_tx__state_max__off__heap__memory=2500m
# APOC関係
- NEO4JLABS_PLUGINS=["apoc"]
- NEO4J_apoc_import_file_use__neo4j__config=true # import時に/importフォルダからの相対パスを指定
- NEO4J_apoc_import_file_enabled=true
CSVインポートはサーバサイドで実行されるため、ロード用のCSVファイルはHTTPで提供するか、サーバに配置する必要があります。今回はサーバに配置する形式なので、ホストマシン側のフォルダをdockerコンテナ側の/import
フォルダにマウントして、そこにCSVファイルを配置しています。
方法1: CSV LOAD
公式サイトはこちら
実際みるとリレーション10万件で30分かかったので、1万件行かないぐらいのデータ量で気軽にCSVロードしたいならこの方式がいいのではないかと思いました。
特徴
- 小規模なデータロードに使える
- トランザクションを利用せず、
USING PERIODIC COMMIT
を使って一括コミットすることで、ある程度の速度は出る。 - ノードとリレーションを別々にロードする
- リレーションは、1レコードずつ既存のノードと
MATCH
させてからCREATE
する
低速ではあるものの、特別な配慮なく通常のCypherクエリと同様にロードできますので、一番気楽に利用できます。
CSV
普通のCSVのformatです。
-
nodes.csv
id 1 2 3 4 5 6 7 8 9
-
relations.csv
from_id,to_id,score 1,2,0.412 1,3,0.623 3,4,0.512 4,5,0.79 6,7,0.31 7,8,0.79
python実装
from neo4j import GraphDatabase, Driver
driver: Driver = GraphDatabase.driver('bolt://localhost:7687', auth=('neo4j', 'password'), encrypted=False)
with driver.session() as session:
# ノード用CSVデータをロード
nodes_query: str = """
USING PERIODIC COMMIT 10000
LOAD CSV WITH HEADERS FROM $csv_path AS line
CREATE (:Node {id:toInteger(line.id)})
"""
session.run(nodes_query, csv_path='file:///nodes.csv')
# リレーション用CSVデータをロード
relations_query: str = """
USING PERIODIC COMMIT 10000
LOAD CSV WITH HEADERS FROM $csv_path AS line
MATCH (nf:Node {id:toInteger(line.from_id)}), (nt:Node {id:toInteger(line.to_id)})
CREATE (nf)-[:LINKED {score: toFloat(line.score)}]->(nt)
"""
session.run(relations_query, csv_path='file:///relations.csv')
少しでも高速化するためにトランザクションを使用せず、USING PERIODIC COMMIT 10000
を指定しています。また、CSVデータはデフォルトで文字列扱いになるため、toInteger
やtoFloat
で目的の型に変換してあげる必要があります。
方法2: apoc.load.csv
公式サイトはこちら
LOAD CSV
同様に、データ量が少なくLOAD CSV
で提供されてない機能(例えば行番号をIDに設定したい、とか)の場合に利用することになると思います。
APOC
apoc
は、Neo4j本体で実装されていない、データインテグレーション、グラフアルゴリズム、データ変換などのプロシージャや関数を提供してくれるライブラリです。
今回は、docker-composeで検証しているため、利用のためにはdocker-compose.yaml
で環境変数の設定をしています。
特徴
- 小規模なデータロードに使える
-
LOAD CSV
と同様だが、色々な機能が追加で追加で提供されている- 行番号を利用できる
- CSVの各行をmap形式やlist形式で返すことができる
- 自動でデータの型を変換してくれる
- など。他にもいくつか
-
apoc.periodic.iterate
でbatchSize
を指定することで、LAOD CSV
と同様に一括コミットすることで高速化できる。詳細はこちら - リレーションは、1レコードずつ既存のノードと
MATCH
させてからCREATE
する
CSV
LOAD CSV
と同様です。
python実装
from neo4j import GraphDatabase, Driver
driver: Driver = GraphDatabase.driver('bolt://localhost:7687', auth=('neo4j', 'password'), encrypted=False)
with driver.session() as session:
nodes_query: str = """
CALL apoc.load.csv($csv_path) yield map as line
CREATE (:Node {id:toInteger(line.id)})
"""
session.run(nodes_query, csv_path='file:///nodes.csv')
# リレーション用CSVデータをロード
relations_query: str = """
CALL apoc.load.csv($csv_path) yield map as line
MATCH (nf:Node {id:toInteger(line.from_id)}), (nt:Node {id:toInteger(line.to_id)})
CREATE (nf)-[:LINKED {score: toFloat(line.score)}]->(nt)
"""
session.run(relations_query, csv_path='file:///relations.csv')
# リレーション用CSVデータをロード(バッチインサート版)
relations_batch_query: str = """
CALL apoc.periodic.iterate('
CALL apoc.load.csv($path) yield map as line
', '
MATCH (nf:Node {id:toInteger(line.from_id)}), (nt:Node {id:toInteger(line.to_id)})
CREATE (nf)-[:LINKED {score: toFloat(line.score)}]->(nt)
', {batchSize:10000, iterateList:true, parallel:false, params:{path:$csv_path}});
"""
session.run(relations_batch_query, csv_path='file:///relations.csv')
方法3: apoc.import.csv
公式サイトはこちら
既存のデータベースに小規模から中規模のデータをロードしたい場合、こちらを利用すると良いと思います。実際にやってみたところ、リレーション100万件で50秒だったので、1000万件ぐらいまでだったら十分実用的ではないかと思います。
特徴
- 小規模〜中規模のデータロードに使える
- Neo4j import toolのheader formatを利用して、ID値、リレーションを結ぶノードのLABEL、プロパティ名・型などを指定する
- ノードとリレーションのCSVを一括でアップロードし、その中に含まれるノード間にリレーションを設定する(CSV内に存在しないノードを指定したリレーションは無視される)
CSV
CSV LOAD
のCSVファイルと、ヘッダーの記載方法だけ変わっています。
-
nodes.csv
:ID 1 2 3 4 5 6 7 8 9
-
relations.csv
Node:START_ID,Node:END_ID,score:float 1,2,0.412 1,3,0.623 3,4,0.512 4,5,0.79 6,7,0.31 7,8,0.79
python実装
ノードのLABEL
やリレーションのType
はprocedureのオプションとして指定します。
from neo4j import GraphDatabase, Driver
driver: Driver = GraphDatabase.driver('bolt://localhost:7687', auth=('neo4j', 'password'), encrypted=False)
with driver.session() as session:
query: str = """
CALL apoc.import.csv(
[{fileName: $node_csv_path, labels: ['Node']}],
[{fileName: $relation_csv_path, type: 'LINKED'}],
{}
)
"""
session.run(query, node_csv_path=f'file:///nodes.csv', relation_csv_path=f'file:///relations.csv')
方法4: neo4j-admin import
公式サイトはこちら
neo4j-admin
のコマンドラインツールの「import
コマンドを利用する方法です。空のDBに、億を超える大量データをロードするようなケースで利用できると思います。
実際にやってみたところ、リレーション3億件で60分ほどでした。
特徴
- 大規模のデータロードに使える
- 既存データが存在するDBには使えない(空のDBにだけ使える)
- 利用ケースによってはこれが致命的w
- Neo4jのDBサーバ側でCLIを実行する必要があるので、pythonから実行するには結構工夫が必要
- Neo4j import toolのheader formatを利用して、ID値、リレーションを結ぶノードのLABEL、プロパティ名・型などを指定する
- ノードとリレーションのCSVを一括でアップロードし、その中に含まれるノード間にリレーションを設定する(CSV内に存在しないノードを指定したリレーションは無視される)
処理の流れ
neo4j-admin
はCLIツールで、DBサーバ内で実行する必要があるため、pythonから実行するためには以下のように結構トリッキーな流れになりました。
- pythonでCSVファイルを作成(この部分の実装は記事上は割愛)
- pythonから
subprocess.call
でdocker-compose up
を実行する - dockerコンテナ起動時に、
EXTENSION_SCRIPT
で指定したbashスクリプトを実行する - bashスクリプト内で、neo4j-adminコマンドを利用しCSVファイルをロードする
- pythonでCSVファイルを削除(この部分の実装は記事上は割愛)
CSV
今回は大量データを想定して、複数CSVファイルに別れて、CSVのヘッダーを別ファイルで指定するケースを想定します。ファイルを分割してるだけで、ファイルの内容はapoc.import.csv
と同様です。
-
nodes_header.csv
id:ID
-
nodes_data_01.csv
1 2 3 4
-
nodes_data_02.csv
5 6 7 8 9
-
relations_header.csv
Node:START_ID,Node:END_ID,score:float
-
relations_data_01.csv
1,2,0.412 1,3,0.623 3,4,0.512
-
relations_data_02.csv
4,5,0.79 6,7,0.31 7,8,0.79
docker-compose.yaml
EXTENSION_SCRIPT
でコンテナ起動時に実行するスクリプトを指定しています。
また、このファイルはDBサーバ内に存在する必要があるため、ホストマシン側のフォルダをdockerコンテナ側の/script
フォルダにマウントしています。
version: '3'
services:
neo4j:
image: neo4j:latest
ports:
- "7474:7474" # 管理画面用port
- "7687:7687" # websocket用port
volumes:
- ${HOME}/neo4j/data:/data
- ${HOME}/neo4j/logs:/logs
- ${HOME}/neo4j/conf:/conf
- ${HOME}/neo4j/import:/import # ここにCSVファイルを配置
- ${HOME}/neo4j/script:/script # ここに起動時実行するスクリプトを配置
environment:
- NEO4J_AUTH=neo4j/password
- EXTENSION_SCRIPT=/script/import_csv.sh # 起動時に実行するスクリプト
import_csv.sh(コンテナ起動時に実行されるシェル)
/import
フォルダにCSVファイルが存在する場合、既存のDBを削除して、CSVロードを実行します。
CSVファイルは正規表現を使って指定することができます。
#!/bin/bash
set -euC
# CSVファイルがなければ何もしない
if [[ "$(ls -1 /import | wc -l)" == "0" ]]; then
echo "import csv skipped."
return
fi
# データを全削除
echo "delete database started."
rm -rf /var/lib/neo4j/data/databases
rm -rf /var/lib/neo4j/data/transactions
echo "delete database finished."
# CSVインポート
echo "importing csv started."
/var/lib/neo4j/bin/neo4j-admin import \
--id-type=INTEGER \
--nodes="/import/nodes_header.csv,/import/nodes_data_[0-9]+.csv" \
--relationships="/import/relations_header.csv,/import/relations_data_[0-9]+.csv"
echo "importing csv finished."
python実装
いくつかポイントがあります。
-
with
で扱えるように__ente__
でサーバを起動し、__exit__
でサーバを停止する作りにしています。 - バックグラウンドでdockerの立ち上げをするために
-d
オプションをつけています - Neo4jが立ち上がったかどうかは、Neo4jに接続してみて
ServiceUnavailable
が発生するかどうかで判断しています
from neo4j import GraphDatabase, Driver
from neobolt.exceptions import ServiceUnavailable
import subprocess
import time
import traceback
class Neo4jServer:
def __enter__(self):
"""
dockerコンテナを起動する
"""
subprocess.call(['docker-compose', 'up', '-d'])
while self.__neo4j_available() is False:
time.sleep(10)
return self
def __exit__(self, exc_type, exc_value, tb):
"""
dockerコンテナを停止する
"""
if tb is not None:
print(''.join(traceback.format_tb(tb)))
subprocess.call(['docker-compose', 'stop'])
def __neo4j_available(self) -> bool:
"""
Neo4jが利用可能かどうかをチェックする
"""
try:
GraphDatabase.driver('bolt://localhost:7687', auth=('neo4j', 'password'), encrypted=False)
except ServiceUnavailable:
print('neo4j is not available yet.')
return False
print('neo4j is already available.')
return True
# 実際の利用イメージ
with Neo4jServer():
driver: Driver = GraphDatabase.driver('bolt://localhost:7687', auth=('neo4j', 'password'), encrypted=False)
with driver.session() as session:
print(f"node_count: {session.run('MATCH (n:Node) RETURN count(n) as cnt').single()['cnt']}")
print(f"relationship_count: {session.run('MATCH ()-[l:LINKED]->() RETURN count(l) as cnt').single()['cnt']}")
ログ
-
pythonの実行ログ
Starting nayose_processing_neo4j_1 ... done // dockerコンテナ立ち上げ neo4j is not available yet. // CSVロード中 neo4j is not available yet. // CSVロード中 neo4j is not available yet. // CSVロード中 neo4j is not available yet. // CSVロード中 neo4j is not available yet. // CSVロード中 neo4j is already available. // CSVロード終了+Neo4jサーバ立ち上げ完了 node_count: 9 // クエリでノードが9件登録されていることを確認 relationship_count: 6 // クエリでリレーションが6件登録されていることを確認 Stopping nayose_processing_neo4j_1 ... done // dockerコンテナ停止
-
docker-compose ログ
docker-compose logs
で確認したログです。ちょっと長いですが、参考までに載せておきます。neo4j_1 | Warning: Folder mounted to "/data/databases" is not writable from inside container. Changing folder owner to neo4j. neo4j_1 | Changed password for user 'neo4j'. neo4j_1 | delete database started. neo4j_1 | delete database finished. neo4j_1 | importing csv started. neo4j_1 | Neo4j version: 4.0.1 neo4j_1 | Importing the contents of these files into /data/databases/neo4j: neo4j_1 | Nodes: neo4j_1 | /import/nodes_header.csv neo4j_1 | /import/nodes_data_01.csv neo4j_1 | /import/nodes_data_02.csv neo4j_1 | neo4j_1 | Relationships: neo4j_1 | /import/relations_header.csv neo4j_1 | /import/relations_data_01.csv neo4j_1 | /import/relations_data_02.csv neo4j_1 | neo4j_1 | neo4j_1 | Available resources: neo4j_1 | Total machine memory: 9.735GiB neo4j_1 | Free machine memory: 8.790GiB neo4j_1 | Max heap memory : 3.833GiB neo4j_1 | Processors: 4 neo4j_1 | Configured max memory: 5.311GiB neo4j_1 | High-IO: false neo4j_1 | neo4j_1 | Type normalization: neo4j_1 | Property type of 'score' normalized from 'float' --> 'double' in /import/relations_header.csv neo4j_1 | neo4j_1 | Import starting 2020-03-23 01:23:06.332+0000 neo4j_1 | Estimated number of nodes: 9.00 neo4j_1 | Estimated number of node properties: 9.00 neo4j_1 | Estimated number of relationships: 6.00 neo4j_1 | Estimated number of relationship properties: 6.00 neo4j_1 | Estimated disk space usage: 963B neo4j_1 | Estimated required memory usage: 1020MiB neo4j_1 | neo4j_1 | (1/4) Node import 2020-03-23 01:23:06.380+0000 neo4j_1 | Estimated number of nodes: 9.00 neo4j_1 | Estimated disk space usage: 513B neo4j_1 | Estimated required memory usage: 1020MiB neo4j_1 | -......... .......... .......... .......... .......... 5% ∆97ms neo4j_1 | .......... .......... .......... .......... .......... 10% ∆4ms neo4j_1 | .......... .......... .......... .......... .......... 15% ∆2ms neo4j_1 | .......... .......... .......... .......... .......... 20% ∆3ms neo4j_1 | .......... .......... .......... .......... .......... 25% ∆2ms neo4j_1 | .......... .......... .......... .......... .......... 30% ∆2ms neo4j_1 | .......... .......... .......... .......... .......... 35% ∆2ms neo4j_1 | .......... .......... .......... .......... .......... 40% ∆3ms neo4j_1 | .......... .......... .......... .......... .......... 45% ∆1ms neo4j_1 | .......... .......... .......... .......... .......... 50% ∆3ms neo4j_1 | .......... .......... .......... .......... .......... 55% ∆1ms neo4j_1 | .......... .......... .......... .......... .......... 60% ∆1ms neo4j_1 | .......... .......... .......... .......... .......... 65% ∆2ms neo4j_1 | .......... .......... .......... .......... .......... 70% ∆1ms neo4j_1 | .......... .......... .......... .......... .......... 75% ∆1ms neo4j_1 | .......... .......... .......... .......... .......... 80% ∆0ms neo4j_1 | .......... .......... .......... .......... .......... 85% ∆9ms neo4j_1 | .......... .......... .......... .......... .......... 90% ∆5ms neo4j_1 | .......... .......... .......... .......... .......... 95% ∆6ms neo4j_1 | .......... .......... .......... .......... .......... 100% ∆6ms neo4j_1 | neo4j_1 | (2/4) Relationship import 2020-03-23 01:23:06.649+0000 neo4j_1 | Estimated number of relationships: 6.00 neo4j_1 | Estimated disk space usage: 450B neo4j_1 | Estimated required memory usage: 1.004GiB neo4j_1 | .......... .......... .......... .......... .......... 5% ∆72ms neo4j_1 | .......... .......... .......... .......... .......... 10% ∆5ms neo4j_1 | .......... .......... .......... .......... .......... 15% ∆4ms neo4j_1 | .......... .......... .......... .......... .......... 20% ∆4ms neo4j_1 | .......... .......... .......... .......... .......... 25% ∆4ms neo4j_1 | .......... .......... .......... .......... .......... 30% ∆2ms neo4j_1 | .......... .......... .......... .......... .......... 35% ∆3ms neo4j_1 | .......... .......... .......... .......... .......... 40% ∆2ms neo4j_1 | .......... .......... .......... .......... .......... 45% ∆3ms neo4j_1 | .......... .......... .......... .......... .......... 50% ∆2ms neo4j_1 | .......... .......... .......... .......... .......... 55% ∆0ms neo4j_1 | .......... .......... .......... .......... .......... 60% ∆1ms neo4j_1 | .......... .......... .......... .......... .......... 65% ∆0ms neo4j_1 | .......... .......... .......... .......... .......... 70% ∆0ms neo4j_1 | .......... .......... .......... .......... .......... 75% ∆0ms neo4j_1 | .......... .......... .......... .......... .......... 80% ∆1ms neo4j_1 | .......... .......... .......... .......... .......... 85% ∆0ms neo4j_1 | .......... .......... .......... .......... .......... 90% ∆0ms neo4j_1 | .......... .......... .......... .......... .......... 95% ∆1ms neo4j_1 | .......... .......... .......... .......... .......... 100% ∆0ms neo4j_1 | neo4j_1 | (3/4) Relationship linking 2020-03-23 01:23:06.754+0000 neo4j_1 | Estimated required memory usage: 1020MiB neo4j_1 | -......... .......... .......... .......... .......... 5% ∆28ms neo4j_1 | .......... .......... .......... .......... .......... 10% ∆5ms neo4j_1 | .......... .......... .......... .......... .......... 15% ∆4ms neo4j_1 | .......... .......... .......... .......... .......... 20% ∆4ms neo4j_1 | .......... .......... .......... .......... .......... 25% ∆4ms neo4j_1 | .......... .......... .......... .......... .......... 30% ∆4ms neo4j_1 | .......... .......... .......... .......... .......... 35% ∆4ms neo4j_1 | .......... .......... .......... .......... .......... 40% ∆4ms neo4j_1 | .......... .......... .......... .......... .......... 45% ∆3ms neo4j_1 | .......... .......... .......... .......... .......... 50% ∆2ms neo4j_1 | .......... .......... .......... .......... .......... 55% ∆0ms neo4j_1 | .......... .......... .......... .......... .......... 60% ∆0ms neo4j_1 | .......... .......... .......... .......... .......... 65% ∆0ms neo4j_1 | .......... .......... .......... .......... .......... 70% ∆0ms neo4j_1 | .......... .......... .......... .......... .......... 75% ∆0ms neo4j_1 | .......... .......... .......... .......... .......... 80% ∆0ms neo4j_1 | .......... .......... .......... .......... .......... 85% ∆1ms neo4j_1 | .......... .......... .......... .......... .......... 90% ∆0ms neo4j_1 | .......... .......... .......... .......... .......... 95% ∆0ms neo4j_1 | .......... .......... .......... .......... .......... 100% ∆0ms neo4j_1 | neo4j_1 | (4/4) Post processing 2020-03-23 01:23:06.938+0000 neo4j_1 | Estimated required memory usage: 1020MiB neo4j_1 | -......... .......... .......... .......... .......... 5% ∆15s 312ms neo4j_1 | .......... .......... .......... .......... .......... 10% ∆1ms neo4j_1 | .......... .......... .......... .......... .......... 15% ∆0ms neo4j_1 | .......... .......... .......... .......... .......... 20% ∆0ms neo4j_1 | .......... .......... .......... .......... .......... 25% ∆0ms neo4j_1 | .......... .......... .......... .......... .......... 30% ∆1ms neo4j_1 | .......... .......... .......... .......... .......... 35% ∆0ms neo4j_1 | .......... .......... .......... .......... .......... 40% ∆0ms neo4j_1 | .......... .......... .......... .......... .......... 45% ∆0ms neo4j_1 | .......... .......... .......... .......... .......... 50% ∆0ms neo4j_1 | .......... .......... .......... .......... .......... 55% ∆0ms neo4j_1 | .......... .......... .......... .......... .......... 60% ∆1ms neo4j_1 | .......... .......... .......... .......... .......... 65% ∆0ms neo4j_1 | .......... .......... .......... .......... .......... 70% ∆0ms neo4j_1 | .......... .......... .......... .......... .......... 75% ∆0ms neo4j_1 | .......... .......... .......... .......... .......... 80% ∆1ms neo4j_1 | .......... .......... .......... .......... .......... 85% ∆0ms neo4j_1 | .......... .......... .......... .......... .......... 90% ∆1ms neo4j_1 | .......... .......... .......... .......... .......... 95% ∆0ms neo4j_1 | .......... .......... .......... .......... .......... 100% ∆1ms neo4j_1 | neo4j_1 | neo4j_1 | IMPORT DONE in 16s 290ms. neo4j_1 | Imported: neo4j_1 | 9 nodes neo4j_1 | 6 relationships neo4j_1 | 15 properties neo4j_1 | Peak memory usage: 1.004GiB neo4j_1 | importing csv finished. neo4j_1 | Directories in use: neo4j_1 | home: /var/lib/neo4j neo4j_1 | config: /var/lib/neo4j/conf neo4j_1 | logs: /logs neo4j_1 | plugins: /var/lib/neo4j/plugins neo4j_1 | import: /import neo4j_1 | data: /var/lib/neo4j/data neo4j_1 | certificates: /var/lib/neo4j/certificates neo4j_1 | run: /var/lib/neo4j/run neo4j_1 | Starting Neo4j. neo4j_1 | 2020-03-23 01:23:23.348+0000 INFO ======== Neo4j 4.0.1 ======== neo4j_1 | 2020-03-23 01:23:23.357+0000 INFO Starting... neo4j_1 | 2020-03-23 01:23:43.913+0000 INFO Bolt enabled on 0.0.0.0:7687. neo4j_1 | 2020-03-23 01:23:43.917+0000 INFO Started. neo4j_1 | 2020-03-23 01:23:44.947+0000 INFO Remote interface available at http://0.0.0.0:7474/ neo4j_1 | 2020-03-23 01:23:53.825+0000 INFO Neo4j Server shutdown initiated by request neo4j_1 | 2020-03-23 01:23:53.880+0000 INFO Stopping... neo4j_1 | 2020-03-23 01:23:59.137+0000 INFO Stopped.