Skip to content

How to add GPS + Location based searching to appsync (Gen 1) entity ? #14588

@BBopanna

Description

@BBopanna

Before opening, please confirm:

JavaScript Framework

React

Amplify APIs

GraphQL API

Amplify Version

v6

Amplify Categories

api

Backend

Amplify CLI

Environment information

amplify --version
12.12.4

Describe the bug

We have a requirement to - Search entities by City and Also by Location - that is to search matching entities which are within say 5km radius of the given lat (lattitude) and lon (longitude).

We have used @searchable config on the below appsync entity so that it can internally use Open Search to search the data - with the below we are able to search by City - But please let us know how to apply location based search with a given radius - a sample code/config will really help.

Our entity -

type ServiceProvider @model(queries: {get: null, list:null}, subscriptions: null) @searchable
@auth(
rules: [
{ allow: public, operations: [read], provider: iam }
]
)
{
id: ID!
domain: String!
entityType: String!

# Relations
organizationId: ID @index(name: "byOrganization")
serviceId: ID @index(name: "byService")
resourceId: ID @index(name: "byResource")

organization: Organization @hasOne(fields: ["organizationId"])
service: Service @hasOne(fields: ["serviceId"])
resource: Resource @hasOne(fields: ["resourceId"])

# Core searchable fields
name: String!
description: String

# Location
city: String!
state: String
country: String @default(value: "India")
gps: GPS

}

type GPS {
lat: Float!
lon: Float!
}

input GPSInput {
lat: Float!
lon: Float!
}

Custom geo-location search inputs and types

input LocationSearchInput {
gps: GPSInput!
radius: Float
}

type SearchServiceProvidersLocationConnection {
items: [ServiceProvider]!
total: Int
nextToken: String
}

Querry we are trying -

searchServiceProvidersByLocation(entityType: EntityType,
domain: Domain,
searchText: String,
location: LocationSearchInput,
limit: Int,
from: Int,
): SearchServiceProvidersLocationConnection @aws_iam @aws_cognito_user_pools

We feel - there is some configuration that is needed for openSearch to tell it that it should use the GPS (lat & long) and radius information to match - which is missing - can you kindly help as this is a critical enhancement for us and we are stuck!!

Resolvers -

[1] Query.searchServiceProvidersByLocation.res.vtl

#set( $args = $util.defaultIfNull($ctx.stash.transformedArgs, $ctx.args) )
#set( $indexPath = "/serviceprovider/_search" )

#set( $allowedAggFields = $util.defaultIfNull($ctx.stash.allowedAggFields, []) )
#set( $aggFieldsFilterMap = $util.defaultIfNull($ctx.stash.aggFieldsFilterMap, {}) )
#set( $nonKeywordFields = ["createdAt", "updatedAt"] )
#set( $keyFields = ["id"] )
#set( $sortValues = [] )
#set( $sortFields = [] )
#set( $aggregateValues = {} )
#set( $primaryKey = "id" )

## Handle geo-distance sorting if location provided
#if( !$util.isNullOrEmpty($args.location) )
  #set( $geoSort = {
    "_geo_distance": {
      "gps": {
        "lat": $args.location.gps.lat,
        "lon": $args.location.gps.lon
      },
      "order": "asc",
      "unit": "km"
    }
  } )
  $util.qr($sortValues.add($util.toJson($geoSort)))
#end

## Handle regular sorting
#if( !$util.isNullOrEmpty($args.sort) )
  #foreach( $sortItem in $args.sort )
    #if( $util.isNullOrEmpty($sortItem.field) )
      $util.qr($sortFields.add($primaryKey))
    #else
      $util.qr($sortFields.add($sortItem.field))
    #end

    #if( $util.isNullOrEmpty($sortItem.field) )
      #if( $nonKeywordFields.contains($primaryKey) )
        #set( $sortField = $util.toJson($primaryKey) )
      #else
        #set( $sortField = $util.toJson("${primaryKey}.keyword") )
      #end
    #else
      #if( $nonKeywordFields.contains($sortItem.field) )
        #set( $sortField = $util.toJson($sortItem.field) )
      #else
        #set( $sortField = $util.toJson("${sortItem.field}.keyword") )
      #end
    #end

    #if( $util.isNullOrEmpty($sortItem.direction) )
      #set( $sortDirection = $util.toJson({"order": "desc"}) )
    #else
      #set( $sortDirection = $util.toJson({"order": $sortItem.direction}) )
    #end

    $util.qr($sortValues.add("{$sortField: $sortDirection}"))
  #end
