neo4jcypher

Neo4j Cypher - Chain optional relationships in path when converting to tree


I have some nodes saved in neo4j and want to return them as tree. My model looks something like this:

RootNode -HAS_CHILDREN-> Element
Element -HAS_CHILDREN-> Element
Element -HAS_ANOTHER_RELATIONSHIP-> AnotherNode

The query that I run:

    const rootNodeAlias = 'r';
    const elementAlias = 'e';
    const anotherNodeAlias = 'a';
    const query "= 
        MATCH (${rootNodeAlias}:RootNode {id: $id})
  
        // Match the related elements
        OPTIONAL MATCH (${rootNodeAlias})-[:HAS_CHILDREN]->(${elementAlias}:Element)

        // Fetch the element tree
        CALL {
            WITH ${elementAlias}
            OPTIONAL MATCH path=(${elementAlias})-[rel:HAS_CHILDREN*0..]->(child:Element)-[:HAS_ANOTHER_RELATIONSHIP]->(${anotherNodeAlias}:AnotherNode)
            WITH COLLECT(path) AS paths
            CALL apoc.convert.toTree(paths) YIELD value AS tree
            RETURN tree AS elementTree
        }
        
        // Return the root node and its related elements with their children
        RETURN ${rootNodeAlias} AS root,
               collect(elementTree) AS elementTrees
      ;"

The problem is that in case an element does not have the HAS_ANOTHER_RELATIONSHIP with AnotherNode , then the path here:

OPTIONAL MATCH path=(${elementAlias})-[rel:HAS_CHILDREN*0..]->(child:Element)-[:HAS_ANOTHER_RELATIONSHIP]->(${anotherNodeAlias}:AnotherNode)

will not return the HAS_CHILDREN relationship either. I tried something like this with optional match:

OPTIONAL MATCH path=(${elementAlias})-[rel:HAS_CHILDREN*0..]->(child:Element) OPTIONAL MATCH (child)-[:HAS_ANOTHER_RELATIONSHIP]->(${anotherNodeAlias}:AnotherNode)

but it didn't work. It returned only the HAS_CHILDREN relationships.

Is there any way to add inside the path the two relationships, if one of them exist? If this can't be done inside the path, what is the workaround?

Thank you in advance!


Solution

  • If you're on Neo4j > 5.9, you can use quantified path patterns to write:

    MATCH (r:RootNode)
    OPTIONAL MATCH path = (r)-[:HAS_CHILDREN]->+(:Element) 
                          (()-[:HAS_ANOTHER_RELATIONSHIP]->(:AnotherNode)){0,1}
    WITH r, COLLECT(path) AS paths
    CALL apoc.convert.toTree(paths) YIELD value AS tree
    RETURN r AS root, collect(tree) AS elementTrees
    

    The {0,1} quantifier will include the final hop over HAS_ANOTHER_RELATIONSHIP if it exists, without having to use a second OPTIONAL.

    The following will work for version < 5.9, as long as there are no nodes with both labels Element and AnotherNode:

    MATCH (r:RootNode)
    OPTIONAL MATCH path = (r)-[:HAS_CHILDREN*]->(:Element)-[:HAS_ANOTHER_RELATIONSHIP*0..1]->
                          (:Element|AnotherNode)
    WITH r, COLLECT(path) AS paths
    CALL apoc.convert.toTree(paths) YIELD value AS tree
    RETURN r AS root, collect(tree) AS elementTrees