Nessun risultato. Prova con un altro termine.
Guide
Notizie
Software
Tutorial

Un motore di ricerca in PHP e MySQL

Scrivere un motore di ricerca completo in PHP. Prima parte: il database, le ricerche fulltext e il motore vero e proprio
Scrivere un motore di ricerca completo in PHP. Prima parte: il database, le ricerche fulltext e il motore vero e proprio
Link copiato negli appunti

In quest'articolo introdurremo le basi per la costruzione di un motore di ricerca completo e funzionale in PHP e MySQL. Sfruttando le potenzialità di questi due prodotti open source saremo in grado di produrre uno strumento utilissimo per il nostro sito internet, capace di migliorare nettamente la semplicità con la quale un utente potrà navigare tra le nostre risorse.

Il motore di ricerca che svilupperemo si appoggerà su interessanti funzionalità fornite con la versione 4.1.1 di MYSQL. Per questo motivo consiglio di prelevare ed installare la versione più aggiornata dell'applicativo dal sito ufficiale.

La prima parte dell'articolo, quella che state leggendo, prenderà in esame la struttura del database di esempio e approfondirà il concetto di ricerca e di ricerca fulltext. La seconda parte dell'articolo avrà come fine quella di costruire il vero e proprio motore.

Il database

Il motore di ricerca che ci accingiamo a sviluppare lavorerà su un database contenete tabelle che simuleranno una semplicissima rivista online. Avremmo una tabella per gestire gli articoli, una per gestire le riviste e l'ultima che ci permetterà di gestire i commenti fatti dagli utenti ad un determinato articolo. Procediamo con la creazione della struttura base delle tabelle:

CREATE TABLE 'articoli' (
  'id' INT UNSIGNED NOT NULL AUTO_INCREMENT ,
  'id_rivista' INT UNSIGNED NOT NULL ,
  'titolo' VARCHAR( 200 ) NOT NULL ,
  'testo' TEXT NOT NULL ,
  'autore' VARCHAR( 100 ) NOT NULL ,
  PRIMARY KEY ( 'id' )
);

CREATE TABLE 'riviste' (
  'id' INT UNSIGNED NOT NULL AUTO_INCREMENT ,
  'titolo' VARCHAR( 200 ) NOT NULL ,
  'datapubblicazione' VARCHAR( 12 ) NOT NULL ,
  PRIMARY KEY ( 'id' )
);

CREATE TABLE 'commenti' (
  'id' INT UNSIGNED NOT NULL AUTO_INCREMENT ,
  'id_articolo' INT UNSIGNED NOT NULL ,
  'commento' TEXT NOT NULL ,
  PRIMARY KEY ( 'id' )
);

Utilizzando il codice SQL elencato sopra, creeremo la struttura necessaria per contenere correttamente i nostri dati: nel nostro database saranno presenti una o più riviste, ognuna delle quali conterà uno o più articoli che potranno o meno essere commentati da uno o più utenti. Passiamo ora ad introdurre uno dei concetti fondamentali che ci accompagneranno per tutto l'articolo: le ricerce FullText.

Le ricerce FullText

Come forse molti di voi sapranno, MYSQL mette a disposizione una soluzione semplice ed efficace che permette di ricercare valori all'interno di campi presenti in una tabella. Questo tipo di ricerca è molto più efficente rispetto a quella eseguita utilizzando la keyword LIKE, e facilita molto la stesura di motori di ricerca anche molto complessi grazie a funzionalità molto potenti, come la ricerca booleana che introdurremo più avanti.

Per utilizzare la ricerca FullText in una tabella dobbiamo apportarle alcune modifiche:

  • La tabella deve essere assolutamente di tipo MyISAM. Difatti queste sono le uniche tabelle che supportano le ricerche fulltext;
  • Non è possibile effettuare ricerche Fulltext su campi di tipo BLOB, i quali dovranno essere convertiti in campi TEXT;
  • La tabella ricercata dovrà contenere 3 o più record perchè le ricerche abbiano l'effetto desiderato. Infatti MySQL utilizza particolari algoritmi per calcolare la frequenza con cui una data parola ricercata appare all'interno di un testo e con meno di tre record si richia di avere frequenze molto alte che obbligheranno MySQL a scartare i record in questione.
  • Le tabelle dvoranno essere modificate per permettere ricerche Fulltext, utilizzando, nel nostro caso, i seguenti comandi SQL:


    ALTER TABLE articoli ADD FULLTEXT(titolo, testo);

