Прежде чем перейти к статье, хочу вам представить, экономическую онлайн игру Brave Knights, в которой вы можете играть и зарабатывать. Регистируйтесь, играйте и зарабатывайте!
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;
2. Загрузить вершины:
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, на мой взгляд, подходит наилучшим образом.
Литература
Рейтинг графовых бд: https://eandy.ru/oop/samye-populyarnye-grafovye-subd-dlya-c
LOAD JSON from URL AS Data: https://neo4j.com/blog/cypher-load-json-from-url/
Efficient Neo4j Data Import Using Cypher-Scripts: https://medium.com/neo4j/efficient-neo4j-data-import-using-cypher-scripts-7d1268b0747
Fast Batched Updates of Graph Structures: https://medium.com/neo4j/5-tips-tricks-for-fast-batched-updates-of-graph-structures-with-neo4j-and-cypher-73c7f693c8cc
Data Import Decisions: Why Choose Kettle for Neo4j Data Import: https://jennifer-reif.medium.com/data-import-decisions-why-choose-kettle-for-neo4j-data-import-1bd91ab85300