Full-text search w Railsach = Ferret

Zapewne spotkaliście się z problemem wydajnego wyszukiwania pełnotekstowego (full-text search).

Założę się, że słyszeliście też o Javowym projekcie zwanym Lucene, który jest w tym temacie pewnym standardem. Pewnie też większość z was wie, że jest port Lucene napisany napisany w Rubym - Ferret.

To czego możecie nie wiedzieć to fakt, że używanie Ferreta w aplikacjach Railsowych jest wybitnie proste, przyjemne i wydajne przy minimalnej ingerencji w kod i minimalnym nakładzie pracy.

Jeżeli tego wam jeszcze mało - kilka testów wydajności: pierwszy, drugi, trzeci.

Teraz krok po kroku zainstalujemy, i zaczniemy używać Ferreta w nazych aplikacjach.

Wpis ten w dużej części oparty jest o znakomity tutorial na RailsEnvy.

Zaczynamy od zainstalowania samego Ferreta:

gem install ferret

Instalujemy Railsowy plugin acts_as_ferret (tu mamy dwie możliwości):

Pierwsza opcja - gem:

gem install acts_as_ferret

i wtedy w pliku environment.rb naszej aplikacji dodajemy wpis:

Code (ruby)
  1. require ‘acts_as_ferret’

Druga opcja - w naszej aplikacji Railsowej jako plugin. W głównym katalogu aplikacji wykonujemy:

ruby script/plugin install svn://projects.jkraemer.net/acts_as_ferret/tags/stable/acts_as_ferret

Możemy już przejść do używania naszego nowego, lśniącego Ferreta.

Mówimy naszym modelom by “zachowywały się jak fretka”

Code (ruby)
  1. class Entry < ActiveRecord::Base
  2.   acts_as_ferret :fields => [:title, :body]
  3. end

W parametrze :fields wyszczególniamy pola, które Ferret ma indeksować w danym modelu.

Możemy zacząć poszukiwania

Załóżmy, że chcemy w bazie naszych wpisów wyszukać te, które zawierają słowo ferret (w polach title lub body bo tylko tych pól indeks jest prowadzony).

Code (ruby)
  1. @entries_with_ferret = Entry.find_by_contents("ferret")

Zmienna @entries_with_ferret będzie zawierała tablicę obiektów ActiveRecord modelu Entry - taką gdy dostajemy używając np:

Code (ruby)
  1. Entry.find(:all)

ale z dodatkowymi atrybutami:

Code (ruby)
  1. results = Entry.find_by_contents("ferret")
  2. puts "Ilość trafień = #{results.total_hits}"
  3. results.each do |entry|
  4.   entry.title #std Active Record
  5.   entry.ferret_score #trafność wyszukiwania
  6. end
  1. Ferret w trakcie naszych działań tworzy w głównym katalogu naszej aplikacji swój indeks. W tym przypadku plik index/development/entry (gdzie development to nazwa tryb w jakim odpalona jest nasza aplikacja)
  2. indeks ten aktualizuje się na bieżąco i nie musimy się nim specjalnie interesować
  3. jeżeli z różnych względów potrzebujemy ręcznie odbudować indeks wystarczy skasować katalog index naszej aplikacji lub któryś z podkatalogów

Krótki opis dodatkowych metod.

Ilość znalezionych rekordów:

Code (ruby)
  1. count = Entry.total_hits("ferret")

Rekordy i trafność znalezionych rekordów:

Code (ruby)
  1. results = Entry.find_id_by_contents("ferret")

Przykładowa zmienna results mogłaby wyglądać tak:

Code (ruby)
  1. results = [{:model => "Entry", :id => "4", :score => "1.0"},
  2. {:model => "Entry", :id => "21", :score => "0.93211"},
  3. {:model => "Entry", :id => "27", :score => "0.32212"}
  4. ]

Standardowo zwracane jest pierwsze dziesięć trafień.
Jako opcje do tej metody możemy przekazać takie opcje jak do metody ferreta search_each np:

  • offset - przesunięcie wyników o daną wartość, domyślnie 0
  • limit - ilość zwracanych pozycji, domyślnie 10
  • sort - obiekt sortowania (?) lub pola wg których mają być sortowane zwrócone dane. Przykładowo “title DESC, author_name”

Pominę tu sporo opcji konfiguracyjnych tego pluginu (odsyłam do dokumentacji lub już wspomnianego poradnika na RailsEnvy).

Ostatniach rzecz, o której chciałbym wspomnieć to opcja przechowywania zawartości pól w indeksie Ferreta (Field Storage).

Jeżeli pola, które indeksujemy są małe i możemy sobie pozwolić na to by przechowywać ich wartość na dysku możemy ograniczyć obciążenie bazy danych przez przechowywanie wartości tych pól bezpośrednio w indeksie.
Nasza poprzednia deklaracja modelu Entry mogłaby wyglądać tak:

Code (ruby)
  1. class Entry < ActiveRecord::Base
  2.     acts_as_ferret :fields => {
  3.       :title => {:store => :yes}
  4.   }
  5. end

W tym przypadku pole title będzie przechowywane w indeksie.
Aby dostać się do tego pola potrzebujemy jednak własnej metody modelu Entry. Mogłaby wyglądać tak:

Code (ruby)
  1. def self.find_storage_by_contents(query, options={})
  2.   index = self.ferret_index #to załatwił za nas już sam Ferret
  3.   results =[]
  4.   total_hits = index.search_each(query, options) do |entry, score|
  5.     result = {}
  6.     result[:title] = index[entry][:title]
  7.     result[:score] = score
  8.     results.push(result)
  9.   end
  10.     return [total_hits, results]
  11. end

Mam nadzieję, że zachęciłem was do choćby spróbowania potężnego narzędzia jakim jest Ferret.

PS. Niedługo może o pluginach autoryzacji i autentykacji.

Leave a Reply »