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

Un motore di ricerca in PHP con lo Zend Framework

Come eseguire l'indicizzazione e la ricerca dei documenti digitali con PHP e la libreria Zend_Search_Lucene dello Zend_Framework
Come eseguire l'indicizzazione e la ricerca dei documenti digitali con PHP e la libreria Zend_Search_Lucene dello Zend_Framework
Link copiato negli appunti

In questo articolo tratteremo gli strumenti per implementare un motore di ricerca fulltext per i nostri siti internet ed applicazioni web utilizzando lo Zend_Framework. In particolare vedremo il comportamento e l'utilità della libreria Zend_Search_Lucene che implementa un wrapper per la libreria Lucene del progetto Apache che permette di effettuare l'indicizzazione e la ricerca fulltext su documenti di diverso tipo.

Normalmente le soluzioni adottate per le ricerche fulltext nelle proprie applicazioni sono librerie sviluppate appositamente che si appoggiano, ove possibile, agli indici fulltext forniti da MySQL. Tempo fa trattai questo argomento, ma al giorno d'oggi la sola indicizzazione dei documenti presenti all'interno del database non è più accettabile. Come vedremo tra poco Zend_Search_Lucene estende questo concetto permettendoci di indicizzare file statici e metadati.

Prima di iniziare sarebbe opportuno introdurre il funzionamento standard di un sistema di indicizzazione e di come vengono effettuate le ricerche fulltext su questi indici generati. Uno script si occupa di indicizzare tutti i documenti di interesse estraendo da questi le parole chiave e salvandole all'interno di strutture dati apposite con riferimenti ai documenti da cui sono state estratte.

Le parole chiave sono solitamente estratte dai documenti in base a determinati criteri al fine di non rendere l'indice troppo voluminoso: solitamente si sceglie una lunghezza minima delle parole indicizzabili, si sceglie una lista di parole considerate comuni che verranno scartate automaticamente e, in casi particolari, si scartano parole chiave non pertinenti con il contesto.

L'estrazione delle parole chiave viene ovviamente effettuata dopo che il documento è stato ripulito di tutti i dati in eccesso, come la punteggiatura ed informazioni di markup. Successivamente una libreria di ricerca si occupa di effettuare le ricerche su questo indice in base a dei criteri specifici; il risultato della query sarà una lista di riferimenti ai documenti che espongono le parole chiave specificate. Alcune volte le librerie di ricerca permettono di utilizzare dei comandi molto semplici al fine di limitare la ricerca a determinati criteri oppure indicando quali parole chiave includere e quali escludere dalla ricerca. I risultati ottenuti vengono poi ordinati per pertinenza dei dati con la ricerca e restituiti all'utente.

Zend_Search_Lucene svolge automaticamente la maggior parte di queste operazioni; vediamo come.

La creazione dell'indice

Come evidenziato precedentemente il primo processo da svolgere per rendere il nostro sito ricercabile è quello di generare un indice fulltext partendo dai documenti che siamo intenzionati ad esporre. Lucene salva l'indice generato all'interno di un file contenuto nel filesystem; questo file può essere aggiornato aggiungendo i risultati di indicizzazione di nuovi documenti.

<?php

/* Ricordiamoci di aggiungere la root dello ZendFramework
* all'interno dell'include_path specificato nel file PHP.ini
*/

require_once "Zend/Search/Lucene.php";
$index_path = "/var/www/data/mysite.lucene.index";
$index = new Zend_Search_Lucene($index_path, true);

// ... continua

?>

Come prima cosa includiamo la libreria che utilizzeremo per la creazione dell'indice; successivamente creiamo un'istanza di questa libreria specificando il path fisico dell'indice che dovremo utilizzare. Il secondo parametro del costruttore, che di default vale false, indica se l'indice che stiamo creando è completamente nuovo o se dovrà basarsi su uno già esistente. Dato che ci troviamo nel primo caso, abbiamo specificato true.

