Home > Mobile >  How to use Combine to show elastic search results using network while falling back on cache in Swift
How to use Combine to show elastic search results using network while falling back on cache in Swift

Time:03-12

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()
  • Related