Використання графічної бази даних Neo4j: частина 2-а

09.02.2016
Використання графічної бази даних Neo4j: частина 2-а
Автор:

У попередньому блозі ми знайомились із найпопулярнішою графовою базою даних 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

Наразі у нас є сторінка, спробуємо вивести на цю сторінку якусь інформацію з 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

У нас є Neo4j база з аеропортами і html сторінка, яка відсилатиме аплікейшену довготу і широту, за якими потрібно знайти 5 найближчих аеропортів. Запит відсилатиметься у форматі /ajax/airports?lat=50.433333&lng=30.516667&limit=5

Очікувана відповідь:

Використання графічної бази даних Neo4j

Для початку створимо 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) { — поміщуємо кожну доступну властивість ноди у масив.

Таким чином ми створили аплікейшен, який вертатиме дані про найближчі аеропорти для вказаних координат, виглядатиме це так:

Використання графічної бази даних Neo4j

Відповідь у даному випадку займає біля 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!

Голосів: 1 Рейтинг: 5

Також по темі

1

Це також інвестиція в те, що я називаю «новою Україною» — країною молодих, добре освічених українців, які жадають попрощатися з минулим і...

2

При створенні сайтів часто можна почути слова «фронтенд» і «бекенд». Вони втілюють у собі протилежну «філософію», але при цьому фронтенд і...

3

2015 рік був для нас багатим на події, досягнення, подорожі, відкриття...Однак, іноді цифри говорять красномовніше за слова. Безсумнівно, всі друпалісти люблять точні науки. Отже, ми вирішили...

5

Почувайтеся, як удома, але не забувайте, що ви...на хостинг-сервері :) Хороший хостинг-провайдер ніколи такого не скаже! Адже вашому сайту потрібне по-справжньому надійне та “комфортне” розміщення...

Subscribe to our blog updates