Как работать с neo4j
Перейти к содержимому

Как работать с neo4j

  • автор:

Введение в графы и Neo4j. Обработка графов в Spark

Программисты автоматизируют мир. Начав с автоматизации подсчета денег, где табличные записи естественны, программисты стали заталкивать в таблички всё, до чего их допустили. И это работало. В прошлом веке. И это хорошо т.к. из глубин вековой давности мы получили SQL – языка запросов к табличке. SQL хватает почти всегда – он простой, логичный и все его знают. А когда не хватает, его расширяют. И дальше мы будем работать с расширениями и диалектами SQL.

В реальном мире всё несколько сложнее. В табличках представлять большие и сложные зависимости сложно и не эффективно по ресурсам (дисковое пространство,процессор, и главный ресурс – время). А какие данные не идеальны для табличек? Может, это лишние данные?

Важный граф

В математике есть понятие граф – набор вершин (узлы) и связей этих вершин (ребро).

Минимальный граф – это две вершины и одно ребро между ними. Например этим минимальным графом мы можем нарисовать семейную пару: М —- Ж. То есть граф – это естественные связи вещей и цепочки этих связей.

Например, записать структуру папок на компьютере в табличку. Для рубистов штук 5 гемов есть и только одно правильное решение – ltree + индексы. Кроме правильного есть идеальное решение: LDAP. А структуру организации в табличку? Тоже можно, но дурная затея, LDAP и тут выручает. И то и то деревья – есть корень и ветки, самая толстая ветка – ствол 🙂

Не видел в жизни ни одного графа

Да даже мой любимый git это граф. Выполните в любом своём репозитории:

git log --graph --abbrev-commit --decorate --date=relative --all 

И вы увидите дерево изменений.

Еще один граф – это родственные связи в Звездных Войнах (как вы помните, там только Император был сиротой): https://nplus1.ru/news/2016/02/10/star-wars-graph.

А еще на графах интернет держится. Например, алгоритм Дейкстры как раз решает задачу как бы быстрейший/кратчайший путь найти.

Когда у нас простой граф мы можем его нарисовать в табличках.

Графы кстати бывают разные. Граф, в котором связи направленные и не создают петель называется . направленный ацикличный граф https://en.wikipedia.org/wiki/Directed_acyclic_graph. Этот тип графа важен для понимания логики работы Spark-приложений, так же он помогает формализировать бизнес процессы и ускорять их.

Если б на почте РФ знали о таких графах, то почтой бы чаще пользовались и не надо было в отделениях торговать сигаретами и водкой. И раз уж мы вспомнили Apache Spark, то RDD представляет из себя дерево разных версий RDD (если вы не поняли о чем речь, то читаем публикации создателя Spark ).

Зависимости в приложении тоже граф, например ruby-erd может для руби приложения нарисовать.

Neo4j и простые запросы

А если у нас тысячи узлов? То тоже можем использовать таблички, только устанем. Вот тут на помощь приходит специализированный инструмент – графовые базы данных. Их много, но мы выберем Neo4j – она сама простая для начала и имеет прекрасный веб-интерфейс с визуализацией, а красивая картинка иногда информативнее тысячи слов.

Есть графовые «базы», использующие табличное хранение данных, но использующие графовую абстракцию – эта порнография работает только в специфичных условиях (в этих же условиях платят хорошо, потому запоминаем cassandra + titan или Spark).

Потихоньку начнем изучать Neo4j с данными, доступными каждому.

Сначала запустим Neo4j, что очень легко, как и всё в Java мире:

  1. Качаете архив neo4j-community-3.0.3 с официального сайта http://neo4j.com/
  2. Распаковываете tar -xzvf ./neo4j-community-3.0.3-unix.tar.gz
  3. Запускаете ./bin/neo4j start

А вообще, не надо. Останавливайте: ./bin/neo4j stop . Сделаем современно и молодежно.

docker run --publish=7474:7474 \ --publish=7687:7687 \ --volume=$HOME/neo4j/data:/data:rw,z \ --env=NEO4J_AUTH=none neo4j:3.0 

А дальше самое интересное, причина почему стоит начинать с Neo4j: откройте в браузере http://localhost:7474/. Всё, вы – властелин графовой базы данных.

Интерактивная консоль и визуализация упростит старт. А еще мы отключили аутентификацию —env=NEO4J_AUTH=none . Для наших экспериментов она не нужна, а для боевых систем надо всё параноидально закрывать. Хотя это и смешно – закрывать и Docker 🙂

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

Создадим минимальный граф.

В данной статье главный диалект языка SQL зовётся “Кефир”: https://neo4j.com/developer/cypher-query-language/ 🙂

CREATE (c:M )-[:rel ]->(b:F ); 

И посмотрим на результат:

MATCH (n) RETURN n LIMIT 1000; 

Граф у нас направленный, даже в наш безумный век мужчина направлен к женщине. Любопытно, что направлением при поиске можно пренебречь:

MATCH p=(n)-->(b) return p 

A можно не пренебрегать:

MATCH (n)-[p]->(b) WHERE startNode(p).id='Male' return b, n; // вернется наш граф MATCH (n)-[p]->(b) WHERE startNode(p).id='Female' return b, n; // а тут уже пустой 

Еще мы можем выбрать самые сильные отношения, чего размениваться на короткие интрижки:

MATCH (n)-[p]->(b) WHERE p.weight > 1000 return b, n /// таких нет, но вы можете снизить порог фильтра 
Заметки по Кефировским запросам для игр на досуге.

Создаем ноды, если нет:

MERGE (c:M ); 

Создаем отношения, если их нет, и ноды если их нет:

MERGE (p:twivi13 ) MERGE (n:Real ) CREATE UNIQUE (p)-[r:rel ]-(n) return r 

Обновляем вес отношений, если они есть:

MATCH (n)-[p]->(b) WHERE b.id='Male' AND n.id='Female' SET p.weight = p.weight + 7 RETURN p 

Смотрим в Neo4j только сотню самых сильных отношений:

MATCH (n)-[p]-(b) RETURN p Order by p.weight DESC limit 100 

Ищем вершины с более чем 2 связями:

MATCH (n) WHERE size((n)--())>2 RETURN n 

Ну и если захотим удалить:

MATCH (n) DETACH DELETE n 

Все классно, всё работает, всё просто. Но пользы ни какой.

Пишем приложения с Neo4j, Spark и данными Твиттера

Надо залить другие данные. И всплывает вопрос о том, как приложение вообще может общаться с Neo4j?

Есть поддержка формата для хипстеров HTTP API с JSON, в нём есть даже загрузка данных пачками. Так что с этой стороны все правильно, но зря.

Следующий вариант: протокол Bolt. Bolt бинарный, шифруемый и изобретен специально для Neo4j.

Каждое приложение должно придумать свой протокол, а то протоколов не хватает.

И раз уж есть такая возможность, то впихиваем все модные инструменты без разбору, особенно если в них нет нужды. И в модных инструментах выберем еще и модные названия: напишем приложение для Spark Streaming, которое будет читать твиттер, строить граф и обновлять связи в графе.

Есть прогноз что Spark Streaming через 5 лет будет не актуален, но сейчас за него платят. И он еще и не стриминг ни разу 🙂

Твиттер – доступный источник большого количества лишней информации. Можно даже считать его графом человеческой глупости. А вот задался бы кто целью проследить развитие идей – был бы наглядный аргумент за классическое образование (у меня его нет, о чем данная статья аж кричит).

Первым делом скачиваем исходники Spark на свою локальную машинку.

git clone https://github.com/apache/spark git checkout branch-1.6 

И в одном из примеров узнаем как подключиться к твиттеру и как его читать. И других примеров там много – полезно просмотреть, пролистать, чтобы знать, что Spark уже умеет.

Если это первое ваше знакомство с твиттером с этой стороны, то ключики и токены брать тут https://apps.twitter.com. После модификации примера получиться может так:

package org.apache.spark.examples.streaming import org.apache.spark.streaming.Seconds, StreamingContext> import org.apache.spark.streaming.StreamingContext._ import org.apache.spark.SparkContext._ import org.apache.spark.streaming.twitter._ // да да - тут эклипс бы отрефакторил всё, но я на виме import org.apache.spark.streaming.twitter.TwitterUtils import org.apache.spark.storage.StorageLevel import org.apache.spark.SparkConf import org.neo4j.driver.v1._ object Twittoneo  def main(args: Array[String])  StreamingExamples.setStreamingLogLevels() // а сюда я сначала настоящие ключи записал :) System.setProperty("twitter4j.oauth.consumerKey", "tFwPvX5s") System.setProperty("twitter4j.oauth.consumerSecret", "KQvGuGColS12k6Mer45jnsxI") System.setProperty("twitter4j.oauth.accessToken", "113404896-O323jjsnI6mi2o6radP") System.setProperty("twitter4j.oauth.accessTokenSecret", "pkyl20ZjhtbtMnX") val sparkConf = new SparkConf().setAppName("Twitter2Neo4j") val ssc = new StreamingContext(sparkConf, Seconds(5)) val stream = TwitterUtils.createStream(ssc, None) val actData = stream.map(status =>  // extract from twitter json only interesting fields val author:String = status.getUser.getScreenName.replaceAll("_", "UNDERSCORE") val ments:Array[String] = status.getUserMentionEntities.map(x =>  x.getScreenName.replaceAll("_", "UNDERSCORE") >) (author, ments) >) actData.foreachRDD(rdd =>  if(!rdd.isEmpty())  println("Open connection to Neo4jdb") val driver = GraphDatabase.driver( "bolt://localhost", AuthTokens.basic( "neo4j", "neo4j" )) try // collect called for execute all logic on driver // http://spark.apache.org/docs/latest/cluster-overview.html rdd.collect().foreach(x => x._2.foreach( target => val session = driver.session() // create relation if not exist // merge (p:twivi13 ) // merge (n:Real ) // create unique (p)-[r:rel ]-(n) return r // // pregix 'twi' and replacement _ to UNDESCORE require because Neo4j has many restrictions for label val relation = "MERGE (p:twi"+ x._1+" +x._1+"'>) MERGE (d:twi"+ target+" + target + "'>) CREATE UNIQUE (p)-[r:rel ]-(d) RETURN 1" println(relation) val result1 = session.run(relation) result1.consume().counters().nodesCreated(); session.close() >) >) > finally  driver.close() println("Close Neo4j connection") > driver.close() > >) ssc.start() ssc.awaitTermination() > > 

Обратите внимание – neo4j-spark-connector нам сейчас не нужен, вместо него мы возьмем neo4j-java-driver, который отвечает только за связь java-приложения и Neo4j , без выгрузок и загрузок в естественные форматы RDD или GraphFrame. Но в след. статье они нам понадобятся для настоящей работы носящей гордое имя Big Data инженер.

Компилируем и ставим в локальный Maven репозиторий библиотеку neo4j-java-driver:

git clone git@github.com:neo4j/neo4j-java-driver.git cd neo4j-java-driver mvn clean package install 

Кладём наш код в src/main/scala/org/apache/spark/examples/streaming/Twitter2neo4j.scala . Находясь в папке examples добавляем в ./pom.xml :

   org.neo4j.driver neo4j-java-driver 1.1-SNAPSHOT  

Дальше компилируем примеры вместе с нашим: вызывайте mvn -Phadoop-2.6 -Dhadoop.version=2.6.0 -DskipTests clean package .

Кстати, на моём курсе первым делом учимся maven, gradle и sbt. Это основа основ. Важнее только bash.

У нас получается jar файлик с которым дальше будем работать. Вместо компиляции Spark опять же будем использовать модный Docker.

Обратите внимание на версию Spark – 1.6.1. Важно что б ваш пакет и Spark были одной версией, а то Spark так быстро развивается , что иногда даже слишком быстро.

Помним о безопасности. И еще один интересный момент за Docker – образ для нас собрал непонятно кто, а докер работает от рута (хипстеры писали , что с них взять).Теоретически, это огромная дыра в безопасности. Я-то смело выполняю данные действия — в наипоследнейшей Fedora с включенным selinux, ограждающим дырявый докер. А пользователей убунты не жалко. Кстати, супер крутая и модная версия докера называется chroot.

Запустим контейнер (коробочку, ведерко), экспортируя текущую папку в /shared c правом на только чтение(ro) и перемаркируя selinux контекст в svirt_sandbox_file_t.

Контекст SELinux для песочницы останется и после остановки контейнера. Так же экспортируем нами скомпилированную библиотеку для neo4j-java-driver.

А вот был бы у нас свой центральный репозиторий, например Nexus – было бы проще.

Или как вариант можно поправить pom.xml и собирать со всеми необходимыми зависимостями. У нас получится два контейнера которые должны общаться, поэтому пойдём по пути наименьшего сопротивления – Spark-контейнер будет использовать сетевой стек Neo4j контейнера, разделяя с ним ip адрес.

Для этого вызываем docker ps | grep -i neo4j | awk » и смотрим id контейнера.

docker run -it -v `pwd`:/shared:ro,z \ -v /path/to/neo4j-java-driver/driver/target:/neo4jadapter:ro,z \ --net=container:ca294ee9e614 \ sequenceiq/spark:1.6.0 bash 

Вот мы оказались в контейнере. У нас есть хадуп и спарк. Но хадуп мы трогать не будем – он нам не мешает, а Spark запустим так, что б он выполнялся локально(—master local[4]):

spark-submit --jars /neo4jadapter/neo4j-java-driver-1.1-SNAPSHOT.jar \ --packages "org.apache.spark:spark-streaming-twitter_2.10:1.6.1" \ --class org.apache.spark.examples.streaming.Twittoneo \ --master local[4] \ --driver-memory 1g \ --executor-memory 1g \ --executor-cores 1 \ --verbous \ /shared/target/spark-examples_2.10-1.6.1.jar 

Ждем минут хотя бы 10 и смотрим результат. Но чем больше данных соберем, тем интереснее.

Если хотите краше, динамичнее и может даже в реальном времени, то на mkdev есть курс Екатерины Шпак. Там учат d3.js и прочим хитростям красивого фронтенда. Вообще в Big Data половина дела – это презентовать ваши достижения среднестатистическому менеджеру, у которого хоть и классическое образование, но без картинки не поймёт.

В данном примере есть один важный момент – не используется суперсила Spark. Ведь в Spark мы можем построить сразу готовый граф, аггрегировать все сообщения и только потом заливать в Neo4j. Этим можно значительно повысить количество информации перерабатываемое системой. Но кого это волнует в обучающем примере. Да даже в реальности это мало кого волнует 🙂

Выводы и прогнозы на следующую часть

Полученная база и та выборка, что мы делаем, не практична. Следующий шаг это найти например самых влиятельных пользователей (найти, дать им деньги и пусть рекламируют наш товар). Или отслеживать упоминание вашего бренда и в зависимости от ситуации (ругают, хвалят, кто отзывается) пытаться увеличить прибыль. Это очевидные примеры использования, но в славной стране Германия есть закон запрещающий хранить данные пользователей позволяющие явно идентифицировать оных.

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

Кстати особенность всех nosql решений – база строится исходя из того что и как вы будете запрашивать. Когда кричат, что что-то schemaless – значит либо врут, либо невозможно работать 🙂

Дополнительное чтение
  • https://www.coursera.org/learn/teoriya-grafov/home/welcome
  • https://www.coursera.org/learn/sluchajnye-graphy
  • http://www.kennybastani.com/2015/03/spark-neo4j-tutorial-docker.html
  • https://github.com/sequenceiq/docker-spark
  • https://www.anchormen.nl/spark-docker/

© Copyright 2014 — 2023 mkdev | Privacy Policy

Начинаем работать с графовой базой данных Neo4j

В нашем проекте возникла следующая задача — есть база с большим количеством товаров, на уровне сотен тысяч. У каждого товара есть сотни динамически создаваемых характеристик. Необходимо обеспечить быструю фильтрацию по товарам по набору различных характеристик. Время формирования ответа должно быть не более 0.3 секунды, нужно поддерживать сложную логику в стиле.

(характеристика1 = true AND (характеристика2 < 100)) OR (характеристика1 = false AND (характеристика3 >17)) . далее обычно мешанина из AND\OR

Пример функционала

У нас все реализовано в рамках MySQL + Symfony2/Doctrine, скорость неудовлетворительная — ответы формируются в течении 1-10 секунд. Мои попытки оптимизировать все это хозяйство — под катом.

Терминология задачи по фильтрации товаров (в упрощенном виде)
  • характеристика — определенное свойство товара. Например, объем памяти.
  • шаблон товара — набор всех возможных характеристик однотипных товаров, например — перечень возможных характеристик компьютерных мышек. При добавлении нового товара администратор может выбирать характеристики в рамках шаблона. Добавить новую характеристику для одного товара невозможно — нужно добавить характеристику в шаблон для этого товара. Одновременно эта характеристика будет доступна для всех товаров, использующих этот шаблон
  • группа товаров — товары на основе одного шаблона. Например, компьютерные мышки. Фильтрация делается только для товаров из одной группы
  • критерий — логическое правило, которое состоит из набора формальных требований к характеристикам товара. Например, «геймерская мышка» — это набор требований к характеристикам (размер не миниатюрный) AND (сенсор лазерный) AND (разрешение сенсора не менее 1500)
  • фильтр — группа критериев для однотипных товаров. В зависимости от критериев, они могут комбинироваться через AND или OR

