Distinct nodes in multiple MATCH query - neo4j

I have a movie database with users rating movies. I want to find the top 5 most similar users to user 1 (first MATCH which works fine) and recommend him the top rated movies watched by those similar users but not watched by user 1. I get the same movie multiple times even though I have "distinct" in my query. What am I doing wrong?
MATCH (target_user:User {id : 1})-[:RATED]->(m:Movie)
<-[:RATED]-(other_user:User)
WITH other_user, count(distinct m.title) AS num_common_movies, target_user
ORDER BY num_common_movies DESC
LIMIT 5
MATCH other_user-[rat_other_user:RATED]->(m2:Movie)
WHERE NOT (target_user-[:RATED]->m2)
WITH distinct m2.title as movietitle, rat_other_user.note AS rating,
other_user.id AS watched_by
RETURN movietitle, rating, watched_by
ORDER BY rating DESC

You dataset probably has many users who have watched and rated the same movies. When you execute that DISTINCT statement it is going to return a distinct row, not a distinct movie title. Different users will have rated the unwatched movies differently and have different names.
You will have to tune this for your particular use case but you can start from:
MATCH (target_user:User { uid : 1 })-[:RATED]->(m:Movie)<-[:RATED]-(other_user:User)
WITH other_user, count(DISTINCT m.title) AS num_common_movies, target_user
ORDER BY num_common_movies DESC
LIMIT 5
MATCH other_user-[rat_other_user:RATED]->(m2:Movie)
WHERE NOT (target_user-[:RATED]->m2)
RETURN DISTINCT m2.name AS movietitle, COLLECT(rat_other_user.note) AS ratings,
MAX(rat_other_user.note) AS maxi, AVG(rat_other_user.note) as aver, COLLECT(other_user.name) AS users
ORDER BY aver DESC
I added a console demo here.
Importantly the you are now aggregating your results per movie title.

Related

why DISTINCT is needed in this Cypher query?

The below query is taken from neo4j movie review dataset sandbox:
MATCH (u:User {name: "Some User"})-[r:RATED]->(m:Movie)
WITH u, avg(r.rating) AS mean
MATCH (u)-[r:RATED]->(m:Movie)-[:IN_GENRE]->(g:Genre)
WHERE r.rating > mean
WITH u, g, COUNT(*) AS score
MATCH (g)<-[:IN_GENRE]-(rec:Movie)
WHERE NOT EXISTS((u)-[:RATED]->(rec))
RETURN rec.title AS recommendation, rec.year AS year, COLLECT(DISTINCT g.name) AS genres, SUM(score) AS sscore
ORDER BY sscore DESC LIMIT 10
what I can not understand is: why the DISTINCT keyword is required in the query's return statement?. Because the expected results from the last MATCH statement is something like this:
g1,x
g1,y
...
g2,z
g2,v
g2,m
...
gn,m
gn,b
gn,x
where g1,g2,..gn are the set of genres and x,y,z,v,m,b... are a set of movies (in addition there is a user and score column deleted for readability).
So according to my understanding what this query is returning: For each movie return its genres and the sum of their scores.
Assumptions:
Every Movie has a unique title. (This is required for the query to work as is.)
Every Genre has a unique name.
Every Movie has at most one IN_GENRE relationship to each distinct Genre.
Given the above assumptions, you are correct that the DISTINCT is not necessary. That is because the RETURN clause is using rec.title as one of the aggregation grouping keys.

Cypher recommendation score

