fix: MPS-24 auto-suggest — keep umbrella unigrams (surface 'holiday' standalone)

Operator: sees 'December Holiday' but not 'Holiday' on its own as a
recommended tag.

Bigram supersession dropped a unigram when the UNION of all bigrams
containing it covered >=0.8 of its products. 'holiday' spans
'december holiday' + 'winter holiday' + 'christmas holiday' -> union
covered it -> 'holiday' hidden as redundant. Changed: supersede only
when a SINGLE bigram covers >=0.8 (a true fragment, e.g.
'write'->'write room'). An umbrella unigram covered only by the union
of DISTINCT bigrams is now KEPT as its own category.

Also answered operator Q in docs: the stem engine NEVER auto-applies
to new products — suggest-then-approve only (/s/{id}/tags button or
operator-run backfill_tags CLI). Adding a product does not
auto-categorize it.

Tests: test_suggest_clusters_multi_bigram_supersedes_unigram renamed
to ..._keeps_umbrella_unigram_over_multi_bigrams (behaviour
intentionally flipped per operator); +..._surfaces_holiday_with_phrase_bigrams;
single-dominant-bigram supersession test unchanged + still green.
1140 passed. Docs: mps-24.md Phase 2.8m.
This commit is contained in:
russell@unturf.com 2026-05-17 10:28:28 -04:00
parent bda90119d8
commit 60a93de763
No known key found for this signature in database
3 changed files with 75 additions and 19 deletions

View file

