Mac miniのDockerでNeo4j 5 + APOCを起動し、CSV読み込みまで確認する

Mac miniのDockerでNeo4j 5 + APOCを起動し、CSV読み込みまで確認する

Mac mini上のDockerでNeo4jを動かし、Graph RAGや金融データ分析、購買管理、在庫・レシピ管理などに使えるグラフDB環境を作っていきます。

以前、M1 MacBook AirでNeo4jをDockerから起動したことがありました。

今回はMac mini側で、最初から以下を意識した構成にします。

  • Docker Composeで管理する
  • Neo4j 5を使う
  • APOC Coreを有効化する
  • JSON / CSV入出力を扱えるようにする
  • import ディレクトリを固定する
  • 外部公開せず、SSHトンネル経由で安全に使う

今回の目的

今回の目的は、Mac mini上でNeo4jを安定して起動し、APOCとCSV読み込みを確認することです。

最終的には、以下のような用途に使う予定です。

  • 自動売買・金融分析
  • 為替・経済指標データの関係管理
  • 購買管理
  • 店舗・価格データの管理
  • 在庫・レシピ管理
  • 防災・非常食管理
  • 株式・四季報・企業関係の整理
  • Graph RAG

作業フォルダを作成する

まず、ホームディレクトリにNeo4j用の作業フォルダを作成します。

mkdir -p ~/neo4j-stack
cd ~/neo4j-stack

docker-compose.ymlを作成する

docker-compose.yml を作成します。

vim docker-compose.yml

最初に作成した内容は以下です。

version: "3.9"

services:
  neo4j:
    image: neo4j:5
    container_name: neo4j
    restart: unless-stopped

    # 安全寄り:Mac mini 内だけで使う(外部からは SSH トンネル推奨)
    # MacBook Air から LAN 直で繋ぐなら 127.0.0.1 を外して "7474:7474" / "7687:7687" に変更
    ports:
      - "127.0.0.1:7474:7474"   # Neo4j Browser (HTTP)
      - "127.0.0.1:7687:7687"   # Bolt

    environment:
      # 認証(.env に書く想定)
      NEO4J_AUTH: ${NEO4J_AUTH}

      # APOC Core(互換版を自動取得)
      NEO4J_PLUGINS: '["apoc"]'

      # セキュリティ:APOC のみ許可(まず安全側)
      NEO4J_dbms_security_procedures_allowlist: "apoc.*"

      # JSON/CSV をファイルで扱う(購買管理・財務データ・指標データ取り込みに必須)
      NEO4J_apoc_import_file_enabled: "true"
      NEO4J_apoc_export_file_enabled: "true"
      NEO4J_apoc_import_file_use__neo4j__config: "true"

      # ファイルアクセスは /import のみに固定(安全)
      NEO4J_dbms_directories_import: "/import"

      # 自動売買/分析用途:安定化(まずは控えめ、必要なら増やす)
      NEO4J_dbms_memory_heap_initial__size: "2G"
      NEO4J_dbms_memory_heap_max__size: "6G"
      NEO4J_dbms_memory_pagecache_size: "4G"

      # 追加のログ(必要なら)
      # NEO4J_dbms_logs_query_enabled: "true"
      # NEO4J_dbms_logs_query_threshold: "2s"

    # 同時接続・大量処理の安全マージン
    ulimits:
      nofile:
        soft: 40000
        hard: 40000

    volumes:
      # データとログは named volume(権限事故が少なく安定)
      - neo4j_data:/data
      - neo4j_logs:/logs

      # import はホストのフォルダに(JSON/CSV を Finder で置ける)
      - ./import:/import

volumes:
  neo4j_data:
  neo4j_logs:

ただし、この設定は後でメモリ設定が原因で起動に失敗します。

そのため、最終的にはメモリ指定部分をコメントアウトします。

.envを作成する

同じディレクトリに .env を作成します。

vim .env

内容は以下です。

NEO4J_AUTH=neo4j/ChangeMe_1234_8chars

NEO4J_AUTH は、Neo4jの初期ユーザーとパスワードを指定する設定です。

