The following takes heavy inspiration from the Sangria docs as well as the HowToGraphQL Scala tutorial. We will create GraphQL queries to filter users and coffee shops based on name and geolocation.

Project Setup

All final code is available on GitHub

ElasticSearch

Easiest setup is with this following docker-compose file, or installing Elastic locally and running on the default port (9200).

Running docker-compose up -d should get you started. Make sure you can get a response from ElasticSearch in your browser at localhost:9200

To load our test users and coffee shops, change directory into the scripts folder and run the load script:

$ cd src/main/resources/scripts/

$ ./load-test-data.sh

Navigating to http://localhost:9200/test-users/_search?pretty should give you a list of users.

Dependencies

Resources

Let’s add the graphiql.html to our resources directory (under /src/main/resources ) so we can have a playground to test our GraphQL queries. We will see a route below that will serve this file from the resources directory.

Main Server

There are just two routes; one that accepts a POST to /graphql and another that serves static resources. Right now, making a POST request to the graphql endpoint will just return a string. We create Main.scala under /src/main/scala

Models

Models can be divided into a few categories; variables, responses, and common. Variables are classes that will be mapped to GraphQL arguments. The responses will the responses from the GraphQL Server and will tie directly into the GraphQL Schema. Common models can be shared between these inputs and outputs, this will be made clear shortly.

Let’s add a models folder with nested directories and classes as follows:

├── Main.scala

└── models

├── common

│ └── Location.scala

├── responses

│ ├── CoffeeShop.scala

│ ├── SearchResponse.scala

│ └── User.scala

└── variables

├── BBox.scala

└── Filter.scala

Common

Location is a simple case class with latitude and longitude properties.

Variables

Let’s build our Bounding Box and Filter classes. BBox takes two Location objects, topLeft and bottomRight as properties, and Filter takes optional BBox and optional String objects as properties. topLeft and bottomRight correlate to our screen’s corners that the map is rendered on.

Responses

Let’s create a generic SearchResponse class:

A User case class will have name, id, and location properties. The location property will be an object of type Location .

Our UsersResponse case class will contain a total property and a list of users.

The CoffeeShop and CoffeeShopResponse models are very similar to the User and User Response ones:

Core Code

Let’s go ahead and make/add the following files in /src/main/scala

├── Elastic.scala

├── GraphQLSchema.scala

├── GraphQLServer.scala

├── Main.scala

└── models

GraphQL Schema

For an in-depth look at Sangria and GraphQL Schema definitions, please see the Sangria docs

Here we tie our models to a GraphQL Schema:

ElasticSearch

In the HowToGraphQL tutorial, the author uses Slick and an in-memory H2 database to save and query data. We will be using elastic4s. We create a trait to hold our Elastic config and methods (makes it easier to test):

Next we will add a class that implements this trait and the methods we have defined:

Sangria has a concept of Context that flows with GraphQL Queries. It’s super important for our use case as it will hold an instance of our Elastic class and the BBox variable.

We can define our context as a simple case class in GraphQLSchema.scala :

case class MyContext(elastic: Elastic, bbox: Option[BBox] = None)

GraphQL Server

Now we will create a GraphQL Server that will have an Akka Http Route and an instance of our Elastic class. The endpoint method below takes a JsValue , parses it, and returns a Route .

You’ll see that we make a call to executeGraphQLQuery . Let’s build that next:

Here is where we pass our Elastic instance as well as our previously defined GraphQLSchema.SchemaDefinition . Let’s not forget to update Main.scala to route requests to our GraphQLServer:

Testing our setup

To run our server, we use sbt. From the root directory:

$ sbt ~reStart

Navigate to localhost:8080 and enter our query and bbox variable:

When we search with the following bounding box coordinates, we will see 3 users returned from the query. All of these users are located in Austin, TX.

{

"data": {

"geoSearch": {

"users": {

"hits": [

{

"id": 3,

"name": "Ricky",

"location": ...

},

{

"id": 4,

"name": "Carter",

"location": ...

},

{

"id": 5,

"name": "Mitch",

"location": ...

}

]

}

}

}

}

Let’s adjust our bounding box to cover more area:

We see a 4th user, who happens to be in San Diego, CA:

{

"id": 1,

"name": "Duane",

"location": {

"lat": "32.715736",

"lon": "-117.161087"

}

}

We adjust our bounding geo box a last time:

And we see our 5th and final user in Mexico City 🇲🇽

{

"name": "Matt",

"id": 2,

"location": {

"lat": "19.42847",

"lon": "-99.12766"

}

}

Extending our setup

The last piece of the puzzle is adding an additional field to search and filter our users by. We are going to add a name filter so that we can make queries like so:

In order to filter by name, let’s update the buildQuery method in Elastic.scala :

Now if we run the new query from above, we will see only Matt and Mitch returned! Super easy to add in new functionality.

{

"data": {

"geoSearch": {

"users": {

"hits": [

{

"id": 2,

"name": "Matt",

"location": {

"lat": "19.42847",

"lon": "-99.12766"

}

},

{

"id": 5,

"name": "Mitch",

"location": {

"lat": "30.366666",

"lon": "-97.833330"

}

}

],

"total": 2

}

}

}

}

Finally, let’s say we want to search for users in our geo box with names that start with “M” as well as coffee shops in the area with the name “Starbucks”

{

"data": {

"geoSearch": {

"users": {

"hits": [

{

"id": 2,

"name": "Matt",

"location": {

"lat": "19.42847",

"lon": "-99.12766"

}

},

{

"id": 5,

"name": "Mitch",

"location": {

"lat": "30.366666",

"lon": "-97.833330"

}

}

],

"total": 2

},

"coffeeShops": {

"hits": [

{

"name": "Starbucks"

},

{

"name": "Starbucks"

}

],

"total": 2

}

}

}

}

We can quickly wire up a simple React app with Mapbox and Apollo to display some of our data (source code):