MariaDB
 sql >> Base de données >  >> RDS >> MariaDB

Une introduction à la recherche en texte intégral dans MariaDB

Les bases de données sont destinées à stocker et interroger efficacement les données. Le problème est qu'il existe de nombreux types de données différents que nous pouvons stocker :nombres, chaînes, JSON, données géométriques. Les bases de données utilisent différentes méthodes pour stocker différents types de données - structure de table, index. Pas toujours la même façon de stocker et d'interroger les données est efficace pour tous ses types, ce qui rend assez difficile l'utilisation d'une solution unique. Par conséquent, les bases de données essaient d'utiliser différentes approches pour différents types de données. Par exemple, dans MySQL ou MariaDB, nous avons une solution générique et performante comme InnoDB, qui fonctionne bien dans la majorité des cas, mais nous avons également des fonctions distinctes pour travailler avec des données JSON, des index spatiaux séparés pour accélérer l'interrogation des données géométriques ou des index de texte intégral. , aidant avec les données textuelles. Dans ce blog, nous verrons comment MariaDB peut être utilisé pour travailler avec des données en texte intégral.

Les index B+Tree réguliers dans InnoDB peuvent également être utilisés pour accélérer les recherches de données textuelles. Le principal problème est qu'en raison de leur structure et de leur nature, ils ne peuvent aider qu'à rechercher les préfixes les plus à gauche. Il est également coûteux d'indexer de gros volumes de texte (ce qui, étant donné les limites du préfixe le plus à gauche, n'a pas vraiment de sens). Pourquoi? Prenons un exemple simple. Nous avons la phrase suivante :

"Le renard brun rapide saute par-dessus le chien paresseux"

En utilisant des index réguliers dans InnoDB, nous pouvons indexer la phrase complète :

"Le renard brun rapide saute par-dessus le chien paresseux"

Le fait est que lorsque nous recherchons ces données, nous devons rechercher le préfixe le plus à gauche. Donc une requête comme :

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

Bénéficiera de cet index mais une requête comme :

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

Ne fera pas. Il n'y a pas d'entrée dans l'index qui commence par "rapide". Il y a une entrée dans l'index qui contient "quick" mais qui commence par "The", donc elle ne peut pas être utilisée. Par conséquent, il est pratiquement impossible d'interroger efficacement des données textuelles à l'aide d'index B+Tree. Heureusement, MyISAM et InnoDB ont implémenté des index FULLTEXT, qui peuvent être utilisés pour travailler avec des données textuelles sur MariaDB. La syntaxe est légèrement différente de celle des SELECT classiques, voyons ce que nous pouvons en faire. En ce qui concerne les données, nous avons utilisé un fichier d'index aléatoire à partir du vidage de la base de données Wikipedia. La structure des données est la suivante :

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

En conséquence, nous avons créé une table avec deux colonnes BIG INT et une VARCHAR.

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

Ensuite, nous avons chargé les données :

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

Nous avons également créé l'index FULLTEXT. Comme vous pouvez le voir, la syntaxe pour cela est similaire à l'index normal, nous avons juste eu à transmettre les informations sur le type d'index car il est par défaut à B+Tree. Ensuite, nous étions prêts à exécuter quelques requêtes.

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)

Comme vous pouvez le voir, la syntaxe du SELECT est légèrement différente de celle à laquelle nous sommes habitués. Pour la recherche en texte intégral, vous devez utiliser la syntaxe MATCH() … AGAINST (), où dans MATCH() vous passez la colonne ou les colonnes que vous souhaitez rechercher et dans AGAINST() vous passez une liste de mots-clés délimités par des virgules. Vous pouvez voir à partir de la sortie que, par défaut, la recherche est insensible à la casse et qu'elle recherche toute la chaîne, pas seulement le début comme c'est le cas avec les index B+Tree. Comparons à quoi cela ressemblerait si nous ajoutions un index normal sur la colonne 'c3' - les index FULLTEXT et B+Tree peuvent coexister sur la même colonne sans aucun problème. Ce qui serait utilisé est décidé en fonction de la syntaxe 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

Une fois l'index créé, examinons le résultat de la recherche :

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)

Comme vous pouvez le voir, notre requête n'a renvoyé que trois lignes. Ceci est normal car nous recherchons des lignes qui ne commencent que par une chaîne "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)

Lorsque nous vérifions la sortie EXPLAIN, nous pouvons voir que l'index a été utilisé pour rechercher les données. Mais que se passe-t-il si nous voulons rechercher toutes les lignes contenant la chaîne 'Starship', peu importe si c'est au début ou non. Nous devons écrire la requête suivante :

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)

