简体   繁体   中英

neo4j cypher - how to find all nodes that have a relationship to list of nodes

I have nodes- named "options". "Users" choose these options. I need a chpher query that works like this:

retrieve users who had chosen all the options those are given as a list.

MATCH (option:Option)<-[:CHOSE]-(user:User) WHERE  option.Key IN ['1','2','2'] Return user

This query gives me users who chose option(1), option(2) and option(3) and also gives me the user who only chose option(2).

What I need is only the users who chose all of them -option(1), option(2) and option(3).

For an all cypher solution (don't know if it's better than Chris' answer, you'll have to test and compare) you can collect the option.Key for each user and filter out those who don't have a option.Key for each value in your list

MATCH (u:User)-[:CHOSE]->(opt:Option)
WITH u, collect(opt.Key) as optKeys
WHERE ALL (v IN {values} WHERE v IN optKeys)
RETURN u

or match all the options whose keys are in your list and the users that chose them, collect those options per user and compare the size of the option collection to the size of your list (if you don't give duplicates in your list the user with an option collection of equal size has chosen all the options)

MATCH (u:User)-[:CHOSE]->(opt:Option)
WHERE opt.Key IN {values}
WITH u, collect(opt) as opts
WHERE length(opts) = length({values}) // assuming {values} don't have duplicates
RETURN u

Either should limit results to users connected with all the options whose key values are specified in {values} and you can vary the length of the collection parameter without changing the query.

If the number of options is limited, you could do:

MATCH 
    (user:User)-[:Chose]->(option1:Option),
    (user)-[:Chose]->(option2:Option),
    (user)-[:Chose]->(option3:Option)
WHERE
    option1.Key = '1'
    AND option2.Key = '2'
    AND option3.Key = '3'
RETURN
    user.Id

Which will only return the user with all 3 options.

It's a bit rubbishy as obviously you end up with 3 lines where you have 1, but I don't know how to do what you want using the IN keyword.

If you're coding against it, it's pretty simple to generate the WHERE and MATCH clause, but still - not ideal. :(

EDIT - Example

Turns out there is some string manipulation going on here (!), but you can always cache bits. Importantly - it's using Params which would allow neo4j to cache the queries and supply faster responses with each call.

public static IEnumerable<User> GetUser(IGraphClient gc)
{
    var query = GenerateCypher(gc, new[] {"1", "2", "3"});
    return query.Return(user => user.As<User>()).Results;
}


public static ICypherFluentQuery GenerateCypher(IGraphClient gc, string[] options)
{
    ICypherFluentQuery query = new CypherFluentQuery(gc);
    for(int i = 0; i < options.Length; i++)
        query = query.Match(string.Format("(user:User)-[:CHOSE]->(option{0}:Option)", i));

    for (int i = 0; i < options.Length; i++)
    {
        string paramName = string.Format("option{0}param", i);
        string whereString = string.Format("option{0}.Key = {{{1}}}", i, paramName);
        query = i == 0 ? query.Where(whereString) : query.AndWhere(whereString);
        query = query.WithParam(paramName, options[i]);
    }

    return query;
}
MATCH (user:User)-[:CHOSE]->(option:Option) 
WHERE option.key IN ['1', '2', '3']
WITH user, COUNT(*) AS num_options_chosen
WHERE num_options_chosen = LENGTH(['1', '2', '3'])
RETURN user.name

This will only return users that have relationships with all the Options with the given keys in the array. This assumes there are not multiple [:CHOSE] relationships between users and options. If it is possible for a user to have multiple [:CHOSE] relationships with a single option, you'll have to add some conditionals as necessary.

I tested the above query with the below dataset:

CREATE (User1:User {name:'User 1'}),
       (User2:User {name:'User 2'}),
       (User3:User {name:'User 3'}),

       (Option1:Option {key:'1'}),
       (Option2:Option {key:'2'}),
       (Option3:Option {key:'3'}),
       (Option4:Option {key:'4'}),

       (User1)-[:CHOSE]->(Option1),
       (User1)-[:CHOSE]->(Option4),

       (User2)-[:CHOSE]->(Option2),
       (User2)-[:CHOSE]->(Option3),

       (User3)-[:CHOSE]->(Option1),
       (User3)-[:CHOSE]->(Option2),
       (User3)-[:CHOSE]->(Option3),
       (User3)-[:CHOSE]->(Option4)

And I get only 'User 3' as the output.

For shorter lists, you can use path predicates in your WHERE clause:

MATCH (user:User)
WHERE (user)-[:CHOSE]->(:Option { Key: '1' })
AND   (user)-[:CHOSE]->(:Option { Key: '2' })
AND   (user)-[:CHOSE]->(:Option { Key: '3' })
RETURN user

Advantages:

  • Clear to read
  • Easy to generate for dynamic length lists

Disadvantages:

  • For each different length, you will have a different query that has to be parsed and cached by Cypher. Too many dynamic queries will watch your cache hit rate go through the floor, query compilation work go up, and query performance go down.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM