amplify-swift icon indicating copy to clipboard operation
amplify-swift copied to clipboard

Search API

Open TheBenck opened this issue 5 years ago • 7 comments

Is your feature request related to a problem? Please describe. There doesn't seem to be any way to query searchable models with the Amplify Libraries API plugin, making Libraries unusable for the search case. A first-class api for this would be very useful.

Describe the solution you'd like Something like list queries but for searching. E.g.: Amplify.API.query(request: .search(...

Describe alternatives you've considered The only alternative is to go back to the mobile SDK. Correct me if I'm missing something.

TheBenck avatar Jun 26 '20 13:06 TheBenck

@TheBenck The list query API allows you to specify a predicate for searching your models. Does that meet your needs? If not, can you provide some more detail around your use case and explain what features are missing?

palpatim avatar Jun 26 '20 15:06 palpatim

@palpatim Thanks! This is what I needed. Is there more documentation somewhere on the predicate? Also, are sorting and limiting possible like with the functions generated for API.swift?

TheBenck avatar Jun 26 '20 22:06 TheBenck

Hey @TheBenck ,

Search queries are not support out-of-the-box yet. There are available workarounds though. The recommended one is to create a custom query, which means create your own function that returns an instance of GraphQLRequest<[M]> that contains the search query.

For example:

import Amplify

let searchTodoQuery = """
query SearchTodos($filter: SearchableTodoFilterInput) {
  searchTodos(filter: $filter) {
    items {
      id
      name
      done
      createdAt
      description
    }
  }
}
"""

extension GraphQLRequest where R == [Todo] {
    static func search(_: Todo.Type, byKeywords keywords: String? = nil) -> Self {
        var variables: [String: Any] = [:]

        if let matches = keywords {
            variables = [
                "filter": [
                    "name": [
                        "match": matches,
                    ],
                ],
            ]
        }

        return GraphQLRequest(document: searchTodoQuery,
                              variables: variables,
                              responseType: [Todo].self,
                              decodePath: "searchTodos.items")
    }
}

Notes:

  • I have a model Todo with @searchable
  • If you check the compiled GraphQL at amplify/build/schema.graphql, you will find the query searchTodos and the related types
  • then I created an extension to GraphQLRequest where R == [Todo] (the response type is an array of Todo)
  • that enables me to call:
_ = Amplify.API.query(request: .search(Todo.self, byKeywords: "important")) {
    switch $0 {
    case let .success(result):
        print(result)
    case let .failure(error):
        print("error")
        print(error)
    }
}

Tests:

  • I created 3 todos using AppSync console, 2 of them had "important" in the name
  • If I query passing byKeywords: "important" I get 2 results back
  • If I query not passing byKeywords (or passing nil) I get all 3 results back

Next steps:

  • I'll mark this issue as a feature request so you or anyone else looking for @searchable support can follow the progress here
  • We have a documentation PR out that @lawmicha is working at documenting custom queries
  • We're actively working on adding first-class support for @searchable queries so you won't have to create the custom query yourself and build the filter using the QueryPredicate operators (at least for the majority of cases, more complex/customized cases will have to fallback to custom queries)

drochetti avatar Aug 18 '20 04:08 drochetti

we have added some documentation for custom queries here: https://docs.amplify.aws/lib/graphqlapi/advanced-workflows/q/platform/ios however, it does not cover the searchable use case directly.

As mentioned, @drochetti's comment above sums it up as the recomended approach for the workaround.

I've expanded on this example to cover all possible search API parameters from AppSync:

import Amplify
import AWSPluginsCore

struct AppSyncSearchResponse<Element: Model>: Codable {
    let items: [Element]
    let nextToken: String?
    let total: Int?
}

extension GraphQLRequest {
    static func search<M: Model>(_ modelType: M.Type,
                                 filter: [String: Any]? = nil,
                                 from: Int? = nil,
                                 limit: Int? = nil,
                                 nextToken: String? = nil,
                                 sort: QuerySortBy? = nil) -> GraphQLRequest<AppSyncSearchResponse<M>> {
        let name = modelType.modelName
        let documentName = "search" + name + "s"
        var variables = [String: Any]()
        if let filter = filter {
            variables.updateValue(filter, forKey: "filter")
        }
        if let from = from {
            variables.updateValue(from, forKey: "from")
        }
        if let limit = limit {
            variables.updateValue(limit, forKey: "limit")
        }
        if let nextToken = nextToken {
            variables.updateValue(nextToken, forKey: "nextToken")
        }
        if let sort = sort {
            switch sort {
            case .ascending(let field):
                let sort = [
                    "direction": "asc",
                    "field": field.stringValue
                ]
                variables.updateValue(sort, forKey: "sort")
            case .descending(let field):
                let sort = [
                    "direction": "desc",
                    "field": field.stringValue
                ]
                variables.updateValue(sort, forKey: "sort")
            }
        }
        let graphQLFields = modelType.schema.sortedFields.filter { field in
            !field.hasAssociation || field.isAssociationOwner
        }.map { (field) -> String in
            field.name
        }.joined(separator: "\n      ")
        let document = """
        query \(documentName)($filter: Searchable\(name)FilterInput, $from: Int, $limit: Int, $nextToken: String, $sort: Searchable\(name)SortInput) {
          \(documentName)(filter: $filter, from: $from, limit: $limit, nextToken: $nextToken, sort: $sort) {
            items {
              \(graphQLFields)
            }
            nextToken
            total
          }
        }
        """
        return GraphQLRequest<AppSyncSearchResponse<M>>(document: document,
                                                        variables: variables.count != 0 ? variables : nil,
                                                        responseType: AppSyncSearchResponse<M>.self,
                                                        decodePath: documentName)
    }
}

You can drop this code into your project and start using it as it exposes the AppSync search API exactly as provisioned.

Example call pattern:

let filter: [String: Any] = [
    "name": [
        "matchPhrase": "first"
    ]
]
Amplify.API.query(request: .search(Blog6.self,
                                   filter: filter,
                                   limit: 1000,
                                   sort: QuerySortBy.ascending(Blog6.keys.id))) 

Sample App project

lawmicha avatar Jan 26 '21 22:01 lawmicha

Hi @TheBenck @randeepbhatia ,

Do you have a example schema you could provide so that we can make sure first-class support covers your use case?

Could you also provide us with which parameters you are using with the search API?

  • which filters are you using?
  • Do you have a use for the from parameter?
  • Do you care about the total count in the response?

lawmicha avatar Jan 26 '21 22:01 lawmicha

Hi @lawmicha here is the schema for the model that I am trying to use

type Vendor @model { id: ID! name: String! address: String! features: String description: String email: [AWSEmail]! imageUrl: [AWSURL] dineInWaitTime: Int carryOutWaitTime: Int isFeatured: Boolean! location: Location! rating: Float! status: String! tags: [String] cuisineType: [String] categories: [String] hoursOfOperation: AWSJSON # This will be JSON as M,T,W,TH,F,SA,SU and hours in 24 HR clock. menuType: [MenuModel] @connection(keyName: "byMenuType", fields: ["id"]) }

My goal is to fetch vendors for a given location Filter them by a. cuisine type b. open now flag c. Dine IN / Take out flag Sort by a. Distance b. Rating c. Wait times With Pagination Don't really care about the total count if pagination can be achieved.

randeepbhatia avatar Jan 27 '21 02:01 randeepbhatia

Thank you so much for your assistance @lawmicha

randeepbhatia avatar Jan 27 '21 02:01 randeepbhatia