Programmazione 5 commenti

Sphinx, “the Russian black magic” / 1

di Ludovico Magnocavallo

Sphinx è un motore di ricerca full text sviluppato da Andrew Aksyonoff con alcune caratteristiche che lo rendono particolarmente interessante per una vasta gamma di applicazioni web-based:

  • indipendenza dal tipo di base dati utilizzata
  • estrema velocità di indicizzazione e ricerca
  • possibilità di distribuire gli indici su sistemi diversi per scalabilità e ridondanza
  • funzionalità avanzate come il grouping per attributo
  • API per i principali linguaggi
  • protocollo di interrogazione molto semplice che permette di sviluppare client per altri linguaggi in poco tempo

Per queste caratteristiche, in particolare la velocità di indicizzazione e ricerca su basi dati di parecchie decine di Gb (BoardReader ad esempio indicizza più di un miliardo di post per circa 1.5Tb di dati), Sphinx è stato definito Russian black magic. La definizione è meno azzardata di quanto sembri: come tutte le arti magiche che si rispettano Sphinx è abbastanza esoterico, e imparare ad utilizzarlo con successo richiede tempo e costanza, per recuperare frammenti di informazione indispensabili dal forum degli utilizzatori e sperimentarne le funzionalità.

Questa serie di articoli su Sphinx vuole quindi essere una introduzione a questo eccezionale motore di ricerca, illustrandone l'installazione e l'utilizzo attraverso una serie di esempi pratici, e fornendo i dati e il codice per eseguirli. La serie è composta da tre articoli:

  • installazione e utilizzo di base (questo articolo)
  • funzionalità avanzate di indicizzazione e ricerca
  • benchmark di utilizzo, in cui Sphinx verrà comparato con Solr e i motori di full text search di MySQL e PostgreSQL

Prerequisiti

Per gli esempi di questa serie di articoli utilizzeremo

  • MySQL, che è la base dati più diffusa sui servizi di shared hosting e probabilmente la più utilizzata per lo sviluppo di applicazioni web, specialmente in PHP
  • Python 2.5 (o 2.4) o, in alternativa, PHP 4.x o 5.x
  • la versione di sviluppo di Sphinx (0.9.8-x), che offre alcune funzionalità non presenti nella versione stabile (0.9.7) e dovrebbe venire comunque rilasciata in versione definitiva fra non molto
  • se volete provare le funzionalità di stemming per la lingua italiana (non è indispensabile, soprattutto per questo primo articolo), una versione recente di libstemmer del progetto Snowball

Se siete su Linux/Unix vi servirà anche un compilatore C++, e gli header e le librerie necessarie per compilare Sphinx. Gli esempi che troverete sotto presuppongono che sphinx sia stato compilato con il supporto a MySQL (--with-mysql), e sia installato in /opt/sphinx/. Se usate PostgreSQL e/o avete installato Sphinx in una cartella diversa, aggiustate le istruzioni SQL e i percorsi degli esempi di conseguenza.

Se invece siete su Windows potete utilizzare gli eseguibili rilasciati da Andrew e quelli ufficiali di MySQL, e dovrete ovviamente sostituire i percorsi Unix con quelli della vostra installazione.

I dati utilizzati per gli esempi di questo articolo sono un dump semplificato degli articoli pubblicati su Qix.it, dato che sono una buona base per illustrare le funzionalità di Sphinx, non hanno problemi di copyright (non per me, almeno), e sono già disponibili in formato SQL. Potete scaricarli qui e, dopo aver creato un nuovo database sphinx su MySQL, caricarli con i comandi qui sotto.

    $ mysql --user=root -p
    mysql> CREATE DATABASE sphinx CHARACTER SET utf8 COLLATE utf8_general_ci;
    mysql> GRANT ALL on sphinx.* TO sphinx@localhost IDENTIFIED BY 'sphinx';
    mysql> \q
    $ cat dataset.sql |mysql --user=sphinx -p sphinx
  

Se siete su Windows, sostituite il comando type a cat.

Architettura e configurazione

L'architettura di Sphinx è piuttosto semplice:

  • un eseguibile per l'indicizzazione, indexer, che viene di solito lanciato periodicamente via cron
  • un demone TCP, searchd, che accetta le richieste, estrae i risultati e li restituisce ai client
  • un eseguibile, search, con cui effettuare ricerche da linea di comando
  • delle librerie client per vari linguaggi che permettono di effettuare ricerche utilizzando searchd

Gli eseguibili e il demone leggono le proprie impostazioni da sphinx.conf, un file di configurazione in formato pseudo-shell che definisce le caratteristiche delle fonti dati, degli indici disponibili, e i parametri di esecuzione di indexer e searchd.

Iniziamo quindi la definizione del file di configurazione che utilizzeremo per gli esempi di questo articolo, specificando la prima fonte di dati.

Definizione di una fonte di dati

Le fonti di dati (source) descrivono dove e come recuperare i dati da indicizzare, e come indicizzarli (per la ricerca, come attributi per il raggruppamento, ecc.). Le tipologie di source accettate da Sphinx sono tre: MySQL, PostgreSQL, e una fonte generica definita XML pipe. Le fonti SQL sono le più sofisticate, dato che offrono diversi parametri per alleggerire il carico sul DB o definire fonti incrementali, che esamineremo in dettaglio nel secondo articolo di questa serie. La fonte XML è più semplice, e serve come “fonte universale” in quelle situazioni dove non sia disponibile una base dati compatibile con Sphinx. Per questo articolo utilizzeremo, come detto in precedenza, una base dati MySQL e la relativa tipologia di source in Sphinx.

