Gode gætterier
Udviklerne bag internetboghandlen Amazon har gået i spidsen med hensyn til udvikling af en del nye webkoncepter. Blandt de smarteste innovationer er købsforslag baseret på brugeradfærd. Ideen er ganske simpel: Når man som bruger læser om et produkt, tilbydes man samtidig en række andre relaterede produkter.
Med det store produktudvalg som Amazon og andre konsum e-handelssteder tilbyder, ville det blive en dyr affære, hvis der skulle sidde redaktører og udvælge relaterede produkter. Samtidig er det ikke sikkert, at det faktisk ville være produkter, der ramte kundens smag.
En mere sikker metode er at benytte brugeradfærd som udgangspunkt for relationer imellem produkter. Metoden kan også bruges i andre sammenhænge end e-handel - faktisk kan den bruges til alle typer af websites med et stort antal sider. Tricket er jo blot at give brugerne et tilbud om videre, relevant læsning. På virksomhedens website kan det forlænge brugernes besøg, og på intranettet kan det give lettere adgang til information.
Statistik er en stor og smuk videnskab, og statiske undersøgelser kan brede sig fra det simple til det højt komplekse. I dette eksempel holder vi os i den lette ende, men som artiklen forhåbentlig viser, er det ikke særlig svært at implementere denne type funktionalitet, og da det meste bygger på sund fornuft kan man sagtens gå videre, end hvad der er vist her.
I eksemplet her benytter vi en nyhedsportal som scenarie, og ideen er så, at hvis mange læsere, som har læst artikel nummer 45, også har læst artikel 13, er der grund til at antage, at de to artikler rammer det samme segment af læserskaren.
Det er selvfølgelig ikke meget mere end et kvalificeret gæt, men hvis man har lusket lidt rundt på Amazon, kan man ikke undgå at bemærke, at det faktisk virker. Denne artikels forfatter er i hvert fald lidt imponeret over, hvorledes e-handelsstedet efterhånden kender min lidt obskure smag i amerikanske rockmusik. Hvis jeg for eksempel søger på det fantastiske orkester med det lange navn Thinking Fellers Union Local 282, får jeg et link til Godspeed You Black Emperor, og det er jo slet ikke så dumt gættet.
Kære læser, hvis det ikke siger dig en disse, så var det faktisk meningen med dette eksempel: Brugeradfærd giver gode vink, uden at der kræves redaktionelle kræfter, og udover at holde brugeren fanget på websitet, så giver det også en oplevelse af personalisering: Det kender min smag.
Et eksempel
Men tilbage til vores eksempel. Vi forestiller os, at vi har en database med en række artikler (eller blot websider). Som tidligere nævnt er der mange af de læsere, som har læst artikel 45, der tidligere har læst artikel nummer 13. Med andre ord har vi brug for en tabel i den bagvedliggende database, som holder styr på artikler, og deres reference-artikler, og hvor mange læsere, der har læst begge. Sådan en tabel kunne se sådan ud:
Her kan vi blandt andet se, at to læsere, som læste artikel 42, også læste artikel 13, mens 10 læsere, som læste 42, også læste artikel 67.
Lad os forestille os, at et ny læser kommer ind på artikel 42. Når vi skal generere de relevante links, er det blot at foretage et udtræk fra tabellen, så vi får de artikler, som er refererede fra artikel 42, sorteret i faldende orden, så vi får de mest hittede referencer først. I SQL kunne det se sådan ud:
SELECT REF_ARTIKEL_ID
FROM REF_ARTIKLER
WHERE ARTIKEL_ID=42
ORDER BY ANTAL_HITS DESC;
Med det førnævnte eksempel ville det give os artikel-indekserne 67, 19, 177 og 13, da 67 er den mest læste med 10 sammenfald, og 13 den mindst læste, med 1 sammenfald.
Det er jo ret simpelt. Men hvordan pokker får vi opsamlet denne information? Vi er nødt til at holde styr på, hvilke artikler brugerne læser, altså deres historik på websitet, og da denne information er tilknyttet hver enkelt læser, kan denne information gemmes i en cookie. Så slipper vi for at holde styr på brugerne, hvilket er en stor fordel, hvis der er mange tusinde brugere.
Vi skal altså skabe en cookie på brugerens browser, og hver gang brugeren har læst en artikel, altså har åbnet en side i browseren, tilføjer vi artiklens id-nummer i enden af cookien.
Derudover skal vi opdatere tabellen ved hver enkel sideeksponering. Hvis vores bruger ser på artikel nummer 65, og de tidligere læste artikler, gemt i cookien, er 12, 46 og 58, skal antal hits fra 65 til 12 opdateres med plus en, 65 til 46 skal også opdateres med plus en, og 65 til 58 skal også opdateres med plus en.
Koden
Mere er der ikke i det. I pseudo-kode kan det se sådan ud:
hent_artikel_fra_tabellen
hent_artikel_referencer
tilføj_det_aktuelle_artikel_id_til_cookie_array
gem_cookie_hos_bruger
udskriv_artikel_og_referencer
opdater_reference_tabel
I vores eksempel vil vi benytte PHP og ODBC, men det skal ikke afholde ASP'er og JSP'er eller ColdFusion'er for at læse videre - det hele er forholdsvist ukompliceret. I PHP kunne det se sådan ud:
/*
Her står de globale variabler...
*/
$artikel = hent_artikel();
$ref_artikler = hent_ref_artikler();
opdater_cookie();
echo $artikel . $ref_artikler;
opdater_reference_tabel();
/*
Her står funktionerne...
*/
?>
...og så er det bare at kode funktionerne. Vi forestiller os, at vi har to globale størrelser til at starte med: $ARTIKEL_ID, som er nummeret på artiklen, vi skal vise, og cookien $LAESTE_ARTIKLER, som indeholder en kodet version af et array med historikken, altså alle de artikler, som brugeren har læst tidligere. Den er selvfølgelig tom, hvis det er brugerens første besøg. $ARTIKEL_ID kunne passende overføres til scriptet via en GET-parameter, så et link til artikel 42 kunne se sådan ud:
http://www.mitwebsite.tld/vis_artikel.php?ARTIKEL_ID=42
Den første funktion vi støder på i vores skitse foroven er hent_artikel(), og den finder blot artiklen i databasen udfra $ARTIKEL_ID, så den gemmer vi til sidst.
Dernæst er det hent_ref_artikler(), som skal hente referencerne i reference-tabellen, som vi viste tidligere. Vi har endda også skrevet SQL-sætningen i tilfældet med artikel 42, så der kræves ikke meget:
function hent_ref_artikler() {
/* På samme vis som i tv-køkkenet har
vi tidligere oprettet en odbc-forbindelse,
gemt i variablen "$conn".
Variablen $ANTAL_REFERENCER er det maksimale
antal referencer, vi skal hente, f.eks. 5.
$PHP_SELF er en global variabel, der
indeholder stien til script-filen. */
global $conn, $ARTIKEL_ID, $ANTAL_REFERENCER, $PHP_SELF;
// Vi udfører vores SELECT:
$res = odbc_exec($conn,
" SELECT REF_ARTIKEL_ID" .
" FROM REF_ARTIKLER" .
" WHERE ARTIKEL_ID=" . $ARTIKEL_ID .
" ORDER BY ANTAL_HITS DESC;"
);
// Og opsamler resultaterne i variablen $links:
$links = "<P>Andre læsere læste også:</P>";
$i = 0;
// Her genereres hvert enkelt link, men
// højst så mange som angivet i $ANTAL_REFERENCER:
while(odbc_fetch_row($res) && ($i<$ANTAL_REFERENCER)) {
$link_id = odbc_result($res,1);
/* "Punktum" (.) betyder strengkonkatenering
i PHP - lige som "plus" (+) i VB og JS. */
$links .= "<A HREF=" . $PHP_SELF . "?ARTIKEL_ID="
. $link_id
. ">Artikel nummer " . $link_id . "</A><BR />";
$i++;
}
// Og tilbage med skidtet:
return $links;
}
Funktionen giver en udskrift i stil med:
Andre læsere læste også:Artikel nummer 167
Artikel nummer 19
Artikel nummer 49
- og en oplagt forbedring ville selvfølgelig være at få artiklernes overskrifter udskrevet i stedet for, så der stod:
Andre læsere læste også:Sådan kodes børneopdragelse i Python
Orientalsk madlavning for nybegyndere
Verden i chok: Ny computer hurtigere end gammel computer
Det kræver et banalt databasekald til, og er hermed overladt til læseren.
Gi' dem kage!
Så kommer det sjove, hvor vi skal manipulere med cookien. I PHP er cookies til rådighed ligesom GET- og POST-parametre: De er der bare. ASP-folket skal benytte Request.Cookies-objektet.
function opdater_cookie() {
global $LAESTE_ARTIKLER, $ARTIKEL_ID, $laeste_artikler_array;
// $LAESTE_ARTIKLER er cookien, hvis den er oprettet
if(isset($LAESTE_ARTIKLER)) {
// Hvis cookien findes, så:
// Denne linie er lidt skummel, men forklaring følger
$laeste_artikler_array = unserialize($LAESTE_ARTIKLER);
// Tilføj $ARTIKEL_ID i enden af arrayet,
// hvis $ARTIKEL_ID ikke allerede optræder:
if (!in_array($ARTIKEL_ID, $laeste_artikler_array)) {
array_push($laeste_artikler_array, $ARTIKEL_ID);
}
} else {
/* Brugeren har ikke cookien, det er
altså første besøg på sitet, og så
skal artikel-arrayet oprettes. */
$laeste_artikler_array = array($ARTIKEL_ID);
}
/* Her oprettes cookien, eller den
eksisterende cookie overskrives: */
// Endnu en dunkel linie:
$laeste_artikler = serialize($laeste_artikler_array);
setCookie("LAESTE_ARTIKLER", $laeste_artikler,
time()*3600*24*365, "/");
}
Den dunkle linie med serialize() skal forklares. Funktionen serialiserer vores artikel-index-array, og det betyder, at arrayet bliver skabt om til en tekststreng, som vi kan gemme i cookien. Når vi så skal benytte vores array igen, indlæser vi den fra cookiens tekststreng med funktionen unserialize(). Det kan man gøre med næsten alle variabeltyper, og det er mægtig smart.
En tilsvarende funktion findes tilsyneladende i Microsofts scripting-univers, men vi kunne ikke rigtigt få det til at spille, så der kan man alternativt danne en kommasepareret liste ved hjælp af de array- og strengfunktioner, som VBScript og JScript tilbyder (vink: kig på Split og Join).
Nu har vi sendt cookien afsted, så nu kan vi udskrive resten af artiklen til brugerens browser. Cookien ligger nemlig i HTTP-headeren, så den skal sendes afsted først. Vi udskriver artiklen med linien:
echo $artikel . $ref_artikler;
Så er der kun et enkelt punkt tilbage på agendaen, og det er at opdatere referencetabellen.
Update
Nu skal vi opdatere referencetabellen. Lad os endnu engang sige, at læseren er inde på artikel 42, altså $ARTIKEL_ID == 42. Hvis artikel nummer 12 ligger i brugerens cookie, skal vi foretage en opdatering der ser sådan ud:
UPDATE REF_ARTIKLER
SET ANTAL_HITS = ANTAL_HITS + 1
WHERE ARTIKEL_ID = 42 AND REF_ARTIKEL_ID = 12;
Det skal vi altså gøre med samtlige artikel-id'er i cookien, på 12's plads i den ovenstående UPDATE.
Det ser jo nemt nok ud, men hvad nu hvis der aldrig er nogen, der før har læst både artikel 42 og artikel 12? En gang skal jo være den første, så vi må altså først tjekke tabellen med en SELECT, og hvis der ikke er nogen række med 42 og 12, må vi bruge INSERT til at lave sådan en fætter. Lad os skrive en funktion.
function opdater_reference_tabel() {
global $conn, $ARTIKEL_ID, $laeste_artikler_array;
$antal_artikler = count($laeste_artikler_array);
for($i=0;$i<$antal_artikler;$i++) {
$ref_artikel_id = $laeste_artikler_array[$i];
if($ref_artikel_id == $ARTIKEL_ID) {
// Vi skal ikke tælle referencer til den aktuelle
// side, så vi hopper et loop over her:
continue;
}
$res = odbc_exec($conn,
" SELECT * " .
" FROM REF_ARTIKLER" .
" WHERE ARTIKEL_ID = " . $ARTIKEL_ID .
" AND REF_ARTIKEL_ID = " . $ref_artikel_id .
" ;"
);
if(!odbc_result($res,1)) {
$res = odbc_exec($conn,
" INSERT INTO REF_ARTIKLER( " .
" ARTIKEL_ID," .
" REF_ARTIKEL_ID," .
" ANTAL_HITS)" .
" VALUES(" . $ARTIKEL_ID .
" ," . $ref_artikel_id .
" , 0);"
);
}
// Nu skulle det hele være på plads...
$res = odbc_exec($conn,
" UPDATE REF_ARTIKLER" .
" SET ANTAL_HITS = ANTAL_HITS + 1" .
" WHERE ARTIKEL_ID = " . $ARTIKEL_ID .
" AND REF_ARTIKEL_ID = ". $ref_artikel_id .
" ;"
);
}
}
Ovenstående funktion er ikke helt fin i kanten: Man bør altid tjekke, at database-manipulationerne er veludført imellem hvert enkelt trin. Prøv at gætte, hvem jeg overlader det job til.
Derudover kan man overveje, om det er nødvendigt at opdatere ved hvert eneste sidevisning. En mere økonomisk løsning ville være at vente en dag imellem hver opdatering, og det kan gøres ved at kigge på cookiens tidsstempel.
Andre forbedringer er at se på, hvor mange artikler der gemmes. En cookie skal ikke nødvendigvis kunne indeholde mere end 4 kilobyte data, så hvis cookien kommer over denne grænse, kan man risikere at den bliver skåret af i enden, og dermed kan den ikke genindlæses.
Hele scriptet og en Acces-testdatabase kan downloades her.