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)
-
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)
-
class Entry < ActiveRecord::Base
-
acts_as_ferret :fields => [:title, :body]
-
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)
-
@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)
-
Entry.find(:all)
ale z dodatkowymi atrybutami:
Code (ruby)
-
results = Entry.find_by_contents("ferret")
-
puts "Ilość trafień = #{results.total_hits}"
-
results.each do |entry|
-
entry.title #std Active Record
-
entry.ferret_score #trafność wyszukiwania
-
end
- 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)
- indeks ten aktualizuje się na bieżąco i nie musimy się nim specjalnie interesować
- 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)
-
count = Entry.total_hits("ferret")
Rekordy i trafność znalezionych rekordów:
Code (ruby)
-
results = Entry.find_id_by_contents("ferret")
Przykładowa zmienna results mogłaby wyglądać tak:
Code (ruby)
-
results = [{:model => "Entry", :id => "4", :score => "1.0"},
-
{:model => "Entry", :id => "21", :score => "0.93211"},
-
{:model => "Entry", :id => "27", :score => "0.32212"}
-
]
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)
-
class Entry < ActiveRecord::Base
-
acts_as_ferret :fields => {
-
:title => {:store => :yes}
-
}
-
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)
-
def self.find_storage_by_contents(query, options={})
-
index = self.ferret_index #to załatwił za nas już sam Ferret
-
results =[]
-
total_hits = index.search_each(query, options) do |entry, score|
-
result = {}
-
result[:title] = index[entry][:title]
-
result[:score] = score
-
results.push(result)
-
end
-
return [total_hits, results]
-
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.