Per definire una fonte dobbiamo avere chiaro il modello della base dati, e ovviamente sapere cosa ci interessa indicizzare. La base dati di esempio mette a disposizione tre tabelle (ne arriveranno altre per la seconda parte dell'articolo):

    +-------------------+
    | Tables_in_sphinx  |
    +-------------------+
    | sphinx_entry      |
    | sphinx_tag        |
    | sphinx_entry_tags |
    +-------------------+
  

Le tabelle contengono — ovviamente — i post di Qix, le singole tag utilizzate, e le relazioni tra post e tag. Esaminiamo brevemente la struttura delle singole tabelle, nell'ordine in cui sono riportate sopra:

    +-----------------+--------------+
    | Field           | Type         |
    +-----------------+--------------+
    | id              | int(11)      |
    | slug            | varchar(255) |
    | author_id       | int(11)      |
    | updated         | datetime     |
    | title           | varchar(255) |
    | comment_count   | int(11)      |
    | trackback_count | int(11)      |
    | body            | longtext     |
    +-----------------+--------------+
    +-------+-------------+
    | Field | Type        |
    +-------+-------------+
    | id    | int(11)     |
    | slug  | varchar(50) |
    | name  | varchar(50) |
    +-------+-------------+
    +----------+---------+
    | Field    | Type    |
    +----------+---------+
    | id       | int(11) |
    | entry_id | int(11) |
    | tag_id   | int(11) |
    +----------+---------+
  

Come accennato, i dati indicizzati da sphinx possono essere divisi in due macrocategorie: quelli utilizzati per la ricerca full text vera e propria; e i dati che servono per limitare l'ambito della ricerca e ordinare o raggruppare i risultati, che Sphinx definisce “attributi”. Mentre per i primi non ci sono grandi limitazioni — devono ovviamente essere campi testo — per gli attributi Sphinx limita la scelta a dati di tipo numerico (interi o float nell'ultima versione) e date. La limitazione è dovuta all'architettura degli indici e ad esigenze di prestazioni, e gli eventuali problemi di compatibilità con strutture dati esistenti (ad esempio tabelle senza una ID numerica) possono essere facilmente aggirati, come vedremo negli articoli successivi.

Tornando al nostro esempio, descriviamo una prima semplice sorgente di dati che ci permette di:

  • effettuare ricerche full text sul contenuto e il titolo dei post
  • limitare, ordinare, o raggruppare i risultati per data di pubblicazione, autore, numero di commenti, tag

Creiamo quindi un nuovo file sphinx.conf nella cartella etc sotto il percorso dove abbiamo installato Sphinx (ad es. /opt/sphinx/etc/sphinx.conf), e utilizziamo come riferimento i commenti alla configurazione di esempio rilasciata insieme ai sorgenti (/opt/sphinx/etc/sphinx.conf.dist). Iniziamo con la definizione della connessione a MySQL, utilizzando se possibile il socket invece della connessione TCP per il collegamento, e specificando il charset (nel nostro caso UTF-8):

    source qix_1 {
    type = mysql
    sql_host = localhost
    sql_port = 3306
    sql_sock = /var/run/mysqld/mysqld.sock # opzionale
    sql_db = sphinx
    sql_user = sphinx
    sql_pass = sphinx
    sql_query_pre = SET NAMES utf8
  

Come potete notare, il comando sql_query_pre con cui impostiamo il charset accetta una o — ripetendolo — più query generiche. Potremmo quindi usarlo anche per altri scopi: ad esempio per impostare un lock che impedisca a due processi di indicizzazione di girare contemporaneamente, per innescare una transazione, o per manipolare un contatore che ci permetta di implementare l'indicizzazione incrementale. Vedremo alcune di queste tecniche nel secondo articolo di questa serie.

Definito il collegamento a MySQL, indichiamo a Sphinx come recuperare i dati da indicizzare. La query deve rispettare alcune semplici condizioni: il primo campo deve essere l'id numerico che identifica univocamente ogni record; gli attributi devono essere interi a 32 bit non negativi (o float nell'ultima versione); i campi non dichiarati come id o attributi (vedremo sotto come) vengono indicizzati come full text. La nostra query quindi sarà:

    sql_query = \
    SELECT \
    id, author_id, unix_timestamp(updated) as updated, \
    comment_count + trackback_count as num_comments, \
    title, body \
    FROM sphinx_entry
  

Definita la query principale, indichiamo a sphinx quali sono gli attributi che ci serviranno per filtrare o raggruppare i risultati delle ricerche. Gli attributi possono essere definiti come

  • sql_attr_uint, per cui Sphinx usa interi a 32 bit
  • sql_attr_bool, attributi boolean che richiedono meno spazio
  • sql_attr_timestamp, date in formato Unix
  • sql_attr_str2ordinal, attributi in formato stringa che vengono ordinati da Sphinx in memoria durante l'indicizzazione e convertiti in interi che rispecchiano l'ordinamento, servono per il sorting dei risultati
  • sql_attr_float, attributi in formato float

Definiamo quindi gli attributi estratti dalla query SQL:

    sql_attr_uint = author_id
    sql_attr_timestamp = updated
    sql_attr_uint = num_comments
  

