MiroFishのナレッジグラフバックエンドをZep CloudからNeo4jに移行した
群知能エンジンMiroFishのナレッジグラフバックエンドをZep CloudからローカルNeo4jに移行した。Zep APIとの互換レイヤーを維持しながら、コストゼロで大量ノード投入を可能にした設計判断について。
MiroFishのナレッジグラフバックエンドをZep CloudからNeo4jに移行した
MiroFishとは
MiroFishは群知能(Swarm Intelligence)エンジンで、シード情報からナレッジグラフを自律的に構築するシステム。企業情報や業界動向のエンティティを抽出し、関係性をグラフとして蓄積することで、業種予測やセクター分析を行う。
バックエンドのグラフDB操作は zep_tools.py(1,735行)に集約されており、Zep CloudのAPIを通じてグラフの読み書きを行っていた。
移行の背景
MiroFishのナレッジグラフ管理にZep Cloudを使っていた。Zepは会話メモリとグラフ検索をセットで提供するマネージドサービスで、立ち上げは速い。
問題は従量課金。グラフにデータを投入するほどコストが増え、実験的な大量投入がやりづらくなる。MiroFishで業種予測やセクター分析のために数千ノードを投入する計画があり、コストが読めない状態は避けたかった。
なぜNeo4jか
グラフDBの選択肢はいくつかある。
| DB | 特徴 | ローカル無料利用 |
|---|---|---|
| Neo4j Community | 最も成熟したグラフDB、Cypher言語 | 無制限 |
| Amazon Neptune | AWSマネージド、SPARQL対応 | なし(有料) |
| ArangoDB | マルチモデル(Document + Graph) | 無制限 |
| Memgraph | Neo4j互換、インメモリ | 無制限 |
Neo4jを選んだ理由:
- エコシステムの成熟度 — ドライバ、可視化ツール、チュートリアルが豊富
- Cypher言語 — グラフクエリが直感的に書ける
- Community Editionが無制限 — ローカルで好きなだけデータを投入できる
- LLM連携の事例が多い — LangChainやLlamaIndexとの統合が充実
移行の設計
データモデルの変換
Zepのメモリ構造をNeo4jのグラフモデルに変換する。
cypher// ノード定義 CREATE (s:Session {id: $sessionId, createdAt: datetime()}) CREATE (m:Message {role: $role, content: $content, timestamp: datetime()}) CREATE (e:Entity {name: $name, type: $type}) CREATE (t:Topic {name: $topicName}) // リレーション CREATE (s)-[:CONTAINS]->(m) CREATE (m)-[:MENTIONS]->(e) CREATE (e)-[:RELATED_TO]->(e2) CREATE (m)-[:ABOUT]->(t)
MiroFishの zep_tools.py が担っていたエンティティ抽出とリレーション構築を、Neo4j向けに再実装する。
互換レイヤーの設計
移行の鍵は neo4j_client.py(767行)。Zep APIのメソッドシグネチャをそのまま維持したドロップイン互換レイヤーとして設計した。
pythonclass Neo4jClient: """Zep APIと同一インターフェースを維持するNeo4jクライアント""" def add_entity(self, name: str, entity_type: str, metadata: dict) -> str: # Zep: self.zep_client.graph.add(...) # Neo4j: CREATE (e:Entity {name: $name, type: $type, ...}) with self.driver.session() as session: result = session.run( "CREATE (e:Entity {name: $name, type: $type}) SET e += $metadata RETURN e.name", name=name, type=entity_type, metadata=metadata ) return result.single()[0] def search_related(self, entity_name: str, depth: int = 2) -> list: # Zep互換のレスポンス形式で返す ...
MiroFishの呼び出し側コードは一切変更不要。zep_tools.py のimport先を neo4j_client に差し替えるだけで移行が完了する。破壊的変更ゼロ。
Claude Codeの claude -p でテキストからエンティティを抽出し、Neo4jに投入するパイプラインも構築した。
クエリパターン
頻用するクエリをまとめておく。
cypher// あるエンティティに関連するすべてのコンテキストを取得 MATCH (e:Entity {name: $name})-[r*1..3]-(related) RETURN e, r, related ORDER BY related.timestamp DESC LIMIT 50 // 2つのエンティティ間の最短パスを探索 MATCH path = shortestPath( (a:Entity {name: $from})-[*]-(b:Entity {name: $to}) ) RETURN path // 特定期間のトピック出現頻度 MATCH (m:Message)-[:ABOUT]->(t:Topic) WHERE m.timestamp > datetime($since) RETURN t.name, count(m) AS frequency ORDER BY frequency DESC
ローカル実行のメリット
Neo4j Community Editionをローカルで動かすと、いくつかの自由度が得られる。
実験コストがゼロ。数千ノードを投入してクエリのパフォーマンスを測り、設計を見直し、データを全削除してやり直す。このサイクルにコストがかからない。
レイテンシが最小。ネットワーク往復がないため、クエリレスポンスがミリ秒単位。インタラクティブな探索に向いている。
データが手元にある。バックアップ、エクスポート、他ツールとの連携がすべてファイルシステム操作で完結する。
移行後に気づいたこと
- Neo4j Browserの可視化は強力。MiroFishが構築したグラフ構造を目で見て確認しながらクエリを調整できる
- Communityエディションにはクラスタリング機能がないが、MiroFishの個人利用では不要
claude -pからCypherクエリを生成してNeo4jに投入するパイプラインは、想像以上にスムーズに動いた- Zepの自動エンティティ抽出は便利だったが、自前で抽出ロジックを書くことで精度と粒度をコントロールできるようになった
neo4j_client.pyの互換レイヤー方式は正解だった。移行作業中もMiroFish本体の開発を止める必要がなかった
マネージドサービスの便利さとセルフホスティングの自由度はトレードオフ。MiroFishのようにデータ量と実験頻度が増え続けるシステムでは、セルフホスティングが正解だった。