Для решения этой задачи я решил опробовать графовую базу данных Neo4j. Для поверхностного ознакомления рекомендую прочитать этот пост.

Терминология Neo4j и графовых баз данных в целом.
  • graph database, графовая база данных — база данных построенная на графах — узлах и связях между ними
  • Cypher — язык для написания запросов к базе данных Neo4j (примерно, как SQL в MYSQL)
  • node, нода — объект в базе данных, узел графа. Количество узлов ограниченно 2 в степени 35 ~ 34 биллиона
  • node label, метка ноды — используется как условный «тип ноды». Например, ноды типа movie могут быть связанны с нодами типа actor. Метки нод — регистрозависимые, причем Cypher не выдает ошибок, если набрать не в том регистре название.
  • relation, связь — связь между двумя нодами, ребро графа. Количество связей ограниченно 2 в степени 35 ~ 34 биллиона
  • relation identirfier, тип связи — в Neo4j у связей. Максимальное количество типов связей 32767
  • properties, свойства ноды — набор данных, которые можно назначить ноде. Например, если нода — это товар, то в свойствах ноды можно хранить id товара из базы MySQL
  • node ID, ID нода — уникальный идентификатор ноды. По умолчанию, при просмотрах результата отображается именно этот ID. как его использовать в Cypher запросах я не нашел
Схема решения задачи

Для каждого товара создать отдельную ноду, в свойствах ноды хранить id товара в базе MySQL. Для каждого критерия создать свою ноду, в свойствах хранить id критерия. Дальше, связать все ноды товаров с нодами критериев, которые подходят для товара. При изменении характеристик товара или свойств критериев обновлять связи между нодами.

Первый вариант решения — с Neo4j

Учитывая, что я с графовыми базами данных никогда не работал — я решил развернуть локально Neo4j, изучить на базовом уровне Cypher и попробовать реализовать требуемую логику. Если все получиться — провести тестирование скорости работы для базы из 1 миллиона товаров, у каждого 500 характеристик.

Разворачивание системы достаточно простое — скачиваем дистрибутив и устанавливаем его.

У Neo4j сервера есть RestAPI, для php есть библиотека neo4jphp. Также есть bundle для интеграции с Symfony2 — klaussilveira/neo4j-ogm-bundle.

В дистрибутив входит веб сервер и приложение для работы с ним, по умолчанию http://localhost:7474/
Есть еще старая версия клиента, с другим функционалом.

В качестве документации удобно использовать краткую документацию. Примеры кода есть в graphgist. По идее, они должны там выполнятся онлайн, но сейчас это не работает. Чтобы посмотреть код нужно перейти по ссылке из graphgist (например, сюда) и там нажать кнопку Page Source.

Для экспериментов с Neo4j очень удобно использовать встроенный веб клиент Там можно выполнять запросы Cypher и просматривать ответ на запросы вместе со связями и характеристиками нод.

Встроенный клиент Node4j

Простые Cypher команды

Создание ноды с меткой

create (n:Ware ); 

Выбрать все ноды

MATCH (n) RETURN n;

Счетчик

MATCH (n:Ware ) RETURN "Our graph have "+count(*)+" Nodes with label Ware and wareId=1" as counter;

Создать 2 связанные ноды

CREATE (n)-[r:SUIT]->(m)

Связать 2 существующие ноды

MATCH (a ), (b ) MERGE (a)-[r:SUIT]->(b)

Удалить все связанные ноды

match (n)-[r]-() DELETE n,r;

Удалить все несвязанные ноды — если попробовать запустить эту команду в базе, где есть связанные ноды — она не пройдет. Нужно удалить вначале связанные ноды.

match n DELETE n;

Выбрать товары, которым подходит критерий 3

MATCH (a:Ware)-->(b:Criteria ) RETURN a;

Сразу несколько Cypher команд веб клиент выполнять не умеет. Тут говорят, что старый клиент это умеет, но я не нашел такой возможности. Поэтому, нужно копировать по 1 строке.

Можно выполнить создание множества нод со связями одной командой, нужно давать разные имена нодам, связям можно не давать имя

CREATE (w1:Ware)-[:SUIT]->(c1:Criteria), (w2:Ware)-[:SUIT]->(c2:Criteria), (w3:Ware)-[:SUIT]->(c3:Criteria), (w4:Ware)-[:SUIT]->(c1), (w5:Ware)-[:SUIT]->(c1), (w4)-[:SUIT]->(c2), (w5)-[:SUIT]->(c3); 

Получится такая структура. Если у вас выглядит менее понятно — можно переставить мышкой ноды.

Тестовая структура

Промежуточные тесты скорости Neo4j

Пришло время протестировать скорость заполнения базы и простых выборок из большой базой.

Для этого клонируем neo4jphp

git clone https://github.com/jadell/neo4jphp.git

Базовое описание этой библиотеки есть в этом посте, поэтому я сразу выложу код для заполнения тестовой базы еxamples/test_fill_1.php

