Skip to main content
Since May 1st, 2024, Apple requires all iOS apps to include a privacy manifest. For more details, see Privacy Manifest.
You may need to query Algolia’s servers from the backend instead of the frontend, while still being able to reuse InstantSearch widgets. Possible motivations could be for security restrictions, for SEO purposes, or to enrich the data sent by the custom server (fetch Algolia data and data from your servers). Keep in mind though that you should use frontend search for performance and high availability reasons. By the end of this guide, you will have learned how to use InstantSearch with your own backend architecture to query Algolia. Even if you’re not using Algolia on your backend and still want to benefit from using InstantSearch, then this guide is also for you.

A quick overview on how InstantSearch works

InstantSearch, as you probably know, offers reactive UI widgets that automatically update when new search events occur. Internally, it uses a Searchable interface that takes care of making network calls to get search results. The most important method of that Searchable is a simple search() function that takes in a parameter that contains all the search query parameters and then expects a callback to be called with the search results that you get from your backend.

Basic implementation of using a custom backend

The most basic implementation of using a custom backend uses the DefaultSearchClient and requires you to implement just one method: search(query:searchResultsHandler:). In this function, you use the query passed to you, make a network request to your backend server, transform the response into a SearchResults instance, and then finally call the searchResultsHandler callback with searchResults. If there’s an error, call the callback with the error. Here is an example using the Alamofire networking library.
Swift
public class DefaultCustomBackend: DefaultSearchClient {
    override public func search(_ query: Query, searchResultsHandler: @escaping SearchResultsHandler) {
        // 1
        let queryText = query.query ?? ""

        // 2
        Alamofire.request("https://yourbackend.com/search?q=\(queryText)").responseJSON { responseJson in

            if let json = responseJson.result.value as? [String: Any] {
                do {
                    // 3
                    let searchResults = try SearchResults(content: json, disjunctiveFacets: [])

                    // 4
                    searchResultsHandler(searchResults, nil)
                } catch let error {
                    // 4
                    searchResultsHandler(nil, error)
                }
            }
        }
    }
}
This is the simplest example and will work only if on your backend, you’re calling Algolia and then just forwarding your result to the mobile app without doing any modification to the JSON data.
  1. Get the query text from the Query parameter that is passed in the method.
  2. Make your request to your backend using the queryText parse in step 1.
  3. Serialize your response into a SearchResults instance. If your response data is different than the original one returned by Algolia, especially if where you’re not using Algolia at all in your backend, use a SearchResults initializer such as SearchResults(nbHits:hits).
  4. Call the searchResultsHandler function to instruct InstantSearch about the new search event, in this case the arrival of new search results, or an error.

Integrate the custom backend

  • To use the custom backend with InstantSearch core only (the Searcher class):
    Swift
    let index = DefaultCustomBackend()
    let searcher = Searcher(index: index)
    
  • To use the custom backend with InstantSearch with a single index:
    Swift
    let index = DefaultCustomBackend()
    let searcher = Searcher(index: index)
    let instantSearch = InstantSearch(searcher: searcher)
    
  • To use the custom backend with InstantSearch with multiple indices:
    Swift
    let productSearchable = CustomBackendProducts()
    let movieSearchable = CustomBackendMovies()
    instantSearch = InstantSearch.init(searchables: [productSearchable, movieSearchable], searcherIds: [SearcherId(index: "products"), SearcherId(index: "movie")])
    tableView.indices = "products,movie"
    

Advanced implementation of using a custom backend

The above snippet only covers the case of doing a basic search of hits, with conjunctive (contrary to disjunctive) filtering. Here, we’ll take a look at improving the structure of our custom backend class, as well as supporting disjunctive faceting. Start with this code snippet:
Swift
// 1
public struct BackendSearchParameters {
    var q: String?
    var disjunctiveFacets: [String]?
}
public struct BackendSearchResults {
    var total: Int
    var hits: [[String: Any]]
}

