Amirali YaghoutiSenior Software Engineer

woocommerce Case study

Style DNA Similar Products

Related-products widgets usually recommend whatever shares a category. I wanted our store to recommend watches that genuinely look and feel alike, so I built an engine that reads each product's attributes as a style signature and scores candidates against it.

The business problem

Scoring one product against a large catalog is brutal on a database: every candidate means attribute-term lookups, and the recommendation block was rendering in around 1.9 seconds on production PDPs. A block that slow either gets cached badly or gets deleted, and neither outcome helps sales.

What I delivered

  • The engine as an MU plugin, a2-style-dna-similar.php, exposing an a2_similar_dna shortcode that renders up to eight similar watches on each product page.
  • A per-product DNA signature persisted in the _a2_dna_sig_json meta key, built from brand, case size, movement, dial color, strap, water resistance, gender, style and origin attributes.
  • A background backfill that signs the catalog 200 products per batch on a one-minute cron tick, with a per-request lazy-sign cap of 18 so fresh products render before the backfill reaches them.
  • Smart refresh: when a cached result expires, the snapshot is reused if candidate fingerprints are unchanged, and only the candidates that changed get rescored.
  • A brand-share cap of 60 percent, so recommendations never collapse into a single-brand list.

Technical approach

  • Candidate pools are bounded at every stage, 600 same-brand, 1200 OR-matched, 800 AND-matched, 180 fallback, 2600 hard cap, because an unbounded similarity engine eventually meets the product that breaks it.
  • Candidate IDs and per-candidate scores live in separate transients with separate lifetimes (24 and 72 hours), so an expired list does not throw away scoring work that is still valid.
  • Payloads over 2 KB are gzip-compressed under an a2gz1: prefix, and a daily cleanup hook prunes _transient_a2_dna_* rows in batches so the cache cannot become its own storage problem.
  • A 120-second build lock absorbs stampedes when a popular PDP's cache expires, and save_post_product busts the affected entries immediately.

Result and evidence

Measured on production product pages, the similar-products render dropped from 1.9s to 0.42s once the layered caches and bounded pools were in place. The block has stayed enabled on every PDP since.

Commercial value

Cross-selling in watches and jewelry depends entirely on taste-level relevance. Putting eight pieces from the same style band in front of a buyer keeps them moving through inventory we want to move.

implementation-brief.readme

Readable implementation brief

implementation_brief {
  project: "Style DNA Similar Products"
  file: "mu-plugins/a2-style-dna-similar.php (v1.0.34)"
  signature: "per-product JSON in _a2_dna_sig_json meta"
  pools: { brand: 600, or: 1200, and: 800, fallback: 180, cap: 2600 }
  cache: { result: "12h + 72h stale", candidate_ids: "24h",
           scores: "72h", compression: "gzip over 2KB (a2gz1:)" }
  backfill: "200/batch, 1-min cron, 6s time budget"
  render: "8 items, min score 55, brand share <= 60%"
  measured: "PDP render 1.9s -> 0.42s"
}

What this project shows

The lesson underneath this engine is cache economics: deciding which computation deserves persistence, at what granularity, and for how long. Those decisions, far more than the scoring formula, are what took the render time down.

It is also evidence I can carry a feature from idea to sustained production life, backfill, housekeeping, stampede control and all, on infrastructure as ordinary as shared hosting.