Mysql
 sql >> Baza danych >  >> RDS >> Mysql

Szukasz prostego wyszukiwania pełnotekstowego? Wypróbuj MySQL InnoDB + CakePHP z Word Stemming

Wdrożenie wyszukiwania przyjaznego dla użytkownika może być trudne, ale może być również wykonane bardzo efektywnie. Skąd mam to wiedzieć? Nie tak dawno musiałem zaimplementować wyszukiwarkę w aplikacji mobilnej. Aplikacja została zbudowana na platformie Ionic i łączy się z backendem CakePHP 2. Pomysł polegał na wyświetlaniu wyników podczas pisania przez użytkownika. Było na to kilka opcji, ale nie wszystkie spełniały wymagania mojego projektu.

Aby zilustrować, na czym polega tego rodzaju zadanie, wyobraźmy sobie wyszukiwanie utworów i ich możliwych relacji (takich jak artyści, albumy itp.).

Rekordy musiałyby być posortowane według trafności, co zależałoby od tego, czy wyszukiwane słowo pasuje do pól z samego rekordu, czy z innych kolumn w powiązanych tabelach. Ponadto wyszukiwanie powinno uwzględniać przynajmniej kilka podstawowych rdzennych słów. (Pnia jest używany do uzyskania formy rdzenia słowa. „Pnie”, „pnia”, „pnia” i „pnia” mają ten sam rdzeń:„rdzeń”.)

Przedstawione tutaj podejście zostało przetestowane na kilkuset tysiącach rekordów i było w stanie uzyskać przydatne wyniki podczas pisania przez użytkownika.

Pełnotekstowe produkty wyszukiwania do rozważenia

Istnieje kilka sposobów na zaimplementowanie tego rodzaju wyszukiwania. Nasz projekt miał pewne ograniczenia w zakresie czasu i zasobów serwerowych, więc musieliśmy zachować jak najprostsze rozwiązanie. W końcu pojawiło się kilku pretendentów:

Elasticsearch

Elasticsearch zapewnia wyszukiwanie pełnotekstowe w usłudze zorientowanej na dokumenty. Został zaprojektowany do zarządzania ogromnymi ilościami obciążenia w sposób rozproszony:może klasyfikować wyniki według trafności, przeprowadzać agregacje i pracować z rdzennymi słowami i synonimy. To narzędzie jest przeznaczone do wyszukiwania w czasie rzeczywistym. Z ich strony internetowej:

Elasticsearch tworzy rozproszone możliwości na bazie Apache Lucene, aby zapewnić najpotężniejsze dostępne możliwości wyszukiwania pełnotekstowego. Potężny, przyjazny dla programistów interfejs API zapytań obsługuje wielojęzyczne wyszukiwanie, geolokalizację, kontekstowe sugestie, autouzupełnianie i fragmenty wyników.

Elasticsearch może działać jako usługa REST, odpowiadając na żądania http i można ją bardzo szybko skonfigurować. Jednak uruchomienie silnika jako usługi wymaga posiadania pewnych uprawnień dostępu do serwera. A jeśli twój dostawca hostingu nie obsługuje Elasticsearch od razu, będziesz musiał zainstalować kilka pakietów.

Najważniejsze jest to, że ten produkt jest świetną opcją, jeśli potrzebujesz solidnego rozwiązania wyszukiwania. (Uwaga:możesz potrzebować VPS lub serwera dedykowanego, ponieważ wymagania sprzętowe są dość wysokie).

Sfinks

Podobnie jak Elasticsearch, Sphinx zapewnia również bardzo solidny produkt wyszukiwania pełnotekstowego:Craigslist obsługuje ponad 300 000 000 zapytań dziennie. Sphinx nie zapewnia natywnego interfejsu RESTful. Jest zaimplementowany w C, z mniejszą ilością sprzętu niż Elasticsearch (który jest zaimplementowany w Javie i może działać na dowolnym systemie operacyjnym z jvm). Potrzebny będzie również dostęp root do serwera z dedykowaną pamięcią RAM/procesorem, aby poprawnie uruchomić Sphinxa.

Pełnotekstowe wyszukiwanie MySQL

