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

相关推荐
code_slave(码畜)12 小时前
微服务架构落地:基础服务 —— 报表服务(中篇:元数据、查询引擎、缓存与权限落地)
spring boot·spring cloud·缓存·微服务·架构
code_slave(码畜)13 小时前
微服务架构落地:基础服务 —— 报表服务(AI 集成篇:AI 增强报表能力)
人工智能·spring boot·spring cloud·微服务·架构
Henry-SAP14 小时前
SAP MRP失效根源业务角度解析
人工智能·云原生·sap·erp
code_slave(码畜)14 小时前
微服务架构落地:公共中间件层总览——不承载业务,只承载稳定性
spring boot·spring cloud·微服务·中间件·架构
江湖有缘19 小时前
3款开源绘图工具整理合集,可Docker一键部署!
docker·容器·开源
nhdh21 小时前
Higress:AI时代云原生网关新选择
人工智能·云原生
code_slave(码畜)1 天前
微服务架构落地:基础服务 —— 报表服务(上篇:定位、边界与整体架构)
java·spring boot·spring cloud·微服务·架构
小匠石钧知1 天前
06_在k8s集群中安装MetalLB实现LoadBalancer
云原生·容器·kubernetes·service·loadbalancer·metallb
troy1282 天前
阿里云 vs 谷歌云:Kubernetes 部署深度对比分析
阿里云·kubernetes·云计算
谢亮_vipxieliang2 天前
容器排障实战手册:从日志到内核的分层排障
docker·云原生·容器·eureka