Skip to main content

Recommendation models

This article walks through the structure, deployment, and implementation of custom recommendation models in BlueConic, including several practical examples to help you get started.

Recommendation models help you deliver personalized product and content recommendations in BlueConic using custom ONNX-based machine learning models. These models score items such as products, articles, or offers in real time, allowing you to tailor recommendations based on customer behavior, profile attributes, item similarity, and business goals.

Use recommendation models to power use cases such as:

  • Product recommendations based on browsing or purchase behavior

  • Personalized content recommendations

  • Basket expansion and cross-sell recommendations

  • Related-content experiences

  • Popularity-based recommendations

  • Profile-driven recommendations using interests or affinities


Before you begin

Before creating or uploading a recommendation model, confirm the following:

  • You have access to the AI Workbench in BlueConic.

  • Your model is in ONNX format, or you have a source model ready to convert.

  • The product or content store you want to use is configured in BlueConic.

  • Any profile properties used by the model already exist in BlueConic.

  • You understand which recommendation inputs your model requires.


Recommendation model format

BlueConic recommendation models are stored in ONNX format. A recommendation model returns a score for each candidate item in the recommendation set.

After filtering is applied, BlueConic recommends the items with the highest scores.


Inputs

Input

Type

Description

id

str[]

External item IDs for all candidate items

current_item

str

The id of the current item

profile

float[]

Vectorized profile features based on profilePropertyIds and featureNames. If the model declares a profile input, then the length of this array must equal the number of featureNames.

last_order_date

int64[]

Hours since the customer last ordered the item

shoppingcart

int64[]

Whether the item is currently in the shopping cart

viewed

int64[]

Number of times the customer viewed the item

ordered

int64[]

Number of times the customer ordered the item

recent_view

int64[]

The number of times the item was viewed within the configured timeframe

recent_click

int64[]

The number of times the item was clicked within the configured timeframe

recent_entrypage

int64[]

Number of times the item was a landing page

recent_recommendation_view

int64[]

Number of recommendation views

recent_shoppingcart

int64[]

Number of recent add-to-cart events

recent_order

int64[]

Number of recent purchases

Outputs

Recommendation models must define the following output:

Output

Type

Description

score

float[]

A list of scores between 0 and 1, one per item, used to rank the recommendations. If the score for an item is negative, that item will not be shown by the recommender.

Metadata

Recommendation models support the following metadata fields.

Metadata field

Python API name

Description

profilePropertyIds

property_ids

The profile property IDs used as input features for the model.

featureNames

feature_names

Feature names used to populate the profile vector. The profile input is filled based on these feature names.

storeId

store_id

The specific content/product store ID the model is trained for. If this metadata property is left empty, the recommendation model works with any content/product store.

Note: If storeId is not configured, the model can work with any product or content store.


Upload a recommendation model

You can upload recommendation models through the BlueConic UI, AI Workbench notebooks, or the REST API.

Using the UI

  1. Go to More > AI Workbench.

  2. Open the Models tab.

  3. Click Add Model.

  4. Set the model type to Recommendation.

  5. Upload the ONNX model file.

  6. (Optional) Select profile properties used by the model.

  7. (Optional) Add feature names.

  8. (Optional) Select a store ID.

  9. Click Save.


Using the AI Workbench (Python notebook)

If you retrain recommendation models on a schedule, use the update_model method together with a model parameter.

First, define your parameters:

import blueconic

bc = blueconic.Client()

# the model to update

MODEL_ID = bc.get_blueconic_parameter_value("Model", "model")

if not MODEL_ID:

raise ValueError("Please configure a model")

Then upload the trained ONNX recommendation model:

model = bc.get_model(MODEL_ID)

model.type = blueconic.domain.ModelType.RECOMMENDATION

model.model = onnx_model.SerializeToString()

bc.update_model(model)

Note: For more information, see the BlueConic Python API documentation.

Using the REST API

Recommendation models can also be uploaded through the BlueConic REST API, for example as part of an external recommendation pipeline.

Note: For more information, see the BlueConic REST API documentation.


Create a recommendation model

