Finding the nearest locations around you using AWS Amplify — Part 2

Introduction to Amazon Elasticsearch service

Introduction to open source Elasticsearch

Searches by distance or within a custom region (using a bounding box or polygon).

Elasticsearch architecture

Elasticsearch Architecture Overview (source elastic.co)

In order to explain Elasticsearch we are going to compare it to a relational database. This is only as an analogy to help understanding and is not accurate.

type BikePoint @model @searchable {
id: ID!
name: String!
description: String
location: Location
bikes: Int
}

Finding your new Amazon Elasticsearch domain

amplify console api
Data Sources for LondonCycles app.

Complete Part 1 of these series if you can’t see them.

Amazon Elasticsearch Dashboard with a new domain created by @searchable.

Kibana is an open source data visualization dashboard for Elasticsearch. You can use Kibana Developer Tools to test your search queries.

Amazon Elasticsearch domain details.

Kibana Developer Tools

$ cat ~/.aws/credentials                           (Linux & Mac)
$ %USERPROFILE%\.aws\credentials > con (Windows)
[default]
aws_access_key_id=<<KEY-ID>>
aws_secret_access_key=<<SECRET-ACCESS-KEY>>

Step 1: setting up a distance-aware Elasticsearch index

# Create a new index
PUT /bikepoint
# Create a new mapping to set location type to geo_point
PUT /bikepoint/_mapping/doc
{
"properties": {
"location": {
"type": "geo_point"
}
}
}

Important: after running these commands you can start using your geolocation data in GraphQL mutations.

Step 2: creating a custom GraphQL distance-aware search query

Find bikes nearby feature in LondonCycles app.
type Query {
nearbyBikeStations(
location: LocationInput!,
m: Int,
limit: Int,
nextToken: String
): ModelBikePointConnection
}
input LocationInput {
lat: Float!
lon: Float!
}
type ModelBikePointConnection {
items: [BikePoint]
total: Int
nextToken: String
}

Step 3: creating a custom AWS AppSync resolver

For an introduction to AWS AppSync resolvers you can read AWS AppSync Velocity Templates Guide.

Custom GraphQL distance-aware search query flow
/amplify/backend/api/
TransportForLondonAPI/resolvers
Query.nearbyBikeStations.req.vtl
Query.nearbyBikeStations.res.vtl
query NearbyBikes {
nearbyBikeStations(
location: { lat: 51.510239, lon: -0.134167 },
m: 500,
limit: 3
) {
items {
id
name
location { lat lon }
}
total
}
}
## Query.nearbyBikeStations.req.vtl#set( $distance = $util.defaultIfNull($ctx.args.m, 500) )
#set( $limit = $util.defaultIfNull($ctx.args.limit, 10) )
{
"version": "2017-02-28",
"operation": "GET",
"path": "/bikepoint/doc/_search",
"params": {
"body": {
#if( $context.args.nextToken )"search_after": ["$context.args.nextToken"], #end
"size" : ${limit},
"query": {
"bool" : { "must" : { "match_all" : {} },
"filter" : {
"geo_distance" : {
"distance" : "${distance}m",
"distance_type": "arc",
"location" : $util.toJson($ctx.args.location) }
}

}
},
"sort": [{
"_geo_distance": {
"location": $util.toJson($ctx.args.location),
"order": "asc",
"unit": "m",
"distance_type": "arc"
}

}]
}
}}
#set( $distance = $util.defaultIfNull($ctx.args.m, 500) )
#set( $limit = $util.defaultIfNull($ctx.args.limit, 10) )

$ctx.args holds an object that corresponds to our query arguments: location, m, limit and nextToken.

"operation": "GET",  
"path": "/bikepoint/doc/_search",
"query": {        
"bool" : {
"must" : { "match_all" : {} },
"filter" : {
"geo_distance" : {
"distance" : "${distance}m",
"distance_type": "arc",
"location" : $util.toJson($ctx.args.location)
}
}

}
}

You can use any of these distance units. Eg: mi, yd, ft, km, m. If you are working with large datasets consider changing distance_type to plane.

