Uniwersalne wyszukiwanie - PHP
10 Jun 2008
Przykład wyszukiwarki wałkowałem już kiedyś na tym blogu przy okazji rantu o Traitsach w PHP. Nie był on jednak rozpracowany zbyt dokładnie, w końcu służył tylko zilustrowaniu pewnego problemu.
Wyszukiwanie danych rozumiane jako dziura na tekst i przycisk "Znajdź" wydaje się być zagadnieniem dość prostym dopóki wiemy wśród czego szukamy (np. poprzez proste SELECT * FROM cars WHERE make LIKE ...). Szukanie bez określonego kontekstu w sposób jak powyżej wydaje się być jest zadaniem beznadziejnym...
W poszukiwaniu prostego i skutecznego indeksowania w aplikacji PHP doszedłem ostatnio do zadawalającego rozwiązania. A ponieważ uwielbiam podniecać się technikami obiektowymi - rozwiązanie to ląduje na jogu. Posługuję się w nim techniką Dependency Injection do indeksowania treści w locie przy operacji zapisu przez DAO. Obowiązkowo występować w nim musi implementacja wyszukiwarki (najprostsza możliwa, wykorzystująca PDO):
CREATE TABLE search (
class TEXT NOT NULL,
key INT NOT NULL,
content TEXT
);
CREATE INDEX search_content_idx ON search (content);
Schemat tabeli przechowującej poindeksowaną treść.
class SearchEngine {
private $pdo;
public function __construct(PDO $pdo) {
$this->pdo = $pdo;
}
public function update($class, $id, $text) {
$this->pdo->exec("DELETE FROM search WHERE class = '$class' AND key = '$id'");
$content = $this->pdo->quote($content);
$this->pdo->exec("INSERT INTO search (class, key, content) VALUES ('$class', '$id', '$content')");
}
public function find($text) {
$condition = array('TRUE');
foreach (explode(' ', $text) as $t) {
$t = $this->pdo->quote($t);
$condition[] = "content LIKE '%t%';
}
$condition = implode(' AND ', $condition);
$sql = "
SELECT class, key FROM search
WHERE $condition;
";
$stmt = $pdo->query($sql);
return $stmt;
}
}
Przykład implementacji wyszukiwarki. Algorytm szeregowania znalezionych obiektów realizujemy w klauzyli ORDER BY (pominięta dla czytelności).
oraz interfejs ISearchable:
interface ISearchable {
public function updateSearchEngine(SearchEngine $se);
}
Metoda update przyjmuje nazwę klasy, unikalny identyfikator obiektu (najczęściej klucz główny) oraz treść do zaindeksowania. Metoda find zwraca pary (nazwa_klasy, klucz_główny), w których szukana fraza została odnaleziona.
Załóżmy, że naszym przykładowym obiektem dziedziny modelu jest wpis na blogu reprezentowany przez klasę BlogPost. Obiekty tej klasy potrafią uaktualnić informację o sobie w wyszukiwarce dzięki implementacji interfejsu ISearchable, konkretnie indeksowaniu podlega tytuł, treść oraz tagi dotyczące wpisu.
class BlogPost implements ISearchable {
public $id, $title, $body, $tags;
// reszta implementacji klasy
public function updateSearchEngine(SearchEngine $se) {
$se->update(get_class($this), $this->id,
$this->title . ' ' . $this->body . ' ' . $this->tags);
}
}
Ponieważ wywoływanie metody updateSearchEngine za każdym razem, gdy zawartość obiektu się zmienia byłoby niepraktyczne, można zadanie powierzyć obiektowi DAO, który rozpozna instancję ISearchable i wykona na niej tą metodę. Do tego potrzebna jest drobna modyfikacja klasy DAO (zakładam, że jakąś już masz):
class DAO {
private $searchEngine;
public function save($o) {
... // stuff DAO normally does
if ($o instanceof ISearchable) {
$o->update($this->searchEngine);
}
}
}
Od teraz, za każdym razem, gdy będziesz utrwalać obiekt za pomocą DAO, będzie on aktualizował wyszukiwarkę informacjami dostarczonymi przez obiekt dziedziny.
Gdy już wszystko działa jak należy, możemy przystąpić do zrozumienia tej kooperacji :-)
Po pierwsze, tworzony jest centralny rejestr poindeksowanej treści. Po drugie, to obiekt dziedziny wie jaką treść (tj. które pola) powinien zaindeksować. Po trzecie, indeksowaniem i aktualizacją zajmuje się ten, kto ma władzę nad trwałością, czyli DAO.
Dążąc do uniwersalności tego rozwiązania, co obiecałem w tytule, wprowadźmy kolejną klasę, której obiekty będą indeksowane. W przypadku bloga dość naturalnie nasuwa się na myśl BlogPostComment.
class BlogPostComment implements ISearchable {
public $id, $blogPostId, $body, $author;
public function updateSearchEngine(SearchEngine $se) {
$se->update(get_class($this), $this->id, $this->body . ' ' . $this->author);
}
}
Nie zmieniając nic w implementacji wyszukiwarki możemy w ten sposób dodawać funkcjonalność wyszukiwania do dowolnego obiektu dziedziny zagadnienia.
Final remarks.
- Implementacja wspiera jedynie indeksowanie treści oraz wyszukiwanie obiektów zawierających ją. Nie wspiera natomiast prezentowania tej treści. Będziesz do tego potrzebował jakiegoś mechanizmu, który na podstawie zwróconej pary (nazwa_klasy, klucz_główny) powoła obiekt (lub wydobędzie treść bezpośrednio z bazy) i wyświetli odpowiednie pola w pożądanym formacie.
-
Implementacja w oparciu o formalny interfejs nie jest wcale obowiązkowa. Można to zrobić w szpanerskim stylu używając adnotacji - przykład używania AOP opisywałem przy okazji wywodu nad prawami dostępu. Adnotacja
@Searchablebędzie wyglądać niezwykle szykownie przed definicją klasy obiektu dziedziny :-) - Opisana implementacja samej wyszukiwarki nie jest w żaden sposób kompletna, czy optymalna. Jest ona uproszczona do granic możliwości, by przykład nadawał się do analizy. Algorytm szeregowania treści należy umieścić w klauzuli ORDER BY. Warto również zwrócić uwagę na redundancję powstającą w wyniku indeksowania treści. Nie jest ona niczym niewłaściwym dopóki nie modyfikujemy tabel obiektów dziedziny ręcznie, lub w jakikolwiek inny sposób z pominięciem DAO.
Miłej zabawy!
- BTM
Jeżeli dobrze rozumiem, to w wyniku takiej implementacji wszelkie dane, które chcemy przeszukiwać są dublowane - znajdują się w swojej własnej tabeli (np. z postami, tagami etc) oraz w tabeli z danymi do przeszukania?
Zapewne jest to i szybsze, bo przeszukujemy jedną tabelę, ale ja zostanę na razie przy swoim ISearchable i function search($string) bo DAO do mnie nie przemawia :)- str()
Zasadniczo tak, choć nie zawsze. Jeśli w blogu zezwalamy na składnię HTML to z pewnością nie chcemy aby znaczniki łapały się do wyników wyszukiwania. Do tego celu metodę BlogPost::updateSearchEngine należałoby lekko zmodyfikować dodając strip_tags dla pola body.
Co do wydajności - moje zdanie co do wszelkiej optymalizacji poznasz we wpisie http://stronger.jogger.pl/2008/06/02/faszysci-optymalizacji/
DAO? - jeszcze przemówi ;-)- BTM
Hm, sprytne z tym wycinaniem znaczników - w takim wypadku jest to duży inplus.
Jeżeli chodzi o optymalizację - cóż, zdanie ciekawe nie będę się spierał ;-) Tym nie mniej gdybym u siebie zastosował takie rozwiązanie jak opisujesz to przy przekształcaniu wyników na tabele, modułu i strony wyszło by na to samo pod wzg. prędkości pewnie :-)