The following examples demonstrate how to build recommendation models directly in ONNX.

Your first model: random recommendations

The following model generates random scores for all candidate items.

from onnx import helper, TensorProto

id_input = helper.make_tensor_value_info("id", TensorProto.STRING, [None])

score_output = helper.make_tensor_value_info("score", TensorProto.FLOAT, [None])

random_node = helper.make_node(

op_type = "RandomUniformLike",

inputs = ["id"],

outputs = ["score"],

dtype = TensorProto.FLOAT

)

graph = helper.make_graph(

name = "Random recommendations",

nodes = [random_node],

inputs = [id_input],

outputs = [score_output]

)

model = helper.make_model(

graph = graph,

ir_version=10,

opset_imports=[

helper.make_opsetid("", 21),

]

)

Although this model is not useful in production on its own, it demonstrates several important concepts:

  • Recommendation models declare the inputs they require.

  • Recommendation models must return a score output.

  • Models should define both ir_version and opsetid to ensure platform compatibility.

Tip: When creating your own ONNX model, it's important to set the ir_version and opsetid to ensure your model is compatible with the platform.


Show popular items

One of the simplest recommendation strategies is ranking items by popularity, such as recent purchases or views.

The following example ranks products based on recent order counts.

# the number of times the product was bought recently

ordered_input = helper.make_tensor_value_info(

"recent_order",

TensorProto.INT64,

[None]

)

score_output = helper.make_tensor_value_info(

"score",

TensorProto.FLOAT,

[None]

)

cast_node = helper.make_node(

op_type="Cast",

inputs=["recent_order"],

outputs=["recent_order_f"],

to=TensorProto.FLOAT

)

max_ordered_node = helper.make_node(

op_type="ReduceMax",

inputs=["recent_order_f"],

outputs=["ordered_max"]

)

div_node = helper.make_node(

op_type="Div",

inputs=["recent_order_f", "ordered_max"],

outputs=["score"]

)

Popularity-based recommendation models provide a strong baseline when evaluating more advanced algorithms.

BlueConic also includes several built-in recommendation algorithms, including:

  • RECENT_VIEW

  • RECENT_ORDER

  • RECENT_SHOPPINGCART

  • RECENT_ENTRYPAGE

  • RECENT_CTR

Note: For a full list of recommendation algorithms, see:

Product Recommendation Algorithms | Content Recommendation Algorithms | BlueConic REST API


Filter items

Many recommendation use cases require excluding specific items.

Common examples include:

  • Excluding the current product

  • Excluding products already in the shopping cart

  • Excluding articles already viewed

When the score is negative, the item is not shown by the recommender. You can use this to let your model hide items that should not be recommended. For example, if you want to ensure that the product currently being viewed (i.e. current_item) does not show up in your recommendations, you can adapt the model above as follows:

# the ID for each candidate product

id_input = helper.make_tensor_value_info("id", TensorProto.STRING, [None])

# the ID of the item currently being viewed

current_item_input = helper.make_tensor_value_info("current_item", TensorProto.STRING, [])

# the number of times the product was bought in the configured timeframe

ordered_input = helper.make_tensor_value_info("recent_order", TensorProto.INT64, [None])

# a list of scores between 0 and 1 for all items

score_output = helper.make_tensor_value_info("score", TensorProto.FLOAT, [None])

# convert the order count to float to simplify the calculations later

cast_recent_order_node = helper.make_node(

op_type = "Cast",

inputs = ["recent_order"],

outputs = ["recent_order_f"],

to = TensorProto.FLOAT

)

# find the maximum number of times any product was bought

max_ordered_node = helper.make_node(

op_type = "ReduceMax",

inputs = ["recent_order_f"],

outputs = ["ordered_max"]

)

# scale the counts so that the score falls between 0 and 1

div_node = helper.make_node(

op_type = "Div",

inputs = ["recent_order_f", "ordered_max"],

outputs = ["scaled_score"]

)

# build a boolean mask that is True for the current item

equal_node = helper.make_node(

op_type = "Equal",

inputs = ["id", "current_item"],

outputs = ["is_current_item"]

)

