Статьи

Агрегирование отношений внутри пути

Недавно я натолкнулся на интересный случай использования путей на графике, где мы хотели вычислить частоту общения между двумя людьми, показывая, как часто каждый из них отправляет электронное письмо другому.

Модель выглядела так:

Сообщения электронной почты

который мы можем создать с помощью следующих шифровальных выражений:

CREATE (email1 { name: 'Email 1', title: 'Some stuff' })
CREATE (email2 { name: 'Email 2', title: "Absolutely irrelevant" })
CREATE (email3 { name: 'Email 3', title: "Something else" })
CREATE (person1 { name: 'Mark' })
CREATE (person2 { name: 'Jim' })
CREATE (person3 { name: 'Alistair' })
CREATE (person1)-[:SENT]->(email1)
CREATE (person2)-[:RECEIVED]->(email1)
CREATE (person3)-[:RECEIVED]->(email1)
CREATE (person1)-[:SENT]->(email2)
CREATE (person2)-[:RECEIVED]->(email2)
CREATE (person2)-[:SENT]->(email3)
CREATE (person1)-[:RECEIVED]->(email3)

Мы хотим вернуть список, содержащий пары людей и сколько раз они писали друг другу по электронной почте, поэтому в этом случае мы хотим вернуть таблицу, показывающую следующее:

+-------------------------------------------+
| Person 1 | Person 2 | P1 -> P2 | P2 -> P1 |
|-------------------------------------------|
| Alistair | Mark     | 0        | 1        |
| Jim      | Mark     | 1        | 2        |
+-------------------------------------------+

Я начал со следующего запроса, который находит все электронные письма, а затем переходит к поиску отправителя и получателя и объединяет их вместе:

START email = node:node_auto_index('name:"Email 1" name:"Email 2" name: "Email 3"') 
MATCH sender-[:SENT]->email<-[:RECEIVED]-receiver 
RETURN sender.name, receiver.name, COUNT(email) AS count

который возвращает следующее:

==> +-------------------------------------+
==> | sender.name | receiver.name | count |
==> +-------------------------------------+
==> | "Mark"      | "Jim"         | 2     |
==> | "Jim"       | "Mark"        | 1     |
==> | "Mark"      | "Alistair"    | 1     |
==> +-------------------------------------+

Это дает нам все пары, которые отправляли электронные письма друг другу, но не группирует их вместе, что немного раздражает.

Поняв, что довольно сложно получить эту группу людей, если я начну с писем, я изменил запрос, чтобы начать с людей, а затем перебирать письма, которые они писали друг другу:

START personA = node:node_auto_index('name:"Mark" name:"Jim" name:"Alistair"'), 
      personB = node:node_auto_index('name:"Mark" name:"Jim" name:"Alistair"') 
MATCH personA-[s:SENT]->email<-[r:RECEIVED]-personB 
WHERE personA <> personB 
 
WITH personA, personB, LENGTH(COLLECT(s)) AS AToB 
MATCH personB-[s?:SENT]->email<-[r:RECEIVED]-personA 
RETURN personA.name, personB.name, AToB, LENGTH(COLLECT(s)) AS BToA

Здесь мы начнем с нашего списка людей, а затем найдем все комбинации, в которых personA отправил электронное письмо, полученное personB.

Если мы выполним только первую половину этого, мы увидим комбинации отправленных / полученных писем:

START personA = node:node_auto_index('name:"Mark" name:"Jim" name:"Alistair"'), 
      personB = node:node_auto_index('name:"Mark" name:"Jim" name:"Alistair"') 
MATCH personA-[s:SENT]->email<-[r:RECEIVED]-personB 
WHERE personA <> personB 
RETURN personA, personB, email
==> +---------------------------------------------------------------------------------------------------------+
==> | personA              | personB                  | email                                                 |
==> +---------------------------------------------------------------------------------------------------------+
==> | Node[4]{name:"Mark"} | Node[5]{name:"Jim"}      | Node[1]{name:"Email 1",title:"Some stuff"}            |
==> | Node[4]{name:"Mark"} | Node[5]{name:"Jim"}      | Node[2]{name:"Email 2",title:"Absolutely irrelevant"} |
==> | Node[5]{name:"Jim"}  | Node[4]{name:"Mark"}     | Node[3]{name:"Email 3",title:"Something else"}        |
==> | Node[4]{name:"Mark"} | Node[6]{name:"Alistair"} | Node[1]{name:"Email 1",title:"Some stuff"}            |
==> +---------------------------------------------------------------------------------------------------------+

Во второй половине запроса мы находим обратные отношения, но мы также хотим указать, не отправил ли один человек какие-либо электронные письма другому (например, Алистер Марку), поэтому я ввел необязательные отношения «ОТПРАВЛЕНО». Если мы запустим это, мы получим следующее:

==> +-------------------------------------------+
==> | personA.name | personB.name | AToB | BToA |
==> +-------------------------------------------+
==> | "Jim"        | "Mark"       | 1    | 2    |
==> | "Mark"       | "Alistair"   | 1    | 0    |
==> | "Mark"       | "Jim"        | 2    | 1    |
==> +-------------------------------------------+

