From 26792c23991a88557a417c5623eeff7e043859a6 Mon Sep 17 00:00:00 2001 From: Ondrej Fiedler Date: Fri, 10 Jan 2025 16:42:26 +0100 Subject: [PATCH] Release: v5.0.0 - Support new Recommend Items to Item Segment endpoint - Series API improvements: - Add Series endpoint creates associated Item for the Series automatically (based on the cascadeCreate parameter) - Breaking change: Remove From Series no longer requires the time parameter - Removed deprecated Groups endpoints --- lib/api-client.js | 2 +- lib/index.d.ts | 136 +++++++++-- lib/requests/add-series.js | 12 +- lib/requests/delete-series.js | 12 +- lib/requests/index.js | 1 + lib/requests/insert-to-series.js | 2 +- ...recommend-item-segments-to-item-segment.js | 4 +- .../recommend-item-segments-to-item.js | 4 +- .../recommend-item-segments-to-user.js | 4 +- .../recommend-items-to-item-segment.js | 214 ++++++++++++++++++ lib/requests/remove-from-series.js | 9 +- package.json | 2 +- ...ommend-items-to-item-segment-batch_test.js | 33 +++ ...end-items-to-item-segment-callback_test.js | 37 +++ test/recommend-items-to-item-segment-test.js | 39 ++++ test/remove-from-series-batch_test.js | 10 +- test/remove-from-series-callback_test.js | 19 +- test/remove-from-series-test.js | 21 +- test/set-environment.js | 171 +++++++------- 19 files changed, 577 insertions(+), 155 deletions(-) create mode 100644 lib/requests/recommend-items-to-item-segment.js create mode 100644 test/recommend-items-to-item-segment-batch_test.js create mode 100644 test/recommend-items-to-item-segment-callback_test.js create mode 100644 test/recommend-items-to-item-segment-test.js diff --git a/lib/api-client.js b/lib/api-client.js index ccc52d7..0f86ed9 100644 --- a/lib/api-client.js +++ b/lib/api-client.js @@ -43,7 +43,7 @@ class ApiClient { method: request.method, headers: {'Accept': 'application/json', 'Content-Type': 'application/json', - 'User-Agent': 'recombee-node-api-client/4.1.5'}, + 'User-Agent': 'recombee-node-api-client/5.0.0'}, timeout: request.timeout, agent: this.options.agent }; diff --git a/lib/index.d.ts b/lib/index.d.ts index 6d05205..ddf1824 100644 --- a/lib/index.d.ts +++ b/lib/index.d.ts @@ -664,15 +664,22 @@ export module "recombee-api-client" { export class AddSeries extends requests.Request { /** * @param seriesId - ID of the series to be created. + * @param optional - Optional parameters given as an object. */ constructor( seriesId: string, + optional?: { + /** If set to `true`, the item will be created with the same ID as the series. Default is `true`. */ + cascadeCreate?: boolean; + } ); seriesId: string; + cascadeCreate?: boolean; protected __response_type: string; bodyParameters(): { + cascadeCreate?: boolean; }; queryParameters(): { @@ -686,15 +693,22 @@ export module "recombee-api-client" { export class DeleteSeries extends requests.Request { /** * @param seriesId - ID of the series to be deleted. + * @param optional - Optional parameters given as an object. */ constructor( seriesId: string, + optional?: { + /** If set to `true`, item with the same ID as seriesId will be also deleted. Default is `false`. */ + cascadeDelete?: boolean; + } ); seriesId: string; + cascadeDelete?: boolean; protected __response_type: string; bodyParameters(): { + cascadeDelete?: boolean; }; queryParameters(): { @@ -758,7 +772,7 @@ export module "recombee-api-client" { itemId: string, time: number, optional?: { - /** Indicates that any non-existing entity specified within the request should be created (as if corresponding PUT requests were invoked). This concerns both the `seriesId` and the `itemId`. If `cascadeCreate` is set to true, the behavior also depends on the `itemType`. Either item or series may be created if not present in the database. */ + /** Indicates that any non-existing entity specified within the request should be created (as if corresponding PUT requests were invoked). This concerns both the `seriesId` and the `itemId`. If `cascadeCreate` is set to true, the behavior also depends on the `itemType`. In case of `item`, an item is created, in case of `series` a series + corresponding item with the same ID is created. */ cascadeCreate?: boolean; } ); @@ -789,28 +803,24 @@ export module "recombee-api-client" { * @param seriesId - ID of the series from which a series item is to be removed. * @param itemType - Type of the item to be removed. * @param itemId - ID of the item iff `itemType` is `item`. ID of the series iff `itemType` is `series`. - * @param time - Time index of the item to be removed. */ constructor( seriesId: string, itemType: string, itemId: string, - time: number, ); seriesId: string; itemType: string; itemId: string; - time: number; protected __response_type: string; bodyParameters(): { + itemType: string; + itemId: string; }; queryParameters(): { - itemType: string; - itemId: string; - time: number; }; } @@ -2179,13 +2189,13 @@ export module "recombee-api-client" { } /** - * Recommends the top Segments from a Segmentation for a particular user, based on the user's past interactions. + * Recommends the top Segments from a [Segmentation](https://docs.recombee.com/segmentations.html) for a particular user, based on the user's past interactions. * Based on the used Segmentation, this endpoint can be used for example for: * - Recommending the top categories for the user * - Recommending the top genres for the user * - Recommending the top brands for the user * - Recommending the top artists for the user - * You need to set the used Segmentation the Admin UI in the Scenario settings prior to using this endpoint. + * You need to set the used Segmentation the Admin UI in the [Scenario settings](https://docs.recombee.com/scenarios) prior to using this endpoint. * The returned segments are sorted by relevance (first segment being the most relevant). * It is also possible to use POST HTTP method (for example in case of very long ReQL filter) - query parameters then become body parameters. */ @@ -2243,13 +2253,13 @@ export module "recombee-api-client" { } /** - * Recommends Segments from a Segmentation that are the most relevant to a particular item. + * Recommends Segments from a [Segmentation](https://docs.recombee.com/segmentations.html) that are the most relevant to a particular item. * Based on the used Segmentation, this endpoint can be used for example for: * - Recommending the related categories * - Recommending the related genres * - Recommending the related brands * - Recommending the related artists - * You need to set the used Segmentation the Admin UI in the Scenario settings prior to using this endpoint. + * You need to set the used Segmentation the Admin UI in the [Scenario settings](https://docs.recombee.com/scenarios) prior to using this endpoint. * The returned segments are sorted by relevance (first segment being the most relevant). * It is also possible to use POST HTTP method (for example in case of very long ReQL filter) - query parameters then become body parameters. */ @@ -2323,12 +2333,12 @@ export module "recombee-api-client" { } /** - * Recommends Segments from a result Segmentation that are the most relevant to a particular Segment from a context Segmentation. + * Recommends Segments from a result [Segmentation](https://docs.recombee.com/segmentations.html) that are the most relevant to a particular Segment from a context Segmentation. * Based on the used Segmentations, this endpoint can be used for example for: * - Recommending the related brands to particular brand * - Recommending the related brands to particular category * - Recommending the related artists to a particular genre (assuming songs are the Items) - * You need to set the used context and result Segmentation the Admin UI in the Scenario settings prior to using this endpoint. + * You need to set the used context and result Segmentation the Admin UI in the [Scenario settings](https://docs.recombee.com/scenarios) prior to using this endpoint. * The returned segments are sorted by relevance (first segment being the most relevant). * It is also possible to use POST HTTP method (for example in case of very long ReQL filter) - query parameters then become body parameters. */ @@ -2402,6 +2412,106 @@ export module "recombee-api-client" { }; } + /** + * Recommends Items that are the most relevant to a particular Segment from a context [Segmentation](https://docs.recombee.com/segmentations.html). + * Based on the used Segmentation, this endpoint can be used for example for: + * - Recommending articles related to a particular topic + * - Recommending songs belonging to a particular genre + * - Recommending products produced by a particular brand + * You need to set the used context Segmentation in the Admin UI in the [Scenario settings](https://docs.recombee.com/scenarios) prior to using this endpoint. + * The returned items are sorted by relevance (the first item being the most relevant). + * It is also possible to use the POST HTTP method (for example, in the case of a very long ReQL filter) — query parameters then become body parameters. + */ + export class RecommendItemsToItemSegment extends requests.Request { + /** + * @param contextSegmentId - ID of the segment from `contextSegmentationId` for which the recommendations are to be generated. + * @param targetUserId - ID of the user who will see the recommendations. + * Specifying the *targetUserId* is beneficial because: + * * It makes the recommendations personalized + * * Allows the calculation of Actions and Conversions + * in the graphical user interface, + * as Recombee can pair the user who got recommendations + * and who afterward viewed/purchased an item. + * If you insist on not specifying the user, pass `null` + * (`None`, `nil`, `NULL` etc., depending on the language) to *targetUserId*. + * Do not create some special dummy user for getting recommendations, + * as it could mislead the recommendation models, + * and result in wrong recommendations. + * For anonymous/unregistered users, it is possible to use, for example, their session ID. + * @param count - Number of items to be recommended (N for the top-N recommendation). + * @param optional - Optional parameters given as an object. + */ + constructor( + contextSegmentId: string, + targetUserId: string, + count: number, + optional?: { + /** Scenario defines a particular application of recommendations. It can be, for example, "homepage", "cart", or "emailing". */ + scenario?: string; + /** If an item of the given *itemId* or user of the given *targetUserId* doesn't exist in the database, it creates the missing entity/entities and returns some (non-personalized) recommendations. This allows, for example, rotations in the following recommendations for the user of the given *targetUserId*, as the user will be already known to the system. */ + cascadeCreate?: boolean; + /** With `returnProperties=true`, property values of the recommended items are returned along with their IDs in a JSON dictionary. The acquired property values can be used to easily display the recommended items to the user. */ + returnProperties?: boolean; + /** Allows specifying which properties should be returned when `returnProperties=true` is set. The properties are given as a comma-separated list. */ + includedProperties?: string[]; + /** Boolean-returning [ReQL](https://docs.recombee.com/reql.html) expression, which allows you to filter recommended items based on the values of their attributes. */ + filter?: string; + /** Number-returning [ReQL](https://docs.recombee.com/reql.html) expression, which allows you to boost the recommendation rate of some items based on the values of their attributes. */ + booster?: string; + /** Logic specifies the particular behavior of the recommendation models. You can pick tailored logic for your domain and use case. */ + logic?: string | object; + /** **Expert option** If the *targetUserId* is provided: Specifies the threshold of how relevant must the recommended items be to the user. Possible values one of: "low", "medium", "high". The default value is "low", meaning that the system attempts to recommend a number of items equal to *count* at any cost. If there is not enough data (such as interactions or item properties), this may even lead to bestseller-based recommendations being appended to reach the full *count*. This behavior may be suppressed by using "medium" or "high" values. In such case, the system only recommends items of at least the requested relevance and may return less than *count* items when there is not enough data to fulfill it. */ + minRelevance?: string; + /** **Expert option** If the *targetUserId* is provided: If your users browse the system in real-time, it may easily happen that you wish to offer them recommendations multiple times. Here comes the question: how much should the recommendations change? Should they remain the same, or should they rotate? Recombee API allows you to control this per request in a backward fashion. You may penalize an item for being recommended in the near past. For the specific user, `rotationRate=1` means maximal rotation, `rotationRate=0` means absolutely no rotation. You may also use, for example, `rotationRate=0.2` for only slight rotation of recommended items. */ + rotationRate?: number; + /** **Expert option** If the *targetUserId* is provided: Taking *rotationRate* into account, specifies how long it takes for an item to recover from the penalization. For example, `rotationTime=7200.0` means that items recommended less than 2 hours ago are penalized. */ + rotationTime?: number; + /** Dictionary of custom options. */ + expertSettings?: { [key: string]: unknown }; + /** If there is a custom AB-testing running, return the name of the group to which the request belongs. */ + returnAbGroup?: boolean; + } + ); + + contextSegmentId: string; + targetUserId: string; + count: number; + scenario?: string; + cascadeCreate?: boolean; + returnProperties?: boolean; + includedProperties?: string[]; + filter?: string; + booster?: string; + logic?: string | object; + minRelevance?: string; + rotationRate?: number; + rotationTime?: number; + expertSettings?: { [key: string]: unknown }; + returnAbGroup?: boolean; + protected __response_type: RecommendationResponse; + + bodyParameters(): { + contextSegmentId: string; + targetUserId: string; + count: number; + scenario?: string; + cascadeCreate?: boolean; + returnProperties?: boolean; + includedProperties?: string[]; + filter?: string; + booster?: string; + logic?: string | object; + minRelevance?: string; + rotationRate?: number; + rotationTime?: number; + expertSettings?: { [key: string]: unknown }; + returnAbGroup?: boolean; + }; + + queryParameters(): { + }; + } + /** * Full-text personalized search. The results are based on the provided `searchQuery` and also on the user's past interactions (purchases, ratings, etc.) with the items (items more suitable for the user are preferred in the results). * All the string and set item properties are indexed by the search engine. diff --git a/lib/requests/add-series.js b/lib/requests/add-series.js index 837274e..b1a81e4 100644 --- a/lib/requests/add-series.js +++ b/lib/requests/add-series.js @@ -13,10 +13,17 @@ class AddSeries extends rqs.Request { /** * Construct the request * @param {string} seriesId - ID of the series to be created. + * @param {Object} optional - Optional parameters given as an object with structure name of the parameter: value + * - Allowed parameters: + * - *cascadeCreate* + * - Type: boolean + * - Description: If set to `true`, the item will be created with the same ID as the series. Default is `true`. */ - constructor(seriesId) { + constructor(seriesId, optional) { super('PUT', `/series/${seriesId}`, 3000, false); this.seriesId = seriesId; + optional = optional || {}; + this.cascadeCreate = optional.cascadeCreate; } /** @@ -26,6 +33,9 @@ class AddSeries extends rqs.Request { bodyParameters() { let params = {}; + if(this.cascadeCreate !== undefined) + params.cascadeCreate = this.cascadeCreate; + return params; } diff --git a/lib/requests/delete-series.js b/lib/requests/delete-series.js index d324960..0d37cfd 100644 --- a/lib/requests/delete-series.js +++ b/lib/requests/delete-series.js @@ -14,10 +14,17 @@ class DeleteSeries extends rqs.Request { /** * Construct the request * @param {string} seriesId - ID of the series to be deleted. + * @param {Object} optional - Optional parameters given as an object with structure name of the parameter: value + * - Allowed parameters: + * - *cascadeDelete* + * - Type: boolean + * - Description: If set to `true`, item with the same ID as seriesId will be also deleted. Default is `false`. */ - constructor(seriesId) { + constructor(seriesId, optional) { super('DELETE', `/series/${seriesId}`, 3000, false); this.seriesId = seriesId; + optional = optional || {}; + this.cascadeDelete = optional.cascadeDelete; } /** @@ -27,6 +34,9 @@ class DeleteSeries extends rqs.Request { bodyParameters() { let params = {}; + if(this.cascadeDelete !== undefined) + params.cascadeDelete = this.cascadeDelete; + return params; } diff --git a/lib/requests/index.js b/lib/requests/index.js index 87afc06..924ae4e 100644 --- a/lib/requests/index.js +++ b/lib/requests/index.js @@ -62,6 +62,7 @@ exports.RecommendUsersToItem = require("./recommend-users-to-item").RecommendUse exports.RecommendItemSegmentsToUser = require("./recommend-item-segments-to-user").RecommendItemSegmentsToUser; exports.RecommendItemSegmentsToItem = require("./recommend-item-segments-to-item").RecommendItemSegmentsToItem; exports.RecommendItemSegmentsToItemSegment = require("./recommend-item-segments-to-item-segment").RecommendItemSegmentsToItemSegment; +exports.RecommendItemsToItemSegment = require("./recommend-items-to-item-segment").RecommendItemsToItemSegment; exports.SearchItems = require("./search-items").SearchItems; exports.SearchItemSegments = require("./search-item-segments").SearchItemSegments; exports.AddSearchSynonym = require("./add-search-synonym").AddSearchSynonym; diff --git a/lib/requests/insert-to-series.js b/lib/requests/insert-to-series.js index 7bdfbf2..b4c852e 100644 --- a/lib/requests/insert-to-series.js +++ b/lib/requests/insert-to-series.js @@ -20,7 +20,7 @@ class InsertToSeries extends rqs.Request { * - Allowed parameters: * - *cascadeCreate* * - Type: boolean - * - Description: Indicates that any non-existing entity specified within the request should be created (as if corresponding PUT requests were invoked). This concerns both the `seriesId` and the `itemId`. If `cascadeCreate` is set to true, the behavior also depends on the `itemType`. Either item or series may be created if not present in the database. + * - Description: Indicates that any non-existing entity specified within the request should be created (as if corresponding PUT requests were invoked). This concerns both the `seriesId` and the `itemId`. If `cascadeCreate` is set to true, the behavior also depends on the `itemType`. In case of `item`, an item is created, in case of `series` a series + corresponding item with the same ID is created. */ constructor(seriesId, itemType, itemId, time, optional) { super('POST', `/series/${seriesId}/items/`, 3000, false); diff --git a/lib/requests/recommend-item-segments-to-item-segment.js b/lib/requests/recommend-item-segments-to-item-segment.js index fa9ba84..f1399d1 100644 --- a/lib/requests/recommend-item-segments-to-item-segment.js +++ b/lib/requests/recommend-item-segments-to-item-segment.js @@ -6,12 +6,12 @@ const rqs = require("./request"); /** - * Recommends Segments from a result Segmentation that are the most relevant to a particular Segment from a context Segmentation. + * Recommends Segments from a result [Segmentation](https://docs.recombee.com/segmentations.html) that are the most relevant to a particular Segment from a context Segmentation. * Based on the used Segmentations, this endpoint can be used for example for: * - Recommending the related brands to particular brand * - Recommending the related brands to particular category * - Recommending the related artists to a particular genre (assuming songs are the Items) - * You need to set the used context and result Segmentation the Admin UI in the Scenario settings prior to using this endpoint. + * You need to set the used context and result Segmentation the Admin UI in the [Scenario settings](https://docs.recombee.com/scenarios) prior to using this endpoint. * The returned segments are sorted by relevance (first segment being the most relevant). * It is also possible to use POST HTTP method (for example in case of very long ReQL filter) - query parameters then become body parameters. */ diff --git a/lib/requests/recommend-item-segments-to-item.js b/lib/requests/recommend-item-segments-to-item.js index ef64066..5d74516 100644 --- a/lib/requests/recommend-item-segments-to-item.js +++ b/lib/requests/recommend-item-segments-to-item.js @@ -6,13 +6,13 @@ const rqs = require("./request"); /** - * Recommends Segments from a Segmentation that are the most relevant to a particular item. + * Recommends Segments from a [Segmentation](https://docs.recombee.com/segmentations.html) that are the most relevant to a particular item. * Based on the used Segmentation, this endpoint can be used for example for: * - Recommending the related categories * - Recommending the related genres * - Recommending the related brands * - Recommending the related artists - * You need to set the used Segmentation the Admin UI in the Scenario settings prior to using this endpoint. + * You need to set the used Segmentation the Admin UI in the [Scenario settings](https://docs.recombee.com/scenarios) prior to using this endpoint. * The returned segments are sorted by relevance (first segment being the most relevant). * It is also possible to use POST HTTP method (for example in case of very long ReQL filter) - query parameters then become body parameters. */ diff --git a/lib/requests/recommend-item-segments-to-user.js b/lib/requests/recommend-item-segments-to-user.js index 9cc1188..548a233 100644 --- a/lib/requests/recommend-item-segments-to-user.js +++ b/lib/requests/recommend-item-segments-to-user.js @@ -6,13 +6,13 @@ const rqs = require("./request"); /** - * Recommends the top Segments from a Segmentation for a particular user, based on the user's past interactions. + * Recommends the top Segments from a [Segmentation](https://docs.recombee.com/segmentations.html) for a particular user, based on the user's past interactions. * Based on the used Segmentation, this endpoint can be used for example for: * - Recommending the top categories for the user * - Recommending the top genres for the user * - Recommending the top brands for the user * - Recommending the top artists for the user - * You need to set the used Segmentation the Admin UI in the Scenario settings prior to using this endpoint. + * You need to set the used Segmentation the Admin UI in the [Scenario settings](https://docs.recombee.com/scenarios) prior to using this endpoint. * The returned segments are sorted by relevance (first segment being the most relevant). * It is also possible to use POST HTTP method (for example in case of very long ReQL filter) - query parameters then become body parameters. */ diff --git a/lib/requests/recommend-items-to-item-segment.js b/lib/requests/recommend-items-to-item-segment.js new file mode 100644 index 0000000..365f96b --- /dev/null +++ b/lib/requests/recommend-items-to-item-segment.js @@ -0,0 +1,214 @@ +/* + This file is auto-generated, do not edit +*/ + +'use strict'; +const rqs = require("./request"); + +/** + * Recommends Items that are the most relevant to a particular Segment from a context [Segmentation](https://docs.recombee.com/segmentations.html). + * Based on the used Segmentation, this endpoint can be used for example for: + * - Recommending articles related to a particular topic + * - Recommending songs belonging to a particular genre + * - Recommending products produced by a particular brand + * You need to set the used context Segmentation in the Admin UI in the [Scenario settings](https://docs.recombee.com/scenarios) prior to using this endpoint. + * The returned items are sorted by relevance (the first item being the most relevant). + * It is also possible to use the POST HTTP method (for example, in the case of a very long ReQL filter) — query parameters then become body parameters. + */ +class RecommendItemsToItemSegment extends rqs.Request { + + /** + * Construct the request + * @param {string} contextSegmentId - ID of the segment from `contextSegmentationId` for which the recommendations are to be generated. + * @param {string} targetUserId - ID of the user who will see the recommendations. + * Specifying the *targetUserId* is beneficial because: + * * It makes the recommendations personalized + * * Allows the calculation of Actions and Conversions + * in the graphical user interface, + * as Recombee can pair the user who got recommendations + * and who afterward viewed/purchased an item. + * If you insist on not specifying the user, pass `null` + * (`None`, `nil`, `NULL` etc., depending on the language) to *targetUserId*. + * Do not create some special dummy user for getting recommendations, + * as it could mislead the recommendation models, + * and result in wrong recommendations. + * For anonymous/unregistered users, it is possible to use, for example, their session ID. + * @param {number} count - Number of items to be recommended (N for the top-N recommendation). + * @param {Object} optional - Optional parameters given as an object with structure name of the parameter: value + * - Allowed parameters: + * - *scenario* + * - Type: string + * - Description: Scenario defines a particular application of recommendations. It can be, for example, "homepage", "cart", or "emailing". + * You can set various settings to the [scenario](https://docs.recombee.com/scenarios.html) in the [Admin UI](https://admin.recombee.com). You can also see the performance of each scenario in the Admin UI separately, so you can check how well each application performs. + * The AI that optimizes models to get the best results may optimize different scenarios separately or even use different models in each of the scenarios. + * - *cascadeCreate* + * - Type: boolean + * - Description: If an item of the given *itemId* or user of the given *targetUserId* doesn't exist in the database, it creates the missing entity/entities and returns some (non-personalized) recommendations. This allows, for example, rotations in the following recommendations for the user of the given *targetUserId*, as the user will be already known to the system. + * - *returnProperties* + * - Type: boolean + * - Description: With `returnProperties=true`, property values of the recommended items are returned along with their IDs in a JSON dictionary. The acquired property values can be used to easily display the recommended items to the user. + * Example response: + * ``` + * { + * "recommId": "0c6189e7-dc1a-429a-b613-192696309361", + * "recomms": + * [ + * { + * "id": "tv-178", + * "values": { + * "description": "4K TV with 3D feature", + * "categories": ["Electronics", "Televisions"], + * "price": 342, + * "url": "myshop.com/tv-178" + * } + * }, + * { + * "id": "mixer-42", + * "values": { + * "description": "Stainless Steel Mixer", + * "categories": ["Home & Kitchen"], + * "price": 39, + * "url": "myshop.com/mixer-42" + * } + * } + * ], + * "numberNextRecommsCalls": 0 + * } + * ``` + * - *includedProperties* + * - Type: string[] + * - Description: Allows specifying which properties should be returned when `returnProperties=true` is set. The properties are given as a comma-separated list. + * Example response for `includedProperties=description,price`: + * ``` + * { + * "recommId": "6842c725-a79f-4537-a02c-f34d668a3f80", + * "recomms": + * [ + * { + * "id": "tv-178", + * "values": { + * "description": "4K TV with 3D feature", + * "price": 342 + * } + * }, + * { + * "id": "mixer-42", + * "values": { + * "description": "Stainless Steel Mixer", + * "price": 39 + * } + * } + * ], + * "numberNextRecommsCalls": 0 + * } + * ``` + * - *filter* + * - Type: string + * - Description: Boolean-returning [ReQL](https://docs.recombee.com/reql.html) expression, which allows you to filter recommended items based on the values of their attributes. + * Filters can also be assigned to a [scenario](https://docs.recombee.com/scenarios.html) in the [Admin UI](https://admin.recombee.com). + * - *booster* + * - Type: string + * - Description: Number-returning [ReQL](https://docs.recombee.com/reql.html) expression, which allows you to boost the recommendation rate of some items based on the values of their attributes. + * Boosters can also be assigned to a [scenario](https://docs.recombee.com/scenarios.html) in the [Admin UI](https://admin.recombee.com). + * - *logic* + * - Type: string | object + * - Description: Logic specifies the particular behavior of the recommendation models. You can pick tailored logic for your domain and use case. + * See [this section](https://docs.recombee.com/recommendation_logics.html) for a list of available logics and other details. + * The difference between `logic` and `scenario` is that `logic` specifies mainly behavior, while `scenario` specifies the place where recommendations are shown to the users. + * Logic can also be set to a [scenario](https://docs.recombee.com/scenarios.html) in the [Admin UI](https://admin.recombee.com). + * - *minRelevance* + * - Type: string + * - Description: **Expert option** If the *targetUserId* is provided: Specifies the threshold of how relevant must the recommended items be to the user. Possible values one of: "low", "medium", "high". The default value is "low", meaning that the system attempts to recommend a number of items equal to *count* at any cost. If there is not enough data (such as interactions or item properties), this may even lead to bestseller-based recommendations being appended to reach the full *count*. This behavior may be suppressed by using "medium" or "high" values. In such case, the system only recommends items of at least the requested relevance and may return less than *count* items when there is not enough data to fulfill it. + * - *rotationRate* + * - Type: number + * - Description: **Expert option** If the *targetUserId* is provided: If your users browse the system in real-time, it may easily happen that you wish to offer them recommendations multiple times. Here comes the question: how much should the recommendations change? Should they remain the same, or should they rotate? Recombee API allows you to control this per request in a backward fashion. You may penalize an item for being recommended in the near past. For the specific user, `rotationRate=1` means maximal rotation, `rotationRate=0` means absolutely no rotation. You may also use, for example, `rotationRate=0.2` for only slight rotation of recommended items. + * - *rotationTime* + * - Type: number + * - Description: **Expert option** If the *targetUserId* is provided: Taking *rotationRate* into account, specifies how long it takes for an item to recover from the penalization. For example, `rotationTime=7200.0` means that items recommended less than 2 hours ago are penalized. + * - *expertSettings* + * - Type: object + * - Description: Dictionary of custom options. + * - *returnAbGroup* + * - Type: boolean + * - Description: If there is a custom AB-testing running, return the name of the group to which the request belongs. + */ + constructor(contextSegmentId, targetUserId, count, optional) { + super('POST', '/recomms/item-segments/items/', 3000, false); + this.contextSegmentId = contextSegmentId; + this.targetUserId = targetUserId; + this.count = count; + optional = optional || {}; + this.scenario = optional.scenario; + this.cascadeCreate = optional.cascadeCreate; + this.returnProperties = optional.returnProperties; + this.includedProperties = optional.includedProperties; + this.filter = optional.filter; + this.booster = optional.booster; + this.logic = optional.logic; + this.minRelevance = optional.minRelevance; + this.rotationRate = optional.rotationRate; + this.rotationTime = optional.rotationTime; + this.expertSettings = optional.expertSettings; + this.returnAbGroup = optional.returnAbGroup; + } + + /** + * Get body parameters + * @return {Object} The values of body parameters (name of parameter: value of the parameter) + */ + bodyParameters() { + let params = {}; + params.contextSegmentId = this.contextSegmentId; + params.targetUserId = this.targetUserId; + params.count = this.count; + + if(this.scenario !== undefined) + params.scenario = this.scenario; + + if(this.cascadeCreate !== undefined) + params.cascadeCreate = this.cascadeCreate; + + if(this.returnProperties !== undefined) + params.returnProperties = this.returnProperties; + + if(this.includedProperties !== undefined) + params.includedProperties = this.includedProperties; + + if(this.filter !== undefined) + params.filter = this.filter; + + if(this.booster !== undefined) + params.booster = this.booster; + + if(this.logic !== undefined) + params.logic = this.logic; + + if(this.minRelevance !== undefined) + params.minRelevance = this.minRelevance; + + if(this.rotationRate !== undefined) + params.rotationRate = this.rotationRate; + + if(this.rotationTime !== undefined) + params.rotationTime = this.rotationTime; + + if(this.expertSettings !== undefined) + params.expertSettings = this.expertSettings; + + if(this.returnAbGroup !== undefined) + params.returnAbGroup = this.returnAbGroup; + + return params; + } + + /** + * Get query parameters + * @return {Object} The values of query parameters (name of parameter: value of the parameter) + */ + queryParameters() { + let params = {}; + return params; + } +} + +exports.RecommendItemsToItemSegment = RecommendItemsToItemSegment diff --git a/lib/requests/remove-from-series.js b/lib/requests/remove-from-series.js index f1b09c1..b169aee 100644 --- a/lib/requests/remove-from-series.js +++ b/lib/requests/remove-from-series.js @@ -15,14 +15,12 @@ class RemoveFromSeries extends rqs.Request { * @param {string} seriesId - ID of the series from which a series item is to be removed. * @param {string} itemType - Type of the item to be removed. * @param {string} itemId - ID of the item iff `itemType` is `item`. ID of the series iff `itemType` is `series`. - * @param {number} time - Time index of the item to be removed. */ - constructor(seriesId, itemType, itemId, time) { + constructor(seriesId, itemType, itemId) { super('DELETE', `/series/${seriesId}/items/`, 3000, false); this.seriesId = seriesId; this.itemType = itemType; this.itemId = itemId; - this.time = time; } /** @@ -31,6 +29,8 @@ class RemoveFromSeries extends rqs.Request { */ bodyParameters() { let params = {}; + params.itemType = this.itemType; + params.itemId = this.itemId; return params; } @@ -41,9 +41,6 @@ class RemoveFromSeries extends rqs.Request { */ queryParameters() { let params = {}; - params.itemType = this.itemType; - params.itemId = this.itemId; - params.time = this.time; return params; } } diff --git a/package.json b/package.json index 6d79a2d..3ad818c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "recombee-api-client", - "version": "4.1.5", + "version": "5.0.0", "description": "Node.js client (SDK) for easy use of the Recombee recommendation API", "main": "index.js", "types": "lib/index.d.ts", diff --git a/test/recommend-items-to-item-segment-batch_test.js b/test/recommend-items-to-item-segment-batch_test.js new file mode 100644 index 0000000..905869f --- /dev/null +++ b/test/recommend-items-to-item-segment-batch_test.js @@ -0,0 +1,33 @@ +/* + This file is auto-generated, do not edit +*/ + +'use strict' +var chai = require('chai').assert; +var recombee = require('./../index.js'); +var rqs = recombee.requests; + +var env = require('./set-environment.js'); + +describe('RecommendItemsToItemSegment', function(){ + this.timeout(150000); + + before(function(done){ + + env.setEnvironment() + .then(()=> { + done(); + }); + }); + it ('works in batch', (done) => { + let requests = [ + new rqs.RecommendItemsToItemSegment('segment_id','entity_id',5,{'scenario': 'scenario1','cascadeCreate': true}) + ]; + + env.client.send(new rqs.Batch(requests)) + .then((responses) => { + chai.equal(responses[0].code, 400); + done(); + }); + }); +}); diff --git a/test/recommend-items-to-item-segment-callback_test.js b/test/recommend-items-to-item-segment-callback_test.js new file mode 100644 index 0000000..48e4fb9 --- /dev/null +++ b/test/recommend-items-to-item-segment-callback_test.js @@ -0,0 +1,37 @@ +/* + This file is auto-generated, do not edit +*/ + +'use strict' +var chai = require('chai').assert; +var recombee = require('./../index.js'); +var rqs = recombee.requests; + +var env = require('./set-environment.js'); + +describe('RecommendItemsToItemSegment', function(){ + this.timeout(150000); + + before(function(done){ + + env.setEnvironment() + .then(()=> { + done(); + }); + }); + + it ('rejects request to scenario which is not set up', (done) => { + let req, req2, resp; + req = new rqs.RecommendItemsToItemSegment('segment_id','entity_id',5,{'scenario': 'scenario1','cascadeCreate': true}); + env.client.send(req,((err,res) => { + if(err) { + chai.equal(err.name, 'ResponseError'); + chai.equal(err.statusCode, 400); + done(); + } + else { + chai.fail(); + } + })); + }); +}); diff --git a/test/recommend-items-to-item-segment-test.js b/test/recommend-items-to-item-segment-test.js new file mode 100644 index 0000000..6bc4397 --- /dev/null +++ b/test/recommend-items-to-item-segment-test.js @@ -0,0 +1,39 @@ +/* + This file is auto-generated, do not edit +*/ + +'use strict' +var chai = require('chai').assert; +var recombee = require('./../index.js'); +var rqs = recombee.requests; + +var env = require('./set-environment.js'); + +describe('RecommendItemsToItemSegment', function(){ + this.timeout(150000); + + before(function(done){ + + env.setEnvironment() + .then(()=> { + done(); + }); + }); + + it ('rejects request to scenario which is not set up', (done) => { + let req, req2, resp; + req = new rqs.RecommendItemsToItemSegment('segment_id','entity_id',5,{'scenario': 'scenario1','cascadeCreate': true}); + env.client.send(req) + .then((res) => { + chai.fail(); + done(); + }) + .catch((err) => { + if (err instanceof recombee.errors.ResponseError) { + chai.equal(err.statusCode, 400); + done(); + } + throw err; + }); + }); +}); diff --git a/test/remove-from-series-batch_test.js b/test/remove-from-series-batch_test.js index db5eca1..5f8f449 100644 --- a/test/remove-from-series-batch_test.js +++ b/test/remove-from-series-batch_test.js @@ -21,16 +21,14 @@ describe('RemoveFromSeries', function(){ }); it ('works in batch', (done) => { let requests = [ - new rqs.RemoveFromSeries('entity_id','item','entity_id',0), - new rqs.RemoveFromSeries('entity_id','item','entity_id',1), - new rqs.RemoveFromSeries('entity_id','item','not_contained',1) + new rqs.RemoveFromSeries('entity_id','item','entity_id'), + new rqs.RemoveFromSeries('entity_id','item','not_contained') ]; env.client.send(new rqs.Batch(requests)) .then((responses) => { - chai.equal(responses[0].code, 404); - chai.equal(responses[1].code, 200); - chai.equal(responses[2].code, 404); + chai.equal(responses[0].code, 200); + chai.equal(responses[1].code, 404); done(); }); }); diff --git a/test/remove-from-series-callback_test.js b/test/remove-from-series-callback_test.js index 31df218..bf9f0b5 100644 --- a/test/remove-from-series-callback_test.js +++ b/test/remove-from-series-callback_test.js @@ -20,24 +20,9 @@ describe('RemoveFromSeries', function(){ }); }); - it ('fails when removing item which have different time', (done) => { - let req, req2, resp; - req = new rqs.RemoveFromSeries('entity_id','item','entity_id',0); - env.client.send(req,((err,res) => { - if(err) { - chai.equal(err.name, 'ResponseError'); - chai.equal(err.statusCode, 404); - done(); - } - else { - chai.fail(); - } - })); - }); - it ('does not fail when removing item that is contained in the set', (done) => { let req, req2, resp; - req = new rqs.RemoveFromSeries('entity_id','item','entity_id',1); + req = new rqs.RemoveFromSeries('entity_id','item','entity_id'); env.client.send(req,((err,res) => { if(err) { chai.fail(); @@ -50,7 +35,7 @@ describe('RemoveFromSeries', function(){ it ('fails when removing item that is not contained in the set', (done) => { let req, req2, resp; - req = new rqs.RemoveFromSeries('entity_id','item','not_contained',1); + req = new rqs.RemoveFromSeries('entity_id','item','not_contained'); env.client.send(req,((err,res) => { if(err) { chai.equal(err.name, 'ResponseError'); diff --git a/test/remove-from-series-test.js b/test/remove-from-series-test.js index c98b0a7..cf29bb3 100644 --- a/test/remove-from-series-test.js +++ b/test/remove-from-series-test.js @@ -20,26 +20,9 @@ describe('RemoveFromSeries', function(){ }); }); - it ('fails when removing item which have different time', (done) => { - let req, req2, resp; - req = new rqs.RemoveFromSeries('entity_id','item','entity_id',0); - env.client.send(req) - .then((res) => { - chai.fail(); - done(); - }) - .catch((err) => { - if (err instanceof recombee.errors.ResponseError) { - chai.equal(err.statusCode, 404); - done(); - } - throw err; - }); - }); - it ('does not fail when removing item that is contained in the set', (done) => { let req, req2, resp; - req = new rqs.RemoveFromSeries('entity_id','item','entity_id',1); + req = new rqs.RemoveFromSeries('entity_id','item','entity_id'); env.client.send(req) .then((res) => { done(); @@ -48,7 +31,7 @@ describe('RemoveFromSeries', function(){ it ('fails when removing item that is not contained in the set', (done) => { let req, req2, resp; - req = new rqs.RemoveFromSeries('entity_id','item','not_contained',1); + req = new rqs.RemoveFromSeries('entity_id','item','not_contained'); env.client.send(req) .then((res) => { chai.fail(); diff --git a/test/set-environment.js b/test/set-environment.js index 2f52910..2838cca 100644 --- a/test/set-environment.js +++ b/test/set-environment.js @@ -1,108 +1,113 @@ -'use strict' +'use strict'; var recombee = require('./../index.js'); var rqs = recombee.requests; var errs = recombee.errors; -var client = new recombee.ApiClient('client-test', 'jGGQ6ZKa8rQ1zTAyxTc0EMn55YPF7FJLUtaMLhbsGxmvwxgTwXYqmUk5xVZFw98L', {'region': 'eu-west'}); +// Read credentials from environment variables +const DB_ID = process.env.DB_ID; +const PRIVATE_TOKEN = process.env.PRIVATE_TOKEN; + +if (!DB_ID || !PRIVATE_TOKEN) { + throw new Error('Environment variables DB_ID and PRIVATE_TOKEN must be set.'); +} + +var client = new recombee.ApiClient(DB_ID, PRIVATE_TOKEN, { region: 'eu-west' }); var _setEnvironmentData = (() => { - let requests = new rqs.Batch([ - - new rqs.AddItem('entity_id'), - new rqs.AddUser('entity_id'), - new rqs.AddSeries('entity_id'), - new rqs.InsertToSeries('entity_id', 'item', 'entity_id', 1), - new rqs.AddItemProperty('int_property', 'int'), - new rqs.AddItemProperty('str_property', 'string'), - new rqs.SetItemValues('entity_id', {'int_property': 42, 'str_property': 'hello'}), - new rqs.AddUserProperty('int_property', 'int'), - new rqs.AddUserProperty('str_property', 'string'), - new rqs.SetUserValues('entity_id', {'int_property': 42, 'str_property': 'hello'}) - ]); - - return client.send(requests); + let requests = new rqs.Batch([ + new rqs.AddItem('entity_id'), + new rqs.AddUser('entity_id'), + new rqs.AddSeries('entity_id'), + new rqs.InsertToSeries('entity_id', 'item', 'entity_id', 1), + new rqs.AddItemProperty('int_property', 'int'), + new rqs.AddItemProperty('str_property', 'string'), + new rqs.SetItemValues('entity_id', { 'int_property': 42, 'str_property': 'hello' }), + new rqs.AddUserProperty('int_property', 'int'), + new rqs.AddUserProperty('str_property', 'string'), + new rqs.SetUserValues('entity_id', { 'int_property': 42, 'str_property': 'hello' }) + ]); + + return client.send(requests); }); var _delay = ((t, v) => { - return new Promise(function(resolve) { - setTimeout(resolve.bind(null, v), t) - }) + return new Promise(function (resolve) { + setTimeout(resolve.bind(null, v), t); + }); }); var _checkDbErased = (() => { - return client.send(new rqs.ListItems()) - .then((resp) => { - return _setEnvironmentData(); - }) - .catch((err) => { - if (err instanceof errs.ResponseError && err.statusCode === 422) { - // Wait until DB is erased - return _delay(2000).then(() => { - return _checkDbErased(); - }); - } - throw err; - }) + return client.send(new rqs.ListItems()) + .then((resp) => { + return _setEnvironmentData(); + }) + .catch((err) => { + if (err instanceof errs.ResponseError && err.statusCode === 422) { + // Wait until DB is erased + return _delay(2000).then(() => { + return _checkDbErased(); + }); + } + throw err; + }); }); var setEnvironment = (() => { - return client.send(new rqs.ResetDatabase()) - .then((resp) => { - return _checkDbErased(); - }) + return _checkDbErased() // Ensure DB is in a clean state before resetting + .then(() => { + return client.send(new rqs.ResetDatabase()); + }) + .then((resp) => { + return _checkDbErased(); // Ensure DB is ready after resetting + }); }); var setInteractions = (() => { - - let requests = new rqs.Batch([ - new rqs.AddUser('user'), - new rqs.AddItem('item'), - new rqs.AddDetailView('user', 'item', {'timestamp': 0}), - new rqs.AddPurchase('user', 'item', {'timestamp': 0}), - new rqs.AddRating('user', 'item', -1, {'timestamp': 0}), - new rqs.AddCartAddition('user', 'item', {'timestamp': 0}), - new rqs.AddBookmark('user', 'item', {'timestamp': 0}), - new rqs.SetViewPortion('user', 'item',1, {'timestamp': 0}) - ]); - - return client.send(requests); + let requests = new rqs.Batch([ + new rqs.AddUser('user'), + new rqs.AddItem('item'), + new rqs.AddDetailView('user', 'item', { 'timestamp': 0 }), + new rqs.AddPurchase('user', 'item', { 'timestamp': 0 }), + new rqs.AddRating('user', 'item', -1, { 'timestamp': 0 }), + new rqs.AddCartAddition('user', 'item', { 'timestamp': 0 }), + new rqs.AddBookmark('user', 'item', { 'timestamp': 0 }), + new rqs.SetViewPortion('user', 'item', 1, { 'timestamp': 0 }) + ]); + + return client.send(requests); }); var setRecommEntities = (() => { - - const NUM = 100 - const PROBABILITY_PURCHASED = 0.1 - - let users = Array.apply(0, Array(NUM)).map(function (_, i) { - return `user-${i}`; - }); - - let items = Array.apply(0, Array(NUM)).map(function (_, i) { - return `item-${i}`; - }); - - let purchases = []; - - users.forEach((user) => { - let p = items.filter(() => Math.random() < PROBABILITY_PURCHASED); - p.forEach((item) => {purchases.push(new rqs.AddPurchase(user, item))}); - }); - - return client.send(new rqs.Batch(users.map((u) => {return new rqs.AddUser(u)}))) - .then(() => { - return client.send(new rqs.Batch([ - new rqs.AddItemProperty('answer', 'int'), - new rqs.AddItemProperty('id2', 'string'), - new rqs.AddItemProperty('empty', 'string')])); - }) - .then(() => { - return client.send(new rqs.Batch(items.map((itemId) => { - return new rqs.SetItemValues(itemId, {'answer': 42, 'id2': itemId}, {'cascadeCreate': true})}))); - }) - .then(() => { - return client.send(new rqs.Batch(purchases)); - }); + const NUM = 100; + const PROBABILITY_PURCHASED = 0.1; + + let users = Array.from({ length: NUM }, (_, i) => `user-${i}`); + let items = Array.from({ length: NUM }, (_, i) => `item-${i}`); + + let purchases = []; + + users.forEach((user) => { + let p = items.filter(() => Math.random() < PROBABILITY_PURCHASED); + p.forEach((item) => { purchases.push(new rqs.AddPurchase(user, item)); }); + }); + + return client.send(new rqs.Batch(users.map((u) => new rqs.AddUser(u)))) + .then(() => { + return client.send(new rqs.Batch([ + new rqs.AddItemProperty('answer', 'int'), + new rqs.AddItemProperty('id2', 'string'), + new rqs.AddItemProperty('empty', 'string') + ])); + }) + .then(() => { + return client.send(new rqs.Batch(items.map((itemId) => { + return new rqs.SetItemValues(itemId, { 'answer': 42, 'id2': itemId }, { 'cascadeCreate': true }); + }))); + }) + .then(() => { + return client.send(new rqs.Batch(purchases)); + }); }); exports.client = client;