aplbrain / grand

Your favorite Python graph libraries, scalable and interoperable. Graph databases in memory, and familiar graph APIs for cloud databases.
Apache License 2.0
80 stars 6 forks source link

Multi-edges between two nodes #52

Open spranger opened 3 months ago

spranger commented 3 months ago

Hello, I use the new version 0.5.1 of grand-graph:

from grand import Graph
from grandcypher import GrandCypher
from grand.backends._sqlbackend import SQLBackend

backend=SQLBackend(db_url="sqlite:///demo2.db")
G = Graph(backend=backend)

G.nx.add_node("spranger", type="Person")
G.nx.add_node("meier", type="Person")
G.nx.add_node("krause", type="Person")
G.nx.add_node("Berlin", type="City")
G.nx.add_node("Paris", type="City")
G.nx.add_node("London", type="City")

G.nx.add_edge("spranger", "Paris", type="LIVES_IN")
G.nx.add_edge("krause", "Berlin", type="LIVES_IN")
G.nx.add_edge("meier", "London", type="LIVES_IN")
G.nx.add_edge("spranger", "Berlin", type="BORN_IN")
G.nx.add_edge("krause", "Berlin", type="BORN_IN")
G.nx.add_edge("meier", "Berlin", type="BORN_IN")

result1 = GrandCypher(G.nx).run("""
MATCH (n)-[r]->(c)
WHERE
    n.type == "Person"
    and
    c.type == "City"

RETURN n, r, c    
""")

from lark.lexer import Token
n = result1[Token('CNAME', 'n')]
r = result1[Token('CNAME', 'r')]
c = result1[Token('CNAME', 'c')]

for i in range(len(n)):
    print(f"{n[i]} - {r[i].get('type')} -> {c[i]}")

backend.commit()
backend.close()

results in

  1. The "krause - LIVES_IN -> Berlin" relation is not stored (as any second relation between two same nodes) . This might be due to the cause that our "G.nx" doesn't cope with multigraphs.
  2. In plain grandcypher I can query "Match (p:Person)" . How do I do this in my cypher query above?
  3. Would it be a good idea to have the backend and the graph layer (e.g. netwrokx) completely transparent and just run Cypher queries, also for creating nodes and relations?

Kind Regards. Steffen, the graphologist

j6k4m8 commented 3 months ago

Hi @spranger! @jackboyla just added support for multigraphs in Grand-Cypher here: https://github.com/aplbrain/grand-cypher/pull/42

So that's certainly going to help here. I also think (need to do a deeper dive before I can confirm) that this is indeed a limitation of Grand right now; I don't think we support MultiGraphs very well. (Maybe @acthecoder23 this would be a fun project to chew on?)

Just thinking out loud, probably the correct answer is to split Graph/DiGraph/MultiGraph/MultiDiGraph implementations like we briefly discuss in this issue: https://github.com/aplbrain/grand/issues/44 ...

A short-term solution might be to create a MultiDiGraph in networkx itself (rather than in Grand) and try out @jackboyla's new implementation (which I'll have on PyPI / pip-installable shortly in grand-cypher==0.8.0). If that works for you, then we'll know the next step is support for multigraphs in the Grand backends!

j6k4m8 commented 3 months ago

PS: Just got back from a week in Berlin, and totally thinking about adding a LIVES_IN edge there someday... :)

acthecoder23 commented 3 months ago

@j6k4m8 I can give this a shot. I'll have to do a little digging but it'd be worthwhile

jackboyla commented 3 months ago

@spranger also to answer 2. -- to MATCH (a:friend) you need to assign the node label to the __labels__ attribute, in a set. We should probably include this in the README as it's not immediately obvious :) hopefully the multigraph support helps too:

from grandcypher import GrandCypher
import networkx as nx

host = nx.MultiDiGraph()
host.add_node("a", name="Alice", age=30)
host.add_node("b", name="Bob", age=40)
host.add_node("c", name="Charlie", age=50)
host.add_edge("a", "b", __labels__={"friend"}, years=3)  # <---
host.add_edge("a", "c", __labels__={"colleague"}, years=10)
host.add_edge("a", "c", __labels__={"parent"}, duration='forever')
host.add_edge("b", "c", __labels__={"colleague"}, duration=10)
host.add_edge("b", "c", __labels__={"mentor"}, years=2)

qry = """
MATCH (a)-[r]->(b)
RETURN a.name, b.name, r.__labels__, r.duration
"""
res = GrandCypher(host).run(qry)
print(res)

'''
{'a.name': ['Alice', 'Alice', 'Bob'], 
'b.name': ['Bob', 'Charlie', 'Charlie'], 
'r.__labels__': [{0: {'friend'}}, {0: {'colleague'}, 1: {'parent'}}, {0: {'colleague'}, 1: {'mentor'}}], 
'r.duration': [{0: None}, {0: None, 1: 'forever'}, {0: 10, 1: None}]}
'''