今回は検証用なのでこの値にしていますが、実運用では必ず強いパスワードに変更します。

importフォルダを作成する

CSVやJSONを配置するための import フォルダを作成します。

mkdir -p import

Neo4jを起動する

Docker Composeで起動します。

docker compose up -d

実行結果は以下です。

WARN[0000] /Users/snowpool/neo4j-stack/docker-compose.yml: the attribute `version` is obsolete, it will be ignored, please remove it to avoid potential confusion 
[+] Running 9/9
 ✔ neo4j Pulled                                                          14.6s 
   ✔ 4f4fb700ef54 Pull complete                                           1.0s 
   ✔ 40745bfacd19 Pull complete                                           1.0s 
   ✔ c192da3a442c Pull complete                                           1.0s 
   ✔ 4cb50a53c4c3 Pull complete                                          10.0s 
   ✔ 2b952d66bc8c Pull complete                                           2.3s 
   ✔ ff17433fd932 Pull complete                                           9.5s 
   ✔ c6f8b1ffa89b Download complete                                       0.0s 
   ✔ 5a70d00e19a9 Download complete                                       0.1s 
[+] Running 4/4
 ✔ Network neo4j-stack_default    Created
 ✔ Volume neo4j-stack_neo4j_data  Created
 ✔ Volume neo4j-stack_neo4j_logs  Created
 ✔ Container neo4j                Started

version がobsoleteという警告が出ていますが、これはDocker Composeの新しい仕様では version 属性が不要になっているためです。

この時点ではコンテナが起動したように見えます。

コンテナの状態を確認する

コンテナの状態を確認します。

docker ps

結果は以下でした。

CONTAINER ID   IMAGE     COMMAND                   CREATED          STATUS                         PORTS     NAMES
c026244bbc0f   neo4j:5   "tini -g -- /startup…"   32 seconds ago   Restarting (3) 3 seconds ago             neo4j

Restarting になっており、正常起動していません。

ログを確認する

原因を確認するためにログを見ます。

docker logs neo4j --tail 200

重要なエラーは以下です。

ERROR Invalid memory configuration - exceeds physical memory. Check the configured values for server.memory.pagecache.size and server.memory.heap_max_size

Neo4jに割り当てたメモリ量が、Docker Desktopが認識している物理メモリ上限を超えているため、起動直後に終了していました。

原因:Neo4jのメモリ指定が大きすぎた

原因になっていたのは、以下の設定です。

NEO4J_dbms_memory_heap_initial__size: "2G"
NEO4J_dbms_memory_heap_max__size: "6G"
NEO4J_dbms_memory_pagecache_size: "4G"

合計で約10GBをNeo4jに要求しています。

macOS上のDocker Desktopでは、ホストMacのメモリが16GBあっても、Docker Desktop側に割り当てられているメモリ上限が小さい場合があります。

そのため、Neo4jから見ると物理メモリ不足になり、以下のエラーで終了していました。

Invalid memory configuration - exceeds physical memory

対処:メモリ指定をコメントアウトする

今回はまずNeo4jを起動することを優先し、メモリ指定をコメントアウトしました。

# NEO4J_dbms_memory_heap_initial__size: "2G"
# NEO4J_dbms_memory_heap_max__size: "6G"
# NEO4J_dbms_memory_pagecache_size: "4G"

必要になったら、Docker Desktop側のメモリ割り当てと合わせて、後から調整する方針にします。

コンテナを停止・削除する

設定を修正したら、いったんコンテナを停止・削除します。

docker compose down

結果は以下です。

WARN[0000] /Users/snowpool/neo4j-stack/docker-compose.yml: the attribute `version` is obsolete, it will be ignored, please remove it to avoid potential confusion 
[+] Running 2/2
 ✔ Container neo4j              Removed
 ✔ Network neo4j-stack_default  Removed

docker compose down により、コンテナ停止、コンテナ削除、作成されたネットワークの削除が行われます。

再度起動する

修正後、再度起動します。

docker compose up -d

