Skip to content

Commit 930a757

Browse files
author
Clément Le Provost
authored
Support offline search for facet values (#306)
* Implement offline search for facet values in `MirroredIndex` * Implement offline search for facet values in `OfflineIndex` * Update pod dependencies Offline search for facet values requires Offline Core’s version 1.1 (not yet released).
1 parent 9c26e3e commit 930a757

File tree

7 files changed

+187
-7
lines changed

7 files changed

+187
-7
lines changed

AlgoliaSearch-Offline-Swift.podspec

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ Pod::Spec.new do |s|
1111

1212
s.ios.deployment_target = '8.0'
1313

14-
s.dependency 'AlgoliaSearchOfflineCore-iOS', '~> 1.0'
14+
s.dependency 'AlgoliaSearchOfflineCore-iOS', '~> 1.1'
1515

1616
# Activate Core-dependent code.
1717
# WARNING: Specifying the preprocessor macro is not enough; it must be added to Swift flags as well.

Podfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
use_frameworks!
44

55
def common_deps
6-
pod 'AlgoliaSearchOfflineCore-iOS', '~> 1.0'
6+
pod 'AlgoliaSearchOfflineCore-iOS', '~> 1.1'
77
end
88

99
target "AlgoliaSearch-Offline-iOS" do

Source/Offline/MirroredIndex.swift

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1097,7 +1097,81 @@ import Foundation
10971097
let (content, error) = OfflineClient.parseResponse(searchResults)
10981098
return (MirroredIndex.tagAsLocal(content: content), error)
10991099
}
1100+
1101+
// ----------------------------------------------------------------------
1102+
// MARK: - Search for facet values
1103+
// ----------------------------------------------------------------------
11001104

1105+
/// Search for facet values, using the current request strategy to choose between online and offline.
1106+
/// Same parameters as `Index.searchForFacetValues(...)`.
1107+
///
1108+
@objc(searchForFacetValuesOf:matching:query:completionHandler:)
1109+
@discardableResult override public func searchForFacetValues(of facetName: String, matching text: String, query: Query? = nil, completionHandler: @escaping CompletionHandler) -> Operation {
1110+
// IMPORTANT: A non-mirrored index must behave exactly as an online index.
1111+
if (!mirrored) {
1112+
return super.searchForFacetValues(of: facetName, matching: text, query: query, completionHandler: completionHandler)
1113+
}
1114+
// A mirrored index launches a mixed offline/online request.
1115+
else {
1116+
let operation = MixedSearchFacetOperation(index: self, facetName: facetName, facetQuery: text, query: query, completionHandler: completionHandler)
1117+
offlineClient.mixedRequestQueue.addOperation(operation)
1118+
return operation
1119+
}
1120+
}
1121+
1122+
private class MixedSearchFacetOperation: OnlineOfflineOperation {
1123+
let facetName: String
1124+
let facetQuery: String
1125+
let query: Query?
1126+
1127+
init(index: MirroredIndex, facetName: String, facetQuery: String, query: Query?, completionHandler: @escaping CompletionHandler) {
1128+
self.facetName = facetName
1129+
self.facetQuery = facetQuery
1130+
self.query = query
1131+
super.init(index: index, completionHandler: completionHandler)
1132+
}
1133+
1134+
override func startOnlineRequest(completionHandler: @escaping CompletionHandler) -> Operation {
1135+
return index.searchForFacetValuesOnline(of: facetName, matching: facetQuery, query: query, completionHandler: completionHandler)
1136+
}
1137+
1138+
override func startOfflineRequest(completionHandler: @escaping CompletionHandler) -> Operation {
1139+
return index.searchForFacetValuesOffline(of: facetName, matching: facetQuery, query: query, completionHandler: completionHandler)
1140+
}
1141+
}
1142+
1143+
/// Search for facet values on the online API, not the local mirror.
1144+
/// This method is an alias of `Index.searchForFacetValues(...)`.
1145+
///
1146+
@objc(searchForFacetValuesOnlineOf:matching:query:completionHandler:)
1147+
@discardableResult public func searchForFacetValuesOnline(of facetName: String, matching text: String, query: Query? = nil, completionHandler: @escaping CompletionHandler) -> Operation {
1148+
return super.searchForFacetValues(of: facetName, matching: text, query: query, completionHandler: {
1149+
(content, error) in
1150+
completionHandler(MirroredIndex.tagAsRemote(content: content), error)
1151+
})
1152+
}
1153+
1154+
/// Search for facet values on the local mirror, not the online API.
1155+
/// This method is the offline equivalent of `Index.searchForFacetValues(...)`.
1156+
///
1157+
@objc(searchForFacetValuesOfflineOf:matching:query:completionHandler:)
1158+
@discardableResult public func searchForFacetValuesOffline(of facetName: String, matching text: String, query: Query? = nil, completionHandler: @escaping CompletionHandler) -> Operation {
1159+
assert(self.mirrored, "Mirroring not activated on this index")
1160+
let operation = AsyncBlockOperation(completionHandler: completionHandler) {
1161+
return self._searchForFacetValuesOffline(of: facetName, matching: text, query: query)
1162+
}
1163+
operation.completionQueue = client.completionQueue
1164+
self.offlineClient.searchQueue.addOperation(operation)
1165+
return operation
1166+
}
1167+
1168+
/// Search for facet values on the local mirror synchronously.
1169+
private func _searchForFacetValuesOffline(of facetName: String, matching text: String, query: Query?) -> (content: JSONObject?, error: Error?) {
1170+
assert(!Thread.isMainThread) // make sure it's run in the background
1171+
let searchResults = localIndex.searchForFacetValues(of: facetName, matching: text, parameters: query?.build())
1172+
return OfflineClient.parseResponse(searchResults)
1173+
}
1174+
11011175
// ----------------------------------------------------------------------
11021176
// MARK: - Notifications
11031177
// ----------------------------------------------------------------------

