Статьи

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

В рамках моей недавней работы над Open Credo я участвовал в проекте, который использовал базу данных Neo4J Graph для хранения приложений.
Neo4J — одна из первых графовых баз данных, появившихся на мировом рынке. Будучи открытым исходным кодом, он не только обладает мощью и простотой поддержки графической модели данных, но и является хорошим выбором для готовой графической базы данных.

Тем не менее, была одна область, в которой я изо всех сил пытался получить достаточно хорошую производительность от Neo4j — супер узлы.

Супер узлы представляют узлы с плотными отношениями (100 КБ и более), которые быстро становятся узкими местами в алгоритмах обхода графа при использовании Neo4J. Я пробовал много разных подходов, чтобы обойти эту проблему, но введение автоматической индексации в Neo4j 1.4 дало мне идею, с которой я добился успеха. Подход, который я выбрал, состоит в том, чтобы попытаться получить отношения суперузлов, используя индексы Lucene вместо стандартных API Neo. В этой записи я поделюсь тем, чего мне удалось достичь и как.
Для начала давайте сгенерируем примерный граф с суперузлами. Вот количество сгенерированных данных:

 

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

 

Это не большой объем данных, но достаточно большой для начала.

 

Допустим, мы хотим загрузить отношения «IS_IN» для данного узла города. Это довольно просто сделать с помощью Neo API:

Листинг 1: Итерация через связи узлов с использованием стандартного API Neo4j

Iterable relationships =
city.getRelationships(DynamicRelationshipType.withName("IS_IN"));
for (Relationship r : relationships) {
cnt++;
}

Сколько времени это займет для одного городского узла? Я запускал этот код 10 раз (для учета попаданий в кэш при последующих запусках), и вот результаты:

Таблица 1: Производительность выборки отношений с суперузла с отношениями 100K, используя стандартный API Neo4J

 
При первом извлечении отношения это занимает 1,6 секунды. Каждый последующий запрос занимает менее 1 мс! Мы можем поблагодарить механизм кэширования и отображения памяти Neo4j за это увеличение скорости (или разработчиков, которые реализовали его, если быть более точным).
Но я все еще беспокоился о производительности первого заезда. Для одной выбранной взаимосвязи (есть только одна взаимосвязь «IS_IN» для каждого городского узла), 1,6 секунды кажется много. Причина низкой производительности заключается в том, что городской узел имеет в общей сложности 100 001 связь, и только один из них имеет тип IS_IN. Чтобы выбрать правильный вариант, движок Neo4j должен будет просмотреть все отношения, затем сравнить их типы и вернуть только те, которые соответствуют запрошенному типу.

Хотя система будет работать хорошо, когда кеш прогревается, это может вызвать проблемы, если вы часто обращаетесь к «холодным» частям графика. Хотя Neo4j пытается хранить большую часть данных в кэш-памяти или отображенной памяти, это не всегда может быть достигнуто. Когда график слишком велик, чтобы полностью поместиться. Если супер-узлы не могут постоянно храниться в памяти (либо потому, что их слишком много, либо доступ к более частым данным занимает всю доступную кэш / отображенную память), тогда производительность будет отрицательно сказываться.

С точки зрения больших систем данных, 100 тыс. Связей на узел — это не так уж много. Что произойдет, если у вас есть супер узлы с отношениями 1M? Посмотрим сами!

Я собираюсь сохранить 5 городских узлов на графике, каждый из которых имеет одну связь IS_IN со страновым узлом Великобритании. Но я увеличу количество пользователей до 1 000 000. И предположим, что каждый пользователь посетил каждый из 5 городов. Таким образом, у нас будет 1 000 001 отношение на узел города. Я повторил код из листинга 1, чтобы получить отношения типа IS_IN (будет только один), и повторил его 10 раз, чтобы увидеть результаты, когда кэш включится. Таблица 2 показывает результаты.

Таблица 2: Производительность выборки отношений с супер-узла с отношениями 1M с использованием стандартного API Neo4J

 
Первый запуск теперь близок к 5 секундам, а последующие вызовы, которые попадают в кеш, по-прежнему занимают менее 1 мс!

Ухудшение производительности не является линейным (при увеличении входного сигнала в 10 раз время увеличилось только в 3 раза), и это хорошо. Когда это тепло, система работает хорошо с ответами sub-ms.

Но иногда наши требования не учитывают время прогрева. Если у вас есть обход, который должен пройти через 10 суперузлов, вы можете приблизиться к 1-минутному обходу. Если у вас есть пакетное задание, которое должно обработать 100 тыс. Таких обходов, а набор данных таков, что он не может храниться в памяти все время, это может занять 100 тыс. Минут или больше, чем месяц! Что мы можем сделать, чтобы улучшить это?

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

Но мы собираемся сконцентрироваться на более простом решении: как мы можем получить только отношения требуемого типа, игнорируя огромное количество отношений других типов, которые нам не интересны. Мы собираемся использовать встроенный Neo4j. в поддержке индексации Lucene для получения необходимых отношений непосредственно из индекса. Чтобы сделать это, нам нужно будет добавлять тип отношения в индекс при каждом создании нового отношения.

Во-первых, мы собираемся настроить возможность автоматической индексации отношений Neo4J, добавив следующие свойства в
neo4j.properties
файл, как показано в листинге 2.

Листинг 2. Настройка автоматической индексации отношений в файле neo4j.properties

relationship_auto_indexing=true
relationship_keys_indexable=type

Мы включаем автоматическое индексирование
отношений
с помощью Relationship_auto_indexing , свойство, и мы собираемся сказать Neo отслеживать и индексировать тип имени свойства каждого созданного отношения.

