Extract the weights from several relationships in 1 cypher query - neo4j

I want to calculate a "corporation index" for a particular user based on how many times a user viewed or updated a file. In order to get this I'll assign values to certain paths. This is an example:
(u1:User {name: 'Alice'})-[:UPDATED]->(f:File)<-[:VIEWED]-(u2:User) // is worth 0.2 points
(u1:User {name: 'Alice'})-[:VIEWED]->(f:File)<-[:VIEWED]-(u2:User) // is worth 0.1 points
(u1:User {name: 'Alice'})-[:VIEWED]->(f:File)<-[:UDATED]-(u2:User) // is worth 0.2 points
(u1:User {name: 'Alice'})-[:UPDATED]->(f:File)<-[:UPDATED]-(u2:User) // is worth 0.5 points
The image shows a possible graph.
I want to know how a query has to look like that returns the following results.
User: Alice, User: Charly, index: (3 * 0.2) // 3 because there are 3 matching paths (Relationship with the lowest weight in the path)
User: Alice, User: Bob, index: (3 * 0.1)
This is what I have so far:
MATCH (u1:User {name:'Alice'})-[r1:VIEWED]->(f:File)<-[r2:UPDATED]-(u2:User)
OPTIONAL MATCH (u1:User {name:'Alice'})-[r3:VIEWED]->(f:File)<-[r4:VIEWED]-(u3:User)
RETURN u2.name, min(r1.weight) AS ViewUpd, u3.name, min(r3.weight) AS ViewView
This query doesn't work at all, but I hope it clarifies what I want.

[EDITED]
Does this query do what you want?
MATCH (u1:User { name:'Alice' })-[r1]->(f:File)<-[r2]-(u2:User)
RETURN
u2.name AS name,
SUM(
r1.weight * (CASE
WHEN (TYPE(r1)= "VIEWED" AND TYPE(r2)= "VIEWED") THEN 0.1
WHEN (TYPE(r1)= "UPDATED" AND TYPE(r2)= "UPDATED") THEN 0.5
ELSE 0.2
END)) AS index;
Here is a console showing this query, and here is a sample result:
+--------------------------------+
| name | index |
+--------------------------------+
| "Bob" | 0.6000000000000001 |
| "Elvis" | 1.5 |
| "David" | 0.6000000000000001 |
| "Charley" | 2.1 |
+--------------------------------+
In my sample data, "Charley" has an UPDATED relationship and a VIEWED relationship with the File that is UPDATED by "Alice". The resulting index for Charley is the sum of the index values for both of those relationships.

Related

In neo4j, a query to count the number of distinct structures

In neo4j my database consists of chains of nodes. For each distinct stucture/layout (does graph theory has a better word?), I want to count the number of chains. For example, the database consists of 9 nodes and 5 relationships as this:
(:a)->(:b)
(:b)->(:a)
(:a)->(:b)
(:a)->(:b)->(:b)
where (:a) is a node with label a. Properties on nodes and relationships are irrelevant.
The result of the counting should be:
------------------------
| Structure | n |
------------------------
| (:a)->(:b) | 2 |
| (:b)->(:a) | 1 |
| (:a)->(:b)->(:b) | 1 |
------------------------
Is there a query that can achieve this?
Appendix
Query to create test data:
create (:a)-[:r]->(:b), (:b)-[:r]->(:a), (:a)-[:r]->(:b), (:a)-[:r]->(:b)-[:r]->(:b)
EDIT:
Thanks for the clarification.
We can get the equivalent of what you want, a capture of the path pattern using the labels present:
MATCH path = (start)-[*]->(end)
WHERE NOT ()-->(start) and NOT (end)-->()
RETURN [node in nodes(path) | labels(node)[0]] as structure, count(path) as n
This will give you a list of the labels of the nodes (the first label present for each...remember that nodes can be multi-labeled, which may throw off your results).
As for getting it into that exact format in your example, that's a different thing. We could do this with some text functions in APOC Procedures, specifically apoc.text.join().
We would need to first add formatting around the extraction of the first label to add the prefixed : as well as the parenthesis. Then we could use apoc.text.join() to get a string where the nodes are joined by your desired '->' symbol:
MATCH path = (start)-[*]->(end)
WHERE NOT ()-->(start) and NOT (end)-->()
WITH [node in nodes(path) | labels(node)[0]] as structure, count(path) as n
RETURN apoc.text.join([label in structure | '(:' + label + ')'], '->') as structure, n

