Статьи

Neo4j: факты как узлы

Недавно я говорил в лондонской группе пользователей Neo4j о постепенном создании механизма рекомендаций и описал шаблон моделирования «факты как узлы», определенный в книге «Базы данных графиков» следующим образом :

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

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

Мы начали со следующей модели, описывающей участника встречи и группы, к которым он присоединился:

2015 12 04 07 26 11

Эта модель хорошо работает для запроса, для которого она была определена — найдите группы, похожие на те, в которых я уже состою

MATCH (member:Member {name: "Mark Needham"})-[:MEMBER_OF]->(group)-[:HAS_TOPIC]->(topic)
WITH member, topic, COUNT(*) AS score
MATCH (topic)<-[:HAS_TOPIC]-(otherGroup) 
WHERE NOT (member)-[:MEMBER_OF]->(otherGroup)
RETURN otherGroup.name, COLLECT(topic.name), SUM(score) as score
ORDER BY score DESC

Префикс этого запроса с ключевым словом «PROFILE» дает план запроса и следующий сводный текст:

Cypher version: CYPHER 2.3, planner: COST. 89100 total db hits in 113 ms.

В этой модели создается впечатление, что существует факт членства , ожидающий стать узлом.

2015 12 04 07 35 38

Мы можем реорганизовать эту модель с помощью следующего запроса:

MATCH (member:Member)-[rel:MEMBER_OF]->(group)

MERGE (membership:Membership {id: member.id + "_" + group.id})
SET membership.joined = rel.joined

MERGE (member)-[:HAS_MEMBERSHIP]->(membership)
MERGE (membership)-[:OF_GROUP]->(group);

Мы ответили бы на наш начальный вопрос следующим запросом:

MATCH (member:Member {name: "Mark Needham"})-[:HAS_MEMBERSHIP]->()-[:OF_GROUP]->(group:Group)-[:HAS_TOPIC]->(topic)
WITH member, topic, COUNT(*) AS score
MATCH (topic)<-[:HAS_TOPIC]-(otherGroup) 
WHERE NOT (member)-[:HAS_MEMBERSHIP]->(:Membership)-[:OF_GROUP]->(otherGroup:Group)
RETURN otherGroup.name, COLLECT(topic.name), SUM(score) as score
ORDER BY score DESC

И по следующей цене:

Cypher version: CYPHER 2.3, planner: COST. 468201 total db hits in 346 ms.

Узел членства еще не доказал свою ценность; это делает в 4 раза больше работы, чтобы получить тот же результат. Однако следующий вопрос, на который мы хотим ответить: «К какой группе присоединяются люди после группы пользователей Neo4j?» где это может пригодиться.

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

MATCH (member:Member)-[:HAS_MEMBERSHIP]->(membership)

WITH member, membership ORDER BY member.id, membership.joined

WITH member, COLLECT(membership) AS memberships
UNWIND RANGE(0,SIZE(memberships) - 2) as idx

WITH memberships[idx] AS m1, memberships[idx+1] AS m2
MERGE (m1)-[:NEXT]->(m2);

И теперь для запроса:

MATCH (group:Group {name: "Neo4j - London User Group"})<-[:OF_GROUP]-(membership)-[:NEXT]->(nextMembership),         
      (membership)<-[:HAS_MEMBERSHIP]-(member:Member)-[:HAS_MEMBERSHIP]->(nextMembership),
      (nextMembership)-[:OF_GROUP]->(nextGroup)
RETURN nextGroup.name, COUNT(*) AS times
ORDER BY times DESC
Cypher version: CYPHER 2.3, planner: COST. 23671 total db hits in 39 ms.

А для сравнения — тот же запрос с использованием исходной модели:

MATCH (group:Group {name: "Neo4j - London User Group"})<-[membership:MEMBER_OF]-(member),
      (member)-[otherMembership:MEMBER_OF]->(otherGroup)
WHERE membership.joined < otherMembership.joined
WITH member, otherGroup 
ORDER BY otherMembership.joined
WITH member, COLLECT(otherGroup)[0] AS nextGroup
RETURN nextGroup.name, COUNT(*) AS times
ORDER BY times DESC
Cypher version: CYPHER 2.3, planner: COST. 86179 total db hits in 138 ms.

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

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

Добавление членства в группе

WITH "Mark Needham" AS memberName, 
     "Neo4j - London User Group" AS groupName,
     timestamp() AS now

MATCH (group:Group {name: groupName})
MATCH (member:Member {name: memberName})

MERGE (member)-[memberOfRel:MEMBER_OF]->(group)
ON CREATE SET memberOfRel.time = now

MERGE (membership:Membership {id: member.id + "_" + group.id})
ON CREATE SET membership.joined = now
MERGE (member)-[:HAS_MEMBERSHIP]->(membership)
MERGE (membership)-[:OF_GROUP]->(group)

Удаление членства в группе

WITH "Mark Needham" AS memberName, 
     "Neo4j - London User Group" AS groupName,
     timestamp() AS now

MATCH (group:Group {name: groupName})
MATCH (member:Member {name: memberName})

MATCH (member)-[memberOfRel:MEMBER_OF]->(group)

MATCH (membership:Membership {id: member.id + "_" + group.id})
MATCH (member)-[hasMembershipRel:HAS_MEMBERSHIP]->(membership)
MATCH (membership)-[ofGroupRel:OF_GROUP]->(group)

DELETE memberOfRel, hasMembershipRel, ofGroupRel

Набор данных находится на github, так что взгляните на него и отправьте мне любые вопросы.