Dopo aver apportato le dovute modifiche alle nostre tabelle, siamo pronti ad eseguire la nostra prima ricerca fulltext:

SELECT *
FROM articoli
WHERE MATCH(titolo, testo) AGAINST ('MYSQL');

Come potete notare, le query utilizzate per le ricerche fulltext sono differenti rispetto a quelle utilizzate abitualmente, ma la struttura è molto semplice e di facile apprendimento. In questo caso abbiamo selezionato tutti i record della tabella articoli che contengono la parola MYSQL nel campo testo o nel campo titolo, scartando (operazione eseguita in automatico da MYSQL), quelli con basso score o quelli che non contengono la parola ricercata. Lo score restituito da MYSQL può essere prelevato utilizzando il seguente comando SQL:

SELECT *, MATCH(titolo, testo) AGAINST('MYSQL') as score
FROM articoli
WHERE MATCH(titolo, testo) AGAINST('MYSQL')
ORDER BY& score DESC;

In questo caso la colonna score conterrà un numero maggiore di zero e solitamente inferiore a uno rappresentante il punteggio di ogni ricerca; va precisato che MYSQL ordina automaticamente i risultati in base al loro punteggio. La doppia chiamata a MATCH-AGAINST non rallenta l'esecuzione della query, dato che MYSQL sfrutta un particolare sistema si Cacheing che gli permette di sfruttare nuovamente i risultati appena prelevati.

Va specificato che la sintassi per le ricerche FullText obbliga a specificare, come attributi di MATCH, tutti gli indici FULLTEXT specificati durante la creazione o l'alterazione della tabella; questo non vale in caso si utilizzino accorgimenti avanzati come descritti nel seguente paragrafo.

Ricerche FullText avanzate

Dopo un po' di esercizio noterete che non è difficile imparare ad utilizzare correttamente i costrutti necessari per effettuare ricerche FullText, dato che possono essere sfruttati in congiunzione con i costrutti di JOIN per sviluppare ricerche molto potenti e utili.

Anche se potenti e veloci, questo tipo di ricerche non permettono la semplice stesura di un motore di ricerca completo. Molte volte ci si trova di fronte allo sviluppo di un'applicativo capace di eseguire ricerche in base ad un input particolare fornito dall'utente che gli permetta di eseguire la ricerca nel più ristretto campo possibile (pensate alle query string di Google, che sono delle vere e proprie espressioni interpretate dal motore di ricerca per restituirci risultati molto precisi) ed è a questo punto che entrano in gioco le ricerche booleane. Aggiungendo alla query un semplice comando, è possibile obbligare l'engine di MySQL ad effettuare ricerche nei campi FullText interpretando alcuni valori speciali presenti all'interno della stringa di ricerca:

SELECT *, MATCH(titolo, testo) AGAINST('+MYSQL -asp' IN BOOLEAN MODE) as score
FROM articoli
WHERE MATCH(titolo, testo) AGAINST('+MYSQL -asp' IN BOOLEAN MODE)
ORDER BY score DESC;

