MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

MySQL全文索引限制与替代方案

2021-11-157.8k 阅读

MySQL 全文索引限制

存储引擎限制

MySQL 中并非所有存储引擎都支持全文索引。最常用的 InnoDB 和 MyISAM 存储引擎对全文索引的支持有所不同。MyISAM 存储引擎从 MySQL 早期版本就开始支持全文索引,并且在文本搜索方面有不错的性能表现。然而,InnoDB 存储引擎虽然在 MySQL 5.6 版本之后对全文索引的支持得到了显著提升,但在一些复杂场景下,与 MyISAM 仍存在差异。

例如,在某些老版本的 MySQL 中,如果使用其他存储引擎如 MEMORY 引擎,是不支持全文索引的。这就限制了在内存表场景下使用全文索引进行高效文本检索的可能性。如果项目需求是基于内存表实现快速的数据读写和检索,而又需要文本搜索功能,就会面临困境。

-- 创建一个 MEMORY 引擎的表
CREATE TABLE memory_table (
    id INT,
    content VARCHAR(255)
) ENGINE = MEMORY;

-- 尝试为该表创建全文索引,会报错
ALTER TABLE memory_table ADD FULLTEXT(content);

上述代码中,当试图为 MEMORY 引擎的表 memory_tablecontent 字段添加全文索引时,MySQL 会抛出错误,提示该存储引擎不支持全文索引。

字符集限制

MySQL 的全文索引对字符集有一定要求。一些字符集可能不完全支持全文索引的某些特性,或者在特定字符集下全文索引的性能会受到影响。例如,在一些多字节字符集如 UTF - 8MB4 下,由于其存储和处理方式的复杂性,全文索引的构建和查询性能可能不如单字节字符集如 latin1。

另外,某些特殊字符集对于分词和索引构建有不同的规则。如果应用程序需要处理多种语言的文本,并且这些语言对应的字符集对全文索引支持不一致,就需要谨慎选择字符集。比如,当处理包含日语、韩语等亚洲语言的文本时,不同字符集下的全文索引效果可能差异很大。

-- 创建一个使用 UTF - 8MB4 字符集的表
CREATE TABLE utf8mb4_table (
    id INT,
    content VARCHAR(255)
) CHARACTER SET = utf8mb4;

-- 为该表添加全文索引
ALTER TABLE utf8mb4_table ADD FULLTEXT(content);

-- 对比在 latin1 字符集下的类似操作
CREATE TABLE latin1_table (
    id INT,
    content VARCHAR(255)
) CHARACTER SET = latin1;

ALTER TABLE latin1_table ADD FULLTEXT(content);

通过上述代码创建两个不同字符集的表并添加全文索引,在实际查询性能测试中,可能会发现 latin1 字符集下的查询速度在某些简单文本查询场景下更快,但在处理多字节字符文本时,UTF - 8MB4 字符集更具优势。

索引字段类型限制

MySQL 全文索引对字段类型有严格限制。只有 CHARVARCHARTEXT 类型的字段可以被添加全文索引。如果使用其他数据类型,如 INTDATE 等,是无法创建全文索引的。这限制了在某些需要对多种数据类型联合进行高效检索场景下的应用。

例如,假设一个电商数据库中有一个商品表,表中除了商品描述(适合使用全文索引)外,还有商品的库存数量(INT 类型)和上架日期(DATE 类型)。如果想要通过商品描述、库存数量和上架日期进行综合检索,单纯依靠全文索引就无法满足需求。

-- 创建一个包含多种数据类型的表
CREATE TABLE product (
    id INT,
    product_desc VARCHAR(255),
    stock INT,
   上架_date DATE
);

-- 尝试为 stock 字段添加全文索引,会报错
ALTER TABLE product ADD FULLTEXT(stock);

上述代码中,试图为 stock 字段(INT 类型)添加全文索引时,MySQL 会抛出错误,表明不支持对该类型字段创建全文索引。

索引长度限制

MySQL 全文索引存在索引长度限制。在 InnoDB 存储引擎中,单个索引的最大长度为 3072 字节,在 MyISAM 存储引擎中,单个索引的最大长度为 1000 字节。这对于一些包含长文本的字段来说是一个较大的限制。

例如,如果要对一篇长文章(如新闻报道、技术文档等)进行全文索引,很可能超过这个长度限制。即使将长文本拆分成多个较短的字段,也可能因为拆分后字段之间的关联性问题,影响全文索引的查询效果。