# convert the boolean mask to floats (1.0 for the current item, 0.0 otherwise)

cast_is_current_item_node = helper.make_node(

op_type = "Cast",

inputs = ["is_current_item"],

outputs = ["is_current_item_f"],

to = TensorProto.FLOAT

)

# subtract 1 from the score of the current item

# to ensure it does not end up in the top recommendations

sub_node = helper.make_node(

op_type = "Sub",

inputs = ["scaled_score", "is_current_item_f"],

outputs = ["score"]

)

graph = helper.make_graph(

name = "Products the customer bought the most",

inputs = [id_input, current_item_input, ordered_input],

outputs = [score_output],

nodes = [cast_recent_order_node, max_ordered_node, div_node, equal_node,

cast_is_current_item_node, sub_node]

)

onnx_model = helper.make_model(

graph=graph,

ir_version=10,

opset_imports=[

helper.make_opsetid("", 21),

]

)

Note: For a full list of available filters, see BlueConic REST API


Show products the customer previously bought

You can recommend products a customer frequently purchases by using the ordered input instead of recent_order.

This enables use cases such as:

  • Replenishment recommendations

  • Loyalty-focused recommendations

  • Personalized discounts

The model can also suppress products the customer never purchased by assigning negative scores.

# the number of times the item was ordered by this customer

ordered_input = helper.make_tensor_value_info("ordered", TensorProto.INT64, [None])

# a list of scores between 0 and 1 for all items

score_output = helper.make_tensor_value_info("score", TensorProto.FLOAT, [None])

# convert the order count to float to simplify the calculations later

cast_node = helper.make_node(

op_type = "Cast",

inputs = ["ordered"],

outputs = ["ordered_f"],

to = TensorProto.FLOAT

)

# find the maximum number of times any product was bought by this customer

max_ordered_node = helper.make_node(

op_type = "ReduceMax",

inputs = ["ordered_f"],

outputs = ["ordered_max"]

)

# a *very* small number used to avoid division by zero

const_epsilon_node = helper.make_node(

op_type = "Constant",

inputs = [],

outputs = ["epsilon"],

value = helper.make_tensor("epsilon", TensorProto.FLOAT, [], [1e-6])

)

# add a small epsilon to avoid division by zero

add_node = helper.make_node(

op_type = "Add",

inputs = ["ordered_max", "epsilon"],

outputs = ["ordered_max_plus_epsilon"]

)

# scale the counts so that the score falls between 0 and 1

div_node = helper.make_node(

op_type = "Div",

inputs = ["ordered_f", "ordered_max_plus_epsilon"],

outputs = ["scaled_ordered_f"]

)

# subtract epsilon from the score so that products the customer

# never purchased end up with a negative score, excluding them

# from recommendations. Note: this may also exclude products that

# were purchased very infrequently compared to the most popular ones.

sub_node = helper.make_node(

op_type = "Sub",

inputs = ["scaled_ordered_f", "epsilon"],

outputs = ["score"]

)

graph = helper.make_graph(

name = "Products the customer bought the most",

inputs = [ordered_input],

outputs = [score_output],

nodes = [const_epsilon_node, cast_node, max_ordered_node, add_node,

div_node, sub_node]

)

onnx_model = helper.make_model(

graph=graph,

ir_version=10,

opset_imports=[

helper.make_opsetid("", 21),

]

)


Show items similar to the current page

You can use cosine similarity to find items similar to the current item.

The trick is to perform L2 normalization when training the model. Calculating cosine similarity then becomes a simple dot product inside the ONNX graph.

First, retrieve the content from the store:

import blueconic

bc = blueconic.Client()

CONNECTION_ID = bc.get_blueconic_parameter_value("Product/Content collector", "connection")

STORE = bc.get_connection(CONNECTION_ID).get_store()

ids = []

descriptions = []

for item in STORE.get_items():

ids.append(item.id)

descriptions.append(item.get_value("description"))

In this example, we are only using the description, but you should experiment with combining multiple fields — for example, by adding the item name as well.

You can then calculate embeddings for each item in the product store using the get_text_embeddings method in the BlueConic Python package:

import numpy as np

def create_embeddings_tensor(descriptions):

"""

Turn the list of descriptions into an embeddings tensor.

"""

embeddings = np.empty((len(descriptions), 256), np.float16)

for i, description in enumerate(descriptions):

embeddings[i] = bc.get_text_embeddings(

text = [description],

config = {"dimensions": 256}

)[0].embedding

return embeddings

You can also use scikit-learn:

import numpy as np

from sklearn.feature_extraction.text import TfidfVectorizer

from sklearn.decomposition import TruncatedSVD

from sklearn.pipeline import Pipeline

from sklearn.preprocessing import Normalizer

def create_embeddings_tensor_sklearn(descriptions):

"""

Turn descriptions into embeddings.

"""

pipeline = Pipeline([

(

'tfidf',

TfidfVectorizer(

max_features=10000,

stop_words='english'

)

),

(

'svd',

TruncatedSVD(n_components=256)

),

(

'l2',

Normalizer(norm='l2', copy=False)

)

])

embeddings = pipeline.fit_transform(descriptions)

return embeddings.astype(np.float16)

If your embeddings are based on a limited vocabulary (for example, product categories only), you can omit the TruncatedSVD step.

def generate_cosine_similarity_recommender(ids, embeddings):

"""

ids: list of strings (item identifiers)

embeddings: numpy array of shape (len(ids), dims)

Note: the embeddings should already be L2-normalized.

"""

# ensure the embeddings array is converted to float16

embeddings = embeddings.astype(dtype=np.float16, copy=False)

# pre-transpose the embedding matrix so it is laid out as (dims, num_items).

# this avoids a runtime Transpose of a large constant on every inference call,

# and gives MatMul its preferred layout: (1, dims) @ (dims, num_items) = (1, num_items).

# .copy() materializes a contiguous buffer (numpy's .T is just a view).

embeddings_t = embeddings.T.copy()

dims = embeddings.shape[1]

num_items = len(ids)

# the candidate item IDs to be ranked

input_ids = helper.make_tensor_value_info("id", TensorProto.STRING, [None])

# the ID of the item currently being viewed (e.g. "item_123")

input_current = helper.make_tensor_value_info("current_item", TensorProto.STRING, [1])

output_score = helper.make_tensor_value_info("score", TensorProto.FLOAT16, [-1])

nodes = [

# the embedding matrix, stored pre-transposed as (dims, num_items)

helper.make_node(

op_type="Constant",

inputs=[],

outputs=["all_embeddings_t"],

value=numpy_helper.from_array(embeddings_t, name="all_embeddings_t")

),

# penalty value used to push the current_item score below zero

helper.make_node(

op_type="Constant",

inputs=[],

outputs=["penalty_val"],

value=numpy_helper.from_array(np.array([2.0], dtype=np.float16), name="penalty_val")

),

# helper constant used to scale cosine similarities from [-1, 1] to [0, 1]

helper.make_node(

op_type="Constant",

inputs=[],

outputs=["const_0_5"],

value=numpy_helper.from_array(np.array([0.5], dtype=np.float16), name="const_0_5")

),

# map the current_item ID to its internal embedding index

helper.make_node(

op_type="LabelEncoder",

inputs=["current_item"],

outputs=["current_idx"],

domain="ai.onnx.ml",

keys_strings=ids,

values_int64s=list(range(num_items)),

default_int64=-1

),

# pull the column for the current item from the (dims, num_items) matrix.

# axis=1 because items now live in columns. result shape: (dims, 1).

helper.make_node(

op_type="Gather",

inputs=["all_embeddings_t", "current_idx"],

outputs=["target_col"],

axis=1

),

# transpose the small (dims, 1) target into (1, dims) for the MatMul.

# this is a tiny op (only `dims` elements move) compared to transposing

# the full (num_items, dims) matrix.

helper.make_node(

op_type="Transpose",

inputs=["target_col"],

outputs=["target_embedding"],

perm=[1, 0]

),

# calculate cosine similarity (dot product).

# (1, dims) @ (dims, num_items) = (1, num_items)

helper.make_node(

op_type="MatMul",

inputs=["target_embedding", "all_embeddings_t"],

outputs=["raw_scores"],

),

# squeeze the result of the MatMul to a single 1D array of scores

helper.make_node(

op_type="Squeeze",

inputs=["raw_scores"],

outputs=["raw_scores_squeezed"]

),

# scale: (x * 0.5) + 0.5 to move the scores from [-1, 1] to [0, 1]

helper.make_node(

op_type="Mul",

inputs=["raw_scores_squeezed", "const_0_5"],

outputs=["half_scores"]

),

helper.make_node(

op_type="Add",

inputs=["half_scores", "const_0_5"],

outputs=["scaled_scores"]

),

# build a boolean mask that is True for the current item

helper.make_node(

op_type="Equal",

inputs=["id", "current_item"],

outputs=["mask_bool"]

),

# convert the boolean mask to floats (1.0 for the current item, 0.0 otherwise)

helper.make_node(

op_type="Cast",

inputs=["mask_bool"],

outputs=["mask_float"],

to=TensorProto.FLOAT16

),

# multiply the mask by the penalty value

helper.make_node(

op_type="Mul",

inputs=["mask_float", "penalty_val"],

outputs=["penalty_vector"]

),

# subtract the penalty from the scores so the current item's score becomes negative

helper.make_node(

op_type="Sub",

inputs=["scaled_scores", "penalty_vector"],

outputs=["score"]

)

]

graph = helper.make_graph(

nodes=nodes,

name="recommender_graph",

inputs=[input_ids, input_current],

outputs=[output_score]

)

return helper.make_model(

graph=graph,

ir_version=10,

opset_imports=[

helper.make_opsetid("", 21),

helper.make_opsetid("ai.onnx.ml", 5)

]

)


Show recommendations based on a profile property

The profile input contains a vectorized representation of the profile based on the profilePropertyIds and featureNames metadata.

For example, you can use an Interest Ranker Listener to recommend products that best match the visitor's favorite categories.


First, collect product categories:

import blueconic

bc = blueconic.Client()

CONNECTION_ID = bc.get_blueconic_parameter_value("Product/Content collector", "connection")

STORE = bc.get_connection(CONNECTION_ID).get_store()

ids = []

categories = []

for item in STORE.get_items():

ids.append(item.id)

categories.append(item.get_values("category"))

Then use a TfidfVectorizer to create an L2-normalized vector for each product:

import numpy as np

from sklearn.feature_extraction.text import TfidfVectorizer

from onnx import helper, TensorProto, numpy_helper

from scipy.sparse import coo_matrix

tfidf = TfidfVectorizer(

analyzer=lambda categories: [category.lower().strip() for category in categories],

token_pattern=None,

min_df = 2,

max_features = 1024,

dtype = np.float32

)

tfidf_matrix = tfidf.fit_transform(categories)

You can then create an ONNX model that calculates the cosine similarity between the categories in the profile and the product categories:

from onnx import helper, TensorProto, numpy_helper

# convert tfidf_matrix to COO format for easy coordinate extraction

coo = tfidf_matrix.tocoo()

# the candidate item IDs to be ranked

id_input = helper.make_tensor_value_info('id', TensorProto.STRING, [None])

# the profile input: 1.0 for categories the customer is interested in,

# 0.0 for categories the customer is not interested in

profile_input = helper.make_tensor_value_info('profile', TensorProto.FLOAT, [tfidf_matrix.shape[1]])

# a list of scores between 0 and 1 for all items

score_output = helper.make_tensor_value_info('score', TensorProto.FLOAT, [None])