"sort": [{ 
"_geo_distance": {
"location": $util.toJson($ctx.args.location),
"order": "asc",
"unit": "m",
"distance_type": "arc"
}
}]
Example search query and results.
## Query.nearbyBikeStations.res.vtl#set( $items = [] )
#foreach( $entry in $context.result.hits.hits )
#if( !$foreach.hasNext )
#set( $nextToken = "$entry.sort.get(0)" )
#end
$util.qr($items.add($entry.get("_source")))
#end
$util.toJson({
"items": $items,
"total": $ctx.result.hits.total,
"nextToken": $nextToken
})
{
"took": 17,
"hits": {
"total": 12,
"hits": [
{
"_index": "bikepoint",
"_type": "doc",
"_id": "BikePoints_83",
"_source": {
"id": "BikePoints_83",
"__typename": "BikePoint",
"name": "Panton Street, West End",
"location": {
"lon": -0.13151,
"lat": 51.509639
},
"createdAt": "2020-03-02T14:48:44.617Z",
"updatedAt": "2020-03-02T14:48:44.617Z"
},
"sort": [
195.6063243635008
]
}
]
}
}
////////////////////////////////////////////////////////////////////
// First query. We grab nextToken from results
nearbyBikeStations(
location: { lat: 51.510239, lon: -0.134167 }, m: 500, limit: 1
) {
items { id name location { lat lon } }
total
nextToken
}
////////////////////////////////////////////////////////////////////
// Query sent to Elasticsearch
GET /bikepoint/doc/_search
{
"size" : 1,
"query": {...},
"sort": [...]
}
////////////////////////////////////////////////////////////////////
// Result
{
"took": 25,
"hits": {
"total": 12,
"hits": [
{
"_index": "bikepoint",
"_type": "doc",
"_id": "BikePoints_83",
"_source": {
"__typename": "BikePoint",
"name": "Panton Street, West End",
"location": {
"lon": -0.13151,
"lat": 51.509639
},
"id": "BikePoints_83",
},
"sort": [
195.6063243635008
]
}
]
}
}
////////////////////////////////////////////////////////////////////
// Second query as user paginates. We use previous nextToken
nearbyBikeStations(
location: { lat: 51.510239, lon: -0.134167 }, m: 500, limit: 1,
nextToken: "195.6063243635008"
) {
items { id name location { lat lon } }
total
nextToken
}
////////////////////////////////////////////////////////////////////
// Query sent to Elasticsearch
GET /bikepoint/doc/_search
{
"search_after": [195.6063243635008],
"size" : 1,
"query": {...},
"sort": [...]
}

Step 4: adding custom AWS AppSync resolver to your Amplify project

/amplify/backend/api/
TransportForLondonAPI/stacks
CustomResources.json
{
"Resources": {
"QueryNearbyBikeStations": {
"Type": "AWS::AppSync::Resolver",
"Properties": {
"ApiId": { "Ref": "AppSyncApiId" },
"DataSourceName": "ElasticSearchDomain",
"TypeName": "Query",
"FieldName": "nearbyBikeStations",
"RequestMappingTemplateS3Location": {
"Fn::Sub": [
"s3://${S3DeploymentBucket}/${S3DeploymentRootKey}/resolvers/Query.nearbyBikeStations.req.vtl", {
"S3DeploymentBucket": { "Ref": "S3DeploymentBucket" },
"S3DeploymentRootKey": { "Ref": "S3DeploymentRootKey" }
}]
},
"ResponseMappingTemplateS3Location": {
"Fn::Sub": [ "s3://${S3DeploymentBucket}/${S3DeploymentRootKey}/resolvers/Query.nearbyBikeStations.res.vtl", {
"S3DeploymentBucket": { "Ref": "S3DeploymentBucket" },
"S3DeploymentRootKey": { "Ref": "S3DeploymentRootKey" }
}]
}
}
},
}

Take special care to make sure template file names and paths match the ones in your project and resources entries don’t conflict with any other resources you may already have.

amplify push

Important: enabling searches in your application may incur in costs as shown in the table at the end of this article.

Conclusion

Ready to code?

Free tier for a new AWS Account. Check out latest pricing.
Picture with Jane Shih from last Open Up Summit in Taipei, Taiwan.

Thanks for reading!

My Name is Gerard Sans. I am a Developer Advocate at AWS Mobile working with AWS Amplify and AWS AppSync teams.

GraphQL is an open-source data query and manipulation language for APIs.

Elasticsearch is an open source search and analytics engine based on the Lucene library.

Kibana is an open source data visualization dashboard for Elasticsearch.

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store