W przeszłości wyszukiwanie pełnotekstowe było obsługiwane w silnikach MyISAM. Po wersji 5.6 MySQL obsługiwał również wyszukiwanie pełnotekstowe w silnikach pamięci masowej InnoDB. To świetna wiadomość, ponieważ umożliwia programistom czerpanie korzyści z integralności referencyjnej InnoDB, możliwości wykonywania transakcji i blokad na poziomie wiersza.

Istnieją zasadniczo dwa podejścia do wyszukiwania pełnotekstowego w MySQL:język naturalny i tryb logiczny. (Trzecia opcja rozszerza wyszukiwanie w języku naturalnym o drugie zapytanie rozszerzające).

Główna różnica między trybami naturalnymi i boolowskimi polega na tym, że wartość logiczna dopuszcza niektóre operatory jako część wyszukiwania. Na przykład operatory logiczne mogą być używane, jeśli słowo ma większe znaczenie niż inne w zapytaniu lub jeśli określone słowo powinno być obecne w wynikach itp. Warto zauważyć, że w obu przypadkach wyniki mogą być sortowane według trafności obliczonej przez MySQL podczas wyszukiwania.

Podejmowanie decyzji

Najlepszym rozwiązaniem dla naszego problemu było użycie wyszukiwania pełnotekstowego InnoDb w trybie logicznym. Czemu?

  • Mieliśmy mało czasu na wdrożenie funkcji wyszukiwania.
  • W tym momencie nie mieliśmy dużych zbiorów danych do zgniatania ani ogromnego obciążenia, które wymagałoby czegoś takiego jak Elasticsearch lub Sphinx.
  • Użyliśmy hostingu współdzielonego, który nie obsługuje Elasticsearch ani Sphinx, a sprzęt był na tym etapie dość ograniczony.
  • Chociaż chcieliśmy w naszej funkcji wyszukiwania odwoływać się do słów, nie była to przerwa:mogliśmy to zaimplementować (w ramach ograniczeń) za pomocą prostego kodowania PHP i denormalizacji danych
  • Wyszukiwania pełnotekstowe w trybie boolowskim mogą wyszukiwać słowa za pomocą symboli wieloznacznych (dla rdzenia słów) i sortować wyniki na podstawie trafności.

Wyszukiwanie pełnotekstowe w trybie logicznym

Jak wspomniano wcześniej, wyszukiwanie w języku naturalnym jest najprostszym podejściem:wystarczy wyszukać frazę lub słowo w kolumnach, w których ustawiono indeks pełnotekstowy, a otrzymasz wyniki posortowane według trafności.

W znormalizowanym modelu Vertabelo

Zobaczmy, jak działa proste wyszukiwanie. Najpierw utworzymy przykładową tabelę:

