meilisearch / meilisearch-swift

Swift client for the Meilisearch API
https://www.meilisearch.com
MIT License
93 stars 26 forks source link

Perform multiple searches in one single HTTP request #379

Open brunoocasali opened 1 year ago

brunoocasali commented 1 year ago

⚠️ This issue is generated, it means the examples and the namings do not necessarily correspond to the language of this repository. Also, if you are a maintainer, feel free to add any clarification and instruction about this issue.

Sorry if this is already partially/completely implemented, feel free to let me know about the state of this issue in the repo.

Related to https://github.com/meilisearch/integration-guides/issues/251


Related to:

Create a new client method called multiSearch/multi_search, which will request POST /multi-search with a body payload containing a structure similar to this:

{
   "queries": [ 
      { "indexUid": "movie", "q": "wonder", "limit": 10 }, 
      { "indexUid": "books", "q": "king", "page": 2 } 
    ]
}

Each object is a simple search object sent in the POST /indexes/:indexUid/search request.

Pay attention to the response, which will follow the order of the requests object and will look like this (note the new indexUid key in the response):

{
   "results": [ 
      {
         "indexUid": "movie",
         "hits": [ { "title": "wonderwoman" } ],
         // other search results fields: processingTimeMs, limit, ...
      },
      {
         "indexUid": "books",
         "hits": [ { "title": "king kong theory" } ],
         // other search results fields: processingTimeMs, limit, ...
      },
   ]
}

TODO:

Sherlouk commented 1 year ago

@brunoocasali how do other compiler safe languages handle this API?

It's proving exceptionally difficult to have a method which expects an array of generics with different document types.

The best I've got (code below) is using message packs which syntaxically to the user looks nice but I'm unable to figure out how to zip up the pack of expected document types to an array of data from the API. I think this is because the feature is so new in Swift it lacks things like message pack iteration.

Work in Progress

  @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *)
  func multiSearch(
    _ searchParameters: [MultiSearchParameters]
  ) async throws -> (repeat Searchable) {

    let data: Data
    do {
      var jsonBuilder = [[String: Any]]()

      for searchParameter in searchParameters {
        let jsonData = try JSONEncoder().encode(searchParameter.parameters)

        var jsonObject = try JSONSerialization.jsonObject(with: jsonData) as! [String: Any]
        jsonObject["indexUid"] = searchParameter.indexUid

        jsonBuilder.append(jsonObject)
      }

      data = try JSONSerialization.data(withJSONObject: [ "queries": jsonBuilder ])
    } catch {
      throw MeiliSearch.Error.invalidJSON
    }

    return try await withCheckedThrowingContinuation { continuation in
      self.request.post(api: "/multi-search", data) { result in
        switch result {
        case .success(let data):

          do {
            let resultHolder = try JSONSerialization.jsonObject(with: data) as! [String: [Any]]

            let results: [Data] = try resultHolder["results"]!.map {
              try JSONSerialization.data(withJSONObject: $0)
            }

            print(results) // An array of JSON objects in Data form, ready for decoding.

            // ... magic?
          } catch {
            continuation.resume(throwing: error)
          }

        case .failure(let failure):
          continuation.resume(throwing: failure)
        }
      }
    }
  }

Usage:

    let params: [MultiSearchParameters] = [
      .init(indexUid: "indexA", parameters: .query("b")),
      .init(indexUid: "indexB", parameters: .init(query: "b", limit: 5))
    ]

    let results: (Searchable, Searchable) = try await search.multiSearch(params)
    print(results.0.hits.first?.document.id)

The simple solution would be to return an array of undecoded types. So the usage would look like:

let params: [MultiSearchParameters] = [
  .init(indexUid: "indexA", parameters: .query("b")),
  .init(indexUid: "indexB", parameters: .init(query: "b", limit: 5))
]

let results = try await search.multiSearch(params)
// notice the new `.decode()` function to move from the undecoded type to something they can use. 
// this helps the compiler to know what the type is though simplifying the solution.
let firstResult: Searchable<Movie> = results[0].decode()
let secondResult: Searchable<Animal> = results[1].decode()

This feels like an okay compromise. The API is still pretty clean and clear what's happening.

It does lose the safety though where a user could do results[2].decode() and crash due to there not being that many searches. The first solution wouldn't allow this.

brunoocasali commented 11 months ago

This will be an issue to be accomplished here indeed.

I guess maybe you could take inspiration of those SDKS: Java (proposal), Rust and C#

Does it helps?

Sherlouk commented 11 months ago

Unfortunately not, none of the three solutions above appear to demonstrate a compiler-safe and type-safe solution which allows for queries to different indexes with different document types.

The Rust one for example seems to show that all documents across the multiple queries must be of type Movie.

This would be an easy limitation for us to implement, but one which dramatically reduces the usefulness.

Separating the decode step (my approach above) adds one extra step to the execution but allows for mixed result responses. Think it's what we need to go for, for now, until more language capabilities are added (message packs)

brunoocasali commented 11 months ago

Separating the decode step (my approach above) adds one extra step to the execution but allows for mixed result responses.

I guess it is the way to go in this case @Sherlouk 😢.

JOyo246 commented 1 month ago

just to double check, multiSearch is not available in the Swift SDK?

Sherlouk commented 1 month ago

Not at this time, we've yet to identify a completely compile/type safe solution which works in Swift.