Мы должны внести небольшое изменение в наш генератор данных и установить
свойство type
для каждого отношения, которое мы создаем:

Листинг 3: Создание отношения с именем типа в качестве свойства, чтобы включить автоматическую индексацию отношений

DynamicRelationshipType relType = DynamicRelationshipType.withName("IS_IN");
Relationship isA = city.createRelationshipTo(ukCountry, relType);
isA.setProperty("type", relType.name());
DynamicRelationshipType hasVisited = DynamicRelationshipType.withName("HAS_VISITED");
Relationship rel = user.createRelationshipTo(city, hasVisited);
rel.setProperty("type", hasVisited.name());

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

Листинг 4: Создание отношения с именем типа в качестве свойства, чтобы включить автоматическую индексацию отношений

Iterable relationships = autoIndex.query("type", "IS_IN", city, null);

for (Relationship r : relationships) {
cnt++;
}

Теперь давайте измерим, сколько времени занимает загрузка отношений из индекса — как и в предыдущих запусках, мы будем извлекать отношения 10 раз, чтобы позволить кэшу заполниться. Таблица 3 показывает результаты:

Таблица 3: Производительность выборки отношений из суперузла с отношениями 1M, используя автоматическую индексацию отношений

Благодаря индексу Lucene «холодная» выборка заняла всего 12 мс в этом пробеге. Последующие вызовы все еще очень быстрые, хотя и немного медленнее, чем раньше, как сейчас мы наблюдаем несколько раз в 1 мс среди большинства вызовов мс.

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

Листинг 5: Реализация IndexedRelationshipExpander, которая использует индекс Lucene для извлечения отношений для узла

public class IndexedRelationshipExpander implements RelationshipExpander{
private final GraphDatabaseService graphDatabaseService;
private final RelationshipType relationshipType;
private final Direction direction;


private IndexedRelationshipExpander(GraphDatabaseService graphDatabaseService, RelationshipType relationshipType, Direction direction) {
this.relationshipType = relationshipType;
this.graphDatabaseService = graphDatabaseService;
this.direction = direction;
}


public static IndexedRelationshipExpander create(GraphDatabaseService graphDatabaseService, RelationshipType relationshipType, Direction direction){
return new IndexedRelationshipExpander(graphDatabaseService, relationshipType, direction); #1
}

@Override
public Iterable expand(Node node) {
Iterable hits = null;

ReadableRelationshipIndex autoIndex = this.graphDatabaseService.index().getRelationshipAutoIndexer().getAutoIndex();
switch (this.direction){
case OUTGOING:
hits = autoIndex.query("type", this.relationshipType.name(), node, null); #2
break;
case INCOMING:
hits = autoIndex.query("type", this.relationshipType.name(), null, node); #3
break;
case BOTH:
IndexHits out = autoIndex.query("type", this.relationshipType.name(), node, null);
IndexHits in = autoIndex.query("type", this.relationshipType.name(), null, node);
hits = Iterables.concat(out, in); #4
break;
}
return hits;
}

@Override
public RelationshipExpander reversed() {
return new IndexedRelationshipExpander(graphDatabaseService, relationshipType, direction.reverse());
}
}

Для доступа к индексу
IndexedRelationshipExpander
требуется ссылка на
GraphDatabaseService
. Кроме того, нам нужно
указать
интересующий нас тип и направление отношений для IndexedRelationshipExpander (# 1). Реализация метода expand () просто проверяет направление отношения и запрашивает авто-индекс отношения (# 2, # 3 — разница только в аргументах startNode / endNode). Там, где нам нужны отношения в обоих направлениях, мы должны выполнить два отдельных поиска, один для входящих и один для исходящих отношений. Затем нам нужно объединить два результата, в примере это делается с помощью библиотеки Google Guava (# 4)

И это все. Теперь мы можем пройти отношения указанного типа, используя новый расширитель отношений:

Листинг 6: Использование пользовательского IndexedRelationshipExpander в Neo4J Traversal API

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

Обратите внимание, что у вас должно быть
 свойство индексированного типа
для каждого отношения, которое содержит имя типа отношения, чтобы IndexedRelationshipExpander работал правильно.
Учитывая базовую реализацию
RelationshipExpander
, его легко адаптировать под ваши конкретные требования. Например, вы можете индексировать свойство creation_date для каждого отношения, а затем проходить только отношения, созданные после указанной даты. Вы можете включить любое индексированное свойство отношения при создании запроса Lucene и реализовать его в
методе RelationshipExpander.expand ()
.

Теперь мы увидели, как мы можем использовать индексирование Lucene для быстрой выборки и прохождения небольшого подмножества отношений на супер узле. Однако эффективность этого подхода зависит от того, заинтересованы ли вы только в небольшой части отношений. В следующей записи блога мы рассмотрим, как мы можем эффективно загружать большее количество отношений из суперузла.

nb Я использовал релиз Neo4j 1.4 для запуска примеров, описанных в этом блоге. Описанный расширитель индексированных отношений не будет работать с более ранней версией Neo4J, поскольку он зависит от возможностей автоматической индексации на Neo4j. Все тесты выполняются на MacBook Pro 2,76 ГГц с 8 ГБ оперативной памяти. Размер кучи Java был установлен на 2 ГБ, и использовались стандартные настройки отображения Neo4J. 

nbb У некоторых ребят из Neo4J произошли некоторые улучшения в обходе супер-узлов, их можно найти здесь https://github.com/peterneubauer/graph-collections . Как только я опробую новые функции, я сообщу вам об этом в одной из следующих записей в блоге.

ПРИМЕЧАНИЕ. Вторая часть этой записи блога, в которой рассматривается выборка большого количества связей из суперузла, теперь доступна здесь.