Working With Neo4j Date And Spatial Types In A React.js App

Building A Dashboard App With Neo4j, Mapbox, React, and Nivo Charts

William Lyon
Jun 11, 2018 · 10 min read
This simple dashboard React app allows users to search for businesses within a radius of a specific point that has received reviews within a specified date range. Try it here.

Overview

In this post we walk through the steps to build a React.js dashboard type application that allows a user to search for businesses by location that have reviews within a certain date range and display some charts based on aggregations of these reviews. We’ll explore how to use neo4j-import with the new spatial and temporal types, how to use the new types in Cypher queries and with the Neo4j drivers, and how that all fits into a React app.

Data

The Graph Data Model

The graph data model for the Yelp Open Dataset.

There have been a few examples showing how to use the Yelp Open Dataset in Neo4j. From how to model and import the dataset, including the super fast neo4j-import tool, how to query the dataset using Cypher to find recommended businesses, and how to apply graph algorithms to the dataset.

Import

  • Use apoc.load.json to import the JSON files
  • Read each JSON line and pass as parameters to a Cypher statement
  • Convert to CSV and use LOAD CSV
  • Convert to CSV and use neo4j-import for fast bulk loading import

I opted for the last approach (using neo4j-import). The dataset is large enough that it will be faster to use the bulk import functionality instead of LOAD CSV or the other options. And my colleague Mark Needham had already written most of what I needed. I just extended his script to support the Point and Date types.

Neo4j-import makes use of header files that define the properties and types for the import. So we’ll need to update the headers files for Business and Review nodes. First Business :

id:ID(Business),name,address,city,state,location:Point(WGS-84)

and Review :

id:ID(Review),text,stars:int,date:Date

We don’t need to change the CSV file for reviews, since Neo4j is able to parse date strings in the format already used (YYYY-MM-DD), but we will need to add out Point type in the Business csv, we write the data to the csv as a map (or dictionary) that contains latitude and longitude:

"FYWN1wneV18bWNgQjJ2GNg","Dental by Design","4855 E Warner Rd, Ste B9","Ahwatukee","AZ","{latitude: 33.3306902, longitude: -111.9785992}"

We then use the neo4j-admin import command, passing in the CSVs to import the data into Neo4j.

It’s important to note that neo4j-admin import does not create indexes for us, so we’ll need to explicitly create any indexes we want to use for initial data lookups. In this case we will create an index on the location property of our Business nodes and the date property of our Review nodes:

CREATE INDEX ON :Business(location);
CREATE INDEX ON :Review(date);

You can find the full code for the import here.

Hosting Neo4j In The Cloud

Fortunately, we can use the certbot tool from Let’s Encrypt to easily generate Certificate Authority signed certificates for Neo4j:

# Use certbot to generate certificates
certbot certonly
# Copy certs to Neo4j directory
cp /path_to_certs/fullchain.pem /var/lib/neo4j/certificates/neo4j.cert
cp /path_to_certs/privkey.pem /var/lib/neo4j/certificates/neo4j.key

I initially used this process to secure my Neo4j connection on a VPS instance (see this page for the myriad options for deploying Neo4j), but ultimately I used Neo4j Cloud, which takes care of all the hassle of obtaining certificates :-) You can sign up for early access to Neo4j Cloud here.

Queries

Spatial Queries

MATCH (b:Business)
WHERE distance(
b.location,
point({latitude:33.329 , longitude:-111.978})
) < 1000
RETURN COUNT(b) AS num_businesses
-------------------------------------------------------
num_businesses
117

We make use of the distance function to filter for businesses within 1000 meters of a point that we specify by latitude and longitude.

If we prepend our query with PROFILE we can see the execution plan to verify that we are indeed using the spatial index.

PROFILE results of querying for Businesses within 1km of a point, using the spatial index.

This query takes about 7ms on my laptop to find businesses within 1km of a point in the Phoenix area.

Date Queries

MATCH (r:Review)
WHERE date("2015-03-24") < r.date < date("2015-04-20")
RETURN COUNT(r) AS num_reviews
-----------------------------------------------------
num_reviews
63527

We have a few options for constructing dates in Cypher. We could pass each date component (year, month, day) as integers or, as we’ve done here, pass a string to be parsed to the date function. Again, we can PROFILE our query to ensure it is using the temporal index. This query takes ~12ms on my laptop, certainly better than if we had to scan over all 5 million Review nodes.

Searching for reviews within a time range using the temporal index.

We only touched on a small piece of the new temporal functionality in Neo4j, there are also other types, likeDateTime and LocalDateTime that take timezone into account, and durations for working with time periods. You can learn more about these features in the Neo4j documentation. My colleague Adam Cowley has put together a couple of great posts showing more detail on the Neo4j temporal types and using them with JavaScript.

