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 :-)

Diagram UML wyszukiwarki

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.

  1. 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.
  2. 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 @Searchable będzie wyglądać niezwykle szykownie przed definicją klasy obiektu dziedziny :-)
  3. 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!

Digg del.icio.us StumbleUpon Wykop Reddit Folksr

permalink | trackback | rss

 
 
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 :-)

Your turn:

nick:
and?:
www (if any):
Wpisz kod:code
Uniwersalne wyszukiwanie - PHP php wyszukiwanie wyszukiwarka search