L'ultima operazione che ci rimane per completare la definizione della sorgente di dati è recuperare le tag, e definirle come attributo multivalore (MVA, Multi-Valued Attribute), una funzionalità introdotta con la versione 0.9.8 di Sphinx. Gli attributi MVA, come quelli semplici visti sopra, possono essere di tipo uint o timestamp e possono venire estratti in due modi:

  • field, uno dei campi della query principale viene utilizzato per recuperare i valori di un singolo record; questo metodo non è ancora supportato (cf. sphinx-0.9.8-svn-rxxxx/src/indexer.cpp intorno alla riga 340), ma dovrebbe tornare utile per quei tipi di attributi che è più conveniente recuperare con istruzioni tipo GROUP_CONCAT, o che sono salvati in campi singoli
  • query, una query SQL che restituisce coppie di valori costituite dalla id del record principale (nel nostro caso i singoli post) e dalla id dell'attributo (nel nostro caso le tag distinte dei post)

La definizione del nostro attributo MVA, le tag, è piuttosto semplice:

    sql_attr_multi = uint tag from query; \
    SELECT entry_id, tag_id FROM sphinx_entry_tags
  

Ci sono altre istruzioni che possono essere definite per una sorgente di dati, ne vedremo alcune nella seconda parte di questa serie quando ci occuperemo di indicizzazione avanzata. Per adesso manca una sola istruzione, che serve all'eseguibile da linea di comando search per recuperare i dati delle singole entry contenute nei risultati della ricerca:

    sql_query_info = SELECT * FROM sphinx_entry WHERE id=$id
    }
  

Definita la fonte di dati, passiamo a vedere come configurare l'indice che la utilizza.

Definizione di un indice

La definizione dell'indice serve a Sphinx per sapere quali parametri fisici utilizzare (file e percorsi, utilizzo della memoria per gli attributi, ecc.) e come manipolare i dati forniti dalla sorgente. Anche per l'indice, in questo articolo esamineremo solo le istruzioni fondamentali, riservando al secondo articolo della serie le istruzioni destinate ad utilizzi avanzati.

Cominciamo con il definire il nome dell'indice, la fonte di dati utilizzata (una fonte di dati può ovviamente essere utilizzata da più indici), il percorso per i file fisici generati, e il metodo di storage degli attributi (nel nostro caso in un file separato, data la dimensione limitata del dataset utilizzato)

    index qix_1 {
    source = qix_1
    path = /opt/sphinx/var/data/qix1
    docinfo = extern
  

Le altre istruzioni che ci interessano a questo livello riguardano il trattamento dei dati ricevuti:

  • morphology definisce lo stemmer da utilizzare, o in alternativa se farne a meno; gli stemmer inclusi con Sphinx sono stem_en per la lingua inglese, stem_ru per il russo, stem_enru che combina inglese e russo, soundex e metaphone che utilizzano algoritmi fonetici; se Sphinx è stato compilato con il supporto per libstemmer, sono disponibili inoltre tutti gli stemmer supportati da Snowball
  • stopwords definisce il percorso della lista opzionale di parole da escludere dall'indicizzazione (ad esempio congiunzioni, articoli, ecc.), che possono essere ottenute facendo generare a indexer una lista della frequenza dei termini di una o più fonti di dati (indexer --buildstops o --buildfreqs)
  • synonims definisce il percorso della lista opzionale di sinonimi
  • min_word_len definisce la lunghezza minima delle parole da indicizzare, con il valore 1 vengono indicizzate tutte le parole (ed evitati problemi con ricerche per frasi, tipo “e lui disse”)
  • charset_type definisce se l'encoding dei dati deve essere single byte (ad esempio ISO-8859-1) o UTF-8
  • charset_table definisce una tabella di conversione dei caratteri, tipicamente per mappare caratteri accentati sui loro equivalenti traslitterati (ad es. per mappare 'à' su 'a')
  • enable_star, infine, attiva il funzionamento della wildcard '*' nelle ricerche

Altre istruzioni utili per un utilizzo non avanzato, che però non utilizzeremo per gli esempi di questo articolo, sono:

  • html_strip rimuove il codice HTML dai campi testo indicizzati
  • html_index_attrs definisce attributi HTML da indicizzare, ad esempio img=alt,title; a=title;

Completiamo quindi la definizione del nostro indice ed esaminiamo brevemente l'ereditarietà di indici e fonti di dati, per poi passare alle impostazioni del demone, all'indicizzazione, e a qualche esempio di utilizzo delle API:

    morphology = none
    stopwords =
    synonyms =
    min_word_len = 1
    charset_type = utf-8
    charset_table = 0..9, A..Z->a..z, _, a..z, \
    U+C0, U+C8, U+E0, U+E8, U+E9, U+EC, U+F2, U+F9, \
    U+C0->U+61, U+C8->e, U+E0->a, U+E8->e, U+E9->e, \
    U+EC->i, U+F2->o, U+F9->u, U+2019->', U+2018->',
    enable_star = 1
    }
  

La configurazione di Sphinx permette di utilizzare l'ereditarietà sia per le fonti di dati che per gli indici: in pratica, dove due indici (o due fonti) condividono un buon numero di impostazioni, è sufficiente definirne uno solo per esteso, e implementare l'altro come estensione del primo. L'ereditarietà è utilissima soprattutto per configurazioni avanzate, e la utilizzeremo a fondo nel prossimo articolo. Per ora, limitiamoci ad utilizzarla per definire un secondo indice uguale a quello che abbiamo già definito, che utilizza però un algoritmo di stemming:

    index qix_1_stemmed : qix_1 {
    morphology = stem_en
    }
  

Tutte le impostazioni che non definiamo specificamente per il nuovo indice (fonte di dati, encoding, ecc.) saranno identiche all'indice indicato come progenitore, nel nostro caso qix_1.

