neo4j의 Graph를 networkx로 가져오기.

4 분 소요

물론, 굳이 networkx로 가져올 필요가 있는가?

  • 물론, 기본적으로 DB에 잘 정리되어 있는 데이터를, 그리고 심지어 보통 이곳에 있는 데이터들은 로컬로 가져오기에는 지나치게 큰 크기의 데이터인 경우가 대부분인데, 이 데이터를 로컬로 가져와서 그것도 networkx로 분석을 하는 것이 큰 의미가 있을까 싶기는 합니다.
  • 다만, 그러함에도 이유를 찾아보자면, 1) 저는 DB에서 데이터를 직접 분석 비슷하게 하는 것이 불안하게 느껴지거든요. 혹시나 DB에 문제를 발생시키지는 않을까, 싶고, DB위에서 돌리는 것이 어떤 의미로든 과부하의 원인이 될 수 있으니까 가능하면 분리시켜서 다양한 분석 및 실험을 해보는 것이 훨씬 효율적으로 느껴지고. 2) Neo4j에서 그래프에 대한 웬만한 알고리즘을 제공하기는 하지만, 여전히 python 위에서 돌아가는 다른 머신러닝 라이브러리들과 비교하면 그 양이 현저하게 적죠. 가령, 머신러닝 패키지들과 함께 이용하려면 아무래도 일단 python으로 가져와서 쓰는 것이 훨씬 편하기마저 합니다. 즉, 이를 위해서라도 networkx로 가져오는 것이 필요할 수 있죠.
  • 아무튼, 그래서 가져와서 써보기로 했씁니다. 일단 가져온다면 그 다음부터는 큰 어려움이 없어지죠.

networkx로 가져옵시다.

  • 기본 얼개는 cypher로 쿼리하여 neo4j에서 데이터를 가져옵니다. 쿼리문은 다음의 형식이 되겠죠.
input_query = """
MATCH (n1)-[e]->(n2)
RETURN n1, e, n2
LIMIT 100
"""
  • 그리고 DB의 세션을 열어서 다음처럼 쿼리를 날리고 결과를 가져옵니다.
# pip install neo4j-driver

import networkx as nx

from neo4j.v1 import GraphDatabase, basic_auth

## 
IP_ADDRESS =""
BOLT_PORT  = ""
USER_NAME = ""
PASSWORD = ""
##

driver = GraphDatabase.driver(
    # bolt protocol로 내 DB의 IP에 BOLT_PORT로 접근하고 
    uri=f"bolt://{IP_ADDRESS}:{BOLT_PORT}", 
    # 주어진 USER_ID와 패스워드로 들어감.
    auth=basic_auth(
        user=USER_NAME, 
        password=PASSWORD)
    )

def run_query(input_query):
    """
    - input_query를 전달받아서 실행하고 그 결과를 출력하는 함수입니다.
    """
    # 세션을 열어줍니다.
    with driver.session() as session: 
        # 쿼리를 실행하고 그 결과를 results에 넣어줍니다.
        results = session.run(
            input_query,
            parameters={}
        )
        return results

# 다음과 같이 쿼리하여, 노드, 엣지, 노드를 모두 가져온다.
# 그래도 전체를 다 가져오는 경우 부하가 많이 걸리므로, 100로 제한하여 가져와 본다.
input_query = """
MATCH (n1)-[e]->(n2)
RETURN n1, e, n2
LIMIT 100
"""

results = run_query(input_query)
  • 자, 이제 쿼리의 결과가 results에 저장되어 있죠. 간단히 말하면, n1, e, n2의 칼럼에 각 값들이 저장되어 있다고 보시면 됩니다.
  • 이제 긁어온 쿼리에서 필요한 정보만 뽑아내어 다음처럼 그래프에 넣어줍니다. 약간의 특이사항은 id, label은 내부 변수로 접근해야 하고, properties는 딕셔너리처럼 접근해야 하죠.
# 긁어온 쿼리를 다음의 방향성이 있는 그래프에 넣어준다.
DG = nx.DiGraph()

for i, path in enumerate(results):
    # 앞서, 쿼리에서 변수명을 n1, n2, e, 로 가져왔으므로 각 값에 할당된 것을 변수에 추가로 넣어준다.
    n1, n2, e = path['n1'], path['n2'], path['e']
    # 그리고, 보통 노드의 경우는 id, labels, properties 로 나누어 정보가 저장되어 있다.
    # 이를 가져오기 편하게, dictionary로 변경한다. 
    n1_dict = {
        'id': path['n1'].id, 
        'labels':path['n1'].labels, 
        'properties':dict(path['n1'])
    }
    n2_dict = {
        'id': path['n2'].id, 
        'labels':path['n2'].labels, 
        'properties':dict(path['n2'])
    }
    # 마찬가지로, edge의 경우도 아래와 같이 정보를 저장한다.
    e_dict = {
        'id':path['e'].id, 
        'type':path['e'].type, 
        'properties':dict(path['e'])
    }
    # print(e_dict)
    # 해당 노드를 넣은 적이 없으면 넣는다.
    if n1_dict['id'] not in DG:
        DG.add_nodes_from([
            (n1_dict['id'], n1_dict)
        ])
    # 해당 노드를 넣은 적이 없으면 넣는다.
    if n2_dict['id'] not in DG:
        DG.add_nodes_from([
            (n2_dict['id'], n2_dict)
        ])
    # edge를 넣어준다. 노드의 경우 중복으로 들어갈 수 있으므로 중복을 체크해서 넣어주지만, 
    # edge의 경우 중복을 체크하지 않아도 문제없다.
    DG.add_edges_from([
        (n1_dict['id'], n2_dict['id'], e_dict)
    ])