Немного лучше, поскольку теперь мы получаем электронные письма, отправленные парами людей в одной строке, но у нас все еще есть дубликаты, например, Jim / Mark и Mark / Jim отображаются в виде отдельных строк, даже если они показывают одну и ту же информацию.

Мы также не увидим строку, если Лицо А не отправило электронное письмо Персоне Б, даже если Лицо Б отправило Лицу А.

На этом этапе я немного застрял, но  Макс  указал, что мы можем использовать  алгоритм всех кратчайших путей  для решения проблемы.

Мы начали со следующего запроса:

START personA = node:node_auto_index('name:"Mark" name:"Jim" name:"Alistair"'), 
      personB = node:node_auto_index('name:"Mark" name: "Jim" name: "Alistair"') 
WITH personA,personB 
MATCH p = AllShortestPaths(personA-[:SENT|RECEIVED*..2]-personB)
RETURN p

который возвращает:

==> +------------------------------------------------------------------------------------------------------------------------------+
==> | p                                                                                                                            |
==> +------------------------------------------------------------------------------------------------------------------------------+
==> | [Node[4]{name:"Mark"}]                                                                                                       |
==> | [Node[4]{name:"Mark"},:SENT[0] {},Node[1]{name:"Email 1",title:"Some stuff"},:RECEIVED[1] {},Node[5]{name:"Jim"}]            |
==> | [Node[4]{name:"Mark"},:SENT[3] {},Node[2]{name:"Email 2",title:"Absolutely irrelevant"},:RECEIVED[4] {},Node[5]{name:"Jim"}] |
==> | [Node[4]{name:"Mark"},:RECEIVED[6] {},Node[3]{name:"Email 3",title:"Something else"},:SENT[5] {},Node[5]{name:"Jim"}]        |
==> | [Node[4]{name:"Mark"},:SENT[0] {},Node[1]{name:"Email 1",title:"Some stuff"},:RECEIVED[2] {},Node[6]{name:"Alistair"}]       |
==> | [Node[5]{name:"Jim"},:RECEIVED[1] {},Node[1]{name:"Email 1",title:"Some stuff"},:SENT[0] {},Node[4]{name:"Mark"}]            |
==> | [Node[5]{name:"Jim"},:RECEIVED[4] {},Node[2]{name:"Email 2",title:"Absolutely irrelevant"},:SENT[3] {},Node[4]{name:"Mark"}] |
==> | [Node[5]{name:"Jim"},:SENT[5] {},Node[3]{name:"Email 3",title:"Something else"},:RECEIVED[6] {},Node[4]{name:"Mark"}]        |
==> | [Node[5]{name:"Jim"}]                                                                                                        |
==> | [Node[5]{name:"Jim"},:RECEIVED[1] {},Node[1]{name:"Email 1",title:"Some stuff"},:RECEIVED[2] {},Node[6]{name:"Alistair"}]    |
==> | [Node[6]{name:"Alistair"},:RECEIVED[2] {},Node[1]{name:"Email 1",title:"Some stuff"},:SENT[0] {},Node[4]{name:"Mark"}]       |
==> | [Node[6]{name:"Alistair"},:RECEIVED[2] {},Node[1]{name:"Email 1",title:"Some stuff"},:RECEIVED[1] {},Node[5]{name:"Jim"}]    |
==> | [Node[6]{name:"Alistair"}]                                                                                                   |
==> +------------------------------------------------------------------------------------------------------------------------------+

Поскольку нас действительно интересуют только те электронные письма, которые люди посылают друг другу, мы сузим набор результатов, включив в него только эти пути:

START personA = node:node_auto_index('name:"Mark" name:"Jim" name:"Alistair"'), 
      personB = node:node_auto_index('name:"Mark" name: "Jim" name: "Alistair"') 
WITH personA,personB 
MATCH p = AllShortestPaths(personA-[:SENT|RECEIVED*..2]-personB) 
WHERE ANY (x IN relationships(p) WHERE TYPE(x)= 'SENT') 
RETURN p
==> +------------------------------------------------------------------------------------------------------------------------------+
==> | p                                                                                                                            |
==> +------------------------------------------------------------------------------------------------------------------------------+
==> | [Node[4]{name:"Mark"},:SENT[0] {},Node[1]{name:"Email 1",title:"Some stuff"},:RECEIVED[1] {},Node[5]{name:"Jim"}]            |
==> | [Node[4]{name:"Mark"},:SENT[3] {},Node[2]{name:"Email 2",title:"Absolutely irrelevant"},:RECEIVED[4] {},Node[5]{name:"Jim"}] |
==> | [Node[4]{name:"Mark"},:RECEIVED[6] {},Node[3]{name:"Email 3",title:"Something else"},:SENT[5] {},Node[5]{name:"Jim"}]        |
==> | [Node[4]{name:"Mark"},:SENT[0] {},Node[1]{name:"Email 1",title:"Some stuff"},:RECEIVED[2] {},Node[6]{name:"Alistair"}]       |
==> | [Node[5]{name:"Jim"},:RECEIVED[1] {},Node[1]{name:"Email 1",title:"Some stuff"},:SENT[0] {},Node[4]{name:"Mark"}]            |
==> | [Node[5]{name:"Jim"},:RECEIVED[4] {},Node[2]{name:"Email 2",title:"Absolutely irrelevant"},:SENT[3] {},Node[4]{name:"Mark"}] |
==> | [Node[5]{name:"Jim"},:SENT[5] {},Node[3]{name:"Email 3",title:"Something else"},:RECEIVED[6] {},Node[4]{name:"Mark"}]        |
==> | [Node[6]{name:"Alistair"},:RECEIVED[2] {},Node[1]{name:"Email 1",title:"Some stuff"},:SENT[0] {},Node[4]{name:"Mark"}]       |
==> +------------------------------------------------------------------------------------------------------------------------------+