Source/Offline/OfflineIndex.swift

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -464,6 +464,28 @@ public struct IOError: CustomNSError {
464464
return self.multipleQueriesSync(queries, strategy: strategy?.rawValue)
465465
}
466466

467+
/// Search for facet values.
468+
/// Same parameters as `Index.searchForFacetValues(...)`.
469+
///
470+
@discardableResult
471+
@objc(searchForFacetValuesOf:matching:query:completionHandler:)
472+
public func searchForFacetValues(of facetName: String, matching text: String, query: Query? = nil, completionHandler: @escaping CompletionHandler) -> Operation {
473+
let queryCopy = query != nil ? Query(copy: query!) : nil
474+
let operation = BlockOperation() {
475+
let (content, error) = self.searchForFacetValuesSync(of: facetName, matching: text, query: queryCopy)
476+
self.callCompletionHandler(completionHandler, content: content, error: error)
477+
}
478+
client.searchQueue.addOperation(operation)
479+
return operation
480+
}
481+
482+
/// Search for facet values (synchronous version).
483+
///
484+
private func searchForFacetValuesSync(of facetName: String, matching text: String, query: Query? = nil) -> APIResponse {
485+
let searchResults = self.localIndex.searchForFacetValues(of: facetName, matching: text, parameters: query?.build())
486+
return OfflineClient.parseResponse(searchResults)
487+
}
488+
467489
// ----------------------------------------------------------------------
468490
// MARK: - Write operations
469491
// ----------------------------------------------------------------------

Tests/Offline/MirroredIndexTests.swift