結果は以下です。

WARN[0000] /Users/snowpool/neo4j-stack/docker-compose.yml: the attribute `version` is obsolete, it will be ignored, please remove it to avoid potential confusion 
[+] Running 1/1
 ✔ neo4j Pulled
[+] Running 2/2
 ✔ Network neo4j-stack_default  Created
 ✔ Container neo4j              Started

コンテナ状態を確認します。

docker ps

今度は正常に起動しました。

CONTAINER ID   IMAGE     COMMAND                   CREATED          STATUS          PORTS                                                NAMES
e766ab83404e   neo4j:5   "tini -g -- /startup…"   40 seconds ago   Up 40 seconds   127.0.0.1:7474->7474/tcp, 127.0.0.1:7687->7687/tcp   neo4j

Mac mini上でNeo4jのHTTP応答を確認する

Mac mini上で、Neo4j Browser用のHTTPポートを確認します。

curl http://localhost:7474

結果は以下です。

{"bolt_routing":"neo4j://localhost:7687","query":"http://localhost:7474/db/{databaseName}/query/v2","transaction":"http://localhost:7474/db/{databaseName}/tx","bolt_direct":"bolt://localhost:7687","neo4j_version":"5.26.19","neo4j_edition":"community"}

Neo4j 5.26.19 Community Editionが起動していることを確認できました。

MacBook AirからSSHトンネルで接続する

今回はNeo4jのポートをMac mini内の 127.0.0.1 に閉じています。

そのため、MacBook Airから直接LANで接続するのではなく、SSHトンネルを使います。

MacBook Air側で以下を実行します。

ssh -N -L 7474:localhost:7474 -L 7687:localhost:7687 macmini

この状態で、MacBook Airのブラウザから以下へアクセスします。

http://localhost:7474

Neo4j Browserが表示されれば成功です。

Neo4j Browserへログインする

ログイン時の設定は以下です。

  • Connect URL:neo4j://localhost:7687
  • Username:neo4j
  • Password:ChangeMe_1234_8chars
  • Database:空欄のままでOK

Database は空欄で問題ありません。

Neo4j Community Editionの単一DB構成では、デフォルトデータベースである neo4j に接続されます。

最初は1DB構成で進める

今回の用途では、最初は1つのデータベースにまとめ、ノードラベルで分ける方針にします。

例えば、以下のようなデータを扱う予定です。

  • 自動売買・為替・経済指標
  • 購買管理・店舗・価格
  • 在庫・レシピ
  • 防災・非常食
  • 株式・四季報・企業関係
  • Graph RAG

データベースを分けるのは、データ量や権限管理が問題になってからで十分と判断しました。

APOCの動作確認

ログイン後、APOCが使えるか確認します。

RETURN apoc.version();

結果は以下です。

apoc.version()
"5.26.19"

Neo4j 5.26.xに対応するAPOC Coreがロードされています。

これで、Docker、plugins、allowlist設定は成功です。

JSON関連のAPOC機能を確認する

次に、JSON関連のAPOC機能を確認します。

CALL apoc.help("json");

結果として、以下のようなJSON関連のprocedureやfunctionが表示されました。

apoc.export.json.all
apoc.export.json.data
apoc.export.json.graph
apoc.export.json.query
apoc.import.json
apoc.load.json
apoc.load.jsonArray
apoc.load.jsonParams
apoc.convert.fromJsonList
apoc.convert.fromJsonMap
apoc.convert.toJson
apoc.json.path

これで、Graph RAG、JSONインポート、エクスポート、高度なグラフ操作に進める状態になりました。

CSV読み込み用のテストファイルを作成する

次に、CSV読み込みを試します。

Mac mini側で import/test.csv を作成します。

vim import/test.csv

内容は以下です。

id,name,value
1,USDJPY,147.25
2,EURUSD,1.084
3,WTI,78.3

apoc.load.csvは使えなかった

最初に、APOCのCSV読み込みを試しました。

CALL apoc.load.csv("file:///test.csv") YIELD map
RETURN map;