Definizione delle impostazioni di runtime

L'ultima parte del file di configurazione definisce le impostazioni utilizzate dai due processi principali di Sphinx: indexer per l'indicizzazione, e searchd per la ricerca. Definiamo quindi i parametri necessari per poter utilizzare gli esempi:

    indexer {
    mem_limit = 32M
    }
    searchd {
    address = 127.0.0.1
    port = 3312
    log = /opt/sphinx/var/log/searchd.log
    query_log = /opt/sphinx/var/log/query.log
    max_children = 15
    pid_file = /opt/sphinx/var/log/searchd.pid
    max_matches = 1000
    }
  

I parametri che abbiamo impostato non sono tutti quelli disponibili, e sono uguali ai valori di default: li abbiamo riportati per darvi un'idea delle principali opzioni di runtime che potete controllare.

Indicizzazione e lancio del demone

Salviamo il file di configurazione (qui la versione completa) nella posizione predefinita, nel nostro caso /opt/sphinx/etc/sphinx.conf ed eseguiamo una prima indicizzazione dei dati.

Assicuratevi prima che

  • l'utente MySQL che avete indicato in sphinx.conf esista e abbia permessi di lettura sulla base dati
  • l'utente con cui fate l'indicizzazione (meglio ovviamente se non è root e se è lo stesso con cui farete girare il demone) abbia i permessi di scrittura sui percorsi degli indici

A questo punto, possiamo lanciare il comando /opt/sphinx/bin/indexer --all che utilizzerà la configurazione di default per rigenerare tutti gli indici che abbiamo definito. La mia istanza di Sphinx gira come utente sphinx, modificate il comando sotto in maniera appropriata per le vostre impostazioni:

    # su - sphinx -c "/opt/sphinx/bin/indexer --all"
    Sphinx 0.9.8-dev (r1038)
    Copyright (c) 2001-2007, Andrew Aksyonoff
    using config file '/opt/sphinx/etc/sphinx.conf'...
    indexing index 'qix_1'...
    collected 743 docs, 1.1 MB
    collected 906 attr values
    sorted 0.0 Mvalues, 100.0% done
    sorted 0.2 Mhits, 100.0% done
    total 743 docs, 1099344 bytes
    total 0.474 sec, 2317931.66 bytes/sec, 1566.59 docs/sec
    indexing index 'qix_1_stemmed'...
    collected 743 docs, 1.1 MB
    collected 906 attr values
    sorted 0.0 Mvalues, 100.0% done
    sorted 0.2 Mhits, 100.0% done
    total 743 docs, 1099344 bytes
    total 0.602 sec, 1824949.25 bytes/sec, 1233.41 docs/sec
  

Omettendo l'opzione --all e specificando i nomi di uno o più indici, indexer rigenererà solo quelli. Le opzioni più importanti oltre a quelle già viste (buildstops, buildfreqs e all) sono:

  • --config, che imposta il file di configurazione da utilizzare
  • --rotate, utilizzata quando il demone searchd è in esecuzione per rigenerare gli indici senza toccare quelli in uso, e riavviare searchd in automatico con i nuovi indici una volta finito il processo di indicizzazione
  • --merge, per unire due indici distinti, utile quando si utilizzano i delta index, come vedremo nel prossimo articolo

Una volta che gli indici sono stati generati con successo, possiamo lanciare il demone searchd e iniziare a usare Sphinx per le ricerche. Anche per searchd valgono le osservazioni fatte sopra sull'utenza con cui lanciare il processo e i permessi di accesso a indici e log. Se utilizzate un sistema Unix che supporta init SysV, in sphinx-0.9.8-svn-rxxxx/contrib/scripts/searchd è disponibile un file per avviare searchd compatibile con chkconfig di RedHat/CentOS. Su Debian/Ubuntu potete utilizzare il mio, aggiustando il percorso del file pid e l'utente nella configurazione. Lanciamo quindi searchd da linea di comando:

    # su - sphinx -c "/opt/sphinx/bin/searchd"
    Sphinx 0.9.8-dev (r1038)
    Copyright (c) 2001-2007, Andrew Aksyonoff
    using config file '/opt/sphinx/etc/sphinx.conf'...
  

Ricerche base

Ora che i dati sono indicizzati e searchd è attivo, possiamo eseguire alcune ricerche di base, utilizzando l'eseguibile search incluso nella distribuzione. Per brevità, le ricerche che vedete qui sotto utilizzano l'opzione -q di search per non ricevere il testo completo dei risultati, potete ovviamente ometterla nei vostri esperimenti per vedere il testo ed i dati completi dei risultati.

Cominciamo con una semplice ricerca per keyword:

    $ /opt/sphinx/bin/search -i qix_1 -e "sphinx" -q
    Sphinx 0.9.8-dev (r1038)
    Copyright (c) 2001-2007, Andrew Aksyonoff
    using config file '/opt/sphinx/etc/sphinx.conf'...
    index 'qix_1': query 'sphinx ': returned 2 matches of 2 total in 0.008 sec
    displaying matches:
    1. document=682, weight=1703, author_id=1, updated=Fri Oct  5 14:21:38 2007, num_comments=4, tag=(21,172)
    2. document=743, weight=1703, author_id=1, updated=Fri Oct  5 14:21:37 2007, num_comments=7, tag=(140,190)
    words:
    1. 'sphinx': 2 documents, 2 hits
  

Modalità di ricerca

