-
Notifications
You must be signed in to change notification settings - Fork 2.2k
Description
Before opening, please confirm:
- I have searched for duplicate or closed issues and discussions.
- I have read the guide for submitting bug reports.
- I have done my best to include a minimal, self-contained set of instructions for consistently reproducing the issue.
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