Successivamente possiamo procedere con l'aggiunta di un documento all'indice; come esempio prenderemo un articolo di HTML.it:

<?php

// ...

$doc = new Zend_Search_Lucene_Document();

$doc->addField(
    Zend_Search_Lucene_Field::UnIndexed('url',
    'http://php.html.it/articoli/leggi/898/un-motore-di-ricerca-in-php-e-mysql/'));

$doc->addField(
    Zend_Search_Lucene_Field::Keyword('date',
    '12 Febbraio 2004'));

$doc->addField(
    Zend_Search_Lucene_Field::Text('title',
    'Un motore di ricerca in PHP e MySQL'));

$doc->addField(
    Zend_Search_Lucene_Field::Text('author',
    'Gabriele Farina'));

$doc->addField(
    Zend_Search_Lucene_Field::UnStored('contents',
    'Contenuto dell'articolo ...'));

$index->addDocument($doc);
$index->commit();

?>

La prima operazione da svolgere è quella di creare un'istanza della classe Zend_Search_Lucene_Document che rappresenta un documento indicizzabile da Lucene. Abbiamo deciso di aggiungere all'indice un articolo ed abbiamo scelto una serie di informazioni che a nostro parere la ricerca dovrebbe evidenziare; nel caso specifico si tratta dell'URL dell'articolo, la data di pubblicazione, l'autore, il titolo ed il contenuto (che potrebbe anche essere un riassunto di quello totale). Per poter essere esposti a Lucene questi campi devono essere aggiunti al documento dopo aver scelto un tipo da associarvi. I tipi di campo sono cinque e coprono una buona varietà di tipologie di dato:

  • UnIndexed: rappresenta un dato che sarà accessibile dal recordset dei risultati ma non verrà indicizzato dal motore di indicizzazione;
  • UnStored: il dato verrà indicizzato completamente ma non verrà salvato. Questo tipo di dato viene solitamente associato a campi che contengono molte informazioni;
  • Keyword: il dato verrà salvato ed indicizzato ma verrà inteso per intero come una parola chiave, senza essere separato dal motore di indicizzazione;
  • Text: il dato verrà indicizzato e salvato interamente all'interno dell'indice. Questo tipo di dato non dovrebbe essere utilizzato per grosse quantità di dati dato che porterebbe ad un grosso dispendio di risorse;
  • Binary: il dato binario verrà associato al recordset ma non indicizzato. Questo tipo di dato viene solitamente utilizzato per associare al risultato della ricerca un'immagine o altri file non testuali;

Tutti questi tipi di dato possono essere generati attraverso metodi statici omonimi esposti dalla classe Zend_Search_Lucene_Field.

Una volta scelti i campi che dovranno comporre il nostro documento possiamo aggiungerlo all'indice attraverso il metodo addDocument e far partire l'indicizzazione utilizzando il metodo commit che si occuperà di salvare i dati sul disco.

Indicizzare un archivio di documenti

Il sistema di indicizzazione è molto semplice ma, come possiamo notare, risulta un po' macchinoso se si devono indicizzare grossi quantitativi di dati come ad esempio un intero archivio di documenti ed articoli. Fortunatamente la programmazione ad oggetti e la struttura della libreria Lucene ci vengono incontro per semplificarci notevolmente il compito. Presupponendo che gli articoli siano salvati all'interno di un database MySQL, vediamo come recuperarli e salvarli all'interno di un nuovo indice:

<?php

require_once 'Zend/Search/Lucene.php';
class ArticleIndexedDocument extends Zend_Search_Lucene_Document

{
    public function __construct($recordset)
        {
            $this->addField(
                Zend_Search_Lucene_Field::UnIndexed('url', $recordset->url));

            $this->addField(
                Zend_Search_Lucene_Field::Keyword('date', $recordset->date));

            $this->addField(
                Zend_Search_Lucene_Field::Text('title', $recordset->title));

            $this->addField(
                Zend_Search_Lucene_Field::Text('author', $recordset->author));

            $this->addField(
                Zend_Search_Lucene_Field::UnStored('contents', $recordset->article));
        }
}

$index_path = '/var/www/data/articles.lucene.index';

$mysqli = new mysqli('localhost', 'user', 'password', 'html_it');

if (mysqli_connect_errno())
{
    printf("Can't connect to MySQL Server. Errorcode: %sn", mysqli_connect_error());
    exit;
}
$index = new Zend_Search_Lucene($index_path, true);
if ($result = $mysqli->query('SELECT * FROM articles))
{
    while( $row = $result->fetch_object() )
        {
            $index->addDocument(new ArticleIndexedDocument($row));
        }
    $result->close();
}

$index->commit();
$mysqli->close();

?>

Come possiamo notare il procedimento è molto semplice: abbiamo esteso la classe Zend_Search_Lucene_Document al fine di recuperare automaticamente i campi di interesse da un recordset, abbiamo recuperato tutti gli articoli dal nostro database e, per ognuno di questi, abbiamo creato un ArticleIndexedDocument che abbiamo aggiunto all'indice. Grazie ad una struttura simile possiamo facilmente portare qualunque applicazione o sito internet a Lucene senza dover toccare minimamente il sistema di archiviazione dei dati ma concentrandoci solamente su quello di indicizzazione e ricerca.

Conclusioni

Ho terminato la prima parte di questo articolo dedicato alla libreria Zend_Search_Lucene. Abbiamo visto come lavora il motore di indicizzazione e come operare affinché i dati salvati nel nostro indice siano coerenti e rispondano a determinate esigenze di archiviazione. Nel prosieguo tratteremo in modo approfondito gli strumenti che ci vengono forniti per effettuare le query e le possibilità di estensione utili per adattare questo strumento alle nostre esigenze.

Nella parte precedente dell'articolo abbiamo analizzato il processo di indicizzazione di un documento, soffermandoci su come Lucene indicizza i dati ed esponendo dei sistemi per indicizzare automaticamente grossi quantitativi di dati senza dover minimamente toccare la struttura di dati in cui questi documenti sono salvati.

In questo articolo ci occuperemo invece di analizzare il processo di ricerca nell'indice. Zend_Search_Lucene esegue le query utilizzando una sintassi particolare che permette di specificare in quali campi del documento ricercare; non supporta ancora le ricerche boolean (che invece sono esposte dal motore di ricerca fulltext implementato in MySQL), ma il team di sviluppo dovrebbe implementarle entro la versione 1.0 del framework.

Le query in Zend_Search_Lucene

Quando si effettua l'indicizzazione di un documento attraverso il sistema esposto da Zend_Serach_Lucene, i singoli campi specificati durante la definizione del documento vengono salvati separatamente per ogni documento indicizzato. Questo processo permette di effettuare ricerche sui singoli campi o su campi multipli senza incidere in modo particolare sulle performance.

Poiché le performance sono legate alla dimensione dell'indice, queste purtroppo subiscono notevoli rallentamenti ogni volta che si effettua l'indicizzazione di nuovi documenti o si aggiornano i dati relativi a quelli esistenti, dato che i nuovi dati estratti vengono aggiunti a quelli precedenti e non viene fatto un merge intelligente del tutto. A parte questo il sistema di ricerca è comunque molto veloce ed affidabile.

La sintassi base per effettuare ricerche all'interno dell'indice di Lucene è simile a quella che si può utilizzare in Google: quando si vuole effettuare la ricerca su un determinato campo si specifica il nome di quel campo, seguito dai due punti seguiti a loro volta dalla parola chiave da ricercare, senza spazi. Ovviamente in una query posso anche essere specificati più campi e ad ognuno assegnato un valore differente. Vediamo qualche esempio di ricerca sul nostro indice degli articoli (in rosso è evidenziato il nome del campo sui cui eseguire la ricerca):

// Recupero tutti gli articoli che contengono la parola chiave mysql
contents:mysql

// Recupero tutti gli articoli che parlano di PHP e che sono stati scritti da gabriele
contents:php author:gabriele

// Recupero tutti gli articoli scritti da farina
author:farina

Nel caso in cui venga inserito un valore senza specificare il campo a cui si riferisce, Lucene utilizzerà automaticamente il campo di default 'contents'. Solitamente il campo di default viene associato alla parte testuale del documento in modo che le ricerche generiche vengano effettuate sul campo che espone le informazioni maggiori:

// Equivale a contents:mysql
mysql

// Equivale a contents:php author:gabriele
php author:gabriele

Oltre a queste semplici query, è possibile specificare degli operatori per includere o escludere determinate keyword. Utilizzando l'operatore addizionale (+) è quello di default ed indica al sistema di includere il termine specificato; l'operatore di sottrazione (-) invece indica al sistema di escludere dalla ricerca il termine specificato. Questo ci permette di scrivere query abbastanza complesse:

// Recupero tutti gli articoli che trattano di php ma non di mysql
php -mysql

// Recupero tutti gli articoli che trattano di php ma non di mysql e che sono stati scritti da Gabriele
php -mysql author:gabriele

Le query così specificate possono essere eseguite su un indice per recuperare i risultati in modo molto semplice e comodo. Vediamo un esempio:

<?php

require_once('Zend/Search/Lucene.php');

$index_path = '/var/www/data/articles.lucene.index';
$index = new Zend_Search_Lucene($index_path, false); // false può essere omesso

$results = $index->find('php -mysql author:gabriele');

echo "Articoli trovati: ", count($results), "<br />";
foreach($results as $result)

{
    echo '< href="', $result->url ,'">', $result->title ,'</a> (<strong>Score:', $result->score, '</strong>)<br />';
    // Metodo alternativo per recuperare un valore
    $document = $result->getDocument();
    echo '<em>', $document->getFieldValue('date'), '</em><br /><br />';
}

?>

Nell'esempio precedente abbiamo utilizzato il metodo find dell'oggetto Zend_Search_Lucene che restituisce un oggetto iterabile e contabile che rappresenta i risultati ottenuti applicando la query di ricerca all'indice. I risultati ottenuti espongono i campi scelti in fase di indicizzazione come proprietà ma è possibile accedere ai valori anche attraverso l'oggetto document che rappresenta il risultato, recuperando quest'ultimo attraverso il metodo getDocument ed interrogandolo usando la funzione getFieldValue.

In caso fosse necessario ottenere l'oggetto che rappresenta un capo specifico è possibile utilizzare il metodo getField. Oltre ai campi del documento, il risultato espone la proprietà id e la proprietà score: la prima rappresenta l'indice interno utilizzato da Lucene per riferirsi al documento, mentre la seconda indica lo scoring del risultato cioè il grado di pertinenza con la query di ricerca specificata.

La stessa query poteva anche essere eseguita manualmente senza demandare il parsing della stringa alla libreria:

<?php

require_once('Zend/Search/Lucene.php');

$index_path = '/var/www/data/articles.lucene.index';
$index = new Zend_Search_Lucene($index_path, false); // false può essere omesso

$query = new Zend_Search_Lucene_Search_Query_MultiTerm();

$query->addTerm(new Zend_Search_Lucene_Index_Term('php'), true);
$query->addTerm(new Zend_Search_Lucene_Index_Term('mysql'), false);
$query->addTerm(new Zend_Search_Lucene_Index_Term('gabriele', 'author'), null);

$results = $index->find($query);
echo "Articoli trovati: ", count($results), "<br />";
foreach($results as $result)

{
    echo '< href="', $result->url ,'">', $result->title ,'</a> (<strong>Score:', $result->score, '</strong>)<br />';
    // Metodo alternativo per recuperare un valore
    $document = $result->getDocument();
    echo '<em>', $document->getFieldValue('date'), '</em><br /><br />';
}

?>

Per poter specificare una query utilizzando le API è necessario istanziare dapprima la classe Zend_Search_Lucene_Search_Query_MultiTerm (o Zend_Search_Lucene_Search_Query_Term nel caso in cui la ricerca sia molto semplice e basata su un unico termine che verrà passato come argomento al costruttore) e poi aggiungervi i termini di ricerca attraverso il metodo addTerm.

Questo metodo accetta come primo argomento un'istanza della classe Zend_Search_Lucene_Index_Term e come secondo parametro un valore booleano che indica se l'argomento deve essere richiesto o proibito. Nel caso sia specificato null come valore il termine non sarà ne richiesto ne proibito. La classe Zend_Search_Lucene_Index_Term accetta come parametro obbligatorio la stringa da cercare (senza spazi) e come parametro facoltativo il campo su cui effettuare la ricerca. Nel caso in cui il campo sia omesso, viene utilizza quello di default (contents).

Phrase query

Specificare le query manualmente ha i suoi vantaggi, tra cui quello di permetterci di sfruttare alcune delle caratteristiche dell'engine di Lucene che non sono ancora accessibili direttamente utilizzando il linguaggio di query. Il motore della versione originale di Lucene sviluppata dal progetto Apache permette di scrivere query molto più complesse, che permettono di specificare intere frasi, indici di prossimità ed operatori booleani.

L'implementazione attuale della libreria e del sistema di parsing non permette tutto questo, ma è possibile utilizzare direttamente le API della libreria per effettuare query su frasi contenenti anche delle wildcard. Il sistema è molto potente e permette di aumentare le potenzialità del motore di ricerca specificando argomenti con peso maggiore rispetto ad altri, sinonimi per alcune parole ed altro ancora. Vediamo un esempio completo:

<?php

require_once('Zend/Search/Lucene.php');

$index_path = '/var/www/data/articles.lucene.index';
$index = new Zend_Search_Lucene($index_path, false); // false può essere omesso

$query1 = new Zend_Search_Lucene_Search_Query_Phrase();
$query1->addTerm(new Zend_Search_Lucene_Index_Term('php'));
$query1->addTerm(new Zend_Search_Lucene_Index_Term('mysql'), 2);

$query2 = new Zend_Search_Lucene_Search_Query_Phrase(array('gabriele', 'gabry', 'farina'), array(0, 0, 1), 'author');

$results_1 = $index->find($query1);
$results_2 = $index->find($query2);

$query2->setSlop(2);
$results_3 = $index->find($query2);

?>

Una query phrase può essere creata direttamente durante la costruzione della classe Zend_Search_Lucene_Search_Query_Phrase oppure eseguendo una serie di step. Nel primo caso il costruttore accetta un'array di stringhe, un'array di numero ed una stringa. Il primo parametro rappresenta le parole che andranno incluse nella frase di ricerca. Il secondo parametro indica la posizione che le singole parole occupano all'interno della frase di ricerca mentre il terzo specifica il campo su cui effettuare la ricerca.

La posizione può contenere valori uguali (nel qual caso i termini verranno interpretati come sinonimi) oppure dei gap (nel qual caso tra la parola precedente e quella successiva verrà accettata una parola qualsiasi). Nel caso in cui invece si decida di costruire la query attraverso degli step, è necessario aggiungere i singoli termini utilizzando il metodo addTerm che accetta come primo parametro un'istanza della classe Zend_Serach_Lucene_Index_Term che rappresenta il termine da ricercare e la posizione di questo. Nel caso in cui sia necessario variare il numero di parole permesse tra i termini in una query phrase possiamo utilizzare il metodo setStlop dell'oggetto Zend_Search_Lucene_Query_Phrase.

Conclusioni

Siamo arrivati alla fine dell'articolo. Non ho potuto trattare il tema molto interessante dell'estendibilità dell'engine di ricerca, ma abbiamo visto che Lucene permetta di effettuare query molto complesse senza dover complicare troppo il lavoro di noi sviluppatori.

Ti consigliamo anche