Arhn - архитектура программирования

Neo4j как смоделировать график с временной версией

Часть моего графика имеет следующую схему:

введите описание изображения здесь

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

В моем случае человек может быть администратором, если к нему привязаны какие-то устройства / календари. Я получаю эти данные из базы данных SQL, куда я импортирую несколько таблиц, чтобы объединить всю картину. Я начинаю с таблицы, в которой есть два столбца: адрес электронной почты администратора и его идентификатор пользователя. Этот идентификатор пользователя специфичен только для производственной базы данных и не используется глобально для других источников. Вот почему я использую электронную почту как глобальный идентификатор для людей. В настоящее время я использую следующий запрос для импорта идентификатора пользователя, с которым связаны все производственные таблицы. Я всегда получаю текущий снимок пользовательских настроек и информации. Этот запрос выполняется 4 раза в день:

CALL apoc.load.jdbc(url, import_query) yield row
MERGE (p:Person{email:row.email})
SET p.user_id = row.id

Затем я импортирую все данные, связанные с этим идентификатором пользователя, из других таблиц.

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

Как изменить мою модель графа (запрос импорта), чтобы:

  1. Чтобы узнать, кто в настоящее время является администратором, не потребуется сложных запросов.
  2. Чтобы узнать, у кого в данный момент подключено устройство, не потребуется сложных запросов.
  3. Запрос истории может быть немного сложнее.
14.08.2017

  • Исходя из вашей репутации, я предполагаю, что вы уже рассмотрели пару решений. Так что простите меня, если я покажусь глупым. Загадка идентификатора пользователя и электронной почты - непростая задача. Если ни один из них не может использоваться в качестве уникального идентификатора во времени, я бы выбрал что-нибудь (еще), что подходит. Что касается управления версиями ... часто используемое решение (хотя и не очень) - поместить версию в тип отношения. Так, например, отношения CONNECTED_DEVICE_20170814 скажут вам, что подключено сегодня. Часто текущее отношение задается типом без версии (здесь CONNECTED_DEVICE). 14.08.2017
  • Хорошо глядя на это, было бы лучше отделить узел пользователя от узла человека, чтобы мы получили (: Person {email}) - [: ADMIN |: WAS_ADMIN] - (: AdminNode {user_id}). Один из вариантов - просто сохранить текущее состояние как отдельный тип rel ADMIN или CONNECTED_DEVICE и историю как WAS_ADMIN. И у вас есть задание cron, которое изменяет тип rel с: ADMIN на: WAS_ADMIN на основе последней временной метки отношения. Проверяю, есть ли версии получше :) 14.08.2017
  • Если идентификатор пользователя является первичным ключом пользователей, вы должны использовать его при слиянии вместо электронной почты. Это нормально, он не уникален в глобальном масштабе, но может гарантировать, что это тот же пользователь из пользовательской таблицы. 17.08.2017

Ответы:


1

Этот ответ основан на сообщении Яна Робинсона о основанных на времени версионных графах .

Я не знаю, охватывает ли этот ответ ВСЕ требования, связанные с вопросом, но я считаю, что это может дать некоторую информацию.

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

Начальное состояние графа:

Рассмотрим этот Cypher для создания начального состояния графа:

CREATE (admin:Admin)

CREATE (person1:Person {person_id : 1})
CREATE (person2:Person {person_id : 2})
CREATE (person3:Person {person_id : 3})

CREATE (domain1:Domain {domain_id : 1})

CREATE (device1:Device {device_id : 1})

CREATE (person1)-[:ADMIN {from : 0, to : 1000}]->(admin)

CREATE (person1)-[:CONNECTED_DEVICE {from : 0, to : 1000}]->(device1)

CREATE (domain1)-[:MEMBER]->(person1)
CREATE (domain1)-[:MEMBER]->(person2)
CREATE (domain1)-[:MEMBER]->(person3)

Результат:

Исходное состояние графика

На приведенном выше графике есть узлы из 3 человек. Эти узлы являются членами узла домена. Узел пользователя с person_id = 1 подключен к устройству с device_id = 1. Кроме того, person_id = 1 является текущим администратором. Свойства from и to внутри отношений :ADMIN и :CONNECTED_DEVICE используются для управления историей структуры графа. from представляет начальную точку во времени, а to - конечную точку во времени. Для упрощения я использую 0 как начальное время графика и 1000 как константу конца времени. На реальном графике текущее время в миллисекундах может использоваться для представления временных точек. Кроме того, вместо константы EOT можно использовать Long.MAX_VALUE. Связь с to = 1000 означает, что нет текущей верхней границы связанного с ним периода.

