##  [Ingen kat i sækken: 10.000 produktbeskrivelser under lup med 60fps](/da/node/293)

    *Indsendt af Lennart den tir, 6 okt 2026 - 08:55*

 *Ti tusind produktbeskrivelser i én tabel, der scroller i 60 billeder i sekundet. Derefter fik tabellen en sprogmodel, der skriver forslag, og en dommer, der vurderer dem. Det hele er bygget i Nushell, og hver ændring ligger som en hændelse i en log.*

![Catifys logo: en rødbrun kat kigger op af en blå indkøbspose med et hvidt prismærke.](/sites/default/files/2026-10/00-logo.png)Catify: en kat i en pose med et prismærke.Jeg havde bygget Topify, et værktøj til at arbejde med Shopify-kataloger. Det voksede. Der kom læsbarhedsrapporter, stilguider og sprogtjek, og til sidst var det svært at sige, hvad det egentlig var til.

Så jeg bad Claude Code om at starte forfra med kun én ting i fokus: produktkataloget. Det blev til Catify, en kat i en pose med et prismærke. Navnet kommer fra katalog, og katten var ikke til at komme uden om.

Kravene var få. Tabellen skulle være hurtig, også med ti tusind produkter. Serveren skulle styre den. Og en beskrivelse skulle kunne redigeres i en stor popup, der kun viser beskrivelsen.

## En tabel, hvor browseren ikke ejer data

Catify kører på cross.stream: xs er hændelsesloggen, http-nu er webserveren, og alt er skrevet i Nushell. Browseren får HTML fra serveren og opdateres med Datastar over Server-Sent Events. Den har ingen kopi af kataloget.

Det kunne lyde som en dårlig opskrift på en hurtig tabel. Tricket er, at browseren selv står for at scrolle. Siden har en tom blok, der er lige så høj som ti tusind rækker. Serveren sender kun de 150 rækker, man er tæt på, og når man nærmer sig kanten, beder siden om de næste.

Vi testede med Kiths offentlige katalog, 10.000 produkter og 62 MB JSON. Et vindue på 150 rækker tager 2-5 millisekunder at lave på serveren. I browseren målte vi 60 billeder i sekundet og ingen tomme rækker, både ved 200 og ved 1.000 rækker i sekundet. Retter man en beskrivelse i én fane, er den opdateret i en anden fane 27 millisekunder senere.

![Catifys tabel med Kiths produkter og kolonnerne LIX, Struct, AQ og New.](/sites/default/files/2026-10/01-oversigt.jpg)Kiths 10.000 produkter med LIX, Struct og AQ. Wilson-ketsjeren øverst har et forslag, der venter (New).Kolonnerne til højre i tabellen er det, der gør den til mere end en liste. LIX er læsbarhed. Struct måler, hvor let beskrivelsen er at skimme. AQ er en vurdering af, om beskrivelsen svarer på kundens spørgsmål, og New viser et forslag, der venter på at blive læst.

## Et søgefelt, der forstår `price > 1000`

Søgefeltet kan også filtrere. Man kan skrive `structure < 40 items = 0 price > 300 type ~ jacket` og blande det med almindelige søgeord. Catify oversætter det til SQL med en fast liste over tilladte felter, og værdierne bliver aldrig sat direkte ind i forespørgslen. Felter, der ikke findes, vises som en rød besked, og resten af filteret virker stadig.

![Søgefeltet med filteret structure &lt; 40 items = 0 price &gt; 300 type ~ jacket og 235 resultater.](/sites/default/files/2026-10/02-filter.jpg)Filtre og fritekst i samme felt: 235 jakker over 300 uden liste og med svag struktur.## Struktur målt med en rigtig HTML-parser

Shopify gemmer beskrivelser som HTML, og den er ofte rodet. Strukturindekset bruger `nu_plugin_query`, Nushells plugin til CSS-selektorer, så det er en rigtig parser, der læser HTML'en, og ikke regulære udtryk. Indekset giver 40 point for en liste, 30 for overskrifter og 30 for afsnit på under 60 ord.

Det viste noget, som læsbarhedstal ikke fanger. Næsten alle Kiths beskrivelser er lister, hvor hver linje er sit eget afsnit. De ser pæne ud, men HTML'en har ingen struktur, og 9.098 af de 10.000 scorer under 40. Hos den danske butik Gumpel er medianen 88. Beregningen for alle ti tusind tager to sekunder.

## En dommer man kan regne med

AQ kommer fra jev, TypeSafes System One. Den vurderer på en skala med fire trin, der beskriver konkrete situationer, fra "intet besvaret" til "næsten alt besvaret med konkrete detaljer".

Vi startede med to vurderinger. Den anden skulle måle, om teksten overvinder købsindvendinger. Den korrelerede 0,92 med den første, så den målte i praksis det samme, og vi tog den ud.

Den vurdering, der blev tilbage, er stabil. Vi kørte 39 produkter tre gange. Den gennemsnitlige forskel var ét point, og ingen produkter skiftede kategori. Om vurderingen er rigtig, er et andet spørgsmål, og det kan kun butiksejeren svare på.

Derfor har Catify en kalibrering. Ejeren læser beskrivelser, som kunden ser dem, og svarer på ét spørgsmål med fire knapper. jevs tal er skjult imens. Efter 20 svar viser Catify, hvor enige ejeren og jev er, og hvor grænsen mellem en god og en svag beskrivelse bør gå. Ejeren kan tage grænsen i brug med ét klik.

![Kalibreringsvisningen med en urbeskrivelse og fire svarknapper fra Nothing useful til Everything.](/sites/default/files/2026-10/06-kalibrering.jpg)Kalibrering: butiksejeren bedømmer beskrivelsen, før jevs tal bliver vist.## Arbejdssæt med to spor

Massetilretning starter med et filter. To standardsæt dækker det meste: `task = create` er produkter uden beskrivelse, og `task = improve` er beskrivelser, der er vurderet som svage. Man kan også vælge efter produkttype eller bygge sit eget filter.

Når man fryser et sæt, bliver det gemt som en hændelse med en fast liste over produkter. Det er vigtigt, fordi filteret ellers flytter sig under én: retter man en svag beskrivelse, falder den ud af `aq < 50`. Hvert spor har sin egen instruktion til sprogmodellen, og den kan gælde for sættet alene eller for hele butikken.

![Et frosset sæt med ti ure og to tekstfelter med instruktioner til sporene create og improve.](/sites/default/files/2026-10/05-set-instruktioner.jpg)Et frosset sæt med en instruktion til hvert spor.Nogle regler kan instruktionen ikke ændre. Sprogmodellen må kun bruge produktets egne data, og den må ikke finde på materialer, mål eller påstande. Den skriver i butikkens eget sprog og efterligner de beskrivelser, ejeren har markeret med en stjerne som forbilleder.

## Forslag, der skal godkendes

Sprogmodellen ændrer aldrig en beskrivelse selv. Den skriver et forslag, som jev vurderer, og forslaget venter i kolonnen New. I popuppen ligger den gamle og den nye tekst ved siden af hinanden, renderet som kunden ser dem. Man kan skifte til HTML-kilden og rette, før man godkender.

![Popup med den nuværende beskrivelse til venstre og forslaget til højre, AQ 78 og 76.](/sites/default/files/2026-10/03-side-by-side.jpg)Wilson-ketsjeren: den gamle tekst og forslaget side om side, begge renderet.Wilson-ketsjeren er et godt eksempel. Den gamle tekst har AQ 78, og forslaget har 76. Indholdet er altså lige godt. Forskellen er strukturen: forslaget har rigtige lister og overskrifter, hvor originalen havde et afsnit pr. linje. Om det er værd at skifte, afgør ejeren, men nu kan man se, hvad man vælger mellem.

Fanen Changes viser forskellen ord for ord.

![Fanen Changes med slettet tekst markeret med rødt og ny tekst med grønt.](/sites/default/files/2026-10/04-changes.jpg)Fanen Changes viser forskellen ord for ord.Hos Gumpel løftede tre forslag til UV-trøjer AQ fra 35, 36 og 45 til 51, 49 og 67. Sprogmodellen fandt størrelserne i produktets valgmuligheder og opfandt ingenting. Det skal stadig læses, før det godkendes. Et forslag til et par skibriller med to linser tilføjede "så du kan skifte efter forholdene". Det står ikke i produktets data, og den slags er lettere at fange, når de to tekster står side om side.

## Hvad det kræver

Kildekoden er Nushell og et par skabeloner. En læsemodel i SQLite folder hændelsesloggen op til tabeller, og den kan smides væk og bygges op igen. Det skete rigtig mange gange undervejs, uden at noget gik tabt.

Det meste af arbejdet bestod i at måle, før vi besluttede noget, og i at teste på en separat kopi af data, så de rigtige sæt og vurderinger ikke blev rodet til. En enkelt fejl kostede mere tid end andre. I Datastar betyder `null` i et signal, at signalet skal slettes, ikke at det er tomt. Så stod der "AQ" uden noget tal.

Næste skridt er at sende de godkendte beskrivelser tilbage til Shopify.