Skip to content

Commit 2bb8ce6

Browse files
committed
Automatically split Batch with more than 10k requests to multiple Batch requests. Added cascadeCreate parameter to Set item/user values.
1 parent f73f37f commit 2bb8ce6

File tree

8 files changed

+84
-88
lines changed

8 files changed

+84
-88
lines changed

README.md

Lines changed: 31 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -29,36 +29,37 @@ Or install it yourself as:
2929
require 'recombee_api_client'
3030
include RecombeeApiClient
3131

32-
# Prepare some items and users
33-
NUM = 100
34-
my_users = (1..NUM).map { |i| "user-#{i}" }
35-
my_items = (1..NUM).map { |i| "item-#{i}" }
32+
client = RecombeeClient.new('client-test', 'jGGQ6ZKa8rQ1zTAyxTc0EMn55YPF7FJLUtaMLhbsGxmvwxgTwXYqmUk5xVZFw98L')
3633

37-
#Generate some random purchases of items by users
34+
# Generate some random purchases of items by users
35+
NUM = 100
3836
PROBABILITY_PURCHASED = 0.1
39-
my_purchases = []
40-
my_users.each do |user|
41-
p = my_items.select { |_| rand(0.0..1.0) < PROBABILITY_PURCHASED }
42-
p.each { |item| my_purchases.push('userId' => user, 'itemId' => item) }
37+
38+
users = (1..NUM).map { |i| "user-#{i}" }
39+
items = (1..NUM).map { |i| "item-#{i}" }
40+
purchases = []
41+
42+
users.each do |user_id|
43+
purchased = items.select { |_| rand(0.0..1.0) < PROBABILITY_PURCHASED }
44+
purchased.each { |item_id| purchases.push(
45+
46+
AddPurchase.new(user_id, item_id,'cascadeCreate' => true)
47+
# Use cascadeCreate to create the
48+
# yet non-existing users and items
49+
)}
50+
4351
end
4452

45-
# Use Recombee recommender
46-
client = RecombeeClient.new('client-test', 'jGGQ6ZKa8rQ1zTAyxTc0EMn55YPF7FJLUtaMLhbsGxmvwxgTwXYqmUk5xVZFw98L')
4753
begin
48-
# Send the data to Recombee, use Batch for faster processing
49-
puts 'Send users'
50-
client.send(Batch.new(my_users.map { |userId| AddUser.new(userId) }))
51-
puts 'Send items'
52-
client.send(Batch.new(my_items.map { |itemId| AddItem.new(itemId) }))
53-
puts 'Send purchases'
54-
client.send(Batch.new(my_purchases.map { |p| AddPurchase.new(p['userId'], p['itemId']) }))
54+
# Send the data to Recombee, use Batch for faster processing of larger data
55+
client.send(Batch.new(purchases))
5556

