Статьи

Почему вы должны использовать Neo4j в вашем следующем приложении Ruby

neo4j-logo-2015 (1)

Мне нужно было хранить много данных в свое время, и я использовал много крупных соперников: PostgreSQL, MySQL, SQLite, Redis и MongoDB. Хотя я приобрел большой опыт работы с этими инструментами, я бы не сказал, что кто-либо из них когда-либо делал эту задачу увлекательной. Я влюбился в Руби, потому что это было весело и потому что это позволяло мне делать более сильные вещи, не мешая мне. Хотя я не осознавал этого, мне мешали обычные подозреваемые в сохранности данных. Но я нашел новую любовь: позвольте мне рассказать вам о Neo4j .

Что такое Neo4j?

Neo4j — это графическая база данных! Это означает, что он оптимизирован для управления и запроса соединений ( отношений ) между сущностями ( узлами ) в отличие от чего-то вроде реляционной базы данных, которая использует таблицы.

Почему это здорово? Представьте себе мир без внешних ключей. Каждая сущность в вашей базе данных может иметь много связей, относящихся непосредственно к другим сущностям. Если вы хотите исследовать отношения, то нет сканирования таблиц или индексов, просто следуйте нескольким соединениям. Это хорошо согласуется с типичной объектной моделью. Однако он более мощный, потому что Neo4j, предоставляя ожидаемую нами большую функциональность базы данных, дает нам инструменты для запроса сложных шаблонов в наших данных.

Представляем ActiveNode

Для подключения к Neo4j мы будем использовать гем neo4j . Инструкции по подключению к Neo4j вы можете найти в приложении Rails в документации к гему . Также в этом репозитории GitHub доступно приложение с кодом, показанным ниже, в качестве запущенного приложения Rails (используйте ветку Git sitepoint ). Когда ваша база данных будет запущена и запущена, используйте команду rake load_sample_data чтобы заполнить вашу базу данных.

Вот базовый пример модели Asset из приложения Rails для управления активами:

app/models/asset.rb

 class Asset include Neo4j::ActiveNode property :title has_many :out, :categories, type: :HAS_CATEGORY end 

Давайте разберем это:

  • neo4j дает нам модуль Neo4j::ActiveNode , который мы include для создания модели.
  • Имя класса Asset означает, что эта модель будет отвечать за все узлы в Neo4j, помеченные как Asset (метки играют роль, аналогичную именам таблиц, за исключением того, что узел может иметь много меток).
  • У нас есть свойство title для описания отдельных узлов
  • У нас есть исходящая ассоциация has_many для categories . Эта связь помогает нам находить объекты Category , следуя отношениям HAS_CATEGORY в базе данных.

С помощью этой модели мы можем выполнить основной запрос, чтобы найти актив и получить его категории:

 2.2.0 :001 > asset = Asset.first => #<Asset uuid: "0098d2b7-a577-407a-a9f2-7ec4153cfa60", title: "ICC World Cup 2015 "> 2.2.0 :002 > asset.categories.to_a => [#<Category uuid: "91cd5369-605c-4aff-aad1-b51d8aa9b5f3", name: "Classification">] 

Любой, кто знаком с ActiveRecord или Mongoid , видел это сотни раз. Чтобы стать немного интереснее, давайте определим модель Category :

 class Category include Neo4j::ActiveNode property :name has_many :in, :assets, origin: :categories end 

Здесь наша ассоциация имеет опцию origin чтобы ссылаться на ассоциацию categories в модели Asset . Вместо этого мы могли бы указать type: :HAS_CATEGORY снова, если бы захотели.

Создание рекомендаций