-- Created by Vertabelo (http://vertabelo.com)
-- Last modification date: 2016-04-25 15:01:22.153
-- tables
-- Table: artists
CREATE TABLE artists (
   id int(11) NOT NULL AUTO_INCREMENT,
   name varchar(255) NOT NULL,
   bio text NOT NULL,
   CONSTRAINT artists_pk PRIMARY KEY (id)
) ENGINE InnoDB;
CREATE  FULLTEXT INDEX artists_idx_1 ON artists (name);
-- End of file.

W trybie języka naturalnego

Możesz wstawić kilka przykładowych danych i rozpocząć testowanie. (Byłoby dobrze dodać go do przykładowego zbioru danych.) Na przykład spróbujemy wyszukać Michaela Jacksona:

SELECT 
    *
FROM
    artists
WHERE
    MATCH (artists.name) AGAINST ('Michael Jackson' IN NATURAL LANGUAGE MODE)

To zapytanie znajdzie rekordy pasujące do wyszukiwanych haseł i posortuje pasujące rekordy według trafności; im lepsze dopasowanie, tym bardziej jest ono trafne i im wyższy wynik pojawi się na liście.

W trybie logicznym

Możemy przeprowadzić to samo wyszukiwanie w trybie boolowskim. Jeśli nie zastosujemy żadnych operatorów do naszego zapytania, jedyną różnicą będzie to, że wyniki nie są posortowane według trafności:

SELECT 
    *
FROM
    artists
WHERE
    MATCH (artists.name) AGAINST ('Michael Jackson' IN BOOLEAN MODE)

Operator wieloznaczny w trybie logicznym

Ponieważ chcemy wyszukiwać słowa rdzeniowe i częściowe, będziemy potrzebować operatora symboli wieloznacznych (*). Ten operator może być używany w wyszukiwaniu w trybie logicznym, dlatego wybraliśmy ten tryb.

Uwolnijmy więc moc wyszukiwania logicznego i spróbujmy wyszukać część nazwiska artysty. Użyjemy operatora symboli wieloznacznych, aby dopasować dowolnego artystę, którego nazwa zaczyna się od „Mich”:

SELECT 
    *
FROM
    artists
WHERE
    MATCH (name) AGAINST ('Mich*' IN BOOLEAN MODE)

Sortowanie według trafności w trybie logicznym

Zobaczmy teraz obliczoną trafność wyszukiwania. Pomoże nam to zrozumieć sortowanie, które będziemy robić później z Cake:

SELECT 
    *, MATCH (name) AGAINST ('mich*' IN BOOLEAN MODE) AS rank
FROM
    artists
WHERE
    MATCH (name) AGAINST ('mich*' IN BOOLEAN MODE)
ORDER BY rank DESC

To zapytanie pobiera dopasowania wyszukiwania i wartość trafności, którą MySQL oblicza dla każdego rekordu. Optymalizator silnika wykryje, że wybieramy trafność, więc nie zawraca sobie głowy ponownym obliczaniem rangi.

Pochodzenie słów w wyszukiwaniu pełnotekstowym

Gdy w wyszukiwaniu umieścimy słowo rdzenne, wyszukiwanie staje się bardziej przyjazne dla użytkownika. Nawet jeśli wynik sam w sobie nie jest słowem, algorytmy próbują wygenerować ten sam rdzeń dla słów pochodnych. Na przykład rdzeń „argu” nie jest słowem angielskim, ale może być użyty jako rdzeń dla „argumentować”, „argumentować”, „argumentować”, „argumentować”, „Argus” i innych słów.

Stemming poprawia wyniki, ponieważ użytkownik może wprowadzić słowo, które nie ma dokładnego dopasowania, ale jego „rdzeń” ma. Chociaż stemmer PHP lub program Snowball w Pythonie może być opcją (jeśli masz rootowy dostęp SSH do swojego serwera), użyjemy klasy PorterStemmer.php.

Ta klasa implementuje algorytm zaproponowany przez Martina Portera do macierzystych słów w języku angielskim. Jak stwierdził autor na swojej stronie internetowej, można z niego korzystać w dowolnym celu. Po prostu upuść plik do swojego katalogu Vendors w CakePHP, dołącz bibliotekę do swojego modelu i wywołaj metodę statyczną, aby zakorzenić słowo:

//include the library (should be called PorterStemmer.php) within CakePHP’s Vendors folder
App::import('Vendor', 'PorterStemmer'); 

//stem a word (words must be stemmed one by one)
echo PorterStemmer::Stem(‘stemming’); 
//output will be ‘stem’

Naszym celem jest przyspieszenie i wydajność wyszukiwania oraz umożliwienie sortowania wyników według ich trafności pełnotekstowej. Aby to zrobić, będziemy musieli użyć podstaw słów na dwa sposoby:

  1. Słowa wprowadzone przez użytkownika
  2. Dane związane z utworami (które będziemy przechowywać w kolumnach i sortować według trafności)

Pierwszy rodzaj rdzennych słów można wykonać w ten sposób:

App::import('Vendor', 'PorterStemmer');
$search = trim(preg_replace('/[^A-Za-z0-9_\s]/', '', $search));//remove undesired characters
$words = explode(" ", trim($search));
$stemmedSearch = "";
$unstemmedSearch = "";
foreach ($words as $word) {
   $stemmedSearch .= PorterStemmer::Stem($word) . "* ";//we add the wildcard after each word
   $unstemmedSearch = $word . "* " ;//to search the artist column which is not stemmed
}
$stemmedSearch = trim($stemmedSearch);
$unstemmedSearch = trim($unstemmedSearch);

if ($stemmedSearch == "*" || $unstemmedSearch=="*") {
   //otherwise mySql will complain, as you cannot use the wildcard alone
   $stemmedSearch = "";
   $unstemmedSearch = "";
}

Utworzyliśmy dwa ciągi:jeden do wyszukiwania nazwy wykonawcy (bez rdzenia), a drugi do wyszukiwania w pozostałych kolumnach z motywami. Pomoże nam to później zbudować nasze „przeciw” część zapytania pełnotekstowego. Zobaczmy teraz, jak możemy zatamować i posortować dane utworu.

Denormalizacja danych utworu

Nasze kryteria sortowania będą opierać się na pierwszym dopasowaniu wykonawcy utworu (bez stemmingu). Następnie pojawi się nazwa utworu, album i powiązane kategorie. Steming zostanie użyty we wszystkich drugorzędnych kryteriach wyszukiwania.

Aby to zilustrować, załóżmy, że szukam słowa „nirvana” i jest tam piosenka „Nirvana Games” autorstwa „XYZ” i inna piosenka „Polly” artysty „Nirvana”. Wyniki powinny zawierać najpierw „Polly”, ponieważ dopasowanie nazwy wykonawcy jest ważniejsze niż dopasowanie nazwy utworu (w oparciu o moje kryteria).

W tym celu dodałem 4 pola w songs tabela, po jednym dla każdego z kryteriów wyszukiwania/sortowania, które chcemy:

ALTER TABLE `songs` 
ADD `denorm_artist` VARCHAR(255) NOT NULL AFTER`trackname`, 
ADD `denorm_trackname` VARCHAR(500) NOT NULL AFTER`denorm_artist`, 
ADD `denorm_album` VARCHAR(255) NOT NULL AFTER`denorm_trackname`,
ADD `denorm_categories` VARCHAR(500) NOT NULL AFTER`denorm_album`, 
ADD FULLTEXT (`denorm_artist`), 
ADD FULLTEXT(`denorm_trackname`), 
ADD FULLTEXT (`denorm_album`), 
ADD FULLTEXT(`denorm_categories`);

Nasz kompletny model bazy danych wyglądałby tak:




Za każdym razem, gdy zapisujesz piosenkę za pomocą dodawania/edycji w CakePHP, wystarczy zapisać nazwę wykonawcy w kolumnie denorm_artist bez powstrzymywania. Następnie dodaj tytułową nazwę utworu w denorm_trackname pole (podobne do tego, co zrobiliśmy w wyszukiwanym tekście) i zapisz nazwę albumu z tematem w denorm_album kolumna. Na koniec zapisz ustawioną kategorię tematyczną dla utworu w denorm_categories pola, łącząc słowa i dodając jedną spację między każdą nazwą kategorii.

Pełnotekstowe wyszukiwanie i sortowanie według trafności w CakePHP

Kontynuując przykład wyszukiwania „Nirvana”, zobaczmy, co może osiągnąć podobne zapytanie:

SELECT 
 trackname, 
 MATCH(denorm_artist) AGAINST ('Nirvana*' IN BOOLEAN MODE) as rank1,   
 MATCH(denorm_trackname) AGAINST ('Nirvana*' IN BOOLEAN MODE) as rank2, 
 MATCH(denorm_album) AGAINST ('Nirvana*' IN BOOLEAN MODE) as rank3, 
 MATCH(denorm_categories) AGAINST ('Nirvana*' IN BOOLEAN MODE) as rank4 
FROM songs 
WHERE 
 MATCH(denorm_artist) AGAINST ('Nirvana*' IN BOOLEAN MODE) OR 
 MATCH(denorm_trackname) AGAINST ('Nirvana*' IN BOOLEAN MODE) OR 
 MATCH(denorm_album) AGAINST ('Nirvana*' IN BOOLEAN MODE) OR 
 MATCH(denorm_categories) AGAINST ('Nirvana*' IN BOOLEAN MODE) 
ORDER BY rank1 DESC, rank2 DESC, rank3 DESC, rank4 DESC

Otrzymalibyśmy następujący wynik:

nazwa utworu rank1 rank2 rank3 rank4
Polly 0.0906190574169159 0 0 0
gry nirwany 0 0.0906190574169159 0 0

Aby to zrobić w CakePHP, znajdź metoda musi być wywołana przy użyciu kombinacji parametrów „pól”, „warunków” i „zamówień”. Kontynuując poprzedni przykładowy kod PHP:

//within Song.php model file
 $fields = array(
            "Song.trackname",
            "MATCH(Song.denorm_artist) AGAINST ({$unstemmedSearch} IN BOOLEAN MODE) as `rank1`",
            "MATCH(Song.denorm_trackname) AGAINST ({$stemmedSearch} IN BOOLEAN MODE) as `rank2`",
            "MATCH(Song.denorm_album) AGAINST ({$stemmedSearch} IN BOOLEAN MODE) as `rank3`",
            "MATCH(Song.denorm_categories) AGAINST ({$stemmedSearch} IN BOOLEAN MODE) as `rank4`"
        );

$order = "`rank1` DESC,`rank2` DESC,`rank3` DESC,`rank4` DESC,Song.trackname ASC";

$conditions = array(
            "OR" => array(
                "MATCH(Song.denorm_artist) AGAINST ({$unstemmedSearch} IN BOOLEAN MODE)",
                "MATCH(Song.denorm_trackname) AGAINST ({$stemmedSearch} IN BOOLEAN MODE)",
                "MATCH(Song.denorm_album) AGAINST ({$stemmedSearch} IN BOOLEAN MODE)",
                "MATCH(Song.denorm_categories) AGAINST ({$stemmedSearch} IN BOOLEAN MODE)"
            )
        );

$results = $this->find(‘all’,array(‘conditions’=>$conditions,’fields’=>$fields,’order’=>$order);

$wyniki będzie tablicą utworów posortowanych według kryteriów, które zdefiniowaliśmy wcześniej.

To rozwiązanie może być używane do generowania wyszukiwań, które są znaczące dla użytkownika – bez poświęcania zbyt wiele czasu programistom lub dodawania większej złożoności do kodu.

Sprawianie, że wyszukiwania CakePHP są jeszcze lepsze

Warto wspomnieć, że „doprawienie” zdenormalizowanych kolumn większą ilością danych może prowadzić do lepszych wyników.

Przez „przyprawianie” rozumiem, że możesz uwzględnić w zdenormalizowanych kolumnach więcej danych z dodatkowych kolumn, które uważasz za przydatne, aby wyniki były bardziej trafne, na przykład jeśli wiesz, że kraj artysty może znaleźć się w wyszukiwanych hasłach, może dodać kraj wraz z nazwą wykonawcy w denorm_artist kolumna. Poprawiłoby to jakość wyników wyszukiwania.

Z mojego doświadczenia (w zależności od rzeczywistych danych, których używasz i kolumn, które denormalizujesz) najwyższe wyniki wydają się być naprawdę dokładne. Jest to świetne rozwiązanie w przypadku aplikacji mobilnych, ponieważ przewijanie długiej listy może być frustrujące dla użytkownika.

Wreszcie, jeśli potrzebujesz uzyskać więcej danych z tabel, do których odnosi się utwór, zawsze możesz dołączyć i uzyskać wykonawcę, kategorie, albumy, komentarze do utworów itp. Jeśli używasz filtra zachowań z zawartością CakePHP, chciałbym zasugeruj dodanie wtyczki EagerLoader, aby skutecznie wykonać połączenia.

Jeśli masz własne podejście do implementacji wyszukiwania pełnotekstowego, podziel się nim w komentarzach poniżej. Wszyscy możemy uczyć się od siebie nawzajem.


  1. Database
  2.   
  3. Mysql
  4.   
  5. Oracle
  6.   
  7. Sqlserver
  8.   
  9. PostgreSQL
  10.   
  11. Access
  12.   
  13. SQLite
  14.   
  15. MariaDB
  1. Mysql - usuń z wielu tabel za pomocą jednego zapytania

  2. FORMAT() Przykłady w MySQL

  3. Wiele zapytań PDO

  4. użyj zmiennej dla nazwy tabeli w mysql sproc

  5. Sortowanie z uwzględnieniem wielkości liter w MySQL