Over a million developers have joined DZone.

Neo4j Spatial, Part 2: Building a Recommendation Engine

· Big Data Zone

Learn how you can maximize big data in the cloud with Apache Hadoop. Download this eBook now. Brought to you in partnership with Hortonworks.



In part 1 of this series we looked at how to get started with Neo4j Spatial and we looked at some of the pieces we’ll use today to build a proof of concept application. I’m calling the application “Nom Nom Nom” in reference to its onomatopoeic meme.

So we’ll get data from Factual, get data from OpenTable, combine them and import them into Neo4j:

rake neo4j:install
rake neo4j:get_spatial
rake neo4j:start
rake neo4j:get_factual
rake neo4j:get_opentable
rake neo4j:combine
rake neo4j:import

This will take a while, but once it is done we’ll have a sample of Restaurant data for the U.S. indexed in the “restaurants” spatial index. I’m only using data from the cities in the list below, so keep that in mind when trying the demo.

locations = ["Arlington", "Atlanta", "Austin", "Baltimore", "Boston", "Bronx", "Brooklyn",
"Charlotte", "Chicago", "Cincinnati", "Cleveland", "Columbia", "Columbus", "Dallas", "Denver",
"Fort Worth", "Honolulu", "Houston", "Indianapolis", "Jacksonville", "Las Vegas", "Los Angeles",
"Louisville", "Memphis", "Miami", "Milwaukee", "Minneapolis", "Nashville", "New Orleans",
"New York", "Newark", "Oklahoma City", "Orlando", "Philadelphia", "Phoenix", "Pittsburgh",
"Portland", "Richmond", "Rochester", "Sacramento", "Saint Louis", "San Antonio", "San Diego",
"San Francisco", "San Jose", "Seattle", "Springfield", "Tampa", "Tucson", "Washington"] 

The whole application comes down to just a single cypher query that looks like:

START n = node:restaurants({location})
WHERE n.price <= {price}
  AND n.rating >= {rating}
  AND n.meal_lunch = true            # when lunch is selected
  AND n.meal_dinner = true           # when dinner is selected
  AND n.alcohol_bar = true           # when drinks is selected
  AND n.kids_goodfor = true          # when... I think you get the idea
  AND n.groups_goodfor = true
  AND n.options_healthy = true
  AND n.options_vegan = true
  AND n.options_vegetarian = true
  AND n.smoking = true
  AND n.accessible_wheelchair = true
  AND n.alcohol = true
  AND n.alcohol_beer_wine = true
  AND n.alcohol_byob = true

The parameters passed into the cypher query will require us to get the latitude and longitude of the hungry person using the application.

{:location => "withinDistance:[#{latitude},#{longitude},#{distance}]",
 :price => price.to_i,
 :rating => rating.to_i}

Luckily for us modern browsers have geolocation support and we can use some libraries (like geoPosition.js ) to support older browsers as well.

                               function(){ Console.log("Couldn't get location");},

We’ll need a Map to display our restaurant locations, and for this project we’ll use Google Maps but we could have used Leaflet.js or any of the other alternatives.

    maptype: 'ROADMAP',
        latitude: latitude,
        longitude: longitude,
        zoom: 15,
        scaleControl: true,
        scrollwheel: false,
    markers: []

However, the hungry user may be looking for a restaurant outside their current location, so we’ll also need a Geocoder that will take an address as input and return latitude and longitude coordinates. Since we’re already using Google Maps, we’ll use their Geocoder as well, but we could have used the Data Science Tool Kit geocoder or any of the other alternatives.

