amplify-swift
amplify-swift copied to clipboard
Search API
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 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 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?
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
Todowith@searchable - If you check the compiled GraphQL at
amplify/build/schema.graphql, you will find thequery searchTodosand the related types - then I created an extension to
GraphQLRequest where R == [Todo](the response type is an array ofTodo) - 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 passingnil) I get all 3 results back
Next steps:
- I'll mark this issue as a
feature requestso you or anyone else looking for@searchablesupport 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
@searchablequeries so you won't have to create the custom query yourself and build the filter using theQueryPredicateoperators (at least for the majority of cases, more complex/customized cases will have to fallback to custom queries)
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)))
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
filtersare you using? - Do you have a use for the
fromparameter? - Do you care about the
totalcount in the response?
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.
Thank you so much for your assistance @lawmicha