makeLabel('Ware'); $neoCriteriaLabel = $neoClient->makeLabel('Criteria'); $wareTemplatesCount = 200; // количество шаблонов товара $criteriasCount = 500; // количество критериев $waresCount = 10000; // количество товаров $commitWares = 100; // количество товаров, которое будет идти в 1 batch $minRelations = 200; // минимальное количество связей товара с критериями $maxRelations = 400; // максимальное количество связей товара с критериями $time = time(); for($wareTemplateId = 0;$wareTemplateId<$wareTemplatesCount;$wareTemplateId++) < $neoClient->startBatch(); print $wareTemplateId." (".$criteriasCount." criterias, ".$waresCount." wares with rand(".$minRelations.",".$maxRelations.") . "; $criterias = array(); // создаем критерии for($criteriaId = 1;$criteriaId <=$criteriasCount;$criteriaId++) < $c = $neoClient->makeNode()->setProperty('criteriaId', $wareTemplateId * $criteriasCount + $criteriaId)->save(); // ->addLabels(array($neoCriteriaLabel)) - не работает с commitBatch $neoCriterias->add($c, 'criteriaId', $wareTemplateId * $wareTemplatesCount + $criteriaId); // ->save() такого метода нет $criterias[] = $c; > // создаем товары for($wareId = 1;$wareId <=$waresCount;$wareId++) < $w = $neoClient->makeNode()->setProperty('wareId', $wareTemplateId * $waresCount + $wareId)->save(); // ->addLabels(array($neoWareLabel)) - не работает с commitBatch $neoWares->add($c, 'wareId', $wareTemplateId * $waresCount + $criteriaId); // каждый товар привязываем к случайному количеству критериев for($i = 1;$i<=rand($minRelations,$maxRelations);$i++) < $w->relateTo($criterias[array_rand($criterias)], "SUIT")->save(); > if(($wareId % $commitWares) == 0) < // комитим, при слишком больших комитах Neo4j зависает $neoClient->commitBatch(); print " [commit ".$commitWares." ".(time() - $time)." sec]"; $time = time(); $neoClient->startBatch(); > > $neoClient->commitBatch(); print " done in ".(time() - $time)." seconds\n"; $time = time(); > 

Скрипт заполнения базы я оставил на ночь. Примерно спустя 4 часа скрипт перестал добавлять данные и сервис Neo4j начал грузить сервер на 100%. Утром по итогу работы было вставлено 78300 товаров из 8 категорий товаров.
Результаты тестового заполнения базы — примерно 20 товаров в секунду с 200-400 связями. Не очень высокий результат — Mysql и Cassandra выдавали около 10-20 тысяч вставок в секунду (10 полей, 1 primary index, 1 индекс). Но скорость вставки для нас не критична — мы можем обновлять граф данных в фоновом режиме после редактирования товара. А вот скорость выборки данных — критична.

Размер тестовой базы данных на диске — 1781 мегабайт. В ней хранится 78300 товаров, 4000 критериев, 15660000-31320000 связей. Общее количество объектов (нодов и связей) менее 32 миллионов — в среднем по 55 байт на сущность. Многовато, как по мне, но главное требование все же скорость выборок, а не размер базы.

Первая попытка протестировать скорость выборки провалилась — сервер Neo4j опять «ушел» в режим 100% загрузки процессора и за несколько минут так и не выдал ответ на запрос.

MATCH (c )(b ) RETURN a.wareId;

Чтобы двигаться дальше нужно разобраться, как оптимизировать запрос в Neo4j. Вначале я хотел ограничить стартовый набор нод в выборке с помощью инструкции START

START n=node:nodeIndexName(key=) MATCH (c)(b) RETURN a.wareId; 

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

:schema

Добавить индексы можно командой

CREATE INDEX ON :Criteria(criteriaId)

Уникальный индекс можно создать командой

CREATE CONSTRAINT ON (n:Criteria) ASSERT n.criteriaId IS UNIQUE;

Индексы, добавленные командами выше, нельзя использовать в START директиве. Тут утверждают, что их можно использовать только в where

The indexes created via Cypher are called Schema indexes, and are not to be used in the START clause. The START clause index lookups are reserved for the legacy indexes that you create via autoindexing or through the non-Cypher APIs.

In order to use the :user index you’ve created, you can do this:

match n:user
where n.name=«aapo»
return n;

Если я правильно понял документацию, можно смело использовать WHERE вместо START

START is optional. If you do not specify explicit starting points, Cypher will try and infer starting points from your query. This is done based on node labels and predicates contained in your query. See Chapter 14, Schema for more information. In general, the START clause is only really needed when using legacy indexes.

Так родился первый рабочий запрос

MATCH (a:Ware)-->(c1:Criteria ),(c2:Criteria ),(c3:Criteria ) WHERE (a)-->(c2) AND (a)-->(c3) RETURN a; 

В нашей тестовой базе индексов не обнаружено, поэтому мы создадим еще одну базу для теста другим способом. Возможности создать независимые наборы данных (аналог базы данных в MySQL) в Neo4j я не нашел. Поэтому для тестирования я просто менял путь к хранилищу данных в настройках Neo4j Community (Database location)

Use в Neo4j делается сменой пути к хранилищу

Внимательные читатели возможно обнаружили пару комментариев в коде test_fill_1.php, а именно

 $c = $neoClient->makeNode()->setProperty('criteriaId', $wareTemplateId * $criteriasCount + $criteriaId)->save(); // ->addLabels(array($neoCriteriaLabel)) - не работает с commitBatch $neoCriterias->add($c, 'criteriaId', $wareTemplateId * $wareTemplatesCount + $criteriaId); // ->save() такого метода нет 