// 2
public class BackendImplementation: SearchClient<BackendSearchParameters, BackendSearchResults> {

    // 3
    public override func map(query: Query) -> BackendSearchParameters {
        let queryText = query.query

        return BackendSearchParameters(q: queryText, disjunctiveFacets: nil)
    }

    // 4
    public override func map(query: Query, disjunctiveFacets: [String], refinements: [String : [String]]) -> BackendSearchParameters {
        let queryText = query.query

        return BackendSearchParameters(q: queryText, disjunctiveFacets: disjunctiveFacets)
    }

    // 5
    public override func map(results: BackendSearchResults) -> SearchResults {
        let nbHits = results.total
        let hits = results.hits

        // 6
        let categoryFacet = ["chairs": 10, "tables": 15]
        let facets = ["category": categoryFacet]
        let extraContent = ["facets": facets]

        return SearchResults(nbHits: nbHits, hits: hits, extraContent: extraContent)
    }

    // 7
    public override func search(_ query: BackendSearchParameters, searchResultsHandler: @escaping SearchResultsHandler) {

        let queryText = query.q ?? ""

        Alamofire.request("https://yourbackend.com/search?q=\(queryText)").responseJSON { responseJson in

            if let json = responseJson.result.value as? [String: Any] {
                do {

                    let hitsJson = json["hits"] as! [String: Any]
                    let total = hitsJson["total"] as! Int
                    let hits = hitsJson["hits"] as! [[String: Any]]

                    let backendSearchResults = BackendSearchResults(total: total, hits: hits)

                    // 8
                    searchResultsHandler(backendSearchResults, nil)

                } catch let error {
                    searchResultsHandler(nil, error)
                }
            }
        }

    }
}
  1. Create your models that will hold the query parameters and results that you need to make your custom backend call.
  2. Create your class that inherits from SearchClient. Use the two models created in step 1 for the generics of that class. This will ensure strong typing and good practices throughout this implementation.
  3. Implement the basic parameter mapper function that converts a query to your parameter model. Make sure you take all the fields you need from the query parameter.
  4. Implement the advanced parameter mapper function. It’s the same as the one used in step 3, but with two more parameters that you can use for your call: disjunctiveFacets and refinements.
  5. Implement the result mapper function that converts your result model back to an Algolia SearchResults that can be understood by InstantSearch.
  6. If you want to specify the possible facets for a refinement list, make sure you specify the facets property appropriately. In the code snippet, we just give an example, but usually you’ll want to get this data from your custom result model.
  7. Implement the search method, same idea as the basic implementation. The only difference is that now it provides your custom parameter model as its parameter.
  8. When you get new search results, you serialize them into your custom response model and then call the searchResultsHandler method.

Integrate the custom backend

  • To use the custom backend with InstantSearch core only (the Searcher class):
    Swift
    let index = BackendImplementation()
    let searcher = Searcher(index: index)
    
  • To use the custom backend with InstantSearch with a single index:
    Swift
    let index = BackendImplementation()
    let searcher = Searcher(index: index)
    let instantSearch = InstantSearch(searcher: searcher)
    
  • To use the custom backend with InstantSearch with multiple indices:
    Swift
    let productSearchable = CustomBackendProducts()
    let movieSearchable = CustomBackendMovies()
    instantSearch = InstantSearch.init(searchables: [productSearchable, movieSearchable], searcherIds: [SearcherId(index: "products"), SearcherId(index: "movie")])
    tableView.indices = "products,movie"
    

Trick to get more out of the query

One little trick you can use to get more detailed information about the query being passed as a parameter is to upcast it to a SearchParameters by doing
Swift
let searchParameters = query as! SearchParameters
In that way, you can access higher level properties like disjunctiveFacets, facetRefinements, disjunctiveNumerics and numericRefinements. This can be useful when transforming Algolia’s Query.