Skip to content

Commit 37b0046

Browse files
feat(recommend): implement Recommend client (#753)
* implement Recommend client methods
1 parent 9b0baf0 commit 37b0046

File tree

10 files changed

+555
-2
lines changed

10 files changed

+555
-2
lines changed

Sources/AlgoliaSearchClient/Client/PersonalizationClient.swift

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,5 @@ public extension PersonalizationClient {
126126

127127
}
128128

129-
130-
131129
@available(*, deprecated, renamed: "PersonalizationClient")
132130
public typealias RecommendationClient = PersonalizationClient
Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
//
2+
// RecommendClient.swift
3+
//
4+
//
5+
// Created by Vladislav Fitc on 31/08/2021.
6+
//
7+
8+
import Foundation
9+
10+
#if canImport(FoundationNetworking)
11+
import FoundationNetworking
12+
#endif
13+
14+
/// Client to perform recommend operations.
15+
public struct RecommendClient: Credentials {
16+
17+
let transport: Transport
18+
let operationLauncher: OperationLauncher
19+
let configuration: Configuration
20+
21+
public var applicationID: ApplicationID {
22+
return transport.applicationID
23+
}
24+
25+
public var apiKey: APIKey {
26+
return transport.apiKey
27+
}
28+
29+
public init(appID: ApplicationID, apiKey: APIKey) {
30+
31+
let configuration = SearchConfiguration(applicationID: appID, apiKey: apiKey)
32+
33+
let sessionConfiguration: URLSessionConfiguration = .default
34+
sessionConfiguration.httpAdditionalHeaders = configuration.defaultHeaders
35+
let session = URLSession(configuration: sessionConfiguration)
36+
37+
self.init(configuration: configuration, requester: session)
38+
39+
}
40+
41+
public init(configuration: SearchConfiguration,
42+
requester: HTTPRequester = URLSession(configuration: .default)) {
43+
44+
let queue = OperationQueue()
45+
queue.qualityOfService = .userInitiated
46+
let operationLauncher = OperationLauncher(queue: queue)
47+
48+
let retryStrategy = AlgoliaRetryStrategy(configuration: configuration)
49+
50+
let httpTransport = HTTPTransport(requester: requester,
51+
configuration: configuration,
52+
retryStrategy: retryStrategy,
53+
credentials: configuration,
54+
operationLauncher: operationLauncher)
55+
self.init(transport: httpTransport, operationLauncher: operationLauncher, configuration: configuration)
56+
57+
}
58+
59+
init(transport: Transport,
60+
operationLauncher: OperationLauncher,
61+
configuration: Configuration) {
62+
self.transport = transport
63+
self.operationLauncher = operationLauncher
64+
self.configuration = configuration
65+
}
66+
67+
/// Initialize an Index configured with SearchConfiguration.
68+
public func index(withName indexName: IndexName) -> Index {
69+
return Index(name: indexName, transport: transport, operationLauncher: operationLauncher, configuration: configuration)
70+
}
71+
72+
}
73+
74+
extension RecommendClient: TransportContainer {}
75+
76+
extension RecommendClient {
77+
78+
@discardableResult func launch<O: Operation>(_ operation: O) -> O {
79+
return operationLauncher.launch(operation)
80+
}
81+
82+
func launch<O: OperationWithResult>(_ operation: O) throws -> O.ResultValue {
83+
return try operationLauncher.launchSync(operation)
84+
}
85+
86+
}
87+
88+
public extension RecommendClient {
89+
90+
/**
91+
Returns recommendations.
92+
93+
- parameter options: Recommend request options
94+
- parameter requestOptions: Configure request locally with RequestOptions
95+
- parameter completion: Result completion
96+
- returns: Launched asynchronous operation
97+
*/
98+
@discardableResult func getRecommendations(options: [RecommendationsOptions],
99+
requestOptions: RequestOptions? = nil,
100+
completion: @escaping ResultCallback<SearchesResponse>) -> Operation {
101+
let command = Command.Recommend.GetRecommendations(options: options, requestOptions: requestOptions)
102+
return execute(command, completion: completion)
103+
}
104+
105+
/**
106+
Returns recommendations.
107+
108+
- parameter options: Recommend request options
109+
- parameter requestOptions: Configure request locally with RequestOptions
110+
- returns: SearchesResponse object
111+
*/
112+
@discardableResult func getRecommendations(options: [RecommendationsOptions],
113+
requestOptions: RequestOptions? = nil) throws -> SearchesResponse {
114+
let command = Command.Recommend.GetRecommendations(options: options, requestOptions: requestOptions)
115+
return try execute(command)
116+
}
117+
118+
/**
119+
Returns [Related Products](https://algolia.com/doc/guides/algolia-ai/recommend/#related-products).
120+
121+
- parameter options: Recommend request options
122+
- parameter requestOptions: Configure request locally with RequestOptions
123+
- parameter completion: Result completion
124+
- returns: Launched asynchronous operation
125+
*/
126+
@discardableResult func getRelatedProducts(options: [RelatedProductsOptions],
127+
requestOptions: RequestOptions? = nil,
128+
completion: @escaping ResultCallback<SearchesResponse>) -> Operation {
129+
return getRecommendations(options: options.map(\.recommendationsOptions),
130+
requestOptions: requestOptions,
131+
completion: completion)
132+
}
133+
134+
/**
135+
Returns [Related Products](https://algolia.com/doc/guides/algolia-ai/recommend/#related-products).
136+
137+
- parameter options: Recommend request options
138+
- parameter requestOptions: Configure request locally with RequestOptions
139+
- returns: SearchesResponse object
140+
*/
141+
@discardableResult func getRelatedProducts(options: [RelatedProductsOptions],
142+
requestOptions: RequestOptions? = nil) throws -> SearchesResponse {
143+
return try getRecommendations(options: options.map(\.recommendationsOptions),
144+
requestOptions: requestOptions)
145+
}
146+
147+
/**
148+
Returns [Frequently Bought Together](https://algolia.com/doc/guides/algolia-ai/recommend/#frequently-bought-together) products.
149+
150+
- parameter options: Recommend request options
151+
- parameter requestOptions: Configure request locally with RequestOptions
152+
- parameter completion: Result completion
153+
- returns: Launched asynchronous operation
154+
*/
155+
@discardableResult func getFrequentlyBoughtTogether(options: [FrequentlyBoughtTogetherOptions],
156+
requestOptions: RequestOptions? = nil,
157+
completion: @escaping ResultCallback<SearchesResponse>) -> Operation {
158+
return getRecommendations(options: options.map(\.recommendationsOptions),
159+
requestOptions: requestOptions,
160+
completion: completion)
161+
}
162+
163+
/**
164+
Returns [Frequently Bought Together](https://algolia.com/doc/guides/algolia-ai/recommend/#frequently-bought-together) products.
165+
166+
- parameter options: Recommend request options
167+
- parameter requestOptions: Configure request locally with RequestOptions
168+
- returns: SearchesResponse object
169+
*/
170+
@discardableResult func getFrequentlyBoughtTogether(options: [FrequentlyBoughtTogetherOptions],
171+
requestOptions: RequestOptions? = nil) throws -> SearchesResponse {
172+
return try getRecommendations(options: options.map(\.recommendationsOptions),
173+
requestOptions: requestOptions)
174+
}
175+
176+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
//
2+
// Command+Recommend.swift
3+
//
4+
//
5+
// Created by Vladislav Fitc on 31/08/2021.
6+
//
7+
8+
import Foundation
9+
10+
extension Command {
11+
12+
enum Recommend {
13+
14+
struct GetRecommendations: AlgoliaCommand {
15+
16+
let method: HTTPMethod = .post
17+
let callType: CallType = .read
18+
let path: RecommendCompletion = .indexesV1 >>> .multiIndex >>> .recommendations
19+
let body: Data?
20+
let requestOptions: RequestOptions?
21+
22+
init(options: [RecommendationsOptions], requestOptions: RequestOptions?) {
23+
var requestOptions = requestOptions.unwrapOrCreate()
24+
requestOptions.setHeader("application/json", forKey: .contentType)
25+
self.requestOptions = requestOptions
26+
self.body = RequestsWrapper(options).httpBody
27+
}
28+
29+
}
30+
31+
}
32+
33+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
//
2+
// RecommendPath.swift
3+
//
4+
//
5+
// Created by Vladislav Fitc on 31/08/2021.
6+
//
7+
8+
import Foundation
9+
10+
struct RecommendCompletion: PathComponent {
11+
12+
var parent: IndexRoute?
13+
14+
let rawValue: String
15+
16+
private init(_ rawValue: String) { self.rawValue = rawValue }
17+
18+
static var recommendations: Self { .init(#function) }
19+
20+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
//
2+
// RecommendationModel.swift
3+
//
4+
//
5+
// Created by Vladislav Fitc on 01/09/2021.
6+
//
7+
8+
import Foundation
9+
10+
public enum RecommendationModel: String, Codable {
11+
12+
/// [Related Products](https://algolia.com/doc/guides/algolia-ai/recommend/#related-products)
13+
case relatedProducts = "related-products"
14+
15+
/// [Frequently Bought Together](https://algolia.com/doc/guides/algolia-ai/recommend/#frequently-bought-together) products
16+
case boughtTogether = "bought-together"
17+
18+
}
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
//
2+
// RecommendationsOptions.swift
3+
//
4+
//
5+
// Created by Vladislav Fitc on 01/09/2021.
6+
//
7+
8+
import Foundation
9+
10+
public struct RecommendationsOptions: Codable {
11+
12+
/// Name of the index to target
13+
public let indexName: IndexName
14+
15+
/// The recommendation model to use
16+
public let model: RecommendationModel
17+
18+
/// The objectID to get recommendations for
19+
public let objectID: ObjectID
20+
21+
/// The threshold to use when filtering recommendations by their score
22+
public let threshold: Int
23+
24+
/// The maximum number of recommendations to retrieve
25+
public let maxRecommendations: Int?
26+
27+
/// Search parameters to filter the recommendations
28+
public let queryParameters: Query?
29+
30+
/// Search parameters to use as fallback when there are no recommendations
31+
public let fallbackParameters: Query?
32+
33+
/**
34+
- parameter indexName: Name of the index to target
35+
- parameter model: The recommendation model to use
36+
- parameter objectID: The objectID to get recommendations for
37+
- parameter threshold: The threshold to use when filtering recommendations by their score
38+
- parameter maxRecommendations: The maximum number of recommendations to retrieve
39+
- parameter queryParameters: Search parameters to filter the recommendations
40+
- parameter fallbackParameters: Search parameters to use as fallback when there are no recommendations
41+
*/
42+
public init(indexName: IndexName,
43+
model: RecommendationModel,
44+
objectID: ObjectID,
45+
threshold: Int = 0,
46+
maxRecommendations: Int? = nil,
47+
queryParameters: Query? = nil,
48+
fallbackParameters: Query? = nil) {
49+
self.indexName = indexName
50+
self.model = model
51+
self.objectID = objectID
52+
self.threshold = threshold
53+
self.maxRecommendations = maxRecommendations
54+
self.queryParameters = queryParameters
55+
self.fallbackParameters = fallbackParameters
56+
}
57+
58+
}
59+
60+
public struct FrequentlyBoughtTogetherOptions {
61+
62+
internal let recommendationsOptions: RecommendationsOptions
63+
64+
/**
65+
- parameter indexName: Name of the index to target
66+
- parameter objectID: The objectID to get recommendations for
67+
- parameter threshold: The threshold to use when filtering recommendations by their score
68+
- parameter maxRecommendations: The maximum number of recommendations to retrieve
69+
- parameter queryParameters: Search parameters to filter the recommendations
70+
*/
71+
public init(indexName: IndexName,
72+
objectID: ObjectID,
73+
threshold: Int = 0,
74+
maxRecommendations: Int? = nil,
75+
queryParameters: Query? = nil) {
76+
recommendationsOptions = .init(indexName: indexName,
77+
model: .boughtTogether,
78+
objectID: objectID,
79+
threshold: threshold,
80+
maxRecommendations: maxRecommendations,
81+
queryParameters: queryParameters,
82+
fallbackParameters: nil)
83+
}
84+
85+
}
86+
87+
public struct RelatedProductsOptions {
88+
89+
internal let recommendationsOptions: RecommendationsOptions
90+
91+
/**
92+
- parameter indexName: Name of the index to target
93+
- parameter objectID: The objectID to get recommendations for
94+
- parameter threshold: The threshold to use when filtering recommendations by their score
95+
- parameter maxRecommendations: The maximum number of recommendations to retrieve
96+
- parameter queryParameters: Search parameters to filter the recommendations
97+
- parameter fallbackParameters: Search parameters to use as fallback when there are no recommendations
98+
*/
99+
public init(indexName: IndexName,
100+
objectID: ObjectID,
101+
threshold: Int = 0,
102+
maxRecommendations: Int? = nil,
103+
queryParameters: Query? = nil,
104+
fallbackParameters: Query? = nil) {
105+
recommendationsOptions = .init(indexName: indexName,
106+
model: .relatedProducts,
107+
objectID: objectID,
108+
threshold: threshold,
109+
maxRecommendations: maxRecommendations,
110+
queryParameters: queryParameters,
111+
fallbackParameters: fallbackParameters)
112+
}
113+
114+
}

Tests/AlgoliaSearchClientTests/Helper/XCTest+Codable.swift

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,17 @@ func AssertEncodeDecode<T: Codable>(_ value: T, _ rawValue: JSON, file: StaticSt
1414
try AssertDecode(rawValue, expected: value, file: file, line: line)
1515
}
1616

17+
func AssertMatch(_ data: Data, _ expected: JSON, file: StaticString = #file, line: UInt = #line) {
18+
let jsonDecoder = JSONDecoder()
19+
jsonDecoder.dateDecodingStrategy = .swiftAPIClient
20+
do {
21+
let decoded = try jsonDecoder.decode(JSON.self, from: data)
22+
XCTAssertEqual(decoded, expected)
23+
} catch let error {
24+
XCTFail("Failed decoding: \(error)")
25+
}
26+
}
27+
1728
func AssertDecode<T: Codable & Equatable>(_ input: JSON, expected: T, file: StaticString = #file, line: UInt = #line) throws {
1829

1930
let encoder = JSONEncoder()

0 commit comments

Comments
 (0)