sql >> Databasteknik >  >> RDS >> MariaDB

En introduktion till fulltextsökning i MariaDB

Databaser är avsedda att effektivt lagra och söka efter data. Problemet är att det finns många olika typer av data vi kan lagra:siffror, strängar, JSON, geometriska data. Databaser använder olika metoder för att lagra olika typer av data - tabellstruktur, index. Inte alltid samma sätt att lagra och fråga data är effektivt för alla dess typer, vilket gör det ganska svårt att använda en lösning som passar alla. Som ett resultat försöker databaser använda olika tillvägagångssätt för olika datatyper. Till exempel, i MySQL eller MariaDB har vi en generisk, välpresterande lösning som InnoDB, som fungerar bra i de flesta fall, men vi har också separata funktioner för att arbeta med JSON-data, separata rumsliga index för att påskynda sökning av geometriska data eller fulltextindex , hjälpa till med textdata. I den här bloggen ska vi ta en titt på hur MariaDB kan användas för att arbeta med fulltextdata.

Vanliga B+Tree-index i InnoDB kan också användas för att påskynda sökningar efter textdata. Huvudfrågan är att de, på grund av sin struktur och karaktär, bara kan hjälpa till med att söka efter prefixen längst till vänster. Det är också dyrt att indexera stora volymer text (vilket, med tanke på begränsningarna för prefixet längst till vänster, inte riktigt är vettigt). Varför? Låt oss ta en titt på ett enkelt exempel. Vi har följande mening:

"Den kvicka bruna räven hoppar över den lata hunden"

Genom att använda vanliga index i InnoDB kan vi indexera hela meningen:

"Den kvicka bruna räven hoppar över den lata hunden"

Poängen är att när vi letar efter dessa data måste vi slå upp hela prefixet längst till vänster. Så en fråga som:

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

Kommer att dra nytta av detta index men en fråga som:

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

Ska inte. Det finns ingen post i indexet som börjar från "snabb". Det finns en post i indexet som innehåller "snabb" men börjar från "The", så den kan inte användas. Som ett resultat är det praktiskt taget omöjligt att effektivt söka efter textdata med B+Tree-index. Lyckligtvis har både MyISAM och InnoDB implementerat FULLTEXT-index, som kan användas för att faktiskt arbeta med textdata på MariaDB. Syntaxen är något annorlunda än med vanliga SELECT, låt oss ta en titt på vad vi kan göra med dem. När det gäller data använde vi en slumpmässig indexfil från Wikipedia-databasens dump. Datastrukturen är enligt nedan:

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

Som ett resultat skapade vi en tabell med två BIG INT-kolumner och en VARCHAR.

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

Efteråt laddade vi in ​​data:

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

Vi skapade också FULLTEXT-indexet. Som du kan se liknar syntaxen för det vanligt index, vi var bara tvungna att skicka informationen om indextypen eftersom den är standard på B+Tree. Sedan var vi redo att köra några frågor.

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)

Som du kan se är syntaxen för SELECT något annorlunda än vad vi är vana vid. För fulltextsökning bör du använda MATCH() … AGAINST () syntax, där du i MATCH() skickar kolumnen eller kolumnerna du vill söka och i AGAINST() passerar du komaavgränsad lista med nyckelord. Du kan se från resultatet att sökning som standard är skiftlägesokänslig och den söker igenom hela strängen, inte bara början som den är med B+Tree-index. Låt oss jämföra hur det kommer att se ut om vi skulle lägga till normalt index i kolumnen 'c3' - FULLTEXT och B+Tree-index kan samexistera på samma kolumn utan problem. Vilken som ska användas avgörs baserat på SELECT-syntaxen.

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

När indexet har skapats, låt oss ta en titt på sökresultatet:

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)

Som du kan se returnerade vår fråga bara tre rader. Detta förväntas eftersom vi letar efter rader som bara börjar med en sträng '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)

När vi kontrollerar EXPLAIN-utgången kan vi se att indexet har använts för att söka efter data. Men tänk om vi vill leta efter alla rader som innehåller strängen "Starship", oavsett om det är i början eller inte. Vi måste skriva följande fråga:

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)