Запросы:

С помощью этого графика, чтобы получить текущего администратора, я могу:

MATCH (person:Person)-[:ADMIN {to:1000}]->(:Admin)
RETURN person

В результате получится:

╒═══════════════╕
│"person"       │
╞═══════════════╡
│{"person_id":1}│
└───────────────┘

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

MATCH (:Device {device_id : 1})<-[:CONNECTED_DEVICE {to : 1000}]-(person:Person)
RETURN person

Результат:

╒═══════════════╕
│"person"       │
╞═══════════════╡
│{"person_id":1}│
└───────────────┘

Для запроса текущего администратора и текущего человека, подключенного к устройству, используется константа конца времени.

Запросить события подключения / отключения устройства:

MATCH (device:Device {device_id : 1})<-[r:CONNECTED_DEVICE]-(person:Person)
RETURN person AS person, device AS device, r.from AS from, r.to AS to
ORDER BY r.from

Результат:

╒═══════════════╤═══════════════╤══════╤════╕
│"person"       │"device"       │"from"│"to"│
╞═══════════════╪═══════════════╪══════╪════╡
│{"person_id":1}│{"device_id":1}│0     │1000│
└───────────────┴───────────────┴──────┴────┘

Приведенный выше результат показывает, что person_id = 1 подключен к device_id = 1 от начала до сегодняшнего дня.

Изменение структуры графа

Учтите, что текущий момент времени - 30. Сейчас user_id = 1 отключается от device_id = 1. user_id = 2 подключится к нему. Чтобы представить это структурное изменение, я выполню следующий запрос:

// Get the current connected person
MATCH (person1:Person)-[old:CONNECTED_DEVICE {to : 1000}]->(device:Device {device_id : 1})
// get person_id = 2
MATCH (person2:Person {person_id : 2}) 
 // set 30 as the end time of the connection between person_id = 1 and device_id = 1
SET old.to = 30
// set person_id = 2 as the current connected user to device_id = 1
// (from time point 31 to now)
CREATE (person2)-[:CONNECTED_DEVICE {from : 31, to: 1000}]->(device) 

Результирующий график будет:

«График

После этого структурного изменения история подключений device_id = 1 будет:

MATCH (device:Device {device_id : 1})<-[r:CONNECTED_DEVICE]-(person:Person)
RETURN person AS person, device AS device, r.from AS from, r.to AS to
ORDER BY r.from

╒═══════════════╤═══════════════╤══════╤════╕
│"person"       │"device"       │"from"│"to"│
╞═══════════════╪═══════════════╪══════╪════╡
│{"person_id":1}│{"device_id":1}│0     │30  │
├───────────────┼───────────────┼──────┼────┤
│{"person_id":2}│{"device_id":1}│31    │1000│
└───────────────┴───────────────┴──────┴────┘

Приведенный выше результат показывает, что user_id = 1 был подключен к device_id = 1 от 0 до 30 раз. person_id = 2 в настоящее время подключен к device_id = 1.

Сейчас к device_id = 1 подключен человек person_id = 2:

MATCH (:Device {device_id : 1})<-[:CONNECTED_DEVICE {to : 1000}]-(person:Person)
RETURN person

╒═══════════════╕
│"person"       │
╞═══════════════╡
│{"person_id":2}│
└───────────────┘

Тот же подход можно применить для управления историей администратора.

Очевидно, у этого подхода есть свои недостатки:

  • Необходимо управлять набором дополнительных отношений
  • Более дорогие запросы
  • Более сложные запросы

Но если вам действительно нужна схема управления версиями, я считаю, что этот подход - хороший вариант или (по крайней мере) хорошая отправная точка.

22.08.2017

2

Разрешение GUID

Первое, что вам нужно, - это надежно разрешить идентификаторы пользователей, чтобы они были согласованными и глобально уникальными. Теперь ты сказал

идентификатор пользователя специфичен только для производственной базы данных и не используется глобально для других источников

Из этого я могу вывести 2 вещи

  1. Пользователи существуют из нескольких источников.
  2. Для каждого источника у пользователей есть уникальный идентификатор.

Это означает, что source + user.id будет идентификатором GUID. (Вы можете хэшировать основной URL-адрес подключения или назвать каждый источник извне). Я предполагаю, что вы не объединяете пользователей из нескольких источников, потому что дублирование и объединение данных по любой сети создает парадокс порядка обновления, которого следует избегать в максимально возможной степени (если в двух источниках указаны разные новые контактные номера, кто прав?).

