Ferret, ovvero Apache Lucene per Ruby, è una libreria che fornisce funzionalità di indicizzazione e di ricerca. Per motivi di prestazioni è interamente scritto in C e può essere installato sia da rubygems
sia a partire dai sorgenti.
In questo articolo introduciamo alcune funzionalità di base, che ci permettono di indicizzare ed effettuare ricerche su semplici stringhe di testo.
Indici
Iniziamo con un esempio banale che vede l'indicizzazione di stringhe utilizzando la classe Ferret::Index::Index
. Il primo passo da fare è creare un'istanza della classe Index
; è possibile crearlo in memoria scrivendo semplicemente:
require 'rubygems' require 'ferret' index = Ferret::Index::Index.new
oppure passando dei parametri che definiscono le caratteristiche dell'indice. Parametri che vedremo in dettaglio nei prossimi esempi. A questo punto si può popolare l'indice appena creato inserendo i documenti sotto forma di stringhe. Ad esempio ipotizzando di dover indicizzare i testi di una libreria possiamo scrivere:
index << "I dolori del giovane Werther " index << "Il giovane Holden" index << "Lettere a un giovane poeta" index << "Diario di un dolore"
Un indice può essere visto come un array di documenti, dove per documento si intende un blocco di dati rappresentato da diversi campi che lo identificano e lo definiscono. L'indicizzazione e la ricerca non si limita solo ai file di testo ma si estende anche ad altri tipi di file come ad esempio i file PDF, MS Word, MP3, ecc.
Un documento in Ferret è un oggetto di tipo Ferret::Document
che non è altro che un Hash
leggermente modificato, e può essere visto come un insieme di campi dotati di un nome e di un array di valori di testo. La differenza con i normali Hash
sta nell'aggiunta dell'attributo boost
che viene utilizzato per dare un diverso peso ai documenti nei risultati delle ricerche. Un esempio di documento con più campi è il seguente:
index << {:title => "Il giovane Holden", :author => "J.D.Salinger"}
I documenti estratti dall'indice sono invece oggetti di tipo Ferret::Index::LazyDoc
. Per visualizzare i campi di un documento va utilizzato il metodo LazyDoc#fields
.
Ci sono diversi modi per aggiungere un documento ad un indice:
index << "Il giovane Holden" index << {:title => "Il giovane Holden", :author => "J.D.Salinger", :price => 11.80} book = Document.new(50.0) book[:title] = "Il giovane Holden" book[:author] = "J.D.Salinger" index << book
In ognuno dei tre esempi vengono aggiunte diverse informazioni, nel primo caso viene inserito nell'indice solo il titolo, nel secondo vengono utilizzati i campi per inserire titolo, autore e prezzo, nell'ultimo esempio viene impostato il valore dell'attributo boost a 50.0
. Quindi questo libro avrà un peso maggiore nelle ricerche poiché il valore di default di boost è 1.0
.
Dopo aver creato, e popolato, l'indice è possibile effettuare delle ricerche utilizzando delle query che possono essere delle semplici stringhe da ricercare nel testo ma anche combinazioni di valori, range e filtri che permettono di affinare la ricerca.
Ricerche
Ferret mette a disposizione diversi metodi per eseguire le ricerche, la ricerca più semplice è quella eseguita utilizzando il metodo Ferret::Index::Index#search
. Ad esempio la ricerca:
puts index.search("giovane")
restiturà i seguenti risultati sotto forma di oggetto Ferret::Search::TopDocs
.
TopDocs: total_hits = 3, max_score = 0.500000 [ 0 "I dolori del giovane Werther ": 0.500000 1 "Il giovane Holden": 0.500000 2 "Lettere a un giovane poeta": 0.500000 ]
Nell'output è mostrato il numero di risultati (total_hits
) e l'attinenza (max_score
) con il termine cercato. Vengono poi elencati i risultati preceduti dal numero che li rappresenta all'interno dell'indice e seguiti dallo score.
È possibile anche iterare sull'array dei risultati della ricerca utilizzando il metodo each:
res = index.search("giovane") res.hits.each do |hit| puts "#{hit.doc} score #{hit.score} (max: #{res.max_score})" end
Dove res.hits
rappresenta tutti i risultati della ricerca mentre hit
è di tipo Ferret::Search::Hit
e rappresenta un singolo risultato.
Il metodo search
prende come argomento la query da ricercare e delle opzioni che definiscono i parametri della ricerca:
offset
: l'offset di partenza nei risultatilimit
: numero di risultati da restituiresort
: modalità di ordinamento dei campifilter
efilter_proc
: filtri dei risultati della ricerca
Esistono due tipi di filtri standard: RangeFilter
e QueryFilter
. Il primo tipo permette di definire un range di valori assumibili da un campo, mentre QueryFilter permette di impostare un ulteriore query per restringere il campo di ricerca.
Un esempio completo di RangeFilter
è il seguente:
require 'rubygems' require 'ferret' include Ferret include Ferret::Index index = Index.new index << {:title => "Il giovane Holden", :author => "J.D.Salinger", :price => "11.80"} index << {:title => "I dolori del giovane Werther", :author => "W.Goethe", :price => "8.00"} index << {:title => "Lettere a un giovane poeta", :author => "R.M.Rilke", :price => "7.80"} index << {:title => "Diario di un dolore", :author => "C.S.Lewis", :price => "7.50"} query = "giovane" price_filter = Search::RangeFilter.new(:price, :>= => "7.00", :<= => "8.00") res = index.search(query, :filter => price_filter) res.hits.each do |hit| puts "#{index[hit.doc][:title]}: score #{hit.score}" end
In pratica vengono aggiunti dei documenti all'indice definiti da tre campi (:title
, :author
e :price
). Viene poi definito un RangeFilter
che filtrerà tutti i libri con un prezzo compreso tra i 7 e gli 8 €, ovvero con 7.00 <= :price <= 8.00
. Infine viene effettuata una ricerca sul termine "giovane
". Il risultato sarà il seguente:
I dolori del giovane Werther: score 0.0473515391349792 Lettere a un giovane poeta: score 0.0473515391349792
Il alternativa a search
si può anche utilizzare il metodo search_each
che come argomenti oltre alla query prende anche un blocco. Ad esempio il codice:
res = index.search_each('title:giovane AND price:8.00') do |id, score| puts "#{index[id][:title]}: score #{score}" end
fornisce come risultato tutti i documenti che hanno la parola "giovane"
nel titolo e hanno un prezzo pari a 8.00 €
. Nell'esempio è anche illustrata una query costruita con l'operatore AND mentre i termini da ricercare sono assegnati ai rispettivi campi.
Query avanzate
Esaminiamo ora le modalità che Ferret mette a disposizione per costruire le query di ricerca. Esistono 15 tipi di query che è possibile utilizzare per affinare la ricerca.
TermQuery
È il tipo più semplice. Basta indicare il nome del campo e il valore da cercare. Ad esempio tornando all'esempio visto prima è possibile scrivere:
query = Search::TermQuery.new(:title, "giovane") res = index.search(query)
RangeQuery
La ricerca viene effettuata su tutti i termini contenuti nel range passato come argomento. Ad esempio volendo ricercare tutti i libri con un prezzo compreso tra 7.00
e 7.99
basta indicare gli estremi dell'intervallo di ricerca scrivendo:
query = Search::RangeQuery.new(:price, :lower => "7.00", :upper => "7.99")
MultiTermQuery
Permette di cercare diversi termini, si comporta come la funzione logica OR
. Ad esempio volendo cercare nei titoli i termini "giovane"
e "holden"
si può scrivere:
query = Search::MultiTermQuery.new(:title) query.add_term("giovane") query.add_term("holden")
o semplicemente
query << "giovane" << "holden"
WildcardQuery
Simile a TermQuery
ma come termine da cercare possono essere utilizzate anche delle wildcard. Ad esempio la ricerca
query = Search::WildcardQuery.new(:title, "dolor*")
fornisce un risultato simile a
I dolori del giovane Werther: score 0.643841028213501 Diario di un dolore: score 0.643841028213501
Le wildcard utilizzabili sono l'asterisco (*
) e il punto interrogativo (?
) che indicano rispettivamente "zero o più caratteri" e "un singolo carattere".
BooleanQuery
Viene utilizzato per combinare più query attraverso il metodo add_query
. Un esempio di query costruita con due TermQuery
è il seguente:
tq1 = Search::TermQuery.new(:title, "giovane") tq2 = Search::TermQuery.new(:title, "holden") query = Search::BooleanQuery.new query.add_query(tq1, :must) query.add_query(tq2, :should)
Per ogni query aggiunta è possibile definire una clausola che definisce il comportmanto in caso di presenza o meno della query cercata. Le clausole utilizzabili sono:
:should
- aumenta la rilevanza quando il termine cercato è presente, quando invece è assente il risultato non viene scartato:must
- indica che la query deve essere presente altrimenti il documento non farà parte dei risultati:must_not
- ha un comportmanto opposto a:must
, ovvero vengono scartati tutti i documenti che contengono la query