Neo4j без преувеличения самая распространенная графовая база данных. Подход «schema free», гибкий язык запросов «cypher» — познакомиться с ней стоит хотя бы для расширения кругозора. Мы в компании Bimeister провели серию экспериментов по переезду на Neo4j для повышения производительности. Под катом я рассмотрю одну из сторон возможного апгрейда — импорт данных в графовую БД, проведу оценку ее преимуществ и недостатков и оценю время загрузки каждым из способов.
Если обратиться к документации, то СУБД предоставляет два основных способа импорта: загрузку CSV-файлов через утилиту администратора neo4j-admin
и загрузку все тех же файлов запросом с клиента. Также есть третий способ, который не раскрыт в документации, но об этом чуть ниже. Для сравнения все варианты буду оценивать по следующим критериям:
возможность импорта на любом этапе цикла жизни БД;
возможность промежуточного сохранения результата;
возможность использования для массовых DM-операций;
возможность передачи данных в соединении.
NEO4J-ADMIN
Импорт с помощью утилиты администратора рассчитан на инициализацию пустой БД. Вы не сможете дописать или обновить уже созданную БД. Все данные необходимо разбить на вершины и связи в отдельные CSV-файлы. Импорт осуществляется одной транзакцией, передача данных — через файловую систему.
LOAD CSV
Импорт с оператором LOAD CSV
возможен с клиента СУБД и рассчитан на порции данных до 10М записей. Есть возможность разбивать на транзакции в конструкции оператора. Этот вариант импорта можно сочетать с командами удаления и обновления данных. Основным требованием остается передача данных через CSV-файлы.
UNWIND
Третий способ загрузки заключается в возможности языка «cypher» работать с коллекциями и словарями c помощью оператора UNWIND
. В качестве параметров запроса можно передавать коллекции, содержащие словари, содержащие коллекции, содержащие словари... ну, вы поняли. Таким образом можно импортировать коллекции вложенных объектов, передавать данные напрямую в соединении с СУБД и обрабатывать все одним запросом.
|
Жизненный цикл БД |
Промежуточное сохранение |
DM-операции |
Данные в соединении |
NEO4J-ADMIN |
- |
- |
- |
- |
LOAD CSV |
+ |
+ |
+ |
- |
UNWIND |
+ |
+ |
+ |
+ |
Сравнение скорости импорта
Для сравнения скорости загрузки использовались данные об иерархии ~1M объектов (~100K связей). Импорт через утилиту neo4j-admin
и LOAD CSV
требует подготовки файлов в формате CSV с заголовками:
Nodes (Uid,Title)
Edges (ParentId,ChildId)
NEO4J-ADMIN
Импорт с помощью утилиты администратора выполняется одной командой с указанием всех файлов:
docker run -v " $(pwd)/data:/data" \
-e "NEO4J_dbms_directories_import:/import" \
-v "$(pwd)/import:/import" \
neo4j:4.4.3-community bin/neo4j-admin import \
--nodes="Exemplar=/import/nodes.csv" \
--relationships="HAS_COMPOSITION=/import/edges.csv"
LOAD CSV
Импорт с оператором LOAD CSV
подразумевает загрузку каждого файла в отдельности. Для выполнения запросов можно воспользоваться утилитой «cypher-shell»:
docker run -v " $(pwd)/data:/data" \
-e "NEO4J_dbms_directories_import:/import" \
-v "$(pwd)/import:/import" \
neo4j:4.4.3-community bin/cypher-shell
В первую очередь необходимо создать индекс на ключевое поле импортируемых вершин:
create constraint exemplar_uid for (n:Exemplar) require n.Uid is unique;
Загрузить вершины:
USING PERIODIC COMMIT 10000
LOAD CSV WITH HEADERS FROM 'file:///nodes.csv' as line
CREATE (:Exemplar {Uid: line.Uid, Title: line.Title});
Загрузить связи:
USING PERIODIC COMMIT 10000
LOAD CSV WITH HEADERS FROM 'file:///edges.csv' as line
MATCH (p:Exemplar {Uid: line.parentId}), (c:Exemplar {Uid: line.childId})
MERGE (p)-[:HAS_COMPOSITION]->(c);
UNWIND
Для импорта оператором UNWIND
необходимо написать клиент на одном из поддерживаемых языков. Данные нужно сгруппировать в коллекцию объектов со связями. В моем случае возможны два способа:
{
"Uid": "...",
"Title": "...",
"ParentId": "..."
}
Или
{
"Uid": "...",
"Title": "...",
"ChildIds": [...]
}
Также сперва стоит создать индекс:
create constraint exemplar_uid for (n:Exemplar) require n.Uid is unique;
В первом случае импорт выполняется запросом:
UNWIND $page as inExemplar
MERGE (c:Exemplar {Uid: inExemplar.Uid})
SET c.Title = inExemplar.Title
WITH c, inExemplar.ParentId as parentId
WHERE parentId is not null
MERGE (p:Exemplar {Uid: parentId})
MERGE (p)-[:HAS_COMPOSITION]->(c)
$page
— параметр, в котором передается коллекция объектов.
Во втором случае:
UNWIND $page as inExemplar
MERGE (p:Exemplar {Uid: inExemplar.Uid})
SET p.Title = inExemplar.Title
WITH p, inExemplar.ChildIds as childIds
WHERE not isEmpty(childIds)
UNWIND childIds as childId
MERGE (c:Exemplar {Uid: childId})
MERGE (p)-[:HAS_COMPOSITION]->(c)
В результате эксперимента были получены следующие результаты:
|
Время импорта |
NEO4J-ADMIN |
34s |
LOAD CSV |
1m 37s |
UNWIND1 |
2m 07s |
UNWIND2 |
2m 55s |
В случае с импортом оператором UNWIND на скорость загрузки оказывает влияние способ группировки данных, наличие вложенных операций увеличивает время.
Вывод
Импорт через утилиту NEO4J-ADMIN и оператор LOAD CSV — это инструменты администратора, которые позволяют инициализировать или редактировать БД из консоли. Самый быстрый способ, с помощью утилиты администратора, имеет больше всего ограничений. Главный недостаток в этих подходах — передача данных через файловую систему. Такой способ требует дополнительных усилий по организации общего файлового хранилища между клиентом и сервером. В условиях, когда СУБД используется в микросервисной архитектуре, такое ограничение может быть весьма неприятным. С другой стороны, импорт с оператором UNWIND дает большую степень свободы при организации архитектуры приложения, хотя и показывает меньшую скорость загрузки. Для создания массовых DM операций оператор UNWIND, на мой взгляд, подходит наилучшим образом.
easimonenko
А что можете сказать о расходовании памяти? Пытался как-то создать граф из CSV-файлов: часть графа создалась, затем процесс надолго завис, и в конце-концов рухнула сама СУБД с ошибкой нехватки памяти. Может вы задавали какие-то настройки для этого, настравали саму JVM?
skywarer Автор
Есть такая проблема. По настройке JVM можно воспользоваться утилитой neo4j-admin для подбора оптимальных значений
neo4j-admin memrec --memory=<memory dedicated to Neo4j>, --verbose, --docker
. Это немного поможет, но основное лекарство - разбивать на транзакции, выполнять массовые операции постранично. Это касается не только импорта, а любых операций, которые могут затронуть большую часть БД.easimonenko
Благодарю за подробный ответ!