Looking at the example from GrandStack movies workshop https://github.com/grand-stack/grand-stack-movies-workshop/blob/master/neo4j-database/answers.md
The query proposed for recommended movies here
MATCH (m:Movie) WHERE m.movieId = $movieId
MATCH (m)-[:IN_GENRE]->(g:Genre)<-[:IN_GENRE]-(movie:Movie)
WITH m, movie, COUNT(*) AS genreOverlap
MATCH (m)<-[:RATED]-(:User)-[:RATED]->(movie:Movie)
WITH movie,genreOverlap, COUNT(*) AS userRatedScore
RETURN movie ORDER BY (0.9 * genreOverlap) + (0.1 * userRatedScore) DESC LIMIT 3
Wouldnt this query be biased in the sense that it will only calculate userRatedScore for movies that share at least one genre with the movie with Id $movieId?
How would a rewritten query look that computes both scores independently, meaning it would still calculate userRatedScore for a given movie even if it does not share genres with the movie with Id $movieId
If you'd like to ignore the weighting provided by Genre, then you could drop the part of the query that seeks it, something like:
MATCH (m:Movie) WHERE m.movieId = $movieId, (m)<-[:RATED]-(:User)-[:RATED]->(movie:Movie)
WITH movie, COUNT(*) AS userRatedScore
RETURN movie ORDER BY (0.1 * userRatedScore) DESC LIMIT 3

Neo4J: query optimisation/model validation

New to Neo4J so apologies in advance if I am doing things horribly wrong. I am trying to show user articles in which they could be interested in based on the categories they have selected and tags they have liked independently.
My model in Neo4j is something like this
(:USER)-[:LIKES]->(:TAG)
(:ARTICLE)-[:PUBLISHED_BY]->(:PROVIDER)
(:ARTICLE)-[:HAS_CATEGORY]->(:CATEGORY)
(:USER)-[:DISLIKES]-(:ARTICLE)
(:USER)-[:INTERESTED_IN]->(:CATEGORY)
When I try to run the following query to get the desired results...I get them but the query is taking 16-18 seconds to execute.
MATCH (u:USER {id: $userid})-[:LIKES]->(t:TAG)
WITH u,t, collect(t.name) as tags
UNWIND tags as tag with u,tag
MATCH (c:CATEGORY)<-[*]-(a:ARTICLE)-[pub:PUBLISHED_BY]->(p:PROVIDER)
WHERE a.keywords contains tag OR c.id in $categoryArray
AND NOT (u)-[:DISLIKES]->(a)
RETURN DISTINCT a.id AS id, a.title AS title, pub.pubDate
ORDER BY pub.pubDate DESC LIMIT 250
Is there a faster and better way to get the desired results?
Note: I am using Neo4j 3.4.1 version on ubuntu machine with page-cache: 512mb and MIN & MAX heap size: 1500mb
It would be better if in your model articles are connected to tags.
This bit: a.keywords contains tag is not index supported, so it will lead to a full scan.
Also, from categories to articles might be a long chain, so add a rel-type there and add an upper limit. It might be better to check found articles against categories.
MATCH (u:USER {id: $userid})-[:LIKES]->(tag:TAG)
MATCH (a:ARTICLE)-[:HAS_TAG]->(tag)
WITH distinct u, a
WHERE any(c IN categories WHERE NOT shortestPath((c)<-[:IN_CATEGORY*]-(a)) IS NULL)
AND NOT (u)-[:DISLIKES]->(a)
MATCH (a)-[pub:PUBLISHED_BY]->(p:PROVIDER)
RETURN DISTINCT a.id AS id, a.title AS title, pub.pubDate
ORDER BY pub.pubDate DESC LIMIT 250
Also check the query plan with PROFILE to see any bottlenecks or unindexed fields (you can expand the boxes with the double arrow in the lower right corner)
Thanks #Michael I understand that having tags as separate nodes related to articles would make the search faster but the following query has brought down the search time from 16-18 seconds to 3-4 seconds at the moment
MATCH (u:USER {id: $userId})-[:INTERESTED_IN]->(c:CATEGORY)<-[*]-(a:ARTICLE)[pub:PUBLISHED_BY]->(p:PROVIDER) WHERE NOT (u)-[:DISLIKES]->(a) RETURN DISTINCT a.id, a.title, pub.pubDate ORDER BY pub.pubDate DESC LIMIT 150 UNION MATCH (u:USER {id: $userId})-[:LIKES]->(t:TAG) WITH u, t, collect(t.name) AS tags UNWIND tags AS tag MATCH (a:ARTICLE)-[pub:PUBLISHED_BY]-(:PROVIDER) WHERE a.keywords CONTAINS tag AND NOT (u)-[:DISLIKES]->(a) RETURN DISTINCT a.id, a.title, pub.pubDate ORDER BY pub.pubDate DESC LIMIT 150

