全文搜索的数据库选择:MySQL Fulltext与Elasticsearch的深度对比

全文搜索的数据库选择:MySQL Fulltext与Elasticsearch的深度对比

一、当"MySQL LIKE"成为全文搜索的唯一工具

某内容平台的搜索功能一直用的是LIKE '%keyword%',在文章数突破10万后,每次搜索耗时3-5秒。运维用MySQL 5.7的Fulltext Index重构了搜索功能,搜索耗时降到了200ms。但新问题接踵而至:中文分词不准("人工智能"被拆成"人工"和"智能")、相关性排序不符合预期(10年前的文章排在第一页)、无法做同义词扩展(搜"AI"不应该漏掉"人工智能"的文章)。

平台最终评估后决定迁移到Elasticsearch,但迁移过程本身是一个6个月的项目。在这个过程中,一个核心问题浮现:哪些场景MySQL Fulltext就够了?哪些必须上ES?

二、两种搜索引擎的底层差异

三、MySQL Fulltext的适用场景与配置

对于中小型内容平台(文章<100万、查询QPS<100),MySQL Fulltext是一个合格的方案:

sql 复制代码
-- 创建全文索引(ngram分词器处理中文)
ALTER TABLE articles 
ADD FULLTEXT INDEX ft_content (title, content) 
WITH PARSER ngram;

-- 配置ngram参数
SET GLOBAL ngram_token_size = 2;

-- 搜索
SELECT 
    article_id, title,
    MATCH(title, content) AGAINST('分布式数据库 查询优化' IN BOOLEAN MODE) AS relevance
FROM articles
WHERE MATCH(title, content) AGAINST('分布式数据库 查询优化' IN BOOLEAN MODE)
ORDER BY relevance DESC
LIMIT 20;

但Fulltext的边界在于:

python 复制代码
class MySQLFulltextSearch:
    def __init__(self, mysql_pool):
        self.mysql = mysql_pool
    
    def search(self, keyword: str, page: int = 1, size: int = 20):
        """MySQL全文搜索(带降级方案)"""
        try:
            conn = self.mysql.get_connection()
            cursor = conn.cursor(dictionary=True)
            
            # 处理特殊字符(防止语法错误)
            keyword = self._sanitize_keyword(keyword)
            
            sql = """
                SELECT article_id, title, 
                       LEFT(content, 200) AS snippet,
                       MATCH(title, content) AGAINST(%s IN BOOLEAN MODE) AS score
                FROM articles
                WHERE MATCH(title, content) AGAINST(%s IN BOOLEAN MODE)
                ORDER BY score DESC
                LIMIT %s OFFSET %s
            """
            cursor.execute(sql, (keyword, keyword, size, (page-1)*size))
            results = cursor.fetchall()
            
            # 检查结果质量:如果相关度太低,降级到LIKE
            if results and results[0]['score'] < 0.01:
                return self._fallback_like_search(keyword, page, size)
            
            return results
            
        except pymysql.OperationalError as e:
            if '1191' in str(e):  # Fulltext index not found
                return self._fallback_like_search(keyword, page, size)
            raise SearchException(f"搜索失败: {e}")
        finally:
            if conn:
                conn.close()
    
    def _sanitize_keyword(self, keyword: str) -> str:
        """清理搜索关键词"""
        # 移除Fulltext特殊字符
        import re
        keyword = re.sub(r'[+\-><\(\)~*\"@]', ' ', keyword)
        # 添加布尔模式操作符
        words = keyword.split()
        return ' +'.join([''] + words)
    
    def _fallback_like_search(self, keyword, page, size):
        """Fulltext不可用时的LIKE降级"""
        conn = self.mysql.get_connection()
        try:
            cursor = conn.cursor(dictionary=True)
            sql = """
                SELECT article_id, title, LEFT(content, 200) AS snippet
                FROM articles
                WHERE title LIKE %s OR content LIKE %s
                LIMIT %s OFFSET %s
            """
            like_kw = f'%{keyword}%'
            cursor.execute(sql, (like_kw, like_kw, size, (page-1)*size))
            return cursor.fetchall()
        finally:
            conn.close()