しかし、以下のエラーになりました。

Neo.ClientError.Procedure.ProcedureNotFound
There is no procedure with the name `apoc.load.csv` registered for this database instance. Please ensure you've spelled the procedure name correctly and that the procedure is properly deployed.

apoc.load.csv が登録されていないようです。

確認のため、以下を実行しました。

CALL apoc.help("load.csv");

結果は何も返りませんでした。

(no changes, no records)

さらに、APOCのload系procedureを確認します。

SHOW PROCEDURES YIELD name
WHERE name STARTS WITH "apoc.load."
RETURN name
ORDER BY name;

結果は以下です。

name
"apoc.load.json"
"apoc.load.jsonArray"
"apoc.load.jsonParams"
"apoc.load.xml"

この環境では、apoc.load.csv は使えないことが分かりました。

標準のLOAD CSVを使う

Neo4jには標準で LOAD CSV があります。

そのため、CSV読み込みは標準の LOAD CSV を使います。

LOAD CSV WITH HEADERS FROM 'file:///test.csv' AS row
RETURN row
LIMIT 10;

結果は以下です。

row
{
  "id": "1",
  "name": "USDJPY",
  "value": "147.25"
}
{
  "id": "2",
  "name": "EURUSD",
  "value": "1.084"
}
{
  "id": "3",
  "name": "WTI",
  "value": "78.3"
}

import/test.csv を問題なく読み込めています。

CSVを読み込んでAssetノードを作成する

CSVから Asset ノードを作成します。

LOAD CSV WITH HEADERS FROM 'file:///test.csv' AS row
MERGE (a:Asset {name: row.name})
SET a.value = toFloat(row.value);

中身を確認します。

MATCH (a:Asset)
RETURN a;

結果は以下です。

╒═══════════════════════════════════════╕
│a                                      │
╞═══════════════════════════════════════╡
│(:Asset {name: "USDJPY",value: 147.25})│
├───────────────────────────────────────┤
│(:Asset {name: "EURUSD",value: 1.084}) │
├───────────────────────────────────────┤
│(:Asset {name: "WTI",value: 78.3})     │
└───────────────────────────────────────┘

重複防止の制約を作成する

MERGE (a:Asset {name: row.name}) を使っているため、理論上は同じnameのAssetは重複しにくいです。

ただし、実務では制約を張っておく方が安全です。

CREATE CONSTRAINT asset_name_unique IF NOT EXISTS
FOR (a:Asset)
REQUIRE a.name IS UNIQUE;

結果は以下です。

Added 1 constraint, completed after 52 ms.

更新時刻とデータ元を付ける

自動売買や分析用途では、値だけでなく「いつ・どこから来たデータか」が重要です。

そこで、更新時刻とデータ元を追加します。

LOAD CSV WITH HEADERS FROM 'file:///test.csv' AS row
MERGE (a:Asset {name: row.name})
SET
  a.value = toFloat(row.value),
  a.updated_at = datetime(),
  a.source = "test_csv";

結果は以下です。

Set 9 properties, completed after 31 ms.

時系列データはAsset直書きより分離した方がよい

ここまでの方法では、最新値を Asset ノードに直接書いています。

ただし、為替やコモディティのように時系列を扱う場合は、価格データを別ノードに分けた方が扱いやすくなります。

考え方としては、以下のようなモデルです。

(:Asset)-[:HAS_PRICE]->(:Price)

ただし、これは図のパターン表記です。

そのままNeo4j Browserで実行するとエラーになります。

Cypherとして実行するには、MATCHCREATEMERGE などの命令文が必要です。

電源断後にDocker Desktopが起動していなかった

途中でコンセント工事のため、Mac miniの電源を落としました。

再度電源を入れた後、Neo4jを起動しようとすると、以下のエラーになりました。

cd ~/neo4j-stack
docker compose up -d
WARN[0000] /Users/snowpool/neo4j-stack/docker-compose.yml: the attribute `version` is obsolete, it will be ignored, please remove it to avoid potential confusion 
unable to get image 'neo4j:5': Cannot connect to the Docker daemon at unix:///Users/snowpool/.docker/run/docker.sock. Is the docker daemon running?

