In una moderna applicazione Web è essenziale offrire funzionalità di ricerca evolute tra i contenuti. Un primo approccio consiste nell'utilizzare, ad esempio, le facility di ricerca messe a disposizione dal sottostante RDBMS con query del tipo:
select * from posts where body like '%foo%'
Ma questo approccio ha delle evidenti limitazioni: cosa succede se il nostro modello ha diversi campi sui quali effettuare la ricerca? E se la ricerca andasse effettuata non solo su un unico modello, ma anche su alcuni (se non tutti) i modelli ad esso direttamente collegati?
Supponiamo di avere un social network di ricette culinarie. Vogliamo offrire un comodo ed intuitivo form per la ricerca delle ricette. Ogni ricetta è rappresentata dal proprio nome, dalla descrizione, dal tempo di preparazione e da un insieme di ingredienti. Se ipotizziamo anche l'immancabile presenza dei tag diventa complicato costruire una query di ricerca su quanto appena elencato, per non parlare dell'impossibilità di prevedere query complesse!
class Recipe < ActiveRecord::Base has_and_belongs_to_many :tags has_and_belongs_to_many :ingredients validates_presence_of :name validates_presence_of :description validates_presence_of :preparation_time end class Ingredient < ActiveRecord::Base has_and_belongs_to_many :recipes validates_presence_of :name end class Tag < ActiveRecord::Base has_and_belongs_to_many :recipes validates_presence_of :name end
Un'idea potrebbe consistere nel tenere, in un campo apposito, tutte le informazioni testuali legate ad una ricetta, ed aggiornarlo ogni volta che una di queste viene aggiornata… ma anche quando vengono aggiornati gli ingredienti ed i tag … impraticabile!
Solr
Solr è un motore di ricerca basato sulla libreria Lucene (già in seno ad Apache Software Foundation) e viene eseguito all'interno di un servlet container. Ma niente paura… esiste un plugin per Rails che permette di installarlo all'interno dell'applicazione corrente e di accedere alle sue (potenti) funzionalità con estrema facilità ed in brevissimo tempo.
Posto di avere un JRE 1.5 nel nostro ambiente, all'interno della nostra applicazione Rails eseguiamo il comando:
$ script/plugin install svn://svn.railsfreaks.com/projects/acts_as_solr/trunk
Tutto qui. A questo punto abbiamo un search engine integrato nella nostra applicazione Rails, occorre solo lanciarlo con il comando:
$ rake solr:start
Partirà il demone Solr in ascolto sulla porta 8982
(o 8983
in production) già configurato per accedere al nostro data base ed indicizzarlo. Ovviamente dobbiamo ancora stabilire cosa deve essere indicizzato.
(http://localhost:8982/solr)
Prima di proseguire, analizziamo alcune caratteristiche di Solr:
- è distribuito con una licenza open source "permissiva": Apache Software License
- la community di sviluppo è molto attiva
- è ricco di funzionalità quali: caching, replicazione, highlighting del testo, etc.
- è pensato per un uso in ambiente enterprise (è in grado di gestire fino a 100 milioni di documenti con molta facilità);
- nel nostro caso viene eseguito nel servlet container Jetty, molto leggero e potente allo stesso tempo (in effetti è una delle soluzioni preferite quando si parla di servlet container embedded).
Indicizzare i modelli
Adesso non ci resta che stabilire quali modelli (e su quali campi) debbano essere indicizzati da Solr
. Nel nostro caso abbiamo la necessità di poter ricercare una ricetta, riferendoci anche a tutti i suoi ingredienti ed agli eventuali tag ad essa associati.
Il precedente codice rivisitato è pressoché autoesplicativo:
class Recipe < ActiveRecord::Base
has_and_belongs_to_many :tags
has_and_belongs_to_many :ingredients
validates_presence_of :name
validates_presence_of :description
validates_presence_of :preparation_time
acts_as_solr :fields => [:name, :description],
:include => [:ingredients, :tags]
end
class Ingredient < ActiveRecord::Base
has_and_belongs_to_many :recipes
validates_presence_of :name
end
class Tag < ActiveRecord::Base
has_and_belongs_to_many :recipes
validates_presence_of :name
end
In pratica indichiamo su quali campi del modello Recipe deve essere attivato il search engine, nonché su quali oggetti collegati.
Effettuare ricerche
Apriamo la console ruby e vediamo qualche esempio:
>> results = Recipe.find_by_solr('pomodoro') 15-nov-2008 11.00.00 org.apache.solr.core.SolrCore execute INFO: /select wt=ruby&q=(pomodoro)+AND+type_t:Recipe&fl=pk_i,score&qt=standard 0 11 => #<ActsAsSolr::SearchResults:0x216a254 @solr_data={:docs=>[#<Recipe id: 932579763, name: "Spaghetti al pomodoro e basilico", description: "...zzz...", preparation_time: 10, created_at: "2008-11-15 09:09:16", updated_at: "2008-11-15 09:09:16">, #<Recipe id: 623753982, name: "Panzanella", description: "...zzz...", preparation_time: 5, created_at: "2008-11-15 09:09:16", updated_at: "2008-11-15 09:09:16">], :max_score=>1.0111289, :total=>2}> >> y results --- !ruby/object:ActsAsSolr::SearchResults solr_data: :docs: - !ruby/object:Recipe attributes: name: Spaghetti al pomodoro e basilico updated_at: 2008-11-15 09:09:16 preparation_time: "10" id: "932579763" description: ...zzz... created_at: 2008-11-15 09:09:16 attributes_cache: {} - !ruby/object:Recipe attributes: name: Panzanella updated_at: 2008-11-15 09:09:16 preparation_time: "5" id: "623753982" description: ...zzz... created_at: 2008-11-15 09:09:16 attributes_cache: {} :max_score: 1.0111289 :total: 2 => nil
Il metodo find_by_solr
restituisce un oggetto del tipo ActsAsSolr::SearchResults
, il quale contiene i docs
oggetto della ricerca (ovvero oggetti Recipe
) oltre ad informazioni ausiliarie quali il massimo score della ricerca (gli oggetti Recipe vengono presentati ordinati in base allo score ottenuto).
Notiamo come la parola oggetto della ricerca (pomodoro
) sia presente solo all'interno del primo risultato ma non
nel secondo. Una cosa interessante consiste nel poter usufruire di parole chiave per customizzare la ricerca, vediamo il seguente esempio:
>> Recipe.find_by_solr('pomodoro spaghetti').total 15-nov-2008 11.25.52 org.apache.solr.core.SolrCore execute INFO: /select wt=ruby&q=(pomodoro+spaghetti)+AND+type_t:Recipe&fl=pk_i,score&qt=standard 0 2 => 1 >> Recipe.find_by_solr('pomodoro OR spaghetti').total 15-nov-2008 11.25.55 org.apache.solr.core.SolrCore execute INFO: /select wt=ruby&q=(pomodoro+OR+spaghetti)+AND+type_t:Recipe&fl=pk_i,score&qt=standard 0 3 => 2
L'uso di "OR" ci permette di allargare l'insieme dei risultati (ovviamente). La sintassi per le query è quella messa a disposizione da Lucene, (consultare la documentazione ufficiale per approfondimenti).
Approfondimenti
Ovviamente in questa sede non possiamo dilungarci troppo, limitiamoci a qualche assaggio delle diverse funzionalità di Solr, da approfondire poi sulla documentazione di riferimento e da sperimentare in prima persona.
È possibile indicizzare in base a campi non presenti nel data base (virtual attributes):
class Recipe < ActiveRecord::Base acts_as_solr :fields => [:name, :description], :include => [:ingredients, :tags], :additional_fields => [:presence] def presence time_ago_in_words(self.updated_at) end end
È possibile indicizzare un oggetto solo se esso soddisfa determinate condizioni:
class Recipe < ActiveRecord::Base acts_as_solr :fields => [:name, :description], :include => [:ingredients, :tags], :if => Proc.new {|recipe| recipe.published? } end
È possibile effettuare le ricerche in base ai facets
:
class Recipe < ActiveRecord::Base
acts_as_solr :fields => [:name, :description],
:include => [:ingredients, :tags],
:facets => [:tags]
end
In questo esempio i risultato verranno categorizzati in base ai tag in cui sono presenti.
È possibile indicare dei "pesi" per ogni field che contribuisce all'indicizzazione. In tal modo possiamo dare maggiore importanza ad field piuttosto che ad altri:
class Recipe < ActiveRecord::Base acts_as_solr :fields => [{:name => {:boost => 7.0}}, :description], :boost => 10.0, :include => [:ingredients, :tags] end
Una cosa molto interessante consiste nel poter effettuare una ricerca su più modelli contemporaneamente.
Rendiamo anche il modello Tag indicizzato:
class Tag < ActiveRecord::Base acts_as_solr end
e proviamo nella console:
>> y Recipe.multi_solr_search('pomodoro OR estate', :models => [Tag])
--- !ruby/object:ActsAsSolr::SearchResults
solr_data:
:docs:
- !ruby/object:Recipe
attributes:
name: Panzanella
updated_at: 2008-11-15 09:09:16
preparation_time: "5"
id: "623753982"
description: ...zzz...
created_at: 2008-11-15 09:09:16
attributes_cache: {}
- !ruby/object:Tag
attributes:
name: estate
updated_at: 2008-11-15 09:09:16
id: "71277473"
created_at: 2008-11-15 09:09:16
attributes_cache: {}
- !ruby/object:Recipe
attributes:
name: Spaghetti al pomodoro e basilico
updated_at: 2008-11-15 09:09:16
preparation_time: "10"
id: "932579763"
description: ...zzz...
created_at: 2008-11-15 09:09:16
attributes_cache: {}
:total: 3
=> nil
Infine, è utile ricordare che ogni volta che si modifica un oggetto indicizzato da Solr, quest'ultimo ne ricalcola gli indici. Volendo forzare tale azione è sufficiente utilizzare:
Recipe.rebuild_solr_index