Resultatet matchar det vi fick från fulltextsökningen.

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 är dock annorlunda - som du kan se använder den fortfarande index men den här gången gör den en fullständig indexskanning. Det är möjligt eftersom vi indexerade hela c3-kolumnen så att all data är tillgänglig i indexet. Indexskanning kommer att resultera i slumpmässiga läsningar från tabellen men för en så liten tabell bestämde MariaDB att det var mer effektivt än att läsa hela tabellen. Observera exekveringstiden:0,084s för vår vanliga SELECT. Om man jämför detta med en fulltextfråga är det dåligt:

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)

Som du kan se tog en fråga som använder FULLTEXT-index 0,001s att köra. Vi talar här om skillnader i storleksordningar.

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)

Så här ser EXPLAIN-utgången ut för frågan med FULLTEXT-index - det faktumet indikeras av typen:fulltext.

Fulltextfrågor har också några andra funktioner. Det är till exempel möjligt att returnera rader som kan vara relevanta för söktermen. MariaDB letar efter ord som ligger nära raden som du söker efter och gör sedan en sökning även efter dem.

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)

I vårt fall kan ordet 'Starship' relateras till ord som 'Troopers', 'class', 'Star Trek', 'Sjukhus' etc. För att använda den här funktionen bör vi köra frågan med "WITH QUERY EXPANSION" modifierare:

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)

Utdatan innehöll ett stort antal rader men detta prov är tillräckligt för att se hur det fungerar. Frågan returnerade rader som:

"Trupparnas trumma och trollkorps"

"Battle Android Troopers"

De är baserade på sökningen efter ordet "Troopers". Den returnerade också rader med strängar som:

"Star trek tos"

"Vem sörjer för Adonais? (Star Trek)”

Som uppenbarligen är baserade på sökningen efter ordet "Start Trek".

Om du skulle behöva mer kontroll över termen du vill söka efter kan du använda "I BOOLENSK LÄGE". Det gör det möjligt att använda ytterligare operatörer. Hela listan finns i dokumentationen, vi visar bara ett par exempel.

Låt oss säga att vi inte bara vill söka efter ordet "stjärna" utan också efter andra ord som börjar med strängen "stjärna":

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)

Som du kan se har vi rader i utgången som innehåller strängar som "Stars", "Starfish" eller "stärkelse".

Ett annat användningsfall för BOOLEAN-läget. Låt oss säga att vi vill söka efter rader som är relevanta för representanthuset i Pennsylvania. Om vi ​​kör en vanlig fråga kommer vi att få resultat som på något sätt är relaterade till någon av dessa strängar:

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)

Som du kan se hittade vi en del användbar data men vi hittade också data som inte är helt relevanta för vår sökning. Lyckligtvis kan vi förfina en sådan fråga:

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)

Som du kan se, genom att lägga till operatorn "+" gjorde vi det klart att vi bara är intresserade av utdata där ett givet ord finns. Som ett resultat är den information vi fick som svar precis vad vi letade efter.

Vi kan också utesluta ord från sökningen. Låt oss säga att vi letar efter flygande saker men våra sökresultat är förorenade av olika flygande djur som vi inte är intresserade av. Vi kan lätt bli av med rävar, ekorrar och grodor:

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)

Den sista egenskapen vi skulle vilja visa är möjligheten att söka efter det exakta citatet:

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)

Som du kan se fungerar fulltextsökning i MariaDB ganska bra, det är också snabbare och mer flexibelt än att söka med B+Tree-index. Kom dock ihåg att detta inte på något sätt är ett sätt att hantera stora datamängder - med datatillväxten kommer genomförbarheten av denna lösning att minska. Ändå är denna lösning helt giltig för de små datamängderna. Det kan definitivt ge dig mer tid för att så småningom implementera dedikerade fulltextsöklösningar som Sphinx eller Lucene. Naturligtvis är alla funktioner vi beskrev tillgängliga i MariaDB-kluster som distribueras från ClusterControl.


  1. BESTÄLL AV med inre fråga, vilket ger ORA-00907 saknad höger parentes

  2. DATEADD() Exempel i SQL Server

  3. De 10 bästa anledningarna till att använda Access och Excel tillsammans

  4. Hur anropar man oracles lagrad procedur som inkluderar användardefinierad typ i java?