В batch режиме в Neo4jphp у меня не получилось добавить метки к нодам, а индексы почему то не сохранились. Учитывая, что Cypher перестал для меня быть китайской грамотой, я решил заполнять базу хардкорно — на чистом Cypher. Так получился test_fill_2.php

 $criteriasCount) < throw new \Exception("maxRelations[".$maxRelations."] should be bigger, that criteriasCount[".$criteriasCount."]"); >$query = new Cypher\Query($neoClient, "CREATE CONSTRAINT ON (n:Criteria) ASSERT n.criteriaId IS UNIQUE;", array()); $result = $query->getResultSet(); $query = new Cypher\Query($neoClient, "CREATE CONSTRAINT ON (n:Ware) ASSERT n.wareId IS UNIQUE;", array()); $result = $query->getResultSet(); for($wareTemplateId = 0;$wareTemplateId<$wareTemplatesCount;$wareTemplateId++) < $time = time(); $queryTemplate = "CREATE "; print $wareTemplateId." (".$criteriasCount." criterias, ".$waresCount." wares with rand(".$minRelations.",".$maxRelations.") . "; $criterias = array(); for($criteriaId = 1;$criteriaId <=$criteriasCount;$criteriaId++) < // создаем нод критерия в виде (w1:Ware) $cId = $criteriaId + $criteriasCount*$wareTemplateId; $queryTemplate .= "(c".$cId.":Criteria), "; $criterias[] = $cId; > for($wareId = 1;$wareId <=$waresCount;$wareId++) < $wId = $wareId + $waresCount*$wareTemplateId; // создаем нод товара в виде (w1:Ware) $queryTemplate .= "(w".$wId.":Ware), "; // создаем связи между нодами в виде (w1)-[:SUIT]->(c1) $possibleLinks = array_merge(array(), $criterias); // clone $criterias не работает for($i = 1;$i<=rand($minRelations,$maxRelations);$i++) < $linkId = $possibleLinks[array_rand($possibleLinks)]; unset($possibleLinks[$linkId]); $queryTemplate .= "w".$wId."-[:SUIT]->c".$linkId.", "; > > $queryTemplate = substr($queryTemplate,0,-2); // удаляем последний ", " $build = time(); $query = new Cypher\Query($neoClient, $queryTemplate, array()); // $queryTemplate будет в районе 42 мегабайт для 10000 товаров, 500 критериев, 200-400 связей между товаром-критерием $result = $query->getResultSet(); print " Query build in ".($build - $time)." seconds, executed in ".(time() - $build)." seconds\n"; // die(); > 

Скорость добавления данных оказалась предсказуемо большей, чем в первом варианте.
Тестовый скрипт с добавлением 30000 нодов и 500000 — 1000000 связей на cypher отработал за 140 секунд, база заняла на диске 62 мегабайта. При попытке запустить скрипт c $waresCount=1000 (не говоря уже о 10000 товаров) я получил ошибку «Stack overflow error». Я переписал скрипт c использованием.

MATCH (a ), (b ) MERGE (a)-[r:SUIT]->(b) 

Это привело к катастрофическому падению скорости работы, модифицированный скрипт работал примерно около часа. Я решил протестировать скорость выборки по нескольким критериям и вернуться к вопросу быстрой вставки данных позже.

(b:Criteria ),(c:Criteria ),(c2:Criteria ) WHERE (a)-->(c) AND (a)-->(c2) RETURN a;", array()); $result = $query->getResultSet(); print "Done in ".(microtime() - $time)." seconds\n"; 

Скрипт выше отработал за 0.02 секунды. В целом — это вполне приемлемо, но проблема как быстро сохранять большое количество связей между нодами при апдейте свойств товара — осталась.

Альтернативное решение

Я решил «для очистки совести» опробовать MySQL в качестве хранилища. Связи между нодами будут храниться в отдельной таблице без дополнительной информации.

