MariaDB
 sql >> Datenbank >  >> RDS >> MariaDB

Eine Einführung in die Volltextsuche in MariaDB

Datenbanken sollen Daten effizient speichern und abfragen. Das Problem ist, dass wir viele verschiedene Arten von Daten speichern können:Zahlen, Strings, JSON, geometrische Daten. Datenbanken verwenden verschiedene Methoden, um verschiedene Datentypen zu speichern - Tabellenstruktur, Indizes. Nicht immer ist die gleiche Art des Speicherns und Abfragens der Daten für alle Typen effizient, was es ziemlich schwierig macht, eine One-fits-all-Lösung zu verwenden. Infolgedessen versuchen Datenbanken, unterschiedliche Ansätze für unterschiedliche Datentypen zu verwenden. Zum Beispiel haben wir in MySQL oder MariaDB eine generische, leistungsstarke Lösung wie InnoDB, die in den meisten Fällen gut funktioniert, aber wir haben auch separate Funktionen, um mit JSON-Daten zu arbeiten, separate räumliche Indizes, um die Abfrage geometrischer Daten oder Volltext-Indizes zu beschleunigen , hilft bei Textdaten. In diesem Blog werfen wir einen Blick darauf, wie MariaDB verwendet werden kann, um mit Volltextdaten zu arbeiten.

Reguläre B+Tree-Indizes in InnoDB können auch verwendet werden, um die Suche nach den Textdaten zu beschleunigen. Das Hauptproblem ist, dass sie aufgrund ihrer Struktur und Art nur bei der Suche nach den Präfixen ganz links helfen können. Es ist auch teuer, große Textmengen zu indizieren (was angesichts der Einschränkungen des Präfixes ganz links nicht wirklich sinnvoll ist). Wieso den? Schauen wir uns ein einfaches Beispiel an. Wir haben den folgenden Satz:

„Der schnelle braune Fuchs springt über den faulen Hund“

Unter Verwendung regulärer Indizes in InnoDB können wir den vollständigen Satz indizieren:

„Der schnelle braune Fuchs springt über den faulen Hund“

Der Punkt ist, dass wir bei der Suche nach diesen Daten das vollständige Präfix ganz links nachschlagen müssen. Also eine Abfrage wie:

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

Von diesem Index profitiert aber eine Abfrage wie:

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

Wird nicht. Es gibt keinen Eintrag im Index, der mit „schnell“ beginnt. Es gibt einen Eintrag im Index, der „quick“ enthält, aber mit „The“ beginnt, daher kann er nicht verwendet werden. Infolgedessen ist es praktisch unmöglich, Textdaten mithilfe von B+Tree-Indizes effizient abzufragen. Glücklicherweise haben sowohl MyISAM als auch InnoDB FULLTEXT-Indizes implementiert, die verwendet werden können, um tatsächlich mit Textdaten auf MariaDB zu arbeiten. Die Syntax ist etwas anders als bei normalen SELECTs, schauen wir uns an, was wir damit machen können. Als Daten haben wir eine zufällige Indexdatei aus dem Speicherauszug der Wikipedia-Datenbank verwendet. Die Datenstruktur ist wie folgt:

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

Als Ergebnis haben wir eine Tabelle mit zwei BIG INT-Spalten und einer VARCHAR erstellt.

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

Danach haben wir die Daten geladen:

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

Wir haben auch den FULLTEXT-Index erstellt. Wie Sie sehen können, ist die Syntax dafür ähnlich wie bei einem normalen Index, wir mussten nur die Informationen über den Indextyp übergeben, da er standardmäßig B+Tree ist. Dann waren wir bereit, einige Abfragen auszuführen.

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)

Wie Sie sehen können, ist die Syntax für SELECT etwas anders als wir es gewohnt sind. Für die Volltextsuche sollten Sie die Syntax MATCH() … AGAINST () verwenden, wobei Sie in MATCH() die Spalte oder Spalten übergeben, die Sie durchsuchen möchten, und in AGAINST() eine durch Kommas getrennte Liste von Schlüsselwörtern übergeben. Sie können der Ausgabe entnehmen, dass bei der Suche standardmäßig die Groß-/Kleinschreibung nicht beachtet wird und die gesamte Zeichenfolge durchsucht wird, nicht nur der Anfang, wie dies bei B+Tree-Indizes der Fall ist. Vergleichen wir, wie es aussehen würde, wenn wir der Spalte „c3“ einen normalen Index hinzufügen würden – FULLTEXT- und B+Tree-Indizes können problemlos in derselben Spalte koexistieren. Welche verwendet wird, wird anhand der SELECT-Syntax entschieden.

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

Nachdem der Index erstellt wurde, werfen wir einen Blick auf die Suchausgabe:

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)

Wie Sie sehen können, hat unsere Abfrage nur drei Zeilen zurückgegeben. Dies ist zu erwarten, da wir nach Zeilen suchen, die nur mit der Zeichenfolge „Starship“ beginnen.

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)

Wenn wir die EXPLAIN-Ausgabe überprüfen, können wir sehen, dass der Index verwendet wurde, um nach den Daten zu suchen. Aber was ist, wenn wir nach allen Zeilen suchen wollen, die die Zeichenfolge „Starship“ enthalten, egal ob sie am Anfang steht oder nicht. Wir müssen die folgende Abfrage schreiben:

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)

