Amplify API Custom Resolver를 사용한 ES 쿼리

김현성·2021년 1월 21일
0

@searchable 자동 생성 쿼리의 문제점

Amplify cli에서 Dynamodb의 문제중 하나인 검색이 어렵다라는 문제를 Dynamo Stream을 통하여 Elasticsearch와 Sync 시키는 람다 함수를 실행시키는 방법으로 해결하였습니다.

이는 Elasticsearch EC2인스턴트를 이용하는 방법으로 기본적인 유지 비용이 존재하지만 개발하는 입장에서는 DynamoDB에서 검색을 어떻게 구현할것인지에 대한 어려움을 가볍게 해결할수 있는 솔루션입니다.

하지만 amplify cli에서 @searchable로 자동 생성되는 쿼리는 데이터의 최상위 레벨에 있는 요소만 필터를 할수 있습니다.

예를들어


type Todo @model @searchable{
    id: ID!
    item: String
    location: Location
}

type Location {
    geohash: Int
    lat: Float
    lng: Float
}

위와 같은 스키마에서 생성된 'searchTodos' 쿼리에서는 Location의 항목에 대한 필터가 불가능합니다.


type Todo @model @searchable{
    id: ID!
    item: String
    
    # Location
    geohash: Int
    lat: Float
    lng: Float
}

따라서 자동 생성된 쿼리에 필터를 사용하려면 필터가 필요한 모든 요소를 최상위 레벨로 올려야한다는 문제가 있습니다.

(관리에 어렵고 이쁘지가 않다! 😭)

해결

따라서 이를 해결하고자 커스텀 리졸버를 통해 Elasticsearch에 대한 별도의 쿼리를 만들어봅시다.

1. 쿼리 추가

검색에 사용할 커스텀 쿼리를 추가해 줍니다.

schema.graphql

type Todo @model @searchable{
    id: ID!
    item: String
    location: Location
}

type Query{
    getAllTodosOnLocation(northEast: Int, southWest: Int):TodosOnLocation
}

type TodosOnLocation {
    total: Int
    items: [Todo]
}

2. 커스텀 리졸버 추가

쿼리에 사용할 리졸버를 추가해 줍니다.

resolvers/

Query.getAllTodosOnLocation.req.vlt

#set( $indexPath = "/todo/doc/_search" )
{
  "version": "2018-05-29",
  "operation": "GET",
  "path": "$indexPath",
    "params": {
        "body": {
            "size": 10000,
            "query": {
                "range": {
                    "location.geoHash": {
                        "gte": $context.args.southWest,
                        "lte": $context.args.northEast,
                    }
                }
            }
        }
    }
}


Query.getAllTodosOnLocation.res.vlt

#set( $es_items = [] )
#foreach( $entry in $context.result.hits.hits )
  $util.qr($es_items.add($entry.get("_source")))
#end
$util.toJson({
  "items": $es_items,
  "total": $ctx.result.hits.total,
})

3. 스택에 리소스 추가

쿼리의 데이터 소스를 추가해 줍니다.

stacks/CustomResources.json

"QueryPlaceFromLocation": {
      "Type": "AWS::AppSync::Resolver",
      "Properties": {
        "ApiId": { "Ref": "AppSyncApiId" },
   // 리소스 이름(@serchable 기본값)
        "DataSourceName": "ElasticSearchDomain",
        "TypeName": "Query",
  // 필드 이름
        "FieldName": "getAllTodosOnLocation",
        "RequestMappingTemplateS3Location": {
          "Fn::Sub": [
  // 커스텀 리졸버          
            "s3://${S3DeploymentBucket}/${S3DeploymentRootKey}/resolvers/Query.getAllTodosOnLocation.req.vtl",
            {
              "S3DeploymentBucket": { "Ref": "S3DeploymentBucket" },
              "S3DeploymentRootKey": { "Ref": "S3DeploymentRootKey" }
            }
          ]
        },
        "ResponseMappingTemplateS3Location": {
          "Fn::Sub": [
 // 커스텀 리졸버          
            "s3://${S3DeploymentBucket}/${S3DeploymentRootKey}/resolvers/Query.getAllTodosOnLocation.res.vtl",
            {
              "S3DeploymentBucket": { "Ref": "S3DeploymentBucket" },
              "S3DeploymentRootKey": { "Ref": "S3DeploymentRootKey" }
            }
          ]
        }
      }
    }

4. 배포 및 쿼리

모든 설정이 끝나고 배포후 쿼리를 해봅시다.

amplify push api
query MyQuery3 {
  getAllTodosOnLocation(northEast: 1005, southWest: 999) {
    total
    items{
      id
    }
  }
}

결과 (999<=geoHash<=1005)

{
  "data": {
    "getAllTodosOnLocation": {
      "total": 3,
      "items": [
        {
          "id": "2",
          "location": {
            "geoHash": 1001,
            "lat": 1.5,
            "lng": 13
          }
        },
        {
          "id": "1",
          "location": {
            "geoHash": 1000,
            "lat": 1.5,
            "lng": 13
          }
        },
        {
          "id": "3",
          "location": {
            "geoHash": 1003,
            "lat": 1.5,
            "lng": 13
          }
        }
      ],
    }
  }
}

참고 자료:
https://docs.amplify.aws/cli/graphql-transformer/resolvers
https://dev.classmethod.jp/articles/amplify-tips-series-7-korean/#toc-5

0개의 댓글