In questo paricolare caso selezionaremo tutti gli articoli che hanno a che fare con MySQL ma non con il linguaggio ASP. La lista degli operatori speciali interpretati dall'engine di MySQL è riassunta di seguito:

  • Operatore +: indica che la parola a cui è anteposto deve essere presente in ogni record restituito;
  • Operatore -: indica che la parola a cui è anteposto NON deve essere presente in alcun record restituito;
  • Operatori < e >: sono utilizzati per variare il contributo che la parola dona alla rilevanza (score) di un singolo record;
  • Operatori (): le parentesi sono utili al fine di raggruppare tra loro sotto-espressioni che avranno un più alto grado di precedenza;
  • Operatore ~: viene utilizzato per segnare una parola in modo che questa diminuisca lo score di un record (viene utilizzato per marcare le cosidette bad words);
  • Operatore *: è l'unico operatore che deve posto alla fine della parola o di una parte di essa. Server ad indicare che caratteri qualsiasi possono seguire la parola;
  • Operatore ": le parole o frasi racchiuse tra apici doppi obbligheranno MySQL ad effettuare ricerche sulla frase completa e non su ogni singola parola.

Utilizzando questi operatori, è semplice dare la possibilità di effettuare ricerche complete e precise senza troppi problemi. Nella seconda parte di questo articolo ci occuperemo del motore di ricerca vero e proprio.

Il motore di ricerca: le basi

Quello che ci accingiamo a scrivere è un semplice motore che permetta la ricerca di articoli e commenti inerenti ad un dato pattern all'interno del nostro database. L'applicativo sarà ben lungi dall'essere completo, ma la struttura con il quale lo svilupperemo dovrebbe permettervi di estenderlo con facilità per adattarlo completamente alle vostre particolari esigenze.

Il motore di ricerca dovrà soddisfare le seguenti richieste:

  • sarà composto da un unica pagina in cui verranno effettuate le ricerche e visualizzati i risultati ottenuti in ordine di rilevanza;
  • per ogni risultato saranno visualizzate: titolo della rivista, data di pubblicazione, titolo dell'articolo, nome dell'autore e rilevanza;
  • sarà possibile scrivere direttamente la query string oppure compilare interamente il form;
    Per ora ogni risultato porterà ad una pagina inesistente a cui verrà passato come parametro l'id dell'articolo. Lascio a voi il compito di completare il tutto.

L'applicazione si compone di diverse classi atte a compiere le operazioni principali e ad immagazzinare i dati:

  • La classe Search che effettuerà le ricerche;
  • La classe Result che conterrà i parametri necessari a differenziare i risultati trovati;
  • La classe QueryString che rappresenterà la query string sulla quale effettueremo la ricerca;
  • La classe Database che effettuerà l'accesso a "basso livello" al database;

La classe Database è semplicissima: permette semplicemente di connettersi ad un database, prelevare le informazioni necessarie, e disconnetersi.

<?php
class Database {
  
  var $conn = NULL;
  
  function Database($host, $user, $pass, $dbname){
    $this->conn = mysql_connect($host, $user, $pass)
      or die("ERRORE MYSQL: ".mysql_error());
    mysql_select_db($dbname, $this->conn)
      or die("ERRORE MYSQL: impossibile connettersi al database");
  }
  
  function ExecuteQuery($query){
    return mysql_query($query, $this->conn)
      or die("ERRORE MYSQL: ".mysql_error());
  }
  
  function FetchResult($result){
    $data = array();
    while($tmp = mysql_fetch_assoc($result))
      $data[] = $tmp;
    return $data;
  }
  
  function Close(){
    mysql_close($this->conn);
  }
}
?>

La classe Result, anch'essa molto semplice, ha il compito di memorizzare le informazioni relative ad un singolo risultato; permetterà, inoltre, di eseguire ricerche particolari relative ad un singolo risultato, per recuperare informazioni aggiuntive che potrebbero tornare utili al momento della visualizzazione dei risultati.

<?php
class Result{
  
  var $article_id = 0;
  var $magazine = "";
  var $date = 0;
  var $title = "";
  var $author = "";
  var $relevance = 0.0;
  var $articlepagepath = "articolo.php";
  
  function Result($aid, $m, $date, $title, $author, $rel){
    $this->article_id = $aid;
    $this->magazine = $m;
    $this->date = date("d-m-y", $date);
    $this->title = $title;
    $this->author = ucwords(strtolower($author));
    $this->rel = $rel;
  }
  
  function Display(){
    return ''.$this->title.'
    di '.$this->author.'
Rivista '.$this->magazine.'del '.$this->date.'

    Rilevanza: '.$this->relevance;
  }
  
