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, because every candidate means attribute-term lookups. 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. A per-request lazy-sign cap of 18 lets 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
- I bound the candidate pools at every stage: 600 same-brand, 1200 OR-matched, 800 AND-matched, 180 fallback, 2600 hard cap. 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. 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.
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
What this engine really taught me is cache economics: 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 also shows I can carry a feature from idea to sustained production life — backfill, housekeeping, stampede control and all — on infrastructure as ordinary as shared hosting.