Databaser er beregnet til effektivt at gemme og forespørge data. Problemet er, at der er mange forskellige typer data, vi kan gemme:tal, strenge, JSON, geometriske data. Databaser bruger forskellige metoder til at gemme forskellige typer data - tabelstruktur, indekser. Ikke altid den samme måde at gemme og forespørge på data er effektiv for alle dens typer, hvilket gør det ret svært at bruge en-fits-all-løsning. Som et resultat forsøger databaser at bruge forskellige tilgange til forskellige datatyper. For eksempel har vi i MySQL eller MariaDB en generisk, velfungerende løsning som InnoDB, som fungerer fint i de fleste tilfælde, men vi har også separate funktioner til at arbejde med JSON-data, separate rumlige indekser for at fremskynde forespørgsler til geometriske data eller fuldtekstindekser , hjælper med tekstdata. I denne blog vil vi tage et kig på, hvordan MariaDB kan bruges til at arbejde med fuldtekstdata.
Almindelige B+Tree-indekser i InnoDB kan også bruges til at fremskynde søgninger efter tekstdataene. Hovedproblemet er, at de på grund af deres struktur og natur kun kan hjælpe med at søge efter præfikserne længst til venstre. Det er også dyrt at indeksere store mængder tekst (hvilket i betragtning af begrænsningerne for præfikset længst til venstre ikke rigtig giver mening). Hvorfor? Lad os tage et kig på et simpelt eksempel. Vi har følgende sætning:
"Den hurtige brune ræv hopper over den dovne hund"
Ved at bruge almindelige indekser i InnoDB kan vi indeksere hele sætningen:
"Den hurtige brune ræv hopper over den dovne hund"
Pointen er, at når vi leder efter disse data, skal vi slå det fulde præfiks længst til venstre op. Så en forespørgsel som:
SELECT text FROM mytable WHERE sentence LIKE “The quick brown fox jumps”;
Vil drage fordel af dette indeks, men en forespørgsel som:
SELECT text FROM mytable WHERE sentence LIKE “quick brown fox jumps”;
Vil ikke. Der er ingen post i indekset, der starter fra 'hurtig'. Der er en post i indekset, der indeholder 'hurtig', men starter fra 'The', så den kan ikke bruges. Som et resultat er det praktisk talt umuligt effektivt at forespørge tekstdata ved hjælp af B+Tree-indekser. Heldigvis har både MyISAM og InnoDB implementeret FULLTEXT-indekser, som kan bruges til rent faktisk at arbejde med tekstdata på MariaDB. Syntaksen er lidt anderledes end med almindelige SELECT'er, lad os tage et kig på, hvad vi kan gøre med dem. Hvad angår data, brugte vi tilfældig indeksfil fra dumpet af Wikipedia-databasen. Datastrukturen er som nedenfor:
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 et resultat oprettede vi tabel med to BIG INT-kolonner og en VARCHAR.
MariaDB [(none)]> CREATE TABLE ft_data.ft_table (c1 BIGINT, c2 BIGINT, c3 VARCHAR, PRIMARY KEY (c1, c2);
Bagefter indlæste vi dataene:
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 har også oprettet FULLTEXT-indekset. Som du kan se, ligner syntaksen for det almindelige indeks, vi skulle bare videregive oplysningerne om indekstypen, da den er standard til B+Tree. Så var vi klar til at køre nogle forespørgsler.
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, er syntaksen for SELECT en smule anderledes, end vi er vant til. Til fuldtekstsøgning skal du bruge MATCH() … AGAINST () syntaks, hvor du i MATCH() sender den eller de kolonner, du vil søge på, og i AGAINST() passerer du en koma-separeret liste med søgeord. Du kan se fra outputtet, at søgning som standard er ufølsom mellem store og små bogstaver, og den søger i hele strengen, ikke kun begyndelsen, som den er med B+Tree-indekser. Lad os sammenligne, hvordan det vil se ud, hvis vi ville tilføje normalt indeks på 'c3'-kolonnen - FULLTEXT og B+Tree-indekser kan sameksistere i den samme kolonne uden problemer. Hvilken der skal bruges afgøres baseret på SELECT-syntaksen.
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
Efter at indekset er blevet oprettet, lad os tage et kig på søgeresultatet:
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, returnerede vores forespørgsel kun tre rækker. Dette forventes, da vi leder efter rækker, der kun starter med en streng '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 tjekker EXPLAIN-outputtet, kan vi se, at indekset er blevet brugt til at søge efter dataene. Men hvad nu hvis vi vil lede efter alle de rækker, der indeholder strengen 'Starship', uanset om den er i begyndelsen eller ej. Vi skal skrive følgende forespørgsel:
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)
Outputtet matcher det, vi fik fra fuldtekstsøgningen.
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 er dog anderledes - som du kan se, bruger den stadig indeks, men denne gang laver den en fuld indeksscanning. Det er muligt, da vi indekserede hele c3-kolonnen, så alle data er tilgængelige i indekset. Indeksscanning vil resultere i tilfældige læsninger fra tabellen, men for en sådan lille tabel besluttede MariaDB, at det var mere effektivt end at læse hele tabellen. Bemærk venligst udførelsestiden:0,084s for vores almindelige SELECT. Sammenligner man dette med fuldtekstforespørgsel, er det dårligt:
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 forespørgsler, der bruger FULLTEXT-indeks, 0,001s at udføre. Vi taler her om størrelsesordensforskelle.
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ådan ser EXPLAIN-outputtet ud for forespørgslen ved hjælp af FULLTEXT-indekset - det faktum er angivet med typen:fuldtekst.
Fuldtekstforespørgsler har også nogle andre funktioner. Det er for eksempel muligt at returnere rækker, som kan være relevante for søgeordet. MariaDB leder efter ord, der er placeret i nærheden af den række, du søger efter, og kører derefter en søgning også 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 vores tilfælde kan ordet 'Starship' være relateret til ord som 'Troopers', 'class', 'Star Trek', 'Hospital' osv. For at bruge denne funktion bør vi køre forespørgslen med "WITH QUERY EXPANSION" modifier:
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)
Outputtet indeholdt et stort antal rækker, men denne prøve er nok til at se, hvordan den fungerer. Forespørgslen returnerede rækker som:
"Troopers Drum and Bugle Corps"
"Kamp mod Android Troopers"
De er baseret på søgningen efter ordet 'troopers'. Det returnerede også rækker med strenge som:
"Star trek tos"
"Hvem sørger over Adonais? (Star Trek)"
Som naturligvis er baseret på opslag for ordet 'Start Trek'.
Hvis du har brug for mere kontrol over det udtryk, du vil søge efter, kan du bruge "I BOOLEAN MODE". Det giver mulighed for at bruge yderligere operatører. Den fulde liste er i dokumentationen, vi viser kun et par eksempler.
Lad os sige, at vi ikke kun vil søge efter ordet 'stjerne', men også efter andre ord, der starter med strengen 'stjerne':
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 i outputtet rækker, der indeholder strenge som 'Stars', 'Starfish' eller 'stivelse'.
Endnu et tilfælde for BOOLEAN-tilstand. Lad os sige, at vi ønsker at søge efter rækker, der er relevante for Repræsentanternes Hus i Pennsylvania. Hvis vi vil køre almindelig forespørgsel, vil vi få resultater, der på en eller anden måde er relateret til en af disse strenge:
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, fandt vi nogle nyttige data, men vi fandt også data, som er fuldstændig ikke relevante for vores søgning. Heldigvis kan vi forfine en sådan forespørgsel:
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, ved at tilføje '+'-operator gjorde vi det klart, at vi kun er interesserede i outputtet, hvor et givet ord findes. Som et resultat er de data, vi fik som svar, præcis, hvad vi ledte efter.
Vi kan også udelukke ord fra søgningen. Lad os sige, at vi leder efter flyvende ting, men vores søgeresultater er forurenet af forskellige flyvende dyr, vi ikke er interesserede i. Vi kan nemt slippe af med ræve, egern og frøer:
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 sidste funktion, vi gerne vil vise, er muligheden for at søge efter det nøjagtige citat:
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, fungerer fuldtekstsøgning i MariaDB ret godt, den er også hurtigere og mere fleksibel end søgning ved hjælp af B+Tree indekser. Vær dog opmærksom på, at dette på ingen måde er en måde at håndtere store mængder data på - med datavæksten vil gennemførligheden af denne løsning reduceres. Alligevel er denne løsning helt gyldig for de små datasæt. Det kan helt sikkert give dig mere tid til i sidste ende at implementere dedikerede fuldtekstsøgeløsninger som Sphinx eller Lucene. Selvfølgelig er alle de funktioner, vi beskrev, tilgængelige i MariaDB-klynger implementeret fra ClusterControl.