Le résultat correspond à ce que nous avons obtenu de la recherche en texte intégral.

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)

L'EXPLAIN est cependant différent - comme vous pouvez le voir, il utilise toujours l'index, mais cette fois, il effectue une analyse complète de l'index. Cela est possible car nous avons indexé la colonne c3 complète afin que toutes les données soient disponibles dans l'index. L'analyse de l'index entraînera des lectures aléatoires de la table, mais pour une table aussi petite, MariaDB l'a décidé plus efficace que la lecture de la table entière. Veuillez noter le temps d'exécution :0,084 s pour notre SELECT habituel. Comparé à une requête en texte intégral, c'est mauvais :

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)

Comme vous pouvez le voir, la requête qui utilise l'index FULLTEXT a pris 0,001 seconde pour s'exécuter. Nous parlons ici de différences d'ordres de grandeur.

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)

Voici à quoi ressemble la sortie EXPLAIN pour la requête utilisant l'index FULLTEXT - ce fait est indiqué par le type :fulltext.

Les requêtes en texte intégral ont également d'autres fonctionnalités. Il est possible, par exemple, de renvoyer des lignes qui pourraient être pertinentes pour le terme de recherche. MariaDB recherche les mots situés près de la ligne que vous recherchez, puis lance également une recherche pour eux.

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)

Dans notre cas, le mot « Starship » peut être lié à des mots comme « Troopers », « class », « Star Trek », « Hospital », etc. Pour utiliser cette fonctionnalité, nous devons exécuter la requête avec le modificateur « 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)

La sortie contenait un grand nombre de lignes, mais cet exemple est suffisant pour voir comment cela fonctionne. La requête a renvoyé des lignes telles que :

« Troopers Drum and Bugle Corps »

"Bataillez les soldats Android"

Celles-ci sont basées sur la recherche du mot « Troopers ». Il a également renvoyé des lignes avec des chaînes telles que :

"Témoignages Star Trek"

« Qui pleure Adonaïs ? (Star Trek)"

Qui, évidemment, sont basés sur la recherche du mot "Start Trek".

Si vous avez besoin de plus de contrôle sur le terme que vous souhaitez rechercher, vous pouvez utiliser "EN MODE BOOLÉEN". Il permet d'utiliser des opérateurs supplémentaires. La liste complète se trouve dans la documentation, nous ne montrerons que quelques exemples.

Supposons que nous souhaitions rechercher non seulement le mot "Star", mais également d'autres mots commençant par la chaîne "Star" :

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)

Comme vous pouvez le voir, dans la sortie, nous avons des lignes contenant des chaînes telles que "Stars", "Starfish" ou "starch".

Un autre cas d'utilisation du mode BOOLÉEN. Supposons que nous souhaitions rechercher des lignes pertinentes pour la Chambre des représentants de Pennsylvanie. Si nous exécutons une requête régulière, nous obtiendrons des résultats liés d'une manière ou d'une autre à l'une de ces chaînes :

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)

Comme vous pouvez le voir, nous avons trouvé des données utiles, mais nous avons également trouvé des données qui ne sont absolument pas pertinentes pour notre recherche. Heureusement, nous pouvons affiner cette requête :

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)

Comme vous pouvez le voir, en ajoutant l'opérateur "+", nous avons clairement indiqué que nous ne nous intéressons qu'à la sortie où le mot donné existe. Par conséquent, les données que nous avons obtenues en réponse correspondent exactement à ce que nous recherchions.

Nous pouvons également exclure des mots de la recherche. Disons que nous recherchons des objets volants mais que nos résultats de recherche sont contaminés par différents animaux volants qui ne nous intéressent pas. Nous pouvons facilement nous débarrasser des renards, des écureuils et des grenouilles :

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)

La dernière fonctionnalité que nous aimerions montrer est la possibilité de rechercher le devis exact :

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)

Comme vous pouvez le constater, la recherche en texte intégral dans MariaDB fonctionne assez bien, elle est également plus rapide et plus flexible que la recherche à l'aide d'index B+Tree. Veuillez cependant garder à l'esprit qu'il ne s'agit en aucun cas d'un moyen de traiter de gros volumes de données - avec la croissance des données, la faisabilité de cette solution diminuera. Pourtant, pour les petits ensembles de données, cette solution est parfaitement valable. Cela peut certainement vous faire gagner plus de temps pour, à terme, mettre en œuvre des solutions de recherche en texte intégral dédiées comme Sphinx ou Lucene. Bien sûr, toutes les fonctionnalités que nous avons décrites sont disponibles dans les clusters MariaDB déployés à partir de ClusterControl.