Skip to content

Commit

Permalink
Merge pull request #170 from commercetools/category-recommendations
Browse files Browse the repository at this point in the history
Support for CategoryRecommendations ML Endpoint
  • Loading branch information
nikola-mladenovic authored Apr 23, 2019
2 parents b130fc8 + 965c632 commit 59c317a
Show file tree
Hide file tree
Showing 5 changed files with 156 additions and 0 deletions.
15 changes: 15 additions & 0 deletions Commercetools.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -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, ); }; };
Expand Down Expand Up @@ -208,6 +213,8 @@
2163750E22202AAC00EDDAB5 /* StoreTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreTests.swift; sourceTree = "<group>"; };
219B76FA1F536281000C857A /* Payment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Payment.swift; sourceTree = "<group>"; };
21A44855224303FE00AFD324 /* SimilarProducts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimilarProducts.swift; sourceTree = "<group>"; };
21B07FEB2253DBBB00C22D21 /* CategoryRecommendations.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CategoryRecommendations.swift; sourceTree = "<group>"; };
21B07FF02253E2D800C22D21 /* CategoryRecommendationsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CategoryRecommendationsTests.swift; sourceTree = "<group>"; };
21BB6F331DDEF8900010127A /* GraphQLTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GraphQLTests.swift; sourceTree = "<group>"; };
21C0B6311E814AD600D666F5 /* ProjectSettingsTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProjectSettingsTests.swift; sourceTree = "<group>"; };
21C608081CE006EE001A68A7 /* Commercetools.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Commercetools.h; sourceTree = "<group>"; };
Expand Down Expand Up @@ -286,6 +293,7 @@
isa = PBXGroup;
children = (
210883A822455156008BF496 /* SimilarProductsTests.swift */,
21B07FF02253E2D800C22D21 /* CategoryRecommendationsTests.swift */,
);
path = MachineLearningEndpoints;
sourceTree = "<group>";
Expand Down Expand Up @@ -366,6 +374,7 @@
isa = PBXGroup;
children = (
21A44855224303FE00AFD324 /* SimilarProducts.swift */,
21B07FEB2253DBBB00C22D21 /* CategoryRecommendations.swift */,
);
name = MachineLearningEndpoints;
sourceTree = "<group>";
Expand Down Expand Up @@ -602,6 +611,7 @@
developmentRegion = English;
hasScannedForEncodings = 0;
knownRegions = (
English,
en,
Base,
);
Expand Down Expand Up @@ -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 */,
Expand Down Expand Up @@ -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 */,
Expand Down Expand Up @@ -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 */,
Expand Down Expand Up @@ -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 */,
Expand Down Expand Up @@ -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 */,
Expand Down
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down
54 changes: 54 additions & 0 deletions Source/CategoryRecommendations.swift
Original file line number Diff line number Diff line change
@@ -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<ProjectCategoryRecommendation, ProjectCategoryRecommendationMeta>

public static let path = "recommendations/project-categories"

/**
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.
- 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<ResponseType>) -> 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<ResponseType>) in
result(response)
}
})
}
}
18 changes: 18 additions & 0 deletions Source/Models.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2245,4 +2245,22 @@ public struct ProductSelector: Codable {
self.includeVariants = includeVariants
self.productSetLimit = productSetLimit
}
}

public struct ProjectCategoryRecommendation: Codable {

// MARK: - Properties

public let category: Reference<Category>
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]
}
58 changes: 58 additions & 0 deletions Tests/MachineLearningEndpoints/CategoryRecommendationsTests.swift
Original file line number Diff line number Diff line change
@@ -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)
}
})
}
}

0 comments on commit 59c317a

Please sign in to comment.