Over a million developers have joined DZone.

Neo4j Spatial, Part 2: Building a Recommendation Engine

DZone's Guide to

Neo4j Spatial, Part 2: Building a Recommendation Engine

· Big Data Zone ·
Free Resource

The Architect’s Guide to Big Data Application Performance. Get the Guide.



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.


Learn how taking a DataOps approach will help you speed up processes and increase data quality by providing streamlined analytics pipelines via automation and testing. Learn More.


Published at DZone with permission of

Opinions expressed by DZone contributors are their own.

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

{{ parent.tldr }}

{{ parent.urlSource.name }}