feat: MPS-24 Phase 2 — auto-suggest tags from title + description
Operator with 481 untagged products (printableprompts.com) gets a one-click path to a usable categorization without hand-tagging each product. Strictly suggest-then-approve — nothing writes Tag or ProductTag rows until the operator clicks Apply on a cluster. - lib/tag_suggest.py: pure-function clusterer. Tokenize title (weight 3) + description (weight 1, capped at 100 unique tokens per product), strip markdown / URLs / HTML, English + per-shop stopwords, simple suffix-strip stemmer, group by stem, drop stems matching existing tag slugs, rank by product count, label each cluster with the most frequent original word for its stem. No new deps, no ML. - scripts/backfill_tags.py: CLI preview + --apply for a single shop. - views/shop.py: shop_tags gains action=apply_suggestion (creates tag + bulk-attaches every product in cluster) and action=dismiss_suggestion (adds the cluster's words to shop.tag_stopwords_json so it never resurfaces). ?show_suggestions=1 triggers the cluster compute. - templates/shop_tags.j2: "Suggest categories from titles + descriptions" button + suggestions well with per-cluster sample titles, Apply, and Dismiss buttons. - 15 new tests (11 unit over tokenize / stem / cluster + 4 functional over the suggest/apply/dismiss flow). 1064 total passing. On a printableprompts-style sample the clusterer surfaces Math, Reading, Literacy, Seasonal, Novel, Activities, Comprehension — matching what an operator would manually pick.
This commit is contained in:
parent
c03ca53fb4
commit
5dbbe697b6
11 changed files with 884 additions and 29 deletions
|
|
@ -215,6 +215,7 @@ mps_page_session (raw rows)
|
|||
| Tag chip / lane caps (MPS-24) | `shop.home_layout_tag_limit` / `shop.home_layout_per_lane_limit` | `home-layout-settings` | 8 / 10 |
|
||||
| Featured products strip (MPS-24) | `shop.featured_product_ids_json` | `home-layout-settings` | empty |
|
||||
| Product tags (MPS-24) | `Tag` + `ProductTag` association | product edit + `/s/{id}/tags` bulk editor | — |
|
||||
| Tag auto-suggest (MPS-24 Phase 2) | `lib/tag_suggest.py` over `Product.title` + `Product.description` | `?show_suggestions=1` on `/s/{id}/tags` + `scripts/backfill_tags.py` | Never auto-applies |
|
||||
|
||||
## Ticket Index
|
||||
|
||||
|
|
@ -244,7 +245,7 @@ mps_page_session (raw rows)
|
|||
| [MPS-21](tickets/mps-21.md) | Make-an-Offer Mode | Complete |
|
||||
| [MPS-22](tickets/mps-22.md) | Kill-Switch Feature Flags — Karaoke + Torrent Off by Default | Complete |
|
||||
| [MPS-23](tickets/mps-23.md) | Consolidated Transactional Sender Identity + Shop Contact Email | Open |
|
||||
| [MPS-24](tickets/mps-24.md) | Shop home page overhaul + product categorization (tags + chips + lanes) | In progress (Phase 1 landed) |
|
||||
| [MPS-24](tickets/mps-24.md) | Shop home page overhaul + product categorization (tags + chips + lanes + auto-suggest) | In progress (Phases 1 + 2 landed) |
|
||||
|
||||
## Related Docs
|
||||
|
||||
|
|
|
|||
|
|
@ -282,6 +282,8 @@ All components are documented with live examples at `/styleguide`. The styleguid
|
|||
| `[data-tag-strip]` | chip strip | JS hook for `tag_filter.js` |
|
||||
| `[data-tag-grid]` | flat `.serp` | JS hook — items inside carry `data-tag-slugs` for in-place filter |
|
||||
| `[data-tag-slugs]` | `.serp-item` | Space-separated tag slugs the item carries; consumed by `tag_filter.js` |
|
||||
| `.tag-suggest-list` / `.tag-suggest-item` | `shop_tags.j2` (Phase 2) | Suggested-cluster well rendered when operator clicks "Suggest categories from titles + descriptions"; per-cluster grid with label, sample titles, Apply / Dismiss actions |
|
||||
| `.tag-suggest-actions` | `shop_tags.j2` | Apply / Dismiss button row, `grid-auto-flow: column` |
|
||||
|
||||
## CSS Conventions
|
||||
|
||||
|
|
|
|||
|
|
@ -2,11 +2,13 @@
|
|||
|
||||
## Status
|
||||
|
||||
**PHASE 1 SHIPPED (2026-05-15).** Tag model + chip strip + sectioned-lanes
|
||||
layout + bulk tagger live behind an opt-in `home_layout` selector (default
|
||||
`0` = flat = unchanged). Phase 2 (auto-tag from titles) and Phase 3
|
||||
(ML-assisted via uncloseai) follow under this same ticket per
|
||||
CLAUDE.md "One Feature, One Ticket".
|
||||
**PHASES 1 + 2 SHIPPED (2026-05-15).** Tag model + chip strip +
|
||||
sectioned-lanes layout + bulk tagger live behind an opt-in `home_layout`
|
||||
selector (default `0` = flat = unchanged). Phase 2 adds a
|
||||
title-plus-description auto-tagger surfaced as one-click cluster apply
|
||||
in the bulk tagger UI + `scripts/backfill_tags.py` CLI. Phase 3
|
||||
(uncloseai-backed ML categorization behind a kill switch) follows under
|
||||
this same ticket per CLAUDE.md "One Feature, One Ticket".
|
||||
|
||||
**Background:** operator feedback on `shop.printableprompts.com` flagged our
|
||||
default home page as the reason for considering a move to Shopify. We
|
||||
|
|
@ -160,28 +162,51 @@ from "scroll 481 items" to "click chip, scan ~50, click product."
|
|||
- Filter chips also added to `/search` results so shopper can refine by tag
|
||||
after a keyword query (`/search?keywords=X&tag=Y`).
|
||||
|
||||
### Phase 2 — Sectioned lanes + auto-tag from titles (A3 + B1)
|
||||
### Phase 2 — Sectioned lanes shipped in Phase 1; auto-tag from title + description
|
||||
|
||||
Even fewer clicks: shopper sees *all* categories on land, no chip click needed.
|
||||
Lanes are scoped to top-N tags by popularity.
|
||||
Phase 1 already shipped sectioned-lane layout (`home_layout == 2`) — we
|
||||
brought it forward because rendering the lanes was a one-line template
|
||||
branch on top of the chip work. What remains for Phase 2 is the
|
||||
**deterministic title + description auto-tagger** so an operator with 481
|
||||
untagged products gets a working categorization in one click.
|
||||
|
||||
- `home_layout == 2` renders one lane per top-N tags, each lane shows up to
|
||||
~10 products of that tag, with a "see all" link to the tag detail page.
|
||||
- Lanes scroll horizontally on touch; stack as single-column below 800px
|
||||
(matches our existing mobile reorder pattern).
|
||||
- Deterministic title-keyword auto-tagger as a standalone script
|
||||
(`scripts/backfill_tags.py`) and a "Suggest tags from titles" button on
|
||||
the bulk tag editor. Rules:
|
||||
- Tokenize all titles in shop.
|
||||
- Drop stopwords + common shop-vocabulary words (per-shop configurable
|
||||
list — printableprompts will drop `write`, `the`, `room`, `activity`
|
||||
because they appear in nearly every title and group nothing).
|
||||
- Stem (simple suffix strip — no new dep, no Porter ports).
|
||||
- Cluster: products sharing >= 2 non-stopword stems form a candidate group.
|
||||
- Label the candidate group by its most frequent shared stem.
|
||||
- **Emit candidate tags to operator for approval — never auto-commit**
|
||||
(A6 hybrid: suggest, don't impose).
|
||||
- This phase has no ML, no external deps. It's grep-flavoured clustering.
|
||||
Inputs:
|
||||
- `Product.title` — full token weight × **3** (short, decisive, intentional).
|
||||
- `Product.description` — raw markdown stripped of formatting, tokenised,
|
||||
weight × **1**, capped at the first ~100 unique tokens per product so
|
||||
long blog posts don't drown short product copy.
|
||||
|
||||
Pipeline:
|
||||
1. Tokenize title + description → lowercased words ≥ 3 chars.
|
||||
2. Drop platform-default English stopwords + per-shop
|
||||
`tag_stopwords_json` overrides. For printableprompts that adds
|
||||
`write`, `room`, `activity`, `the`, etc.
|
||||
3. Stem with a simple suffix-strip (no Porter port, no new dep) —
|
||||
`seasonal`/`seasons`/`season` → `season`.
|
||||
4. Build per-stem product sets across the catalog.
|
||||
5. Drop stems whose slug already exists as a shop tag (we don't
|
||||
re-suggest already-applied categories).
|
||||
6. Keep stems carried by ≥ 2 products; rank by product count desc.
|
||||
7. For each candidate stem, label = most frequent **original** word for
|
||||
that stem (so `valentin` displays as `Valentine's`, not `valentin`).
|
||||
|
||||
Surface:
|
||||
- New "Suggest categories from titles + descriptions" button on
|
||||
`/s/{shop_id}/tags`. Renders a "Suggested categories" well listing each
|
||||
candidate cluster — label, sample product titles, product count.
|
||||
- One-click apply per cluster — creates the tag + bulk-attaches every
|
||||
product in the cluster, all under one form POST.
|
||||
- One-click dismiss per cluster — adds the stem's label to
|
||||
`tag_stopwords_json` so it never resurfaces.
|
||||
- Standalone CLI `scripts/backfill_tags.py --shop=<id> [--dry-run]` for
|
||||
larger shops that prefer a terminal preview.
|
||||
|
||||
**A6 hybrid — suggest, never auto-commit.** Cluster output is rendered to
|
||||
the operator; nothing writes `Tag` / `ProductTag` rows until the operator
|
||||
clicks Apply.
|
||||
|
||||
No ML, no external deps. Pure Python over `Product.title` +
|
||||
`Product.description`. O(N × tokens) over a shop's catalog.
|
||||
|
||||
### Phase 3 — ML-assisted categorization via uncloseai (A5)
|
||||
|
||||
|
|
@ -282,6 +307,18 @@ picker is Phase 2).
|
|||
| `CLAUDE.md` | New "Shop Home Layout + Tags" section + "Ticket Scoping — One Feature, One Ticket" rule |
|
||||
| `~/git/www.makepostsell.com/index.html` + `pricing.html` | "Categorized Home Page" feature card + pricing list entry |
|
||||
|
||||
### Phase 2 — shipped 2026-05-15
|
||||
|
||||
| File | Change |
|
||||
|---|---|
|
||||
| `lib/tag_suggest.py` (new) | Pure-function clusterer: `tokenize`, `simple_stem`, `stem_bag`, `suggest_clusters` over title (weight 3) + description (weight 1, capped at 100 unique tokens) |
|
||||
| `scripts/backfill_tags.py` (new) | CLI: `--shop=<id>` previews suggestions; `--apply` creates tags + attaches products |
|
||||
| `views/shop.py` | `shop_tags` view gained `action=suggest`, `action=apply_suggestion`, `action=dismiss_suggestion`; `?show_suggestions=1` triggers cluster compute |
|
||||
| `templates/shop_tags.j2` | "Suggest categories from titles + descriptions" button + suggestions well with one-click Apply / Dismiss per cluster |
|
||||
| `static/css/common.css` | `.tag-suggest-list` / `.tag-suggest-item` / `.tag-suggest-actions` styles |
|
||||
| `tests/test_models.py` | `TestTagSuggestPureFunctions` — 11 unit tests over tokenize / stem / cluster |
|
||||
| `tests/test_functional.py` | `test_suggest_clusters_renders_candidates`, `test_apply_suggestion_creates_tag_and_attaches_products`, `test_dismiss_suggestion_adds_to_stopwords`, `test_apply_suggestion_rejects_empty_input` |
|
||||
|
||||
## Tests (Phase 1)
|
||||
|
||||
### Unit (`test_models.py`)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue