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

Indici e ricerche con Ruby e Ferret

Come muovere i primi passi con la libreria di ricerca e creare i primi indici
Come muovere i primi passi con la libreria di ricerca e creare i primi indici
Link copiato negli appunti

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 risultati
  • limit: numero di risultati da restituire
  • sort: modalità di ordinamento dei campi
  • filter e filter_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

Ti consigliamo anche