-- 创建一个试图存储长文本的表
CREATE TABLE long_text_table (
    id INT,
    long_content TEXT
);

-- 假设 long_content 中的文本长度超过 3072 字节(InnoDB 限制),添加全文索引可能会遇到问题
ALTER TABLE long_text_table ADD FULLTEXT(long_content);

long_content 字段中的文本长度超过限制时,添加全文索引可能会导致警告甚至错误,并且在查询时可能无法得到预期的结果。

查询语法限制

MySQL 全文索引的查询语法相对复杂,并且有一定限制。在使用 MATCH AGAINST 语法进行查询时,语法结构较为固定,与传统的 LIKE 查询相比,不够灵活。

例如,MATCH AGAINST 语法在处理多词查询时,对词序有一定要求。如果查询的词序与索引中的词序不一致,可能会影响查询结果的准确性。而且,MATCH AGAINST 语法在处理模糊查询时,不像 LIKE 那样可以通过通配符灵活地指定匹配模式。

-- 创建一个文章表
CREATE TABLE articles (
    id INT,
    title VARCHAR(255),
    content TEXT,
    FULLTEXT(content)
);

-- 使用 MATCH AGAINST 进行查询
SELECT * FROM articles WHERE MATCH(content) AGAINST('关键词' IN NATURAL LANGUAGE MODE);

-- 与 LIKE 查询对比,LIKE 更灵活但性能可能较差
SELECT * FROM articles WHERE content LIKE '%关键词%';

在上述代码中,MATCH AGAINST 虽然性能较好,但在词序和模糊匹配灵活性上不如 LIKE。如果需要更灵活的模糊查询,MATCH AGAINST 的语法限制就会成为障碍。

数据更新性能限制

当对已建立全文索引的表进行数据更新操作时,性能会受到较大影响。每次插入、更新或删除操作,MySQL 不仅要更新数据本身,还要更新全文索引结构。由于全文索引的构建和维护相对复杂,这会导致数据更新操作的时间成本增加。

例如,在一个新闻发布系统中,如果频繁地对新闻文章进行修改(包括标题和内容),每次修改都需要更新全文索引,这会导致系统响应时间变长,影响用户体验。在高并发的更新场景下,这种性能问题会更加突出。

-- 假设已经有一个包含全文索引的新闻表 news
-- 插入一条新新闻
INSERT INTO news (title, content) VALUES ('新新闻标题', '新新闻内容');

-- 更新一条新闻的内容
UPDATE news SET content = '修改后的新闻内容' WHERE id = 1;

-- 删除一条新闻
DELETE FROM news WHERE id = 2;

上述插入、更新和删除操作都会触发全文索引的更新,随着数据量的增加和更新频率的提高,性能问题会逐渐显现。

替代方案

使用第三方搜索引擎

Elasticsearch

Elasticsearch 是一个分布式、高可扩展的全文搜索引擎,基于 Lucene 构建。它提供了丰富的查询语法和强大的文本分析功能,能够很好地弥补 MySQL 全文索引的不足。

安装与配置:首先需要安装 Elasticsearch。可以从 Elasticsearch 官方网站下载适合操作系统的安装包。安装完成后,需要对其进行配置,主要是修改 config/elasticsearch.yml 文件,设置集群名称、节点名称、网络绑定地址等参数。

# 集群名称
cluster.name: my - elastic - cluster
# 节点名称
node.name: node - 1
# 绑定网络地址
network.host: 0.0.0.0
# 对外服务端口
http.port: 9200

数据同步:要将 MySQL 中的数据同步到 Elasticsearch,可以使用 Logstash 或者 Elasticsearch - MySQL - Connector 等工具。以 Elasticsearch - MySQL - Connector 为例,它可以定时从 MySQL 数据库中读取数据,并将其索引到 Elasticsearch 中。

// 使用 Elasticsearch - MySQL - Connector 进行数据同步的示例代码(Java)
import org.elasticsearch.client.transport.TransportClient;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.transport.TransportAddress;
import org.elasticsearch.transport.client.PreBuiltTransportClient;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.Statement;
import java.net.InetAddress;

