Working With Spatial Data In Neo4j GraphQL In The Cloud
Serverless GraphQL, Neo4j Aura, and GRANDstack
In this post, we take a look at using GraphQL with Neo4j using Neo4j GraphQL’s spatial Point type to find businesses nearby. We’ll use Neo4j Aura database-as-a-service and deploy our serverless GraphQL API using Zeit Now and Codesandbox.
First, we’ll create a Neo4j Aura graph database instance in the cloud. We’ll load a sample dataset of businesses then we'll explore how to use the Point
data type in Cypher, as well as the distance
function to find businesses near other businesses. Then we’ll create a Node.js GraphQL server using neo4j-graphql.js to serve a GraphQL API exposing the business data we loaded into Neo4j Aura. Finally, we deploy our API using Zeit Now — creating a serverless GraphQL API in the cloud fetching data from our Neo4j Aura instance.
Starting With Aura
First, we’ll create a Neo4j instance on Neo4j Aura, the Neo4j database as a service platform. After signing in, we select the region and database size. For our purposes, we’ll just choose the 1GB Memory size, which will give us a Neo4j cluster running in the cloud ☁️☁️☁️
After creating the database we’ll be presented with a randomly generated password, which we’ll want to save and change.
Once our database cluster is provisioned, the Aura dashboard will give us the Bolt URL, which is the endpoint used to connect to our Neo4j instance.
We’ll create a file .env
to store the Bolt URL and database user and password. Later, we will load these values into environment variables to pass to our GraphQL API app to connect to our Neo4j Aura instance.
.env
NEO4J_URI=bolt+routing://522b2c8b.databases.neo4j.io
NEO4J_USER=neo4j
NEO4J_PASSWORD=G6OhRNPLeGQCdW0c_VtAHn1wZPubD_M2EMQ5iaEo05s
Now that we’ve created our Neo4j Aura instance, let’s open Neo4j Browser and take a look at how to use the Point type with Cypher.
Spatial Point Type In Neo4j
Neo4j supports a native Point
data type that can be used with properties on nodes and relationships. Each point can have either 2 or 3 dimensions and supports a variety of coordinate reference systems, but in this post, we focus on the geographic coordinate reference system using latitude and longitude.
Creating A Point Property
The Point
function is used to create Point types and can be passed a combination of x, y, z, latitude, longitude, height, and crs values depending on what data we want to store. Here we create two Business nodes and store their location as a Point using latitude and longitude:
CREATE (b:Business)
SET b.location = Point(
{latitude: 37.575968, longitude: -122.336041}),
b.name = "Ducky's Car Wash"CREATE (b:Business)
SET b.location = Point(
{latitude:37.563534, longitude: -122.322269}),
b.name = "Neo4j"
See the documentation for examples of creating 3D points and point using other coordinate reference systems.
The Distance Function
Once we’ve created nodes with Point
type properties we can see how far away they are from each other:
How far is it from the Neo4j office to Ducky’s Car Wash?
MATCH (neo4j:Business {name: "Neo4j"})
MATCH (ducky:Business {name: "Ducky's Car Wash"})
RETURN distance(neo4j.location, ducky.location)
-------------------------------------------------
1841.8594204207154
Or filter for businesses within a certain radius:
What businesses are within 2km of the Neo4j office?
MATCH (b:Business) WHERE distance(b.location, Point({latitude:37.563534, longitude:-122.322269})) < 2000
RETURN b.name
-----------------------------------
│"Ducky's Car Wash"│
├──────────────────┤
If we add PROFILE
at the beginning of our query we can see the query plan:
Looking at the query plan we see a label scan operation. This is fine for such a small dataset, but if we had millions of businesses in our database this scan might be a bit slow. Fortunately, we can create an index on the location
property that can avoid this scan operation.
Spatial Index
To create an index on the location
property, run:
CREATE INDEX ON :Business(location)
now if we run the query again with PROFILE
after creating the index we can see an index lookup is used instead of the scan operation:
Load Sample Dataset
We saw that searching for businesses near the Neo4j office wasn’t very exciting with just the two nodes we created, so let’s bring in some more sample data to play with. To do that we’ll load a Neo4j Browser Guide by running
:play grandstack
in the browser query editor. This command will load a browser guide that embeds a pre-canned query that we can use to load some sample data.
This is a browser guide used for exercises from the book Fullstack GraphQL Applications With GRANDstack, but we’ll just use it to load some sample data. Click on the embedded query in the guide to load it into the query editor, then click the play button to load the data into our Neo4j Aura instance:
Once the data is loaded we can run CALL db.schema()
to see a visualization of the data we’ve loaded:
And we can see we have some data about businesses and users who have reviewed them.
Building A GraphQL API With neo4j-graphql.js And Apollo Server
Next, we want to expose our business graph as a GraphQL API. To do this we’ll use neo4j-graphql.js to build a Node.js GraphQL API. Neo4j-graphql.js will allow us to quickly build a GraphQL schema capable of fetching data from Neo4j, and we’ll use Apollo Server to serve the GraphQL API, handling network requests and GraphQL execution.
neo4j-graphql.js
neo4j-graphql.js is a node.js library that makes it easy to build GraphQL APIs backed by Neo4j. It does this by using GraphQL type definitions to auto-generate a GraphQL API, adding queries and mutations, filtering, ordering, pagination and generating Cypher queries from GraphQL at query time. This means that building a GraphQL API is as simple as writing GraphQL type definitions.
First, we’ll create a new Node.js project
npm init -y
Then install our dependencies. In addition to neo4j-graphql.js and Apollo Server we install the Neo4j JavaScript driver and dotenv, a utility for reading credentials from our .env file and setting them as environment variables.
npm install apollo-server neo4j-graphql-js neo4j-driver@1.7.6 dotenv
neo4j-graphql.js’s makeAugmentedSchema
The makeAugmentedSchema
export from neo4j-graphql.js will take our GraphQL type definitions as input, and return an executable GraphQL schema object which we can then pass to Apollo Server. This executable schema object will contain all the data fetching logic for translating GraphQL requests into Cypher queries.
Create an index.js
file that will read GraphQL type definitions from a graphql.schema
file, then uses makeAugmentedSchema
and Apollo Server to serve the GraphQL API. We’ll create this graphql.schema
file in the next step
index.js
const { makeAugmentedSchema } = require("neo4j-graphql-js");
const { ApolloServer } = require("apollo-server");
const neo4j = require("neo4j-driver").v1;
const fs = require("fs");
const dotenv = require("dotenv");// Load contents of .env as environment variables
dotenv.config();// Load GraphQL type definitions from schema.graphql file
const typeDefs = fs.readFileSync("schema.graphql").toString("utf-8");// Create executable GraphQL schema from GraphQL type definitions,
// using neo4j-graphql.js to autogenerate resolversconst schema = makeAugmentedSchema({
typeDefs
});// Create Neo4j driver instance
const driver = neo4j.driver(process.env.NEO4J_URI,
neo4j.auth.basic(process.env.NEO4J_USER, process.env.NEO4J_PASSWORD)
);// Create ApolloServer instance to serve GraphQL schema
// Inject Neo4j driver instance into the context object
// which is passed into each (autogenerated) resolverconst server = new ApolloServer({
context: { driver },
schema
});// Start ApolloServer
server.listen().then(({ url }) => {
console.log(`GraphQL server ready at ${url}`);
});
Inferring GraphQL Type Definitions From An Existing Neo4j Database
When building GraphQL APIs with neo4j-graphql.js we have two options for generating the schema:
- Start with an empty Neo4j database and write GraphQL type definitions to drive the database model, mapping GraphQL type definitions to the property graph model used by Neo4j, or
- Start with an existing Neo4j database and use the database data model to infer GraphQL type definitions which will then be used to create the GraphQL API
We’ll go with the second approach, inferring GraphQL type definitions from the Neo4j Aura database that we populated with sample data. To do this we use the inferSchema
export from neo4j-graphql.js:
inferSchema.js
const neo4j = require("neo4j-driver").v1;
const { inferSchema } = require("neo4j-graphql-js");
const dotenv = require("dotenv");
const fs = require("fs");dotenv.config();const driver = neo4j.driver(
process.env.NEO4J_URI,
neo4j.auth.basic(process.env.NEO4J_USER, process.env.NEO4J_PASSWORD)
);const schemaInferenceOptions = {
alwaysIncludeRelationships: false
};inferSchema(driver, schemaInferenceOptions).then(result => {
fs.writeFile("schema.graphql", result.typeDefs, err => {
if (err) throw err;
console.log("Updated schema.graphql");
});
});
which will then populate schema.graphql
with the GraphQL type definitions
Let’s run this file:
node inferSchema.js
and we can see that the schema.graphql
file is now populated with GraphQL type definitions that match the data model of our Neo4j database:
schema.graphql
type User {
_id: Long!
name: String!
wrote: [Review] @relation(name: "WROTE", direction: "OUT")
}type Review {
_id: Long!
date: Date!
reviewId: String!
stars: Float!
text: String
reviews: [Business] @relation(name: "REVIEWS", direction: "OUT")
users: [User] @relation(name: "WROTE", direction: "IN")
}type Category {
_id: Long!
name: String!
businesss: [Business] @relation(name: "IN_CATEGORY", direction: "IN")
}type Business {
_id: Long!
address: String!
city: String!
location: Point!
name: String!
state: String!
in_category: [Category] @relation(name: "IN_CATEGORY", direction: "OUT")
reviews: [Review] @relation(name: "REVIEWS", direction: "IN")
}
Querying GraphQL With GraphQL Playground
The API generated by makeAugmentedSchema
includes Query and Mutation fields for full CRUD operations for each type and relationship defined in the GraphQL type definitions.
For example, we can search for all businesses and return their name and location with this query
{
Business {
name
location {
latitude
longitude
}
}
}
In addition, the generated API includes pagination, ordering, and complex filtering. A filter argument is generated for each type, based on the fields of that type. We can use the distance filter to find businesses within some radius of a point. Here we search for businesses within 2km:
Find Businesses Near Me With The Distance Filter
{
Business(
filter: {
location_distance_lt: {
point: { latitude: 46.860992, longitude: -113.985122 }
distance: 2000
}
}
) {
name
address
city
state
}
}
The auto-generated GraphQL CRUD API that neo4j-graphql.js gives us is powerful for creating simple CRUD APIs, but we can also add custom logic using the @cypher
schema directive. We’ll explore this functionality in a future post, but for now, you can see some examples of it in the documentation.
Deploying Serverless GraphQL With Zeit Now
Once we’ve built our GraphQL server we need to deploy it somewhere and Zeit Now is a great option to deploy our GraphQL API taking advantage of a serverless approach. This means we don’t have to worry about provisioning server instances and we’re only billed for the usage of our application.
You’ll need to create a Zeit account if you don’t have one already, then add a now.json
file that specifies how to build the project and serve the project as well as specify the credentials for our Neo4j Aura instance:
now.json
{
"version": 2,
"name": "business-search-graphql",
"builds": [{ "src": "index.js", "use": "@now/node-server" }],
"routes": [{ "src": "/", "dest": "index.js" }],
"env": {
"NEO4J_URI": "bolt+routing://32a85dd9.databases.neo4j.io",
"NEO4J_USER": "ukcompanies",
"NEO4J_PASSWORD": "ukcompanies"
}
}
You can read more about configuring Zeit Now deployments in the docs here.
Try In Codesandbox
You can find the code from this post on GitHub here or you can run it directly in a Codesandbox here. It’s set up to point to my Neo4j Aura instance (don’t worry it’s just a read-only user), so just change the credentials in .env
to point to your own instance.
If you found this interesting, subscribe to the GRANDstack mailing list to be kept up to date on all things GRANDstack: