Статьи

Эффективные пользовательские временные шкалы в приложении PHP с Neo4j

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

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

В этом руководстве мы собираемся расширить демонстрационное приложение, используемое в двух вводных статьях о Neo4j и PHP соответственно:

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

Вы откроете для себя особую технику моделирования под названием « Связанный список» и несколько сложных запросов с помощью Cypher.

Исходный код этой статьи можно найти в собственном репозитории Github .

Моделирование временной шкалы в графической базе данных

Люди, которые привыкли к другим методам моделирования баз данных, склонны связывать каждый пост с пользователем. Сообщение будет иметь свойство timestamp, и порядок сообщений будет сделан для этого свойства.

Вот простое представление:

Обычный пользователь - /> отношение публикации ”title =” ”> </ p> <p> Хотя такая модель будет работать без проблем, у нее есть некоторые недостатки: </ p> <ul> <li> Для каждого пользователя вам нужно будет упорядочить его посты по времени, чтобы получить последний </ li> <li> Операция заказа будет расти линейно с количеством постов и пользователей, за которыми вы следите </ li> <li> Это заставит базу данных выполнить операции для заказа </ li> </ ul> <h3 id = Используйте мощь графической базы данных

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

Общий метод моделирования для пользовательских каналов называется Связанный список . В нашем приложении пользовательский узел будет иметь отношение с именем LAST_POST к последнему сообщению, созданному пользователем. Этот пост будет иметь отношение PREVIOUS_POST к предыдущему, который также имеет PREVIOUS_POST ко второму предыдущему сообщению и т. Д., И т. Д.

Linked list

Благодаря этой модели вы получаете немедленный доступ к последнему сообщению пользователя. На самом деле вам даже не нужно иметь временную метку, чтобы получить ее временную шкалу (хотя мы будем ее сохранять, чтобы сортировать сообщения по разным пользователям).

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

Начальная настройка

Я предлагаю вам скачать репозиторий, использованный для вводных статей, и переименовать его в социальную хронологию, например:

git clone [email protected]:sitepoint-editors/social-network mv social-network social-timeline cd social-timeline rm -rf .git composer install bower install 

Как и в предыдущих статьях, мы собираемся загрузить базу данных сгенерированным фиктивным набором данных с помощью Graphgen .

Вам понадобится работающая база данных (локальная или удаленная), перейдите по этой ссылке , нажмите «Создать», а затем «Заполните свою базу данных».

Если вы используете Neo4j 2.2, вам нужно будет neo4j имя пользователя neo4j и ваш пароль в графе popgen:

Graphgen population

Это позволит импортировать 50 пользователей с логином, именем и фамилией. У каждого пользователя будет два сообщения в блоге, одно с отношением LAST_POST к пользователю и одно с отношением PREVIOUS_POST к другому фиду.

Если вы сейчас откроете браузер Neo4j, вы увидите, как моделируются пользователи и сообщения:

Neo4j users and posts relationships

Отображение пользовательских каналов

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

Пользователь кормит маршрут

Сначала мы добавим маршрут для отображения каналов конкретного пользователя. Добавьте эту часть кода в конец файла web/index.php

 $app->get('/users/{user_login}/posts', 'Ikwattro\\SocialNetwork\\Controller\\WebController::showUserPosts') ->bind('user_post'); 

Пользователь кормит контроллер и запрос Cypher

Мы сопоставим маршрут с действием в файле src/Controller/WebController.php .

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

 public function showUserPosts(Application $application, Request $request) { $login = $request->get('user_login'); $neo = $application['neo']; $query = 'MATCH (user:User) WHERE user.login = {login} MATCH (user)-[:LAST_POST]->(latest_post)-[PREVIOUS_POST*0..2]->(post) RETURN user, collect(post) as posts'; $params = ['login' => $login]; $result = $neo->sendCypherQuery($query, $params)->getResult(); if (null === $result->get('user')) { $application->abort(404, 'The user $login was not found'); } $posts = $result->get('posts'); return $application['twig']->render('show_user_posts.html.twig', array( 'user' => $result->getSingle('user'), 'posts' => $posts, )); } 

Некоторые объяснения:

  • Сначала мы MATCH пользователя по его логину.
  • Затем мы MATCH последний канал пользователя и расширяемся до PREVIOUS_FEED (использование глубины отношения *0..2 будет влиять на встраивание узла latest_post в коллекцию узлов post), и мы ограничиваем максимальную глубину до 2.
  • Мы возвращаем найденные каналы в коллекцию.

Отображение каналов в шаблоне

Сначала мы добавим ссылку в профиле пользователя для доступа к его каналам, просто добавив эту строку после в конце блока информации о пользователе:

 <p><a href="{{ path('user_post', {user_login: user.property('login') }) }}">Show posts</a></p> 

Теперь мы создадим наш шаблон, показывающий временную шкалу пользователя (сообщения). Мы устанавливаем заголовок и цикл, повторяющий нашу коллекцию каналов для отображения их в выделенном HTML-элементе div:

 {% extends "layout.html.twig" %} {% block content %} <h1>Posts for {{ user.property('login') }}</h1> {% for post in posts %} <div class="row"> <h4>{{ post.properties.title }}</h4> <div>{{ post.properties.body }}</div> </div> <hr/> {% endfor %} {% endblock %} 

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

A user's feed

Отображение временной шкалы

Если вы импортировали образец набора данных с помощью Graphgen, каждый из ваших пользователей будет подписываться примерно на 40 других пользователей.

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

Когда вы получите все эти сообщения, вам нужно отфильтровать их по времени, чтобы упорядочить их между пользователями.

Пользовательский график маршрута

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

