I database hanno lo scopo di archiviare ed eseguire query sui dati in modo efficiente. Il problema è che ci sono molti tipi diversi di dati che possiamo memorizzare:numeri, stringhe, JSON, dati geometrici. I database utilizzano metodi diversi per archiviare diversi tipi di dati:struttura delle tabelle, indici. Non sempre lo stesso modo di archiviare e interrogare i dati è efficiente per tutti i suoi tipi, rendendo piuttosto difficile l'utilizzo di una soluzione universale. Di conseguenza, i database tentano di utilizzare approcci diversi per tipi di dati diversi. Ad esempio, in MySQL o MariaDB abbiamo una soluzione generica e performante come InnoDB, che funziona bene nella maggior parte dei casi, ma abbiamo anche funzioni separate per lavorare con i dati JSON, indici spaziali separati per velocizzare l'interrogazione di dati geometrici o indici fulltext , aiutando con i dati di testo. In questo blog, daremo un'occhiata a come MariaDB può essere utilizzata per lavorare con dati full-text.
Gli indici B+Tree regolari in InnoDB possono essere utilizzati anche per velocizzare le ricerche dei dati di testo. Il problema principale è che, a causa della loro struttura e natura, possono solo aiutare nella ricerca dei prefissi più a sinistra. È anche costoso indicizzare grandi volumi di testo (che, date le limitazioni del prefisso più a sinistra, non ha molto senso). Come mai? Diamo un'occhiata a un semplice esempio. Abbiamo la seguente frase:
“La veloce volpe bruna salta sopra il cane pigro”
Usando gli indici regolari in InnoDB possiamo indicizzare l'intera frase:
“La veloce volpe bruna salta sopra il cane pigro”
Il punto è che, quando cerchiamo questi dati, dobbiamo cercare il prefisso più a sinistra completo. Quindi una query come:
SELECT text FROM mytable WHERE sentence LIKE “The quick brown fox jumps”;
Trarrà vantaggio da questo indice ma una query come:
SELECT text FROM mytable WHERE sentence LIKE “quick brown fox jumps”;
Non lo farà. Non ci sono voci nell'indice che inizino da "veloce". C'è una voce nell'indice che contiene "quick" ma inizia da "The", quindi non può essere utilizzata. Di conseguenza, è praticamente impossibile interrogare in modo efficiente i dati di testo utilizzando gli indici B+Tree. Fortunatamente, sia MyISAM che InnoDB hanno implementato indici FULLTEXT, che possono essere utilizzati per lavorare effettivamente con dati di testo su MariaDB. La sintassi è leggermente diversa rispetto ai normali SELECT, diamo un'occhiata a cosa possiamo fare con loro. Per quanto riguarda i dati, abbiamo utilizzato un file di indice casuale dal dump del database di Wikipedia. La struttura dei dati è la seguente:
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
Di conseguenza, abbiamo creato una tabella con due colonne BIG INT e una VARCHAR.
MariaDB [(none)]> CREATE TABLE ft_data.ft_table (c1 BIGINT, c2 BIGINT, c3 VARCHAR, PRIMARY KEY (c1, c2);
Successivamente abbiamo caricato i dati:
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
Abbiamo anche creato l'indice FULLTEXT. Come puoi vedere, la sintassi è simile all'indice normale, abbiamo semplicemente dovuto passare le informazioni sul tipo di indice poiché è predefinito su B+Tree. Quindi eravamo pronti per eseguire alcune query.
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)
Come puoi vedere, la sintassi per SELECT è leggermente diversa da quella a cui siamo abituati. Per la ricerca fulltext dovresti usare la sintassi MATCH() … AGAINST(), dove in MATCH() passi la colonna o le colonne che vuoi cercare e in AGAINST() passi l'elenco di parole chiave delimitate da coma. Puoi vedere dall'output che per impostazione predefinita la ricerca non fa distinzione tra maiuscole e minuscole e cerca l'intera stringa, non solo l'inizio come accade con gli indici B+Tree. Confrontiamo come apparirà se aggiungessimo un indice normale alla colonna "c3":gli indici FULLTEXT e B+Tree possono coesistere sulla stessa colonna senza problemi. Quale verrebbe utilizzato viene deciso in base alla sintassi 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
Dopo aver creato l'indice, diamo un'occhiata all'output della ricerca:
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)
Come puoi vedere, la nostra query ha restituito solo tre righe. Questo è previsto poiché stiamo cercando righe che iniziano solo con una stringa "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)
Quando controlliamo l'output di EXPLAIN, possiamo vedere che l'indice è stato utilizzato per cercare i dati. Ma cosa succede se vogliamo cercare tutte le righe che contengono la stringa "Starship", indipendentemente dal fatto che sia all'inizio o meno. Dobbiamo scrivere la seguente query:
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)
L'output corrisponde a ciò che abbiamo ottenuto dalla ricerca fulltext.
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 è diverso però - come puoi vedere, usa ancora l'indice ma questa volta esegue una scansione completa dell'indice. Ciò è possibile poiché abbiamo indicizzato la colonna c3 completa in modo che tutti i dati siano disponibili nell'indice. La scansione dell'indice risulterà in letture casuali dalla tabella, ma per una tabella così piccola MariaDB ha deciso che è più efficiente della lettura dell'intera tabella. Si prega di notare il tempo di esecuzione:0,084 s per il nostro normale SELECT. Confrontandolo con la query fulltext, non è valido:
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)
Come puoi vedere, la query che utilizza l'indice FULLTEXT ha richiesto 0,001 secondi per essere eseguita. Stiamo parlando di differenze di ordini di grandezza.
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)
Ecco come appare l'output EXPLAIN per la query che utilizza l'indice FULLTEXT - questo fatto è indicato dal tipo:fulltext.
Le query fulltext hanno anche altre funzionalità. È possibile, ad esempio, restituire righe che potrebbero essere rilevanti per il termine di ricerca. MariaDB cerca le parole che si trovano vicino alla riga che stai cercando e quindi esegue una ricerca anche per loro.
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)
Nel nostro caso, la parola 'Starship' può essere correlata a parole come 'Troopers', 'class', 'Star Trek', 'Hospital' ecc. Per utilizzare questa funzione dovremmo eseguire la query con il modificatore "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)
L'output conteneva un numero elevato di righe, ma questo esempio è sufficiente per vedere come funziona. La query ha restituito righe come:
"Troopers Drum and Bugle Corps"
"Combatti truppe Android"
Questi si basano sulla ricerca della parola "Troopers". Ha anche restituito righe con stringhe come:
"Star Trek tos"
“Chi piange per Adonais? (Star Trek)”
Che, ovviamente, si basano sulla ricerca della parola "Start Trek".
Se hai bisogno di un maggiore controllo sul termine che desideri cercare, puoi utilizzare "IN MODALITÀ BOOLEANA". Consente di utilizzare operatori aggiuntivi. L'elenco completo è nella documentazione, mostreremo solo un paio di esempi.
Supponiamo di voler cercare non solo la parola "Stella" ma anche altre parole che iniziano con la stringa "Stella":
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)
Come puoi vedere, nell'output abbiamo righe che contengono stringhe come "Stars", "Starfish" o "amido".
Un altro caso d'uso per la modalità BOOLEAN. Supponiamo di voler cercare righe rilevanti per la Camera dei rappresentanti in Pennsylvania. Se eseguiremo query regolari, otterremo risultati in qualche modo correlati a una di queste stringhe:
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)
Come puoi vedere, abbiamo trovato alcuni dati utili ma abbiamo anche trovato dati che non sono assolutamente rilevanti per la nostra ricerca. Fortunatamente, possiamo perfezionare tale query:
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)
Come puoi vedere, aggiungendo l'operatore '+' abbiamo chiarito che siamo interessati solo all'output in cui esiste una determinata parola. Di conseguenza, i dati che abbiamo ricevuto in risposta sono esattamente quello che stavamo cercando.
Possiamo anche escludere parole dalla ricerca. Diciamo che stiamo cercando oggetti volanti ma i nostri risultati di ricerca sono contaminati da diversi animali volanti che non ci interessano. Possiamo sbarazzarci facilmente di volpi, scoiattoli e rane:
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)
L'ultima caratteristica che vorremmo mostrare è la possibilità di cercare la citazione esatta:
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)
Come puoi vedere, la ricerca fulltext in MariaDB funziona abbastanza bene, è anche più veloce e flessibile della ricerca che utilizza gli indici B+Tree. Tieni presente, tuttavia, che questo non è affatto un modo per gestire grandi volumi di dati:con la crescita dei dati, la fattibilità di questa soluzione si ridurrà. Tuttavia, per i piccoli set di dati questa soluzione è perfettamente valida. Può sicuramente farti guadagnare più tempo per, eventualmente, implementare soluzioni di ricerca full-text dedicate come Sphinx o Lucene. Naturalmente, tutte le funzionalità che abbiamo descritto sono disponibili nei cluster MariaDB distribuiti da ClusterControl.