From c0452ac90cffccea29a197a2a7957b5620756d91 Mon Sep 17 00:00:00 2001 From: Nikola Mladenovic Date: Tue, 2 Apr 2019 22:56:08 +0200 Subject: [PATCH 1/3] Add CategoryRecommendations machine learning endpoint --- Commercetools.xcodeproj/project.pbxproj | 15 +++++++ Source/CategoryRecommendations.swift | 54 +++++++++++++++++++++++++ Source/Models.swift | 18 +++++++++ 3 files changed, 87 insertions(+) create mode 100644 Source/CategoryRecommendations.swift diff --git a/Commercetools.xcodeproj/project.pbxproj b/Commercetools.xcodeproj/project.pbxproj index b5ff443..c93182d 100644 --- a/Commercetools.xcodeproj/project.pbxproj +++ b/Commercetools.xcodeproj/project.pbxproj @@ -134,6 +134,11 @@ 21A44857224303FE00AFD324 /* SimilarProducts.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21A44855224303FE00AFD324 /* SimilarProducts.swift */; }; 21A44858224303FE00AFD324 /* SimilarProducts.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21A44855224303FE00AFD324 /* SimilarProducts.swift */; }; 21A44859224303FE00AFD324 /* SimilarProducts.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21A44855224303FE00AFD324 /* SimilarProducts.swift */; }; + 21B07FEC2253DBBB00C22D21 /* CategoryRecommendations.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21B07FEB2253DBBB00C22D21 /* CategoryRecommendations.swift */; }; + 21B07FED2253DBBB00C22D21 /* CategoryRecommendations.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21B07FEB2253DBBB00C22D21 /* CategoryRecommendations.swift */; }; + 21B07FEE2253DBBB00C22D21 /* CategoryRecommendations.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21B07FEB2253DBBB00C22D21 /* CategoryRecommendations.swift */; }; + 21B07FEF2253DBBB00C22D21 /* CategoryRecommendations.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21B07FEB2253DBBB00C22D21 /* CategoryRecommendations.swift */; }; + 21B07FF12253E2D800C22D21 /* CategoryRecommendationsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21B07FF02253E2D800C22D21 /* CategoryRecommendationsTests.swift */; }; 21BB6F341DDEF8900010127A /* GraphQLTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21BB6F331DDEF8900010127A /* GraphQLTests.swift */; }; 21C0B6321E814AD600D666F5 /* ProjectSettingsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21C0B6311E814AD600D666F5 /* ProjectSettingsTests.swift */; }; 21C6080A1CE006EE001A68A7 /* Commercetools.h in Headers */ = {isa = PBXBuildFile; fileRef = 21C608081CE006EE001A68A7 /* Commercetools.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -208,6 +213,8 @@ 2163750E22202AAC00EDDAB5 /* StoreTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreTests.swift; sourceTree = ""; }; 219B76FA1F536281000C857A /* Payment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Payment.swift; sourceTree = ""; }; 21A44855224303FE00AFD324 /* SimilarProducts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimilarProducts.swift; sourceTree = ""; }; + 21B07FEB2253DBBB00C22D21 /* CategoryRecommendations.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CategoryRecommendations.swift; sourceTree = ""; }; + 21B07FF02253E2D800C22D21 /* CategoryRecommendationsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CategoryRecommendationsTests.swift; sourceTree = ""; }; 21BB6F331DDEF8900010127A /* GraphQLTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GraphQLTests.swift; sourceTree = ""; }; 21C0B6311E814AD600D666F5 /* ProjectSettingsTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProjectSettingsTests.swift; sourceTree = ""; }; 21C608081CE006EE001A68A7 /* Commercetools.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Commercetools.h; sourceTree = ""; }; @@ -286,6 +293,7 @@ isa = PBXGroup; children = ( 210883A822455156008BF496 /* SimilarProductsTests.swift */, + 21B07FF02253E2D800C22D21 /* CategoryRecommendationsTests.swift */, ); path = MachineLearningEndpoints; sourceTree = ""; @@ -366,6 +374,7 @@ isa = PBXGroup; children = ( 21A44855224303FE00AFD324 /* SimilarProducts.swift */, + 21B07FEB2253DBBB00C22D21 /* CategoryRecommendations.swift */, ); name = MachineLearningEndpoints; sourceTree = ""; @@ -602,6 +611,7 @@ developmentRegion = English; hasScannedForEncodings = 0; knownRegions = ( + English, en, Base, ); @@ -679,6 +689,7 @@ 210BE6D061EE9D05445A74E5 /* ProductProjection.swift in Sources */, 210BE4548F3C235ECB82C6D4 /* ActiveCart.swift in Sources */, 210BE70EB096D488EDBE3A13 /* GraphQL.swift in Sources */, + 21B07FED2253DBBB00C22D21 /* CategoryRecommendations.swift in Sources */, 210BE7665A0C0B192AAD6255 /* ProductType.swift in Sources */, 210BECAA37F4E4A1C50AD762 /* Order.swift in Sources */, 210BE58576ED1F03720612E8 /* Models.swift in Sources */, @@ -715,6 +726,7 @@ 210BE0A21654CA7C3FCC5B7B /* ProductProjection.swift in Sources */, 210BEC9F114FFE8E9070E63D /* ActiveCart.swift in Sources */, 210BE2D44101246E68061B67 /* GraphQL.swift in Sources */, + 21B07FEE2253DBBB00C22D21 /* CategoryRecommendations.swift in Sources */, 210BEB11D16D1D795228E4E9 /* ProductType.swift in Sources */, 210BE2B248108674195B273D /* Order.swift in Sources */, 210BE4867740D665BC9481E0 /* Models.swift in Sources */, @@ -751,6 +763,7 @@ 210BE49B24AA8A71939BD275 /* ProductProjection.swift in Sources */, 210BE9498F7A3C6874DA64F1 /* ActiveCart.swift in Sources */, 210BEA28BDE4F67CB57AAFB3 /* GraphQL.swift in Sources */, + 21B07FEF2253DBBB00C22D21 /* CategoryRecommendations.swift in Sources */, 210BE521BC6739DD59127152 /* ProductType.swift in Sources */, 210BEBEE015A11BAB91AA3A1 /* Order.swift in Sources */, 210BEBA74FEEFE5C3F5A61D2 /* Models.swift in Sources */, @@ -787,6 +800,7 @@ 210BE0A61DAE421ED1A5FAD9 /* ProductProjection.swift in Sources */, 210BE5E0E615B5F1E8A83EAB /* ActiveCart.swift in Sources */, 210BE6620B334E21C42272BB /* GraphQL.swift in Sources */, + 21B07FEC2253DBBB00C22D21 /* CategoryRecommendations.swift in Sources */, 210BE1FB8C6C0D3C45615701 /* ProductType.swift in Sources */, 210BE149EDF31C1497850A80 /* Order.swift in Sources */, 210BE69D8A9B605889CB3AE6 /* Models.swift in Sources */, @@ -814,6 +828,7 @@ 21CDEC4D1F56CF8A00ECF30E /* PaymentTests.swift in Sources */, 210BEBAB76278A85D14568D0 /* ByKeyEndpointTests.swift in Sources */, 21BB6F341DDEF8900010127A /* GraphQLTests.swift in Sources */, + 21B07FF12253E2D800C22D21 /* CategoryRecommendationsTests.swift in Sources */, 210BEA16377561C1FC741F28 /* CreateEndpointTests.swift in Sources */, 210BE69C056B75CF70357AFF /* UpdateEndpointTests.swift in Sources */, 213398191CDB8BB2003248BD /* ProductProjectionTests.swift in Sources */, diff --git a/Source/CategoryRecommendations.swift b/Source/CategoryRecommendations.swift new file mode 100644 index 0000000..7dd282d --- /dev/null +++ b/Source/CategoryRecommendations.swift @@ -0,0 +1,54 @@ +// +// Copyright (c) 2018 Commercetools. All rights reserved. +// + +import Foundation + +/** + Provides a set of methods used for category recommendations machine learning endpoint. +*/ +public struct CategoryRecommendations: MLEndpoint, Codable { + + public typealias ResponseType = PagedQueryResult + + public static let path = "recommendations/project-categories" + + /** + Initiates a similar products search. + + - parameter productId: Specific product ID to be used for searching for the best-fitting categories. + - parameter staged: Flag to target either the staged or the current version of a product. + - parameter confidenceMin: An Optional min value for confidence bounds on the returned predictions. + - parameter confidenceMax: An Optional max value for confidence bounds on the returned predictions. + - parameter limit: An optional parameter to limit the number of returned results. + - parameter offset: An optional parameter to set the offset of the first returned result. + - parameter result: The code to be executed after processing the response. + */ + public static func query(productId: String, staged: Bool? = nil, confidenceMin: Double? = nil, confidenceMax: Double? = nil, limit: Int? = nil, offset: Int? = nil, result: @escaping (Result) -> Void) { + requestWithTokenAndPath(result, { token, path in + var urlParameters = [String: String]() + + if let staged = staged { + urlParameters["staged"] = staged ? "true" : "false" + } + if let confidenceMin = confidenceMin { + urlParameters["confidenceMin"] = "\(confidenceMin)" + } + if let confidenceMax = confidenceMax { + urlParameters["confidenceMax"] = "\(confidenceMax)" + } + if let limit = limit { + urlParameters["limit"] = "\(limit)" + } + if let offset = offset { + urlParameters["offset"] = "\(offset)" + } + + let request = self.request(url: path + productId, urlParameters: urlParameters, headers: self.headers(token)) + + perform(request: request) { (response: Result) in + result(response) + } + }) + } +} \ No newline at end of file diff --git a/Source/Models.swift b/Source/Models.swift index 16f3df1..a462392 100644 --- a/Source/Models.swift +++ b/Source/Models.swift @@ -2245,4 +2245,22 @@ public struct ProductSelector: Codable { self.includeVariants = includeVariants self.productSetLimit = productSetLimit } +} + +public struct ProjectCategoryRecommendation: Codable { + + // MARK: - Properties + + public let category: Reference + public let confidence: Double + public let path: String +} + +public struct ProjectCategoryRecommendationMeta: Codable { + + // MARK: - Properties + + public let productName: String? + public let productImageUrl: String? + public let generalCategoryNames: [String] } \ No newline at end of file From 4626f0ee95a7d281287915b7a3a9ef6d8bc54c4d Mon Sep 17 00:00:00 2001 From: Nikola Mladenovic Date: Wed, 10 Apr 2019 14:17:22 +0200 Subject: [PATCH 2/3] Category recommendations tests --- Source/CategoryRecommendations.swift | 4 +- .../CategoryRecommendationsTests.swift | 58 +++++++++++++++++++ 2 files changed, 60 insertions(+), 2 deletions(-) create mode 100644 Tests/MachineLearningEndpoints/CategoryRecommendationsTests.swift diff --git a/Source/CategoryRecommendations.swift b/Source/CategoryRecommendations.swift index 7dd282d..5c7613e 100644 --- a/Source/CategoryRecommendations.swift +++ b/Source/CategoryRecommendations.swift @@ -14,7 +14,7 @@ public struct CategoryRecommendations: MLEndpoint, Codable { public static let path = "recommendations/project-categories" /** - Initiates a similar products search. + Queries for best-fitting / recommended categories for the specified product. - parameter productId: Specific product ID to be used for searching for the best-fitting categories. - parameter staged: Flag to target either the staged or the current version of a product. @@ -44,7 +44,7 @@ public struct CategoryRecommendations: MLEndpoint, Codable { urlParameters["offset"] = "\(offset)" } - let request = self.request(url: path + productId, urlParameters: urlParameters, headers: self.headers(token)) + let request = self.request(url: "\(path)/\(productId)", urlParameters: urlParameters, headers: self.headers(token)) perform(request: request) { (response: Result) in result(response) diff --git a/Tests/MachineLearningEndpoints/CategoryRecommendationsTests.swift b/Tests/MachineLearningEndpoints/CategoryRecommendationsTests.swift new file mode 100644 index 0000000..ff9c25e --- /dev/null +++ b/Tests/MachineLearningEndpoints/CategoryRecommendationsTests.swift @@ -0,0 +1,58 @@ +// +// Copyright (c) 2018 Commercetools. All rights reserved. +// + +import XCTest +@testable import Commercetools + +class CategoryRecommendationsTests: XCTestCase { + + override func setUp() { + super.setUp() + + setupTestConfiguration() + } + + override func tearDown() { + cleanPersistedTokens() + super.tearDown() + } + + func testCategoryRecommendationsForProject() { + let categoryRecommendationsExpectation = expectation(description: "category recommendations expectation") + + sampleProduct { product in + CategoryRecommendations.query(productId: product.id) { result in + XCTAssert(result.isSuccess) + XCTAssertNotNil(result.model) + categoryRecommendationsExpectation.fulfill() + } + } + + waitForExpectations(timeout: 30, handler: nil) + } + + func testRecommendationsInConfidenceRange() { + let categoryRecommendationsExpectation = expectation(description: "category recommendations expectation") + + sampleProduct { product in + CategoryRecommendations.query(productId: product.id, confidenceMin: 0.3, confidenceMax: 0.8, limit: 20) { result in + XCTAssert(result.isSuccess) + XCTAssertNotNil(result.model) + let results = result.model!.results + XCTAssertEqual(results.filter({ $0.confidence < 0.3 || $0.confidence > 0.8 }).count, 0) + categoryRecommendationsExpectation.fulfill() + } + } + + waitForExpectations(timeout: 30, handler: nil) + } + + private func sampleProduct(_ completion: @escaping (ProductProjection) -> Void) { + ProductProjection.query(limit: 1, result: { result in + if let product = result.model?.results.first, result.isSuccess { + completion(product) + } + }) + } +} \ No newline at end of file From 965c632395266ba9571c6bb013e5788732de4c07 Mon Sep 17 00:00:00 2001 From: Nikola Mladenovic Date: Sat, 20 Apr 2019 23:48:11 +0200 Subject: [PATCH 3/3] Add Category recommendations to readme --- README.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/README.md b/README.md index 188c2a1..05f8a23 100644 --- a/README.md +++ b/README.md @@ -553,6 +553,17 @@ SimilarProducts.status(for: taskToken.taskId) { result in } ``` +#### Category recommendations +Searching for best-fitting categories for a specific product ID is possible using provided query method. +- Searching for category recommendations +```swift +CategoryRecommendations.query(productId: product.id) { result in + if let results = result.model?.results { + // results contains an array of `ProjectCategoryRecommendation`s + } +} +``` + ## Handling Results In order to check whether any action with Commercetools services was successfully executed, you should use `isSuccess` or `isFailure` property of the result in question. For all successful operations, there're two properties, which can be used to consume actual responses. Recommended one for all endpoints which have incorporated models is `model`. This property has been used in all of the examples above. Alternatively, in case you are writing a custom endpoint, and do not wish to add model properties and mappings, `json` property will give you access to `[String: Any]` (dictionary representation of the JSON received from the Commercetools platform).