Complementary items
This page covers complementary items queries for recommending items that go well with a set of items. For query fundamentals, see Query Basics.
A complementary items query recommends items that go well with a set of items, typically items in a shopping cart or a viewing session. Use this for "customers also bought" or "goes well with" recommendations.
The key difference from similar items is that complementary items find items that complete or enhance the set rather than items that are similar.
Implementation approach
Complementary items are typically implemented by training a collaborative model on purchase interactions, where a bag of interactions refers to a user session of purchases at a nearby time. You often want to treat the user session as the unique user id rather than just persistent user_id (so that bags don't overlap).
For retrieval, use a combination of rules, collaborative similarity, and content similarity. For scoring, use a model that can score based on an interaction sequence input (e.g., item2vec), then pass the cart_ids as interaction_ids directly into the input of the scoring model. You'll also want to add a filter so that anything in the cart is removed from the candidates.
Complementary items for a cart
Prerequisites
- An engine with item data configured
- A trained collaborative model on purchase interactions (using session-based user IDs)
- A trained sequence-based scoring model (e.g., item2vec)
- A set of item IDs in the cart
Query example
This example retrieves candidates using multiple strategies, scores them with a sequence-based model, and filters out cart items:
- Python SDK
- TypeScript SDK
- ShapedQL
- JSON
from shaped import RankQueryBuilder, SimilarItems, Filter
# Example cart item IDs
cart_ids = ["item1", "item2", "item3"]
# Build the complementary items query
query = (
RankQueryBuilder()
.from_entity('item')
.retrieve([
# Get candidates from collaborative filtering
SimilarItems(
item_ids=cart_ids,
model='collaborative',
limit=100
),
# Get candidates from content-based similarity
SimilarItems(
item_ids=cart_ids,
model='content',
limit=100
),
# Get candidates from category-based rules
Filter("category = $category")
.limit(100)
])
.filter(f"item_id NOT IN $cart_ids") # Remove items already in cart
.score(
value_model='sequence_model_score($cart_ids, item_id)',
input_user_id='$user_id',
input_interactions_item_ids='$interaction_item_ids'
)
.limit(10)
.build()
)
# Example usage with client
# response = client.rank(
# query=query,
# parameters={
# 'cart_ids': cart_ids
# }
# )
#
# complementary_items = [item['item_id'] for item in response['items']]
import { RankQueryBuilder } from '@shaped-ai/api';
// Example cart item IDs
const cartIds = ["item1", "item2", "item3"];
// Build the complementary items query
const query = new RankQueryBuilder()
.from('item')
.retrieve([
// Get candidates from collaborative filtering
step => step.similarItems({
itemIds: cartIds,
model: 'collaborative',
limit: 100
}),
// Get candidates from content-based similarity
step => step.similarItems({
itemIds: cartIds,
model: 'content',
limit: 100
}),
// Get candidates from category-based rules
step => step.filter("category = $category")
.limit(100)
])
.filter(`item_id NOT IN $cart_ids`) // Remove cart items
.score({
valueModel: 'sequence_model_score($cart_ids, item_id)',
inputUserId: '$user_id',
inputInteractionsItemIds: '$interaction_item_ids'
})
.limit(10)
.build();
// Example usage with client
// const response = await client.rank({
// query,
// parameters: {
// cart_ids: cartIds
// }
// });
//
// const complementaryItems = response.items.map(item => item.item_id);
SELECT *
FROM (
-- Get candidates from collaborative filtering
SELECT * FROM similar_items(
item_ids=$cart_ids,
model='collaborative',
limit=100
)
UNION ALL
-- Get candidates from content-based similarity
SELECT * FROM similar_items(
item_ids=$cart_ids,
model='content',
limit=100
)
UNION ALL
-- Get candidates from category-based rules
SELECT * FROM items
WHERE category = $category
LIMIT 100
)
-- Remove items already in cart
WHERE item_id NOT IN $cart_ids
-- Score with sequence model
ORDER BY score(expression='sequence_model_score($cart_ids, item_id)',
input_user_id='$user_id', input_interactions_item_ids='$cart_ids') DESC
LIMIT 10;
{
"query": {
"type": "rank",
"from": "item",
"retrieve": [
{
"type": "similar_items",
"item_ids": ["item1", "item2", "item3"],
"model": "collaborative",
"limit": 100
},
{
"type": "similar_items",
"item_ids": ["item1", "item2", "item3"],
"model": "content",
"limit": 100
},
{
"type": "filter",
"where": "category = $parameters.category",
"limit": 100
}
],
"where": "item_id NOT IN $parameters.cart_ids",
"score": {
"value_model": "sequence_model_score($parameters.cart_ids, item_id)",
"input_user_id": "$parameters.user_id",
"input_interactions_item_ids": "$parameters.cart_ids"
},
"limit": 10
},
"parameters": {
"cart_ids": ["item123", "item456", "item789"],
"user_id": "user123"
}
}
This query:
- Retrieves candidates from three sources:
- Collaborative similarity (items frequently bought together)
- Content similarity (items with similar attributes)
- Popular items (fallback for coverage)
- Filters out items already in the cart
- Scores candidates using item2vec with the cart items as the interaction sequence
- Returns the top 10 complementary items
Training considerations
When training the collaborative model for complementary items:
- Use purchase interactions (not views or clicks)
- Group interactions by user session (not persistent user_id)
- A session represents a bag of interactions (purchases at nearby times)
- This ensures bags don't overlap and the model learns true complementarity