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 viacron
-
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 utilizzandosearchd
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 bitsql_attr_bool
, attributi boolean che richiedono meno spaziosql_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 tipoGROUP_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
sonostem_en
per la lingua inglese,stem_ru
per il russo,stem_enru
che combina inglese e russo,soundex
emetaphone
che utilizzano algoritmi
fonetici; se Sphinx è stato compilato con il supporto perlibstemmer
, 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 aindexer
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 valore1
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 esempioISO-8859-1
) oUTF-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 indicizzatihtml_index_attrs
definisce attributi HTML da indicizzare, ad esempioimg=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 demonesearchd
è 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 utilizzatoSPH_MATCH_ANY
(-a
usandosearch
), che cerca una corrispondenza con almeno uno dei termini di ricercaSPH_MATCH_PHRASE
(-p
), cerca una corrispondenza esatta con i termini, interpretandoli come una unica frase nell’ordine in cui sono stati inseritiSPH_MATCH_BOOLEAN
(-b
), permette l’utilizzo di operatori booleani (& | + -
) e parentesi per il raggruppamento, considerando un&
implicito tra i termini di ricercaSPH_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 risultatiSPH_SORT_ATTR_DESC
, ordina secondo il valore di un attributo (che deve essere specificato), con ordinamento decrescenteSPH_SORT_ATTR_ASC
, ordina secondo il valore di un attributo (che deve essere specificato), con ordinamento crescenteSPH_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 decrescenteSPH_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.
A costo di essere ripetitivo…pubblicate articoli davvero molto interssanti.
Questo in particolare ha stuzzicato la mia curiosità! Aspetto il seguito!
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!
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…
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
Ottimo. Davvero ottimo articolo, complimenti.