#end

## Add primary key sorting if not already included
#foreach( $keyItem in $keyFields )
  #if( !$sortFields.contains($keyItem) )
    #if( $nonKeywordFields.contains($keyItem) )
      #set( $sortField = $util.toJson($keyItem) )
    #else
      #set( $sortField = $util.toJson("${keyItem}.keyword") )
    #end
    #set( $sortDirection = $util.toJson({"order": "desc"}) )
    $util.qr($sortValues.add("{$sortField: $sortDirection}"))
  #end
#end

## Handle aggregations
#foreach( $aggItem in $args.aggregates )
  #if( $allowedAggFields.contains($aggItem.field) )
    #set( $aggFilter = { "match_all": {} } )
  #elseif( $aggFieldsFilterMap.containsKey($aggItem.field) )
    #set( $aggFilter = { "bool": { "should": $aggFieldsFilterMap.get($aggItem.field) } } )
  #else
    $util.error("Unauthorized to run aggregation on field: ${aggItem.field}", "Unauthorized")
  #end

  #set( $aggregateValue = {} )
  $util.qr($aggregateValue.put("filter", $aggFilter))

  #set( $aggsValue = {} )
  #set( $aggItemType = {} )

  #if( $nonKeywordFields.contains($aggItem.field) )
    $util.qr($aggItemType.put("$aggItem.type", { "field": "$aggItem.field" }))
  #else
    $util.qr($aggItemType.put("$aggItem.type", { "field": "${aggItem.field}.keyword" }))
  #end

  $util.qr($aggsValue.put("$aggItem.name", $aggItemType))
  $util.qr($aggregateValue.put("aggs", $aggsValue))
  $util.qr($aggregateValues.put("$aggItem.name", $aggregateValue))
#end

## Build the main query with filters
#set( $queryFilters = [] )

## Add authorization filter if exists
#if( !$util.isNullOrEmpty($ctx.stash.authFilter) )
  $util.qr($queryFilters.add($ctx.stash.authFilter))
#end

## Add regular filter if provided
#if( !$util.isNullOrEmpty($args.filter) )
  $util.qr($queryFilters.add($util.parseJson($util.transform.toElasticsearchQueryDSL($args.filter))))
#end

## Add entity type filter
#if( !$util.isNullOrEmpty($args.entityType) )
  #set( $entityFilter = {
    "term": {
      "entityType.keyword": "$args.entityType"
    }
  } )
  $util.qr($queryFilters.add($entityFilter))
#end

## Add geo-distance filter if location provided
#if( !$util.isNullOrEmpty($args.location) )
  #set( $radius = $util.defaultIfNull($args.location.radius, 10) )
  #set( $geoFilter = {
    "geo_distance": {
      "distance": "${radius}km",
      "gps": {
        "lat": $args.location.gps.lat,
        "lon": $args.location.gps.lon
      }
    }
  } )
  $util.qr($queryFilters.add($geoFilter))
#end

## Build main query structure
#if( !$util.isNullOrEmpty($args.searchText) )
  #set( $textQuery = {
    "multi_match": {
      "query": "$args.searchText",
      "fields": ["name^2", "description", "tags", "specialization"],
      "type": "best_fields"
    }
  } )

  #if( $queryFilters.size() > 0 )
    #set( $filter = {
      "bool": {
        "must": [$textQuery],
        "filter": $queryFilters
      }
    } )
  #else
    #set( $filter = $textQuery )
  #end