nodes = [

# the TF-IDF item-category matrix.

# NOTE: tfidf_matrix rows MUST be L2-normalized — the cosine similarity

# below assumes ||item_vector|| == 1 and only normalizes the profile.

helper.make_node(

op_type = 'Constant',

inputs=[],

outputs=['tfidf_categories'],

sparse_value=helper.make_sparse_tensor(

values=helper.make_tensor(

name = 'sparse_vals',

data_type = TensorProto.FLOAT,

dims = [len(coo.data)],

vals = coo.data.astype(np.float32)

),

indices=helper.make_tensor(

name = 'sparse_indices',

data_type = TensorProto.INT64,

dims = [len(coo.data), 2],

vals = np.stack((coo.row, coo.col), axis=1).astype(np.int64).flatten()

),

# add a dummy row at the end to handle unknown items

dims=[tfidf_matrix.shape[0] + 1, tfidf_matrix.shape[1]]

)

),

# index of the dummy row, used to detect item IDs that were not in the training data

helper.make_node(

op_type = 'Constant',

inputs = [],

outputs = ['last_row_index'],

value = helper.make_tensor('last_row_index', TensorProto.INT64, [1], [tfidf_matrix.shape[0]])

),

# negative value used to filter unknown items out of recommendations

helper.make_node(

op_type = 'Constant',

inputs = [],

outputs = ['minus_one'],

value = helper.make_tensor('minus_one', TensorProto.FLOAT, [1], [-1.0])

),

# threshold for empty-profile detection.

# p_norm is either 0 (empty) or >= ~1 (at least one category set, modulo float rounding)

helper.make_node(

op_type = 'Constant',

inputs = [],

outputs = ['half'],

value = helper.make_tensor('half', TensorProto.FLOAT, [1], [0.5])

),

# safe norm value used when the profile is empty

helper.make_node(

op_type = 'Constant',

inputs = [],

outputs = ['one'],

value = helper.make_tensor('one', TensorProto.FLOAT, [1], [1.0])

),

# axis for the Squeeze op below

helper.make_node(

op_type = 'Constant',

inputs = [],

outputs = ['squeeze_axes'],

value = helper.make_tensor('squeeze_axes', TensorProto.INT64, [1], [1])

),

# target shape used to reshape the profile vector into a column for the cosine calculation

helper.make_node(

op_type = 'Constant',

inputs = [],

outputs = ['profile_reshape'],

value = helper.make_tensor('profile_reshape', TensorProto.INT64, [2], [tfidf_matrix.shape[1], 1])

),

# map item IDs to row indices in the TF-IDF matrix

helper.make_node(

op_type='LabelEncoder',

inputs=['id'],

outputs=['indices'],

domain='ai.onnx.ml',

keys_strings=ids, # the original list of item IDs

values_int64s=np.arange(tfidf_matrix.shape[0], dtype=np.int64),

default_int64=tfidf_matrix.shape[0] # index of the dummy row for unknown IDs

),

# detect unknown IDs (i.e. those that mapped to the dummy row)

helper.make_node('Equal', ['indices', 'last_row_index'], ['is_unknown']),

# gather the category vectors for the candidate item IDs

helper.make_node('Gather', ['tfidf_categories', 'indices'], ['item_vectors'], axis=0),

# normalize the profile for cosine similarity

helper.make_node('ReduceL2', ['profile'], ['p_norm'], keepdims=1),

# detect an empty profile and substitute 1.0 to avoid division by zero

helper.make_node('Less', ['p_norm', 'half'], ['is_empty_profile']),

helper.make_node('Where', ['is_empty_profile', 'one', 'p_norm'], ['p_norm_safe']),

helper.make_node('Div', ['profile', 'p_norm_safe'], ['p_normed']),

helper.make_node('Reshape', ['p_normed', 'profile_reshape'], ['p_col']),

# calculate the score (cosine similarity)

helper.make_node('MatMul', ['item_vectors', 'p_col'], ['raw_scores_2d']),

helper.make_node('Squeeze', ['raw_scores_2d', 'squeeze_axes'], ['raw_scores']),

# replace the scores of unknown IDs with -1.0

helper.make_node('Where', ['is_unknown', 'minus_one', 'raw_scores'], ['scores_with_unknown_handled']),

# if the profile is empty, replace all scores with -1.0

helper.make_node('Where', ['is_empty_profile', 'minus_one', 'scores_with_unknown_handled'], ['score'])

]

graph = helper.make_graph(

nodes=nodes,

name="Profile interests recommender",

inputs=[id_input, profile_input],

outputs=[score_output],

)

