poniedziałek, 30 marca 2009

Lucynko, gdzie są moje klucze? Zend_Search_Lucene. cz. 1

Mając duży portal jasnym staje się, że potrzebna jest wyszukiwarka, która pozwoli w łatwy sposób znaleźć strony które mogą nas interesować. Napisanie wyszukiwarki dla dużej strony nie jest łatwą sprawą, trzeba wziąć pod uwagę, że dane mogą być przechowywane w wielu tabelach. Zamiast pisać wielolinijkowe zapytania do bazy danych, zawierające łączenia, unie i inne dziwactwa warto za interesować się indeksowaniem stron. Na rynku od dawna istnieje Xapian, zaawansowany mechanizm indeksowania oraz wyszukiwania napisany w C++ ale oferujący moduł do PHP. Działa świetnie, ale.., wymagane jest za instalowanie specjalnego modułu co jak wiadomo na serwerach hostingowych graniczy z cudem. I tu na pole bitwy wkracza Lucene, system pierwotnie napisany w Javie, udostępniony dla programistów PHP jako moduł Zend_Framework.


Zend_Search_Lucene bo tak się w pełni nazywa do swojego działania potrzebuje:
  • ctype - sprawdzanie znaków i liczb z wyznaczonymi kryteriami
  • dom - obiektowy dostęp do xml-a
  • iconv - konwersja między systemami kodowania znaków
  • libxml - obsługa xml-a
  • bitset - wydajne zbiory dla php - nie jest wymagane

Powyższe elementy są standardowym wyposażeniem niemal każdego serwera hostingowego w Polsce, więc nie powinniśmy mieć większych problemów z instalacją Zend_Search_Lucene na serwerze (no może poza małą konfiguracją dodatkową, o czym później).

Co dostaniemy dzięki Zend_Search_Lucene


1. Wszystko w jednym miejscu


System indeksujący Lucene trzyma wszystkie dane jakie zindeksował w jednym miejscu, dane z różnych tabel i baz danych, dane z plików, grafiki, można znaleźć wywołując jedno zapytanie.


2. Łatwe indeksowanie wielu typów danych


System Lucene potrafi zindeksować dowolny rodzaj danych tekstowych jaki mu podamy, co więcej, jest w stanie zindeksować pliki worda (07), excela (07) oraz power pointa (07), a także strony www (w zasadzie jest to rodzaj pliku tekstowego, ale posiada specjalnie zdefiniowaną do tego klasę).


3. Jednolite wyniki


Przy dobrze zaprojektowanym indeksie dostajesz zawsze jednolite wyniki, nie musisz sprawdzać czy dane pole istnieje, z jakiego miejsca pochodzą dane, by odpowiednio je pobrać, zawsze wiesz jakie pole przechowuje jakie dane.


4. Wydajność


O wiele szybciej wyszukać dane z pliku lokalnego niż z bazy danych która może być na innym serwerze, lub mocno obciążona przez innych użytkowników. Indeksy są dodatkowo optymalizowane by wyszukiwanie było maksymalnie wydajne.



O co się musimy martwić?


1. Metoda indeksowania i struktura indeksu


Wraz z Zend_Search_Lucene dostajemy 2 główne moduły, moduł indeksowania oraz moduł wyszukiwania. Jednak nie jest to w pełni działająca wyszukiwarka, sami musimy "zbudować" index, określić jego strukturę (jakie dane będą indeksowane jaką metodą i pod jakim kluczem zostaną zapisane) , a także napisać funkcje obsługujące wyszukiwarkę (co jednak jest banalnie proste)

2. Dbanie o aktualność indeksu,


Aby indeks był zawsze aktualny musimy napisać skrypty które będą dodawać bądź usuwać elementy,  jest to dodatkowy kod który trzeba stworzyć, a gdy dojdą nowe źródła danych zaktualizować.

. Ograniczona wielkość indeksu


W środowiskach opartych o 32bitowe systemy, Lucene może stworzyć maksymalnie 2GB indeks, jest to co prawda wystarczający rozmiar dla średniej/dużej wielkości serwisu, ale jak wiadomo zdarzają się większe.

Jak indeksować dane?


Sam proces indeksowania danych jest niemal autonomiczny  i trywialny w rozpisaniu, musimy jedynie wskazać co zindeksować. Brzmi prosto ale ma swój haczyk. Aby nasza wyszukiwarka działała optymalnie i wygodnie dla nas warto wcześniej przygotować listę pól które mają być zindeksowane, które pola chcielibyśmy aby wyszukiwarka dla nas przechowywała w indeksie a które ma tylko przeskanować w poszukiwaniu słów-kluczy. Do tego celu służą metody klasy Zend_Search_Lucene_Field.
Do dyspozycji mamy kilka metod, każda z nich działa w inny sposób, jednak jeśli odpowiednio ich użyjemy stworzony zostanie wydajny indeks. Ważne jest byśmy przechowywali tylko te dane, które potem chcemy wyświetlać w wynikach wyszukiwania, resztę danych możemy jedynie zindeksować. Dzięki samemu indeksowaniu zyskujemy na wielkości indeksu nie tracąc jakości otrzymywanych wyników.

