Dynamic properties to map

I am trying to transform a group of nodes into a map. I am not sure if this is best done with straight Cypher or whether there is an apoc function to help. ... maybe a combination of both?

If I have the following nodes

CREATE (e1:EntityDef{name:"Person"})
MERGE (e1)-[:HAS_A]->(:Property{name:"givenName",label:"Given Name",length:35})
MERGE (e1)-[:HAS_A]->(:Property{name:"surname",label:"Surname",length:35})
MERGE (e1)-[:HAS_A]->(:Property{name:"dob",label:"Date of Birth",format:"dd-mm-yyyy"})
CREATE (e2:EntityDef{name:"Organisation"})
MERGE (e2)-[:HAS_A]->(:Property{name:"name",label:"Name",length:40})
MERGE (e2)-[:HAS_A]->(:Property{name:"established",label:"Established",format:"dd-mm-yyyy"})

I would like to get this map.

=> {Person:[{name:"givenName",label:"Given Name",length:35},
{name:"surname",label:"Surname",length:35},
{name:"dob",label:"Date of Birth",format:"dd-mm-yyyy"}],
Organisation:[{name:"name",label:"Name",length:40},
{name:"established",label:"Established",format:"dd-mm-yyyy"}]
}

Can some one suggest an aproach to this?

I have considered 'apoc.map.fromNodes', but it seems that I need to provide a single property.

1 Like

I tried my hand at this for several minutes, but to no avail: couldn't get values stored in variables to be used as map keys... So I googled a bit and came to this blog: Neo4j: Cypher - Create Cypher map with dynamic keys | Mark Needham

From there, I was able to formulate this query, which comes pretty dang close to what you want:

MATCH (p:EntityDef)-[:HAS_A]->(n)
WITH p.name AS dynamicKey, collect(n) AS dynamicValue
RETURN apoc.map.fromValues([dynamicKey, dynamicValue]) AS map

The last step is to combine the two maps into one... But I ran out of time at the moment. Good luck! Let me know if you figure out that last step.

Had a chance to come back and figure it out, and -- glad to say -- figure it out I did!

MATCH (p:EntityDef)-[:HAS_A]->(n)
WITH p.name AS dynamicKey, collect(n) AS dynamicValue
WITH apoc.map.fromValues([dynamicKey, dynamicValue]) as map
WITH collect(map) as map_list
WITH map_list[0] as first,
     map_list[1] as second
RETURN apoc.map.merge(first, second)

Output:

{
  "Organisation": [
    {
      "format": "dd-mm-yyyy",
      "name": "established",
      "label": "Established"
    },
    {
      "name": "name",
      "length": 40,
      "label": "Name"
    }
  ],
  "Person": [
    {
      "format": "dd-mm-yyyy",
      "name": "dob",
      "label": "Date of Birth"
    },
    {
      "length": 35,
      "name": "surname",
      "label": "Surname"
    },
    {
      "length": 35,
      "name": "givenName",
      "label": "Given Name"
    }
  ]
}
1 Like

Thanks for your time.

The approach I ended up taking is

MATCH (d:EntityDef)
OPTIONAL MATCH (d)-[:HAS_A]->(p:Property)-[:OF]->(t:PropertyType)
WITH d,collect(apoc.map.merge(properties(p),{type:t.name})) as properties
RETURN collect({name:d.name,properties:properties}) as entityDefs

This query includes a PropertyType that was not in my initial example, without it we would not have needed the merge.

Looks pretty good, here's a few recommendations that use map projection to make assembling those maps a bit easier:

MATCH (d:EntityDef)
OPTIONAL MATCH (d)-[:HAS_A]->(p:Property)-[:OF]->(t:PropertyType)
WITH d,collect(p {.*, type:t.name}) as properties
RETURN collect(d {.name, properties}) as entityDefs

@andrew_bowman

"Map Projection" that is what I was looking for! I could not recall the syntax (or the correct term) and so missed it in the cypher ref card.

Thanks a heap.

@andrew_bowman the feature @kevin.urban solution has that I was particularly after is that d.name is a key in the map. Using map projection we end up with
{name:"organisation",
properties:}

rather than
{organisation:[properties]}

In that case you'll want to keep apoc.map.fromValues() at the very end:

RETURN collect(apoc.map.fromValues([d.name, properties])) as entityDefs
1 Like

Hi @andrew_bowman -- Still learning here, so I have a question: Wouldn't your solution give back 2 rows of maps instead of just one map? I would test it myself, but not fully sure how @taffyb re-did their data set.

I ask because originally tinkered with some similar approaches, but found I had to use that apoc.map.merge statement to make it come out like requested:

{Person:[{name:"givenName",label:"Given Name",length:35},
{name:"surname",label:"Surname",length:35},
{name:"dob",label:"Date of Birth",format:"dd-mm-yyyy"}],
Organisation:[{name:"name",label:"Name",length:40},
{name:"established",label:"Established",format:"dd-mm-yyyy"}]
}

You're partially correct. My solution followed taffyb's approach of returning a collection of maps, so it will return a single row of a list of map values, one element per :EntityDef node.

In order to return back just a single map, instead of a list of maps, then yes you'd need to use apoc.map.merge() as in your suggested solution.

@kevin.urban

The nodes that are used in the final solution are

CREATE (e1:EntityDef{name:"Person"})
MERGE (e1)-[:HAS_A]->(p1:Property{name:"givenName",label:"Given Name",length:35})
MERGE (e1)-[:HAS_A]->(p2:Property{name:"surname",label:"Surname",length:35})
MERGE (e1)-[:HAS_A]->(p3:Property{name:"dob",label:"Date of Birth",format:"dd-mm-yyyy"})
CREATE (e2:EntityDef{name:"Organisation"})
MERGE (e2)-[:HAS_A]->(p4:Property{name:"name",label:"Name",length:40})
MERGE (e2)-[:HAS_A]->(p5:Property{name:"established",label:"Established",format:"dd-mm-yyyy"})
CREATE (t1:PropertyType{name:"text"})
CREATE (t2:PropertyType{name:"date"})
MERGE (p1)-[:OF]->(t1)
MERGE (p2)-[:OF]->(t1)
MERGE (p3)-[:OF]->(t2)
MERGE (p4)-[:OF]->(t1)
MERGE (p5)-[:OF]->(t2)

the final cypher is
MATCH (d:EntityDef)
OPTIONAL MATCH (d)-[:HAS_A]->(p:Property)-[:OF]->(t:PropertyType)
WITH d,collect(p {.*, type:t.name}) as properties
RETURN apoc.map.mergeList(collect(apoc.map.fromValues([d.name, properties]))) as entityDefs

result:
{
"Organisation": [
{ "format": "dd-mm-yyyy", "name": "established", "label": "Established", "type": "date" },
{ "name": "name", "length": 40, "label": "Name", "type": "text" }
],
"Person": [
{ "format": "dd-mm-yyyy", "name": "dob", "label": "Date of Birth", "type": "date" },
{ "length": 35, "name": "surname", "label": "Surname", "type": "text" },
{ "length": 35, "name": "givenName", "label": "Given Name", "type": "text" }
]
}

1 Like