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

Creare un motore di ricerca con Rails e Solr

Un potente search engine per le applicazioni Rails, da applicare con poco sforzo al modello
Un potente search engine per le applicazioni Rails, da applicare con poco sforzo al modello
Link copiato negli appunti

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.

Figura 1. Pannello di amministrazione di Solr
(http://localhost:8982/solr)

Pannello di amministrazione di 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

Ti consigliamo anche