В модели графа свойств Neo4j каждое отношение должно быть типизировано и направлено Это означает, что они должны иметь определенное имя (ДРУЗЬЯ, ЛАЙКИ, СЛЕДУЮЩИЕ и т. Д.) И иметь Начальный узел и Конечный узел для указания направления. Важно то, что когда вы пишете свои запросы, вы можете игнорировать это. Следующие запросы действительны:
// Get all the people I follow MATCH (u1:Person)-[:FOLLOWS]->(u2:Person) WHERE u1.username = "maxdemarzi" RETURN u2.username // Get all the people that I follow or follow me MATCH (u1:Person)-[:FOLLOWS]-(u2:Person) WHERE u1.username = "maxdemarzi" RETURN u2.username // Get all the people related to me MATCH (u1:Person)--(u2:Person) WHERE u1.username = "maxdemarzi" RETURN u2.username
Допустим, мы создаем новое отношение FOLLOWS от узла 1 к узлу 2. Neo4j обновит оба затронутых узла, добавив запись в список FOLLOWS-Outgoing для узла 1 и запись в список FOLLOWS-Incoming для узла 2. Это очень мощная функция, потому что это означает, что я могу пройти эту связь с любого узла и перейти к другому. Это делается на атомарном уровне , поэтому вы никогда не получите половину записи отношения в базе данных, и она всегда будет поддерживать вашу базу данных непротиворечивой (в отличие от некоторых других баз данных ).
0,9% времени, это то, что мы хотим. Чтобы иметь возможность пересечь отношения в любом направлении. Итак, давайте поговорим о 0,1%. Допустим, вы создаете мобильное приложение для покупок, в котором ваши пользователи могут «Провести пальцем вправо», если им нравится элемент, «Повернуть влево», если им не нравится товар, и «Двойное касание», чтобы добавить его в корзину покупок.
Мы могли бы смоделировать это двумя способами в Neo4j. Первый способ — стандартный. Каждое действие пользователя приводит к Отношениям для LIKES, DISLIKES или PURCHASES. С точки зрения пользователя, вы хотите знать, какие элементы вам не нравятся, чтобы их можно было исключить из рекомендаций или поисковых запросов. Однако, с точки зрения Предмета, вам когда-нибудь нужно было знать, каким пользователям это не понравилось? Давайте попробуем это с точки зрения приложения для знакомств. Какую функцию оценят ваши пользователи, которая показывает им людей, которые их не любят? Быстро, кто-нибудь возьмется за данные Tinder и создаст приложение под названием «Asholes», которое покажет вам всех людей, которые смахнули налево!
Да, это была бы ужасная идея. Итак, когда Neo4j создает новое отношение, он использует 34 байта. Ему нужно все это, потому что он имеет указатели на начальный узел и конечный узел, а также на первое свойство отношения и остальную часть цепочки отношений. Так что, если вы ожидаете массу таких отношений, что вы можете сделать?
Вы можете получить помощь от Даниэля Лемира и его Ревущей растровой библиотеки. То, что мы собираемся сделать, это не создавать отношения DISLIKES, вместо этого мы собираемся создать сжатое растровое изображение и сохранить идентификаторы узлов элементов, которые пользователю не нравились. Затем мы собираемся сохранить это растровое изображение в виде байтового массива в свойстве пользовательского узла «не нравится» следующим образом:
try (Transaction tx = db.beginTx()) { Node user = db.findNode(Labels.User, "username", "maxdemarzi"); Node thingDisliked = db.findNode(Labels.Thing, "name", "One Direction Boy Band"); int thingDislikedNodeId = ((Number) thingDisliked.getId()).intValue(); RoaringBitmap dislikes = new RoaringBitmap(); dislikes.add(thingDislikedNodeId); ByteArrayOutputStream baos = new ByteArrayOutputStream(); dislikes.serialize(new DataOutputStream(baos)); user.setProperty("dislikes", baos.toByteArray()); tx.success(); }
Отлично, но как нам вернуть идентификаторы узлов? Мы просто делаем это задом наперед и десериализовываем массив байтов обратно в RoaringBitmap.
private static RoaringBitmap getRoaringBitmap(Node user, String property) throws IOException { RoaringBitmap rb = new RoaringBitmap(); byte[] nodeIds = (byte[])user.getProperty(property); ByteArrayInputStream bais = new ByteArrayInputStream(nodeIds); rb.deserialize(new DataInputStream(bais)); return rb; }
Мы бы вызвали этот метод и использовали бы db.getNodeById, чтобы добраться до узлов Item, которые нам не нравятся
Set<Node> dislikedItems = new HashSet<>(); RoaringBitmap dislikes = getRoaringBitmap(user, "dislikes"); for (int nodeId : dislikes.toArray()) { dislikedItems.add(db.getNodeById(nodeId)); }
Итак, давайте использовать это в рекомендации двигателя. Сначала мы найдем все предметы, которые целевой пользователь приобрел. Затем мы будем использовать отношения Incoming PURCHASED и LIKES, чтобы найти 25 лучших пользователей с такими же вкусами, что и наш целевой пользователь. За каждый купленный товар мы дадим 5 баллов любому пользователю, который его купил, и 3 балла любому пользователю, который его любил.
private static HashMap<Node, MutableInt> getOtherUsers(Set<Node> likedItems, Set<Node> purchasedItems, Node user) { HashMap<Node, MutableInt> otherUsers = new HashMap<>(); for (Node item : purchasedItems) { // Give 5 points to every person who purchased an Item I also purchased for (Relationship rel : item.getRelationships(RelationshipTypes.PURCHASED, Direction.INCOMING)) { Node otherUser = rel.getStartNode(); MutableInt mutableInt = otherUsers.get(otherUser); if (mutableInt == null) { otherUsers.put(otherUser, new MutableInt(5)); } else { mutableInt.add(5); } } // Give 3 points to every person who liked an Item I purchased for (Relationship rel : item.getRelationships(RelationshipTypes.LIKES, Direction.INCOMING)) { Node otherUser = rel.getStartNode(); MutableInt mutableInt = otherUsers.get(otherUser); if (mutableInt == null) { otherUsers.put(otherUser, new MutableInt(3)); } else { mutableInt.add(3); } } }
За каждый понравившийся товар мы дадим 2 балла любому пользователю, которому он понравился, и 1 балл любому пользователю, который его купил.
for (Node item : likedItems) { // Give 2 points to every person who liked an Item I also liked for (Relationship rel : item.getRelationships(RelationshipTypes.LIKES, Direction.INCOMING)) { Node otherUser = rel.getStartNode(); MutableInt mutableInt = otherUsers.get(otherUser); if (mutableInt == null) { otherUsers.put(otherUser, new MutableInt(2)); } else { mutableInt.add(2); } } // Give 1 point to every person who purchased an Item I liked for (Relationship rel : item.getRelationships(RelationshipTypes.PURCHASED, Direction.INCOMING)) { Node otherUser = rel.getStartNode(); MutableInt mutableInt = otherUsers.get(otherUser); if (mutableInt == null) { otherUsers.put(otherUser, new MutableInt(1)); } else { mutableInt.increment(); } } } // Remove self from similar users otherUsers.remove(user); return otherUsers; }
Мы возьмем 25 лучших пользователей и затем найдем понравившиеся или купленные им предметы. Мы будем давать 1 очко каждый раз, когда элемент появляется в списке.
private static HashMap<Node, MutableInt> getOtherItems(ArrayList<Node> similarUsers) { HashMap<Node, MutableInt> otherItems = new HashMap<>(); for (Node similarUser : similarUsers) { for (Relationship rel : similarUser.getRelationships(Direction.OUTGOING, RelationshipTypes.PURCHASED, RelationshipTypes.LIKES)) { Node item = rel.getEndNode(); MutableInt mutableInt = otherItems.get(item); if (mutableInt == null) { otherItems.put(item, new MutableInt(1)); } else { mutableInt.increment(); } } } return otherItems; }
Затем мы исключим элементы, которые пользователь уже купил, понравился или не понравился. Помните, что наши неприязни элементы пришли из RoaringBitmap.
for (Node item : purchasedItems) { otherItems.remove(item); } for (Node item : likedItems) { otherItems.remove(item); } for (Node item : dislikedItems) { otherItems.remove(item); }
Наконец, мы рекомендуем 10 лучших из оставшихся. Оформите полный исходный код на github .
Я также написал другой набор тестов JMH, используя обе модели, и производительность довольно близка.
Benchmark Mode Cnt Score Error Units ServiceBenchmark.measureRecommend thrpt 5 339.319 ± 9.334 ops/s ServiceBenchmark.measureRecommend2 thrpt 5 376.071 ± 22.711 ops/s
Итак, насколько экономна память? Вот тест, который показывает размер RoaringBitmap, когда мы добавляем в него 3 элемента и затем еще 1000 идентификаторов. Мы находимся на 18 байтов против 34 с 1 предметом, 20 против 68 с 2 предметами, 30 против 102 с 3 предметами и так далее. Окончательный размер был около 8 Кб, что лучше 34 Кб, но если бы идентификаторы узлов были бы менее распределенными, сжатие было бы больше.
@Test public void shouldbeSmallSize() throws IOException { RoaringBitmap dislikes = new RoaringBitmap(); dislikes.add(1); assertEquals(18, dislikes.serializedSizeInBytes()); dislikes.add(1000); assertEquals(20, dislikes.serializedSizeInBytes()); dislikes.add(100000000); assertEquals(30, dislikes.serializedSizeInBytes()); int maxUsers = 100000000; Random rand = new Random(); for (int i = 0; i < 1000; i++){ int randomItem = rand.nextInt(maxUsers); dislikes.add(randomItem); } int sizeInRelationships = 34 * 1000; assertTrue(dislikes.serializedSizeInBytes() <= sizeInRelationships); }
Односторонние отношения в виде сжатого растрового изображения — немного продвинутый хак. Это произошло потому, что у одного клиента было 1 миллиард односторонних отношений, которые тратили слишком много памяти. Другой клиент хранит фильтр Блума как свойство узла. Они используют его в качестве предварительного фильтра перед прохождением графика. В Neo4j мне очень нравится то, что у вас есть полная свобода делать все, что вы хотите. Дни обращения с базой данных, как с тупым хранилищем, прошли, так что проявите творческий подход и сходите с ума.