perf(tags): protective-alias uses tag kind, drops the image_record full scan
_create_protective_aliases scanned every image_record's tagger_predictions JSON (unindexed full scan, ~59k rows) to find the categories a merged-away tag's name was predicted under. That scan ran inside the merge transaction AFTER it had locked series_page — on a large library it held that lock for minutes and is what blocked migration 0040 (and starved the standardization task into its 40-min timeout). The scan was redundant: the tagger's tag_to_category map is one-to-one (a name has exactly one category) and a tag's kind is set from that category when created, so kind already IS the tagger's category for the name. The scan only ever rediscovered the kind. Build the single protective alias from src_kind directly — no scan, no lock-holding slow step in the merge. Rewrote test_alias_per_observed_prediction_category (which encoded the can't-actually-happen one-name-two-categories case) → test_protective_alias_uses_tag_kind. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -562,50 +562,33 @@ class TagService:
|
||||
async def _create_protective_aliases(
|
||||
self, src_name: str, src_kind: TagKind, tgt: int
|
||||
) -> bool:
|
||||
"""One alias per category the tagger has actually emitted for
|
||||
src_name (so future predictions resolve to target); fall back to
|
||||
the tag's kind when the tagger never predicted this exact name.
|
||||
Idempotent — never clobbers a pre-existing operator alias."""
|
||||
"""Alias (src_name, category) -> tgt so future tagger predictions of
|
||||
src_name resolve to the merge survivor. Idempotent — never clobbers a
|
||||
pre-existing operator alias.
|
||||
|
||||
The category is the tag's kind. The tagger's tag_to_category map is
|
||||
one-to-one (a name has exactly ONE category), and a tag's kind is set
|
||||
from that category when it's created — so kind already IS the tagger's
|
||||
category for this name. This used to SELECT DISTINCT category by scanning
|
||||
every image_record's tagger_predictions JSON (an unindexed full scan that,
|
||||
on a large library, held a series_page lock for minutes inside the merge
|
||||
and blocked migration 0040 — operator-flagged 2026-06-07). That scan only
|
||||
ever rediscovered the kind, so it's gone."""
|
||||
from ..models.tag_alias import TagAlias
|
||||
|
||||
rows = (
|
||||
await self.session.execute(
|
||||
text(
|
||||
"SELECT DISTINCT "
|
||||
" (tagger_predictions::jsonb -> :n ->> 'category') "
|
||||
" AS cat "
|
||||
"FROM image_record "
|
||||
"WHERE tagger_predictions IS NOT NULL "
|
||||
" AND (tagger_predictions::jsonb) ? :n"
|
||||
),
|
||||
{"n": src_name},
|
||||
category = src_kind.value if hasattr(src_kind, "value") else str(src_kind)
|
||||
res = await self.session.execute(
|
||||
pg_insert(TagAlias)
|
||||
.values(
|
||||
alias_string=src_name,
|
||||
alias_category=category,
|
||||
canonical_tag_id=tgt,
|
||||
)
|
||||
).all()
|
||||
categories = {r.cat for r in rows if r.cat}
|
||||
if not categories:
|
||||
kind_val = (
|
||||
src_kind.value
|
||||
if hasattr(src_kind, "value")
|
||||
else str(src_kind)
|
||||
.on_conflict_do_nothing(
|
||||
index_elements=["alias_string", "alias_category"]
|
||||
)
|
||||
categories = {kind_val}
|
||||
|
||||
created = False
|
||||
for cat in categories:
|
||||
res = await self.session.execute(
|
||||
pg_insert(TagAlias)
|
||||
.values(
|
||||
alias_string=src_name,
|
||||
alias_category=cat,
|
||||
canonical_tag_id=tgt,
|
||||
)
|
||||
.on_conflict_do_nothing(
|
||||
index_elements=["alias_string", "alias_category"]
|
||||
)
|
||||
)
|
||||
if res.rowcount:
|
||||
created = True
|
||||
return created
|
||||
)
|
||||
return bool(res.rowcount)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user