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

相关推荐
mohesashou1 小时前
红帽linux的K8S部署
linux·kubernetes·ansible
CJY6214 小时前
k8s集群部署的方法原理
云原生·容器·kubernetes
JavaPub-rodert4 小时前
使用 Docker 部署 Ollama:Docker Compose 一键运行 DeepSeek-R1 1.5B
运维·docker·容器
ZY小袁5 小时前
K8S集群部署(脚本方法)
云原生·容器·kubernetes
FoldWinCard5 小时前
K8s集群部署的方法原理
大数据·容器·kubernetes
二宝哥8 小时前
docker安装apisix
docker·容器·apisix
lisin-lee-cooper9 小时前
简单聊聊 OpenFeign 框架源码
java·微服务
Kina_C9 小时前
从零搭建Kubernetes集群:Harbor私有仓库 + K8s v1.35 + Flannel网络插件完整部署指南
网络·容器·kubernetes
王da魔10 小时前
部署Kubernetes
云原生·容器·kubernetes
RobinDevNotes10 小时前
K8s+Megatron-LM+vLLM打通超大模型训练全链路
人工智能·kubernetes