В настоящее время есть некоторое дублирование, потому что у нас есть пути, идущие в обоих направлениях между людьми. например

[Node[4]{name:"Mark"},:SENT[0] {},Node[1]{name:"Email 1",title:"Some stuff"},:RECEIVED[1] {},Node[5]{name:"Jim"}]

и:

[Node[5]{name:"Jim"},:RECEIVED[1] {},Node[1]{name:"Email 1",title:"Some stuff"},:SENT[0] {},Node[4]{name:"Mark"}]

ссылаются на то же самое.

Мы отфильтруем, сохранив начальный узел с более высоким идентификатором узла — мы можем использовать любое свойство для этого сравнения, но идентификатор будет делать:

START personA = node:node_auto_index('name:"Mark" name:"Jim" name:"Alistair"'), 
      personB = node:node_auto_index('name:"Mark" name: "Jim" name: "Alistair"') 
WITH personA,personB 
MATCH p = AllShortestPaths(personA-[:SENT|RECEIVED*..2]-personB) 
WHERE ANY (x IN relationships(p) WHERE TYPE(x)= 'SENT') 
AND ID(head(nodes(p))) > ID(head((tail(tail(nodes(p)))))) 
RETURN p
==> +------------------------------------------------------------------------------------------------------------------------------+
==> | p                                                                                                                            |
==> +------------------------------------------------------------------------------------------------------------------------------+
==> | [Node[5]{name:"Jim"},:RECEIVED[1] {},Node[1]{name:"Email 1",title:"Some stuff"},:SENT[0] {},Node[4]{name:"Mark"}]            |
==> | [Node[5]{name:"Jim"},:RECEIVED[4] {},Node[2]{name:"Email 2",title:"Absolutely irrelevant"},:SENT[3] {},Node[4]{name:"Mark"}] |
==> | [Node[5]{name:"Jim"},:SENT[5] {},Node[3]{name:"Email 3",title:"Something else"},:RECEIVED[6] {},Node[4]{name:"Mark"}]        |
==> | [Node[6]{name:"Alistair"},:RECEIVED[2] {},Node[1]{name:"Email 1",title:"Some stuff"},:SENT[0] {},Node[4]{name:"Mark"}]       |
==> +------------------------------------------------------------------------------------------------------------------------------+

Теперь нам просто нужно немного больше манипулировать этими путями, и у нас есть именно то, что нам нужно:

START personA = node:node_auto_index('name:"Mark" name:"Jim" name:"Alistair"'), 
      personB = node:node_auto_index('name:"Mark" name: "Jim" name: "Alistair"') 
WITH personA,personB 
MATCH p = AllShortestPaths(personA-[:SENT|RECEIVED*..2]-personB) 
WHERE ANY (x IN relationships(p) WHERE TYPE(x)= 'SENT') 
AND ID(head(nodes(p))) > ID(head((tail(tail(nodes(p)))))) 
RETURN HEAD(NODES(p)) AS personA, 
       HEAD((TAIL(TAIL(NODES(p))))) AS personB, 
       LENGTH(FILTER(y IN COLLECT(HEAD(RELS(p))): TYPE(y)= 'SENT')) as AToB,
       LENGTH(FILTER(y IN COLLECT(HEAD(TAIL(RELS(p)))): TYPE(y)= 'SENT')) as BToA

То, что мы здесь сделали, — это подсчитали количество «ОТПРАВЛЕННЫХ» отношений со стороны пути человека А, а затем проделали то же самое для стороны пути человека В.

В настоящее время нет способа сделать нарезку коллекции в cypher, иначе нам не понадобятся эти вложенные вызовы TAIL!

Однако мы получаем желаемый результат:

==> +---------------------------------------------------------------+
==> | personA                  | personB              | AToB | BToA |
==> +---------------------------------------------------------------+
==> | Node[6]{name:"Alistair"} | Node[4]{name:"Mark"} | 0    | 1    |
==> | Node[5]{name:"Jim"}      | Node[4]{name:"Mark"} | 1    | 2    |
==> +---------------------------------------------------------------+

Я понятия не имею, насколько хорошо этот запрос будет работать для любого значительно большего размера набора данных, но, тем не менее, это интересный запрос.