Bought Together

Cross-sell ranked by co-purchase lift: category pairs on Patterns, and per product "often in the same basket" in the Shop. Mined live from order data — not from a curated rule.

Bought Together: screenshot from the E-commerce demo
🛒 E-commerce_relateE-commerce
Production anchorDog dry food and dental treats go in the same basket 3.17× more often than chance in the generated PetNord data, mined live from co-purchase data — not from a curated rule.

The problem

Bought-together recommendations are the load-bearing cross-sell pattern in e-commerce. Show the buyer of dog food the dental treats; show the buyer of running shoes the moisture-wicking socks; show the buyer of the SaaS subscription the integration add-ons. The cross-sell decision has been made millions of times across e-commerce history; what is missing in most catalog tools is a system that finds the actual statistical co-occurrence in the data rather than relying on hand-curated cross-sell rules.

Hand-curated rules are fragile. They reflect the merchandiser's best guess at launch; they do not update as buying patterns shift; they miss the long-tail patterns no one thought to encode. The data has the answer — millions of orders show which products actually co-occur — but the data has been silent because there is no easy way to query the answer.

How it works

The demo answers the question at two levels. Patterns › Baskets runs _relate over orders for an anchor category and ranks the categories that share its orders by lift: of the orders holding the anchor, how much more often do they also hold the candidate than orders overall? In the Shop, a product's drawer runs the same _relate over the orders' SKUs and lists the products often in the same basket. Product pairs are thin (a few orders each), so a pair is shown only with at least five orders and a 95 % interval above 1, with its count beside it.

The dog dry food and dental treats result, 3.17× in the generated PetNord data, is not a curated rule. It is a live conditional probability over the actual co-purchase data. Run the query, get the number. Change the data (more orders accumulate, seasonal patterns shift, a new product enters the catalog), and the next query returns the updated number — no batch recommendation rebuild, no separate model.

{
  "from": "orders",
  "where": {
    "skus": {
      "$match": "SKU-PT-0003"
    }
  },
  "relate": "skus",
  "limit": 40
}

For the full architecture, see the technology overview. For the broader narrative across multiple use cases, read The Predictive Application.

See it live

This use case runs in the 🛒 E-commerce demo today. Click through to the live application and inspect the queries that produce the result.

Open the live demo →

Frequently asked

How does this compare to collaborative-filtering recommendation engines (matrix factorization, neural CF)?

Collaborative filtering builds a user-item matrix and decomposes it. Effective on dense data; struggles on cold-start items and tail products. _relate over co-purchase data is a simpler statistical primitive that returns conditional lift directly, with the counts behind it, so every pair is explainable and reflects the current orders without a rebuild.

What about user-personalized cross-sell (different recommendations per user)?

Personalized cross-sell is _recommend against the impressions or clicks data conditioned on the user's segment or behavior. Bought-together via _relate is the catalog-level pattern; personalized cross-sell composes that with user-conditional probability. Both run from the same data, no separate model.

How do we avoid the obvious-bundle problem (recommending things customers always buy together anyway)?

The lift metric handles this. Lift = conditional / baseline. A product pair that is bought together a lot but also bought separately a lot has low lift — the recommendation is not surprising. A pair with high lift is statistically over-represented among co-purchases; that is the recommendation worth surfacing.

Does this surface seasonality (e.g., dog dental treats at vet-visit time)?

_relate is computed over the current data at query time. For explicit seasonality, add a month or season filter to the where clause; the demo's Patterns view does this for categories in its "who buys what, and when" section.

Will this scale to a catalog with millions of SKUs?

The columnar index makes _relate scale with the data volume, not the catalog size. In a large catalog, product-level pairs get thinner, which is why the demo shows category pairs on Patterns and keeps a product pair only when its count and interval hold up.