四、何时必须升级到Elasticsearch:决策矩阵

维度 MySQL Fulltext OK 必须上ES
数据量 <100万文档 >100万文档
搜索QPS <100 >100
中文分词精度 可接受ngram盲切 需要词典级分词
同义词 不需要 必须(AI=人工智能)
拼音搜索 不需要 必须
聚合统计 不需要 需要(按分类/时间聚合)
多字段权重 简单 复杂(标题>标签>正文)
搜索建议 不需要 需要(自动补全)
运维团队 无ES经验 有ES集群运维能力

ES的配置实现(Java客户端):

java 复制代码
public class ESSearchService {
    private final RestHighLevelClient client;
    
    public List<Article> search(String keyword, int page, int size) {
        try {
            SearchRequest request = new SearchRequest("articles");
            
            // 多字段匹配 + 权重
            QueryStringQueryBuilder queryBuilder = 
                QueryBuilders.queryStringQuery(keyword)
                    .field("title", 3.0f)      // 标题权重3
                    .field("tags", 2.0f)        // 标签权重2
                    .field("content", 1.0f);    // 内容权重1
            
            SearchSourceBuilder sourceBuilder = new SearchSourceBuilder()
                .query(queryBuilder)
                .from((page - 1) * size)
                .size(size)
                .highlight(new HighlightBuilder()
                    .field("content")
                    .fragmentSize(200)
                    .numOfFragments(1));
            
            request.source(sourceBuilder);
            SearchResponse response = client.search(request, RequestOptions.DEFAULT);
            
            return parseHits(response);
            
        } catch (ElasticsearchStatusException e) {
            if (e.status() == RestStatus.NOT_FOUND) {
                return Collections.emptyList();
            }
            throw new SearchException("ES搜索异常", e);
        } catch (IOException e) {
            // ES不可用时降级到MySQL
            return mysqlFallback.search(keyword, page, size);
        }
    }
}

五、总结

MySQL Fulltext和Elasticsearch不是竞争关系,而是数据量和技术栈的演进路径。初创内容平台在MySQL Fulltext上可以跑到100万文档、100QPS,这个阶段的维护成本为零。当搜索需求开始涉及中文语义、同义词、智能排序时,ES的代价(独立集群、调优、监控)才开始变得值得。

一条简单的决策线:如果你的搜索需求可以用布尔匹配模式描述("包含A且不包含B"),MySQL Fulltext足够。如果需要"用户搜AI时也能找到人工智能相关的文章",那就是ES的主场了。


本文属于「行业场景与项目复盘」系列,深入对比MySQL Fulltext与Elasticsearch在不同规模下的选型决策。

相关推荐
allnlei8 小时前
s6-overlay - 装在 Docker 容器里的轻量级管家
java·docker·容器
一技安身14 小时前
【Docker】 镜像导出 & 导入,实验环境aarch64(arm64)Linux操作系统
linux·docker·容器
鹤落晴春18 小时前
【K8s】从 ConfigMap 到 Argo CD:Kubernetes 配置管理链路
运维·云原生·容器·kubernetes
汇智信科19 小时前
基于 Jmis 框架的制度与流程管控系统设计与应用|项目全生命周期管理
大数据·人工智能·微服务·云原生·汇智信科
ezreal_pan19 小时前
Pod 重启多场景复盘:OOM、日志阻塞、并发 map 读写导致程序崩溃
docker·k8s
名字还没想好☜1 天前
Docker 镜像瘦身进阶:用 distroless/scratch 把 Go 服务打到 10MB,以及没 shell 怎么调试
运维·docker·容器·golang·kubernetes
JavaPub-rodert1 天前
Docker 深入理解:从容器原理到生产环境最佳实践
运维·docker·容器
陈聪.1 天前
Kubernetes 集群网络插件迁移:从 Flannel 到 Calico 实战总结
云原生·kubernetes
暖核1 天前
Docker 入门|吃透容器生态与底层原理
运维·docker·容器
滕州市燕猫虎计算机科技工作室个体工商户1 天前
Docker常用命令
运维·docker·容器