全文搜索的数据库选择: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在不同规模下的选型决策。

相关推荐
时间售卖铺2 小时前
MySQL 日志管理
运维·数据库·mysql·云原生
老郭a2 小时前
流量一涨 Pod 扩不出来?K8s HPA 弹性伸缩配置避坑指南
云原生
程序员清风3 小时前
LibreChat 实战:用 Docker 搭建可扩展的多模型对话平台
运维·docker·容器
Web3&Basketball4 小时前
从0到1落地AI Agent沙箱隔离Docker实战:踩坑全记录
运维·docker·容器·agent·容器安全·沙箱隔离
张彦峰ZYF4 小时前
拆得开不等于该拆开:微服务与模块化单体的边界判据、规模阈值、团队适配与一致性代价
后端·微服务·边界是否稳定·扩展天花板是否到顶·有无小组能独立值班·独立部署的自由会否被用上·这一刀是否切断不可降级的不变量
xianyuCcCcCCCcc4 小时前
Kubernetes Service 与网络代理
网络·云原生·kubernetes
bluebonnet274 小时前
【docker】使用regctl免安装docker下载镜像
运维·docker·容器
我不会起名字3225 小时前
PV/PVC 一直 Pending:StorageClass 与绑定条件的 4 个检查点
云原生·kubernetes·pvc·排查
孔令飞18 小时前
工具分享:OneX AI 工具安装、配置及功能介绍
云原生·go·ai编程
程序猿阿越20 小时前
从 StatefulSet 到容器挂载:CSI 源码走读
后端·kubernetes·源码阅读