Die Ausgabe stimmt mit der Volltextsuche überein.

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)

Das EXPLAIN ist jedoch anders - wie Sie sehen können, verwendet es immer noch den Index, aber diesmal führt es einen vollständigen Index-Scan durch. Das ist möglich, da wir die vollständige c3-Spalte indiziert haben, sodass alle Daten im Index verfügbar sind. Der Index-Scan führt zu zufälligen Lesevorgängen aus der Tabelle, aber für eine so kleine Tabelle hat MariaDB entschieden, dass dies effizienter ist als das Lesen der gesamten Tabelle. Bitte beachten Sie die Ausführungszeit:0,084s für unser reguläres SELECT. Im Vergleich zur Volltextabfrage ist es schlecht:

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)

Wie Sie sehen können, dauerte die Ausführung einer Abfrage, die den FULLTEXT-Index verwendet, 0,001 Sekunden. Wir sprechen hier von Größenordnungsunterschieden.

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)

So sieht die EXPLAIN-Ausgabe für die Abfrage mit dem FULLTEXT-Index aus - diese Tatsache wird durch den Typ angegeben:fulltext.

Volltextabfragen haben auch einige andere Funktionen. Beispielsweise ist es möglich, Zeilen zurückzugeben, die für den Suchbegriff relevant sein könnten. MariaDB sucht nach Wörtern in der Nähe der Zeile, nach der Sie suchen, und führt dann auch eine Suche nach ihnen durch.

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)

In unserem Fall kann das Wort „Raumschiff“ mit Wörtern wie „Troopers“, „class“, „Star Trek“, „Hospital“ usw. in Verbindung gebracht werden. Um diese Funktion zu verwenden, sollten wir die Abfrage mit dem Modifikator „WITH QUERY EXPANSION“ ausführen:

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)

Die Ausgabe enthielt eine große Anzahl von Zeilen, aber dieses Beispiel reicht aus, um zu sehen, wie es funktioniert. Die Abfrage hat Zeilen zurückgegeben wie:

„Troopers Drum and Bugle Corps“

„Kampf gegen Android-Trooper“

Diese basieren auf der Suche nach dem Wort „Troopers“. Es hat auch Zeilen mit Strings zurückgegeben wie:

„Star Trek tos“

„Wer trauert um Adonais? (Star Trek)“

Welche natürlich auf der Suche nach dem Wort „Start Trek“ basieren.

Wenn Sie mehr Kontrolle über den Begriff benötigen, nach dem Sie suchen möchten, können Sie „IN BOOLEAN MODE“ verwenden. Es ermöglicht die Verwendung zusätzlicher Operatoren. Die vollständige Liste finden Sie in der Dokumentation, wir zeigen nur ein paar Beispiele.

Angenommen, wir möchten nicht nur nach dem Wort „Stern“, sondern auch nach anderen Wörtern suchen, die mit der Zeichenfolge „Stern“ beginnen:

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)

Wie Sie sehen können, haben wir in der Ausgabe Zeilen, die Zeichenfolgen wie „Stars“, „Seestern“ oder „Stärke“ enthalten.

Ein weiterer Anwendungsfall für den BOOLEAN-Modus. Angenommen, wir möchten nach Zeilen suchen, die für das Repräsentantenhaus in Pennsylvania relevant sind. Wenn wir eine regelmäßige Abfrage ausführen, erhalten wir Ergebnisse, die irgendwie mit diesen Zeichenfolgen zusammenhängen:

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)

Wie Sie sehen können, haben wir einige nützliche Daten gefunden, aber wir haben auch Daten gefunden, die für unsere Suche völlig irrelevant sind. Glücklicherweise können wir eine solche Abfrage verfeinern:

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)

Wie Sie sehen können, haben wir durch Hinzufügen des Operators „+“ deutlich gemacht, dass wir nur an der Ausgabe interessiert sind, wenn ein bestimmtes Wort vorhanden ist. Daher sind die Daten, die wir als Antwort erhalten haben, genau das, wonach wir gesucht haben.

Wir können auch Wörter von der Suche ausschließen. Nehmen wir an, wir suchen nach fliegenden Dingen, aber unsere Suchergebnisse sind mit verschiedenen fliegenden Tieren kontaminiert, an denen wir nicht interessiert sind. Wir können Füchse, Eichhörnchen und Frösche leicht loswerden:

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)

Das letzte Feature, das wir zeigen möchten, ist die Möglichkeit, nach dem genauen Zitat zu suchen:

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)

Wie Sie sehen, funktioniert die Volltextsuche in MariaDB recht gut, sie ist auch schneller und flexibler als die Suche mit B+Tree-Indizes. Bitte bedenken Sie jedoch, dass dies keinesfalls eine Möglichkeit ist, mit großen Datenmengen umzugehen - mit dem Datenwachstum wird die Machbarkeit dieser Lösung abnehmen. Für die kleinen Datensätze ist diese Lösung jedoch vollkommen gültig. Es kann Ihnen definitiv mehr Zeit verschaffen, um schließlich dedizierte Volltextsuchlösungen wie Sphinx oder Lucene zu implementieren. Natürlich sind alle beschriebenen Funktionen in MariaDB-Clustern verfügbar, die von ClusterControl bereitgestellt werden.