Что если бы мы хотели получить все активы, которые делят категорию с нашим активом?

 2.2.0 :003 > asset.categories.assets.to_a => [#<Asset uuid: "d2ef17b5-4dbf-4a99-b814-dee2e96d4a09", title: "WineGraph">, ...] 

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

 MATCH asset436, asset436-[rel1:`HAS_CATEGORY`]->(node3:`Category`), node3<-[rel2:`HAS_CATEGORY`]-(result_assets:`Asset`) WHERE (ID(asset436) = {ID_asset436}) RETURN result_assets Parameters: {ID_asset436: 436} 

Это язык запросов, называемый Cypher , который является эквивалентом Neo4j для SQL. Обратите особое внимание на художественный стиль ASCII скобок, окружающих определения узлов и стрелки, представляющие отношения. Этот Cypher-запрос немного более подробный, потому что ActiveNode генерирует его алгоритмически. Если бы человек написал запрос, он бы выглядел примерно так:

 MATCH source_asset-[:HAS_CATEGORY]->(:Category)<-[:HAS_CATEGORY]-(result_assets:Asset) WHERE ID(source_asset) = {source_asset_id} RETURN result_assets Parameters: {source_asset_id: 436} 

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

Как видите, мы можем использовать Neo4j для охвата наших объектов. Большое дело! Мы также можем сделать это в SQL с помощью пары JOINS . В то время как Сайфер кажется крутым, мы еще не разбираемся. Что если мы захотим использовать этот запрос, чтобы дать рекомендации по активам на основе общих категорий? Мы хотим отсортировать активы, чтобы ранжировать те из них, которые имеют наибольшее количество общих категорий. Давайте создадим метод на нашей модели:

 class Asset ... Recommendation = Struct.new(:asset, :categories, :score) def asset_recommendations_by_category(common_links_required = 3) categories(:c) .assets(:asset) .order('count(c) DESC') .pluck('asset, collect(c), count(c)').reject do |_, _, count| count < common_links_required end.map do |other_asset, categories, count| Recommendation.new(other_asset, categories, count) end end end 

Здесь есть несколько интересных вещей:

  • Мы определяем переменные как часть нашей цепочки для последующего использования ( c и asset ).
  • Мы используем функцию collect Cypher, чтобы получить столбец результатов, содержащий массив общих категорий (см. Таблицу ниже). Также обратите внимание, что мы получаем полные объекты, а не только столбцы / свойства:
актив Collect (с) кол (с)
# <Объект> [# <Категория>] 1
# <Объект> [# <Category>, # <Category>,…] 4
# <Объект> [# <Category>, # <Category>] 2

Вы заметили, что нет предложения GROUP BY ? Neo4j достаточно умен, чтобы понять, что collect и count являются функциями агрегации, и он группирует по столбцам неагрегирования в нашем результате (в данном случае это просто переменная asset ).

Возьми этот SQL!

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

В дополнение к общим категориям, давайте учтем, как много общего у создателей и зрителей:

 class Asset ... Recommendation = Struct.new(:asset, :score) def secret_sauce_recommendations query_as(:source) .match('source-[:HAS_CATEGORY]->(category:Category)<-[:HAS_CATEGORY]-(asset:Asset)').break .optional_match('source<-[:CREATED]-(creator:User)-[:CREATED]->asset').break .optional_match('source<-[:VIEWED]-(viewer:User)-[:VIEWED]->asset') .limit(5) .order('score DESC') .pluck( :asset, '(count(category) * 2) + (count(creator) * 4) + (count(viewer) * 0.1) AS score').map do |other_asset, score| Recommendation.new(other_asset, score) end end end 

Здесь мы углубимся и начнем формировать наш собственный запрос. Структура та же, но вместо того, чтобы искать только один путь между двумя активами через общую категорию, мы также указываем еще два дополнительных пути. Мы можем сделать все три пути необязательными, но тогда Neo4j нужно будет сравнить наш актив с любым другим активом в базе данных. Используя match а не optional_match для нашего пути через узлы Category мы требуем, чтобы была хотя бы одна общая категория. Это значительно ограничивает наше пространство поиска.

На диаграмме есть одна общая категория, ноль общих создателей и два общих зрителя. Это означает, что счет между «Ruby» и «Ruby on Rails» будет:

 (1 * 2) + (0 * 4) + (2 * 0.1) = 2.2 

Также обратите внимание, что мы выполняем расчет (и сортировку) для агрегации count этих трех путей. Это так здорово для меня, что заставляет меня немного покалывать, чтобы думать об этом …

Простая авторизация

Давайте рассмотрим еще одну распространенную проблему. Предположим, ваш генеральный директор приходит к вам за столом и говорит: «Мы создали отличное приложение, но клиенты хотят иметь возможность контролировать, кто их видит. Не могли бы вы встроить некоторые элементы управления конфиденциальностью? »Это кажется достаточно простым. Давайте просто добавим флаг, чтобы разрешить частные активы:

 class Asset ... property :public, default: true def self.visible_to(user) query_as(:asset) .match_nodes(user: user) .where("asset.public OR asset<-[:CREATED]-user") .pluck(:asset) end end 

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

Менеджер по продукту приходит к вам и говорит: «Привет, спасибо за это, но теперь люди хотят иметь возможность предоставить другим пользователям прямой доступ к их личным материалам». Нет проблем! Вы можете создать пользовательский интерфейс, чтобы пользователи могли добавлять и удалять отношения VIEWABLE_BY для своих активов, а затем запрашивать их следующим образом:

 class Asset ... def self.visible_to(user) query_as(:asset) .match_nodes(user: user) .where("asset.public OR asset<-[:CREATED]-user OR asset-[:VIEWABLE_BY]->user") .pluck(:asset) end end 

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

Удовлетворенный работой дня, вы откидываетесь на спинку стула и пьете свой послеобеденный кофе. Конечно, именно тогда представитель службы поддержки клиентов в социальных сетях говорит: «Пользователям нравится новая функция, но они хотят иметь возможность создавать группы и назначать доступ к группам. Вы можете сделать это? О, кроме того, не могли бы вы допустить произвольную иерархию групп? »Вы несколько секунд смотрите в их глаза, прежде чем ответить:« Конечно! ». Поскольку это начинает усложняться, давайте рассмотрим пример:

Если оба ресурса являются частными, ваш код пока предоставляет Matz и tenderlove доступ к Ruby и DHH к Ruby on Rails. Чтобы добавить поддержку групп, вы начинаете со следующих непосредственно назначенных групп:

 class Asset ... def self.visible_to(user) query_as(:asset) .match_nodes(user: user) .where("asset.public OR asset<-[:CREATED]-user OR asset-[:VIEWABLE_BY]->user OR asset-[:VIEWABLE_BY]->(:Group)<-[:BELONGS_TO]-user") .pluck('DISTINCT asset') end end 

Это было довольно легко, так как вам просто нужно было добавить другой путь. Конечно, это два прыжка, но это уже старая шляпа для нас. Tenderlove и Yehuda смогут увидеть актив «Ruby on Rails», потому что они являются членами группы «Railsists». Также обратите внимание: теперь, когда некоторые пользователи имеют несколько путей к активу (например, от Matz к Ruby через группу Rubyists и через отношение CREATED ), вам необходимо вернуть DISTINCT asset .

Однако указание произвольного пути через иерархию групп занимает немного больше времени. Вы просматриваете документацию Neo4j, пока не найдете то, что называется «переменные отношения», и сделаете это:

 class Asset ... def self.visible_to(user) query_as(:asset) .match_nodes(user: user) .where("asset.public OR asset<-[:CREATED]-user OR asset-[:VIEWABLE_BY]->user OR asset-[:VIEWABLE_BY]->(:Group)<-[:HAS_SUBGROUP*0..5]-(:Group)<-[:BELONGS_TO]-user") .pluck('DISTINCT asset') end end 

Вот ты это сделал! Этот запрос найдет активы, доступные для группы, и HAS_SUBGROUP любой набор от нуля до пяти отношений HAS_SUBGROUP , в конце концов заканчивая проверку, чтобы увидеть, находится ли пользователь в последней группе. Вы герой этой истории, и ваша компания предлагает вам бонусы за то, что вы сделали работу так быстро!

Вывод

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