CREATE TABLE IF NOT EXISTS `edges` ( `criteriaId` int(11) NOT NULL, `wareId` int(11) NOT NULL, UNIQUE KEY `criteriaId` (`criteriaId`,`wareId`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; 

Тестовый скрипт для заполнения базы ниже

 for($wareId = 1;$wareId <=$waresCount;$wareId++) < $edges = array(); $wId = $wareTemplateId * $waresCount + $wareId; $links = array_rand($criterias,rand($minRelations,$maxRelations)); foreach($links as $linkId) < $edges[] = "(".$criterias[$linkId].",".$wareId.")"; >// заносим сразу связи между товарами и критериями mysql_query("INSERT INTO edges VALUES ".implode(",",$edges)); > print "."; > print " [added ".$wareTemplatesCount." templates in ".(time() - $time)." sec]"; $time = time(); 

Заполнение базы заняло 12 секунд. Размер таблицы — 37 мегабайт. Поиск по 2 критериям занимает 0.0007 секунд

SELECT e1.wareId FROM `edges` AS e1 JOIN edges AS e2 ON e1.wareId = e2.wareId WHERE e1.criteriaId =17 AND e2.criteriaId =31 
Еще один вариант

Под mysql есть полноценное графовое хранилище данных — но я его не тестировал. Судя по документации, он гораздо примитивнее Neo4j.

Выводы

Neo4j — очень крутая штука. Запрос наподобие «Выбрать контакты пользователей, которые лайкнули киноактёров, которые снялись в фильмах, в которых звучали саунтдтреки, которые были написаны музыкантами, которым я поставил лайк» в Neo4j решается тривиально. Примерно так

MATCH (me:User )-[:Like]->(musicants:User)-[:Author]->(s:Soundtrack)-[:Used]->(f:Film)<-[:Starred]-(actor: User)<-[:Like]-(u:User) RETURN u 

Для SQL это гораздо более хлопотное занятие.

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

UPDATE. Изменил url'ы картинок, по идее должны у всех загрузаться.

UPDATE 2. Предложили еще несколько вариантов — MongoDB, elasticsearch, solr, sphinx, OrientDB. Планирую протестировать MongoDB, результаты тестов выложу тут же.

Основы работы с Neo4j в браузере

В статье рассматривается как начать работать с графовой СУБД Neo4j, используя Neo4j Browser. Это руководство может быть полезным как дополнение к книге Редмонда и Уилсона "Семь баз данных за семь недель", так как рассматриваемый веб-интерфейс был полностью переработан, а также к книге "Графовые базы данных" (Робинсон, Вебер, Эифрем), так как в ней этот вопрос вообще не рассматривается. Статья рассчитана на приступающих к изучению Neo4j. Те, кто уже знаком с этой СУБД, могут смело её пропустить.

Neo4j Browser: home screen

Примечание. В статье не рассматривается как установить и настроить Neo4j. Рассматриваемые версии Neo4j 3.3.2 и 3.4.0, Neo4j Browser 3.1.4 и 3.1.12, соответственно.

Начало работы

Для начала убедимся, что Neo4j запущена (пример для Linux):

service --status-all | grep neo4j
 [ + ] neo4j

Знак "плюс" означает, что СУБД уже запущена, "минус" — ещё нет. Для запуска Neo4j выполните команду:

sudo service neo4j start

После запуска перейдите по ссылке http://localhost:7474/browser/. Вы должны увидеть интерфейс Neo4j Browser представленный на изображении выше.

Сейчас нас будут интересовать два элементы интерфейса, изображённые ниже: редактор и учебник.

Neo4j Browser: play

Учебник

Neo4j предоставляет великолепный интерактивный учебник для начинающих. Очень рекомендую его пройти. Для этого просто щёлкните на Start Learning и сначала ознакомьтесь с основными понятиями Neo4j:

Neo4j Browser: graph fundamentals

Neo4j Browser: graph relationships

По достижению последнего шага щёлкните на Intro и ознакомьтесь с возможностями Neo4j Browser:

Neo4j Browser: next steps

Neo4j Browser: introduction

Редактор

В верхней части окна Neo4j Browser располагается строка так называемого редактора:

Neo4j Browser: editor

Начиная набор команд с двоеточия, увидим список всех доступных команд с кратким описанием:

Neo4j Browser: list of commands

Вызовем команду :help :

Neo4j Browser: help

Чтобы ознакомиться с примерами работы с графами можно выбрать :play movie graph или :play northwind graph .

Мы не будем здесь рассматривать эти примеры, а рассмотрим как создать свой граф с помощью языка Cypher.

Создаём граф

Для начала можно ознакомиться с языком Cypher, вызвав команду:

:play cypher

Neo4j Browser: play cypher

Итак, начнём. Создадим небольшой социальный граф. Перейдём в редактор и наберём первую команду на языке Cypher:

CREATE (u1:Person )

После выполнения команды Browser сообщит нам результат:

Neo4j Browser: create result

Добавим ещё один узел:

CREATE (u2:Person )

Теперь запросим все узлы типа Person и извлечём значения свойства name :

MATCH (ee:Person) RETURN ee.name

Neo4j Browser: property match result

Примечание. Как и в SQL есть возможность упорядочить извлекаемые данные по какому-либо полю:

MATCH (ee:Person) RETURN ee.name ORDER BY ee.name

Далее можем запросить все узлы данного типа:

MATCH (ee:Person) RETURN ee

Neo4j Browser: edges match result

Обратите внимание на появившуюся кнопку Graph. Щёлкним на ней и увидим наши узлы в графическом виде:

Neo4j Browser: graph match result

Примечание. В версии 3.4 по-умолчанию как-раз открывается графическое представление. Для получения табличного представления нужно щёлкнуть на кнопку с надписью "Table". Хотя, бывает и наоборот.

Добавим связь между узлами:

MATCH (e:Person) WHERE e.name = "Evgeny" MATCH (d:Person) WHERE d.name = "Dmitry" CREATE (e)-[:KNOWS]->(d), (d)-[:KNOWS]->(e)

И вновь запросим наш граф:

Neo4j Browser: graph with relationships

С помощью Cypher можно также выполнять различные операции над графами, например, запрашивать смежные вершины, друзей друзей в социальном графе, удалять рёбра и вершины и многое другое, но это тема для отдельного разговора.

Также можно настраивать Neo4j Browser на различный стиль отображения узлов и связей в зависимости от заданных им меток.

Ссылки

  • Neo4j Browser User Interface Guide
  • The Neo4j Developer Manual v3.4
  • The Neo4j Developer Manual v3.3
  • Neo4j Graph Database Sandbox
  • Neo4j GraphGists

Браузер не поддерживается

Вы используете браузер, который Facebook не поддерживает. Чтобы все работало, мы перенаправили вас в упрощенную версию.

Разрешить использование файлов cookie от Facebook в этом браузере?

Мы используем файлы cookie и подобные технологии, чтобы показывать и улучшать контент в Продукты Meta, обеспечивать безопасный пользовательский опыт с помощью информации, полученной из файлов cookie от Facebook и других источников, а также чтобы предоставлять и улучшать Продукты Meta для зарегистрированных пользователей.

  • Основные файлы cookie. Эти файлы cookie необходимы для использования Продуктов Meta и корректной работы наших сайтов.
  • Файлы cookie от других компаний. Мы используем эти файлы cookie, чтобы показывать вам рекламу вне Продуктов Meta и предоставлять такие функции, как карты и видео, в Продуктах Meta. Эти файлы cookie не обязательны.

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

Информация о файлах cookie
Что такое файлы cookie?

Файлы cookie — это небольшие текстовые файлы, которые используются для хранения и получения идентификаторов в браузере. Мы используем файлы cookie и аналогичные технологии, чтобы предоставлять вам Продукты Meta и анализировать полученную информацию о пользователях (например, сведения об их действиях на других сайтах и в других приложениях).

Если у вас нет аккаунта, мы не используем файлы cookie, чтобы персонализировать для вас рекламу. Информация о ваших действиях, которую мы получаем, будет использоваться только для обеспечения безопасности и целостности наших Продуктов.

Больше информации о файлах cookie и аналогичных используемых технологиях — в нашей Политика в отношении файлов cookie.

Почему мы используем файлы cookie?

Файлы cookie помогают нам предоставлять, защищать и улучшать Продукты Meta, например с помощью персонализации контента, подбора и оценки рекламы, а также повышения безопасности их использования.

Хотя файлы cookie, которые мы используем, могут время от времени меняться по мере улучшения и обновления Продуктов Meta, мы используем их для следующих целей:

  • Выполнять аутентификацию, чтобы пользователям не приходилось входить каждый раз повторно
  • Чтобы обеспечивать безопасность и целостность сайтов и продуктов
  • Чтобы предоставлять рекламу, рекомендации, статистическую информацию и результаты измерений
  • Чтобы предоставлять функции и услуги на сайтах
  • Чтобы оценивать производительность наших Продуктов
  • Чтобы обеспечивать возможность анализа данных и проведения исследований
  • На сторонних сайтах и в сторонних приложениях — чтобы помочь компаниям, использующим платформы Meta, делиться с нами информацией о действиях в их приложениях и на их сайтах.

Узнайте подробнее, что такое файлы cookie и как мы их используем, в нашей Политика в отношении файлов cookie.

Что такое Продукты Meta?

Продукты Meta включают в себя приложения Facebook, Instagram и Messenger, а также любые другие функции, приложения, технологии, программное обеспечение или услуги, предлагаемые Meta в соответствии с нашей Политикой конфиденциальности.

Подробнее о Продукты Meta в нашей Политике конфиденциальности.
Ваши настройки файлов cookie
Вы можете контролировать, какие необязательные файлы cookie мы будем использовать:

  • Мы можем использовать наши файлы cookie в других приложениях и на других сайтах, принадлежащих компаниям, которые используют возможности платформ Meta (например, кнопку "Нравится" и пиксель Meta), для персонализации показываемой вам рекламы.
  • Мы используем файлы cookie от других компаний, чтобы показывать вам рекламу вне Продуктов Meta и предоставлять такие функции, как карты и видео, в Продуктах Meta.

Вы в любое время можете проверить или изменить свои параметры в настройках файлов cookie.
Файлы cookie других компаний

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

Как мы используем эти файлы cookie
Мы используем файлы cookie от других компаний в наших Продуктах:

  • Чтобы рекламировать наши Продукты и функции в приложениях и на сайтах других компаний.
  • Для предоставления таких функций наших Продуктов Meta, как карты, платежные услуги и видео.
  • Для аналитики.

Если вы разрешите использовать эти файлы cookie

  • Это не повлияет на работу используемых вами функций в Продуктах Meta.
  • Мы сможем лучше персонализировать для вас рекламу вне Продуктов Meta и измерять ее результативность.
  • Другие компании будут получать информацию о вас с помощью своих файлов cookie.

Если вы не разрешите использовать эти файлы cookie

  • Некоторые функции в наших продуктах могут не работать.
  • Мы не будем использовать файлы cookie других компаний для персонализации рекламы вне Продуктов Meta или измерения ее результативности.

Другие способы управления вашими данными
Управляйте показом рекламы в Центре аккаунтов

С помощью этих инструментов пользователи, у которых есть аккаунт Facebook, могут управлять использованием различных данных для персонализации рекламы.

Настройки рекламы

Для более точного подбора рекламы мы используем данные о ваших действиях вне Продуктов компаний Meta, полученные от рекламодателей и других партнеров. Сюда входят сведения о том, как вы используете сайты и приложения. В настройках рекламы вы можете включить или выключить использование ваших данных для подбора рекламы.

Meta Audience Network — это инструмент, который позволяет рекламодателям показывать вам рекламу в приложениях и на сайтах вне Продуктов компаний Meta. Для подбора актуальной рекламы Audience Network использует ваши рекламные предпочтения, которые помогают нам определить, какая реклама вас заинтересует. Вы можете управлять ими в настройках рекламы.

Рекламные предпочтения

В разделе "Рекламные предпочтения" можно разрешить или запретить показ рекламы, а также определить, какую информацию мы можем использовать для показа рекламы.

Действия вне Facebook

Вы можете просмотреть перечень ваших действий вне Facebook. Он представляет собой сводку полученных от компаний и организаций данных о ваших взаимодействиях с ними. Например, это могут быть сведения о посещении их приложений или сайтов. Они передают нам эту информацию с помощью наших инструментов для бизнеса, таких как вход через Facebook и пиксель Meta. Среди прочего, это помогает нам персонализировать ваш опыт использования Продуктов Meta. Узнайте подробнее, что такое действия вне Facebook, как мы используем сведения о них и какие инструменты управления вам доступны.

Подробнее об онлайн-рекламе

Вы можете отказаться от онлайн-рекламы на основе интересов на сайте Альянса цифровой рекламы в США, Альянса цифровой рекламы Канады, если вы находитесь в Канаде, и Европейского альянса интерактивной цифровой рекламы — в ЕС. Таким способом можно заблокировать показ рекламы, которую для вас подбирает Meta и другие компании, состоящие в указанных организациях. Настройки для отказа от персонализированной рекламы также доступны на мобильных устройствах с Android, iOS 13 или более ранней версией iOS. Важно! Средства блокировки рекламы или ограничения использования файлов cookie могут препятствовать действию описанных здесь инструментов управления.

Большинство рекламодателей, с которыми мы сотрудничаем, используют файлы cookie и подобные технологии для обеспечения корректной работы своих сервисов. Чтобы узнать подробнее, как рекламодатели обычно используют файлы cookie и какой выбор в связи с этим предлагается пользователям, см. следующие ресурсы:

  • Альянс цифровой рекламы
  • Альянс цифровой рекламы Канады
  • Европейский альянс интерактивной цифровой рекламы

Управление использованием файлов cookie c помощью настроек браузера

В браузере или на устройстве, которые вы используете, могут быть настройки, позволяющие задать параметры использования файлов cookie и удалить их. Эти настройки различаются в разных браузерах, и производители могут в любой момент изменить набор предлагаемых параметров и то, как они работают. Начиная с 5 октября 2020 г. по ссылкам ниже можно найти дополнительную информацию о настройках, доступных в популярных браузерах. Если в браузере заблокировано использование файлов cookie, некоторые функции Продуктов Meta могут работать некорректно. Обратите внимание, что настройки браузеров отличаются от настроек Facebook.

  • Google Chrome
  • Internet Explorer
  • Firefox
  • Safari
  • Safari для мобильных устройств
  • Opera

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *