Categories

(111)
(57)
(117)
(19)

Using Neo4j graph database: part 2

09.02.2016
Using Neo4j graph database: part 2
Author:

In the ‘Using Neo4j graph database: part 1’ blog post
by InternetDevels developer, 
we have learned the basics
of Neo4J. Now, let’s move on to more practical Neo4J use.

In this part, we will create an application that will return the data from Neo4j (the nearest airports according to the given coordinates) in the Json format. For this application, we will use the Phalcon framework. In our example, Neo4j contains the data on 8 thousand airports and a spatial index for these airports. For caching, we will use Redis.

Phalcon

Phalcon is one of the fastest PHP frameworks. All components are written in C, there is support for MVC, ORM, and Volt template engine is available (which resembles Jinja).

Installation instructions (for Ubuntu):

sudo apt-add-repository ppa:phalcon/stable

sudo apt-get update

sudo apt-get install php5-phalcon

find more details here: https://phalconphp.com/en/download

Then create the application itself. Detailed instructions are available here: https://docs.phalconphp.com/en/latest/reference/tutorial.html

Basic structure:

route-finder.loc

── app

│ ├── controllers

│ ├── models

│ └── views

└── public

       ├── css

       ├── img

       └── js

Create a .htaccess file in the application root folder and in the public folder. Next, create index.php in the public folder (do not forget to change the base URI). Create a app/controllers/IndexController.php controller, as in the example. If everything is done right, then, at this address localhost/route-finder.loc, you will see the following:

Using Neo4j graph database: part 2

So now you have a page. Try to output some information from Neo4j to this page. To do this, install the neo4jphp library using Composer:

In the console (the application root) enter the following:

composer require "everyman/neo4jphp" "dev-master"

At this stage, the application structure should look like this:

route-finder.loc

├── app

│ ├── controllers

│ │ └── IndexController.php

│ ├── models

│ └── views

├── composer.json

├── composer.lock

├── composer.phar

├── public

│ ├── css

│ ├── img

│ ├── index.php

│ └── js

└── vendor

...

The IndexController.php file will look like this:

 <?php

use Phalcon\Mvc\Controller;

class IndexController extends Controller
{

    public function indexAction()
    {
        echo "<h1>Hello!</h1>";
    }
}

Change it in the following way:

 <?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;
    }
}

Explanation of code:

require_once __DIR__. '/../../vendor/autoload.php'; — enable autololad;

$client = new Everyman\Neo4j\Client('localhost', 7474); — connect to Neo4j. If you need to take the data from Neo4j in other routes as well, then it is better to inject Neo4j as a service (then the data will be available in all routes);

$this->view->disable(); — the views is not needed, since the controller itself will return the response;

$response->setContent(json_encode($client->getServerInfo())); — return something from Neo4j;

$response->setContentType('application/json', 'UTF-8'); — return as Json.

Now the main page will look like this:

Using Neo4j graph database: part 2

You have a Neo4j base with airports and an html page that will provide your application with the longitude and latitude data for finding the 5 nearest airports. The query will be sent in this format: /ajax/airports?lat=50.433333&lng=30.516667&limit=5

Expected answer:

Using Neo4j graph database: part 2

First, create a route. For this, add the following to the index.php file:

 $di->set('router', function () {

        $router = new \Phalcon\Mvc\Router();

        $router->add("/ajax/airports", array(
            'controller' => 'airports',
            'action' => 'index',
        ));

        return $router;
    });

The index.php will now look like this:

 <?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();
}

Next, create a copy of the IndexController.php file and name it AirportsController.php in the app/controllers folder. If you have done everything right, the /ajax/airports page will look similar to the main page.

Now, copy this code there:

<?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;
    }
}

Explanation of code:

header('Access-Control-Allow-Origin: *'); — allow cross-domain queries;

function _distance($lat1, $lon1, $lat2, $lon2) — an auxiliary function to calculate the distance according to the given coordinates;

$lat = number_format($this->request->getQuery("lat", "float", 0.0), 6); — save the "lat" query parameter in the variable, in addition, format it as a number with 6 decimal places, because in Neo4j it is required that floating-point numbers should have at least one decimal place;

$query = sprintf( — here, set a template for the query to Neo4j, using the Cypher query language (you can read more about Neo4j and Cypher in the previous article). With this query, you can find the nodes that are closest to the given coordinates (for this, SpatialPlugin for Neo4j is installed and the corresponding index is created);

$query = new Cypher\Query($client, $query); — sending the query;

$query = $query->getResultSet(); — getting the results;

foreach ($query as $row) { — formatting the results;

'id' => $row['n']->getId(), — formatting the node ID using the getId() method;

'distance' => _distance( — finding the distance between the current airport and the given point using the above-mentioned auxiliary function;

foreach ($row['n']->getProperties() as $key => $value) { — adding every available property of the node to the array.

So we created an application that will return data about the nearest airports for the given coordinates, like this:

Using Neo4j graph database: part 2

The answer in this case takes around 200ms.

Let's add caching in order to reduce the response time with the help of Redis.

1. Install and configure Redis, if it is not installed.

Instructions can be found here - https://www.digitalocean.com/community/tutorials/how-to-configure-a-redis-cluster-on-ubuntu-14-04

2. Install a php extension for Redis:

sudo pecl install redis

3. Add the following line to php.ini:

extension=redis.so

4. Restart your web server.

5. Change the index.php in the following way:

 <?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;
    });

Explanation of code:

$di->set('cache', function () { — add the cache to the application

6. Change the AirportsController.php in the following way:

 <?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;
    }
}

Explanation of code:

$redis_key = implode('|', array( — generating a unique key for Redis by combining the class name, the method name and the parameters;

try { — using “try” so that, in case of an unsuccessful attempt to connect to Redis, you get no errors and the service works without Redis;

$result = $cache->get($redis_key); — trying to get the value from Redis according to the previously generated key;

$cache_connected = true; — if the previous line didn’t call Exception, then Redis is available and you save “true” in the variable;

} catch (Exception $e) { — the code below will be run if there was an Exception;

$cache_connected = false; — Redis is unavailable and you save false in the variable;

$result = unserialize($result); — if you got the value from Redis, then you deserialize it and put in the variable;

$cache->set($redis_key, serialize($result)); — if Redis is enabled, you save the serialized value according to the previously generated key (the code is run only if the value according to the previously generated key was not in Redis).

Due to this, the response time for the uncached result will barely change, and the cached result will be returned in about 7ms. Thus, we reduced the response time by about 30 times and relieved Neo4j. In addition, the application will work correctly without Redis.

Here's the repository with the described application — https://github.com/petrykpjatochkin/route-finder

Hope these tips have been useful and good luck to you in using Neo4j graph database!

8 votes, Rating: 4.9

Read also

1

Let's keep getting the most useful web development...

2

Creation of RTL CSS is quite an important process in ...

3

After the official release, more and more Drupal developers are...

4

Useful tips by the developers of our Drupal development company make social networks closer and your life easier ;)...

5

Hey there! If you are interested in Drupal web development, tips by our dev could do you a world of good. Welcome to...

Need a quote? Let's discuss the project

Are you looking for someone to help you with your Drupal Web Development needs? Let’s get in touch and discuss the requirements of your project. We would love to hear from you.

Join the people who have already subscribed!

Want to be aware of important and interesting things happening? We will inform you about new blog posts on Drupal development, design, QA testing and more, as well news about Drupal events.

No charge. Unsubscribe anytime