Статьи

Отношения в одном направлении в Neo4j

onedirectionchop

В модели графа свойств 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%. Допустим, вы создаете мобильное приложение для покупок, в котором ваши пользователи могут «Провести пальцем вправо», если им нравится элемент, «Повернуть влево», если им не нравится товар, и «Двойное касание», чтобы добавить его в корзину покупок.

Трут-Kwoller

Мы могли бы смоделировать это двумя способами в Neo4j. Первый способ — стандартный. Каждое действие пользователя приводит к Отношениям для LIKES, DISLIKES или PURCHASES. С точки зрения пользователя, вы хотите знать, какие элементы вам не нравятся, чтобы их можно было исключить из рекомендаций или поисковых запросов. Однако, с точки зрения Предмета, вам когда-нибудь нужно было знать, каким пользователям это не понравилось? Давайте попробуем это с точки зрения приложения для знакомств. Какую функцию оценят ваши пользователи, которая показывает им людей, которые их не любят? Быстро, кто-нибудь возьмется за данные Tinder и создаст приложение под названием «Asholes», которое покажет вам всех людей, которые смахнули налево!

Зола-Heart-01

Да, это была бы ужасная идея. Итак, когда Neo4j создает новое отношение, он использует 34 байта. Ему нужно все это, потому что он имеет указатели на начальный узел и конечный узел, а также на первое свойство отношения и остальную часть цепочки отношений. Так что, если вы ожидаете массу таких отношений, что вы можете сделать?

8bitroar

Вы можете получить помощь от Даниэля Лемира и его Ревущей растровой библиотеки. То, что мы собираемся сделать, это не создавать отношения 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 мне очень нравится то, что у вас есть полная свобода делать все, что вы хотите. Дни обращения с базой данных, как с тупым хранилищем, прошли, так что проявите творческий подход и сходите с ума.