5657
# Get recommendations for user 'user-25'
57-
puts 'Recommend for a user'
58-
recommended = client.send(UserBasedRecommendation.new('user-25', 5, 'rotationRate' => 0))
59-
puts "Recommended items: #{recommended}"
58+
recommended = client.send(UserBasedRecommendation.new('user-25', 5))
59+
puts "Recommended items for user-25: #{recommended}"
6060
rescue APIError => e
6161
puts e
62+
# Use fallback
6263
end
6364
```
6465

@@ -71,7 +72,8 @@ NUM = 100
7172
PROBABILITY_PURCHASED = 0.1
7273

7374
client = RecombeeClient.new('client-test', 'jGGQ6ZKa8rQ1zTAyxTc0EMn55YPF7FJLUtaMLhbsGxmvwxgTwXYqmUk5xVZFw98L')
74-
client.send(ResetDatabase.new)
75+
client.send(ResetDatabase.new) # Clear everything from the database
76+
7577
# We will use computers as items in this example
7678
# Computers have three properties
7779
# - price (floating point number)
@@ -82,6 +84,7 @@ client.send(ResetDatabase.new)
8284
client.send(AddItemProperty.new('price', 'double'))
8385
client.send(AddItemProperty.new('num-cores', 'int'))
8486
client.send(AddItemProperty.new('description', 'string'))
87+
client.send(AddItemProperty.new('time', 'timestamp'))
8588

8689
# Prepare requests for setting a catalog of computers
8790
requests = (1..NUM).map do |i|
@@ -92,14 +95,18 @@ requests = (1..NUM).map do |i|
9295
'price' => rand(15000.0 .. 25000.0),
9396
'num-cores' => rand(1..8),
9497
'description' => 'Great computer',
95-
'!cascadeCreate' => true # Use !cascadeCreate for creating item
98+
'time' => DateTime.now
99+
},
100+
#optional parameters:
101+
{
102+
'cascadeCreate' => true # Use cascadeCreate for creating item
96103
# with given itemId, if it doesn't exist
97104
}
98105
)
99106
end
100107

101108
# Send catalog to the recommender system
102-
client.send(Batch.new(requests))
109+
puts client.send(Batch.new(requests))
103110

104111
# Prepare some purchases of items by users
105112
requests = []

lib/recombee_api_client.rb

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ module RecombeeApiClient
1717
class RecombeeClient
1818
include HTTParty
1919

20+
BATCH_MAX_SIZE = 10000
21+
2022
##
2123
# - +account+ -> Name of your account at Recombee
2224
# - +token+ -> Secret token obtained from Recombee for signing requests
@@ -33,6 +35,9 @@ def initialize(account, token, protocol = 'http', options = {})
3335
##
3436
# - +request+ -> ApiRequest to be sent to Recombee recommender
3537
def send(request)
38+
39+
return send_multipart_batch(request) if request.kind_of? Batch and request.requests.size > BATCH_MAX_SIZE
40+
3641
timeout = request.timeout / 1000
3742
uri = process_request_uri(request)
3843
uri = sign_url(uri)
@@ -94,6 +99,12 @@ def check_errors(response, request)
9499
fail ResponseError.new(request, status_code, response.body)
95100
end
96101

102+
def send_multipart_batch(request)
103+
requests_parts = request.requests.each_slice(BATCH_MAX_SIZE)
104+
responses = requests_parts.map {|rqs| Batch.new(rqs)}.map{|batch| send(batch)}
105+
responses.inject([]){|result,resp| result + resp}
106+
end
107+
97108
def process_request_uri(request)
98109
uri = request.path
99110
uri.slice! ('/{databaseId}/')

lib/recombee_api_client/api/item_based_recommendation.rb

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,12 @@ class ItemBasedRecommendation < ApiRequest
2121
#
2222
# * *Optional arguments (given as hash optional)*
2323
# - +targetUserId+ -> ID of the user who will see the recommendations.
24+
#
25+
#Specifying the *targetUserId* is beneficial because:
26+
#
27+
#* It makes the recommendations personalized
28+
#* Allows calculations of Actions and Conversions in the graphical user interface, as Recombee can pair the user who got recommendations and who afterwards viewed/purchased an item.
29+
#
2430
# - +userImpact+ -> If *targetUserId* parameter is present, the recommendations are biased towards the user given. Using *userImpact*, you may control this bias. For an extreme case of `userImpact=0.0`, the interactions made by the user are not taken into account at all (with the exception of history-based blacklisting), for `userImpact=1.0`, you'll get user-based recommendation. The default value is `0.1`
2531
#
2632
# - +filter+ -> Boolean-returning [ReQL](https://docs.recombee.com/reql.html) expression which allows you to filter recommended items based on the values of their attributes.
@@ -70,11 +76,11 @@ class ItemBasedRecommendation < ApiRequest
7076
#
7177
# - +diversity+ -> **Expert option** Real number from [0.0, 1.0] which determines how much mutually dissimilar should the recommended items be. The default value is 0.0, i.e., no diversification. Value 1.0 means maximal diversification.
7278
#
73-
# - +minRelevance+ -> **Expert option** Specifies the threshold of how much 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 number of items equal to *count* at any cost. If there are not enough data (such as interactions or item properties), this may even lead to bestseller-based recommendations to be 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 qualit, and may return less than *count* items when there is not enough data to fulfill it.
79+
# - +minRelevance+ -> **Expert option** If the *targetUserId* is provided: Specifies the threshold of how much 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 number of items equal to *count* at any cost. If there are not enough data (such as interactions or item properties), this may even lead to bestseller-based recommendations to be 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 qualit, and may return less than *count* items when there is not enough data to fulfill it.
7480
#
75-
# - +rotationRate+ -> **Expert option** 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 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.
81+
# - +rotationRate+ -> **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 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.
7682
#
77-
# - +rotationTime+ -> **Expert option** Taking *rotationRate* into account, specifies how long time it takes to an item to fully recover from the penalization. By example, `rotationTime=7200.0` means that items recommended more than 2 hours ago are definitely not penalized anymore. Currently, the penalization is linear, so for `rotationTime=7200.0`, an item is still penalized by `0.5` to the user after 1 hour.
83+
# - +rotationTime+ -> **Expert option** If the *targetUserId* is provided: Taking *rotationRate* into account, specifies how long time it takes to an item to fully recover from the penalization. For example, `rotationTime=7200.0` means that items recommended more than 2 hours ago are definitely not penalized anymore. Currently, the penalization is linear, so for `rotationTime=7200.0`, an item is still penalized by `0.5` to the user after 1 hour.
7884
#
7985
#
8086
def initialize(item_id, count, optional = {})

lib/recombee_api_client/api/set_item_values.rb

Lines changed: 9 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,12 @@
1-
#
2-
# This file is auto-generated, do not edit
3-
#
4-
51
module RecombeeApiClient
6-
require_relative 'request'
2+
require_relative 'set_values'
73
require_relative '../errors'
84

95
##
106
#Set/update (some) property values of a given item. The properties (columns) must be previously created by [Add item property](https://docs.recombee.com/api.html#add-item-property).
117
#
12-
class SetItemValues < ApiRequest
13-
attr_reader :item_id, :values
8+
class SetItemValues < SetValues
9+
attr_reader :item_id
1410
attr_accessor :timeout
1511
attr_accessor :ensure_https
1612

@@ -26,39 +22,24 @@ class SetItemValues < ApiRequest
2622
# "product_description": "4K TV with 3D feature",
2723
# "categories": ["Electronics", "Televisions"],
2824
# "price_usd": 342,
25+
# "in_stock_from": "2016-11-16T08:00Z",
2926
# "!cascadeCreate": true
3027
# }
3128
#```
3229
#
3330
#Special parameter `!cascadeCreate` may be used. It indicates that the item of the given itemId should be created if it does not exist in the database, as if the corresponding PUT method was used. Note the exclamation mark (!) at the beginning of the parameter's name to distinguish it from item property names.
3431
#
3532
#
36-
def initialize(item_id, values)
33+
# * *Optional arguments (given as hash optional)*
34+
# - +cascadeCreate+ -> Sets whether the item should be created if not present in the database.
35+
#
36+
def initialize(item_id, values, optional = {})
37+
super(values, optional)
3738
@item_id = item_id
38-
@values = values
3939
@timeout = 1000
4040
@ensure_https = false
4141
end
4242

