Content Classification: improve relevance of taxonomy suggestions - #633
Conversation
|
The following accounts have interacted with this PR and/or linked issues. I will continue to update these lists as activity occurs. You can also manually ask me to refresh this list by adding the If you're merging code through a pull request on GitHub, copy and paste the following into the bottom of the merge commit message. To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook. |
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## develop #633 +/- ##
=============================================
+ Coverage 79.68% 79.74% +0.05%
- Complexity 2460 2469 +9
=============================================
Files 104 104
Lines 9955 9992 +37
=============================================
+ Hits 7933 7968 +35
- Misses 2022 2024 +2
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
|
This was the data seeded for the evaluation: |
Plugin Check is a known failure due to upstream problems. I re-ran the E2E tests and they are passing now so seems like a flaky test somewhere in there |
|
|
||
| // Only fetch existing terms when we need them for post-processing (existing_only strategy). | ||
| $existing_terms = Content_Classification_Experiment::STRATEGY_EXISTING_ONLY === $strategy | ||
| ? $this->get_existing_terms( $taxonomy ) |
There was a problem hiding this comment.
existing_only strategy currently loads all existing terms:
$this->get_existing_terms( $taxonomy )
which uses:
get_terms(
[
'hide_empty' => false,
'fields' => 'names',
]
)
Suggestion:
For very large taxonomies, fetching every term name on every request could become expensive. It may be worth caching the lookup or limiting retrieval to only what is needed for matching.
There was a problem hiding this comment.
Fair point, and thanks for flagging it. The constraint is that the full set is needed for correctness: parse_suggestions() matches the model's output case-insensitively against every existing term to resolve canonical casing and decide is_new, so we can't safely cap the retrieval without risking dropped matches.
Caching is the right fix, but it carries invalidation cost (term create/edit/delete), so I'd rather not fold it into this PR. I'll open a follow-up issue for an object-cache-backed lookup so it's tracked separately. Let me know if you'd prefer it handled here instead.
@dkotter what are your thoughts on this?
There was a problem hiding this comment.
Yeah, this is something we discussed at a few points in the original PR.
Originally that PR was sending all terms to the LLM when the existing_only strategy was on. I flagged that as a real risk for prompt bloat for sites with thousands of tags (which unfortunately is fairly common).
That strategy was then changed to send a smaller set of terms (I think 100?), asking the LLM to prioritize those in it's suggestions. We then grab all terms and filter the returned terms against that list to ensure we only recommend existing terms.
These get_terms queries typically run pretty fast (in my experience) and they do already have an internal cache so not sure there'd be much benefit to adding our own caching around that (wouldn't hurt but may not matter).
I think for now this is fine and if we get real-world user complaints I think there's two easy options here:
- Add our own caching layer
- Since we send the top 100 terms to the LLM to prioritize, we could modify our post processing to only allow those terms. That likely will give us the same results we're getting now anyway and will be faster
the model as content, so it is injected as-is rather than entity-encoded.
the candidate pool surfaced to the model under the `existing_only` strategy.
|
I will be on leave for some time. Will be officially back at 20th of July. |
|
@jeffpaul back at my desk. Any updates on this PR? I saw that you postponed it 1.3.0 milesstone. |
Jeff will be out himself for a bit but happy to help see this across the finish line. I know there's been quite a few comments on the PR, is there anything else you were tracking that needed worked on or is this ready for another round of review/testing? |
|
@dkotter this ready for another round of review |
What?
Closes #452.
Improves the relevance of category and tag suggestions produced by the
Content Classification experiment. Six commits, all scoped to the
ability and its tests; no schema changes, no UI changes.
Why?
#452 reports that suggestions are often not relevant to the post.
Audit found three concrete drivers in the existing pipeline:
from those terms" when the existing-only candidate pool was
present. That pool was the top 100 terms ordered by usage count, so
the model was being nudged toward generic popular tags
(
Tutorial,Guide,Beginner,WordPress,News) regardlessof fit.
intent. Tag vs. category guidance was identical, and the rubric
("prioritize specificity") was wrong for categories.
("somewhat relevant") were treated identically to 0.95 matches.
How?
The change rolls up four small, independently-shippable adjustments
to
includes/Abilities/Content_Classification/:Lower temperature + confidence floor.
0.5 → 0.2for moreconsistent classification. New
MIN_CONFIDENCE = 0.6constant andwpai_content_classification_min_confidencefilter. Suggestionsbelow the floor are dropped before sort and slice; the filter
return is clamped to
[0, 1]defensively.Richer taxonomy descriptor in the prompt. Replaces
<taxonomy>slug</taxonomy>with<taxonomy name="…" label="…" kind="tag|category" hierarchical="…">description</taxonomy>,built from
get_taxonomy(). Thekindattribute is what thenext step branches on.
System instruction rewrite — the headline change.
<available-terms>reframed as a candidate pool: "Use theseonly when they genuinely fit. Relevance always outweighs
popularity. Do not force a match."
kind="category"broad, thematic; respect hierarchy; do notpad with loosely-related parents.
kind="tag"specific, descriptive; explicit nudge againstgeneric process-style tags ("Tutorial", "Guide", "Beginner",
"News", "WordPress") unless the post is genuinely framed that
way and there is no more specific tag.
high-quality suggestions is better than padding" directive.
wpai_content_classification_available_termsfilter. Letssites swap the default popularity-ordered candidate pool for a
relevance-ranked one (e.g. embedding-based retrieval) without
touching prompt code. Returns the same default as before when no
filter is registered, so this is purely additive.
Evaluation
To make sure each step actually moved the needle, I built an
out-of-tree fixture corpus (12 posts spanning English/Spanish/Finnish,
short/medium/long, hierarchical and orphan categories, long-tail and
near-duplicate tags) plus a
wp eval-file-driven runner that invokesthe ability per fixture × taxonomy and computes precision / recall
against a ground-truth
relevant_termsblock. The eval harness isintentionally not included in this PR it's a measurement tool,
not part of the plugin but the resulting numbers are:
Eight of twelve fixtures hit precision 1.00 on tag suggestions after
step 3. The generic-tag FP cluster identified in the audit
("Tutorial", "Guide", "Beginner", "WordPress") is essentially gone.
LLM variance at
temp=0.2is ±0.03 precision / ±0.04 recall betweenidentical-code runs, so the step-by-step deltas should be read as
trend rather than precise; the headline finding roughly +30
percentage points of precision over baseline with recall held
roughly flat is well outside that envelope.
Use of AI Tools
AI assistance: Yes
Tool(s): Claude Code
Model(s): Claude Opus 4.7
Used for: Prompt audit, instruction redraft, test scaffolding, eval
harness design, and step-by-step implementation. All code, tests,
and the prompt rewrite were reviewed before commit; eval numbers
were measured against a real OpenAI provider (gpt-4.1-mini /
gpt-5-mini fallback chain).
Testing Instructions
the Content Classification experiment under
Settings → AI Plugin.(env / constant / settings).
Content Classification panel and request suggestions for tags and
for categories.
places) rather than generic ("Tutorial", "Guide", "Beginner")
unless the post is genuinely tutorial-style.
popular site-wide categories.