#else
  #if( $queryFilters.size() > 1 )
    #set( $filter = {
      "bool": {
        "must": [{ "match_all": {} }],
        "filter": $queryFilters
      }
    } )
  #elseif( $queryFilters.size() == 1 )
    #set( $filter = {
      "bool": {
        "must": [{ "match_all": {} }],
        "filter": [$queryFilters.get(0)]
      }
    } )
  #else
    #set( $filter = { "match_all": {} } )
  #end
#end

{
  "version": "2018-05-29",
  "operation": "GET",
  "path": "$indexPath",
  "params": {
    "body": {
      #if( $context.args.nextToken )"search_after": $util.base64Decode($args.nextToken), #end
      #if( $context.args.from )"from": $args.from, #end
      "size": #if( $args.limit ) $args.limit #else 100 #end,
      "sort": $sortValues,
      "version": true,
      "query": $util.toJson($filter),
      "aggs": $util.toJson($aggregateValues)
    }
  }
}

[2] Query.searchServiceProvidersByLocation.res.vtl

#set( $args = $util.defaultIfNull($ctx.stash.transformedArgs, $ctx.args) )
#set( $indexPath = "/serviceprovider/_search" )

#set( $allowedAggFields = $util.defaultIfNull($ctx.stash.allowedAggFields, []) )
#set( $aggFieldsFilterMap = $util.defaultIfNull($ctx.stash.aggFieldsFilterMap, {}) )
#set( $nonKeywordFields = ["createdAt", "updatedAt"] )
#set( $keyFields = ["id"] )
#set( $sortValues = [] )
#set( $sortFields = [] )
#set( $aggregateValues = {} )
#set( $primaryKey = "id" )

## Handle geo-distance sorting if location provided
#if( !$util.isNullOrEmpty($args.location) )
  #set( $geoSort = {
    "_geo_distance": {
      "gps": {
        "lat": $args.location.gps.lat,
        "lon": $args.location.gps.lon
      },
      "order": "asc",
      "unit": "km"
    }
  } )
  $util.qr($sortValues.add($util.toJson($geoSort)))
#end

## Handle regular sorting
#if( !$util.isNullOrEmpty($args.sort) )
  #foreach( $sortItem in $args.sort )
    #if( $util.isNullOrEmpty($sortItem.field) )
      $util.qr($sortFields.add($primaryKey))
    #else
      $util.qr($sortFields.add($sortItem.field))
    #end

    #if( $util.isNullOrEmpty($sortItem.field) )
      #if( $nonKeywordFields.contains($primaryKey) )
        #set( $sortField = $util.toJson($primaryKey) )
      #else
        #set( $sortField = $util.toJson("${primaryKey}.keyword") )
      #end
    #else
      #if( $nonKeywordFields.contains($sortItem.field) )
        #set( $sortField = $util.toJson($sortItem.field) )
      #else
        #set( $sortField = $util.toJson("${sortItem.field}.keyword") )
      #end
    #end

    #if( $util.isNullOrEmpty($sortItem.direction) )
      #set( $sortDirection = $util.toJson({"order": "desc"}) )
    #else
      #set( $sortDirection = $util.toJson({"order": $sortItem.direction}) )
    #end

    $util.qr($sortValues.add("{$sortField: $sortDirection}"))
  #end
#end

## Add primary key sorting if not already included
#foreach( $keyItem in $keyFields )
  #if( !$sortFields.contains($keyItem) )
    #if( $nonKeywordFields.contains($keyItem) )
      #set( $sortField = $util.toJson($keyItem) )
    #else
      #set( $sortField = $util.toJson("${keyItem}.keyword") )
    #end
    #set( $sortDirection = $util.toJson({"order": "desc"}) )
    $util.qr($sortValues.add("{$sortField: $sortDirection}"))
  #end
#end

