neo4jrb / activegraph

An active model wrapper for the Neo4j Graph Database for Ruby.
http://neo4jrb.io
MIT License
1.4k stars 276 forks source link

Translate cypher query to ActiveNode #1523

Closed iehdk closed 3 years ago

iehdk commented 5 years ago

How can this cypher expression:

MATCH (Scaffold {branch_count: 1, edge_count: 4})-[HAS_MOLECULE]->(m:Molecule {atom_count: 5, formula: 'C2N1O2'}),
(m)-[HAS_ATOM]->(a0:Atom),
(a0:Atom {element: 'C'})-[:HAS_BOND {bond_type: 'single'}]-(a1:Atom {element: 'C'}),
(a0:Atom {element: 'C'})-[:HAS_BOND {bond_type: 'single'}]-(a2:Atom {element: 'O'}),
(a0:Atom {element: 'C'})-[:HAS_BOND {bond_type: 'double'}]-(a3:Atom {element: 'O'}),
(a1:Atom {element: 'C'})-[:HAS_BOND {bond_type: 'single'}]-(a4:Atom {element: 'N'})
RETURN m

Be translated to #ActiveNode?

jorroll commented 5 years ago

Given these models

class Scaffold
  include Neo4j::ActiveNode

  property :branch_count
  property :edge_count

  has_many :out, :molecules, type: :HAS_MOLECULE
end

class Molecule
  include Neo4j::ActiveNode

  property :atom_count
  property :formula

  has_many :in, :scaffolds, type: :HAS_MOLECULE
end

class Atom
  include Neo4j::ActiveNode

  property :element

  has_many :both, :bonds, rel_class: "HasBondRel"
end

class HasBondRel
  include Neo4j::ActiveRel

  type :HAS_BOND

  from_class "Atom"
  to_class "Atom"

  property :bond_type
end

I think this query should get you what you want.

Scaffold
  .where(branch_count: 1, edge_count: 4)
  .molecules.where(atom_count: 5, formula: 'C2N102')
  .branch {
    atoms.where(element: 'C')
      .bonds.where(element: 'C').rel_where(bond_type: 'single')
      .bonds.where(element: 'N').rel_where(bond_type: 'single')
  }.branch {
    atoms.where(element: 'C')
      .bonds.where(element: 'O').rel_where(bond_type: 'single')
  }.branch {
    atoms.where(element: 'C')
      .bonds.where(element: 'O').rel_where(bond_type: 'double')
  }

See the docs for more info

This being said, personally I find the cypher version more expressive and more readable. Unless you're composing query chains dynamically, I'd just stick with the cypher:

Neo4j::ActiveBase.query("
  MATCH (Scaffold {branch_count: 1, edge_count: 4})-[HAS_MOLECULE]->(m:Molecule {atom_count: 5, formula: 'C2N1O2'}),
        (m)-[HAS_ATOM]->(a0:Atom),
        (a0:Atom {element: 'C'})-[:HAS_BOND {bond_type: 'single'}]-(a1:Atom {element: 'C'}),
        (a0:Atom {element: 'C'})-[:HAS_BOND {bond_type: 'single'}]-(a2:Atom {element: 'O'}),
        (a0:Atom {element: 'C'})-[:HAS_BOND {bond_type: 'double'}]-(a3:Atom {element: 'O'}),
        (a1:Atom {element: 'C'})-[:HAS_BOND {bond_type: 'single'}]-(a4:Atom {element: 'N'})
  RETURN m
", {
  # any query params go here
}).pluck(:m).first
iehdk commented 5 years ago

I do in fact agree that the Cypher version is more readable - and it is way easier to find good help on Cypher queries because of the bigger user group.

I will in fact be constructing queries dynamically, but putting together a cypher string is pretty straight forward. Let me see if I can get this this ActiveBase.query to work. (For now it goes undefined methodpluck' for # (NoMethodError)` for some reason).

Before was testing neo4j-core's session.query, but I would like to get ActiveNode objects returned instead of Neo4j::Core::Node objects - unless I should go completely neo4j-core (is that recommended?).

jorroll commented 5 years ago

Regarding the error you're running into, are you sure neo4jrb is set up properly? I.e. it's connecting to the database, etc.

If you just want to dynamically compose cypher strings, definitely check out neo4j-core. You can get a neo4j-core query object with Neo4j::ActiveBase.new_query, and use can use it like

query = Neo4j::ActiveBase.new_query

query = query.match("(Scaffold {branch_count: 1, edge_count: 4})-[HAS_MOLECULE]->(m:Molecule {atom_count: 5, formula: 'C2N1O2'})")
     .match("(m)-[HAS_ATOM]->(a0:Atom)")
     .match("(a0:Atom {element: 'C'})-[:HAS_BOND {bond_type: 'single'}]-(a1:Atom {element: 'C'})")
     .match("(a0:Atom {element: 'C'})-[:HAS_BOND {bond_type: 'single'}]-(a2:Atom {element: 'O'})")
     .match("(a0:Atom {element: 'C'})-[:HAS_BOND {bond_type: 'double'}]-(a3:Atom {element: 'O'})")

query = query.match("(a1:Atom {element: 'C'})-[:HAS_BOND {bond_type: 'single'}]-(a4:Atom {element: 'N'})")

query.pluck(:m)

The above should return ActiveNode / ActiveRel objects (assuming you've created the models).

One potential "gotcha" with neo4j-core query clause methods is that it will automatically combine clauses. I.e., multiple .match calls (like above) are, by default, combined into the same MATCH clause with a comma ,. In this case that's what you want, but if you wanted separate match clauses you'd need to call .break on the chain.

I.e. this:

query = Neo4j::ActiveBase.new_query

query = query.match("(Scaffold {branch_count: 1, edge_count: 4})-[HAS_MOLECULE]->(m:Molecule {atom_count: 5, formula: 'C2N1O2'})")

query = query.break.match("(m)-[HAS_ATOM]->(a0:Atom)")

This leads to

MATCH (Scaffold {branch_count: 1, edge_count: 4})-[HAS_MOLECULE]->(m:Molecule {atom_count: 5, formula: 'C2N1O2'})
MATCH (m)-[HAS_ATOM]->(a0:Atom)

Regarding Neo4j-core vs Neo4jrb, it's up to user preference.

Also:

I just updated the ActiveNode query in my original answer because I hadn't noticed that your query referred to the a0 node multiple times. Regardless, hopefully you get the idea.

jorroll commented 5 years ago

Also, off the top of my head, I don't remember how Neo4jrb translates queries using .branch. It may use a WHERE clause, which, while ultimately returning the same results as your original cypher query, wouldn't be as performant (I think).

iehdk commented 5 years ago

Right. Thanks for the help. I got the results = Neo4j::ActiveBase.query() to work (though no pluck-luck, so iterating over the results instead).

bobmazanec commented 3 years ago

CodeTriage referred me here. Seems like this can be closed. If not, how can I help?

amitsuryavanshi commented 3 years ago

Closing this. @iehdk, you can reopen it you still have any issues. Thanks.

amitsuryavanshi commented 3 years ago

Thanks @bobmazanec