<commit_before>//
//  Copyright (c) 2016 Algolia
//  http://www.algolia.com/
//
//  Permission is hereby granted, free of charge, to any person obtaining a copy
//  of this software and associated documentation files (the "Software"), to deal
//  in the Software without restriction, including without limitation the rights
//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
//  copies of the Software, and to permit persons to whom the Software is
//  furnished to do so, subject to the following conditions:
//
//  The above copyright notice and this permission notice shall be included in
//  all copies or substantial portions of the Software.
//
//  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
//  THE SOFTWARE.
//

import AlgoliaSearch
import Foundation


/// A value of a given facet, together with its number of occurrences.
///
public struct FacetValue {
    let value: String
    let count: Int
    
}


/// Manages search on an Algolia index.
///
/// The purpose of this class is to maintain a state between searches and handle pagination.
///
public class SearchHelper {
    public let index: Index
    public let completionHandler: CompletionHandler
    
    /// The query that will be used for the next search.
    /// It can be modified at will. It is not taken into account until the `search()` method is called.
    public var query: Query = Query()
    
    // MARK: On-going request
    // ----------------------
    
    /// The query corresponding to the currently on-going search, if any.
    private var requestedQuery: Query?

    /// The last requested page.
    private var requestedPage: Int = 0
    
    /// The initial page, i.e. that of the first request. Defaults to 0, unless the user provided one.
    private var initialPage: Int {
        return requestedQuery?.page?.integerValue ?? 0
    }
    
    // MARK: Last received results
    // ---------------------------
    
    /// Total number of pages in results.
    private var nbPages: Int = 0

    /// The initial page for the last received results.
    public var receivedInitialPage: Int {
        return receivedQuery?.page?.integerValue ?? 0
    }

    /// The last received page.
    public private(set) var receivedPage: Int = 0
    
    /// The query corresponding to the last received results.
    public private(set) var receivedQuery: Query?
    
    /// Results for the last query.
    ///
    /// This contains results of all pages for the last query, i.e. the results of the initial `search()` call, plus
    /// any subsequent calls to `loadMore()`. It is reset when results are received for a new query, i.e. after
    /// another call to `search()`.
    ///
    public private(set) var hits: [[String: AnyObject]] = []
    
    /// Facets returned by the last query.
    /// Maps each facet name to the array of corresponding (value, count) pairs.
    ///
    public private(set) var facets: [String: [FacetValue]] = [:]
    
    // MARK: -
    
    public init(index: Index, completionHandler: CompletionHandler) {
        self.index = index
        self.completionHandler = completionHandler
    }

    /// Search.
    public func search() {
        requestedQuery = Query(copy: query)
        requestedPage = initialPage
        index.search(requestedQuery!, completionHandler: self.handleResults)
    }
    
    /// Load more content, if possible.
    public func loadMore() {
        // Cannot load more when no results have been received.
        if receivedQuery == nil {
            return
        }
        // Must not load more if the results are outdated with respect to the currently on-going search.
        if requestedQuery != receivedQuery {
            return
        }
        let nextPage = receivedPage + 1
        // Cannot load more if the end has already been reached.
        if nextPage >= nbPages {
            return
        }
        // Must not load more if already loading.
        if nextPage <= requestedPage {
            return
        }
        // OK, everything's fine; let's go!
        let newQuery = Query(copy: requestedQuery!)
        requestedPage = nextPage
        newQuery.page = nextPage
        index.search(newQuery, completionHandler: self.handleResults)
    }

    /// Completion handler for search requests.
    private func handleResults(content: [String: AnyObject]?, error: NSError?) {
        do {
            receivedQuery = self.requestedQuery
            if content != nil {
                try _handleResults(content!)
            }
            completionHandler(content: content, error: error)
        } catch let e as NSError {
            completionHandler(content: nil, error: e)
        }
    }
    
    /// Exception-throwing flavor of `handleResults`.
    private func _handleResults(content: [String: AnyObject]) throws {
        guard let receivedPage = content["page"] as? Int,
            nbPages = content["nbPages"] as? Int,
            hits = content["hits"] as? [[String: AnyObject]] else {
                throw NSError(domain: Client.ErrorDomain, code: 500, userInfo: [NSLocalizedDescriptionKey: "Invalid response from server"])
        }
        // Update page.
        self.receivedPage = receivedPage
        self.nbPages = nbPages
        
        // Update hits.
        if receivedPage == initialPage {
            self.hits = hits
        } else {
            self.hits.appendContentsOf(hits)
        }
        
        // Update facets.
        self.facets.removeAll()
        if let facets = content["facets"] as? [String: [String: Int]] {
            for (facetName, facetValues) in facets {
                var values = [FacetValue]()
                for (value, count) in facetValues {
                    values.append(FacetValue(value: value, count: count))
                }
                self.facets[facetName] = values
            }
        }
    }
}<commit_msg>Always mention the refined facets in the results<commit_after>//
//  Copyright (c) 2016 Algolia
//  http://www.algolia.com/
//
//  Permission is hereby granted, free of charge, to any person obtaining a copy
//  of this software and associated documentation files (the "Software"), to deal
//  in the Software without restriction, including without limitation the rights
//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
//  copies of the Software, and to permit persons to whom the Software is
//  furnished to do so, subject to the following conditions:
//
//  The above copyright notice and this permission notice shall be included in
//  all copies or substantial portions of the Software.
//
//  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
//  THE SOFTWARE.
//

