Come costruire un Recommender Engine in PHP e MySQL

Posted on 7 febbraio 2011

0


Settimana scorsa abbiamo visto che cosa siano gli strumenti di Recommendation.
Oggi costruiamo un semplice Recommender Engine in PHP e MySQL utilizzando una funzionalità di Clustering.

SI tratta di raggruppare (in automatico) una serie di elementi (prodotti, documenti, libri, utenti…) sulla base delle loro caratteristiche, di modo che all’interno dei diversi gruppi ci siano elementi che condividono il maggior numero possibile di caratteristiche. In questo modo sarà possibile, per ogni elemento, suggerire quali siano gli elementi a questo affini.

Cominciamo a creare una tabella che conterrà i principali riferimenti ai nostri elementi (identificativo e nome) e l’elenco dei tag che li caratterizzano:

DROP TABLE IF EXISTS `tagcount`;
CREATE TABLE `tagcount` (
   `id` int(10) unsigned NOT NULL auto_increment,
   `item` varchar(200) NOT NULL,
   `tag` text NOT NULL,
PRIMARY KEY (`id`)
   ) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=utf8;

Ovviamente in questo esempio la tabella è molto rudimentale, e contiene giusto un identificativo, un nome e lo spazio dove scrivere le varie caratteristiche. Popoliamo la tabella con un elenco di documenti e relativi tag:

INSERT INTO `provevarie`.`tagcount` VALUES
    (0,'doc1', ',rifiuto,,ostile,,opercolo,,brivido,,latino,');
INSERT INTO `provevarie`.`tagcount` VALUES
    (0,'doc2', ',sensibilita,,logo,,opercolo,,altolocato,,senso,');
INSERT INTO `provevarie`.`tagcount` VALUES
    (0,'doc3', ',ostile,,inglese,,latino,,dolce,,moda,,italiano,');
INSERT INTO `provevarie`.`tagcount` VALUES
    (0,'doc4', ',latino,,ostile,,dolce,,sineddoche,');

Ora costruiamo una pagina PHP in cui effettuiamo le seguenti operazioni:

Decidiamo la tolleranza che accettiamo nell’identificare i gruppi (a numero minore corrisponde tolleranza minore) poi leggiamo tutti gli elementi della tabella.

<?php
$dblink = mysql_connect('localhost', 'root', '');
mysql_select_db('DATABASE_NAME', $dblink);
$tollerance = 2;
$min = 2;
$query = "SELECT id,doc,tag FROM tagcount";
$result = mysql_query($query);
if ($result) {
 while ( $row = mysql_fetch_array($result) )
  {
  echo $row['doc'] . "<br>";
  echo $row['tag'];
  echo "<p>&nbsp;</p>";
 }
} ?>

Per ognuno di questi elementi ricaviamo un array che contenga i tag.

 while ( $row = mysql_fetch_array($result) )
  {
  echo $row['doc'] . "<br>";
  // echo $row['tag'] ;
  if ( $row['tag'] )
   {
   $arr = explode( ',,' , substr( $row['tag'],1,-1 ) );
   print_r( $arr );
  }
  echo "<p>&nbsp;</p>"; }
}

Contiamo il numero di elementi dell’array e vi sottraiamo la tolleranza stabilita in precedenza.

   // print_r( $arr );
   $q = count($arr) - $tollerance;
   if ( $q < $min ) $q = $min;
   echo " - " . $q;

Costruiamo una stringa partendo dall’array di modo che ogni elemento sia separato da “|”, e facendo attenzione che tutti gli elementi abbiano una virgola sia all’inizio che alla fine (di modo che la regola per “italiano” non valga anche per un’etichetta quale “romanzo italiano”).

   // echo " - " . $q;
   $tags = ',' . implode($arr, ',|,') . ',';
   echo $tags;

Effettuiamo una query del tipo “SELECT * FROM tagcount WHERE tag REGEXP(‘((,a|,b|,c|,d)(.*)){2}’)”

   // echo $tags;
   $query2 = "SELECT * FROM tagcount WHERE tag REGEXP('(($tags)(.*)){".$q."}')";
   $query2 .= " AND id <> " . $row['id'];
   // echo $query2."<br>";
   $result2 = mysql_query($query2);
   if ($result2)
    {
    while ( $row2 = mysql_fetch_array($result2) )
     {
     echo $row2['doc'] . "<br>";
    }
   }

In questo modo stiamo cercando tutti quegli elementi che condividono con quello attuale un numero di tag che corrisponde al numero di tag dell’elemento attuale al netto del grado di tolleranza stabilito all’inizio.

Possiamo ottenere due cose, entrambe interessanti:

  • per ogni documento, l’elenco di quelli “simili” al netto del documento da cui si parte (riga $query2 .= ” AND id <> ” . $row[‘id’];); in tal caso è accettato che non vi sia alcun risultato (nessun documento “simile”);
  • un gruppo che elenchi tutti gli elementi correlati (commentando la riga $query2 .= ” AND id <> ” . $row[‘id’];); in tal caso occorre fare in modo che se un documento di partenza non fornisce risultati non scriva nulla.

Qui mi sono limitato a mostrare a monitor i risultati: ovviamente è possibile prendere i risultati di $row2 e scriverli in un DB, come meglio aggrada (ad esempio, corredando ogni documento di una colonna “Pagine simili”; oppure creando una tabella “Cluster” e inserendovi i gruppi così come sono.

Attenzione:

Una coppia di elementi mostrerà un grado di affinità differente a seconda che lo si guardi partendo da un elemento piuttosto che un altro. Facciamo un esempio:

  • Documento1: ,marchionne,fiat,sindacati,fiom,
  • Documento2: ,sindacati,fiom,
  • Grado di tolleranza: 1.

Analizzando Documento1 cercheremo REGEXP(‘((,marchionne|,fiat|,sindacati|,fiom)(.*)){3}’), che non restituisce alcun valore oltre a se stesso.

Analizzando Documento2 cercheremo REGEXP(‘((,sindacati|,fiom)(.*)){1}’), che restituisce Documento1 (oltre a se stesso).

Questo perché il secondo documento si occupa di un argomento più ristretto del primo, e quindi potenzialmente non del tutto affine. Avendo comunque ottenuto l’indicazione che Documento1 e Documento2 sono simili dal punto di vista di Documento2, l’informazione sul gruppo è stata comunque ottenuta, e ora si può consigliare il documento relativo sia che si stia leggendo Documento1, sia che si stia leggendo Documento2. E’ questione di scelte.

Annunci