Skip to content

Commit 775d8cd

Browse files
feat(search): Client-level search method (#751)
1 parent 892dfd8 commit 775d8cd

File tree

10 files changed

+273
-21
lines changed

10 files changed

+273
-21
lines changed

Sources/AlgoliaSearchClient/Client/Search/SearchClient+MultiIndex.swift

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ public extension SearchClient {
7474
/**
7575
Perform a search on several indices at the same time, with one method call.
7676

77-
- Parameter queries: The list of IndexedQuery objects mathcing index name and a query to execute on it.
77+
- Parameter queries: The list of IndexedQuery objects matching index name and a query to execute on it.
7878
- Parameter strategy: The MultipleQueriesStrategy of the query.
7979
- Parameter requestOptions: Configure request locally with RequestOptions
8080
- Parameter completion: Result completion
@@ -91,7 +91,7 @@ public extension SearchClient {
9191
/**
9292
Perform a search on several indices at the same time, with one method call.
9393

94-
- Parameter queries: The list of IndexedQuery objects mathcing index name and a query to execute on it.
94+
- Parameter queries: The list of IndexedQuery objects matching index name and a query to execute on it.
9595
- Parameter strategy: The MultipleQueriesStrategy of the query.
9696
- Parameter requestOptions: Configure request locally with RequestOptions
9797
- Returns: SearchesResponse object
@@ -103,6 +103,39 @@ public extension SearchClient {
103103
return try execute(command)
104104
}
105105

106+
/**
107+
Perform a search for hits or facet values on several indices at the same time, with one method call.
108+
109+
- Parameter queries: The list of MultiSearchQuery objects encapsulating either IndexedQuery or IndexedFacetQuery.
110+
- Parameter strategy: The MultipleQueriesStrategy of the query.
111+
- Parameter requestOptions: Configure request locally with RequestOptions
112+
- Parameter completion: Result completion
113+
- Returns: Launched asynchronous operation
114+
*/
115+
@discardableResult func search(queries: [MultiSearchQuery],
116+
strategy: MultipleQueriesStrategy = .none,
117+
requestOptions: RequestOptions? = nil,
118+
completion: @escaping ResultCallback<MultiSearchResponse>) -> Operation {
119+
let command = Command.MultipleIndex.Queries(queries: queries, strategy: strategy, requestOptions: requestOptions)
120+
return execute(command, completion: completion)
121+
}
122+
123+
/**
124+
Perform a search for hits or facet values on several indices at the same time, with one method call.
125+
126+
- Parameter queries: The list of MultiSearchQuery objects encapsulating either IndexedQuery or IndexedFacetQuery.
127+
- Parameter strategy: The MultipleQueriesStrategy of the query.
128+
- Parameter requestOptions: Configure request locally with RequestOptions
129+
- Parameter completion: Result completion
130+
- Returns: MultiSearchResponse object
131+
*/
132+
@discardableResult func search(queries: [MultiSearchQuery],
133+
strategy: MultipleQueriesStrategy = .none,
134+
requestOptions: RequestOptions? = nil) throws -> MultiSearchResponse {
135+
let command = Command.MultipleIndex.Queries(queries: queries, strategy: strategy, requestOptions: requestOptions)
136+
return try execute(command)
137+
}
138+
106139
// MARK: - Multiple get objects
107140

108141
/**

Sources/AlgoliaSearchClient/Command/Command+MultipleIndex.swift

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -46,12 +46,31 @@ extension Command {
4646
let body: Data?
4747
let requestOptions: RequestOptions?
4848

49-
init(indexName: IndexName, queries: [Query], strategy: MultipleQueriesStrategy = .none, requestOptions: RequestOptions?) {
50-
let queries = queries.map { IndexedQuery(indexName: indexName, query: $0) }
51-
self.init(queries: queries, strategy: strategy, requestOptions: requestOptions)
49+
init(indexName: IndexName,
50+
queries: [Query],
51+
strategy: MultipleQueriesStrategy = .none,
52+
requestOptions: RequestOptions?) {
53+
let queries = queries
54+
.map { IndexedQuery(indexName: indexName, query: $0) }
55+
.map(MultiSearchQuery.init)
56+
self.init(queries: queries,
57+
strategy: strategy,
58+
requestOptions: requestOptions)
5259
}
60+
61+
init(queries: [IndexedQuery],
62+
strategy: MultipleQueriesStrategy = .none,
63+
requestOptions: RequestOptions?) {
64+
let queries = queries.map(MultiSearchQuery.init)
65+
self.init(queries: queries,
66+
strategy: strategy,
67+
requestOptions: requestOptions)
68+
}
69+
5370

54-
init(queries: [IndexedQuery], strategy: MultipleQueriesStrategy = .none, requestOptions: RequestOptions?) {
71+
init(queries: [MultiSearchQuery],
72+
strategy: MultipleQueriesStrategy = .none,
73+
requestOptions: RequestOptions?) {
5574
self.requestOptions = requestOptions
5675
self.body = MultipleQueriesRequest(requests: queries, strategy: strategy).httpBody
5776
self.path = (.indexesV1 >>> .multiIndex >>> .queries)
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
//
2+
// IndexedFacetQuery.swift
3+
//
4+
//
5+
// Created by Vladislav Fitc on 05/11/2021.
6+
//
7+
8+
import Foundation
9+
10+
/// The composition of search for facet values parameters with an associated index name
11+
public struct IndexedFacetQuery {
12+
13+
/// The name of the index to search in.
14+
public let indexName: IndexName
15+
16+
/// The attribute to facet on.
17+
public let attribute: Attribute
18+
19+
/// The query to filter results.
20+
public let query: Query
21+
22+
/// The textual query used to search for facets.
23+
public let facetQuery: String
24+
25+
/**
26+
- Parameter indexName: The name of the index to search in
27+
- Parameter attribute: The Attribute to facet on.
28+
- Parameter query: The Query to filter results.
29+
- Parameter facetQuery: The textual query used to search for facets.
30+
*/
31+
public init(indexName: IndexName,
32+
attribute: Attribute,
33+
facetQuery: String,
34+
query: Query) {
35+
self.indexName = indexName
36+
self.attribute = attribute
37+
self.facetQuery = facetQuery
38+
var parameters = query.customParameters ?? [:]
39+
parameters["facetQuery"] = .init(facetQuery)
40+
self.query = query.set(\.customParameters, to: parameters)
41+
}
42+
43+
}
44+
45+
extension IndexedFacetQuery: Encodable {
46+
47+
enum CodingKeys: String, CodingKey {
48+
case indexName
49+
case query = "params"
50+
case type
51+
case facet
52+
case facetQuery
53+
}
54+
55+
public func encode(to encoder: Encoder) throws {
56+
var container = encoder.container(keyedBy: CodingKeys.self)
57+
try container.encode(indexName, forKey: .indexName)
58+
try container.encode(query.urlEncodedString, forKey: .query)
59+
try container.encode(attribute, forKey: .facet)
60+
try container.encode("facet", forKey: .type)
61+
}
62+
63+
}

Sources/AlgoliaSearchClient/Models/Search/MultipleIndex/IndexedQuery.swift

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,22 @@
77

88
import Foundation
99

10+
/// The composition of search parameters with an associated index name
1011
public struct IndexedQuery {
11-
12+
13+
/// The name of the index to search in.
1214
public let indexName: IndexName
15+
16+
/// The Query to filter results.
1317
public let query: Query
14-
18+
19+
/// - parameter indexName: The name of the index to search in.
20+
/// - parameter query: The Query to filter results.
1521
public init(indexName: IndexName, query: Query) {
1622
self.indexName = indexName
1723
self.query = query
1824
}
19-
25+
2026
}
2127

2228
extension IndexedQuery: Codable {
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
//
2+
// MultiSearchQuery.swift
3+
//
4+
//
5+
// Created by Vladislav Fitc on 05/11/2021.
6+
//
7+
8+
import Foundation
9+
10+
/// Polymorphic wrapper for either and IndexedQuery or an IndexedFacetQuery to perform a multi-index search
11+
public enum MultiSearchQuery {
12+
13+
case hitsSearch(IndexedQuery)
14+
case facetsSearch(IndexedFacetQuery)
15+
16+
/**
17+
- Parameter indexedQuery: The IndexedQuery to wrap
18+
*/
19+
public init(_ indexedQuery: IndexedQuery) {
20+
self = .hitsSearch(indexedQuery)
21+
}
22+
23+
/**
24+
- Parameter indexedFacetQuery: The IndexedFacetQuery to wrap
25+
*/
26+
public init(_ indexedFacetQuery: IndexedFacetQuery) {
27+
self = .facetsSearch(indexedFacetQuery)
28+
}
29+
30+
}
31+
32+
extension MultiSearchQuery: Encodable {
33+
34+
public func encode(to encoder: Encoder) throws {
35+
switch self {
36+
case .facetsSearch(let facetsSearch):
37+
try facetsSearch.encode(to: encoder)
38+
case .hitsSearch(let hitsSearch):
39+
try hitsSearch.encode(to: encoder)
40+
}
41+
}
42+
43+
}

Sources/AlgoliaSearchClient/Models/Search/MultipleIndex/MultipleQueriesRequest.swift

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,24 +9,18 @@ import Foundation
99

1010
struct MultipleQueriesRequest {
1111

12-
let requests: [IndexedQuery]
12+
let requests: [MultiSearchQuery]
1313
let strategy: MultipleQueriesStrategy
1414

1515
}
1616

17-
extension MultipleQueriesRequest: Codable {
17+
extension MultipleQueriesRequest: Encodable {
1818

1919
enum CodingKeys: String, CodingKey {
2020
case requests
2121
case strategy
2222
}
2323

24-
init(from decoder: Decoder) throws {
25-
let container = try decoder.container(keyedBy: CodingKeys.self)
26-
self.requests = try container.decode(forKey: .requests)
27-
self.strategy = try container.decodeIfPresent(forKey: .strategy) ?? .none
28-
}
29-
3024
func encode(to encoder: Encoder) throws {
3125
var container = encoder.container(keyedBy: CodingKeys.self)
3226
try container.encode(requests, forKey: .requests)
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
//
2+
// MultiSearchResponse.swift
3+
//
4+
//
5+
// Created by Vladislav Fitc on 21/07/2021.
6+
//
7+
8+
import Foundation
9+
10+
/// Wraps the list of multi search results (either FacetSearchResponse or SearchResponse)
11+
public struct MultiSearchResponse: Codable {
12+
13+
/// List of result in the order they were submitted, one element for each IndexedQuery.
14+
public var results: [Response]
15+
16+
/// - parameter results: List of result in the order they were submitted, one element for each IndexedQuery.
17+
public init(results: [Response]) {
18+
self.results = results
19+
}
20+
21+
}
22+
23+
public extension MultiSearchResponse {
24+
25+
/// Container for either FacetSearchResponse or SearchResponse
26+
enum Response: Codable {
27+
28+
case facets(FacetSearchResponse)
29+
case hits(SearchResponse)
30+
31+
public var facetsResponse: FacetSearchResponse? {
32+
if case .facets(let response) = self {
33+
return response
34+
} else {
35+
return .none
36+
}
37+
}
38+
39+
public var hitsResponse: SearchResponse? {
40+
if case .hits(let response) = self {
41+
return response
42+
} else {
43+
return .none
44+
}
45+
}
46+
47+
public init(from decoder: Decoder) throws {
48+
do {
49+
let searchResponse = try SearchResponse(from: decoder)
50+
self = .hits(searchResponse)
51+
} catch let searchResponseError {
52+
do {
53+
let facetSearchResponse = try FacetSearchResponse(from: decoder)
54+
self = .facets(facetSearchResponse)
55+
} catch let facetSearchResponseError {
56+
throw DecodingError(searchResponseDecodingError: searchResponseError,
57+
facetSearchResponseDecodingError: facetSearchResponseError)
58+
}
59+
}
60+
}
61+
62+
public func encode(to encoder: Encoder) throws {
63+
switch self {
64+
case .facets(let response):
65+
try response.encode(to: encoder)
66+
case .hits(let response):
67+
try response.encode(to: encoder)
68+
}
69+
}
70+
71+
public struct DecodingError: Error {
72+
73+
/// Error occured while search response decoding
74+
public let searchResponseDecodingError: Error
75+
76+
/// Error occured while facets search response decoding
77+
public let facetSearchResponseDecodingError: Error
78+
79+
init(searchResponseDecodingError: Error,
80+
facetSearchResponseDecodingError: Error) {
81+
self.searchResponseDecodingError = searchResponseDecodingError
82+
self.facetSearchResponseDecodingError = facetSearchResponseDecodingError
83+
}
84+
85+
}
86+
87+
}
88+
89+
}

Sources/AlgoliaSearchClient/Models/Task/Index/IndexedTask.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import Foundation
99

1010
public struct IndexedTask: Task {
1111

12-
/// The IndexName this task is running on.
12+
/// The name of the index this task is running on.
1313
public let indexName: IndexName
1414

1515
/// The TaskID which can be used with the .waitTask method.

Tests/AlgoliaSearchClientTests/Integration/PersonalizationIntegrationTests.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ class PersonalizationIntegrationTests: IntegrationTestCase {
1919
}
2020

2121
func getStrategy() throws {
22-
let recommendationClient = PersonalizationClient(appID: client.applicationID, apiKey: client.apiKey, region: .custom("eu"))
22+
let recommendationClient = PersonalizationClient(appID: client.applicationID, apiKey: client.apiKey, region: .custom("us"))
2323
let _ = try recommendationClient.getPersonalizationStrategy()
2424
}
2525

Tests/AlgoliaSearchClientTests/Unit/Command/MultiIndexCommandTest.swift

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,13 +35,18 @@ class MultiIndexCommandTest: XCTestCase, AlgoliaCommandTest {
3535
}
3636

3737
func testQueries() {
38-
let command = Command.MultipleIndex.Queries(queries: [.init(indexName: "index0", query: "query0"), .init(indexName: "index1", query: "query1")], strategy: .stopIfEnoughMatches, requestOptions: test.requestOptions)
38+
let command = Command.MultipleIndex.Queries(queries: [
39+
IndexedQuery(indexName: "index0", query: "query0"),
40+
IndexedQuery(indexName: "index1", query: "query1")
41+
],
42+
strategy: .stopIfEnoughMatches,
43+
requestOptions: test.requestOptions)
3944
check(command: command,
4045
callType: .read,
4146
method: .post,
4247
urlPath: "/1/indexes/*/queries",
4348
queryItems: [.init(name: "testParameter", value: "testParameterValue")],
44-
body: MultipleQueriesRequest(requests: [.init(indexName: "index0", query: "query0"), .init(indexName: "index1", query: "query1")], strategy: .stopIfEnoughMatches).httpBody,
49+
body: MultipleQueriesRequest(requests: [.init(IndexedQuery(indexName: "index0", query: "query0")), .init(IndexedQuery(indexName: "index1", query: "query1"))], strategy: .stopIfEnoughMatches).httpBody,
4550
requestOptions: test.requestOptions)
4651
}
4752

0 commit comments

Comments
 (0)