Files
FabledCurator/backend/app/services/ml/aliases.py
T
bvandeusen 3610ba495f
CI / lint (push) Successful in 3s
CI / frontend-build (push) Successful in 23s
CI / backend-lint-and-test (push) Successful in 33s
CI / integration (push) Successful in 3m14s
feat(ml): drop image_record.tagger_predictions — image_prediction is sole store (#768 step 3)
Read cutover verified in prod (suggestions + allowlist read image_prediction;
backfill complete at 908k rows / 51k images). Removes the old JSON column and
everything that fed it:

- ImageRecord.tagger_predictions column removed; migration 0046 DROPs it.
  tagger_model_version kept as the "tagged / current?" signal the backfill
  sweep reads (needs-tagging check switched to tagger_model_version IS NULL).
- tag_and_embed no longer dual-writes the JSON — image_prediction is the only
  write path.
- importer re-import reset drops the JSON line (image_prediction rows are
  already deleted on re-import).
- Retired the one-time #768 backfill task + the #764 prune task, their admin
  endpoints, and their Maintenance cards (Backfill/PrunePredictionsCard).
- Tests seed/assert via image_prediction; stale column refs removed.

Disk reclaim is NOT automatic: DROP COLUMN is a catalog change. Run
`VACUUM FULL image_record` off-hours afterward to return the ~100 GB to the OS
so DB backups go small (#739). image_prediction (~90 MB) stays in pg_dump — it's
the source of truth now.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 18:52:33 -04:00

105 lines
3.3 KiB
Python

"""Alias resolution + CRUD.
A tag_alias maps (model_name, model_category) -> canonical Tag. Resolution
happens at suggestion-read time so the raw image_prediction rows stay unmolested.
"""
from collections.abc import Sequence
from dataclasses import dataclass
from sqlalchemy import delete, select
from sqlalchemy.dialects.postgresql import insert
from sqlalchemy.ext.asyncio import AsyncSession
from ...models import Tag, TagAlias
@dataclass(frozen=True)
class AliasRow:
alias_string: str
alias_category: str
canonical_tag_id: int
canonical_tag_name: str
class AliasService:
def __init__(self, session: AsyncSession):
self.session = session
async def resolve(self, name: str, category: str) -> Tag | None:
"""Return the canonical Tag for (name, category), or None if no alias."""
stmt = (
select(Tag)
.join(TagAlias, TagAlias.canonical_tag_id == Tag.id)
.where(TagAlias.alias_string == name)
.where(TagAlias.alias_category == category)
)
return (await self.session.execute(stmt)).scalar_one_or_none()
async def resolve_many(
self, pairs: list[tuple[str, str]]
) -> dict[tuple[str, str], Tag]:
"""Batch-resolve. Returns only the pairs that have an alias.
Used by SuggestionService so it does one query instead of N.
"""
if not pairs:
return {}
strings = {p[0] for p in pairs}
stmt = (
select(TagAlias, Tag)
.join(Tag, Tag.id == TagAlias.canonical_tag_id)
.where(TagAlias.alias_string.in_(strings))
)
rows = (await self.session.execute(stmt)).all()
wanted = set(pairs)
out: dict[tuple[str, str], Tag] = {}
for alias, tag in rows:
key = (alias.alias_string, alias.alias_category)
if key in wanted:
out[key] = tag
return out
async def create(
self, alias_string: str, alias_category: str, canonical_tag_id: int
) -> None:
"""Idempotent create (ON CONFLICT DO NOTHING)."""
stmt = insert(TagAlias).values(
alias_string=alias_string,
alias_category=alias_category,
canonical_tag_id=canonical_tag_id,
)
stmt = stmt.on_conflict_do_nothing(
index_elements=["alias_string", "alias_category"]
)
await self.session.execute(stmt)
async def remove(self, alias_string: str, alias_category: str) -> None:
await self.session.execute(
delete(TagAlias)
.where(TagAlias.alias_string == alias_string)
.where(TagAlias.alias_category == alias_category)
)
async def list_all(self) -> Sequence[AliasRow]:
stmt = (
select(
TagAlias.alias_string,
TagAlias.alias_category,
TagAlias.canonical_tag_id,
Tag.name,
)
.join(Tag, Tag.id == TagAlias.canonical_tag_id)
.order_by(TagAlias.alias_string.asc())
)
rows = (await self.session.execute(stmt)).all()
return [
AliasRow(
alias_string=r[0],
alias_category=r[1],
canonical_tag_id=r[2],
canonical_tag_name=r[3],
)
for r in rows
]