import AlgoliaSearch
import Foundation


/// A value of a given facet, together with its number of occurrences.
///
public struct FacetValue {
    let value: String
    let count: Int
    
}


/// Manages search on an Algolia index.
///
/// The purpose of this class is to maintain a state between searches and handle pagination.
///
public class SearchHelper {
    public let index: Index
    public let completionHandler: CompletionHandler
    
    /// The query that will be used for the next search.
    /// It can be modified at will. It is not taken into account until the `search()` method is called.
    public var query: Query = Query()
    
    // MARK: On-going request
    // ----------------------
    
    /// The query corresponding to the currently on-going search, if any.
    private var requestedQuery: Query?

    /// The last requested page.
    private var requestedPage: Int = 0
    
    /// The initial page, i.e. that of the first request. Defaults to 0, unless the user provided one.
    private var initialPage: Int {
        return requestedQuery?.page?.integerValue ?? 0
    }
    
    // MARK: Last received results
    // ---------------------------
    
    /// Total number of pages in results.
    private var nbPages: Int = 0

    /// The initial page for the last received results.
    public var receivedInitialPage: Int {
        return receivedQuery?.page?.integerValue ?? 0
    }

    /// The last received page.
    public private(set) var receivedPage: Int = 0
    
    /// The query corresponding to the last received results.
    public private(set) var receivedQuery: Query?
    
    /// Results for the last query.
    ///
    /// This contains results of all pages for the last query, i.e. the results of the initial `search()` call, plus
    /// any subsequent calls to `loadMore()`. It is reset when results are received for a new query, i.e. after
    /// another call to `search()`.
    ///
    public private(set) var hits: [[String: AnyObject]] = []
    
    /// Facets returned by the last query.
    /// Maps each facet name to the array of corresponding (value, count) pairs.
    ///
    public private(set) var facets: [String: [FacetValue]] = [:]
    
    // MARK: -
    
    public init(index: Index, completionHandler: CompletionHandler) {
        self.index = index
        self.completionHandler = completionHandler
    }

    /// Search.
    public func search() {
        requestedQuery = Query(copy: query)
        requestedPage = initialPage
        index.search(requestedQuery!, completionHandler: self.handleResults)
    }
    
    /// Load more content, if possible.
    public func loadMore() {
        // Cannot load more when no results have been received.
        if receivedQuery == nil {
            return
        }
        // Must not load more if the results are outdated with respect to the currently on-going search.
        if requestedQuery != receivedQuery {
            return
        }
        let nextPage = receivedPage + 1
        // Cannot load more if the end has already been reached.
        if nextPage >= nbPages {
            return
        }
        // Must not load more if already loading.
        if nextPage <= requestedPage {
            return
        }
        // OK, everything's fine; let's go!
        let newQuery = Query(copy: requestedQuery!)
        requestedPage = nextPage
        newQuery.page = nextPage
        index.search(newQuery, completionHandler: self.handleResults)
    }

    /// Completion handler for search requests.
    private func handleResults(content: [String: AnyObject]?, error: NSError?) {
        do {
            receivedQuery = self.requestedQuery
            if content != nil {
                try _handleResults(content!)
            }
            completionHandler(content: content, error: error)
        } catch let e as NSError {
            completionHandler(content: nil, error: e)
        }
    }
    
    /// Exception-throwing flavor of `handleResults`.
    private func _handleResults(content: [String: AnyObject]) throws {
        guard let receivedPage = content["page"] as? Int,
            nbPages = content["nbPages"] as? Int,
            hits = content["hits"] as? [[String: AnyObject]] else {
                throw NSError(domain: Client.ErrorDomain, code: 500, userInfo: [NSLocalizedDescriptionKey: "Invalid response from server"])
        }
        // Update page.
        self.receivedPage = receivedPage
        self.nbPages = nbPages
        
        // Update hits.
        if receivedPage == initialPage {
            self.hits = hits
        } else {
            self.hits.appendContentsOf(hits)
        }
        
        // Update facets.
        self.facets.removeAll()
        if let requestedFacets = receivedQuery!.facets,
            returnedFacets = content["facets"] as? [String: [String: Int]] {
            let queryBuilder = QueryBuilder(query: receivedQuery!)
            // NOTE: We iterate on the *requested* facets, because the search may return no results and therefore no
            // facets, whereas we want the arrays to be always populated at least for the refined values.
            for facetName in requestedFacets {
                let returnedValues = returnedFacets[facetName] ?? [String: Int]()
                var values = [FacetValue]()
                for (value, count) in returnedValues {
                    values.append(FacetValue(value: value, count: count))
                }
                // Make sure there is a value at least for the refined values.
                for value in queryBuilder.listConjunctiveFacetRefinements(facetName) {
                    if returnedValues[value] == nil {
                        values.append(FacetValue(value: value, count: 0))
                    }
                }
                self.facets[facetName] = values
            }
        }
    }
}
{'fraction_non_alphanumeric': 0.0928257468524175, 'fraction_numerical': 0.002160470833643746, 'mean_word_length': 2.0995151235280534, 'pattern_counts': {'":': 0, '<': 5, '<?xml version=': 0, '>': 5, 'https://': 0, 'lorem ipsum': 0, 'www.': 2, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '4479115', 'n_tokens_mistral': 3487, 'n_tokens_neox': 3344, 'n_words': 1397}