I have a function which returns a list of Items using elastic search and falls back on realm cache. I'm wondering how can I use Combine
to achieve the same.
I am trying to do something like this where I have a publisher for each store but I am getting stuck on the sorting them by score.
func search(for text: String) -> AnyPublisher<[Item], Error> {
return store.search(with: text)
// Invalid syntax *
.map { searchResults in
let sorted = cacheStore.search(with: text)
.map { items in
items
.map { item in (item, searchResults.first { $0.id == item.id }?.score ?? 0) }
.sorted { $0.1 > $1.1 } // by score
.map { $0.0 } // to item
}
return sorted.eraseToAnyPublisher()
}
// *
.catch { _ in cacheStore.search(with: text) }
.eraseToAnyPublisher()
}
This is the original function.
func search(for text: String, completion: @escaping (Result<[Item], Error>) -> Void) {
store.search(with: text) {
// Search network via elastic search or fall back to cache search
// searchResults is of type [(id: Int, score: Double)] where id is item.id
guard let searchResult = $0.value, $0.isSuccess else {
return self.cacheStore.search(with: text, completion: completion)
}
self.cacheStore.fetch(ids: searchResult.map { $0.id }) {
guard let items = $0.value, $0.isSuccess else {
return self.cacheStore.search(with: text, completion: completion)
}
let scoredItems = items
.map { item in (item, searchResult.first { $0.id == item.id }?.score ?? 0) }
.sorted { $0.1 > $1.1 } // by score
.map { $0.0 } // to item
completion(.success(scoredItems))
}
}
}
CodePudding user response:
I think what you are aiming for is something like the Playground below.
Most of the playground is code is code that mocks up searches using Futures
. The particularly relevant section is:
return searchNetwork(key: key)
.map { key,value in cache[key] = value; return value }
.catch {_ in searchCache(key: key) }
.eraseToAnyPublisher()
If the network request from searchNetwork
succeeds then the value passes through the map which adds it to the cache and returns the value from the network. If searchNetwork
fails then catch
will substitute the publisher that searches the cache.
import Foundation
import Combine
var cache = [
"one" : "for the money",
"two" : "for the show"
]
enum SearchError: Error {
case cacheMiss
case networkFailure
}
func searchCache(key : String) -> AnyPublisher<String, SearchError>
{
return Future<String, SearchError> { fulfill in
DispatchQueue.main.asyncAfter(deadline: .now() .seconds(1)) {
if let value = cache[key] {
fulfill(.success(value))
} else {
fulfill(.failure(.cacheMiss))
}
}
}.eraseToAnyPublisher()
}
func searchNetwork(key: String) -> AnyPublisher<(String, String), SearchError> {
return Future<(String, String), SearchError> { fulfill in
fulfill(.failure(.networkFailure))
}.eraseToAnyPublisher()
}
func search(for key: String) -> AnyPublisher<String, SearchError> {
return searchNetwork(key: key)
.map { key,value in cache[key] = value; return value }
.catch {_ in searchCache(key: key) }
.eraseToAnyPublisher()
}
let searchForOne = search(for: "one").sink(
receiveCompletion: { debugPrint($0) },
receiveValue: { print("Search for one : \($0)") }
)
let searchForThree = search(for: "three").sink(
receiveCompletion: { debugPrint($0) },
receiveValue: { print("Search for three : \($0)") }
)
CodePudding user response:
I figured out the solution by doing something like this:
let cachedPublisher = cacheStore.search(with: text)
let createPublisher: (Item) -> AnyPublisher<Item, Error> = {
return Just($0).eraseToAnyPublisher()
}
return store.search(with: request)
.flatMap { Item -> AnyPublisher<[Item], Error> in
let ids = searchResults.map { $0.id }
let results = self.cacheStore.fetch(ids: ids, filterActive: true)
.flatMap { items -> AnyPublisher<[Item], Error> in
let sorted = items
.map { item in (item, searchResults.first { $0.id == item.id }?.score ?? 0) }
.sorted { $0.1 > $1.1 } // by score
.map{ $0.0 } // to item
return Publishers.mergeMappedRetainingOrder(sorted, mapTransform: createPublisher) // Helper function that calls Publishers.MergeMany
}
return results.eraseToAnyPublisher()
}
.catch { _ in cachedPublisher }
.eraseToAnyPublisher()