Putting It Together

MATCH (b:Business)<-[:REVIEWS]-(r:Review)
WHERE distance(
b.location,
point({latitude:33.329 , longitude:-111.978})
) < 1000
AND date("2015-03-24") < r.date < date("2015-04-20")
RETURN COUNT(b) AS num_businesses_with_reviews
---------------------------------------------------------
num_businesses_with_reviews
69

This query is just giving us the count of businesses in our radius that have any reviews within our date range. To populate our UI we actually need a bit more information. You can see the full query in section below “Querying Neo4j From Our React App”.

If we inspect the PROFILE of this query we’ll see that the query planner chooses to use the temporal index on :Review(date). This makes sense since it should be the most selective index as this index contain 5 million entries (reviews), while the spatial index only contains less than 200 thousand (businesses).

Sometimes we want to force the use of one index over another using an index hint, for example if we wanted to use the spatial index instead of the temporal index. Currently index hints aren’t supported for spatial indexes, but this will be added in the next Neo4j release.

React App

Create React App

npx create-react-app spacetime-reviews

Components

App Component

This is our main component which will handle sending queries to the database using the Neo4j JavaScript driver, storing the results in (and maintaining other application) state. All other components in our application will be children of the App component.

Map Component

The job of the Map component is to show a map that allows the user to select a point and display businesses as map markers.

I’ve previously used Leaflet.js and Mapbox for a few different projects, like this Panama Papers address geocode example and this US Congressional district map, but not in a React app. So the first thing I looked for was a React component wrapper for Mapbox GL JS. I found react-mapbox-gl, published by Uber’s data science team, which seemed like what I needed.

After starting to work with react-mapbox-gl however I felt constrained by working with only the props that the library makes available. Fortunately I found this blog post from Tristen Brown that helps explain how to use Mapbox GL JS alongside React. The basic idea is to use the Mapbox GL JS library inside our Map component, encapsulating the use of the library within this component. This felt less constraining to me, but I’m sure I could have gotten the app to work with react-mapbox-gl since Kepler.gl is built using it.

We also can use React’s two-way data binding approach for triggering a call to fetch fresh data from Neo4j when the user selects a new location on the map.

Here’s the event handler for our draggable marker in the Map component. The user drags it around the map and then releases it to select the center of a new location to search:

In addition to updating the circle showing our search area, we grab the latitude and longitude from the map (as well as the zoom level), and call this.props.mapSearchPointChange(viewport). This function is actually defined in the App component and is passed to our Map component through props as a kind of callback. This is how React’s two way data binding pattern works, as callback functions passed to child components as props. Here’s the implementation of mapSearchPointChange in the App component:

mapSearchPointChange simply updates state in the App component, which will trigger a re-render and thus a call to Neo4j to update data for the new location selected.

ReviewSummary and CategorySummaryComponents

ReviewSummary and CategorySummary are purely presentational components, responsible only for drawing charts based on the data received as props. They make use of the Nivo chart component library.

As data is retrieved from Neo4j in the App component it is passed to these two presentational components to render the charts showing the histogram of review counts by stars and a pie chart of business categories.

I found it useful to use the AutoSizer component from the React Virtualized library to allow the chart components to resize, taking the full height and width of their containers.

Querying Neo4j From Our React App

To fetch data from Neo4j we’ll query the database directly from our client application. This might not be an ideal architecture for a real world application, but will work fine for our app. A more realistic architecture might be to create a GraphQL API that queries our Neo4j database.

To actually fetch data from Neo4j we create a function fetchBusiness that contains the Cypher query we want to execute, and updates state in the App component with the result of the query. We can call this function from the appropriate component lifecycle functions, such as componentWillUpdate and componentDidMount . Note that we import the Neo4j Date temporal type and instantiate a Date object to pass as a parameter to the query.

Deploy With Netlify

Build settings for deploying on Netlify.

To deploy our React app we just need to be able to serve static content, so we we have lots of options. I used Netlify, which makes it easy to build and deploy our app.

We can integrate a Netlify project with Github and create a git commit hook that triggers a build on each commit. We can also specify our environment variables for our Mapbox token and Neo4j credentials with Netlify.

You can find the code for this project on Github and try it live here.

The SpaceTime Reviews dashboard. Search for businesses by location that have reviews within a time range. Try it here.

Neo4j Developer Blog

Developer Content around Graph Databases, Neo4j, Cypher…

William Lyon

Written by

🤠 Code Cowboy @neo4j | lyonwj.com

Neo4j Developer Blog

Developer Content around Graph Databases, Neo4j, Cypher, Data Science, Graph Analytics, GraphQL and more.

More From Medium

More from Neo4j Developer Blog

More from Neo4j Developer Blog

More from Neo4j Developer Blog

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade