У попередньому блозі ми знайомились із найпопулярнішою графовою базою даних Neo4j. Сьогодні ми створимо аплікейшен, який буде вертати дані з Neo4j (найближчі аеропорти за вказаними координатами) у форматі json. Для аплікейшену використаємо візьмемо фреймворк Phalcon. У нашому прикладі Neo4j містить дані про 8 тис. аеропортів та просторовий індекс для цих аеропортів. Для кешування ми візьмемо Redis.
Phalcon
Phalcon — це один з найшвидших PHP фреймворків. Всі компоненти написані на C, є підтримка MVC, ORM, доступний шаблонізатор Volt (схожий на Jinja).
Інструкція для встановлення (Ubuntu):
sudo apt-add-repository ppa:phalcon/stable
sudo apt-get update
sudo apt-get install php5-phalcon
детальніше тут — https://phalconphp.com/en/download
Далі створюємо сам аплікейшен.
Базова структура:
route-finder.loc
── app
│ ├── controllers
│ ├── models
│ └── views
└── public
├── css
├── img
└── js
Створюємо .htaccess файли у корені аплікейшену і у папці public. Далі створюємо index.php у папці public (не забуваємо змінити base URI). Створюємо контролер app/controllers/IndexController.php, як у прикладі. Якщо все зробили вірно, то за адресою localhost/route-finder.loc має бути таке:
Наразі у нас є сторінка, спробуємо вивести на цю сторінку якусь інформацію з Neo4j, для цього за допомогою composer встановимо бібліотеку neo4jphp:
У консолі (корінь аплікейшену) вводимо:
composer require "everyman/neo4jphp" "dev-master"
На цьому етапі структура аплікейшену має виглядати так:
route-finder.loc
├── app
│ ├── controllers
│ │ └── IndexController.php
│ ├── models
│ └── views
├── composer.json
├── composer.lock
├── composer.phar
├── public
│ ├── css
│ ├── img
│ ├── index.php
│ └── js
└── vendor
...
Файл IndexController.php виглядатиме так:
<?php use Phalcon\Mvc\Controller; class IndexController extends Controller { public function indexAction() { echo "<h1>Hello!</h1>"; } }
Змінимо його наступним чином:
<?php require_once __DIR__. '/../../vendor/autoload.php'; use Phalcon\Mvc\Controller; class IndexController extends Controller { public function indexAction() { $client = new Everyman\Neo4j\Client('localhost', 7474); $this->view->disable(); //Create a response instance $response = new \Phalcon\Http\Response(); //Set the content of the response $response->setContent(json_encode($client->getServerInfo())); $response->setContentType('application/json', 'UTF-8'); //Return the response return $response; } }
Пояснення щодо коду:
require_once __DIR__. '/../../vendor/autoload.php'; — підключаємо автолоад;
$client = new Everyman\Neo4j\Client('localhost', 7474); — конектимось до Neo4j. Якщо нам потрібно брати дані з Neo4j і в інших роутах, тоді краще заінжектити Neo4j як сервіс (тоді доступ буде у всіх роутах);
$this->view->disable(); — в’юшка не потрібна, оскільки контролер сам вертатиме відповідь;
$response->setContent(json_encode($client->getServerInfo())); — вернемо щось з Neo4j;
$response->setContentType('application/json', 'UTF-8'); — вернемо як json.
Тепер головна сторінка виглядатиме так:
У нас є Neo4j база з аеропортами і html сторінка, яка відсилатиме аплікейшену довготу і широту, за якими потрібно знайти 5 найближчих аеропортів. Запит відсилатиметься у форматі /ajax/airports?lat=50.433333&lng=30.516667&limit=5
Очікувана відповідь:
Для початку створимо route, для цього у файл index.php додамо наступне:
$di->set('router', function () { $router = new \Phalcon\Mvc\Router(); $router->add("/ajax/airports", array( 'controller' => 'airports', 'action' => 'index', )); return $router; });
Index.php тепер виглядатиме так:
<?php use Phalcon\Loader; use Phalcon\Mvc\View; use Phalcon\Mvc\Application; use Phalcon\DI\FactoryDefault; use Phalcon\Mvc\Url as UrlProvider; use Phalcon\Db\Adapter\Pdo\Mysql as DbAdapter; try { // Register an autoloader $loader = new Loader(); $loader->registerDirs(array( '../app/controllers/', '../app/models/' ))->register(); // Create a DI $di = new FactoryDefault(); // Setup the view component $di->set('view', function () { $view = new View(); $view->setViewsDir('../app/views/'); return $view; }); // Setup a base URI so that all generated URIs include the "route-finder.loc" folder $di->set('url', function () { $url = new UrlProvider(); $url->setBaseUri('/route-finder.loc/'); return $url; }); $di->set('router', function () { $router = new \Phalcon\Mvc\Router(); $router->add("/ajax/airports", array( 'namespace' => 'RouteFinder', 'controller' => 'airports', 'action' => 'index', )); return $router; }); // Handle the request $application = new Application($di); echo $application->handle()->getContent(); } catch (\Exception $e) { echo "PhalconException: ", $e->getMessage(); }
Далі створимо копію файла IndexController.php і назвемо її AirportsController.php у папці app/controllers. Якщо все зробили вірно, то сторінка /ajax/airports виглядатиме аналогічно головній.
Тепер скопіюємо туди наступний код:
<?php require_once __DIR__. '/../../vendor/autoload.php'; use Phalcon\Mvc\Controller; use Everyman\Neo4j\Client; use Everyman\Neo4j\Cypher; header('Access-Control-Allow-Origin: *'); function _distance($lat1, $lon1, $lat2, $lon2) { $theta = $lon1 - $lon2; $dist = sin(deg2rad($lat1)) * sin(deg2rad($lat2)) + cos(deg2rad($lat1)) * cos(deg2rad($lat2)) * cos(deg2rad($theta)); $dist = acos($dist); $dist = rad2deg($dist); return $dist * 111.18957696; } class AirportsController extends Controller { public function indexAction() { $client = new Client('localhost', 7474); $lat = number_format($this->request->getQuery("lat", "float", 0.0), 6); $lng = number_format($this->request->get('lng', "float", 0.0), 6); $result = array(); $query = sprintf( "START n=node:geom('withinDistance:[%s, %s, %s]') MATCH n-[r:AVAILABLE_DESTINATION]->() RETURN DISTINCT n SKIP %s LIMIT %s", $lat, $lng, number_format($this->request->get('distance', "float", 500), 1), $this->request->get('offset', "int", 0), $this->request->get('limit', "int", 5) ); $query = new Cypher\Query($client, $query); $query = $query->getResultSet(); foreach ($query as $row) { $item = array( 'id' => $row['n']->getId(), 'distance' => _distance( $row['n']->getProperties()['latitude'], $row['n']->getProperties()['longitude'], $lat, $lng ), ); foreach ($row['n']->getProperties() as $key => $value) { $item[$key] = $value; } $result[] = $item; } $response = new \Phalcon\Http\Response(); //Set the content of the response $response->setContent(json_encode(array('json_list' => $result))); $response->setContentType('application/json', 'UTF-8'); return $response; } }
Пояснення щодо коду:
header('Access-Control-Allow-Origin: *'); — дозволяємо крос-доменні запити;
function _distance($lat1, $lon1, $lat2, $lon2) — допоміжна функція для обчислення відстані за заданими координатами;
$lat = number_format($this->request->getQuery("lat", "float", 0.0), 6); — зберігаємо query параметр “lat” у змінну, крім цього, форматуємо як число з 6 знаками після коми, оскільки для Neo4j вимагається, щоб числа з плаваючою комою мали хоча б 1 знак після коми;
$query = sprintf( — тут ми прописуємо шаблон для запиту до Neo4j; використовуючи мову запитів Cypher (детальніше про Neo4j та Cypher можна почитати у попередній статті). Цим запитом ми знаходимо ноди, які лежать найближче до заданих координат (для цього встановлений SpatialPlugin для Neo4j та створений відповідний індекс);
$query = new Cypher\Query($client, $query); — відсилаємо запит;
$query = $query->getResultSet(); — забираємо результати;
foreach ($query as $row) { — тут ми форматуємо результат;
'id' => $row['n']->getId(), — форматуємо id ноди за допомогою методу getId();
'distance' => _distance( — знаходимо відстань між поточним аеропортом і заданою точкою за допомогою допоміжної функції оголошеної вище;
foreach ($row['n']->getProperties() as $key => $value) { — поміщуємо кожну доступну властивість ноди у масив.
Таким чином ми створили аплікейшен, який вертатиме дані про найближчі аеропорти для вказаних координат, виглядатиме це так:
Відповідь у даному випадку займає біля 200мс. Давайте додамо кешування для того щоб зменшити час відповіді, для цього використаємо Redis.
1. Встановлюємо і налаштовуємо Redis, якщо він не встановлений.
Інструкцію можна знайти тут — https://www.digitalocean.com/community/tutorials/how-to-configure-a-redis-cluster-on-ubuntu-14-04
2. Встановлюємо php розширення для Redis:
sudo pecl install redis
3. Додаємо наступний рядок до php.ini:
extension=redis.so
4. Перезапускаємо веб-сервер.
5. Змінюємо index.php наступним чином:
<?php use Phalcon\Loader; use Phalcon\Mvc\View; use Phalcon\Mvc\Application; use Phalcon\DI\FactoryDefault; use Phalcon\Mvc\Url as UrlProvider; use Phalcon\Db\Adapter\Pdo\Mysql as DbAdapter; use Phalcon\Cache\Backend; try { // Register an autoloader $loader = new Loader(); $loader->registerDirs(array( '../app/controllers/', '../app/models/' ))->register(); // Create a DI $di = new FactoryDefault(); // Setup the view component $di->set('view', function () { $view = new View(); $view->setViewsDir('../app/views/'); return $view; }); // Setup a base URI so that all generated URIs include the "route-finder.loc" folder $di->set('url', function () { $url = new UrlProvider(); $url->setBaseUri('/route-finder.loc/'); return $url; }); $di->set('router', function () { $router = new \Phalcon\Mvc\Router(); $router->add("/ajax/airports", array( 'controller' => 'airports', 'action' => 'index', )); return $router; });
Пояснення щодо коду:
$di->set('cache', function () { — додаємо кеш до нашого аплікейшену
6. Змінюємо AirportsController.php наступним чином:
<?php require_once __DIR__. '/../../vendor/autoload.php'; use Phalcon\Mvc\Controller; use Everyman\Neo4j\Client; use Everyman\Neo4j\Cypher; header('Access-Control-Allow-Origin: *'); function _distance($lat1, $lon1, $lat2, $lon2) { $theta = $lon1 - $lon2; $dist = sin(deg2rad($lat1)) * sin(deg2rad($lat2)) + cos(deg2rad($lat1)) * cos(deg2rad($lat2)) * cos(deg2rad($theta)); $dist = acos($dist); $dist = rad2deg($dist); return $dist * 111.18957696; } class AirportsController extends Controller { public function indexAction() { $client = new Client('localhost', 7474); $lat = $this->request->getQuery("lat", "float", 0.0); $lng = $this->request->get('lng', "float", 0.0); $distance = $this->request->get('distance', "float", 500); $offset = $this->request->get('offset', "int", 0); $limit = $this->request->get('limit', "int", 5); $cache =& $this->di->get('cache'); $redis_key = implode('|', array( 'AirportsController', 'indexAction', $lat, $lng, $distance, $offset, $limit, )); try { // If Redis is connected - try to get result. $result = $cache->get($redis_key); $cache_connected = true; } catch (Exception $e) { $result = false; $cache_connected = false; } if ($result) { $result = unserialize($result); } else { $result = array(); $query = sprintf( "START n=node:geom('withinDistance:[%s, %s, %s]') MATCH n-[r:AVAILABLE_DESTINATION]->() RETURN DISTINCT n SKIP %s LIMIT %s", number_format($lat, 6), number_format($lng, 6), number_format($distance, 1), $offset, $limit ); $query = new Cypher\Query($client, $query); $query = $query->getResultSet(); foreach ($query as $row) { $item = array( 'id' => $row['n']->getId(), 'distance' => _distance( $row['n']->getProperties()['latitude'], $row['n']->getProperties()['longitude'], $lat, $lng ), ); foreach ($row['n']->getProperties() as $key => $value) { $item[$key] = $value; } $result[] = $item; } if ($cache_connected) { $cache->set($redis_key, serialize($result)); } } $this->view->disable(); $response = new \Phalcon\Http\Response(); // Set the content of the response. $response->setContent(json_encode(array('json_list' => $result))); $response->setContentType('application/json', 'UTF-8'); return $response; } }
Пояснення щодо коду:
$redis_key = implode('|', array( — генеруємо унікальний ключ для Redis, для цього ми об’єднуємо назву класу, назву методу і параметри;
try { — використовуємо try для того щоб у випадку невдалої спроби законектитись до Redis у нас не було помилки, і сервіс працював без Redis;
$result = $cache->get($redis_key); — пробуємо забрати значення з Redis по попередньо згенерованому ключу;
$cache_connected = true; — якщо попередній рядок не викликав Exception, отже, Redis доступний і ми зберігаємо у змінній true;
} catch (Exception $e) { — код нижче виконається, якщо був Exception;
$cache_connected = false; — Redis недоступний і ми зберігаємо у змінній false;
$result = unserialize($result); — якщо ми забрали значення з Redis, то десереалізуємо його і поміщаємо у змінну;
$cache->set($redis_key, serialize($result)); — якщо Redis підключений, зберігаємо серіалізоване значення за попередньо згенерованим ключем (цей код виконається лише якщо значення за попередньо згенерованим ключем не було у Redis).
Завдяки цьому час відповіді за незакешованим результатом практично не зміниться, а закешований результат видаватиметься приблизно за 7мс. Таким чином ми зменшили час відповіді приблизно у 30 разів і зменшили навантаження на Neo4j. Крім цього, аплікейшен коректно працюватиме і без Redis. Репозиторій з описаним аплікейшеном — https://github.com/petrykpjatochkin/route-finder
Удачі вам у роботі з Neo4j!