原因は、Docker Desktopが起動していないことでした。

MacではDocker Desktopを起動しないと、Docker daemonに接続できません。

以下でDocker Desktopを起動します。

open -a Docker

その後、再度起動します。

docker compose up -d

結果は以下です。

WARN[0000] /Users/snowpool/neo4j-stack/docker-compose.yml: the attribute `version` is obsolete, it will be ignored, please remove it to avoid potential confusion 
[+] Running 1/1
 ✔ Container neo4j  Running

これでNeo4jが再び動作するようになりました。

AssetとPriceの関係を確認する

続きとして、まず既存の AssetPrice の関係を確認します。

MATCH p = (:Asset)-[:HAS_PRICE]->(:Price)
RETURN p
LIMIT 25;

まだ作成していない場合は、何も返りません。

CSVからAssetとPriceを作成する

今回の test.csv には、idnamevalue だけがあり、観測日時はありません。

そのため、まずは datetime() で現在時刻を入れる形にします。

LOAD CSV WITH HEADERS FROM 'file:///test.csv' AS row
MERGE (a:Asset {name: row.name})
CREATE (p:Price {
  value: toFloat(row.value),
  observed_at: datetime(),
  source: 'test.csv'
})
MERGE (a)-[:HAS_PRICE]->(p);

中身を確認します。

MATCH p = (a:Asset)-[:HAS_PRICE]->(pr:Price)
RETURN a.name, pr.value, pr.observed_at
ORDER BY pr.observed_at DESC
LIMIT 50;

結果は以下です。

a.name    pr.value    pr.observed_at
"USDJPY"  147.25      "2026-01-10T21:49:59.314000000Z"
"EURUSD"  1.084       "2026-01-10T21:49:59.314000000Z"
"WTI"     78.3        "2026-01-10T21:49:59.314000000Z"

同じ日時でPriceが増えすぎる問題

上記の方法では、CSVを読み込むたびに新しい Price ノードが作られます。

履歴として残すならそれでもよいですが、同じデータを何度も読み込むと、Price が増えすぎる可能性があります。

本来は、CSV側に観測日や時刻を入れるのがベストです。

ただし、今回のようにCSVに日付がない場合は、「今日の最新値」として扱う方法もあります。

1銘柄×1日で重複を抑える

とりあえず、1銘柄×1日で1レコードに落ち着くようにします。

LOAD CSV WITH HEADERS FROM 'file:///test.csv' AS row
MERGE (a:Asset {name: row.name})
MERGE (d:Date {date: date()})
MERGE (a)-[:HAS_PRICE]->(d)
SET d.value = toFloat(row.value),
    d.source = 'test.csv',
    d.updated_at = datetime();

確認します。

MATCH (a:Asset)-[:HAS_PRICE]->(d:Date)
RETURN a.name, d.date, d.value
ORDER BY d.date DESC, a.name
LIMIT 50;

結果は以下です。

a.name    d.date        d.value
"EURUSD"  "2026-01-10"  78.3
"USDJPY"  "2026-01-10"  78.3
"WTI"     "2026-01-10"  78.3

ただし、この結果を見ると、すべての Asset が同じ Date ノードに接続されているため、値が最後に読み込まれた WTI78.3 で上書きされています。

これは実務上は問題です。

注意:Dateノードだけにvalueを持たせると値が混ざる

今回のクエリでは、以下のように Date ノードを日付だけでMERGEしています。

MERGE (d:Date {date: date()})

この場合、USDJPY、EURUSD、WTIがすべて同じ日付ノードを共有します。

そのうえで、d.value に価格を入れているため、最後に処理された行の値で上書きされます。

その結果、以下のように全銘柄の値が 78.3 になってしまいました。

"EURUSD"  "2026-01-10"  78.3
"USDJPY"  "2026-01-10"  78.3
"WTI"     "2026-01-10"  78.3

これは「1日1レコード」にはなっていますが、「1銘柄×1日1レコード」にはなっていません。

