mysql - MySQL / MariaDB 全文搜索非常慢
问题描述
全文搜索需要几分钟而不是几秒钟。该表有 50K 行。每个 ocr_text 包含大量文本,所有文本来自 2 整页报纸。
MariaDB [scu_db]> describe frames;
+-------------+------------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+-------------+------------------+------+-----+---------+----------------+
| id | int(10) unsigned | NO | PRI | NULL | auto_increment |
| dbRollID | int(10) unsigned | NO | MUL | 0 | |
| frame_num | int(10) unsigned | NO | | 0 | |
| xLeadEdge | int(11) | NO | | 0 | |
| yTrailEdge | int(11) | NO | | 0 | |
| wLeftEdge | int(11) | NO | | 0 | |
| hRightEdge | int(11) | NO | | 0 | |
| ocr_text | mediumtext | NO | MUL | NULL | |
| ocr_rects | longblob | NO | | NULL | |
+-------------+------------------+------+-----+---------+----------------+
此外,一个词在数据中出现的频率越高,查询速度就越慢。
此查询需要 10 秒:
SELECT dbRollID
FROM frames
WHERE MATCH (ocr_text) AGAINST ('+1912' IN BOOLEAN MODE)
ORDER BY id
查询一个常用词需要 2.5 分钟:
SELECT dbRollID
FROM frames
WHERE MATCH (ocr_text) AGAINST ('+john' IN BOOLEAN MODE)
ORDER BY id
如果我添加LIMIT 50
(我认为应该这样做?),这没有什么区别。
这看起来慢得可怕。我在这里做错了什么?
SHOW CREATE TABLE
结果:
CREATE TABLE `frames` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`dbRollID` int(10) unsigned NOT NULL DEFAULT 0,
`frame_num` int(10) unsigned NOT NULL DEFAULT 0,
`xLeadEdge` int(11) NOT NULL DEFAULT 0,
`yTrailEdge` int(11) NOT NULL DEFAULT 0,
`wLeftEdge` int(11) NOT NULL DEFAULT 0,
`hRightEdge` int(11) NOT NULL DEFAULT 0,
`ocr_text` mediumtext COLLATE utf8mb4_unicode_ci NOT NULL,
`ocr_rects` longblob NOT NULL,
PRIMARY KEY (`id`),
KEY `fk_roll_id_1` (`dbRollID`),
FULLTEXT KEY `ocr_text` (`ocr_text`),
CONSTRAINT `fk_roll_id_1` FOREIGN KEY (`dbRollID`) REFERENCES `scansettings` (`dbRollID`) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB AUTO_INCREMENT=474139 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
explain SELECT dbRollID FROM frames WHERE MATCH (ocr_text) AGAINST ('+john' IN BOOLEAN MODE) ORDER BY id
结果:
+------+-------------+--------+----------+---------------+----------+---------+------+------+-----------------------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+------+-------------+--------+----------+---------------+----------+---------+------+------+-----------------------------+
| 1 | SIMPLE | frames | fulltext | ocr_text | ocr_text | 0 | | 1 | Using where; Using filesort |
+------+-------------+--------+----------+---------------+----------+---------+------+------+-----------------------------+
编辑/更新:
好的,我有一个假设,但它可能基于不正确的假设。
所以有人告诉我,通过在文本列上使用 FULLTEXT KEY,MySQL 会为该列创建一个索引,然后在您进行全文搜索时巧妙地使用该索引(即 innodb 引擎中的 MATCH AGAINST)。所以理论上我不应该过度考虑这个并创建我自己的索引系统。这是真的?
好的,假设这是真的,那么 MySQL 会以某种巧妙的方式创建这个词索引,因为实际上并没有那么多不同的词。正确的?
但。我的文字是 OCR 文字。这是非常糟糕的 OCR 文本。这是在数以百万计的图像上执行的非常糟糕的 OCR 文本,其中许多甚至不包含 OCRable 文本,而是草书书写。所以,我的文字中有数以亿计的垃圾词。也许它主要是垃圾词。我不知道。但这是否意味着 MySQL 建立的索引会很大呢?并且足够大以至于不能缓存在 innodb 的内存缓冲区中?
想法?这可能是真的吗?如果是这样,如果我可以清除文本中的所有垃圾,也许它会起作用?
SHOW TABLE STATUS LIKE "frames";
+--------+--------+---------+------------+-------+----------------+-------------+-----------------+--------------+-----------+----------------+---------------------+---------------------+------------+--------------------+----------+----------------+---------+------------------+-----------+
| Name | Engine | Version | Row_format | Rows | Avg_row_length | Data_length | Max_data_length | Index_length | Data_free | Auto_increment | Create_time | Update_time | Check_time | Collation | Checksum | Create_options | Comment | Max_index_length | Temporary |
+--------+--------+---------+------------+-------+----------------+-------------+-----------------+--------------+-----------+----------------+---------------------+---------------------+------------+--------------------+----------+----------------+---------+------------------+-----------+
| frames | InnoDB | 10 | Dynamic | 51419 | 80792 | 4154245120 | 0 | 3997696 | 7340032 | 474566 | 2021-08-17 13:26:24 | 2021-08-19 18:03:18 | NULL | utf8mb4_unicode_ci | NULL | | | 0 | N |
+--------+--------+---------+------------+-------+----------------+-------------+-----------------+--------------+-----------+----------------+---------------------+---------------------+------------+--------------------+----------+----------------+---------+------------------+-----------+
EXPLAIN SELECT on RDS(不同但相似的服务器,MySQL 而不是 MariaDB)
explain SELECT dbRollID FROM frames WHERE MATCH (ocr_text) AGAINST ('+john' IN BOOLEAN MODE) ORDER BY id;
+----+-------------+--------+------------+----------+---------------+----------+---------+-------+------+----------+---------------------------------------------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+--------+------------+----------+---------------+----------+---------+-------+------+----------+---------------------------------------------------+
| 1 | SIMPLE | frames | NULL | fulltext | ocr_text | ocr_text | 0 | const | 1 | 100.00 | Using where; Ft_hints: no_ranking; Using filesort |
+----+-------------+--------+------------+----------+---------------+----------+---------+-------+------+----------+---------------------------------------------------+
2021 年 8 月 27 日更新做更多实验:我不明白为什么限制查询的范围,无论是使用额外的 WHERE 条件,还是使用子查询,或者使用 LIMIT,对所花费的时间绝对没有影响:
SELECT dbRollID FROM frames
WHERE MATCH (ocr_text) AGAINST ('+john' IN BOOLEAN MODE)
ORDER BY dbRollID;
29219 rows in set (1 min 46.959 sec)
SELECT xLeadEdge FROM frames
WHERE dbRollID=110
AND MATCH (ocr_text) AGAINST ('+john' IN BOOLEAN MODE)
ORDER BY xLeadEdge;
340 rows in set (1 min 45.984 sec)
SELECT * FROM frames
WHERE dbRollID=110;
512 rows in set (0.272 sec)
SELECT xLeadEdge
FROM
(
SELECT * FROM frames WHERE dbRollID=110
) AS a
WHERE MATCH (ocr_text) AGAINST ('+john' IN BOOLEAN MODE)
ORDER BY xLeadEdge;
340 rows in set (1 min 47.044 sec)
SELECT dbRollID FROM frames
WHERE MATCH (ocr_text) AGAINST ('+john' IN BOOLEAN MODE)
ORDER BY dbRollID LIMIT 1;
1 row in set (1 min 46.575 sec)
SELECT xLeadEdge FROM frames
WHERE dbRollID=110 AND MATCH (ocr_text) AGAINST ('+john' IN BOOLEAN MODE)
ORDER BY xLeadEdge LIMIT 1;
1 row in set (1 min 46.939 sec)
(注意:1 分钟 45 而不是 3 分钟是因为我的 innodb 缓冲区设置比原始测试大得多。但是近 2 分钟的查询仍然很慢。)
解决方案
拥有更多 RAM 并增加 innodb_buffer_pool_size 会产生显着效果,具体取决于我的表的大小,并且有一些警告。
在我的本地测试服务器上,表中有一半的行,缓冲区设置为 2GB,会发生以下情况:
第一次搜索 +john 需要 2 分钟。连续搜索 +john 不到一秒钟。然后,搜索任何其他常用词只需要 10 到 15 秒。如果我重新启动服务器,第一次搜索总是需要 2 分钟。连续搜索很快。似乎索引正在被缓存。因此,当服务器重新启动时,第一次搜索总是很慢。我想知道,还有哪些其他条件会导致此缓存被刷新,并使搜索再次变慢。我是否只是告诉我的用户,“哦,你每天进行的第一次搜索会非常慢。等待它然后它就会起作用!” 编辑更新:在此找到另一个 SO q/a:mysql 在第一次查询时慢,然后对相关查询快 它被称为预热缓存!
此外,我在代码中添加了超时,因此使用它的网页不会被锁定。但我不能这样做,因为如果我在漫长的 2 分钟第一次搜索之前超时,缓存永远不会被填满(如果这实际上是这样的话)并且所有搜索都很慢,并且总是超时。
此外,如果我的表变得太大,相对于我的 RAM 和 innodb_buffer_pool_size,它就像一个阈值被跨越,并且所有常见的单词搜索开始需要几分钟,就像缓存太小一样。
所以也许答案,或者一个答案,是我只需要一个更强大的服务器,更多的内存。但我仍然认为这不应该是答案。特别是因为我的生产数据集会更大。
我注意到的另一个稍微相关的问题。如果我的 SELECT 请求更多列,尤其是更大的列,它会破坏这一切,并且真的会进一步减慢查询速度。因此,即使我不得不忍受这个,我也将不得不一次编写更智能的多个查询来获取数据。
更新:所以这里是我为缓解这个问题所做的所有事情的完整列表:
- 增加系统 RAM 和 innodb_buffer_pool_size 以将完整索引放入缓存
- “预热”缓存 - 编写代码以在服务器重新启动时对 +john 进行全文搜索
- 从数据中删除所有不超过 3 个字母的单词(也许这也有助于缓存)
- 修改任何可能进行子查询的查询
- 修改查询以在执行 MATCH AGAINST 时不选择任何其他列
- 修改查询以执行后续查询以获取附加列
- 为最坏的情况添加 MySQL 超时
推荐阅读
- java - 无法识别的选项 'bitmap1'。拆分参数列表时出错:找不到选项
- java - 如何立即执行 Java 程序,然后按照定义使用 Job 执行程序
- python - 为字典中的每个字典添加一个键,在一行中执行此操作
- android - WifiP2pManager 无法添加LocalService 并且发现服务在应用程序处于后台时返回 WifiP2pManager.Error
- javascript - onsubmit 在部分视图的表单标签中不起作用
- spring-boot - 在 Chrome 浏览器上运行 springBoot 应用程序时出现错误 404
- javascript - 纯 Javascript:单击计算器按钮组中的按钮的最佳方法是什么?
- django - Django:如何在 admin.py 中使用关键字 static
- javascript - Firestore 快照分离不起作用
- reactjs - 反应状态没有用 socket.io 更新