How to get a unique set of node pairs for undirected relationships

I have a simple set of undirected relationships:
(p1)-[r:appears_in_same_doc]-(p2)
I would like to extract id(p1), id(p2) pairs from this. However, when I do the following:
MATCH (p1:person)-[r:appear_in_same_document]-(p2:person)
return id(p1), id(p2)
I get:
id(p1) | id(p2
-------+------
1 | 2
2 | 1
1 | 3
3 | 1
etc.
That is, I get for each pair of nodes, both possibilities:
p1, p2 AND p2, p1
in the result set. This is undesired in the application I work on, but I cannot figure out how to get only half of the "possibilities". I.e.
id(p1) | id(p2
-------+------
1 | 2
1 | 3
etc.
Just do
MATCH (p1:person)-[r:appear_in_same_document]-(p2:person)
where id(p1) > id(p2)
return id(p1), id(p2)

Neo4j Cypher - How to Count Multiple Property Values With Cypher Efficiently And Paginate Properly

I am struggling to get the proper cypher that is both efficient and allows pagination through skip and limit.
Here is the simple scenario: I have the related nodes (company)<-[A]-(set)<-[B]-(job) where there are multiple instances of (set) with distinct (job) instances related to them. The (job) nodes have a specific status property that can hold one of several states. We need to count the number of (job) nodes in a particular state per (set) and use skip and limit to paginate on the distinct (set) nodes.
So we can get a very efficient query for job.status counts using this.
match (c:Company {id: 'MY.co'})<-[:type_of]-(s:Set)<-[:job_for]-(j:Job)
return s.Description, j.Status, count(*) as StatusCount;
Which will give us a rows of the Set.Description, Job.Status, and JobStatus count. But we will get multiple rows for the Set based on the Job.Status. This is not conducive to paging over distinct sets though. Something like:
s.Description j.Status StatusCount
-------------------+--------------+----------------
Set 1 | Unassigned | 10
Set 1 | Completed | 2
Set 2 | Unassigned | 3
Set 1 | Reviewed | 10
Set 3 | Completed | 4
Set 2 | Reviewed | 7
What we are trying to achieve with the proper cypher is result rows based on distinct Sets. Something like this:
s.Description Unassigned Completed Reviewed
-------------------+--------------+-------------+----------
Set 1 | 10 | 2 | 10
Set 2 | 3 | 0 | 7
Set 3 | 0 | 4 | 0
This would then allow us to paginate over Sets using skip and limit properly.
I have tried many different approaches and cannot seem to find the right combination for this type of result. Anyone have any ideas? Thanks!
** EDIT - Using the answer provided by MIchael, here's how to get the status count values in java **
match (c:Company {id: 'MY.co'})<-[:type_of]-(s:Set)<-[:job_for]-(j:Job)
with s, j.Status as Status,count(*) as StatusCount
return s.Description, collect({Status:Status,StatusCount:StatusCount]) as StatusCounts;
List<Object> statusMaps = (List<Object>) row.get("StatusCounts");
for(Object statusEntry : statusMaps ) {
Map<String,Object> statusMap = (Map<String,Object>) statusEntry;
String status = (String) statusMap.get("Status");
Number count = statusMap.get("StatusCount");
}
You can use WITH and aggregation, and optionally a map result
match (c:Company {id: 'MY.co'})<-[:type_of]-(s:Set)<-[:job_for]-(j:Job)
with s, j.Status as Status,count(*) as StatusCount
return s.Description, collect([Status,StatusCount]);
or
match (c:Company {id: 'MY.co'})<-[:type_of]-(s:Set)<-[:job_for]-(j:Job)
with s, j.Status as Status,count(*) as StatusCount
return s.Description, collect({Status:Status,StatusCount:StatusCount]);

Performance in Neo4j cypher query

I have the following cypher query:
MATCH (country:Country { name: 'norway' }) <- [:LIVES_IN] - (person:Person)
WITH person
MATCH (skill:Skill { name: 'java' }) <- [:HAS_SKILL] - (person)
WITH person
OPTIONAL MATCH (skill:Skill { name: 'javascript' }) <- [rel:HAS_SKILL] - (person)
WITH person, CASE WHEN skill IS NOT NULL THEN 1 ELSE 0 END as matches
ORDER BY matches DESC
LIMIT 50
RETURN COLLECT(ID(person)) as personIDs
It seems to perform worse when adding more nodes. Right now with only 5000 Person nodes (a Person node can have multiple HAS_SKILL relationships to Skill nodes). Right now it takes around 180 ms to perform the query, but adding another 1000 Person nodes with relationships adds 30-40 ms to the query. We are planning on having millions of Person nodes, so adding 40 ms every 1000 Person is a no go.
I use parameters in my query instead of 'norway', 'java', 'javascript' in the above query. I have created indexes on :Country(name) and :Skill(name).
My goal with the query is to match every person that lives in a specified country (norway) which also have the skill 'java'. If the person also have the skill 'javascript' it should be ordered higher in the result.
How can I restructure the query to improve performance?
Edit:
There also seems to be an issue with the :Country nodes, if I switch out
MATCH (country:Country { name: 'norway' }) <- [:LIVES_IN] - (person:Person)
with
MATCH (city:City { name: 'vancouver' }) <- [:LIVES_IN] - (person:Person)
the query time jumps down to around 15-50 ms, depending on what city i query for. It is still a noticeable increase in query time when adding more nodes.
Edit 2:
I seems like the query time is increased by a huge amount when there is a lot of rows in the first match clause. So if I switch the query to match on Skill nodes first, the query times decreases substantially. The query is part of an API and it is created dynamically and I do not know which of the match clauses that will return the smallest amount of rows. It will probably also be a lot more rows in every match clause when the database grows.
Edit 3
I have done some testing from the answers and I now have the following query:
MATCH (country:Country { name: 'norway'})
WITH country
MATCH (country) <- [:LIVES_IN] - (person:Person)
WITH person
MATCH (person) - [:HAS_SKILL] -> (skill:Skill) WHERE skill.name = 'java'
MATCH (person) - [:MEMBER_OF_GROUP] -> (group:Group) WHERE group.name = 'some_group_name'
RETURN DISTINCT ID(person) as id
LIMIT 50
this still have performance issues, is it maybe better to first match all the skills etc, like with the Country node? The query can also grow bigger, I may have to add matching against multiple skills, groups, projects etc.
Edit 4
I modified the query slightly and it seems like this did the trick. I now match all the needed skills, company, groups, country etc first. Then use those later in the query. In the profiler this reduced the database hits from 700k to 188 or something. It is a slightly different query from my original query (different labeled nodes etc), but it solves the same problem. I guess this can be further improved by maybe matching on the node with the least relationships first etc, to start with a reduced number of nodes. I'll do some more testing later!
MATCH (company:Company { name: 'relinkgroup' })
WITH company
MATCH (skill:Skill { name: 'java' })
WITH company, skill
MATCH (skill2:Skill { name: 'ajax' })
WITH company, skill, skill2
MATCH (country:Country { name: 'canada' })
WITH company, skill, skill2, country
MATCH (company) <- [:WORKED_AT] - (person:Person)
, (person) - [:HAS_SKILL] -> (skill)
, (person) - [:HAS_SKILL] -> (skill2)
, (person) - [:LIVES_IN] -> (country)
RETURN DISTINCT ID(person) as id
LIMIT 50
For the first line of your query, the execution has to look for all possible paths between the country and person. Limiting your initial match (thus defining a more accurate starting point for the traversal) you'll win some performance.
So instead of
MATCH (country:Country { name: 'norway' }) <- [:LIVES_IN] - (person:Person)
Try doing it in two steps :
MATCH (country:Country { name: 'norway' })
WITH country
MATCH (country)<-[:LIVES_IN]-(person:Person)
WITH person
As an example, I'll use the simple movie app in the neo4j console : http://console.neo4j.org/
Doing a query equivalent to yours for finding people that knows cypher :
MATCH (n:Crew)-[r:KNOWS]-m WHERE n.name='Cypher' RETURN n, m
The execution plan will be :
Execution Plan
ColumnFilter
|
+Filter
|
+TraversalMatcher
+------------------+------+--------+-------------+----------------------------------------+
| Operator | Rows | DbHits | Identifiers | Other |
+------------------+------+--------+-------------+----------------------------------------+
| ColumnFilter | 2 | 0 | | keep columns n, m |
| Filter | 2 | 14 | | Property(n,name(0)) == { AUTOSTRING0} |
| TraversalMatcher | 7 | 16 | | m, r, m |
+------------------+------+--------+-------------+----------------------------------------+
Total database accesses: 30
And by defining an accurate starting point :
MATCH (n:Crew) WHERE n.name='Cypher' WITH n MATCH (n)-[:KNOWS]-(m) RETURN n,m
Result in the following execution plan :
Execution Plan
ColumnFilter
|
+SimplePatternMatcher
|
+Filter
|
+NodeByLabel
+----------------------+------+--------+-------------------+----------------------------------------+
| Operator | Rows | DbHits | Identifiers | Other |
+----------------------+------+--------+-------------------+----------------------------------------+
| ColumnFilter | 2 | 0 | | keep columns n, m |
| SimplePatternMatcher | 2 | 0 | m, n, UNNAMED53 | |
| Filter | 1 | 8 | | Property(n,name(0)) == { AUTOSTRING0} |
| NodeByLabel | 4 | 5 | n, n | :Crew |
+----------------------+------+--------+-------------------+----------------------------------------+
Total database accesses: 13
As you can see, the first method use the traversal pattern, which is quite a bit exponantionnaly expensive with the amount of nodes, and you're doing a global match on the graph.
The second uses an explicit starting point, using the labels index.
EDIT
For the skills part, I would do something like this, if you have some test data to provide it could be more helpful for testing :
MATCH (country:Country { name: 'norway' })
WITH country
MATCH (country)<-[:LIVES_IN]-(person:Person)-[:HAS_SKILL]->(skill:Skill)
WHERE skill.name = 'java'
WITH person
OPTIONAL MATCH (person)-[:HAS_SKILL]->(skillb:Skill) WHERE skillb.name = 'javascript'
WITH person, skillb
There is no need for global lookups, as he already found persons, he just follows the "HAS_SKILL" relationships and filter on skill.name value
Edit 2:
Concerning your last edit, maybe this last part of the query :
MATCH (company) <- [:WORKED_AT] - (person:Person)
, (person) - [:HAS_SKILL] -> (skill)
, (person) - [:HAS_SKILL] -> (skill2)
, (person) - [:LIVES_IN] -> (country)
Could be better written as :
MATCH (person:Person)-[:WORKED_AT]->(company)
WHERE (person)-[:HAS_SKILL]->(skill)
AND (person)-[:HAS_SKILL]->(skill2)
AND (person)-[:LIVES_IN]->(country)

neo4j - multiple optional matches

I have the following neo4j database:
http://console.neo4j.org/?id=gkkmha
I then run the following query:
MATCH (person:Person)-[:plays]->(instrument:Instrument {name: 'Drums'})
OPTIONAL MATCH (band:Band { name: 'bandname' })-[:genre]->(genre:Genre)<-[:likes]-(person)
OPTIONAL MATCH (band)-[:influenced]->(influence:Influence)<-[:influenced]-(person)
RETURN person.name, COLLECT(genre.name) as matched_genres, COLLECT (influence.name) as matched_influences, (count(genre)/4.0) as score
ORDER BY score DESC
I want to be able to find musicians who play the specified instrument but also have similar genre matches and influences to the band. So far I've got it working for matching genres and returning a list of those genres, but I can't make it do the same for influences. I want it to return a list of matching influences as well.
Ideally it'd also get the total number of genres and influences the band is associated with (though this is just a nice to have).
Current output:
+-----------------------------------------------------------------+
| person.name | matched_genres | matched_influences | score |
+-----------------------------------------------------------------+
| "Robert Smith" | ["Soul","Motown"] | [] | 0.5 |
| "Alex Smith" | ["Soul"] | [] | 0.25 |
| "Mr Drummer" | [] | [] | 0.0 |
+-----------------------------------------------------------------+
3 rows
54 ms
Any thoughts?
i believe you got there a typo, instead of :influenced try :Influenced
MATCH (person:Person)-[:plays]->(instrument:Instrument { name: 'Drums' })
OPTIONAL
MATCH (band:Band { name: 'bandname' })-[:genre]->(genre:Genre)<-[:likes]-(person)
OPTIONAL
MATCH (band)-[:Influenced]->(influence:Influence)<-[:Influenced]-(person)
RETURN person.name, COLLECT(genre.name) AS matched_genres, COLLECT(influence.name) AS matched_influences,(count(genre)/4.0) AS score
ORDER BY score DESC

Resources