## Handle aggregations
#foreach( $aggItem in $args.aggregates )
  #if( $allowedAggFields.contains($aggItem.field) )
    #set( $aggFilter = { "match_all": {} } )
  #elseif( $aggFieldsFilterMap.containsKey($aggItem.field) )
    #set( $aggFilter = { "bool": { "should": $aggFieldsFilterMap.get($aggItem.field) } } )
  #else
    $util.error("Unauthorized to run aggregation on field: ${aggItem.field}", "Unauthorized")
  #end

  #set( $aggregateValue = {} )
  $util.qr($aggregateValue.put("filter", $aggFilter))

  #set( $aggsValue = {} )
  #set( $aggItemType = {} )

  #if( $nonKeywordFields.contains($aggItem.field) )
    $util.qr($aggItemType.put("$aggItem.type", { "field": "$aggItem.field" }))
  #else
    $util.qr($aggItemType.put("$aggItem.type", { "field": "${aggItem.field}.keyword" }))
  #end

  $util.qr($aggsValue.put("$aggItem.name", $aggItemType))
  $util.qr($aggregateValue.put("aggs", $aggsValue))
  $util.qr($aggregateValues.put("$aggItem.name", $aggregateValue))
#end

## Build the main query with filters
#set( $queryFilters = [] )

## Add authorization filter if exists
#if( !$util.isNullOrEmpty($ctx.stash.authFilter) )
  $util.qr($queryFilters.add($ctx.stash.authFilter))
#end

## Add regular filter if provided
#if( !$util.isNullOrEmpty($args.filter) )
  $util.qr($queryFilters.add($util.parseJson($util.transform.toElasticsearchQueryDSL($args.filter))))
#end

## Add entity type filter
#if( !$util.isNullOrEmpty($args.entityType) )
  #set( $entityFilter = {
    "term": {
      "entityType.keyword": "$args.entityType"
    }
  } )
  $util.qr($queryFilters.add($entityFilter))
#end

## Add geo-distance filter if location provided
#if( !$util.isNullOrEmpty($args.location) )
  #set( $radius = $util.defaultIfNull($args.location.radius, 10) )
  #set( $geoFilter = {
    "geo_distance": {
      "distance": "${radius}km",
      "gps": {
        "lat": $args.location.gps.lat,
        "lon": $args.location.gps.lon
      }
    }
  } )
  $util.qr($queryFilters.add($geoFilter))
#end

## Build main query structure
#if( !$util.isNullOrEmpty($args.searchText) )
  #set( $textQuery = {
    "multi_match": {
      "query": "$args.searchText",
      "fields": ["name^2", "description", "tags", "specialization"],
      "type": "best_fields"
    }
  } )

  #if( $queryFilters.size() > 0 )
    #set( $filter = {
      "bool": {
        "must": [$textQuery],
        "filter": $queryFilters
      }
    } )
  #else
    #set( $filter = $textQuery )
  #end
#else
  #if( $queryFilters.size() > 1 )
    #set( $filter = {
      "bool": {
        "must": [{ "match_all": {} }],
        "filter": $queryFilters
      }
    } )
  #elseif( $queryFilters.size() == 1 )
    #set( $filter = {
      "bool": {
        "must": [{ "match_all": {} }],
        "filter": [$queryFilters.get(0)]
      }
    } )
  #else
    #set( $filter = { "match_all": {} } )
  #end
#end

{
  "version": "2018-05-29",
  "operation": "GET",
  "path": "$indexPath",
  "params": {
    "body": {
      #if( $context.args.nextToken )"search_after": $util.base64Decode($args.nextToken), #end
      #if( $context.args.from )"from": $args.from, #end
      "size": #if( $args.limit ) $args.limit #else 100 #end,
      "sort": $sortValues,
      "version": true,
      "query": $util.toJson($filter),
      "aggs": $util.toJson($aggregateValues)
    }
  }
}

Expected behavior

Need option to configure GPS based search

Reproduction steps

See Above

Metadata

Metadata

Assignees

No one assigned

    Labels

    questionGeneral question

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions