MariaDB
 sql >> Baza danych >  >> RDS >> MariaDB

Wprowadzenie do wyszukiwania pełnotekstowego w MariaDB

Bazy danych są przeznaczone do wydajnego przechowywania i wykonywania zapytań o dane. Problem polega na tym, że istnieje wiele różnych typów danych, które możemy przechowywać:liczby, łańcuchy, JSON, dane geometryczne. Bazy danych wykorzystują różne metody do przechowywania różnych typów danych - struktury tabel, indeksów. Nie zawsze ten sam sposób przechowywania i odpytywania danych jest wydajny dla wszystkich ich typów, co sprawia, że ​​korzystanie z uniwersalnego rozwiązania jest dość trudne. W rezultacie bazy danych próbują stosować różne podejścia do różnych typów danych. Na przykład w MySQL lub MariaDB mamy ogólne, dobrze działające rozwiązanie, takie jak InnoDB, które w większości przypadków działa dobrze, ale mamy też oddzielne funkcje do pracy z danymi JSON, oddzielne indeksy przestrzenne, aby przyspieszyć zapytania o dane geometryczne lub indeksy pełnotekstowe , pomagając z danymi tekstowymi. W tym blogu przyjrzymy się, jak MariaDB może być używana do pracy z danymi pełnotekstowymi.

Zwykłe indeksy B+Tree w InnoDB można również wykorzystać do przyspieszenia wyszukiwania danych tekstowych. Główny problem polega na tym, że ze względu na swoją strukturę i charakter mogą one pomóc jedynie w wyszukiwaniu przedrostków najbardziej po lewej stronie. Kosztowne jest również indeksowanie dużych ilości tekstu (co, biorąc pod uwagę ograniczenia lewego przedrostka, nie ma sensu). Czemu? Rzućmy okiem na prosty przykład. Mamy następujące zdanie:

„Szybki brązowy lis przeskakuje nad leniwym psem”

Używając zwykłych indeksów w InnoDB możemy zindeksować całe zdanie:

„Szybki brązowy lis przeskakuje nad leniwym psem”

Chodzi o to, że szukając tych danych, musimy wyszukać pełny lewy prefiks. Więc zapytanie takie jak:

SELECT text FROM mytable WHERE sentence LIKE “The quick brown fox jumps”;

Skorzystaj z tego indeksu, ale zapytanie takie jak:

SELECT text FROM mytable WHERE sentence LIKE “quick brown fox jumps”;

Nie będzie. W indeksie nie ma wpisu rozpoczynającego się od „szybko”. W indeksie znajduje się wpis, który zawiera „szybkie”, ale zaczyna się od „The”, dlatego nie można go użyć. W rezultacie praktycznie niemożliwe jest efektywne wyszukiwanie danych tekstowych przy użyciu indeksów B+Tree. Na szczęście zarówno MyISAM, jak i InnoDB zaimplementowały indeksy FULLTEXT, które można wykorzystać do rzeczywistej pracy z danymi tekstowymi w MariaDB. Składnia jest nieco inna niż w przypadku zwykłych SELECTów, przyjrzyjmy się, co możemy z nimi zrobić. Jeśli chodzi o dane, użyliśmy losowego pliku indeksu ze zrzutu bazy danych Wikipedii. Struktura danych jest następująca:

617:11539268:Arthur Hamerschlag
617:11539269:Rooster Cogburn (character)
617:11539275:Membership function
617:11539282:Secondarily Generalized Tonic-Clonic Seizures
617:11539283:Corporate Challenge
617:11539285:Perimeter Mall
617:11539286:1994 St. Louis Cardinals season

W rezultacie stworzyliśmy tabelę z dwiema kolumnami BIG INT i jednym VARCHAR.