La ricerca che abbiamo appena eseguito utilizza la modalità di default, che cerca una corrispondenza esatta con tutte i termini di ricerca inseriti. Sphinx supporta cinque modalità di ricerca differenti:

  • SPH_MATCH_ALL, la modalità di default che abbiamo appena utilizzato
  • SPH_MATCH_ANY (-a usando search), che cerca una corrispondenza con almeno uno dei termini di ricerca
  • SPH_MATCH_PHRASE (-p), cerca una corrispondenza esatta con i termini, interpretandoli come una unica frase nell'ordine in cui sono stati inseriti
  • SPH_MATCH_BOOLEAN (-b), permette l'utilizzo di operatori booleani (& | + -) e parentesi per il raggruppamento, considerando un & implicito tra i termini di ricerca
  • SPH_MATCH_EXTENDED (-e), modalità estesa che utilizza il linguaggio di interrogazione interno di Sphinx

La modalità estesa è quella più interessante sia per l'utente comune che per eseguire ricerche avanzate: all'utente comune offre il riconoscimento degli operatori booleani e del raggruppamento con virgolette, permettendo quindi di utilizzare una sintassi di ricerca simile a quella base di Google e in genere dei motori di ricerca su web; all'utente avanzato, offre la possibilità di restringere l'applicazione di alcuni termini a singoli campi, e un operatore di prossimità.

Vediamo un esempio semplice di funzionamento della ricerca estesa. Eseguiamo prima una ricerca con più keyword in modalità SPH_MATCH_ALL:

    $ /opt/sphinx/bin/search -i qix_1 '"classifica dei blog"' -q
    Sphinx 0.9.8-dev (r1038)
    Copyright (c) 2001-2007, Andrew Aksyonoff
    using config file '/opt/sphinx/etc/sphinx.conf'...
    index 'qix_1': query '"classifica dei blog" ': returned 21 matches of 21 total in 0.001 sec
    displaying matches:
    1. document=682, weight=4, author_id=1, updated=Fri Oct  5 14:21:38 2007, num_comments=4, tag=(21,172)
    2. document=452, weight=3, author_id=1, updated=Fri Oct  5 14:21:39 2007, num_comments=16, tag=(21,100,208,214)
    [...]
    20. document=681, weight=1, author_id=1, updated=Fri Oct  5 14:21:38 2007, num_comments=6, tag=(16,100,215)
    words:
    1. 'classifica': 26 documents, 47 hits
    2. 'dei': 342 documents, 628 hits
    3. 'blog': 297 documents, 773 hits
  

Sphinx estrae i documenti che contengono i singoli termini e li incrocia, restituendo tutti i 21 documenti che contengono — in qualsiasi posizione e rapporto tra loro — i tre termini che abbiamo indicato. Eseguiamo la stessa ricerca in modalità estesa:

    $ /opt/sphinx/bin/search -i qix_1 -e '"classifica dei blog"' -q
    Sphinx 0.9.8-dev (r1038)
    Copyright (c) 2001-2007, Andrew Aksyonoff
    using config file '/opt/sphinx/etc/sphinx.conf'...
    index 'qix_1': query '"classifica dei blog" ': returned 6 matches of 6 total in 0.006 sec
    displaying matches:
    1. document=665, weight=3548, author_id=1, updated=Fri Oct  5 14:21:38 2007, num_comments=20, tag=(21,23,100,214)
    2. document=672, weight=3548, author_id=1, updated=Fri Oct  5 14:21:38 2007, num_comments=6, tag=(23,100,141,214)
    [...]
    6. document=659, weight=3545, author_id=1, updated=Fri Oct  5 14:21:38 2007, num_comments=2, tag=(199,234)
    words:
    1. 'classifica': 26 documents, 47 hits
    2. 'dei': 342 documents, 628 hits
    3. 'blog': 297 documents, 773 hits
  

Come potete notare, i risultati sono cambiati radicalmente, dato che Sphinx cerca adesso i documenti in cui è presente la frase intera che abbiamo specificato. Possiamo anche approssimare il riconoscimento della frase utilizzando l'operatore di prossimità, dicendo a Sphinx che i singoli termini che compongono la frase non devono essere consecutivi, ma possono anche essere separati da un massimo di 10 parole:

    $ /opt/sphinx/bin/search -i qix_1 -e '"classifica dei blog"~10' -q
    Sphinx 0.9.8-dev (r1038)
    Copyright (c) 2001-2007, Andrew Aksyonoff
    using config file '/opt/sphinx/etc/sphinx.conf'...
    index 'qix_1': query '"classifica dei blog"~10 ': returned 7 matches of 7 total in 0.001 sec
    displaying matches:
    1. document=665, weight=3548, author_id=1, updated=Fri Oct  5 14:21:38 2007, num_comments=20, tag=(21,23,100,214)
    2. document=672, weight=3548, author_id=1, updated=Fri Oct  5 14:21:38 2007, num_comments=6, tag=(23,100,141,214)
    [...]
    7. document=578, weight=577, author_id=1, updated=Fri Oct  5 14:21:38 2007, num_comments=11, tag=(21,214)
    words:
    1. 'classifica': 26 documents, 47 hits
    2. 'dei': 342 documents, 628 hits
    3. 'blog': 297 documents, 773 hits
  

Il risultato non cambia molto, dato che il set di dati su cui stiamo eseguendo le ricerche è abbastanza limitato, ma l'utilizzo dell'operatore di prossimità ha comunque allargato il campo di ricerca e restituito un documento in più.

Un altro operatore disponibile nella ricerca avanzata è quello che permette di restringere l'applicazione di uno o più termini di ricerca ad un singolo campo. Vediamo una semplice ricerca prima e dopo l'applicazione di uno dei termini al campo title:

    $ /opt/sphinx/bin/search -i qix_1 -e 'classifica blog' -q
    Sphinx 0.9.8-dev (r1038)
    Copyright (c) 2001-2007, Andrew Aksyonoff
    using config file '/opt/sphinx/etc/sphinx.conf'...
    index 'qix_1': query 'classifica blog ': returned 24 matches of 24 total in 0.000 sec
    displaying matches:
    1. document=587, weight=2618, author_id=1, updated=Fri Oct  5 14:21:38 2007, num_comments=38, tag=(21,100,214)
    2. document=452, weight=2604, author_id=1, updated=Fri Oct  5 14:21:39 2007, num_comments=16, tag=(21,100,208,214)
    [...]
    20. document=678, weight=1566, author_id=1, updated=Fri Oct  5 14:21:38 2007, num_comments=12, tag=(101,102,115,172)
    words:
    1. 'classifica': 26 documents, 47 hits
    2. 'blog': 297 documents, 773 hits
  

restringendo l'applicazione del termine "blog" al solo titolo dei post, i risultati cambiano drasticamente:

    $ /opt/sphinx/bin/search -i qix_1 -e 'classifica @title blog' -q
    Sphinx 0.9.8-dev (r1038)
    Copyright (c) 2001-2007, Andrew Aksyonoff
    using config file '/opt/sphinx/etc/sphinx.conf'...
    index 'qix_1': query 'classifica @title blog ': returned 6 matches of 6 total in 0.000 sec
    displaying matches:
    1. document=587, weight=2618, author_id=1, updated=Fri Oct  5 14:21:38 2007, num_comments=38, tag=(21,100,214)
    2. document=452, weight=2604, author_id=1, updated=Fri Oct  5 14:21:39 2007, num_comments=16, tag=(21,100,208,214)
    [...]
    6. document=570, weight=2568, author_id=1, updated=Fri Oct  5 14:21:38 2007, num_comments=1, tag=(138,180)
    words:
    1. 'classifica': 26 documents, 47 hits
    2. 'blog': 297 documents, 773 hits
  

Infine, gli operatori booleani funzionano allo stesso modo che per le ricerche su Google e altri motori di ricerca. Proviamo ad esempio una delle ricerche qui sopra, escludendo un termine con l'operatore -:

    $ /opt/sphinx/bin/search -i qix_1 -e 'classifica blog -technorati' -q
    Sphinx 0.9.8-dev (r1038)
    Copyright (c) 2001-2007, Andrew Aksyonoff
    using config file '/opt/sphinx/etc/sphinx.conf'...
    index 'qix_1': query 'classifica blog -technorati ': returned 20 matches of 20 total in 0.001 sec
    displaying matches:
    1. document=581, weight=2570, author_id=1, updated=Fri Oct  5 14:21:38 2007, num_comments=15, tag=(21,100,214)
    2. document=682, weight=2570, author_id=1, updated=Fri Oct  5 14:21:38 2007, num_comments=4, tag=(21,172)
    [...]
    20. document=813, weight=1564, author_id=1, updated=Thu Nov 15 16:45:57 2007, num_comments=3, tag=(20)
    words:
    1. 'classifica': 26 documents, 47 hits
    2. 'blog': 297 documents, 773 hits
  

Escludendo il termine "technorati" il numero di risultati restituiti è passato da 24 a 20.

Ordinamento

Come le modalità di ricerca, anche l'ordinamento dei risultati supporta diverse modalità:

  • SPH_SORT_RELEVANCE (default), ordina per rilevanza dei risultati
  • SPH_SORT_ATTR_DESC, ordina secondo il valore di un attributo (che deve essere specificato), con ordinamento decrescente
  • SPH_SORT_ATTR_ASC, ordina secondo il valore di un attributo (che deve essere specificato), con ordinamento crescente
  • SPH_SORT_TIME_SEGMENTS, ordina temporalmente per segmenti (ora/giorno/settimana/mese) su un attributo (che deve essere specificato), e all'interno dei segmenti temporali per rilevanza decrescente
  • SPH_SORT_EXTENDED, ordina utilizzando un'espressione con operatori simili a SQL

L'ordinamento per segmenti temporali è particolarmente comodo per la ricerca di notizie, o dati dove la freschezza di pubblicazione ha un'importanza pari o superiore alla rilevanza dei termini di ricerca, ed è spiegata con qualche dettaglio in più sul manuale di Sphinx.

L'ordinamento esteso utilizza due operatori: @id, l'id del risultato; @rank (o @weight @relevance), la rilevanza. Gli operatori possono essere combinati tra di loro e con gli attributi, specificando per ognuno se l'ordinamento deve essere crescente o decrescente. Un esempio di ordinamento esteso dal manuale di Sphinx è @relevance DESC, price ASC, @id DESC.

Proviamo a ripetere una delle query precedenti, ordinando questa volta i risultati per time segments:

    $ /opt/sphinx/bin/search -i qix_1 -e 'classifica blog -technorati' -q --sort=ts
    Sphinx 0.9.8-dev (r1038)
    Copyright (c) 2001-2007, Andrew Aksyonoff
    using config file '/opt/sphinx/etc/sphinx.conf'...
    index 'qix_1': query 'classifica blog -technorati ': returned 20 matches of 20 total in 0.001 sec
    displaying matches:
    1. document=813, weight=1564, author_id=1, updated=Thu Nov 15 16:45:57 2007, num_comments=3, tag=(20)
    2. document=682, weight=2570, author_id=1, updated=Wed Nov 22 14:28:58 2006, num_comments=4, tag=(21,172)
    3. document=581, weight=2570, author_id=1, updated=Wed Jul 19 18:39:40 2006, num_comments=15, tag=(21,100,214)
    4. document=643, weight=2569, author_id=1, updated=Mon Oct  9 00:33:12 2006, num_comments=22, tag=(21,100)
    5. document=570, weight=2568, author_id=1, updated=Sat Jul  1 12:10:56 2006, num_comments=1, tag=(138,180)
    6. document=691, weight=1607, author_id=1, updated=Sun Dec  3 00:01:42 2006, num_comments=25, tag=(16,21,37,100,214,215)
    [...]
    20. document=595, weight=1564, author_id=1, updated=Thu Aug 24 22:32:02 2006, num_comments=1, tag=(7,86)
    words:
    1. 'classifica': 26 documents, 47 hits
    2. 'blog': 297 documents, 773 hits
  

Come potete notare, il primo risultato restituito ha una rilevanza più bassa rispetto ai seguenti, ma è molto più recente e quindi viene portato in prima posizione. Altri ordinamenti, come quello esteso o quello per attributi, possono essere utilizzati solo dalle API e li vedremo quindi in seguito.

Filtri

Gli attributi non servono solo per ordinare i risultati, ma anche per restringerli a determinati valori di uno o più attributi utilizzando dei filtri. I filtri possono specificare un attributo e un valore che deve avere in tutti i risultati restituiti, o un attributo e un range di valori. Come per l'ordinamento, i tipi di filtri avanzati possono essere utilizzati solo dalle API, e li mostreremo nei prossimi articoli. Vediamo quindi un semplice tipo di filtro, che limita i risultati restituiti ad un singolo autore. I risultati senza filtro sono:

    $ /opt/sphinx/bin/search -i qix_1 -e 'ebay ' -q
    Sphinx 0.9.8-dev (r1038)
    Copyright (c) 2001-2007, Andrew Aksyonoff
    using config file '/opt/sphinx/etc/sphinx.conf'...
    index 'qix_1': query 'ebay  ': returned 37 matches of 37 total in 0.001 sec
    displaying matches:
    1. document=332, weight=2693, author_id=3, updated=Thu Mar 10 09:37:00 2005, num_comments=0, tag=()
    2. document=121, weight=2671, author_id=3, updated=Fri Aug 13 17:05:00 2004, num_comments=0, tag=()
    [...]
    18. document=739, weight=1639, author_id=1, updated=Mon Apr  2 23:07:02 2007, num_comments=14, tag=(126,234)
    19. document=794, weight=1639, author_id=1, updated=Sun Oct 14 14:25:13 2007, num_comments=4, tag=(13,42,83)
    20. document=1, weight=1601, author_id=2, updated=Wed Aug  4 17:40:00 2004, num_comments=0, tag=()
    words:
    1. 'ebay': 37 documents, 81 hits
  

e applicando un filtro che limita l'id dell'autore a 1:

    $ /opt/sphinx/bin/search -i qix_1 -e 'ebay ' -q -f author_id 1
    Sphinx 0.9.8-dev (r1038)
    Copyright (c) 2001-2007, Andrew Aksyonoff
    using config file '/opt/sphinx/etc/sphinx.conf'...
    index 'qix_1': query 'ebay  ': returned 15 matches of 15 total in 0.000 sec
    displaying matches:
    1. document=118, weight=1685, author_id=1, updated=Mon Jul 19 17:35:02 2004, num_comments=0, tag=()
    2. document=300, weight=1671, author_id=1, updated=Mon Jan 10 21:49:51 2005, num_comments=3, tag=()
    [...]
    15. document=804, weight=1601, author_id=1, updated=Sat Oct 27 23:17:29 2007, num_comments=10, tag=(120,124)
    words:
    1. 'ebay': 37 documents, 81 hits
  

Raggruppamenti

Oltre che per ordinare e filtrare i risultati, gli attributi possono essere utilizzati anche per creare raggruppamenti, che isolano i singoli valori di un attributo presenti nei risultati e restituiscono il documento con la migliore corrispondenza per ogni valore, insieme a due nuovi attributi: @groupby, che indica il valore dell'attributo utilizzato per il raggruppamento; @count, che indica il numero di documenti tra i risultati che hanno l'attributo di raggruppamento corrispondente al valore di @groupby. I raggruppamenti funzionano ancora solo su attributi singoli, se (come me) siete interessati ad utilizzarli su attributi MVA, magari per replicare le funzionalità di faceting di Apache Solr, aggiungetevi a questo bug sul tracker di Sphinx.

Vediamo un semplice raggruppamento per id dell'autore:

    $ /opt/sphinx/bin/search -i qix_1 -e 'ebay ' -q -g author_id
    Sphinx 0.9.8-dev (r1038)
    Copyright (c) 2001-2007, Andrew Aksyonoff
    using config file '/opt/sphinx/etc/sphinx.conf'...
    index 'qix_1': query 'ebay  ': returned 4 matches of 4 total in 0.000 sec
    displaying matches:
    1. document=71, weight=1659, author_id=6, updated=Thu Aug 26 16:23:00 2004, num_comments=0, tag=(), @groupby=6, @count=1
    2. document=332, weight=2693, author_id=3, updated=Thu Mar 10 09:37:00 2005, num_comments=0, tag=(), @groupby=3, @count=19
    3. document=1, weight=1601, author_id=2, updated=Wed Aug  4 17:40:00 2004, num_comments=0, tag=(), @groupby=2, @count=2
    4. document=118, weight=1685, author_id=1, updated=Mon Jul 19 17:35:02 2004, num_comments=0, tag=(), @groupby=1, @count=15
    words:
    1. 'ebay': 37 documents, 81 hits
  