改善案:DailyPriceノードを使う

1銘柄×1日で価格を管理するなら、以下のように DailyPrice ノードを作る方が分かりやすいです。

LOAD CSV WITH HEADERS FROM 'file:///test.csv' AS row
MERGE (a:Asset {name: row.name})
MERGE (p:DailyPrice {asset: row.name, date: date()})
SET
  p.value = toFloat(row.value),
  p.source = 'test.csv',
  p.updated_at = datetime()
MERGE (a)-[:HAS_DAILY_PRICE]->(p);

確認クエリは以下です。

MATCH (a:Asset)-[:HAS_DAILY_PRICE]->(p:DailyPrice)
RETURN a.name, p.date, p.value
ORDER BY p.date DESC, a.name;

この形なら、assetdate の組み合わせでノードが分かれるため、銘柄ごとの値が混ざりません。

改善案:制約も作成しておく

DailyPrice に対して、1銘柄×1日で一意になる制約を作成します。

CREATE CONSTRAINT daily_price_unique IF NOT EXISTS
FOR (p:DailyPrice)
REQUIRE (p.asset, p.date) IS UNIQUE;

これで、同じ銘柄・同じ日付の価格ノードが重複しにくくなります。

今回の到達点

今回の作業で、以下まで確認できました。

  • Mac mini上のDockerでNeo4j 5を起動できた
  • APOC Coreを有効化できた
  • Neo4j BrowserへSSHトンネル経由で接続できた
  • RETURN apoc.version(); でAPOCの動作確認ができた
  • apoc.load.csv は使えないことが分かった
  • 標準の LOAD CSV でCSVを読み込めた
  • CSVから Asset ノードを作成できた
  • 重複防止の制約を作成できた
  • 時系列データを分離する方向性を確認できた

ハマりどころ

Neo4jがRestartingを繰り返す

原因はメモリ設定でした。

Docker Desktopが認識しているメモリ上限を超える設定にすると、Neo4jは以下のエラーで起動できません。

Invalid memory configuration - exceeds physical memory

まずはメモリ指定を外して起動し、必要に応じて後から調整する方が安全です。

apoc.load.csvは使えなかった

今回のAPOC Core環境では、apoc.load.csv は登録されていませんでした。

CSV読み込みは、Neo4j標準の LOAD CSV を使えば問題ありません。

Docker Desktopを起動しないとdocker composeが使えない

MacではDocker Desktopが起動していないと、Docker daemonに接続できません。

電源断や再起動後に以下のエラーが出た場合は、Docker Desktopを起動します。

Cannot connect to the Docker daemon at unix:///Users/snowpool/.docker/run/docker.sock. Is the docker daemon running?
open -a Docker

Dateノードにvalueを持たせると値が混ざる

Date ノードを日付だけで共有し、そこに value を持たせると、複数銘柄の値が混ざります。

価格データは、DailyPricePrice のような別ノードに分ける方が安全です。

次にやること

今回はCSVを手動で作成して、Neo4jに読み込むところまで確認しました。

次は、実際のデータ取得処理を作ります。

  • 為替データの取得
  • WTIなどコモディティ価格の取得
  • FREDなど経済指標データの取得
  • SQLiteからNeo4jへの連携
  • Graph RAG用のノード設計

金融危機監視レーダーや購買管理システムで使っているデータを、Neo4j側にも展開していく予定です。

まとめ

Mac mini上のDockerでNeo4j 5 + APOCを起動し、Neo4j Browserへの接続、APOCの確認、CSV読み込みまで実施しました。

最初はメモリ設定が大きすぎてNeo4jが再起動ループになりましたが、メモリ指定をコメントアウトすることで起動できました。

また、apoc.load.csv は使えませんでしたが、標準の LOAD CSV でCSVを読み込めることを確認しました。

今後は、実際の為替・コモディティ・経済指標データを取得し、Neo4jに取り込んでGraph RAGや分析基盤に使える形へ進めていきます。

コメント

タイトルとURLをコピーしました