🗄️
데이터베이스
Neo4j
Graph Database
세계에서 가장 널리 사용되는 그래프 데이터베이스. 노드(Node)와 관계(Relationship)로 연결된 데이터를 Cypher 쿼리 언어로 직관적으로 탐색합니다.
Graph Database
세계에서 가장 널리 사용되는 그래프 데이터베이스. 노드(Node)와 관계(Relationship)로 연결된 데이터를 Cypher 쿼리 언어로 직관적으로 탐색합니다.
Neo4j는 2007년에 출시된 네이티브 그래프 데이터베이스로, 속성 그래프(Property Graph) 모델을 사용합니다. 노드와 관계에 속성을 저장하고, 관계 기반 데이터 탐색에 최적화된 인덱스 프리 인접성(Index-Free Adjacency)을 제공합니다.
// 인덱스 및 제약조건 생성
CREATE CONSTRAINT user_email_unique FOR (u:User) REQUIRE u.email IS UNIQUE;
CREATE INDEX user_name_idx FOR (u:User) ON (u.name);
// 노드 생성
CREATE (alice:User {
id: 'u-001',
name: 'Alice',
email: 'alice@example.com',
created_at: datetime()
});
CREATE (bob:User {id: 'u-002', name: 'Bob', email: 'bob@example.com'});
CREATE (charlie:User {id: 'u-003', name: 'Charlie', email: 'charlie@example.com'});
// 관계 생성
MATCH (a:User {name: 'Alice'}), (b:User {name: 'Bob'})
CREATE (a)-[:FOLLOWS {since: date('2024-01-15')}]->(b);
// 게시물과 좋아요 관계
CREATE (p:Post {id: 'p-001', title: 'Graph DB 입문', content: '...'})
WITH p
MATCH (u:User {name: 'Alice'})
CREATE (u)-[:WROTE {at: datetime()}]->(p);
// 친구의 친구 추천 (2-hop, 내가 아직 팔로우하지 않은 사람)
MATCH (me:User {name: 'Alice'})-[:FOLLOWS]->(friend)-[:FOLLOWS]->(fof:User)
WHERE me <> fof
AND NOT (me)-[:FOLLOWS]->(fof)
WITH fof, COUNT(friend) AS mutual_friends
RETURN fof.name, mutual_friends
ORDER BY mutual_friends DESC
LIMIT 10;
// 특정 사용자까지의 최단 경로
MATCH (start:User {name: 'Alice'}), (end:User {name: 'Charlie'})
MATCH path = shortestPath((start)-[:FOLLOWS*..6]->(end))
RETURN path, length(path) AS hops;
// 인기 게시물 (좋아요 수 기준)
MATCH (p:Post)<-[:LIKED]-(u:User)
WITH p, COUNT(u) AS like_count
ORDER BY like_count DESC
LIMIT 10
RETURN p.title, like_count;
// 협업 필터링: 비슷한 취향의 사용자가 좋아한 상품 추천
MATCH (me:User {id: 'u-001'})-[:PURCHASED]->(product:Product)<-[:PURCHASED]-(similar:User)
MATCH (similar)-[:PURCHASED]->(rec:Product)
WHERE NOT (me)-[:PURCHASED]->(rec)
WITH rec, COUNT(DISTINCT similar) AS score
ORDER BY score DESC
LIMIT 10
RETURN rec.name, rec.category, score;
// 카테고리 기반 추천
MATCH (me:User {id: 'u-001'})-[:INTERESTED_IN]->(cat:Category)<-[:BELONGS_TO]-(product:Product)
WHERE NOT (me)-[:PURCHASED]->(product)
RETURN product.name, cat.name, product.price
ORDER BY product.rating DESC
LIMIT 20;
from neo4j import GraphDatabase
from contextlib import contextmanager
class Neo4jRepository:
def __init__(self, uri: str, user: str, password: str):
self.driver = GraphDatabase.driver(uri, auth=(user, password))
def close(self):
self.driver.close()
@contextmanager
def session(self):
session = self.driver.session()
try:
yield session
finally:
session.close()
def create_user(self, user_id: str, name: str, email: str):
with self.session() as session:
session.run("""
MERGE (u:User {id: $id})
SET u.name = $name, u.email = $email, u.updated_at = datetime()
""", id=user_id, name=name, email=email)
def follow_user(self, from_id: str, to_id: str):
with self.session() as session:
session.run("""
MATCH (a:User {id: $from_id}), (b:User {id: $to_id})
MERGE (a)-[r:FOLLOWS]->(b)
ON CREATE SET r.since = date()
""", from_id=from_id, to_id=to_id)
def get_recommendations(self, user_id: str, limit: int = 10) -> list:
with self.session() as session:
result = session.run("""
MATCH (me:User {id: $user_id})-[:FOLLOWS]->(friend)-[:FOLLOWS]->(fof:User)
WHERE me <> fof AND NOT (me)-[:FOLLOWS]->(fof)
WITH fof, COUNT(friend) AS score
RETURN fof.id AS id, fof.name AS name, score
ORDER BY score DESC
LIMIT $limit
""", user_id=user_id, limit=limit)
return [dict(record) for record in result]
def find_shortest_path(self, from_id: str, to_id: str, max_hops: int = 6):
with self.session() as session:
result = session.run(f"""
MATCH (start:User {{id: $from_id}}), (end:User {{id: $to_id}})
MATCH path = shortestPath((start)-[:FOLLOWS*..{max_hops}]->(end))
RETURN [n IN nodes(path) | n.name] AS path, length(path) AS hops
""", from_id=from_id, to_id=to_id)
record = result.single()
return dict(record) if record else None
# 사용 예시
repo = Neo4jRepository("neo4j://localhost:7687", "neo4j", "password")
repo.create_user("u-001", "Alice", "alice@example.com")
repo.create_user("u-002", "Bob", "bob@example.com")
repo.follow_user("u-001", "u-002")
recommendations = repo.get_recommendations("u-001")
print(recommendations)
repo.close()
// APOC: 대량 데이터 로드
CALL apoc.periodic.iterate(
"LOAD CSV WITH HEADERS FROM 'file:///users.csv' AS row RETURN row",
"MERGE (u:User {id: row.id}) SET u.name = row.name, u.email = row.email",
{batchSize: 1000, parallel: true}
);
// APOC: 경로 확장 (BFS/DFS)
MATCH (start:User {name: 'Alice'})
CALL apoc.path.expand(start, "FOLLOWS", "User", 1, 3) YIELD path
RETURN path;
// APOC: 가상 관계 생성 (유사도 기반)
MATCH (u1:User)-[:PURCHASED]->(p:Product)<-[:PURCHASED]-(u2:User)
WHERE u1 <> u2
WITH u1, u2, COUNT(p) AS common_products
WHERE common_products >= 3
CALL apoc.create.vRelationship(u1, 'SIMILAR_TO', {score: common_products}, u2) YIELD rel
RETURN u1.name, u2.name, rel.score;
-[:REL*]->처럼 상한 없는 가변 길이 경로는 성능에 치명적입니다.
반드시 -[:REL*1..5]->처럼 최대 홉 수를 제한하세요.
MATCH의 시작점이 되는 노드 속성에는 반드시 인덱스를 생성하세요.
CREATE INDEX FOR (u:User) ON (u.id). 인덱스 없이 전체 노드 스캔하면 성능이 급락합니다.
Neo4j Community Edition은 오픈소스(GPL)지만 단일 인스턴스만 지원합니다. 클러스터링, 고가용성, 역할 기반 접근 제어가 필요하면 Enterprise Edition(상용) 라이선스가 필요합니다.