onnx_model = helper.make_model(

graph = graph,

ir_version = 10,

opset_imports=[

helper.make_opsetid("", 21),

helper.make_opsetid("ai.onnx.ml", 5)

])

Finally, upload the model and configure metadata:

MODEL_ID = bc.get_blueconic_parameter_value("Recommendation model", "model")

if not MODEL_ID:

raise ValueError("Please configure a model")

FAVORITE_CATEGORIES_PROPERTY_ID = bc.get_blueconic_parameter_value(

"Favorite categories",

"profile_property"

)

if not FAVORITE_CATEGORIES_PROPERTY_ID:

raise ValueError("Please configure a Favorite categories profile property")

model = bc.get_model(MODEL_ID)

model.type = blueconic.domain.ModelType.RECOMMENDATION

model.model = onnx_model.SerializeToString()

model.property_ids = [FAVORITE_CATEGORIES_PROPERTY_ID]

# build a one-hot encoded vector of categories for the "profile" input

model.feature_names = [

FAVORITE_CATEGORIES_PROPERTY_ID + "=" + category

for category in tfidf.get_feature_names_out()

]


Other models

The examples above are only a starting point. Many recommendation algorithms can be converted to ONNX and used in the BlueConic recommender.


Association rules

Association rules (FP-Growth / Apriori) are commonly used for:

  • basket expansion

  • cross-sell recommendations

  • frequently-bought-together recommendations

You can store antecedents and lift values as ONNX tensors and rank recommendations using shopping-cart inputs.


Collaborative filtering

Collaborative filtering models recommend items based on shared user behavior patterns.

Typical approaches include:

  • matrix factorization

  • LightFM

  • two-tower architectures

These models are especially effective for:

  • personalized recommendations

  • product discovery

  • "Customers also bought" experiences


Unit testing your model

It is good practice to validate ONNX model output before deployment.


First, configure your testing environment:

import onnxruntime as ort

import ipytest

import numpy as np

ipytest.autoconfig(raise_on_error=True)

Then add your unit tests. For example, to test the popular products model described earlier:

%%ipytest

def test_popular_products_model():

onnx_model = create_popular_products_model()

session = ort.InferenceSession(onnx_model.SerializeToString())

output = session.run(output_names = None, input_feed= {

"id": ["a", "b", "c", "d", "e", "f"],

"current_item": ["b"],

"recent_order": [0, 2, 1, 3, 0, 4]

})

np.testing.assert_equal(output[0], [ 0.0 , -0.5 , 0.25, 0.75, 0.0 , 1.0])

Some unit tests worth considering:

  • Do the scores match your expectations?

  • Does the model behave correctly when the profile input consists of only zeros?

  • Does the model behave correctly if current_item is empty or an unknown ID?

  • Does the model behave correctly if id contains unknown IDs?

  • Does the model behave correctly if all IDs in id are unknown?


Performance tips

Use sparse tensors where appropriate

Many recommendation models rely on sparse matrices where most values are zero, such as item-to-item or TF-IDF matrices.

Using sparse tensors can:

  • reduce model size

  • lower memory usage

  • improve runtime efficiency

Consider sparse tensors when your data contains mostly zero values (typically less than 10% non-zero values).

Use Float16 where appropriate

Converting tensors from Float32 to Float16 can significantly reduce model size and improve inference performance.

Always validate model accuracy after conversion to ensure the reduced precision does not negatively impact recommendations.

Prefer vectorized operations

Operators such as If and Loop are supported in ONNX but can reduce performance.

Whenever possible, use vectorized operators instead, such as:

  • Where for conditional logic

  • MatMul or Einsum for matrix operations

  • ReduceSum for aggregations

  • Gather and ScatterElements for indexing

Vectorized tensor operations are typically faster and more scalable than iterative control flow.

Minimize data movement

Operations such as:

  • Transpose

  • Reshape

  • Squeeze

can introduce unnecessary runtime overhead because they reorganize tensor memory.

Whenever possible:

  • precompute tensor layouts

  • reduce reshaping operations

  • minimize runtime transposes

Did this answer your question?