Lines changed: 59 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -85,11 +85,14 @@ class MirroredIndexTests: OfflineTestCase {
8585
return
8686
}
8787
// Populate the online index.
88-
index.addObjects(Array(self.moreObjects.values)) { (content, error) in
89-
guard let taskID = content?["taskID"] as? Int else { XCTFail(); return }
90-
index.waitTask(withID: taskID) { (content, error) in
91-
XCTAssertNil(error)
92-
completionBlock(error)
88+
index.setSettings(self.settings) { (content, error) in
89+
XCTAssertNil(error)
90+
index.addObjects(Array(self.moreObjects.values)) { (content, error) in
91+
guard let taskID = content?["taskID"] as? Int else { XCTFail(); return }
92+
index.waitTask(withID: taskID) { (content, error) in
93+
XCTAssertNil(error)
94+
completionBlock(error)
95+
}
9396
}
9497
}
9598
}
@@ -625,4 +628,55 @@ class MirroredIndexTests: OfflineTestCase {
625628
}
626629
waitForExpectations(timeout: onlineExpectationTimeout, handler: nil)
627630
}
631+
632+
func testSearchForFacetValues() {
633+
let expectation_indexing = self.expectation(description: "indexing")
634+
let expectation_onlineQuery = self.expectation(description: "onlineQuery")
635+
let expectation_offlineQuery = self.expectation(description: "offlineQuery")
636+
let expectation_mixedQuery = self.expectation(description: "offlineQuery")
637+
638+
// Populate the online index & sync the offline mirror.
639+
let index: MirroredIndex = client.index(withName: safeIndexName(#function))
640+
sync(index: index) { (error) in
641+
if let error = error { XCTFail("\(error)"); return }
642+
643+
// Query the online index explicitly.
644+
index.searchForFacetValuesOnline(of: "series", matching: "") { (content, error) in
645+
XCTAssertNil(error)
646+
guard let facetHits = content?["facetHits"] as? [JSONObject] else { XCTFail(); return }
647+
XCTAssertEqual(2, facetHits.count)
648+
XCTAssertEqual("remote", content?["origin"] as? String)
649+
expectation_onlineQuery.fulfill()
650+
}
651+
652+
// Query the offline index explicitly.
653+
index.searchForFacetValuesOffline(of: "series", matching: "") { (content, error) in
654+
XCTAssertNil(error)
655+
guard let facetHits = content?["facetHits"] as? [JSONObject] else { XCTFail(); return }
656+
XCTAssertEqual(1, facetHits.count)
657+
XCTAssertEqual("Peanuts", facetHits[0]["value"] as? String)
658+
XCTAssertEqual(3, facetHits[0]["count"] as? Int)
659+
XCTAssertEqual("local", content?["origin"] as? String)
660+
expectation_offlineQuery.fulfill()
661+
}
662+
663+
// Test offline fallback.
664+
self.client.readHosts = [ "unknown.algolia.com" ]
665+
index.requestStrategy = .fallbackOnFailure
666+
let query = Query()
667+
query.query = "snoopy"
668+
index.searchForFacetValues(of: "series", matching: "pea", query: query) { (content, error) in
669+
XCTAssertNil(error)
670+
guard let facetHits = content?["facetHits"] as? [JSONObject] else { XCTFail(); return }
671+
XCTAssertEqual(1, facetHits.count)
672+
XCTAssertEqual("Peanuts", facetHits[0]["value"] as? String)
673+
XCTAssertEqual(1, facetHits[0]["count"] as? Int)
674+
XCTAssertEqual("local", content?["origin"] as? String)
675+
expectation_mixedQuery.fulfill()
676+
}
677+
678+
expectation_indexing.fulfill()
679+
}
680+
waitForExpectations(timeout: onlineExpectationTimeout, handler: nil)
681+
}
628682
}

Tests/Offline/OfflineIndexTests.swift

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -467,4 +467,29 @@ class OfflineIndexTests: OfflineTestCase {
467467
}
468468
waitForExpectations(timeout: 10, handler: nil)
469469
}
470+
471+
func testSearchForFacetValues() {
472+
let expectation = self.expectation(description: #function)
473+
let index = client.offlineIndex(withName: #function)
474+
let transaction = index.newTransaction()
475+
transaction.setSettings(settings) { (content, error) in
476+
XCTAssertNil(error)
477+
transaction.saveObjects(Array(self.objects.values)) { (content, error) in
478+
XCTAssertNil(error)
479+
transaction.commit() { (content, error) in
480+
guard error == nil else { XCTFail(); return }
481+
let query = Query(query: "snoopy")
482+
index.searchForFacetValues(of: "series", matching: "pea", query: query) { (content, error) in
483+
guard let content = content else { XCTFail(); return }
484+
guard let facetHits = content["facetHits"] as? [JSONObject] else { XCTFail(); return }
485+
XCTAssertEqual(1, facetHits.count)
486+
XCTAssertEqual("Peanuts", facetHits[0]["value"] as? String)
487+
XCTAssertEqual(1, facetHits[0]["count"] as? Int)
488+
expectation.fulfill()
489+
}
490+
}
491+
}
492+
}
493+
waitForExpectations(timeout: expectationTimeout, handler: nil)
494+
}
470495
}

Tests/Offline/OfflineTestCase.swift

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,11 @@ class OfflineTestCase: XCTestCase {
5050
super.tearDown()
5151
}
5252

53+
let settings: [String: Any] = [
54+
"searchableAttributes": ["name", "kind", "series"],
55+
"attributesForFaceting": ["searchable(series)"]
56+
]
57+
5358
let objects: [String: JSONObject] = [
5459
"snoopy": [
5560
"objectID": "1",

0 commit comments

Comments
 (0)