Sphinx, “the Russian black magic” / 1

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.

Comments

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

  2. 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!

  3. 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…

  4. manfred dardenne says:

    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

  5. Rosario says:

    Ottimo. Davvero ottimo articolo, complimenti.

Policy per i commenti: Apprezzo moltissimo i vostri commenti, critiche incluse. Per evitare spam e troll, e far rimanere il discorso civile, i commenti sono moderati e prontamente approvati poco dopo il loro invio.


Trackbacks