43-
# HTTP method
44-
def method
45-
:post
46-
end
47-
48-
# Values of body parameters as a Hash
49-
def body_parameters
50-
p = Hash.new
51-
p = p.merge(@values)
52-
p
53-
end
54-
55-
# Values of query parameters as a Hash.
56-
# name of parameter => value of the parameter
57-
def query_parameters
58-
params = {}
59-
params
60-
end
61-
6243
# Relative path to the endpoint
6344
def path
6445
"/{databaseId}/items/#{@item_id}"

lib/recombee_api_client/api/set_user_values.rb

Lines changed: 8 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,12 @@
1-
#
2-
# This file is auto-generated, do not edit
3-
#
4-
51
module RecombeeApiClient
6-
require_relative 'request'
2+
require_relative 'set_values'
73
require_relative '../errors'
84

95
##
106
#Set/update (some) property values of a given user. The properties (columns) must be previously created by [Add user property](https://docs.recombee.com/api.html#add-user-property).
117
#
12-
class SetUserValues < ApiRequest
13-
attr_reader :user_id, :values
8+
class SetUserValues < SetValues
9+
attr_reader :user_id
1410
attr_accessor :timeout
1511
attr_accessor :ensure_https
1612

@@ -25,39 +21,19 @@ class SetUserValues < ApiRequest
2521
# {
2622
# "country": "US",
2723
# "sex": "F",
28-
# "!cascadeCreate": true
2924
# }
3025
#```
3126
#
32-
#Special parameter `!cascadeCreate` may be used. It indicates that the user of the given userId should be created if it does not exist in the database, as if the corresponding PUT method was used. Note the exclamation mark (!) at the beginning of the parameter's name to distinguish it from user property names.
33-
#
27+
# * *Optional arguments (given as hash optional)*
28+
# - +cascadeCreate+ -> Sets whether the item should be created if not present in the database.
3429
#
35-
def initialize(user_id, values)
30+
def initialize(user_id, values, optional = {})
31+
super(values, optional)
3632
@user_id = user_id
37-
@values = values
3833
@timeout = 1000
3934
@ensure_https = false
4035
end
41-
42-
# HTTP method
43-
def method
44-
:post
45-
end
46-
47-
# Values of body parameters as a Hash
48-
def body_parameters
49-
p = Hash.new
50-
p = p.merge(@values)
51-
p
52-
end
53-
54-
# Values of query parameters as a Hash.
55-
# name of parameter => value of the parameter
56-
def query_parameters
57-
params = {}
58-
params
59-
end
60-
36+
6137
# Relative path to the endpoint
6238
def path
6339
"/{databaseId}/users/#{@user_id}"