  function GetCommentsNumber($db){
    $db->ExecuteQuery("SELECT COUNT(*) as tot FROM commenti WHERE id_articolo='".$this->article_id."'");
  }
}
?>

Per ora mi sono limitato a inserire un unico metodo aggiuntivo che permette di visualizzare il numero di commenti relativi ad un dato articolo. Si potranno aggiungere in futuro altre funzionalità più interessanti in base al tipo di dati presenti nel database. Organizzare ogni risultato in un oggetto rende il suo utilizzo molto semplice ed ordinato, ed oltretutto rende possibile estendere le funzionalità del singolo oggetto in modo che si adattino ad ogni sua istanza presente durante l'esecuzione dello script.

Il cuore del motore di ricerca

Il cuore del motore di ricerca è composto dalle classi Search e QueryString, che si occupano di eseguire praticamente la ricerca all'interno del database. Segue il listato della classe QueryString, che server per immagazzinare i dati relativi ad una query eseguita dall'utente. L'oggetto QueryString può accettare una query già preparata, oppure può generarne una partendo dai dati passati ai suoi metodi. In questo modo potremo permettere all'utente più esperto di specificare la query string manualmente, ed all'utente inesperto di seguire un meccanismo più semplice basato sui form, occupandoci noi della stesura della query. Anche se i metodi potranno sembrare un po' ripetitivi, strutturare la classe in questo modo amplia le possibilità di personalizzazione apportabili.

La classe QueryString è pensata per adattarsi sia a ricerche con un form a campo singolo, sia per ricerche avanzate con form multipli.

<?php
class QueryString {
  
  var $string = "";
  var $all = array();
  var $any = array();
  var $none = array();
  var $bad = array();
  var $passed = array();
  
  function QueryString(){
    $nargs = func_num_args();
    if($nargs > 0){
      $arg = func_get_arg(0);
      $this->string = $this->CleanString($arg);
    }
  }
  
  function CleanString($str){
    $str = addslashes($str);
  }
  
  function CleanWord($word){
    if(preg_match("[a-zA-Z0-9_", $word)){
      return $word;
    }else{
      $this->passed[] = $word;
      return NULL;
    }
  }
  
  function AddBadWord($word){
    $word = $this->CleanWord($word);
    if(!is_null($word)) $this->bad[] = $word;
  }
  
  function AddBadWords($words){
    if(!is_array($words)){
      $words = explode(" ", $words);
    }
    foreach($words as $word){
      $this->AddBadWord($word);
    }
  }
  
  function AddAnyWord($word){
    $word = $this->CleanWord($word);
    if(!is_null($word)) $this->any[] = $word;
  }
  
  function AddAnyWords($words){
    if(!is_array($words)){
      $words = explode(" ", $words);
    }
    foreach($words as $word){
      $this->AddAnyWord($word);
    }
  }
  
  function AddAllWord($word){
    $word = $this->CleanWord($word);
    if(!is_null($word)) $this->all[] = $word;
  }
  
  function AddAllWords($words){
    if(!is_array($words)){
      $words = explode(" ", $words);
    }
    foreach($words as $word){
      $this->AddAllWord($word);
    }
  }
  
  function AddNoneWord($word){
    $word = $this->CleanWord($word);
    if(!is_null($word)) $this->none[] = $word;
  }
  
  function AddNoneWords($words){
    if(!is_array($words)){
      $words = explode(" ", $words);
    }
    foreach($words as $word){
      $this->AddNoneWord($word);
    }
  }
  
  function ToString(){
    return ($this->string != "") ? $this->string : $this->BuildString();
  }
  
  function BuilString(){
    $values = array(
      "+" => &$this->all,
      "-" => &$this->none,
      " " => &$this->any,
      "~" => &$this->bad);
    $tmp = array();
    foreach($values as $operator => $words)
      if(count($words) > 0)
        $tmp[] = $operator."(".implode(" ", $words).")";
    return implode(" ", $tmp);
  }
}
?>