Запрос текущих данных

Логика запросов не должна зависеть от отслеживания версий, которые вы выполняете. Если ваше управление версиями вызывает проблемы с логикой, добавьте мета-метку, например :Versioned с индексированным свойством isLatest, и добавьте Where n.isLatest, чтобы отфильтровать старые "мусорные" данные из ваших результатов.

Так что нет, вам не нужно беспокоиться о версии, запросы 1 и 2 можно обрабатывать в обычном режиме.

  1. Чтобы найти людей, которые являются администраторами, я бы рекомендовал просто добавить метку :Admin к человеку и удалить его, когда он больше не применяется (при необходимости). Это происходит благодаря метке "Admin". Вы также можете просто использовать свойство isAdmin (вероятно, именно так вы уже сохраняете его в базе данных, поэтому более согласованно). Таким образом, окончательный запрос будет просто MATCH (p:Person:Admin) или MATCH (p:Person{isAdmin:true}).

  2. Если отфильтровать информацию о старой версии, запрос о том, у кого есть устройство, будет просто MATCH (p:Person:Versioned{isCurrent:true})-[:HasDevice{isConnected:true}]->(d:Device:Versioned{isCurrent:true})

На самом деле этот бит сводится к следующему: «Какая у вас схема?»

История данных

Здесь действительно все становится сложно. В зависимости от того, как вы используете версию данных, вы можете легко увеличить размер данных и снизить производительность БД. Вам ДЕЙСТВИТЕЛЬНО нужно спросить себя: «Почему я использую это обновление?», «Как часто это обновление / будет прочитано?», «Кто будет его использовать и что они будут с ним делать?». Если в какой-то момент вы ответите «Я не знаю / не волнуюсь», вы либо не должны этого делать, либо сделайте резервную копию своих данных в базе данных, которая изначально обрабатывает это за вас, например SQLAlchemy-Continuum. (Связанный ответ)

Если вам необходимо сделать это в Neo4j, я бы рекомендовал использовать дельта-цепочку. Так, если, например, вы изменили {a:1, b:2} на {a:1, b:null, c:3}, у вас будет (:Thing{a:1, b:null, c:3})-[_DELTA{timestamp:<value>}]->(:_ThingDelta{b: 2, c:null}). Таким образом, чтобы получить прошлое значение, вы просто добавляете свойства дельта-цепочки в карту. Итак MATCH (a:Thing) OPTIONAL MATCH (a)-[d:_DELTA*]->(d) WHERE d.timestamp >= <value> WITH reduce(v = {_id:ID(a)}, n IN nodes(p)| v += PROPERTIES(n)) AS OldVersion. Это может оказаться очень утомительным и съесть ваше пространство БД, поэтому я настоятельно рекомендую любой ценой использовать существующую систему управления версиями БД, если сможете.

18.08.2017
Новые материалы

Коллекции публикаций по глубокому обучению
Последние пару месяцев я создавал коллекции последних академических публикаций по различным подполям глубокого обучения в моем блоге https://amundtveit.com - эта публикация дает обзор 25..

Представляем: Pepita
Фреймворк JavaScript с открытым исходным кодом Я знаю, что недостатка в фреймворках JavaScript нет. Но я просто не мог остановиться. Я хотел написать что-то сам, со своими собственными..

Советы по коду Laravel #2
1-) Найти // You can specify the columns you need // in when you use the find method on a model User::find(‘id’, [‘email’,’name’]); // You can increment or decrement // a field in..

Работа с временными рядами спутниковых изображений, часть 3 (аналитика данных)
Анализ временных рядов спутниковых изображений для данных наблюдений за большой Землей (arXiv) Автор: Рольф Симоэс , Жильберто Камара , Жильберто Кейрос , Фелипе Соуза , Педро Р. Андраде ,..

3 способа решить квадратное уравнение (3-й мой любимый) -
1. Методом факторизации — 2. Используя квадратичную формулу — 3. Заполнив квадрат — Давайте поймем это, решив это простое уравнение: Мы пытаемся сделать LHS,..

Создание VR-миров с A-Frame
Виртуальная реальность (и дополненная реальность) стали главными модными терминами в образовательных технологиях. С недорогими VR-гарнитурами, такими как Google Cardboard , и использованием..

Демистификация рекурсии
КОДЕКС Демистификация рекурсии Упрощенная концепция ошеломляющей О чем весь этот шум? Рекурсия, кажется, единственная тема, от которой у каждого начинающего студента-информатика..