0
votes

Lets say I have collections (Collection) that have things (Thing) attached to them. These collections can have sub collections which contain a subset of the parent collection. There is a [:FILTERS] relationship between parent and the sub collection that contain attribute lists by which the Things should be filtered. The attributes key are the same as the key on the Thing, and the values for the key on the relationship is all the values are accepted on the Thing. If the relationship does not have any attributes, all Things should "shared" between the collections.

The problem that I'm having a hard time solving is how to filter the things by the attributes on the relationship.

Here is what the graph could look like: Graph view enter image description here

and here is the Cypher code for creating said graph:

// Collections
CREATE
(C1:Collection {name: 'C1'})-[:FILTERS {type: ['image']}]->(C1_img:Collection {name:'C1_img'}),
(C1_img)-[:FILTERS {user: ['john']}]->(C1_user_img:Collection {name:'C1_user_img'}),

(C2:Collection {name: 'C2'})-[:FILTERS {type: ['image']}]->(C2_img:Collection {name:'C2_img'}),
(C2_img)-[:FILTERS]->(C1)

// C1 Things
CREATE
(C1I1:Thing {id:1, title:"C1 Thing 1", type:'image', user:'john'})-[:BELONGS_TO]->(C1),
(C1I2:Thing {id:1, title:"C1 Thing 2", type:'image'})-[:BELONGS_TO]->(C1)

// C2 Things
CREATE
(C2I1:Thing {id:1, title:"C2 Thing 1", type:'image'})-[:BELONGS_TO]->(C2),
(C2I2:Thing {id:1, title:"C2 Thing 2", type:'image', user:'john'})-[:BELONGS_TO]->(C2);

Lets say I want to get Things that should be in C1_user_img. That would be all Things with the type image and the user john.

C1_img -> C1_user_img filters on user=john,
C1->C1_img filters on type=image,
C2_img -> C1 has no filter,
C2 -> C2_img filters on type=images

In other words, the Things C1I1 and C2I2 should be in the C1_user_img collection.

Hopefully this explains my problem.

I've just started using Neo4j and everything is still fairly new for me. I have tried a lot of different approaches to this already but have yet to find something that works.

I can't just create one list of filters and filter all things by that, but I need to filter things for each collection node as the filters might vary by collections.

For instance, if I try:

MATCH (start_node:Collection {name: 'C1_user_img'})<-[thing_filters:FILTERS*]-(collections:Collection)<-[:BELONGS_TO]-(t:Thing)
RETURN thing_filters:FILTERS, t

I get back something that I probably could work with; each Thing with what it should be filtered by. And then comes the big problem, how to match these values to the values of the Thing node. I guess something like all() could be used here, but since all of this is fairly new I haven't figured that out yet.

It might be that this is just too complex to handle in Cypher and some parts needs to be done in code, but it would be nice if it could be done with just a query.

EDIT

Since the example that I gave might have been a bit too simple, and my explanation might have been a bit lacking I'm adding a bit more advanced example.

Lets say that you have multiple filters in one FILTERS relationship, then all the filters should be taken into consideration. Also, as the filters are lists, as in {user:['john', 'tom']}, all values in the list should be accepted. In this case, Things with the user 'john' or 'tom'.

Here is a couple of additional nodes for testing:

MATCH (C2:Collection {name:'C2'}), (C1_img:Collection {name:'C1_img'})
CREATE
(C2I3:Thing {id:12, title:"C2 Thing 3", type:'image', user:'john', extra:'foo'}),
(C1_user_extra_img:Collection {name:'C1_user_extra_img'})
CREATE
(C2I3)-[:BELONGS_TO]->(C2),
(C1_img)-[:FILTERS {user: ['john'], extra: ['foo']}]->(C1_user_extra_img)
return C1_user_extra_img, C2I3

Now when getting all Things for C1_user_extra_img, would require filtering on both user and extra. One could also add a Thing node with the user 'tom' and then relationship filter on both 'john' and 'tom' and that should return all things that has either the user 'john' or 'tom.

1

1 Answers

0
votes

Thats an interesting one. I've found a solution - maybe someone else comes up with a more elegant one:

MATCH (start_node:Collection {name: 'C1_user_img'})<-[thing_filters:FILTERS*]-(collections:Collection)<-[:BELONGS_TO]-(t:Thing)
WITH t, 
reduce(acc=[], x in thing_filters | acc + keys(x)) AS keys,
reduce(acc=[], x in thing_filters | acc + reduce(b=[], y in keys(x) | x[y]))     AS values
WHERE all(x in range(0,size(keys)-1) WHERE t[keys[x]] = values[x])
RETURN t

After matching the path we build a collection for all property keys and property values along the relationships of that path (keys, values).

Using the all predicate we make sure that all elements of keys and values are set as properties on t.

update

If the property values on the relationships are arrays and the condition is that the respective property value on a thing needs to be in that list a small modification to the existing cypher statement is necessary:

MATCH (start_node:Collection {name: 'C1_user_img'})<-[thing_filters:FILTERS*]-(collections:Collection)<-[:BELONGS_TO]-(t:Thing)
WITH t, 
   reduce(acc=[], x in thing_filters | acc + keys(x)) AS keys,
   reduce(acc=[], x in thing_filters | acc + reduce(b=[], y in keys(x) | [x[y]])) AS values
WHERE all(x in range(0,size(keys)-1) WHERE t[keys[x]] in (values[x]) )
RETURN t

By introduction square brackets in the reduce for values we build up an array of arrays that gets evaluated in the WHERE's all predicate.