データベースは、データを効率的に保存およびクエリすることを目的としています。問題は、保存できるデータには、数値、文字列、JSON、幾何学的データなど、さまざまな種類があることです。データベースは、さまざまな方法を使用して、さまざまなタイプのデータ(テーブル構造、インデックス)を格納します。データの保存とクエリの同じ方法がすべてのタイプで効率的であるとは限らないため、万能のソリューションを使用することは非常に困難です。その結果、データベースはデータ型ごとに異なるアプローチを使用しようとします。たとえば、MySQLまたはMariaDBには、ほとんどの場合に正常に機能するInnoDBのような汎用的でパフォーマンスの高いソリューションがありますが、JSONデータを処理するための個別の関数、幾何学的データまたは全文インデックスのクエリを高速化するための個別の空間インデックスもあります。 、テキストデータの支援。このブログでは、MariaDBを使用してフルテキストデータを処理する方法を見ていきます。
InnoDBの通常のB+Treeインデックスを使用して、テキストデータの検索を高速化することもできます。主な問題は、その構造と性質のために、左端のプレフィックスの検索にしか役立たないということです。また、大量のテキストのインデックスを作成するのにもコストがかかります(左端のプレフィックスの制限を考えると、実際には意味がありません)。なんで?簡単な例を見てみましょう。次の文があります:
「速い茶色のキツネが怠惰な犬を飛び越えます」
InnoDBの通常のインデックスを使用して、文全体にインデックスを付けることができます:
「速い茶色のキツネが怠惰な犬を飛び越えます」
重要なのは、このデータを探すときは、左端のプレフィックス全体を検索する必要があるということです。したがって、次のようなクエリ:
SELECT text FROM mytable WHERE sentence LIKE “The quick brown fox jumps”;
このインデックスの恩恵を受けますが、次のようなクエリがあります:
SELECT text FROM mytable WHERE sentence LIKE “quick brown fox jumps”;
しない。 「クイック」で始まるインデックスにはエントリがありません。インデックスに「quick」を含むが「The」で始まるエントリがあるため、使用できません。その結果、B+Treeインデックスを使用してテキストデータを効率的にクエリすることは事実上不可能です。幸い、MyISAMとInnoDBの両方にFULLTEXTインデックスが実装されており、これを使用して、MariaDB上のテキストデータを実際に処理できます。構文は通常のSELECTとは少し異なります。それらを使用して、何ができるかを見てみましょう。データについては、ウィキペディアデータベースのダンプからランダムインデックスファイルを使用しました。データ構造は次のとおりです。
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
その結果、2つのBIGINT列と1つのVARCHARを持つテーブルを作成しました。
MariaDB [(none)]> CREATE TABLE ft_data.ft_table (c1 BIGINT, c2 BIGINT, c3 VARCHAR, PRIMARY KEY (c1, c2);
その後、データをロードしました:
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
FULLTEXTインデックスも作成しました。ご覧のとおり、その構文は通常のインデックスに似ています。デフォルトではB + Treeであるため、インデックスタイプに関する情報を渡す必要がありました。これで、いくつかのクエリを実行する準備が整いました。
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)
ご覧のとおり、SELECTの構文は、これまでの構文とは少し異なります。全文検索の場合は、MATCH()…AGAINST()構文を使用する必要があります。ここで、MATCH()では検索する1つまたは複数の列を渡し、AGAINST()ではコマ区切りのキーワードリストを渡します。出力から、デフォルトで検索では大文字と小文字が区別されず、B + Treeインデックスの場合のように最初だけでなく、文字列全体が検索されることがわかります。 「c3」列に通常のインデックスを追加した場合の外観を比較してみましょう。FULLTEXTインデックスとB+Treeインデックスは同じ列に問題なく共存できます。どちらを使用するかは、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
インデックスが作成されたら、検索出力を見てみましょう。
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)
ご覧のとおり、クエリは3行しか返しませんでした。これは、文字列「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)
EXPLAIN出力を確認すると、データの検索にインデックスが使用されていることがわかります。しかし、文字列「Starship」を含むすべての行を検索する場合は、先頭にあるかどうかに関係なくどうなりますか。次のクエリを作成する必要があります:
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)
出力は、全文検索から得られたものと一致します。
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は異なりますが、ご覧のとおり、まだインデックスを使用していますが、今回は完全なインデックススキャンを実行します。これは、完全なc3列にインデックスを付けて、すべてのデータがインデックスで利用できるようにすることで可能になります。インデックススキャンの結果、テーブルからランダムに読み取られますが、このような小さなテーブルの場合、MariaDBはテーブル全体を読み取るよりも効率的であると判断しました。実行時間に注意してください:通常のSELECTの場合は0.084秒。これを全文クエリと比較すると、悪いことです:
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)
ご覧のとおり、FULLTEXTインデックスを使用するクエリの実行には0.001秒かかりました。ここでは、桁違いの違いについて話しています。
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)
FULLTEXTインデックスを使用したクエリのEXPLAIN出力は次のようになります。その事実はタイプ:fulltextで示されます。
全文クエリには他にもいくつかの機能があります。たとえば、検索語に関連する可能性のある行を返すことができます。 MariaDBは、検索する行の近くにある単語を検索してから、それらの単語も検索します。
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)
この場合、「Starship」という単語は「Troopers」、「class」、「Star Trek」、「Hospital」などの単語に関連付けることができます。この機能を使用するには、「WITHQUERYEXPANSION」修飾子を使用してクエリを実行する必要があります。
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)
出力には多数の行が含まれていましたが、このサンプルはそれがどのように機能するかを確認するのに十分です。クエリは次のような行を返しました:
「TroopersDrumandBugleCorps」
「バトルAndroidトルーパー」
これらは、「Troopers」という単語の検索に基づいています。また、次のような文字列を含む行を返しました:
「スタートレクトス」
「神との対決は誰ですか? (スタートレック)」
これは明らかに、「StartTrek」という単語の検索に基づいています。
検索する用語をさらに制御する必要がある場合は、「ブールモード」を使用できます。追加の演算子を使用できます。完全なリストはドキュメントにあります。いくつかの例を示します。
「スター」という単語だけでなく、「スター」という文字列で始まる他の単語も検索するとします。
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)
ご覧のとおり、出力には「Stars」、「Starfish」、「starch」などの文字列を含む行があります。
BOOLEANモードの別のユースケース。ペンシルベニア州の衆議院に関連する行を検索するとします。通常のクエリを実行すると、これらの文字列のいずれかに何らかの形で関連する結果が得られます。
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)
ご覧のとおり、有用なデータがいくつか見つかりましたが、検索にまったく関係のないデータも見つかりました。幸いなことに、このようなクエリを絞り込むことができます:
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)
ご覧のとおり、「+」演算子を追加することで、特定の単語が存在する出力にのみ関心があることを明確にしました。結果として、私たちが応答して得たデータは、まさに私たちが探していたものです。
検索から単語を除外することもできます。飛んでいるものを探しているが、検索結果が興味のないさまざまな飛んでいる動物に汚染されているとします。キツネ、リス、カエルを簡単に駆除できます。
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)
表示したい最後の機能は、正確な見積もりを検索する機能です:
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)
ご覧のとおり、MariaDBでの全文検索は非常にうまく機能し、B+Treeインデックスを使用した検索よりも高速で柔軟性があります。ただし、これは大量のデータを処理する方法ではないことに注意してください。データが増えると、このソリューションの実現可能性は低下します。それでも、小さなデータセットの場合、このソリューションは完全に有効です。最終的には、SphinxやLuceneなどの専用の全文検索ソリューションを実装するための時間を増やすことができます。もちろん、ここで説明したすべての機能は、ClusterControlからデプロイされたMariaDBクラスターで利用できます。