@ -363,6 +363,24 @@ Tests (`test_functional.py::TestProductTagsSpa`):
Deferred (occasional click, not the hot path): AJAX-ifying the
"Suggest categories" link — still a full navigation by design.
**Phase 2.8m — supersession keeps umbrella unigrams** (shipped
2026-05-17): operator saw "December Holiday" but not "Holiday" on its
own. Bigram supersession dropped a unigram when the *union* of all
bigrams containing it covered ≥0.8 of its products — so "holiday"
(spanning "december holiday" + "winter holiday" + "christmas holiday")
was hidden as redundant. Changed to supersede only when a **single**
bigram covers ≥0.8 (true fragment, e.g. "write"→"write room"); an
umbrella unigram covered only by the union of DISTINCT bigrams is now
KEPT as its own category. Tests:
`test_suggest_clusters_keeps_umbrella_unigram_over_multi_bigrams`
(was `_multi_bigram_supersedes_unigram`, behaviour intentionally
flipped), `test_suggest_clusters_surfaces_holiday_with_phrase_bigrams`;
the single-dominant-bigram test still passes unchanged. Also clarified
(operator Q): the stem engine **never auto-applies** to new products
— it's suggest-then-approve only (`/s/{id}/tags` button or the
operator-run `backfill_tags` CLI); adding a product does not
auto-categorize it.
**Phase 2.8l — SERP right rail (featured / random) + smaller thumbs**
(shipped 2026-05-16): operator wanted smaller SERP thumbnails and a
right column. Thumb column shrunk (140/200/260/320 → 88/110/130/150

View file

@ -449,13 +449,19 @@ def suggest_clusters(
reverse=True,
)
# Bigram supersession: a unigram drops if a bigram (or collectively
# the union of bigrams) containing it covers ≥ SUPERSESSION_THRESHOLD
# of the unigram's product set. The operator wants a single phrase
# row ("Write Room") instead of three near-duplicates
# ("Write Room" + "Write" + "Room"), and a single "Day" row should
# drop when "Valentine's Day" + "Patrick's Day" + … collectively
# cover most of its products.
# Bigram supersession: a unigram drops ONLY when a *single* bigram
# already covers ≥ SUPERSESSION_THRESHOLD of its product set — i.e.
# the unigram is a fragment of one phrase ("write" → "write room";
# the operator wants one "Write Room" row, not "Write Room" + "Write"
# + "Room").
#
# An umbrella unigram covered only by the UNION of several DISTINCT
# bigrams (e.g. "holiday" → "december holiday" + "winter holiday" +
# "christmas holiday"; "day" → "valentine day" + "patrick day") is a
# real standalone category and is KEPT. (Operator: they want
# "holiday" recommended on its own even though "December Holiday"
# also surfaces.) Previously the union was superseded too, which
# hid these broad categories.
SUPERSESSION_THRESHOLD = 0.8
bigram_components = defaultdict(list)
for c in candidates:
@ -470,11 +476,12 @@ def suggest_clusters(
c_ids = set(c["product_ids"])
if not c_ids:
continue
covered = set()
best_single = 0.0
for big in bigram_components.get(c["stem"], []):
covered |= set(big["product_ids"])
overlap = len(c_ids & covered) / len(c_ids)
if overlap >= SUPERSESSION_THRESHOLD:
overlap = len(c_ids & set(big["product_ids"])) / len(c_ids)
if overlap > best_single:
best_single = overlap
if best_single >= SUPERSESSION_THRESHOLD:
superseded.add(c["stem"])
filtered_count += 1

View file

@ -5570,12 +5570,14 @@ class TestTagSuggestPureFunctions(unittest.TestCase):
f"expected an apostrophe label in {labels}",
)
def test_suggest_clusters_multi_bigram_supersedes_unigram(self):
"""A unigram drops when the UNION of bigrams covering it
collectively exceeds the supersession threshold. Mirrors the
printableprompts gotcha: 'Day' shouldn't surface when
'Valentine's Day' + 'Patrick's Day' + 'Christmas' bigrams
already cover all its products."""
def test_suggest_clusters_keeps_umbrella_unigram_over_multi_bigrams(self):
"""An umbrella unigram is KEPT when it's covered only by the
UNION of several DISTINCT bigrams (no single bigram dominates).
Operator: they want 'holiday' recommended on its own even
though 'December Holiday' etc. also surface. Here 'Day' spans
'Valentine Day' + 'Patrick Day' (each ~50%, neither 0.8), so
'Day' survives as its own broad category. (A single dominant
bigram still supersedes see the test above.)"""
from types import SimpleNamespace
from ..lib.tag_suggest import suggest_clusters
# 4 products: two carry "Valentine's Day", two carry
@ -5600,7 +5602,36 @@ class TestTagSuggestPureFunctions(unittest.TestCase):
# Bigrams survive
self.assertTrue(any("alentin" in l.lower() for l in labels))
self.assertTrue(any("atrick" in l.lower() for l in labels))
# "Day" unigram drops — collectively covered by both bigrams
self.assertNotIn("Day", labels)
# "Day" umbrella unigram SURVIVES — only the UNION of the two
# distinct bigrams covers it; no single bigram reaches the
# supersession threshold, so it stays a standalone category.
self.assertIn("Day", labels)
def test_suggest_clusters_surfaces_holiday_with_phrase_bigrams(self):
"""Operator's exact case: 'December Holiday' / 'Winter Holiday' /
'Christmas Holiday' bigrams surface AND 'Holiday' is also
recommended on its own (umbrella unigram across distinct
bigrams no single bigram dominates 0.8)."""
from types import SimpleNamespace
from ..lib.tag_suggest import suggest_clusters
products = [
SimpleNamespace(id="a", title="December Holiday Math", description=""),
SimpleNamespace(id="b", title="December Holiday Reading", description=""),
SimpleNamespace(id="c", title="Winter Holiday Crafts", description=""),
SimpleNamespace(id="d", title="Christmas Holiday Activities", description=""),
]
clusters, _ = suggest_clusters(
products, stopwords=[], existing_tag_slugs=[],
min_products=2, max_share=1.0, min_title_share=0.0,
bigrams=True,
)
labels = [c["label"].lower() for c in clusters]
# 'holiday' on its own is recommended...
self.assertIn("holiday", labels)
# ...alongside at least one of the phrase bigrams.
self.assertTrue(
any("holiday" in l and l != "holiday" for l in labels),
f"expected a *_holiday bigram too, got {labels}",
)