Come possiamo notare, anche il codice di questa classe è molto semplice, e le funzioni sono basilari e facilmente comprensibili:

  • Le funzioni di tipo Add* servono per aggiungere una o più parole che devono essere classificate come bad words, eliminate dalla ricerca incluse nella ricerca totalmente o solo parzialmente. Viene effettuato un semplice controllo per verificare che la parola passata sia accettata come parola utilizzabile nella ricerca, ed in caso affermativo questa viene aggiunta ad una lista di parole aventi le stesse prioprietà
  • Le funziondi di tipo Clean* eseguono semplicissimi controlli per la pulizia di una parola o di una querystring. Mi sono limitato a pochissime righe di codice, ma ho incluso ugualmente le funzioni per permettervi di estenderle in futuro, in modo da prevenire le SQL Injection o formati di ricerca non supportati
  • Il costruttore accetta come argomento opzionale una query string già creata
  • La funzione ToString restituisce la query string pronta per essere inserita direttamente nel blocco SQL con cui effettueremo la ricerca
  • La funzione BuildString si occupa di generare la query string partendo dai parametri passati ai metodi in precedenza.

La classe Search esegue attivamente le ricerche, basandosi sui dati passati dall'utente attraverso un normalissimo form. Segue il listato della classe:

include_once("result.class.php");

class Search{
  
  var $results = array(),
    $getcommentsnumber = false;

  function Search($gcn){
    $this->getcommentsnumber = $gcn;
  }
  
  function DoSearch($qstr, $db){
    $str = $qstr->ToString();
    $result = $db->ExecuteQuery(sprintf("
    SELECT    articoli.id as aid, riviste.titolo as magazine,
          riviste.datapubblicazione as date, articoli.titolo as title,
          articoli.autore as author,
          MATCH(articoli.titolo, articoli.testo) AGAINST('%s' IN BOOLEAN MODE) as score
    FROM    articoli, riviste
    WHERE    articoli.id_rivista = riviste.id AND MATCH(articoli.titolo, articoli.testo) AGAINST('%s' IN BOOLEAN MODE)
    ORDER BY  score DESC",
    $str, $str));
    $data = $db->FetchResult($result);
    foreach($data as $article){
      $res = new Result($data['aid'], $data['magazine'], $data['date'], $data['title'], $data['author'], $data['score']);
      if($this->getcommentsnumber){
        $res->GetCommentsNumber(&$db);
      }
      $this->results[] = $res;
    }
  }
  
  function GetResults(){
    return $this->results;
  }
}

Come potete notare, anche questa classe è molto semplice: il metodo principale (DoSearch) si occupa generare una query che effettui una ricerca fulltext booleana in base alla query string passata come argomento. Se ottiene dei risultati, genera un array di oggetti Result che possono essere prelevati con il metodo GetResults. Una possibile estensione della classe potrebbe essere quella di dare la possibilità di limitare ad un numero precisato i risultati visualizzabili, oppure di permettere ricerche anche all'interno dei commenti.

Una volta prelevati i risultati, ci basterà chiamare il metodo Display() su ognuno di loro per visualizzarli sullo schermo.

Conclusione

Concludo qui l'articolo odierno lasciando a voi il compito di implementare quello che abbiamo trattato oggi: aggiungendo un seplice form ed una pagina per visualizzare i dati, avremo sotto mano un semplice ma efficace motore di ricerca. Ricordo che a volte le ricerche FullText non restituiscono i risultati sperati a causa dell'algoritmo di calcolo della frequenza delle parole.

In caso abbiate bisogno di chiarimenti di qualsiasi tipo, o in caso incontriate errori o imprecisioni all'interno dell'articolo, vi prego di comunicarmelo.

Non ho trattato in questo articolo il funzionamento di ricerche su pagine statiche ne il funzionamento gli ormai noti spider web per motivi di spazio. Sarò comunque felice di rispondere alle vostre domande per chiarimenti o richieste di qualunque tipo, sia sul forum che via email.

Ti consigliamo anche