lib/recombee_api_client/api/user_based_recommendation.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ class UserBasedRecommendation < ApiRequest
7171
#
7272
# - +rotationRate+ -> **Expert option** 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 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.
7373
#
74-
# - +rotationTime+ -> **Expert option** Taking *rotationRate* into account, specifies how long time it takes to an item to fully recover from the penalization. By example, `rotationTime=7200.0` means that items recommended more than 2 hours ago are definitely not penalized anymore. Currently, the penalization is linear, so for `rotationTime=7200.0`, an item is still penalized by `0.5` to the user after 1 hour.
74+
# - +rotationTime+ -> **Expert option** Taking *rotationRate* into account, specifies how long time it takes to an item to fully recover from the penalization. For example, `rotationTime=7200.0` means that items recommended more than 2 hours ago are definitely not penalized anymore. Currently, the penalization is linear, so for `rotationTime=7200.0`, an item is still penalized by `0.5` to the user after 1 hour.
7575
#
7676
#
7777
def initialize(user_id, count, optional = {})

spec/api/batch_spec.rb

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,4 +34,14 @@
3434
expect(repl[9]['json'].sort).to match_array []
3535
end
3636

37+
it 'send large multi-part batch' do
38+
39+
NUM = 23648
40+
reqs = (1..NUM).map{|i| AddItem.new("item-#{i}")}
41+
repl = @client.send(Batch.new(reqs))
42+
expect(repl.size).to eq(NUM)
43+
44+
repl.each{|r| expect(r['code']).to eq(201)}
45+
46+
end
3747
end

spec/api/set_values.rb

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,11 @@
2727
resp = @client.send(req)
2828
end
2929

30+
it 'does not fail with cascadeCreate optional parameter' do
31+
req = described_class.new('new_entity2',{'int_property' => 5,'str_property' => 'test'},{'cascadeCreate' => true})
32+
resp = @client.send(req)
33+
end
34+
3035
it 'fails with nonexisting entity' do
3136
req = described_class.new('nonexisting',{'int_property' => 5})
3237
expect { @client.send(req) }.to raise_exception { |exception|

0 commit comments

Comments
 (0)