address = document.getElementById('where').value;
if (address != '') {
  geocoder.geocode( { 'address': address}, function(results, status) {
    if (status == google.maps.GeocoderStatus.OK) {
      latitude = parseFloat(results[0].geometry.location.d);
      longitude = parseFloat(results[0].geometry.location.e);
      $.goMap.setMap({latitude: latitude, longitude: longitude});

We will build a form for user input, make an AJAX call that passes in our parameters.

  type: "POST",
     url: "/search",
     data: "what="+$("#search option:selected").val() +
           "&latitude=" + latitude +
           "&longitude=" + longitude +
           "&distance=" + document.getElementById("distance").innerHTML.replace(" km", ".0") +
           "&price=" + document.getElementById("price").innerHTML.replace("< ", "").length +
           "&rating=" + document.getElementById("rating").innerHTML.replace(">= ", "") +
           "&alcohol=" + $("#alcohol-selector-advanced").val() +
           "&good=" +  $("#good-selector-advanced").val(),
     dataType: "html",
     success: function(data) {

We’ll make that cypher call to Neo4j.

restaurants = $neo.execute_query(cypher, location)["data"]
@results = []
restaurants.each do |r|       
  restaurant = r[0]["data"]
  node_id = r[0]["self"].split("/").last
  @results << { :pic => "/images/food/bigger/#{pick(restaurant["cuisine"])}_128.png",
                :cuisine => Array(restaurant["cuisine"]).join(", "),
                :name => restaurant["name"],
                :address => restaurant["address"],
                :rating => (restaurant["rating"] || -1),
                :price => (restaurant["price"] || -1),
                :latitude => restaurant["latitude"],
                :longitude => restaurant["longitude"],
                :group => pick(restaurant["cuisine"]),
                :node_id => node_id }
slim :index, :layout => false

We’ll then display our results:

h1 Search Results
  -@results.each do |result|
    div class="company-listing clearfix"
      a href="#" class="listing-image"
        img src="#{result[:pic]}" alt=""
      div class="listing-body"
        div class="listing-title"
          a href='restaurant?id=#{result[:node_id]}' class='text-colorful' = result[:name]

…and sprinkle in a little javascript to make the markers appear on the map.

  latitude: "#{result[:latitude]}",
  longitude: "#{result[:longitude]}",
  group: "#{result[:group]}",
  icon: "/images/marker-#{result[:group]}.png",
  html: {
    content: "<a href='restaurant?id=#{result[:node_id]}'>#{result[:name]}</a>"

Finally we need to find some food icons to make it all look nice:

hamburger_128 sandwich_128 baked_salmon_128

… and that’s all there is to the application. Now we need to deploy it somewhere. We’ll use Heroku and GrapheneDB.

GrapheneDB is a hosted Neo4j service, and one of the neat things about it is that it lets us use the Neo4j Spatial Plugin. The plugins feature is enabled by default on all paid plans. It’s enabled by request on the free plans. Let’s set it up:

Update: GrapheneDB has added support for Spatial in 1.9.6 and 2.0.1 out of the box. No need to upload the plugin!

graphene plugin

We can get the plugin url from the Neo4j Spatial instructions:


Next, we’ll need to restart the database to enable the plugin:

graphene plugin enabled

Then we’ll upload the graph.db directory of our already created database using their “Restore” feature:

graphene upload

Finally we’ll point Neography to our provisioned server:

$neo = Neography::Rest.new("http://myusername:mypassword@nomnomnom.sb01.stations.graphenedb.com:24789")

… and deploy our application to Heroku. For more information about using GrapheneDB on Heroku, see this guide. You can see the app running by going to http://nomnomnomus.herokuapp.com.

We can get fancier here and add Facebook integration so you can get recommendations that your friends like, but that’s pretty trivial and we’ve seen how to do that with Neo4j already. Add food pictures from Foursquare, and menu data from single platform and you’re well on your way to building a restaurant recommendation application with Neo4j Spatial.


Hortonworks DataFlow is an integrated platform that makes data ingestion fast, easy, and secure. Download the white paper now.  Brought to you in partnership with Hortonworks


Published at DZone with permission of Max De Marzi, DZone MVB. See the original article here.

Opinions expressed by DZone contributors are their own.

The best of DZone straight to your inbox.

Please provide a valid email address.

Thanks for subscribing!

Awesome! Check your inbox to verify your email so you can start receiving the latest in tech news and resources.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}