feat: automatische Wort-Kategorisierung (Batches API + Sofort-Backfill)

Feste ~20er-Taxonomie geseedet (de/en/sv, published; bestehende
Kategorien werden wiederverwendet) + Tabelle category_batches.

src/lib/classifyWords.js: findet in Pairs verwendete Wörter ohne
Kategorie und klassifiziert sie per Haiku gegen die feste Liste.
- Stundenjob über die Message Batches API (asynchron, ~50% günstiger):
  submit/collect-Ticks, in index.js nach Boot + stündlich.
- Sofortiger synchroner One-Shot-Backfill (classifyWordsSync) für
  Live-Test ohne 24h-Verzug.
Beides materialisiert pair_categories via derivePairCategories.

POST /api/categories/auto-assign (admin): ?sync=true = Sofort-Backfill,
sonst ein Batch-Tick. Entkoppelt von generate-words und Publish.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-15 14:27:09 +02:00
parent 9738d3e35a
commit d66cff3f61
4 changed files with 365 additions and 0 deletions

View File

@@ -131,6 +131,56 @@ async function migrate() {
)
`);
// Feste Alltags-Taxonomie seeden (de/en/sv, published). Basis für die automatische
// Wort-Kategorisierung (src/lib/classifyWords.js) und die Kategorie-Punkte im Profil.
// Idempotent: bestehende Kategorie (z. B. "Tiere") wird wiederverwendet, keine Dubletten.
const CATEGORY_TAXONOMY = [
['Lebensmittel', 'Food', 'Mat'],
['Tiere', 'Animals', 'Djur'],
['Körper', 'Body', 'Kropp'],
['Kleidung', 'Clothing', 'Kläder'],
['Familie & Menschen','Family & People', 'Familj & människor'],
['Beruf & Arbeit', 'Job & Work', 'Jobb & arbete'],
['Haushalt', 'Household', 'Hushåll'],
['Wohnen & Möbel', 'Home & Furniture', 'Hem & möbler'],
['Natur & Pflanzen', 'Nature & Plants', 'Natur & växter'],
['Wetter', 'Weather', 'Väder'],
['Verkehr & Reisen', 'Transport & Travel', 'Transport & resor'],
['Stadt & Gebäude', 'City & Buildings', 'Stad & byggnader'],
['Schule & Bildung', 'School & Education', 'Skola & utbildning'],
['Technik & Geräte', 'Technology & Devices','Teknik & apparater'],
['Sport & Freizeit', 'Sports & Leisure', 'Sport & fritid'],
['Gefühle', 'Emotions', 'Känslor'],
['Farben', 'Colors', 'Färger'],
['Zahlen & Zeit', 'Numbers & Time', 'Tal & tid'],
['Werkzeuge', 'Tools', 'Verktyg'],
['Sonstiges', 'Other', 'Övrigt'],
];
for (const [de, en, sv] of CATEGORY_TAXONOMY) {
await query(
`INSERT INTO categories (titel_de, titel_en, titel_sv, status, requested_at, published_at)
SELECT $1, $2, $3, 'published', NOW(), NOW()
WHERE NOT EXISTS (SELECT 1 FROM categories WHERE lower(titel_de) = lower($1))`,
[de, en, sv]
).catch(() => {});
}
// Bestehende Treffer auf published heben (z. B. die alte "Tiere"-Kategorie)
await query(
`UPDATE categories
SET status = 'published', published_at = COALESCE(published_at, NOW())
WHERE lower(titel_de) = ANY($1) AND status <> 'published'`,
[CATEGORY_TAXONOMY.map(([de]) => de.toLowerCase())]
).catch(() => {});
// Asynchroner Kategorisierungs-Batch (Message Batches API) — Status über Boots/Redeploys merken
await query(`
CREATE TABLE IF NOT EXISTS category_batches (
batch_id TEXT PRIMARY KEY,
status TEXT NOT NULL DEFAULT 'submitted',
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
)
`);
await query(`
CREATE TABLE IF NOT EXISTS questions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),