Статьи

Работа с супер-узлами и индексированными отношениями в Neo4j, часть 2

В предыдущем посте мы сравнивали производительность выборки отношений из густонаселенных узлов с использованием собственного хранилища Neo4j и индекса lucene. Мы видели, что мы можем извлечь небольшое подмножество отношений из суперузла (содержащего ~ 1M отношений) непосредственно из индекса Lucene, производительность первого запуска (cold-caches) лучше, чем при использовании магазина Neo непосредственно. Последующие прогоны с разогретыми кэшами показывают сопоставимую производительность, слегка в пользу прямой выборки из магазина Neo, что соответствует низкоуровневой оптимизации кэша.

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

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

  • 5 узлов, представляющих города / поселки (например, «Лондон», «Манчестер», «Эдинбург», «Кембридж» и «Оксфорд»)
  • Каждый городской узел имеет отношение «IS_IN» к узлу страны UK (каждый город связан с узлом страны с отношением IS_IN, и у нас есть только один узел страны — Великобритания)
  • У нас есть 1M пользователей, и каждый из них имеет отношение «HAS_VISITED» к каждому из пяти городов. Я знаю, что это маловероятный сценарий, но в большом наборе данных (скажем, среди всех пользователей Facebook) вы найдете более 5 городов, которые посетили более 100 000 пользователей.
  • В итоге мы имеем 1 000 006 узлов (1M пользователей + 5 городов + 1 узел страны в Великобритании) и 5 ​​000 005 отношений (1M пользователей-HAS) _VISITED-5 городов + 5 городов IS_IN UK).

В предыдущей записи блога мы извлекали отношения IS_IN из каждого суперузла города, причем у каждого города было только одно отношение этого типа (1 000 001 или 0,0001%).

В этом примере. мы собираемся загрузить все отношения HAS_VISITED для городского узла, что означает, что мы заинтересованы в загрузке 99,9999% отношений для данного узла. Мы собираемся сравнить использование API Neo4j для обхода отношений и
IndexedRelationshipExapnder, который мы представили в предыдущей записи блога.

Чтобы получить все отношения HAS_VISITED с помощью Neo API, мы будем использовать код, аналогичный описанному выше:


Листинг 1: Использование Neo4j API для загрузки отношений
Iterable<Relationships> relationships = city.getRelationships(DynamicRelationshipType.withName("IS_IN"));
for (Relationship r : relationships) {
   cnt++;
}

Чтобы получить отношения из индекса Lucene, мы будем использовать API обхода Neo с нашим IndexedRelationshipExpander


Листинг 2: Использование IndexedRelationhipExpander (реализовано в предыдущем посте) для загрузки отношений из индекса Lucene
RelationshipType relationshiptype = DynamicRelationshipType.withName("HAS_VISITED");
Direction direction = Direction.INCOMING;
TraversalDescription traversal =
    Traversal.description()
    .evaluator(Evaluators.atDepth(1))
    .expand(
        IndexedRelationhipExpander.create(this.embeddedGraphDatabase, relationshipType, direction)
    );
traversal.traverse(city);

В таблице 1 показано сравнение производительности стандартного расширителя и нашего нового индексированного расширителя при извлечении всех связей типа HAS_VISITED из городского узла.
Помните, что каждый узел города будет иметь отношения 1M HAS_VISITED.

Таблица 1: Сравнение производительности двух подходов путем загрузки всех отношений с супер-узлов
 
 
 

 

 
 

Как видите, первый запуск с использованием стандартного расширителя занимает чуть более 5 секунд, по сравнению с почти 15 секундами с использованием Lucene. Последующие прогоны с прогретыми кешами показывают еще большую вариацию: чуть более 1 секунды при использовании стандартного расширителя и около 8 секунд при использовании индексированного расширителя отношений. Это показывает, что загрузка больших наборов результатов из индекса Lucene значительно медленнее, чем загрузка отношений из хранилища Neo4j напрямую. Это, вероятно, ожидается, поскольку Neo4j не предназначен для использования в качестве хранилища Lucene, а представляет собой базу данных графиков.

