Статьи

Neo4j: конкретные отношения против родовых отношений + свойство

Для оптимальной скорости прохождения в запросах Neo4j мы должны сделать наши типы отношений как можно более конкретными .

Давайте посмотрим на пример из доклада « Моделирование механизма рекомендаций », который я представил на Skillsmatter пару недель назад.

Мне нужно было решить, как смоделировать отношения «RSVP» между Участником и Событием . Человек может ответить «да» или «нет» на событие, и я хотел бы запечатлеть оба эти ответа.

т.е. мы можем выбирать между:

2015-12-13_20-39-05

и:

2015-12-13_20-39-54

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

Давайте посмотрим на каждый из них по очереди:

Какие запросы мы хотим написать?

Первый запрос собирался использовать предыдущие RSVP «да» в качестве индикатора интереса для будущих событий. Мы не заинтересованы в «нет» RSVP для этого запроса.

Я начал с общего типа отношений RSVP со свойством «response», чтобы различать «yes» и «no»:

1
2
3
4
5
6
7
8
9
MATCH (member:Member {name: "Mark Needham"})
MATCH (futureEvent:Event) WHERE futureEvent.time >= timestamp()
MATCH (futureEvent)<-[:HOSTED_EVENT]-(group)
  
OPTIONAL MATCH (member)-[rsvp:RSVPD {response: "yes"}]->(pastEvent)<-[:HOSTED_EVENT]-(group)
WHERE pastEvent.time < timestamp()
  
RETURN group.name, futureEvent.name, COUNT(rsvp) AS previousEvents
ORDER BY  previousEvents DESC

Это выполнялось достаточно быстро, но мне было любопытно, смогу ли я заставить запрос работать быстрее, переключившись на более конкретную модель. Используя более конкретный тип отношений, наш запрос гласит:

01
02
03
04
05
06
07
08
09
10
11
MATCH (member:Member {name: "Mark Needham"})
MATCH (futureEvent:Event) WHERE futureEvent.time >= timestamp()
MATCH (futureEvent)<-[:HOSTED_EVENT]-(group)
  
OPTIONAL MATCH (member)-[rsvp:RSVP_YES]->(pastEvent)<-[:HOSTED_EVENT]-(group)
WHERE pastEvent.time < timestamp()
  
RETURN group.name,
       futureEvent.name,
       COUNT(rsvp) AS previousEvents
ORDER BY  previousEvents DESC

Теперь мы можем профилировать наш запрос и сравнить попадания в БД обоих решений:

1
2
3
4
5
RSVPD {response: "yes"}
Cypher version: CYPHER 2.3, planner: COST. 688635 total db hits in 232 ms.
  
RSVP_YES
Cypher version: CYPHER 2.3, planner: COST. 559866 total db hits in 207 ms.

Таким образом, мы получаем небольшой выигрыш, используя более конкретный тип отношений. Причина, по которой попадания в БД ниже, отчасти объясняется тем, что мы устранили необходимость искать свойство ‘response’ в каждом свойстве ‘RSVP’ и проверять, соответствует ли оно ‘yes’. Мы также оцениваем меньшее количество отношений, так как мы смотрим только на положительные RSVP, отрицательные игнорируются.

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

1
2
3
4
MATCH (member:Member {name: "Mark Needham"})-[rsvp:RSVPD]->(event)
WHERE event.time < timestamp()
RETURN event.name, event.time, rsvp.response
ORDER BY event.time DESC
1
2
3
4
MATCH (member:Member {name: "Mark Needham"})-[rsvp:RSVP_YES|:RSVP_NO]->(event)
WHERE event.time < timestamp()
RETURN event.name, event.time, CASE TYPE(rsvp) WHEN "RSVP_YES" THEN "yes" ELSE "no" END AS response
ORDER BY event.time DESC

Снова мы видим, что маргинальные удары по дб выигрывают для более конкретного типа отношений:

1
2
3
4
5
RSVPD {response: "yes"} / RSVPD {response: "no"}
Cypher version: CYPHER 2.3, planner: COST. 684 total db hits in 37 ms.
  
RSVP_YES / RSVP_NO
Cypher version: CYPHER 2.3, planner: COST. 541 total db hits in 24 ms.

Тем не менее, запрос довольно громоздкий, и если мы не сохраним ответ как свойство отношения, код для возврата «да» или «нет» будет немного неудобным. С более конкретным подходом запрос станет еще более болезненным, если мы введем RSVP «лист ожидания», который мы решили исключить.

Нужно ли нам обновить отношения?

Да! Пользователи могут изменять свои RSVP до тех пор, пока событие не произойдет, поэтому мы должны быть в состоянии справиться с этим.

Давайте посмотрим на запросы, которые нам нужно написать, чтобы обработать изменение в RSVP с использованием обеих моделей:

Общий тип отношений

1
2
3
4
5
6
MATCH (event:Event {id: {event_id}})
MATCH (member:Member {id: {member_id}})
MERGE (member)-[rsvpRel:RSVPD {id: {rsvp_id}}]->(event)
ON CREATE SET rsvpRel.created = toint({mtime})
ON MATCH  SET rsvpRel.lastModified = toint({mtime})
SET rsvpRel.response = {response}

Конкретный тип отношений

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
MATCH (event:Event {id: {event_id}})
MATCH (member:Member {id: {member_id}})
  
FOREACH(ignoreMe IN CASE WHEN {response} = "yes" THEN [1] ELSE [] END |
  MERGE (member)-[rsvpYes:RSVP_YES {id: {rsvp_id}}]->(event)
  ON CREATE SET rsvpYes.created = toint({mtime})
  ON MATCH  SET rsvpYes.lastModified = toint({mtime})
  
  MERGE (member)-[oldRSVP:RSVP_NO]->(event)
  DELETE oldRSVP
)
  
FOREACH(ignoreMe IN CASE WHEN {response} = "no" THEN [1] ELSE [] END |
  MERGE (member)-[rsvpNo:RSVP_NO {id: {rsvp_id}}]->(event)
  ON CREATE SET rsvpNo.created = toint({mtime})
  ON MATCH  SET rsvpNo.lastModified = toint({mtime})
  
  MERGE (member)-[oldRSVP:RSVP_YES]->(event)
  DELETE oldRSVP
)

Как вы можете видеть, код для обновления RSVP более сложен при использовании определенного типа отношения из-за того, что у Cypher еще нет поддержки первого класса для условных выражений.

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

В зависимости от кардинальности отношений в вашей модели ваш пробег может варьироваться, но стоит провести некоторое профилирование, чтобы сравнить все ваши варианты.