Neo4j: Query to find the nodes with most relationships, and their connected nodes

I am using Neo4j CE 3.1.1 and I have a relationship WRITES between authors and books. I want to find the N (say N=10 for example) books with the largest number of authors. Following some examples I found, I came up with the query:
MATCH (a)-[r:WRITES]->(b)
RETURN r,
COUNT(r) ORDER BY COUNT(r) DESC LIMIT 10
When I execute this query in the Neo4j browser I get 10 books, but these do not look like the ones written by most authors, as they show only a few WRITES relationships to authors. If I change the query to
MATCH (a)-[r:WRITES]->(b)
RETURN b,
COUNT(r) ORDER BY COUNT(r) DESC LIMIT 10
Then I get the 10 books with the most authors, but I don't see their relationship to authors. To do so, I have to write additional queries explicitly stating the name of a book I found in the previous query:
MATCH ()-[r:WRITES]->(b)
WHERE b.title="Title of a book with many authors"
RETURN r
What am I doing wrong? Why isn't the first query working as expected?
Aggregations only have context based on the non-aggregation columns, and with your match, a unique relationship will only occur once in your results.
So your first query is asking for each relationship on a row, and the count of that particular relationship, which is 1.
You might rewrite this in a couple different ways.
One is to collect the authors and order on the size of the author list:
MATCH (a)-[:WRITES]->(b)
RETURN b, COLLECT(a) as authors
ORDER BY SIZE(authors) DESC LIMIT 10
You can always collect the author and its relationship, if the relationship itself is interesting to you.
EDIT
If you happen to have labels on your nodes (you absolutely SHOULD have labels on your nodes), you can try a different approach by matching to all books, getting the size of the incoming :WRITES relationships to each book, ordering and limiting on that, and then performing the match to the authors:
MATCH (b:Book)
WITH b, SIZE(()-[:WRITES]->(b)) as authorCnt
ORDER BY authorCnt DESC LIMIT 10
MATCH (a)-[:WRITES]->(b)
RETURN b, a
You can collect on the authors and/or return the relationship as well, depending on what you need from the output.
You are very close: after sorting, it is necessary to rediscover the authors. For example:
MATCH (a:Author)-[r:WRITES]->(b:Book)
WITH b,
COUNT(r) AS authorsCount
ORDER BY authorsCount DESC LIMIT 10
MATCH (b)<-[:WRITES]-(a:Author)
RETURN b,
COLLECT(a) AS authors
ORDER BY size(authors) DESC

Get the full graph of a query in Neo4j

Suppose tha I have the default database Movies and I want to find the total number of people that have participated in each movie, no matter their role (i.e. including the actors, the producers, the directors e.t.c.)
I have already done that using the query:
MATCH (m:Movie)<-[r]-(n:Person)
WITH m, COUNT(n) as count_people
RETURN m, count_people
ORDER BY count_people DESC
LIMIT 3
Ok, I have included some extra options but that doesn't really matter in my actual question. From the above query, I will get 3 movies.
Q. How can I enrich the above query, so I can get a graph including all the relationships regarding these 3 movies (i.e.DIRECTED, ACTED_IN,PRODUCED e.t.c)?
I know that I can deploy all the relationships regarding each movie through the buttons on each movie node, but I would like to know whether I can do so through cypher.
Use additional optional match:
MATCH (m:Movie)<--(n:Person)
WITH m,
COUNT(n) as count_people
ORDER BY count_people DESC
LIMIT 3
OPTIONAL MATCH p = (m)-[r]-(RN) WHERE type(r) IN ['DIRECTED', 'ACTED_IN', 'PRODUCED']
RETURN m,
collect(p) as graphPaths,
count_people
ORDER BY count_people DESC

Resources