Come potete notare abbiamo quattro risultati, uno per ogni id di autore restituito dalla ricerca. Ogni risultato ha il documento con la corrispondenza migliore ai termini di ricerca per l'autore identificato in @groupby, e il numero di documenti che la ricerca restituirebbe impostando un filtro su quell'autore contenuto in @count. L'autore con id 3, ad esempio, è quello che ha scritto più post su ebay (19), e il suo post dove ebay è utilizzato di più è il numero 332.

Se utilizzassimo le API invece del comando search, potremmo ordinare i risultati, oltre che per rilevanza come mostrato qui sopra, anche per numero di post per ogni autore (@count) o id dell'autore (@groupby).

Utilizzo delle API

Il sorgente di Sphinx contiene tre client direttamente supportati per PHP, Python, e Java; e due sviluppati dagli utenti per Perl e Ruby. Tutti i client si uniformano alla sintassi del modulo PHP, sviluppato direttamente da Andrew e l'unico disponibile con le prime versioni di Sphinx, in modo da facilitare la documentazione ufficiale e semplificare il passaggio da un linguaggio all'altro.

Il risvolto negativo di questa scelta (fatta direttamente da Andrew) è che l'utilizzo dei client non rispetta le convenzioni dei singoli linguaggi e risulta quindi poco intuitivo (e a volte decisamente macchinoso). Fortunatamente il protocollo utilizzato da searchd è molto semplice, e scrivere un client “personalizzato” (o uno per un linguaggio non supportato direttamente) utilizzando come punto di partenza uno dei client disponibili è questione di poche ore. Nei prossimi articoli di questa serie vedremo come, esaminando il client alternativo per Python che ho sviluppato per BlogBabel, che è l'unico oltre a quello ufficiale in PHP che — per ora — supporta alcune nuove funzionalità come le query multiple, che veicolano più set di richieste/risposte all'interno di un'unica connessione TCP.

Per stuzzicarvi l'appetito, ecco due semplici esempi di utilizzo del driver standard per PHP

    require_once 'sphinxapi.php';
    $c =& new SphinxClient();
    $cl->SetServer('localhost', 3312);
    if ($err = $cl->GetLastError())
        die("Error while querying Sphinx: " . $err);
    $cl->setMode(SPH_MATCH_EXTENDED);
    $res = $cl->Query('sphinx', 'qix_1');
    if (!$res)
        die("Error while querying Sphinx: " . $cl->GetLastError());
  

e del mio driver per Python

    import sphinx
    try:
        c = sphinx.SphinxClient('localhost', 3312)
        r = c.query('sphinx', index='qix_1', mode=sphinx.SPH_MATCH_EXTENDED)
    except sphinx.SphinxError, e:
        raise SystemExit("Error while querying Sphinx: %s" % e)
  

Spero che questa prima infarinatura di Sphinx sia stata sufficiente a farvi intravedere le potenzialità di questo motore di ricerca. Continueremo ad esaminarlo nei prossimi articoli, se avete qualche curiosità o argomento che vi piacerebbe fosse trattato, segnalatemelo nei commenti.

Pubblicato il
25 Gen 2008
Tag

Trackback

Commenti

  • Luca Berna il 27 Gen 2008

    A costo di essere ripetitivo...pubblicate articoli davvero molto interssanti. Questo in particolare ha stuzzicato la mia curiosità! Aspetto il seguito!

  • TheBox il 28 Gen 2008

    I javaisti all'ascolto avranno probabilmente piu' familiarita' con Lucene, che poi altro non e' che l'engine alla base di Solr. Un anno e mezzo fa questo benchmark dava Lucene vincente su Sphinx con l'aumentare del numero di thread, ma come ben evidenziato nei commenti e ammesso dall'autore stesso i risultati sono solo parzialmente comparabili (e l'autore non ha di certo tirato sphinx come avrebbe dovuto).

    Sono molto interessato a vedere il prosieguo. Ottimo lavoro!

  • ludo il 28 Gen 2008

    Beh, tieni conto che confrontare Lucene a Sphinx non è completamente fair: Sphinx ha una componente di rete, mentre Lucene lo usi direttamente tramite API.

    I risultati sono comunque paragonabili, e la mia breve esperienza con Solr mi fa pensare che su query più complesse, con raggruppamenti e/o filtri e ordinamenti particolari, Sphinx sia nettamente più veloce. Oltretutto, Solr ha il grande vantaggio di permettere live update (cosa che Sphinx può fare solo nell'ultima versione, e solo con modifiche sugli attributi) che però paghi pesantemente con i warm up richiesti ai singoli thread.

    Vedremo come saranno i risultati quando arriverò a fare i benchmark promessi, io sono davvero curioso...

  • manfred dardenne il 6 Mar 2008

    Very nice article thanks ! I had to use some translators since my italian is no so good This deserves an english translation at least !

    Many thanks by he way

  • Rosario il 6 Lug 2008

    Ottimo. Davvero ottimo articolo, complimenti.

Screencast e videocorsi di programmazione
Stacktrace RSS Feed Stacktrace via E-mail
Hai idee per un articolo? Faccelo sapere!