Първи стъпки с Neo4j Graph Database
В нашия проект възникна следната задача - има база данни с голям брой стоки, на ниво стотици хиляди. Всеки продукт има стотици динамично създадени функции. Необходимо е да се осигури бързо филтриране по стоки по набор от различни характеристики. Времето за генериране на отговор трябва да бъде не повече от 0,3 секунди, трябва да поддържате сложна логика в стила.
Имаме всичко внедрено в рамките на MySQL + Symfony2/Doctrine, скоростта е незадоволителна - отговорите се генерират в рамките на 1-10 секунди. Опитите ми да оптимизирам цялата тази икономика са под разрез.
Терминология на задачата за филтриране на стоки (опростена)
- характеристика— определено свойство на продукта. Например количеството памет.
- Шаблон на продукте набор от всички възможни характеристики на един и същи вид стоки, например списък с възможни характеристики на компютърни мишки. Когато добавя нов продукт, администраторът може да избере характеристиките в шаблона. Невъзможно е да добавите нова характеристика за един продукт - трябва да добавите характеристика към шаблона за този продукт. В същото време тази функция ще бъде достъпна за всички продукти, използващи този шаблон.
- продуктова група— продукти, базирани на същия шаблон. Например компютърни мишки. Филтрирането се извършва само за продукти от една група
- критериие логическо правило, което се състои от набор от формални изисквания за характеристиките на продукта. Например „мишка за игри“ е набор от изисквания за производителност (размер не е миниатюрен) И (лазерен сензор) И (резолюция на сензора най-малко 1500)
- филтър— група от критерии за продукти от един и същи тип. В зависимост от критериите те могат да се комбинират чрез И или ИЛИ
Горещата линия има по-усъвършенстванаопция - с подсказка колко продукта ще останат след активирането на критерия. Например, ако изберете филтър "Bluetooth", то след зареждане на страницата, филтърът "Тип сензор на мишка - оптичен" ще има номер 17. Всъщност за такава реализация трябва не само да направите селекция според критериите, но и да изчислите броя на продуктите за всеки оставащ филтър, когато е активиран.
За да разреша този проблем, реших да изпробвам графичната база данни Neo4j. За повърхностно запознаване препоръчвам да прочетете тази публикация.
Neo4j терминология и графични бази данни като цяло.
- графична база данни,графична база данни- база данни, изградена върху графики - възли и връзки между тях
- Cypher- език за писане на заявки към базата данни Neo4j (като SQL в MYSQL)
- възел,възел— обект в базата данни, възел на графика. Броят на възлите е ограничен до 2 на степен 35
34 милиарда
Схема за решаване на проблема
За всеки продукт създайте отделен възел, съхранете идентификатора на продукта в базата данни MySQL в свойствата на възела. За всеки критерий създайте свой собствен възел, запазете идентификатора на критерия в свойствата. След това свържете всички възли на продукта към възлите на критериите, които съответстват на продукта. Когато променяте характеристиките на продукта или свойствата на критериите, актуализирайте връзките между възлите.
Първото решение е с Neo4j
Като се има предвид, че съм с графични базиданни никога не работеха - реших да разположа Neo4j локално, да проуча Cypher на основно ниво и да се опитам да внедря необходимата логика. Ако всичко работи, тествайте скоростта на работа за база данни от 1 милион продукта, всеки с 500 характеристики.
Внедряването на системата е съвсем просто - изтеглете дистрибуцията и я инсталирайте.
Neo4j сървърът има RestAPI, php има neo4jphp библиотека. Има и пакет за интеграция със Symfony2 - klaussilveira/neo4j-ogm-bundle.
Комплектът за разпространение включва уеб сървър и приложение за работа с него, по подразбиране http://localhost:7474/ Има и стара версия на клиента с различна функционалност.
Удобно е да използватекратка документациякато документация.Примерите за кодса в graphgist. На теория те трябва да се изпълняват онлайн там, но сега не работи. За да видите кода, трябва да следвате връзката от graphgist (например тук) и да щракнете върху бутона Page Source там.
За експериментиране с Neo4j е много удобно да използвате вградения уеб клиент.Там можете да изпълнявате Cypher заявки и да видите отговора на заявките заедно с връзките и характеристиките на възлите.
Прости Cypher команди
Създаване на възел с етикет
Изберете всички възли
Създайте 2 свързани възела
Свържете 2 съществуващи възела
Изтрийте всички свързани възли
Изтриване на всички несвързани възли- ако се опитате да изпълните тази команда в база данни, където има свързани възли, тя няма да работи. Първо трябва да премахнете свързаните възли.
Изберете продукти, които отговарят на критерий 3
Уеб клиентът не може да изпълни няколко Cypher команди наведнъж. Казват, че старият клиент може да направи това, но не намерих такава възможност. Следователно трябва да копирате по 1 ред наведнъж.
Можете да създадете много възли с връзки с една команда, трябва да дадете различни имена на възлите, не можете да дадете име на връзките
Вземете структура като тази. Ако ви изглежда по-малко ясно, можете да пренаредите възлите с мишката.
Neo4j междинни тестове за скорост
Време е да тествате скоростта на попълване на базата данни и прости проби от голяма база данни.
За да направите това, клонирайте neo4jphp
Основното описание на тази библиотека е в тази публикация, така че веднага ще публикувам кода за попълване на тестовата база данни examples/test_fill_1.php
Размерът на тестовата база данни на диска е 1781 мегабайта. Съхранява 78300 продукта, 4000 критерия, 15660000-31320000 връзки. Общият брой на обектите (възли и връзки) е по-малко от 32 милиона - средно 55 байта на обект. За мен твърде много, но основното изискване все пак е скоростта на пробите, а не размера на базата данни.
Първият опит за тестване на честотата на дискретизация се провали - сървърът Neo4j отново "мина" на 100% натоварване на процесора и в продължение на няколко минути не върна отговор на заявката.
За да продължите напред, трябва да разберете как да оптимизирате заявка в Neo4j. Първоначално исках да огранича началния набор от възли в селекцията с помощта на инструкцията START
Индексимогат да се добавят с командата
Уникален индексможе да бъде създаден с командата
Индексите, добавени от горните команди, не могат да се използват в директивата START. Тук казват, че могат да се използват само където
Индексите, създадени чрез Cypher, се наричат индекси на схема и не трябва да се използват в клаузата START. Търсенията на индекса на клаузата START са запазени за наследените индекси, които създавате чрез автоматично индексиране или чрез API, различни от Cypher.
За да използвате индекса :user, който сте създали, можете да направите следното:
съвпадаn:user където n.name="aapo" return n;
Ако разбирам документацията правилно, можете спокойно да използвате WHERE вместо START
СТАРТ не е задължителен. Ако не посочите изрични начални точки, Cypher ще се опита да изведе начални точки от вашата заявка. Това се прави въз основа на етикети на възли и предикати, съдържащи се във вашата заявка. Вижте Глава 14, Схема за повече информация. По принцип клаузата START е наистина необходима само когато се използват наследени индекси.
Така се роди първата работеща заявка
В нашата тестова база не бяха намерени индекси, така че ще създадем друга база за теста по различен начин. Не намерих възможност за създаване на независими набори от данни (аналогични на база данни в MySQL) в Neo4j. Следователно, за тестване, просто промених пътя до хранилището на данни в настройките на Neo4j Community (Местоположение на базата данни)
В пакетен режим в Neo4jphp не можах да добавя етикети към възлите и по някаква причина индексите не бяха запазени. Като се има предвид, че Cypher престана да бъде китайска буква за мен, реших да попълня основния хардкор - на чист Cypher. Така се оказа test_fill_2.php
Скоростта на добавяне на данни се оказа очаквано по-висока, отколкото в първия вариант. Тестовият скрипт с добавяне на 30 000 възли и 500 000 - 1 000 000 връзки на cypher завършен за 140 секунди, базата данни зае 62 мегабайта на диска. Когато се опитах да стартирам скрипта с $waresCount=1000 (да не говорим за 10 000 елемента), получих грешка „Грешка при препълване на стека“. Пренаписах скрипта с помощта на .
Това доведе до катастрофален спад в скоростта на работа, модифицираният скрипт работи около час. Реших да тествамскоростта на избор спрямо няколко критерияи да се върна към въпроса за бързото вмъкване на данни по-късно.
Скриптът по-горе завърши за 0,02 секунди. Като цяло това е доста приемливо, ноостава проблемът как бързо да се запазят голям брой връзки между възлите при актуализиране на свойствата на продукта.
Алтернативно решение
Реших "за да изчистя съвестта си" да изпробвам MySQL като хранилище. Връзките между възлите ще се съхраняват в отделна таблица без допълнителна информация.
Тестови скрипт за попълване на базата по-долу
Попълването на базата данни отне 12 секунди. Размерът на таблицата е 37 мегабайта. Търсенето по 2 критерия отнема 0.0007 секунди
Друг вариант
Под mysql има пълноценно хранилище за графични данни - но не съм го тествал. Съдейки по документацията, той е много по-примитивен от Neo4j.
Neo4j е много готино нещо. Заявка като „Изберете контакти на потребители, които са харесали филмови актьори, които са участвали във филми, които звучат като саундтраци, написани от музиканти, които харесвам“ в Neo4j се решава тривиално. За SQL това е много по-трудна задача.
Сравняването на пълноценна графична база данни с чиста индексна таблица в MySQL е неправилно, нов рамките на решаването на проблема ми използването на Neo4j не даде никакви предимства.