DEV Community

The Guide to implement Geo Search in your React Native App with AWS Amplify

rpostulart on December 08, 2019

In this guide I am going to show how you can apply advanced GEO search in React Native in combination with AWS Amplify. More and more apps are usin...
Collapse
 
rosswilliams profile image
rosswilliams

The best way to add the ES mapping is to use a cloudformation custom resource lambda, you can write an inline lambda function that makes the right API calls when the template is first deployed, if you make the ES domain a dependency of the custom resource Lambda then it will always run after the ES domain has been setup.

Collapse
 
rpostulart profile image
rpostulart AWS Community Builders

Ok nice, I was not aware of that. Do you have some more info / referencence documentation about this?

Collapse
 
rosswilliams profile image
rosswilliams

there are some gists floating around to adapt, most look like this block of JSON for the cloudformation custom resources file. You likely want to lock down the permission a bit more.

{
  "ConfigureESCustom": {
    "Type": "Custom::ConfigureES",
    "Properties": {
      "ServiceToken": {
        "Fn::GetAtt": [
          "ConfigureES",
          "Arn"
        ]
      }
    }
  },
  "ConfigureES": {
    "Type": "AWS::Lambda::Function",
    "Properties": {
      "Environment": {
        "Variables": {
          "ES_ENDPOINT": {
            "Fn::ImportValue": {
              "Fn::Join": [
                ":",
                [
                  {
                    "Ref": "AppSyncApiId"
                  },
                  "GetAtt",
                  "Elasticsearch",
                  "DomainEndpoint"
                ]
              ]
            }
          },
          "ES_REGION": {
            "Ref": "AWS::Region"
          }
        }
      },
      "Code": {
            "ZipFile": "import base64
    import json
    import logging
    import string
    import boto3
    import os
    import time
    import datetime
    import traceback
    from urllib.parse import urlparse, quote
    from botocore.vendored import requests
    from botocore.auth import SigV4Auth
    from botocore.awsrequest import AWSRequest
    from botocore.credentials import get_credentials
    from botocore.endpoint import BotocoreHTTPSession
    from botocore.session import Session
    import cfnresponse

    logger = logging.getLogger()
    logger.setLevel(logging.INFO)

    # The following parameters are required to configure the ES cluster
    ES_ENDPOINT = os.environ['ES_ENDPOINT']
    ES_REGION = os.environ['ES_REGION']
    DEBUG = True if os.environ['DEBUG'] is not None else False

    def es_put(payload, region, creds, host, path, method='PUT', proto='https://'):
    '''Put index data to ES endpoint with SigV4 signed http headers'''
    req = AWSRequest(method=method, url=proto + host +
        quote(path), data=payload, headers={'Host': host, 'Content-Type': 'application/json'})
    SigV4Auth(creds, 'es', region).add_auth(req)
    http_session = BotocoreHTTPSession()
    res = http_session.send(req.prepare())
    return res._content

    def lambda_handler(event, context):
    logger.info('got event {}'.format(event))

    if event['RequestType'] == 'Create':
        # Get aws_region and credentials to post signed URL to ES
        es_region = ES_REGION or os.environ['AWS_REGION']
        session = Session({'region': es_region})
        creds = get_credentials(session)
        es_url = urlparse(ES_ENDPOINT)
        # Extract the domain name in ES_ENDPOINT
        es_endpoint = es_url.netloc or es_url.path
        es_put('', es_region, creds,
        es_endpoint, '/s12doctor')
        es_actions = []
        es_actions.append('')  # Add one empty line to force final \

        es_payload = '\
    '.join(es_actions)
        action = {\"properties\": {\"location\": {\"type\": \"geo_point\"}, \"coordinates\": {\"type\": \"geo_point\"}}}
        es_actions.append(json.dumps(action))
    es_actions.append('')  # Add one empty line to force final \

    es_payload = '\
'.join(es_actions)
    es_put(es_payload, es_region, creds,
      es_endpoint, '/INDEX_NAME_HERE/_mapping/doc')

  cfnresponse.send(event, context, cfnresponse.SUCCESS, {})
"
      },
      "FunctionName": {
        "Fn::Join": [
          "-",
          [
            "ConfigureES",
            {
              "Ref": "env"
            }
          ]
        ]
      },
      "Handler": "index.lambda_handler",
      "Timeout": 30,
      "Role": {
        "Fn::GetAtt": [
          "LambdaRole",
          "Arn"
        ]
      },
      "Runtime": "python3.6",
      "Layers": [
        "arn:aws:lambda:eu-west-2:142628438157:layer:AWSLambda-Python-AWS-SDK:1"
      ]
    }
  },
  "LambdaRole": {
    "Type": "AWS::IAM::Role",
    "Properties": {
      "AssumeRolePolicyDocument": {
        "Version": "2012-10-17",
        "Statement": [
          {
            "Effect": "Allow",
            "Principal": {
              "Service": [
                "lambda.amazonaws.com"
              ]
            },
            "Action": [
              "sts:AssumeRole"
            ]
          }
        ]
      },
      "Path": "/",
      "Policies": [
        {
          "PolicyName": "lambda-logs",
          "PolicyDocument": {
            "Version": "2012-10-17",
            "Statement": [
              {
                "Effect": "Allow",
                "Action": [
                  "logs:CreateLogGroup",
                  "logs:CreateLogStream",
                  "logs:PutLogEvents"
                ],
                "Resource": [
                  "arn:aws:logs:*:*:*"
                ]
              }
            ]
          }
        },
        {
          "PolicyName": "ES",
          "PolicyDocument": {
            "Version": "2012-10-17",
            "Statement": [
              {
                "Effect": "Allow",
                "Action": [
                  "es:*"
                ],
                "Resource": {
                  "Fn::Join": [
                    "",
                    [
                      {
                        "Fn::ImportValue": {
                          "Fn::Join": [
                            ":",
                            [
                              {
                                "Ref": "AppSyncApiId"
                              },
                              "GetAtt",
                              "Elasticsearch",
                              "DomainArn"
                            ]
                          ]
                        }
                      },
                      "/*"
                    ]
                  ]
                }
              }
            ]
          }
        }
      ]
    }
  }
}
Thread Thread
 
rpostulart profile image
rpostulart AWS Community Builders

Thx, will look into it and update this post accordingly

Collapse
 
zolcsi profile image
Zoltan Magyar

Great article and super examples, thank you! Something that made me think is why the two dimensional array of the polygon is done with 3dim array in the schema def:
coordinates: [[[Float]]]!
Also the react counterpart is a 3dim array?
Any thoughts on that?

Collapse
 
stelldogg profile image
stelldogg

Do you have a good example of how to push data to the backend from React when using something like a nested Coordinate object? I struggle with using the normal setFormData pattern when custom objects are in play.

Collapse
 
njreid profile image
Nicholas Reid

Hey rpostulart, love this detailed article. Thanks for putting it together. Wondering if you might have a moment to try out our new official Amplify Geo category? It's currently in developer preview. There are some things you did here which become much easier.

docs.amplify.aws/lib/geo/getting-s...

Collapse
 
rpostulart profile image
rpostulart AWS Community Builders

Yes I have. It is a great replacement instead of using Elasticsearch.

I am having a discussion with one of your team member about how to have a realtime api response instead that it is streamed to eventbridge and then you need to put a lambda in between to send it to appsync (subscription)

Collapse
 
fudr profile image
fudr

Thanks, Is there a way to create a subscription based on this query? Lets say I want to get updated coordinates of the runners filtered by distance? I knos that subscriptions are based on mutations, how can I do that if posible?

Collapse
 
fudr profile image
fudr

Nice! I would love to know the way to create events from the same ReactNative app, different screen?