Neo4j Spatial Part 2

nomnomnom

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
rackup

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 
RETURN n 
LIMIT 25

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.

geoPosition.getCurrentPosition(showPosition, 
                               function(){ Console.log("Couldn't get location");},
                               {enableHighAccuracy:true});

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.

jQuery('#map').goMap({
	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.

$.ajax({
  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("&lt; ", "").length +
           "&rating=" + document.getElementById("rating").innerHTML.replace("&gt;= ", "") +
           "&alcohol=" + $("#alcohol-selector-advanced").val() +
           "&good=" +  $("#good-selector-advanced").val(),
     dataType: "html",
     success: function(data) {
       $('#restaurants').html(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 }
    end
    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.

$.goMap.createMarker({
  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
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:

http://dist.neo4j.org/spatial/neo4j-spatial-0.12-neo4j-2.0.0-server-plugin.zip

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.

single_platform_gray

Tagged , , , ,

5 thoughts on “Neo4j Spatial Part 2

  1. […] Update: Part 2 has now been published. […]

  2. Awesome blog post!
    It would be good to have a graph domain model so that it becomes more obvious how the different types of nodes relate to each other.
    Also this would perhaps make clear that this app (besides the spatial r-tree) does not yet leverage the graph features, that perhaps a part 3 could add (social, ratings, friends).

  3. Great post! I like especially that you mention how to get this into a production setup. I wrote on a similar experience here: http://thinkingonthinking.com/Exploring-Graphs-With-Neography/ – maybe interesting for the one or other Neo4J beginner

  4. Udit says:

    Does neo4j perform equally good with frequent location updates. Lets us say for building social networking application to find people with similar interests around you.

  5. srini says:

    Nice post.
    can you please share few examples on how the rest api works for spatial query?

Leave a comment