В действительности (или, по крайней мере, в тех случаях использования, с которыми я сталкивался) вы редко хотите проходить через огромное количество плотных связей на супер узлах (таких как отношения HAS_VISITED в нашем примере). Если вы загрузите небольшое подмножество отношений из суперузла, индексированный расширитель отношений будет работать хорошо. Если вы хотите получить большее количество отношений без извлечения их всех за один раз, лучшим подходом будет упорядочить попадания из индекса Lucene в расширителе (это быстро в lucene) и вернуть только настроенное максимальное число отношений, которые вы можете справиться своевременно. Зачем загружать 1 млн пользователей, посетивших данный город, когда на экране можно отобразить только 10 — вместо этого загрузите 10!

Мы реализовали
OrderedIndexedRelationshipExpander, чтобы проиллюстрировать, как мы можем использовать возможности Lucene для выполнения сортировки и возврата только лучших N результатов:
 

Листинг 3: Расширитель отношений, который упорядочивает и просматривает результаты поиска Lucene
public class OrderedIndexedRelationhipExpander implements RelationshipExpander{
 //The rest of the class is same as in the
 //IndexedRelationshipExpander
 //Omitted for clarity
public static final int DEFAULT_MAX_HITS = 100000;

    @Override
    public Iterable<Relationship> expand(Node node) {

        QueryContext queryContext = new QueryContext(this.relationshipType.name())
          .sort(Sort.INDEXORDER)
      .top(DEFAULT_MAX_HITS); #1
        Iterable<Relationship> hits = null;
        ReadableRelationshipIndex autoIndex = this.graphDatabaseService.index().getRelationshipAutoIndexer().getAutoIndex();
        switch (this.direction){
            case OUTGOING:
                hits = autoIndex.query("type",queryContext, node, null);
                break;
            case INCOMING:
                hits = autoIndex.query("type", queryContext, null, node);
                break;
            case BOTH:
                IndexHits<Relationship> out = autoIndex.query("type", queryContext, node, null);
                IndexHits<Relationship> in = autoIndex.query("type", queryContext, null, node);
                hits = Iterables.concat(out, in);
                break;
        }

        return hits;
    }

}

Единственное изменение, которое мы внесли в предыдущий пример, — это строка (# 1), где мы создаем
QueryContext, используя критерий поиска, порядок сортировки и количество возвращаемых топ-хитов. Все остальное точно так же, как в
IndexedRelationshipExpander .

Давайте теперь повторим шаблон, пройдя через отношения HAS_VISITED на супер-узле. Помните, мы возвращаем 100K лучших результатов — все еще достаточно большая коллекция.

Листинг 4: Использование OrderedIndexedRelationshipExpander для загрузки 100-тысячных отношений

RelationshipType relationshiptype = DynamicRelationshipType.withName("HAS_VISITED");
Direction direction = Direction.BOTH;
TraversalDescription traversal =
    Traversal.description()
    .evaluator(Evaluators.atDepth(1))
    .expand(
        OrderedIndexedRelationhipExpander.create(this.embeddedGraphDatabase, relationshipType, direction)
    );
traversal.traverse(city);

Таблица 2 показывает результаты измерения производительности:

Таблица 2: Результаты производительности OrderedIndexedRelationshipExpander

 

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

Мы знали, что Neo4j иногда борется с обходом супер узлов. Мы также знали, что Neo4j имеет отличные возможности поисковой системы Lucene, встроенные в серверную систему.
В этом блоге мы продемонстрировали, что использование магазина Neo имеет лучшую производительность, чем индекс Lucene, при извлечении большого количества связей из супер-узлов. Тем не менее, производительность не была идеальной (более 1 секунды).


Используя некоторый практический здравый смысл и используя движок Lucene для упорядочивания и разбивки на страницы результатов поиска, нам удалось увеличить производительность более чем в два раза.

Результаты «теплого кэша» по-прежнему в пользу поиска узла / отношения магазина Neo — если вы можете жить с медленным первым проходом, то вам следует рассмотреть возможность использования мощного механизма Neo. Однако, если вы можете воспользоваться преимуществами подкачки и если производительность первого запроса важна для вашего варианта использования, вы можете рассмотреть поиск связей в индексе Lucene.


Этот подход проверен для узлов с большим количеством связей.
Движок Neo оптимизирован для обработки обходов «стандартных» узлов (например, до 1000 связей), поэтому, если ваш домен может быть смоделирован в виде графика без каких-либо суперузлов, вы должны попытаться использовать только мощь Neo Графический движок.