Co i jak zapamiętywać


Jak wspomniałem wcześniej Lucene dostarcza metodę indeksowania, a wraz znią kilka typów pól indeksowalnych zawartych w obiekcie Zend_Search_Lucene_Field. Dzięki tym metodą możemy wskazać jakie dane jak mają być dodane do indeksu, a są to odpowiednio:
  • Keyword - jest idealnym typem pola do przechowywania typów wyliczeniowych, dane przechowywane w takim polu są przechowywane w bazie danych, a także są indeksowane, nie następuje ich rozbiór na czynniki pierwsze (tokenizacja)
  • UnIndexed - jak sama nazwa wskazuje dane takie nie są indeksowane, nie są również tokenizowane, dane takie natomiast są zachowywane w bazie, jest to bardzo dobry wybór, jeśli chcemy przechowywać adres dokumentu, jego klucz główny w bazie, daty
  • Text - dane są ineksowane, tokenizowane i przechowywane w ineksie, w tym typie pola będziemy przechowywać wszelkie teksty gdyż najczęście na podstawie zawartych w nich słowach będziemy znajdować elementy, jeśli wyniki nie muszą zwracać pełnych wpisów w bazie, to pole może przechowywać tylko krótki opis co zawiera dany rekord
  • Unstored - tak samo jak pole typu Text dane są indeksowane i tokenizowane, jednak nie są zapisywane w bazie, idealnie nadaje się do indeksowania długich tekstów, nie musimy martwić się o zbyt duży rozrost naszego indeksu zachowując jednocześnie jakość wyników
  • Binary - ten typ jedynie przechowuje dane, jednak nie są to typowe dane, a dane binarne (np. zdjęcia, pliki tekstowe, muzyka), używać z rozwagą

Wszystkie powyższe metody poza metodą Binary są trójargumentowe. Pierwszy argument to klucz (nazwa) pola, po nim będziemy się dostawać do wartości jakie on reprezentuje, drugim są same dane przekazane modułowi indeksowania, a ostatni opcjonalny to kodowanie danych które przekazujemy.

Uwaga!


Należy pamiętać, że żadne z pól nie może się nazywać "id" jest to nazwa zastrzeżona (niestety w dokumentacji nic o tym nie mówią) dla systemu indeksującego.

Budujemy indeks


Tyle czytania a jeszcze nie było kodu, czas to zmienić.
Proces indeksowania odbywa się w 4 etapach, w etapie pierwszym tworzymy nowy obiekt Zend_Search_Lucene_Document, następnie (etap 2), dodajemy dane do tego dokumentu, należy również otworzyć indeks (etap 3), by można go było dodać (etap 4) wcześniej stworzony dokument do niego. W kodzie PHP wygląda to następująco:

dokument = new Zend_Search_Lucene_Document();
$dokument->addField(Zend_Search_Lucene_Field::Text('klucz',$dane));
$index = Zend_Search_Lucene::create('/nasz/index');
$index->addDocument($dokument);</pre>

Prawda, że prosto? Oczywiście indeks taki możemy rozbudować o kolejne pola danych co na pewno rozbuduje nasza wyszukiwarkę. Jeśli nie mamy jeszcze indeksu należy go stworzyć robimy to poleceniem:

$index = Zend_Search_Lucene::create('/nasz/index',true);


