1
votes

Alice loves kittens and puppies and will only go to places where they are both present. She likes guppies and goldfish and insists on at least one of them being present. She hates echidnas though, and will not show up anywhere that hosts spiny things. Bob likes kittens and hamsters, but has no strong feelings regarding other animals, piscine, spiny or otherwise.

CREATE (alice:PERSON {name: 'Alice', loves: ['kittens','puppies'], likes: ['guppies','goldfish'], hates:['echidnas']})
CREATE (bob:PERSON {name: 'Bob', likes: ['kittens','hamsters']})

I'm throwing a party where I will have kittens, puppies, guppies and manatees. Who will come? Here's how I'm asking.

MATCH (a:PERSON) 
WHERE 
(NOT HAS (a.loves) OR (LENGTH(FILTER(love IN a.loves WHERE love IN ['kittens','puppies','guppies','manatees'])) = LENGTH(a.loves)))
AND
(NOT HAS (a.likes) OR (LENGTH(FILTER(like IN a.likes WHERE like IN ['kittens','puppies','guppies','manatees'])) > 0))
AND
(NOT HAS (a.hates) OR (LENGTH(FILTER(hate IN a.hates WHERE hate IN ['kittens','puppies','guppies','manatees'])) = 0))
RETURN a.name

Huzzah, Alice and Bob are both OK with that.

However, is that the smartest way to do it in Cypher?

This is of course a toy example: there will also be a shape in the MATCH and other filtering conditions.

However, my focus is on every person in the shape optionally having none, one two or three set of things[*], one of which contains elements which must ALL be matched by the elements of a provided collection, one of which ANY must be matched, and one which contains elements of which NONE must be matched by anything in the (common) provided collection. Implementing this is a core requirement.

[*] I say "things" rather than "properties", because I'm not averse to modelling my animals as nodes, and linking my people to them. Like this, for Carol and Dan who conveniently share the same taste in animals as Alice and Bob.

CREATE (carol:PERSON {name: 'Carol'})-[:LOVES]->(kittens {name:'kittens'}),
(carol)-[:LOVES]->(puppies {name:'puppies'}),
(carol)-[:LIKES]->({name:'guppies'}),
(carol)-[:LIKES]->({name:'goldfish'}),
(carol)-[:HATES]->({name:'echidnas'}),
(dan:PERSON {name: 'Dan'})-[:LIKES]->(kittens),
(dan)-[:LIKES]->({name:'hamsters'})

MATCH (a:PERSON), (a)-[:LOVES]->(a_loves), (a)-[:LIKES]->(a_likes), (a)-[:HATES]->(a_hates)
WHERE
ALL (loves_name IN a_loves.name WHERE loves_name IN ['kittens','puppies','guppies','manatees'])
AND
ANY (likes_name IN a_likes.name WHERE likes_name IN ['kittens','puppies','guppies','manatees'])
AND
NONE (hates_name IN a_hates.name WHERE hates_name IN ['kittens','puppies','guppies','manatees'])
RETURN a.name

This works for each person who does love, like and hate at least one animal, i.e. for Carol. However, it does not work when a person does not have all three of LOVES and LIKES and HATES relationships, as that shape isn't found in the graph, and so it does not find Dan. I cannot see an obvious way to make OPTIONAL MATCH perform that kind of pruning.

To get around that issue, I can add fake animal nodes and give each and every PERSON relationships to them, thusly: -[:LOVES]->(unicorns), -[:LIKES]->(manticores) -[:HATES]->(basilisks) and then always add 'unicorns' to the collection being compared against the LOVES node. However, that feels very contrived and clunky.

Long post short, what would be your favoured way to model this in Neo4J and Cypher, and why?

1

1 Answers

1
votes

Here's a first cut http://gist.neo4j.org/?8932364

Do take a look at let me know if first of all, the problem is understood and second, if the query fits.

I'd like to come back later and improve on that query, it was put together quickly.