wrap-up

  • 뭐, 생각보다는 간단한 것 같습니다. 결국은 테이블의 형태로 쿼리 결과를 가져온 다음 그 데이터를 사용해서 그래프를 다시 만들어주는 형식인 것이죠.
  • 결국 이는 그래프(Neo4j) ==> 테이블 ==> 그래프(networkx)로 변환됩니다. 중간에 테이블로 변환하는 것을 빼고, 더 빠르게 graph to graph로 변환하는 방법이 가능하다면 훨씬 효율적일 것 같은데요, 아직은 이걸 어떻게 해야 하는지 선명하게 보이지는 않네요.

raw code

import networkx as nx
# pip install neo4j-driver
from neo4j.v1 import GraphDatabase, basic_auth

## 
IP_ADDRESS =""
BOLT_PORT  = ""
USER_NAME = ""
PASSWORD = ""
##

driver = GraphDatabase.driver(
    # bolt protocol로 내 DB의 IP에 BOLT_PORT로 접근하고 
    uri=f"bolt://{IP_ADDRESS}:{BOLT_PORT}", 
    # 주어진 USER_ID와 패스워드로 들어감.
    auth=basic_auth(
        user=USER_NAME, 
        password=PASSWORD)
    )

def run_query(input_query):
    """
    - input_query를 전달받아서 실행하고 그 결과를 출력하는 함수입니다.
    """
    # 세션을 열어줍니다.
    with driver.session() as session: 
        # 쿼리를 실행하고 그 결과를 results에 넣어줍니다.
        results = session.run(
            input_query,
            parameters={}
        )
        return results

# 다음과 같이 쿼리하여, 노드, 엣지, 노드를 모두 가져온다.
# 그래도 전체를 다 가져오는 경우 부하가 많이 걸리므로, 100로 제한하여 가져와 본다.
input_query = """
MATCH (n1)-[e]->(n2)
RETURN n1, e, n2
LIMIT 100
"""

results = run_query(input_query)
# result => neo4j.BoltStatementResult object
print(results)
print(type(results))
print("=="*30)

# 긁어온 쿼리를 다음의 방향성이 있는 그래프에 넣어준다.
DG = nx.DiGraph()

for i, path in enumerate(results):
    # 앞서, 쿼리에서 변수명을 n1, n2, e, 로 가져왔으므로 각 값에 할당된 것을 변수에 추가로 넣어준다.
    n1, n2, e = path['n1'], path['n2'], path['e']
    # 그리고, 보통 노드의 경우는 id, labels, properties 로 나누어 정보가 저장되어 있다.
    # 이를 가져오기 편하게, dictionary로 변경한다. 
    n1_dict = {
        'id': path['n1'].id, 
        'labels':path['n1'].labels, 
        'properties':dict(path['n1'])
    }
    n2_dict = {
        'id': path['n2'].id, 
        'labels':path['n2'].labels, 
        'properties':dict(path['n2'])
    }
    # 마찬가지로, edge의 경우도 아래와 같이 정보를 저장한다.
    e_dict = {
        'id':path['e'].id, 
        'type':path['e'].type, 
        'properties':dict(path['e'])
    }
    # print(e_dict)
    # 해당 노드를 넣은 적이 없으면 넣는다.
    if n1_dict['id'] not in DG:
        DG.add_nodes_from([
            (n1_dict['id'], n1_dict)
        ])
    # 해당 노드를 넣은 적이 없으면 넣는다.
    if n2_dict['id'] not in DG:
        DG.add_nodes_from([
            (n2_dict['id'], n2_dict)
        ])
    # edge를 넣어준다. 노드의 경우 중복으로 들어갈 수 있으므로 중복을 체크해서 넣어주지만, 
    # edge의 경우 중복을 체크하지 않아도 문제없다.
    DG.add_edges_from([
        (n1_dict['id'], n2_dict['id'], e_dict)
    ])
print("=="*30)
print("==: network is generated")
for n in DG.nodes(data=True):
    print(n, n[1]['labels'], n[1]['properties'])
    print("--"*30)
for e in DG.edges(data=True):
    print(e)
    

댓글남기기