Polecenie to tworzy indeks w katalogu "/nasz/index", drugi argument zapewnia nam, że jest to nowy, czysty indeks (jeśli wcześniej istniał tam indeks jest on usuwany.

Indeksowanie wielu dokumentów

Jedyna różnica między indeksowaniem jednego dokumentu a ich całą grupą jest komenda
$index->commit();

którą zatwierdza chęć zindeksowania dodanych plików, funkcję tą uzywamy również gdy chcemy usunąć dokument z indeksu.

$data = array(array('id'=>1,'title'=>'Tytul1','text'=>'text1'),array('id'=>2,'title'=>'Tytul2','text'=>'Text2');
$infex = Zend_Search_Lucene_Index::open('/moj/index');

foreach($data AS $d){
$d = new Zend_Search_Lucene_Document();
$d->addField(Zend_Search_Lucene_Dokument:: UnStored('doc',$d['id']);
$d->addField(Zend_Search_Lucene_Dokument::Text('title',$d['title']);
$d->addField(Zend_Search_Lucene_Dokument::Text('text',$d['text']);
$d->addField(Zend_Search_Lucene_Dokument::UnIndexed('date',date("F j, Y, g:i a"));
}
$index->commit();

Warto zauważyć, że pole dla klucza ID jest typu UnStored, dzięki temu nie marnujemy miejsca na niepotrzebne dane (jeśli potrzebujemy wyciągać ID przy wyszukiwaniu pole powinno być typu Text), po drugie zapewniamy sobie możliwość wyciągnięcia wpisu po znanym nam identyfikatorze co umożliwi nam usunięcie danego dokumenut z indeksu.

Aktualizowanie indeksu


Jeśli indeks nie będzie aktualiwoany to szybko traci on sens, jego wyniki powoli stają się mało jakościowe dla nas jak i dla klienta,warto więc zadbać o aktualizacje danych w indeksie, niestety Lucene nie udostępnia metod które pozwalają na zaktualizowanie zmienionego pola w danym dokumencie jedynym rozwiązaniem jest skaskowanie dokumentu i dodanie go do indeksu jeszcze raz. Aby dany wpis wykasować najlepiej wyszukać dane wpisy po ich unikalnym identyfikatorze (w poprzednim kodzie ten identyfikator miał nazwę "id", a potem iterując po wynikach kasować dokumenty.

$results = $index->find('id:' .$identyfikator);
foreach ($results as $_r) {
$index->delete($r->id);
}
$index->commit();

Powyższy kod znajdzie wszystkie dokumenty reprezentowane przez podany identyfikator i zaznaczy je do usunięcia. Po usunięciu takich wpisów, możemy na nowo dodać nasz dokument.

Problemy z indeksowaniem na serwerach hostingowych


Podczas wdrażania jednego z projektów które posiadały powyższy system indeksowania i wyszukiwania napotkałem dość dziwny problem, otóż pomimo, że na 2 komputerach (windows i macos), indeksy tworzyły się poprawnie, tak już na maszynie produkcyjnej nie działały w cale. Okazało się, że problem był w ustawieniach iconv, które domyślnie były ustawione na ISO-8859-1 (polski serwer). Jeśli i u was wystąpi problem rozwiązaniem jest dodanie poniższych linijek:

setlocale(LC_ALL, 'pl_PL.utf-8');
iconv_set_encoding("internal_encoding", "UTF-8");
iconv_set_encoding("output_encoding", "UTF-8");
iconv_set_encoding("input_encoding", "UTF-8");

Podsumowanie


Coraz częściej widzimy w serwisach błąd związany z przeciążeniem baz danych, wiąże się to z coraz większym zainteresowaniem młodych ludzi stawianiem for i innych systemów mocno wykorzystujących bazy danych. Dzięki Zend_Search_Lucene i przemyślanemu cache-owaniu, niemal do zera spada wykorzystanie baz danych zapewniając naszej aplikacji internetowej większą stabilność i dostępność. Sam mechanizm ułatwia stworzenie jednolitej wyszukiwarki bez potrzeby martwienia się o optymalizację nieraz skomplikowanych zapytać do bazy danych. Na pewno warto przyjrzeć się temu systemowi a co odważniejsi być może spróbują wdrożyć go w swoich systemach. W drugiej części  opowiem o procesie przeszukiwania indeksu i jego różnych metodach. Zapraszam.

7 komentarzy:

  1. Bardzo dobry tekst. Dzięki - nic tylko czekać na kolejne części.
    Pozdrawiam

    ps. Mała literówka - Zapomniałeś zamknąć cudzysłowowa w drugim bloku kodu (tworzenie indeksu).

    OdpowiedzUsuń
  2. A nie powinno być w zapytanie o wyniki:
    $results = $index->find('doc:' .$identyfikator);

    Ale tekst się przydał, dzięki.

    OdpowiedzUsuń
  3. @Paweł S - zależy co chcesz osiągnąć, w podanym przez Ciebie zapytaniu Wyszukiwarka zwróci dokument którego pole doc jest równie identyfikatorami, nie będzie przeszukiwał innych pól.

    OdpowiedzUsuń
  4. chodzi mi o to skąd się wzięło pole 'id'. Pisałeś, że jest to nazwa zastrzeżona, ale w jaki sposób możemy ją pozyskać.
    Dokładniej zastanawiam się jak najlepiej zarządzać usuwaniem/edycją dokumentów w wyszukiwarce. Zrobić w dokumencie pole np. "doc_id" w którym będzie przechowywane id z bazy i wszelkie te operacje wykonywać tak: $results = $index->find(’doc_id:’ .$idRekordu);

    OdpowiedzUsuń
  5. @Paweł S - pole id jest polem zastrzeżonym dla Indeksera, przechowywany jest tam unikalny identyfikator dla każdego dokumentu, i nie powinno się operować na nich bezpośrednio.
    Jeśli chcesz zarządzać swoim indeksem, najlepiej każdemu dokumentowi przypisać unikalny identyfikator po którym go wyszukasz, zobacz część "Aktualizowanie indeksu", więcej o edycji indeksu postaram się napisać w ten weekend.

    OdpowiedzUsuń
  6. [...] W ten sposób pozbędziemy się dokumentu który chcemy zaktualizować, po wykonaniu tych czynności możemy bez przeszkód dodać ponownie nasz dokument o czym było w poprzednim wpisie [...]

    OdpowiedzUsuń