Добавьте маршрут в файл web/index.php

 $app->get('/user_timeline/{user_login}', 'Ikwattro\\SocialNetwork\\Controller\\WebController::showUserTimeline') ->bind('user_timeline'); 

Действие контроллера:

 public function showUserTimeline(Application $application, Request $request) { $login = $request->get('user_login'); $neo = $application['neo']; $query = 'MATCH (user:User) WHERE user.login = {user_login} MATCH (user)-[:FOLLOWS]->(friend)-[:LAST_POST]->(latest_post)-[:PREVIOUS_POST*0..2]->(post) WITH user, friend, post ORDER BY post.timestamp DESC SKIP 0 LIMIT 20 RETURN user, collect({friend: friend, post: post}) as timeline'; $params = ['user_login' => $login]; $result = $neo->sendCypherQuery($query, $params)->getResult(); if (null === $result->get('user')) { $application->abort(404, 'The user $login was not found'); } $user = $result->getSingle('user'); $timeline = $result->get('timeline'); return $application['twig']->render('show_timeline.html.twig', array( 'user' => $result->get('user'), 'timeline' => $timeline, )); } 

Пояснения к запросу:

  • Сначала мы сопоставляем нашего пользователя.
  • Затем мы сопоставляем путь между этим пользователем, другими пользователями, за которыми он следует, и их последним потоком (см. Здесь, как Cypher действительно выразителен в том, что вы хотите получить).
  • Мы заказываем каналы по их отметке времени.
  • Мы возвращаем фиды в коллекции, содержащие автора и фид.
  • Мы ограничиваем результат до 20 каналов.

Добавьте ссылку на шаблон профиля пользователя сразу после ссылки на ленту пользователей:

 <p><a href="{{ path('user_timeline', {user_login: user.property('login') }) }}">Show timeline</a></p> 

И создайте шаблон временной шкалы:

 % extends "layout.html.twig" %} {% block content %} <h1>Timeline for {{ user.property('login') }}</h1> {% for friendFeed in timeline %} <div class="row"> <h4>{{ friendFeed.post.title }}</h4> <div>{{ friendFeed.post.body }}</div> <p>Written by: {{ friendFeed.friend.login }} on {{ friendFeed.post.timestamp | date('Ymd H:i:s') }}</p> </div> <hr/> {% endfor %} {% endblock %} 

Теперь у нас есть довольно крутая временная шкала, показывающая последние 20 каналов людей, за которыми вы следите, которые эффективны для базы данных.

Timeline implemented

Добавление поста во временную шкалу

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

Просто не так ли? Пошли!

Как обычно, мы создадим POST-маршрут для формы, указывающей на действие WebController:

 $app->post('/new_post', 'Ikwattro\\SocialNetwork\\Controller\\WebController::newPost') ->bind('new_post'); 

Далее мы добавим базовую HTML-форму для вставки заголовка и текста сообщения в шаблон пользователя:

 #show_user.html.twig <div class="row"> <div class="col-sm-6"> <h5>Add a user status</h5> <form id="new_post" method="POST" action="{{ path('new_post') }}"> <div class="form-group"> <label for="form_post_title">Post title:</label> <input type="text" minLength="3" name="post_title" id="form_post_title" class="form-control"/> </div> <div class="form-group"> <label for="form_post_body">Post text:</label> <textarea name="post_body" class="form-control"></textarea> </div> <input type="hidden" name="user_login" value="{{ user.property('login') }}"/> <button type="submit" class="btn btn-success">Submit</button> </form> </div> </div> 

И, наконец, мы создаем наше действие newPost :

 public function newPost(Application $application, Request $request) { $title = $request->get('post_title'); $body = $request->get('post_body'); $login = $request->get('user_login'); $query = 'MATCH (user:User) WHERE user.login = {user_login} OPTIONAL MATCH (user)-[r:LAST_POST]->(oldPost) DELETE r CREATE (p:Post) SET p.title = {post_title}, p.body = {post_body} CREATE (user)-[:LAST_POST]->(p) WITH p, collect(oldPost) as oldLatestPosts FOREACH (x in oldLatestPosts|CREATE (p)-[:PREVIOUS_POST]->(x)) RETURN p'; $params = [ 'user_login' => $login, 'post_title' => $title, 'post_body' => $body ]; $result = $application['neo']->sendCypherQuery($query, $params)->getResult(); if (null !== $result->getSingle('p')) { $redirectRoute = $application['url_generator']->generate('user_post', array('user_login' => $login)); return $application->redirect($redirectRoute); } $application->abort(500, sprintf('There was a problem inserting a new post for user "%s"', $login)); } 

Некоторые объяснения:

  • Сначала мы сопоставляем пользователя, а затем необязательно сопоставляем его узел LAST_POST .
  • Мы удаляем отношения между пользователем и его последним последним постом.
  • Мы создаем наш новый пост (который фактически является его последним постом в реальной жизни).
  • Мы создаем отношения между пользователем и его «новым» последним постом.
  • Мы разбиваем запрос и передаем пользователю, последний пост и коллекцию его старых latest_posts.
  • Затем мы перебираем коллекцию и создаем связь PREVIOUS_POST между новым последним постом и следующим.

Сложность в том, что коллекция oldLatestPosts всегда будет содержать 0 или 1 элемент, что идеально подходит для нашего запроса.

New post form

Вывод

В этой статье мы обнаружили метод моделирования под названием «Связанный список», узнали, как реализовать его в социальном приложении и как эффективно извлекать узлы и отношения. Мы также изучили некоторые новые предложения Cypher, такие как SKIP и LIMIT, полезные для нумерации страниц.

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