MariaDB [(none)]> CREATE TABLE ft_data.ft_table (c1 BIGINT, c2 BIGINT, c3 VARCHAR, PRIMARY KEY (c1, c2);

Następnie załadowaliśmy dane:

MariaDB [ft_data]> LOAD DATA INFILE '/vagrant/enwiki-20190620-pages-articles-multistream-index17.txt-p11539268p13039268' IGNORE INTO  TABLE ft_table COLUMNS TERMINATED BY ':';
MariaDB [ft_data]> ALTER TABLE ft_table ADD FULLTEXT INDEX idx_ft (c3);
Query OK, 0 rows affected (5.497 sec)
Records: 0  Duplicates: 0  Warnings: 0

Stworzyliśmy również indeks FULLTEXT. Jak widać, składnia tego jest podobna do zwykłego indeksu, musieliśmy tylko przekazać informacje o typie indeksu, ponieważ domyślnie jest to B+Drzewo. Następnie byliśmy gotowi do uruchomienia kilku zapytań.

MariaDB [ft_data]> SELECT * FROM ft_data.ft_table WHERE MATCH(c3) AGAINST ('Starship');
+-----------+----------+------------------------------------+
| c1        | c2       | c3                                 |
+-----------+----------+------------------------------------+
| 119794610 | 12007923 | Starship Troopers 3                |
| 250627749 | 12479782 | Miranda class starship (Star Trek) |
| 250971304 | 12481409 | Starship Hospital                  |
| 253430758 | 12489743 | Starship Children's Hospital       |
+-----------+----------+------------------------------------+
4 rows in set (0.009 sec)

Jak widać, składnia SELECT jest nieco inna niż ta, do której jesteśmy przyzwyczajeni. Do wyszukiwania pełnotekstowego powinieneś użyć składni MATCH() … AGAINST(), gdzie w MATCH() podajesz kolumnę lub kolumny, które chcesz przeszukać, a w AGAINST() podajesz oddzieloną przecinkami listę słów kluczowych. Z danych wyjściowych widać, że domyślnie wyszukiwanie nie uwzględnia wielkości liter i przeszukuje cały ciąg, a nie tylko początek, jak to ma miejsce w przypadku indeksów B+Tree. Porównajmy więc, jak by to wyglądało, gdybyśmy dodali normalny indeks na kolumnie „c3” - indeksy FULLTEXT i B+Tree mogą bez problemu współistnieć na tej samej kolumnie. Który zostanie użyty, jest określany na podstawie składni SELECT.

MariaDB [ft_data]> ALTER TABLE ft_data.ft_table ADD INDEX idx_c3 (c3);
Query OK, 0 rows affected (1.884 sec)
Records: 0  Duplicates: 0  Warnings: 0

Po utworzeniu indeksu spójrzmy na wyniki wyszukiwania:

MariaDB [ft_data]> SELECT * FROM ft_data.ft_table WHERE c3 LIKE 'Starship%';
+-----------+----------+------------------------------+
| c1        | c2       | c3                           |
+-----------+----------+------------------------------+
| 253430758 | 12489743 | Starship Children's Hospital |
| 250971304 | 12481409 | Starship Hospital            |
| 119794610 | 12007923 | Starship Troopers 3          |
+-----------+----------+------------------------------+
3 rows in set (0.001 sec)

Jak widać, nasze zapytanie zwróciło tylko trzy wiersze. Jest to oczekiwane, ponieważ szukamy wierszy, które zaczynają się tylko od ciągu „Statek kosmiczny”.

MariaDB [ft_data]> EXPLAIN SELECT * FROM ft_data.ft_table WHERE c3 LIKE 'Starship%'\G
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: ft_table
         type: range
possible_keys: idx_c3,idx_ft
          key: idx_c3
      key_len: 103
          ref: NULL
         rows: 3
        Extra: Using where; Using index
1 row in set (0.000 sec)

Kiedy sprawdzamy wyjście EXPLAIN, widzimy, że indeks został użyty do wyszukania danych. Ale co, jeśli chcemy wyszukać wszystkie wiersze zawierające ciąg „Starship”, bez względu na to, czy jest na początku, czy nie. Musimy napisać następujące zapytanie:

MariaDB [ft_data]> SELECT * FROM ft_data.ft_table WHERE c3 LIKE '%Starship%';
+-----------+----------+------------------------------------+
| c1        | c2       | c3                                 |
+-----------+----------+------------------------------------+
| 250627749 | 12479782 | Miranda class starship (Star Trek) |
| 253430758 | 12489743 | Starship Children's Hospital       |
| 250971304 | 12481409 | Starship Hospital                  |
| 119794610 | 12007923 | Starship Troopers 3                |
+-----------+----------+------------------------------------+
4 rows in set (0.084 sec)

Wynik jest zgodny z tym, co otrzymaliśmy z wyszukiwania pełnotekstowego.

MariaDB [ft_data]> EXPLAIN SELECT * FROM ft_data.ft_table WHERE c3 LIKE '%Starship%'\G
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: ft_table
         type: index
possible_keys: NULL
          key: idx_c3
      key_len: 103
          ref: NULL
         rows: 473367
        Extra: Using where; Using index
1 row in set (0.000 sec)

EXPLAIN jest jednak inny - jak widać nadal używa indeksu, ale tym razem wykonuje pełne skanowanie indeksu. Jest to możliwe, ponieważ zaindeksowaliśmy pełną kolumnę c3, dzięki czemu wszystkie dane są dostępne w indeksie. Skanowanie indeksu spowoduje losowe odczyty z tabeli, ale dla tak małej tabeli MariaDB uznała, że ​​jest to bardziej wydajne niż czytanie całej tabeli. Zwróć uwagę na czas wykonania:0,084 s dla naszego zwykłego SELECT. Porównując to z zapytaniem pełnotekstowym, jest to złe:

MariaDB [ft_data]> SELECT * FROM ft_data.ft_table WHERE MATCH(c3) AGAINST ('Starship');
+-----------+----------+------------------------------------+
| c1        | c2       | c3                                 |
+-----------+----------+------------------------------------+
| 119794610 | 12007923 | Starship Troopers 3                |
| 250627749 | 12479782 | Miranda class starship (Star Trek) |
| 250971304 | 12481409 | Starship Hospital                  |
| 253430758 | 12489743 | Starship Children's Hospital       |
+-----------+----------+------------------------------------+
4 rows in set (0.001 sec)

Jak widać, wykonanie zapytania korzystającego z indeksu FULLTEXT zajęło 0,001s. Mówimy tutaj o różnicach rzędu wielkości.

MariaDB [ft_data]> EXPLAIN SELECT * FROM ft_data.ft_table WHERE MATCH(c3) AGAINST ('Starship')\G
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: ft_table
         type: fulltext
possible_keys: idx_ft
          key: idx_ft
      key_len: 0
          ref:
         rows: 1
        Extra: Using where
1 row in set (0.000 sec)

Oto jak wygląda wynik EXPLAIN dla zapytania korzystającego z indeksu FULLTEXT - na ten fakt wskazuje typ:fulltext.

Zapytania pełnotekstowe mają również kilka innych funkcji. Możliwe jest na przykład zwrócenie wierszy, które mogą być istotne dla wyszukiwanego hasła. MariaDB szuka słów znajdujących się w pobliżu wyszukiwanego wiersza, a następnie uruchamia wyszukiwanie również dla nich.

MariaDB [(none)]> SELECT * FROM ft_data.ft_table WHERE MATCH(c3) AGAINST ('Starship');
+-----------+----------+------------------------------------+
| c1        | c2       | c3                                 |
+-----------+----------+------------------------------------+
| 119794610 | 12007923 | Starship Troopers 3                |
| 250627749 | 12479782 | Miranda class starship (Star Trek) |
| 250971304 | 12481409 | Starship Hospital                  |
| 253430758 | 12489743 | Starship Children's Hospital       |
+-----------+----------+------------------------------------+
4 rows in set (0.001 sec)

W naszym przypadku słowo 'Starship' może być powiązane ze słowami takimi jak 'Żołnierze', 'klasa', 'Star Trek', 'Szpital' itp. Aby skorzystać z tej funkcji, należy uruchomić zapytanie z modyfikatorem „Z ROZSZERZENIEM ZAPYTANIA”:

MariaDB [(none)]> SELECT * FROM ft_data.ft_table WHERE MATCH(c3) AGAINST ('Starship' WITH QUERY EXPANSION) LIMIT 10;
+-----------+----------+-------------------------------------+
| c1        | c2       | c3                                  |
+-----------+----------+-------------------------------------+
| 250627749 | 12479782 | Miranda class starship (Star Trek)  |
| 119794610 | 12007923 | Starship Troopers 3                 |
| 253430758 | 12489743 | Starship Children's Hospital        |
| 250971304 | 12481409 | Starship Hospital                   |
| 277700214 | 12573467 | Star ship troopers                  |
|  86748633 | 11886457 | Troopers Drum and Bugle Corps       |
| 255120817 | 12495666 | Casper Troopers                     |
| 396408580 | 13014545 | Battle Android Troopers             |
|  12453401 | 11585248 | Star trek tos                       |
|  21380240 | 11622781 | Who Mourns for Adonais? (Star Trek) |
+-----------+----------+-------------------------------------+
10 rows in set (0.002 sec)

Dane wyjściowe zawierały dużą liczbę wierszy, ale ten przykład wystarczy, aby zobaczyć, jak to działa. Zapytanie zwróciło wiersze takie jak:

„Troopers Drum and Bugle Corps”

„Walcz z żołnierzami Androida”

Opierają się one na wyszukiwaniu słowa „Żołnierze”. Zwracał również wiersze z ciągami takimi jak:

„Star trek tos”

„Kto opłakuje Adonaisa? (Star Trek)”

Które, oczywiście, są oparte na wyszukiwaniu słowa „Rozpocznij wędrówkę”.

Jeśli potrzebujesz większej kontroli nad terminem, który chcesz wyszukać, możesz użyć „W TRYBIE BOOLEAN”. Pozwala na użycie dodatkowych operatorów. Pełna lista znajduje się w dokumentacji, pokażemy tylko kilka przykładów.

Załóżmy, że chcemy wyszukać nie tylko słowo „Gwiazda”, ale także inne słowa, które zaczynają się od ciągu „Gwiazda”:

MariaDB [(none)]> SELECT * FROM ft_data.ft_table WHERE MATCH(c3) AGAINST ('Star*' IN BOOLEAN MODE) LIMIT 10;
+----------+----------+---------------------------------------------------+
| c1       | c2       | c3                                                |
+----------+----------+---------------------------------------------------+
| 20014704 | 11614055 | Ringo Starr and His third All-Starr Band-Volume 1 |
|   154810 | 11539775 | Rough blazing star                                |
|   154810 | 11539787 | Great blazing star                                |
|   234851 | 11540119 | Mary Star of the Sea High School                  |
|   325782 | 11540427 | HMS Starfish (19S)                                |
|   598616 | 11541589 | Dwarf (star)                                      |
|  1951655 | 11545092 | Yellow starthistle                                |
|  2963775 | 11548654 | Hydrogenated starch hydrolysates                  |
|  3248823 | 11549445 | Starbooty                                         |
|  3993625 | 11553042 | Harvest of Stars                                  |
+----------+----------+---------------------------------------------------+
10 rows in set (0.001 sec)

Jak widać, na wyjściu mamy wiersze zawierające ciągi takie jak „Gwiazdy”, „Rozgwiazda” lub „Skrobia”.

Kolejny przypadek użycia trybu BOOLEAN. Załóżmy, że chcemy wyszukać wiersze, które są istotne dla Izby Reprezentantów w Pensylwanii. Jeśli uruchomimy zwykłe zapytanie, otrzymamy wyniki w jakiś sposób powiązane z dowolnym z tych ciągów:

MariaDB [ft_data]> SELECT COUNT(*) FROM ft_data.ft_table WHERE MATCH(c3) AGAINST ('House, Representatives, Pennsylvania');
+----------+
| COUNT(*) |
+----------+
|     1529 |
+----------+
1 row in set (0.005 sec)
MariaDB [ft_data]> SELECT * FROM ft_data.ft_table WHERE MATCH(c3) AGAINST ('House, Representatives, Pennsylvania') LIMIT 20;
+-----------+----------+--------------------------------------------------------------------------+
| c1        | c2       | c3                                                                       |
+-----------+----------+--------------------------------------------------------------------------+
| 198783294 | 12289308 | Pennsylvania House of Representatives, District 175                      |
| 236302417 | 12427322 | Pennsylvania House of Representatives, District 156                      |
| 236373831 | 12427423 | Pennsylvania House of Representatives, District 158                      |
| 282031847 | 12588702 | Pennsylvania House of Representatives, District 47                       |
| 282031847 | 12588772 | Pennsylvania House of Representatives, District 196                      |
| 282031847 | 12588864 | Pennsylvania House of Representatives, District 92                       |
| 282031847 | 12588900 | Pennsylvania House of Representatives, District 93                       |
| 282031847 | 12588904 | Pennsylvania House of Representatives, District 94                       |
| 282031847 | 12588909 | Pennsylvania House of Representatives, District 193                      |
| 303827502 | 12671054 | Pennsylvania House of Representatives, District 55                       |
| 303827502 | 12671089 | Pennsylvania House of Representatives, District 64                       |
| 337545922 | 12797838 | Pennsylvania House of Representatives, District 95                       |
| 219202000 | 12366957 | United States House of Representatives House Resolution 121              |
| 277521229 | 12572732 | United States House of Representatives proposed House Resolution 121     |
|  20923615 | 11618759 | Special elections to the United States House of Representatives          |
|  20923615 | 11618772 | List of Special elections to the United States House of Representatives  |
|  37794558 | 11693157 | Nebraska House of Representatives                                        |
|  39430531 | 11699551 | Belgian House of Representatives                                         |
|  53779065 | 11756435 | List of United States House of Representatives elections in North Dakota |
|  54048114 | 11757334 | 2008 United States House of Representatives election in North Dakota     |
+-----------+----------+--------------------------------------------------------------------------+
20 rows in set (0.003 sec)

Jak widać, znaleźliśmy kilka przydatnych danych, ale znaleźliśmy również dane, które są całkowicie nieistotne dla naszego wyszukiwania. Na szczęście możemy doprecyzować takie zapytanie:

MariaDB [ft_data]> SELECT * FROM ft_data.ft_table WHERE MATCH(c3) AGAINST ('+House, +Representatives, +Pennsylvania' IN BOOLEAN MODE);
+-----------+----------+-----------------------------------------------------+
| c1        | c2       | c3                                                  |
+-----------+----------+-----------------------------------------------------+
| 198783294 | 12289308 | Pennsylvania House of Representatives, District 175 |
| 236302417 | 12427322 | Pennsylvania House of Representatives, District 156 |
| 236373831 | 12427423 | Pennsylvania House of Representatives, District 158 |
| 282031847 | 12588702 | Pennsylvania House of Representatives, District 47  |
| 282031847 | 12588772 | Pennsylvania House of Representatives, District 196 |
| 282031847 | 12588864 | Pennsylvania House of Representatives, District 92  |
| 282031847 | 12588900 | Pennsylvania House of Representatives, District 93  |
| 282031847 | 12588904 | Pennsylvania House of Representatives, District 94  |
| 282031847 | 12588909 | Pennsylvania House of Representatives, District 193 |
| 303827502 | 12671054 | Pennsylvania House of Representatives, District 55  |
| 303827502 | 12671089 | Pennsylvania House of Representatives, District 64  |
| 337545922 | 12797838 | Pennsylvania House of Representatives, District 95  |
+-----------+----------+-----------------------------------------------------+
12 rows in set (0.001 sec)

Jak widać, dodając operator „+” jasno zaznaczyliśmy, że interesują nas tylko dane wyjściowe, w których istnieje dane słowo. W rezultacie dane, które otrzymaliśmy w odpowiedzi, są dokładnie tym, czego szukaliśmy.

Możemy również wykluczyć słowa z wyszukiwania. Powiedzmy, że szukamy latających rzeczy, ale nasze wyniki wyszukiwania są skażone różnymi latającymi zwierzętami, którymi nie jesteśmy zainteresowani. Z łatwością możemy się pozbyć lisów, wiewiórek i żab:

MariaDB [ft_data]> SELECT * FROM ft_data.ft_table WHERE MATCH(c3) AGAINST ('+flying -fox* -squirrel* -frog*' IN BOOLEAN MODE) LIMIT 10;
+----------+----------+-----------------------------------------------------+
| c1       | c2       | c3                                                  |
+----------+----------+-----------------------------------------------------+
| 13340153 | 11587884 | List of surviving Boeing B-17 Flying Fortresses     |
| 16774061 | 11600031 | Flying Dutchman Funicular                           |
| 23137426 | 11631421 | 80th Flying Training Wing                           |
| 26477490 | 11646247 | Kites and Kite Flying                               |
| 28568750 | 11655638 | Fear of Flying                                      |
| 28752660 | 11656721 | Flying Machine (song)                               |
| 31375047 | 11666654 | Flying Dutchman (train)                             |
| 32726276 | 11672784 | Flying Wazuma                                       |
| 47115925 | 11728593 | The Flying Locked Room! Kudou Shinichi's First Case |
| 64330511 | 11796326 | The Church of the Flying Spaghetti Monster          |
+----------+----------+-----------------------------------------------------+
10 rows in set (0.001 sec)

Ostatnią funkcją, którą chcielibyśmy pokazać, jest możliwość wyszukania dokładnego cytatu:

MariaDB [ft_data]> SELECT * FROM ft_data.ft_table WHERE MATCH(c3) AGAINST ('"People\'s Republic of China"' IN BOOLEAN MODE) LIMIT 10;
+-----------+----------+------------------------------------------------------------------------------------------------------+
| c1        | c2       | c3                                                                                                   |
+-----------+----------+------------------------------------------------------------------------------------------------------+
|  12093896 | 11583713 | Religion in the People's Republic of China                                                           |
|  25280224 | 11640533 | Political rankings in the People's Republic of China                                                 |
|  43930887 | 11716084 | Cuisine of the People's Republic of China                                                            |
|  62272294 | 11789886 | Office of the Commissioner of the Ministry of Foreign Affairs of the People's Republic of China in t |
|  70970904 | 11824702 | Scouting in the People's Republic of China                                                           |
| 154301063 | 12145003 | Tibetan culture under the People's Republic of China                                                 |
| 167640800 | 12189851 | Product safety in the People's Republic of China                                                     |
| 172735782 | 12208560 | Agriculture in the people's republic of china                                                        |
| 176185516 | 12221117 | Special Economic Zone of the People's Republic of China                                              |
| 197034766 | 12282071 | People's Republic of China and the United Nations                                                    |
+-----------+----------+------------------------------------------------------------------------------------------------------+
10 rows in set (0.001 sec)

Jak widać, wyszukiwanie pełnotekstowe w MariaDB działa całkiem dobrze, jest też szybsze i bardziej elastyczne niż wyszukiwanie za pomocą indeksów B+Tree. Należy jednak pamiętać, że nie jest to sposób na obsługę dużych wolumenów danych - wraz ze wzrostem ilości danych wykonalność tego rozwiązania będzie się zmniejszać. Jednak dla małych zbiorów danych to rozwiązanie jest jak najbardziej trafne. To zdecydowanie może dać Ci więcej czasu na wdrożenie dedykowanych rozwiązań wyszukiwania pełnotekstowego, takich jak Sphinx lub Lucene. Oczywiście wszystkie opisane przez nas funkcje są dostępne w klastrach MariaDB wdrożonych z ClusterControl.


  1. Database
  2.   
  3. Mysql
  4.   
  5. Oracle
  6.   
  7. Sqlserver
  8.   
  9. PostgreSQL
  10.   
  11. Access
  12.   
  13. SQLite
  14.   
  15. MariaDB
  1. Jak działa UPPER() w MariaDB

  2. Przygotowanie serwera MySQL lub MariaDB do produkcji — część druga

  3. Jak działa UPDATEXML() w MariaDB

  4. ClusterControl — Zaawansowane zarządzanie kopiami zapasowymi — mariabackup Część III

  5. MariaDB SESSION_USER() Objaśnienie