Базите данни са предназначени за ефективно съхраняване и запитване на данни. Проблемът е, че има много различни типове данни, които можем да съхраняваме:числа, низове, JSON, геометрични данни. Базите данни използват различни методи за съхраняване на различни типове данни – структура на таблици, индекси. Не винаги един и същи начин за съхранение и запитване на данните е ефективен за всичките им типове, което прави доста трудно използването на универсално решение. В резултат на това базите данни се опитват да използват различни подходи за различни типове данни. Например в MySQL или MariaDB имаме общо, добре работещо решение като InnoDB, което работи добре в повечето случаи, но също така имаме отделни функции за работа с JSON данни, отделни пространствени индекси за ускоряване на заявките за геометрични данни или пълнотекстови индекси , помагайки с текстови данни. В този блог ще разгледаме как MariaDB може да се използва за работа с пълнотекстови данни.
Редовните B+Tree индекси в InnoDB също могат да се използват за ускоряване на търсенето на текстови данни. Основният проблем е, че поради тяхната структура и естество те могат да помогнат само при търсене на най-левите префикси. Също така е скъпо да се индексират големи обеми текст (което, като се имат предвид ограниченията на най-левия префикс, всъщност няма смисъл). Защо? Нека да разгледаме един прост пример. Имаме следното изречение:
„Бързата кафява лисица прескача мързеливото куче“
Използвайки обикновени индекси в InnoDB, можем да индексираме цялото изречение:
„Бързата кафява лисица прескача мързеливото куче“
Въпросът е, че когато търсим тези данни, трябва да търсим целия ляв префикс. Така че заявка като:
SELECT text FROM mytable WHERE sentence LIKE “The quick brown fox jumps”;
Ще се възползва от този индекс, но заявка като:
SELECT text FROM mytable WHERE sentence LIKE “quick brown fox jumps”;
Няма да. В индекса няма запис, който да започва от „бързо“. В индекса има запис, който съдържа „бързо“, но започва от „The“, следователно не може да се използва. В резултат на това е практически невъзможно ефективно да се заявят текстови данни с помощта на B+Tree индекси. За щастие и MyISAM, и InnoDB са внедрили индекси FULLTEXT, които могат да се използват за действителна работа с текстови данни в MariaDB. Синтаксисът е малко по-различен от обикновените SELECT, нека да разгледаме какво можем да правим с тях. Що се отнася до данните, използвахме произволен индексен файл от дъмпа на базата данни на Wikipedia. Структурата на данните е както следва:
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
В резултат на това създадохме таблица с две BIG INT колони и една VARCHAR.
MariaDB [(none)]> CREATE TABLE ft_data.ft_table (c1 BIGINT, c2 BIGINT, c3 VARCHAR, PRIMARY KEY (c1, c2);
След това заредихме данните:
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
Създадохме и индекса FULLTEXT. Както можете да видите, синтаксисът за това е подобен на обикновения индекс, просто трябваше да предадем информацията за типа индекс, тъй като по подразбиране е B+Tree. Тогава бяхме готови да изпълним някои заявки.
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)
Както можете да видите, синтаксисът за SELECT е малко по-различен от този, с който сме свикнали. За пълнотекстово търсене трябва да използвате синтаксис MATCH() ... AGAINST (), където в MATCH() предавате колоната или колоните, които искате да търсите, а в AGAINST() предавате разделен със запетая списък с ключови думи. Можете да видите от изхода, че по подразбиране търсенето не е чувствително към малки и големи букви и търси целия низ, а не само началото, както е с индексите B+Tree. Нека сравним как ще изглежда, ако добавим нормален индекс към колоната „c3“ – индексите FULLTEXT и B+Tree могат да съществуват съвместно в една и съща колона без никакви проблеми. Кое ще се използва се решава въз основа на синтаксиса 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
След като индексът е създаден, нека да разгледаме резултатите от търсенето:
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)
Както можете да видите, нашата заявка върна само три реда. Това се очаква, тъй като търсим редове, които започват само с низ „Starship“.
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)
Когато проверим изхода EXPLAIN, можем да видим, че индексът е използван за търсене на данните. Но какво ще стане, ако искаме да търсим всички редове, които съдържат низа „Starship“, без значение дали е в началото или не. Трябва да напишем следната заявка:
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)
Резултатът съответства на това, което получихме от пълнотекстово търсене.
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 обаче е различен - както можете да видите, той все още използва индекс, но този път прави пълно сканиране на индекса. Това е възможно, тъй като индексирахме пълна колона c3, така че всички данни да са налични в индекса. Индексното сканиране ще доведе до произволни четения от таблицата, но за такава малка таблица MariaDB реши, че е по-ефективно от четенето на цялата таблица. Моля, обърнете внимание на времето за изпълнение:0,084s за нашия редовен SELECT. Сравнявайки това с пълнотекстова заявка, това е лошо:
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)
Както можете да видите, изпълнението на заявка, която използва индекс FULLTEXT, отне 0.001s. Тук говорим за разлики в порядък.
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)
Ето как изглежда изходът EXPLAIN за заявката, използваща индекс FULLTEXT – този факт се обозначава с тип:пълен текст.
Пълнотекстовите заявки имат и някои други функции. Възможно е например да се върнат редове, които биха могли да са подходящи за думата за търсене. MariaDB търси думи, разположени близо до реда, който търсите, и след това стартира търсене и за тях.
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)
В нашия случай думата „Starship“ може да бъде свързана с думи като „Troopers“, „class“, „Star Trek“, „Hospital“ и т.н. За да използваме тази функция, трябва да изпълним заявката с модификатор „WITH QUERY EXPANSION“:
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)
Резултатът съдържа голям брой редове, но тази извадка е достатъчна, за да видите как работи. Заявката върна редове като:
„Войнически барабанен корпус“
„Battle Android Troopers“
Те се основават на търсенето на думата „войници“. Той също така връща редове с низове като:
„Звезден път“
„Кой скърби за Адонаис? (Стар Трек)”
Които очевидно се основават на търсенето на думата „Start Trek“.
Ако имате нужда от повече контрол върху термина, който искате да търсите, можете да използвате „В БУЛЕВ РЕЖИМ“. Позволява използването на допълнителни оператори. Пълният списък е в документацията, ще покажем само няколко примера.
Да кажем, че искаме да търсим не само думата „звезда“, но и други думи, които започват с низа „звезда“:
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)
Както можете да видите, в изхода имаме редове, които съдържат низове като „Звезди“, „Морска звезда“ или „нишесте“.
Друг случай на използване на режима BOOLEAN. Да кажем, че искаме да търсим редове, които са от значение за Камарата на представителите в Пенсилвания. Ако изпълняваме редовна заявка, ще получим резултати, свързани по някакъв начин с някой от тези низове:
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)
Както можете да видите, намерихме някои полезни данни, но също така намерихме данни, които напълно не са подходящи за нашето търсене. За щастие можем да прецизираме такава заявка:
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)
Както можете да видите, с добавянето на оператор „+“ ясно посочихме, че се интересуваме само от изхода, където съществува дадена дума. В резултат на това данните, които получихме в отговор, са точно това, което търсихме.
Можем също да изключим думи от търсенето. Да кажем, че търсим летящи неща, но резултатите от търсенето ни са заразени от различни летящи животни, които не ни интересуват. Лесно можем да се отървем от лисици, катерици и жаби:
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)
Последната характеристика, която бихме искали да покажем, е възможността за търсене на точния цитат:
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)
Както можете да видите, пълнотекстово търсене в MariaDB работи доста добре, освен това е по-бързо и по-гъвкаво от търсенето с B+Tree индекси. Моля, имайте предвид обаче, че това в никакъв случай не е начин за боравене с големи обеми данни - с нарастването на данните осъществимостта на това решение ще намалее. Все пак, за малките набори от данни това решение е напълно валидно. Определено може да ви спечели повече време, за да приложите в крайна сметка специални решения за пълнотекстово търсене като Sphinx или Lucene. Разбира се, всички функции, които описахме, са налични в MariaDB клъстери, разгърнати от ClusterControl.