Search code examples
swiftcachingsearchcombinepublisher

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


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))
        }
    }
}

Solution

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