public class MySQLToElasticsearchSync {
    public static void main(String[] args) {
        try {
            // Elasticsearch 客户端设置
            Settings settings = Settings.builder()
                  .put("cluster.name", "my - elastic - cluster")
                  .build();
            TransportClient client = new PreBuiltTransportClient(settings)
                  .addTransportAddress(new TransportAddress(InetAddress.getByName("localhost"), 9300));

            // MySQL 数据库连接
            Connection conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/mydb", "root", "password");
            Statement stmt = conn.createStatement();
            ResultSet rs = stmt.executeQuery("SELECT * FROM articles");

            while (rs.next()) {
                int id = rs.getInt("id");
                String title = rs.getString("title");
                String content = rs.getString("content");

                // 将数据索引到 Elasticsearch
                client.prepareIndex("articles", "article", String.valueOf(id))
                      .setSource("title", title, "content", content)
                      .execute()
                      .actionGet();
            }

            rs.close();
            stmt.close();
            conn.close();
            client.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

查询示例:在 Elasticsearch 中进行查询非常灵活。可以使用 RESTful API 或者各种客户端库进行查询。以下是一个使用 Java 客户端进行复杂查询的示例,查询包含特定关键词且发布时间在某个范围内的文章。

import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.client.transport.TransportClient;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.transport.TransportAddress;
import org.elasticsearch.index.query.BoolQueryBuilder;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.search.SearchHit;
import org.elasticsearch.transport.client.PreBuiltTransportClient;
import java.net.InetAddress;

public class ElasticsearchQueryExample {
    public static void main(String[] args) {
        try {
            // Elasticsearch 客户端设置
            Settings settings = Settings.builder()
                  .put("cluster.name", "my - elastic - cluster")
                  .build();
            TransportClient client = new PreBuiltTransportClient(settings)
                  .addTransportAddress(new TransportAddress(InetAddress.getByName("localhost"), 9300));

            // 构建查询条件
            BoolQueryBuilder queryBuilder = QueryBuilders.boolQuery()
                  .must(QueryBuilders.matchQuery("content", "关键词"))
                  .filter(QueryBuilders.rangeQuery("publish_date").gte("2023 - 01 - 01").lte("2023 - 12 - 31"));

            // 执行查询
            SearchResponse response = client.prepareSearch("articles")
                  .setQuery(queryBuilder)
                  .execute()
                  .actionGet();

            for (SearchHit hit : response.getHits().getHits()) {
                System.out.println(hit.getSourceAsString());
            }

            client.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

Solr

Solr 也是基于 Lucene 的开源企业级搜索平台。它具有良好的可扩展性和高性能,支持丰富的查询语法和文本分析功能。

安装与配置:从 Solr 官方网站下载安装包并解压。进入 Solr 安装目录,通过命令行启动 Solr 服务。然后需要创建一个新的核心(Core),并配置其 schema.xml 和 solrconfig.xml 文件。

<!-- schema.xml 示例配置,定义字段类型和字段 -->
<schema name="example" version="1.6">
    <fieldType name="text_general" class="solr.TextField" positionIncrementGap="100">
        <analyzer type="index">
            <tokenizer class="solr.StandardTokenizerFactory"/>
            <filter class="solr.StopFilterFactory" ignoreCase="true" words="stopwords.txt"/>
            <filter class="solr.LowerCaseFilterFactory"/>
        </analyzer>
        <analyzer type="query">
            <tokenizer class="solr.StandardTokenizerFactory"/>
            <filter class="solr.StopFilterFactory" ignoreCase="true" words="stopwords.txt"/>
            <filter class="solr.SynonymFilterFactory" synonyms="synonyms.txt" ignoreCase="true" expand="true"/>
            <filter class="solr.LowerCaseFilterFactory"/>
        </analyzer>
    </fieldType>
    <field name="id" type="string" indexed="true" stored="true" required="true"/>
    <field name="title" type="text_general" indexed="true" stored="true"/>
    <field name="content" type="text_general" indexed="true" stored="true"/>
    <uniqueKey>id</uniqueKey>
</schema>
<!-- solrconfig.xml 示例配置,定义查询处理器等 -->
<config>
    <luceneMatchVersion>8.11.1</luceneMatchVersion>
    <query>
        <maxBooleanClauses>1024</maxBooleanClauses>
    </query>
    <requestHandler name="/select" class="solr.SearchHandler">
        <lst name="defaults">
            <str name="echoParams">explicit</str>
            <int name="rows">10</int>
        </lst>
    </requestHandler>
</config>

数据同步:可以使用 DataImportHandler 来实现 MySQL 数据到 Solr 的同步。在 solrconfig.xml 中配置 DataImportHandler,并编写一个 data-config.xml 文件来定义数据源和同步规则。

<!-- data - config.xml 示例 -->
<dataConfig>
    <dataSource type="JdbcDataSource" driver="com.mysql.jdbc.Driver" url="jdbc:mysql://localhost:3306/mydb" user="root" password="password"/>
    <document>
        <entity name="article" query="SELECT * FROM articles">
            <field column="id" name="id"/>
            <field column="title" name="title"/>
            <field column="content" name="content"/>
        </entity>
    </document>
</dataConfig>

查询示例:Solr 支持通过 HTTP GET 或 POST 请求进行查询。以下是一个通过 HTTP GET 请求查询包含特定关键词的文章的示例。

http://localhost:8983/solr/mycore/select?q=content:关键词&wt=json&indent=true

通过上述 URL 可以在浏览器中发起查询请求,Solr 会返回符合条件的文章数据,以 JSON 格式展示,并且会进行缩进格式化,方便查看。

自定义分词与索引

基于 MySQL 自定义分词

虽然 MySQL 自身的全文索引分词功能有限,但可以通过自定义函数和存储过程来实现更灵活的分词。例如,可以编写一个自定义函数,将文本按照特定规则进行分词,并将分词结果存储在新的表中,然后基于这些分词结果构建普通索引。

-- 创建一个自定义分词函数
DELIMITER //
CREATE FUNCTION custom_tokenize(text VARCHAR(255)) RETURNS VARCHAR(255)
DETERMINISTIC
BEGIN
    DECLARE result VARCHAR(255);
    DECLARE pos INT;
    SET result = '';
    SET pos = 1;
    WHILE LENGTH(text) > 0 DO
        SET @token = SUBSTRING_INDEX(text, ' ', 1);
        SET result = CONCAT(result, @token,'');
        SET text = SUBSTRING(text, LENGTH(@token) + 2);
    END WHILE;
    RETURN result;
END //
DELIMITER ;

-- 创建一个存储分词结果的表
CREATE TABLE tokens (
    id INT AUTO_INCREMENT PRIMARY KEY,
    token VARCHAR(255),
    article_id INT
);

-- 创建一个文章表
CREATE TABLE articles (
    id INT AUTO_INCREMENT PRIMARY KEY,
    title VARCHAR(255),
    content VARCHAR(255)
);

-- 插入文章数据
INSERT INTO articles (title, content) VALUES ('示例文章标题', '这是一篇示例文章内容');

-- 将文章内容分词并插入到 tokens 表
INSERT INTO tokens (token, article_id)
SELECT custom_tokenize(content), id FROM articles;

-- 为 tokens 表的 token 字段添加普通索引
ALTER TABLE tokens ADD INDEX idx_token(token);

-- 通过分词结果进行查询
SELECT * FROM articles
JOIN tokens ON articles.id = tokens.article_id
WHERE tokens.token = '示例';

上述代码中,首先创建了一个自定义分词函数 custom_tokenize,将输入文本按空格分词。然后创建了 tokens 表用于存储分词结果,articles 表用于存储文章。插入文章数据后,通过函数将文章内容分词并插入到 tokens 表,最后为 tokens 表的 token 字段添加普通索引,以便进行查询。

使用第三方分词库

除了基于 MySQL 自定义分词,还可以使用第三方分词库,如结巴分词(用于中文)、Snowball 分词(支持多种语言)等。以结巴分词为例,在 Python 环境下,可以先安装结巴分词库,然后通过 Python 脚本将文本分词,并将分词结果存储到 MySQL 数据库中。

import pymysql
import jieba

# 连接 MySQL 数据库
conn = pymysql.connect(host='localhost', user='root', password='password', database='mydb')
cursor = conn.cursor()

# 读取文章内容
cursor.execute("SELECT id, content FROM articles")
articles = cursor.fetchall()

for article in articles:
    article_id = article[0]
    content = article[1]
    # 使用结巴分词进行分词
    words = jieba.lcut(content)
    for word in words:
        # 将分词结果插入到 tokens 表
        cursor.execute("INSERT INTO tokens (token, article_id) VALUES (%s, %s)", (word, article_id))

conn.commit()
cursor.close()
conn.close()

在上述 Python 代码中,首先连接到 MySQL 数据库,读取 articles 表中的文章内容。然后使用结巴分词对文章内容进行分词,并将每个分词结果插入到 tokens 表中。这样就可以基于这些分词结果在 MySQL 中进行更灵活的文本检索。

其他优化与替代思路

分块处理与局部索引

当面对长文本字段超过索引长度限制的情况,可以采用分块处理的方式。将长文本拆分成多个较小的块,然后对每个块建立局部索引。例如,对于一篇很长的技术文档,可以按段落或者固定字数将其拆分成多个部分,为每个部分创建一个独立的索引。

-- 创建一个存储长文本分块的表
CREATE TABLE long_text_chunks (
    id INT AUTO_INCREMENT PRIMARY KEY,
    article_id INT,
    chunk_number INT,
    chunk_text VARCHAR(255),
    FULLTEXT(chunk_text)
);

-- 假设已经有一个 articles 表存储完整文章
-- 将文章按固定字数(如 200 字)拆分成块并插入到 long_text_chunks 表
DELIMITER //
CREATE PROCEDURE split_long_text(IN article_id INT, IN long_text TEXT)
BEGIN
    DECLARE chunk_number INT DEFAULT 1;
    DECLARE start_pos INT DEFAULT 1;
    DECLARE end_pos INT;
    WHILE start_pos <= LENGTH(long_text) DO
        SET end_pos = LEAST(start_pos + 200, LENGTH(long_text) + 1);
        INSERT INTO long_text_chunks (article_id, chunk_number, chunk_text)
        VALUES (article_id, chunk_number, SUBSTRING(long_text, start_pos, end_pos - start_pos));
        SET start_pos = end_pos + 1;
        SET chunk_number = chunk_number + 1;
    END WHILE;
END //
DELIMITER ;

-- 调用存储过程拆分文章
CALL split_long_text(1, '很长的文章内容');

-- 查询包含特定关键词的文章块
SELECT * FROM long_text_chunks
WHERE MATCH(chunk_text) AGAINST('关键词' IN NATURAL LANGUAGE MODE);

上述代码创建了一个 long_text_chunks 表用于存储长文本的分块,并编写了一个存储过程 split_long_text 将长文本按固定字数拆分成块插入到该表。每个块都建立了全文索引,查询时可以通过 MATCH AGAINST 语法查询包含特定关键词的文章块。

结合缓存技术

在应用程序层面结合缓存技术,可以减轻数据库的压力,提高查询性能。例如,可以使用 Redis 作为缓存,将频繁查询的文本搜索结果缓存起来。当有新的查询请求时,首先检查缓存中是否有相应结果,如果有则直接返回,避免对数据库进行重复查询。

import redis
import pymysql

# 连接 Redis
redis_client = redis.StrictRedis(host='localhost', port=6379, db=0)

# 连接 MySQL 数据库
conn = pymysql.connect(host='localhost', user='root', password='password', database='mydb')
cursor = conn.cursor()

def search_articles(keyword):
    # 尝试从 Redis 缓存中获取结果
    result = redis_client.get(keyword)
    if result:
        return result.decode('utf - 8')

    # 如果缓存中没有,从 MySQL 数据库查询
    cursor.execute("SELECT * FROM articles WHERE MATCH(content) AGAINST(%s IN NATURAL LANGUAGE MODE)", (keyword,))
    articles = cursor.fetchall()
    result = str(articles)

    # 将查询结果存入 Redis 缓存
    redis_client.set(keyword, result)

    return result

# 调用搜索函数
print(search_articles('关键词'))

cursor.close()
conn.close()

在上述 Python 代码中,首先连接到 Redis 和 MySQL 数据库。定义了一个 search_articles 函数,该函数在接收到关键词查询请求时,先从 Redis 缓存中查找结果。如果缓存中不存在,则从 MySQL 数据库查询,并将查询结果存入 Redis 缓存,以便下次查询时直接使用。这样可以有效提高查询性能,减轻数据库压力。

通过以上对 MySQL 全文索引限制的分析以及各种替代方案的介绍,可以根据具体的业务需求和场景,选择合适的方法来优化文本检索功能,提高系统的性能和可用性。无论是使用第三方搜索引擎,还是自定义分词与索引,或者结合其他优化思路,都需要综合考虑开发成本、维护成本以及性能提升效果等因素。