背景:なぜ数あるデータベースの中からNeo4jを選んだのか?
エンジニアになりたての頃、私はMySQLやPostgreSQLがあれば世の中のすべての問題が解決できると信じていました。しかし、現実はそう甘くはありません。プロジェクトが拡大し、「友達のレコメンド機能」や「ユーザー行動分析」の実装が求められるようになると、苦難の道が始まりました。
従来のSQLでは、「友達の友達の友達」(3階層のリレーションシップ)を探すことは非常に過酷なタスクです。5〜7つのテーブルを連続してJOINしなければなりません。その時のクエリは非常に長くなるだけでなく、パフォーマンスも劇的に低下します。実際、約100万件のデータセットでは、この種の複雑なクエリのレスポンスに20〜30秒かかることもあり、これは本番環境では到底許容できない数値でした。
Neo4jこそが、この問題に対する「欠けていたパズルの一片」でした。硬直化した行・列形式の保存スタイルとは異なり、Neo4jはNode(ノード)とRelationship(リレーションシップ)を第一級市民として扱います。リレーションシップは物理的にディスク上に保存されます。これにより、データベースがリンクを探すためにテーブル全体のインデックスをスキャンする必要がないため、複雑に絡み合った接続のクエリがSQLよりも数千倍も高速になります。
Neo4jのインストール:Dockerで手軽に構築
ローカル環境を汚さず管理しやすくするため、私は常にDockerの使用を優先しています。たった一つのコマンドで、Neo4jを試せる環境が整います。
以下のコマンドを実行して、イメージをプルし、コンテナを即座に起動します:
docker run -d \
--name neo4j_itfromzero \
-p 7474:7474 -p 7687:7687 \
-v $HOME/neo4j/data:/data \
-v $HOME/neo4j/logs:/logs \
-e NEO4J_AUTH=neo4j/password123 \
neo4j:latest
各ポートの意味を簡単に説明します:
- 7474: Neo4j Browser(HTTP)へのアクセスポート。クエリを視覚的に実行できます。
- 7687: Boltプロトコル用ポート。PythonやNode.jsアプリからデータベースに直接接続するための「ホットライン」です。
完了したら、http://localhost:7474を開き、ユーザー名 neo4j、パスワード password123 でログインすれば、グラフを描き始めることができます。
CypherとPythonによる実践
Neo4jを操作するには、Cypherという言語を使用します。無機質なSELECT文の代わりに、Cypherではノードに ()、リレーションシップに -[]-> といった記号を使います。クエリを見ると、まるでマインドマップのような直感的な構造をしています。
1. サンプルデータの即時作成
直感的な操作を体感するために、簡単な友人ネットワークを作成してみましょう:
CREATE (sato:Person {name: '佐藤', age: 25})
CREATE (suzuki:Person {name: '鈴木', age: 28})
CREATE (tanaka:Person {name: '田中', age: 22})
CREATE (sato)-[:FRIEND]->(suzuki)
CREATE (suzuki)-[:FRIEND]->(tanaka)
この例では、Person がテーブル名のような役割を果たし、{} 内の属性がオブジェクトのデータになります。
2. Pythonでのレコメンドロジック構築
ここからが面白いところです。Pythonを使って「佐藤さんの友人の知人だが、佐藤さんとはまだ繋がっていない人」を探します。これはFacebookの「知り合いかもしれません」機能の根幹となるロジックです。
まずはライブラリをインストールします:
pip install neo4j
以下は、バックエンドですぐに応用できるよう簡略化したコードです:
from neo4j import GraphDatabase
class Neo4jApp:
def __init__(self, uri, user, password):
self.driver = GraphDatabase.driver(uri, auth=(user, password))
def recommend_friends(self, person_name):
with self.driver.session() as session:
# わずか3行のCypherで、SQLにおける数十行のJOINを置き換えられます
query = """
MATCH (p:Person {name: $name})-[:FRIEND]->(friend)-[:FRIEND]->(fof)
WHERE NOT (p)-[:FRIEND]->(fof) AND p <> fof
RETURN DISTINCT fof.name AS recommended_friend
"""
result = session.run(query, name=person_name)
return [record["recommended_friend"] for record in result]
app = Neo4jApp("bolt://localhost:7687", "neo4j", "password123")
print(f"佐藤さんへのレコメンド: {app.recommend_friends('佐藤')}")
app.close()
実践で得た教訓:モニタリングと最適化
Neo4jを本番環境に導入する際、RAMは最も重要な要素です。Neo4jは、グラフ全体をキャッシュして可能な限り高速にアクセスしようとするため、メモリを「貪欲」に消費する傾向があります。
1. 適切なメモリ設定
サーバーがすぐにフリーズしないよう、デフォルト設定のままにしないでください。neo4j.conf で以下の2つのパラメータを調整する必要があります:
dbms.memory.heap.max_size: クエリ処理用のRAM制限。ガベージコレクターの負荷を避けるため、大きくしすぎないようにします。dbms.memory.pagecache.size: ディスクからのデータキャッシュ用。例えばサーバーに16GBのRAMがある場合、最適なファイル読み込み速度を確保するために、この部分に8GB程度を割り当てることが多いです。
2. INDEXとPROFILEを忘れずに
SQLと同様に、INDEXを貼らなければ、Neo4jはノード全体をスキャン(Node Scan)することになります。クエリを確定させる前に、必ず PROFILE コマンドを使用して db hits の数値を確認してください。CREATE INDEX FOR (p:Person) ON (p.name) のような簡単な操作一つで、検索速度が数秒から数ミリ秒へと劇的に向上します。
3. システムの「限界」のサイン
サーバーが突然重くなった場合は、docker logs -f neo4j_itfromzero でログをすぐに確認してください。通常、エラーの90%はページキャッシュの溢れか、意図せず「デカルト積(cartesian product)」を生成するCypher文を書いてしまい、メモリ内の一時データが爆発したことが原因です。
Neo4jは強力な武器ですが、単純なフラットデータに乱用しないでください。注文情報や基本的なユーザー情報の保存だけであれば、PostgreSQLの方が安全な選択です。しかし、あなたの課題が複雑に絡み合ったリレーションシップに満ちているなら、Neo4jを輝かせましょう。

